@heroiclands/package-build 20.4.0 → 20.5.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 +276 -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 +798 -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 +144 -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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,281 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 20.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cbe3266: **A reference for every subpath entry this package exports**
8
+
9
+ `docs/api.md` documents the whole programmatic surface — `engine`, `sohl`
10
+ and `hm3` included, on the same footing as the packaging half — organized by
11
+ how a consumer imports it: signature, what it returns, and when to reach
12
+ for it, with a runnable example per subpath. Most of it is pure — source
13
+ text or already-loaded data in, findings or values out, leaving discovery,
14
+ I/O and reporting to the caller — and the reference names the handful of
15
+ packaging functions that necessarily touch the filesystem or a subprocess.
16
+ - 8e2b5d3: **A key-by-key reference for `package-build.config.yaml`**
17
+
18
+ `docs/configuration.md` documents every top-level key, every nested key, its
19
+ type, whether it is required, its default, and the exact message an author
20
+ sees when a value is wrong — quoted from the validator, so the error a build
21
+ prints is searchable against the page that explains it.
22
+
23
+ It also documents the values a repository never writes: `rootDir` and
24
+ `foundryPackage` are derived from where the file sits and from the adjacent
25
+ `package.json`; `stats.systemVersion` is derived from `package.json` or from
26
+ `systems:`; `itemBuilders` accepts a registry name (`sohl`, `hm3`) in YAML,
27
+ resolved before validation. Authoring the first three directly is refused,
28
+ with the message that says so.
29
+
30
+ `packageBuild:` — the section `@heroiclands/package-build`'s own packaging
31
+ half reads — gets the same treatment: staging assets, the manifest
32
+ pass-through and the keys it derives and refuses to have overwritten, the
33
+ localization coverage settings, the container and end-to-end test
34
+ configuration.
35
+ - cc3d90b: **The content tree publishes as a book**
36
+
37
+ A third surface beside the compendium packs and the website: one searchable,
38
+ bookmarked PDF, built by `content-build pdf` and attached to a release by
39
+ `package-build release`.
40
+
41
+ **A book is a selection, not a rendering of everything.** The packs and the site
42
+ publish the whole tree; a book is a declared structure whose leaves pick notes
43
+ out of the corpus with a `WHERE` clause, interleaved with prose that need not be
44
+ in the tree at all. `pdf.document` names that structure.
45
+
46
+ ```yaml
47
+ pdf:
48
+ title: The Hârn Ensemble
49
+ document: book.yaml
50
+ fonts:
51
+ serif: Libertinus Serif
52
+ mono: DejaVu Sans Mono
53
+ ```
54
+
55
+ **`publish.site` decides whether one is built, and it is the only switch.**
56
+ `content` builds a book; `homepage` does not — the same fence that stops the
57
+ tree being walked for pages stops it being walked for a book, so the four
58
+ packages that publish only a homepage cannot start emitting a content document
59
+ because a `pdf:` block appeared. A package with no block, no tree, or no
60
+ compiler builds nothing, says why, and exits 0.
61
+
62
+ | Property | How it is got |
63
+ | ----------------------- | ------------------------------------------------------------------- |
64
+ | Searchable | Real text; every embedded font carries a `ToUnicode` map. |
65
+ | Bookmark outline | Every section and entry is a heading, so the sidebar is the way in. |
66
+ | Page-numbered contents | Shallower than the bookmarks — 2,500 entries would be 40 pages. |
67
+ | Repeating table headers | A property table spilling a page keeps its column names. |
68
+ | Internal references | A wikilink between two notes of the book resolves inside the PDF. |
69
+ | External references | A cross-package link, or a note not selected, stays a URL. |
70
+
71
+ **Typst is a binary, not a dependency.** It is found on `PATH` or named by
72
+ `pdf.binary`. Bundling a native compiler would put a platform-specific artefact
73
+ in the dependency tree of three repositories, only one of which is mostly a
74
+ book. A missing binary is a finding, and the `.typ` source is written anyway.
75
+
76
+ Proved against `harn-ensemble`: 2,517 entries, 1,270 pages, 10,421 outline
77
+ nodes, 34 seconds, no findings.
78
+
79
+ Additive — a package that configures no `pdf:` block is unaffected.
80
+ - c9dfa50: **A `doc` has two more genres to choose from**
81
+
82
+ `subType: howto` and `subType: concept` join `rules`, `userguide` and
83
+ `reference`:
84
+
85
+ - **`howto`** — a task with an outcome, written as the steps that reach it.
86
+ - **`concept`** — an explanation of how something works and why it is shaped
87
+ that way, read to understand rather than to follow.
88
+
89
+ Between them they cover the prose a package writes about itself, which had no
90
+ genre to declare and was left to a directory name to imply.
91
+
92
+ `subType` stays _a genre and only a genre_: it says what kind of page this is,
93
+ never who reads it. A page written for a developer is a `howto` or a `concept`
94
+ like any other, and the audience is the section it sits in.
95
+
96
+ Both spellings are one word, as `userguide` is: a `subType` is held to the
97
+ address charset, so `how-to` is refused for its hyphen.
98
+ - 00ce257: **A package that publishes documentation**
99
+
100
+ `packageKind` takes a third value, `documentation`: a package that publishes a
101
+ website and the book built from the same notes, and compiles no Foundry
102
+ documents at all. `systems` and `modules` say where Foundry installs a package;
103
+ `documentation` says it installs nowhere.
104
+
105
+ ```yaml
106
+ contentPackage: toolkit
107
+ packageKind: documentation
108
+
109
+ publish:
110
+ site: content
111
+ address: { prefix: guide/ }
112
+
113
+ site:
114
+ out: site/content
115
+ ```
116
+
117
+ `publish` is required, and `site: content` — publishing the tree is the whole of
118
+ what the kind does, and `content` is also what builds the book.
119
+
120
+ **What it refuses, by name and with a locator.** `packs`, `itemBuilders`,
121
+ `docs`, `compatibility`, `relationships`, `systems`, `requiresSystem`, `stats`
122
+ and `foundryPackage` each describe a Foundry package, so each fails at load
123
+ naming the key and the line and column it was written on, rather than being read
124
+ and ignored. `packs` is otherwise still required to declare at least one pack.
125
+
126
+ **What follows from the kind.**
127
+
128
+ - _No manifest, and no compile._ `package-build manifest` refuses rather than
129
+ writing a `module.json` for a package Foundry never installs. The compile
130
+ passes refuse too, rather than exiting 0 having compiled nothing.
131
+ - _No Foundry ids to author._ A note's id is derived from its address, and no
132
+ pass here asks for a pinned one.
133
+ - _`doc` and `homepage` are the whole vocabulary._ Every other note type exists
134
+ to become a Foundry document, so a note carrying one has no destination;
135
+ `content-build lint` reports it at its `type:` line.
136
+ - _No asset root._ `assetRoot` and `foundryPackage` resolve to nothing, so a
137
+ note's `img:` names the owning package (`systems/…`, `modules/…`) or a URL.
138
+ A path this package would have to serve itself is refused.
139
+ - _A content index all the same._ `content-build content-index` emits
140
+ `<contentPackage>-metadata.jsonl` as it does everywhere else, so another
141
+ package can resolve an address into this one.
142
+
143
+ Systems and modules are unchanged: the asset root, the `_stats` stamp and the
144
+ pack requirement all behave exactly as before.
145
+ - 46d6de3: **Getting started** — `docs/getting-started.md` walks an empty directory to a
146
+ package that builds: identity, configuration, a first note, the checks, the
147
+ compiled packs, the manifest, the content index and the release archive. Each
148
+ step says what it produces and how to tell it worked, with the output it
149
+ actually prints. It was written by walking it.
150
+
151
+ **Project setup** — `docs/project-setup.md` covers what a repository carries
152
+ beyond the build configuration: `package.json` and what each script in the chain
153
+ is _for_, the shared Prettier re-export and what `.prettierignore` is really
154
+ guarding, the packaged git hooks and their per-hook switches, `.changeset/` and
155
+ the four settings that are decisions rather than preferences, the label registry
156
+ pair, and the directory layout — what the build reads, and everything it writes
157
+ under `build/`.
158
+ - f9e0e27: **`content-build pdf --version` is now `--book-version`.**
159
+
160
+ `--version` collided with yargs' own reserved top-level `--version`, so the
161
+ option could not actually take a value — passing one failed with
162
+ `Unknown argument`. `--book-version` stamps the title page and the file name
163
+ exactly as before; the plain `content-build --version` answers this package's
164
+ own version, unaffected.
165
+
166
+ **A thrown error from `content-build pdf` no longer crashes with a
167
+ `ReferenceError`.** It now reports the same located diagnostic and non-zero
168
+ exit every other command's catch block produces.
169
+ - a6c77d2: **A guide to reading this toolchain's diagnostics**
170
+
171
+ `docs/diagnostics.md` explains the `file:line:column: severity: message` form
172
+ every warning and error carries: why a field is dropped rather than guessed,
173
+ why both severities print to stderr, how a configuration error is located,
174
+ and which commands fail a run on an error-severity finding. It closes with a
175
+ runnable example that parses a command's output programmatically.
176
+ - 51e334a: **A full command reference**
177
+
178
+ `docs/commands.md` documents every command both binaries expose — `package-build`
179
+ and `content-build`, 25 commands between them — with what each reads, what it
180
+ writes, its options and their defaults, its exit codes, and a worked example.
181
+
182
+ Several corners never had a home before this: `content-build pdf`, and the
183
+ options `--coverage`, `--doc`, `--fields`, `--id`, `--references`, `--registry`
184
+ and `--root`.
185
+
186
+ The document is checked against the actual `yargs` definitions in both
187
+ binaries, so an option or an action added to either one and left undocumented
188
+ fails the build.
189
+
190
+ ### Patch Changes
191
+
192
+ - 6e80fbd: **`content-config.mjs`'s own documentation now matches what `defineConfig` accepts**
193
+
194
+ The worked example at the top of `content-config.mjs` — and its `.mjs` twin in
195
+ `CONTENT.md` — authored `stats.systemId`, a key `defineConfig` refuses as
196
+ derived. Both now carry only what loads.
197
+
198
+ The `contentPackage` refusal, and the sibling refusal for a section's
199
+ `listType` / `listSubType`, named the wrong character class — `[A-Za-z0-9]`
200
+ when the enforced charset is lowercase only. Both now say `lowercase
201
+ alphanumeric` and print the pattern actually enforced, so an author who writes
202
+ `contentPackage: PackageBuild` is told what is wrong with it rather than sent
203
+ looking for a character they do not have.
204
+ - bd6c20b: **`docs/configuration.md` now matches `packageKind: documentation`**
205
+
206
+ The configuration reference names all three `packageKind` values —
207
+ `systems`, `modules` and `documentation` — in the summary table and in the
208
+ `packageKind` section itself, and states which keys a `documentation`
209
+ package requires, which it refuses (quoting each located refusal message
210
+ verbatim), and how `foundryPackage`, `assetRoot` and `stats` resolve for it.
211
+ - fb04049: **`content-build docs item-fields --check` reports a stale page in the located form**
212
+
213
+ The stale-page diagnostic now starts with the file's path, unprefixed by a
214
+ timestamp — matching every other located failure this command line emits, and
215
+ readable by the same tools that already parse the rest of them.
216
+ - 46b39ef: **The published types match the code they are generated from**
217
+
218
+ The `.d.mts` files ship with `@heroiclands/package-build` and are generated
219
+ from the JSDoc, so a block that is not the one belonging to a symbol ships as
220
+ that symbol's type. `packRelease` now declares the `pdf` option it accepts and
221
+ the `pdf`, `pdfFindings` and `pdfSkipped` fields it returns; `buildSite` and
222
+ `expandNoteTables` declare `sqlTables`; a compiler's `resolveEmbedded`
223
+ declares `modelPackage`; and `expandContentTables` declares the `warnings` it
224
+ returns and the `column` on every error entry. Type-checking the whole
225
+ published declaration surface is clean.
226
+
227
+ **Every exported symbol carries documentation of its own**
228
+
229
+ `NOTE_VOCABULARY`, `DECLARED_TAGS`, `buildIndexRecord`, `collectContentIndex`,
230
+ `expandContentTables`, `walkMarkdownTree`, `declaredPredecessors` and the seven
231
+ pack compiler classes each describe what they take and what they return, so
232
+ hovering one in an editor answers the question asked of it.
233
+ - de54d45: `docs/commands.md` reads as a manual page. Every command carries the same
234
+ labelled sections — **NAME**, **SYNOPSIS**, **DESCRIPTION**, **OPTIONS**,
235
+ **EXIT STATUS**, **EXAMPLES**, **SEE ALSO** — in the same order, so jumping to
236
+ the right command means jumping to the right label. **SEE ALSO** cross-links
237
+ the commands that answer a related question, and every command that emits
238
+ findings or reads a configuration key now points at _Diagnostics_ or
239
+ _Configuration_ directly.
240
+
241
+ `package-build lang check` / `coverage` / `hardcoded` and
242
+ `content-build content-format schema` / `fields` / `notes` each get their own
243
+ section, rather than sharing one.
244
+ - 40fcc1a: **A frontmatter reference resolves in any package the tree can reach.**
245
+
246
+ A `ref:` field — `parentSkillCode`, `assocSkillCode`, `assocAffiliationCode` —
247
+ holds a shortcode, not an address. The system persists it as written and looks
248
+ it up at runtime among the items embedded on one actor, and an actor assembled
249
+ from several packages carries their items side by side. So the reference check
250
+ now asks only whether _any_ reachable package declares the `type`/`shortcode`
251
+ pair: local notes first, then the fetched dependency indexes, with package and
252
+ system wildcarded.
253
+
254
+ This is the rule for a reference alone. A wikilink is unchanged — its target is
255
+ a document to point at, so an omitted package still means this one, and reaching
256
+ another package still requires the fully qualified form.
257
+
258
+ A tree whose references name a parent in a dependency saw every one of them
259
+ reported:
260
+
261
+ | note | `sohl.parentSkillCode` | before | after |
262
+ | ----------------------- | ---------------------- | ------------ | -------------------------- |
263
+ | a language skill | `lang` | no such note | resolves in the dependency |
264
+ | a spirit specialisation | `spirit` | no such note | resolves in the dependency |
265
+
266
+ A reference naming a shortcode no package declares is still an error, and the
267
+ value a note writes is still the value its document carries.
268
+
269
+ `buildLinkIndex` gains `referenceHit(target)`, which is what performs this
270
+ lookup.
271
+ - a331709: **The address-segment charset is documented as lowercase, matching what it enforces.**
272
+
273
+ Comments, JSDoc and the reference documentation described `contentPackage`,
274
+ `type`, `subType` and `shortcode` as `^[A-Za-z0-9]+$` or plainly "alphanumeric",
275
+ which reads as case-insensitive. The charset every one of them is held to is
276
+ lowercase-only, and the wording now says so — in prose and in the refusal
277
+ messages an author hits when a value breaks it.
278
+
3
279
  ## 20.4.0
4
280
 
5
281
  ### Minor Changes
package/CONTENT.md CHANGED
@@ -25,8 +25,10 @@ A consuming repository declares one `package-build.config.yaml` at its root:
25
25
  # every canonical address, the name of the content index it emits, and the
26
26
  # package a cross-package wikilink writes to reach one of its notes.
27
27
  contentPackage: thalorna
28
- # Where Foundry installs it: "systems" or "modules". Also decides the served
29
- # asset root a note's `img:` resolves to — `modules/sohl-thalorna/assets/…`.
28
+ # What kind of package this is. "systems" and "modules" are where Foundry
29
+ # installs it, and each decides the served asset root a note's `img:` resolves
30
+ # to — `modules/sohl-thalorna/assets/…`. "documentation" is a package Foundry
31
+ # never installs; see "A package that compiles nothing" below.
30
32
  packageKind: modules
31
33
 
32
34
  # The Foundry core range this package supports. `minimum` is stamped into every
@@ -161,13 +163,13 @@ packageBuild:
161
163
  assets:
162
164
  - { from: assets/icons, to: assets/icons }
163
165
 
164
- # Three independent switches every combination is real plus the address
165
- # scheme both `manifest` and `site` derive addresses under. `site` is a mode,
166
- # not a boolean: `homepage` (the default) publishes the authored homepage and
167
- # no other page; `content` publishes it plus every page the tree compiles to.
166
+ # How much of the package reaches the web, plus the address scheme both
167
+ # `manifest` and `site` derive addresses under. `site` is a mode, not a
168
+ # boolean: `homepage` (the default) publishes the authored homepage and no
169
+ # other page; `content` publishes it plus every page the tree compiles to, and
170
+ # is also what builds the book.
168
171
  publish:
169
172
  site: content
170
- manifests: { publish: true, consume: true }
171
173
  address:
172
174
  prefix: kb/
173
175
 
@@ -182,8 +184,8 @@ site:
182
184
 
183
185
  The loader validates the document, resolves every path against the directory
184
186
  the file sits in, fills the optional halves with their defaults
185
- (`skipDirectories: []`, `packageBuild: {}`, the conventional `paths`, both
186
- manifest switches off and `publish.site` at its `homepage` floor),
187
+ (`skipDirectories: []`, `packageBuild: {}`, the conventional `paths` and
188
+ `publish.site` at its `homepage` floor),
187
189
  derives `assetRoot`, `packDirectories`, `itemTypes` and `docEntryTypes`, and
188
190
  freezes the result. A malformed configuration throws a `TypeError` naming the
189
191
  offending field — and the line and column it was written on, in the
@@ -212,6 +214,69 @@ never existed. A module declaring no usable system relationship fails the build
212
214
  rather than guessing — a wrong `_stats.systemVersion` is invisible until
213
215
  something migrates on it.
214
216
 
217
+ ### A package that compiles nothing
218
+
219
+ `packageKind: documentation` is the answer "not a Foundry package at all". It
220
+ publishes a website and the book built from the same notes, and compiles no
221
+ compendium: nothing is installed into a Foundry data directory, so there is no
222
+ manifest, no pack and no document.
223
+
224
+ ```yaml
225
+ contentPackage: toolkit
226
+ packageKind: documentation
227
+
228
+ # Required, and `content`: publishing the tree is the whole of what this kind
229
+ # does. `homepage` would leave one authored page, no book and nothing compiled.
230
+ publish:
231
+ site: content
232
+ address: { prefix: guide/ }
233
+
234
+ site:
235
+ out: site/content
236
+
237
+ pdf:
238
+ title: The Toolkit
239
+ document: book.yaml
240
+ fonts: { serif: Libertinus Serif }
241
+ ```
242
+
243
+ `contentPackage`, `paths`, `skipDirectories`, `icons`, `site`, `pdf` and
244
+ `packageBuild` mean exactly what they mean anywhere else. **Every key that
245
+ describes a Foundry package is refused**, by name, with the line and column it
246
+ was written on — `packs`, `itemBuilders`, `docs`, `compatibility`,
247
+ `relationships`, `systems`, `requiresSystem`, `stats` and `foundryPackage`:
248
+
249
+ ```text
250
+ package-build.config.yaml:7:1: error: package-build config: `packs` is refused in a `documentation` package, which compiles no compendium, so there are no packs to declare.
251
+ ```
252
+
253
+ A key that cannot mean anything in this shape fails at load rather than being
254
+ read and ignored, which is the difference between a configuration that is wrong
255
+ and a build that is quietly wrong.
256
+
257
+ What follows from the kind:
258
+
259
+ - **`foundryPackage` is not derived.** For every other kind the loader reads it
260
+ from the adjacent `package.json`; there is no Foundry package here to carry an
261
+ id, so `foundryPackage` and `assetRoot` are both `null`. A note's `img:`
262
+ therefore names the owning package (`systems/…`, `modules/…`) or a URL — a
263
+ path this package would have to serve itself is refused, because Foundry
264
+ serves no files for a package it does not install.
265
+ - **`package-build manifest` refuses**, rather than emitting a `module.json`
266
+ advertising an installable package with no id, no packs and no compatibility
267
+ range. So does `content-build package compile`, which would otherwise exit 0
268
+ having compiled nothing.
269
+ - **Notes need no `id:`.** An id is derived from a note's address, and a
270
+ hand-assigned one is only ever a pin for a document that already shipped —
271
+ which needs a compendium, and there is none.
272
+ - **The note vocabulary is `doc` and `homepage`.** Every other type exists to
273
+ become a Foundry document, so a note carrying one has no destination;
274
+ `content-build lint` reports it at its `type:` line.
275
+ - **A content index is still published.** `content-build content-index` emits
276
+ `<contentPackage>-metadata.jsonl` exactly as it does elsewhere, so another
277
+ package can resolve an address into this one and a wikilink from its notes can
278
+ reach a page here.
279
+
215
280
  ### A note's package is the repository's, not the note's
216
281
 
217
282
  `contentPackage` is the **address namespace** every note in the tree is
@@ -240,7 +305,7 @@ package's id to _be_ its system id.
240
305
  Because it is a segment of an address, the value is **validated** rather than
241
306
  taken as written, and a violation fails the build naming the line it is on:
242
307
 
243
- - **Alphanumeric** (`^[A-Za-z0-9]+$`). An address is read by counting
308
+ - **Lowercase alphanumeric** (`^[a-z0-9]+$`). An address is read by counting
244
309
  hyphen-separated segments, so the hyphen has to be purely a separator — which
245
310
  is why `harn-adventures` is configured as `harnadventures`. This is the same
246
311
  rule `shortcode` is already held to, and the two are one constant.
@@ -257,7 +322,7 @@ taken as written, and a violation fails the build naming the line it is on:
257
322
  refused.
258
323
 
259
324
  ```text
260
- package-build.config.yaml:1:1: error: package-build config: `contentPackage` is `harn-adventures`, which is not alphanumeric. 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 `[A-Za-z0-9]` here makes those addresses unreadable rather than merely ugly. `harn-adventures` became `harnadventures`.
325
+ package-build.config.yaml:1:1: error: 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`.
261
326
  ```
262
327
 
263
328
  **`package:` in a note's frontmatter is retired, and declaring it fails the
@@ -312,7 +377,7 @@ export default defineConfig({
312
377
  foundryPackage: "sohl-kethira-basic",
313
378
  packageKind: "modules",
314
379
  compatibility: { minimum: "14.359", verified: "14.364" },
315
- stats: { systemId: "sohl", systemVersion: "0.4.3", lastModifiedBy: "…" },
380
+ stats: { lastModifiedBy: "…" },
316
381
  itemBuilders: ITEM_BUILDERS,
317
382
  packs: [{ name: "items", type: "Item" }],
318
383
  });
@@ -658,7 +723,9 @@ and there are exactly three answers:
658
723
 
659
724
  `assetRoot` is derived, never authored: it is
660
725
  `<packageKind>/<foundryPackage>/assets`, and it is the one place `systems/sohl`
661
- (or `modules/sohl-thalorna`) is ever spelled. So `icons/relic.svg` in a module's
726
+ (or `modules/sohl-thalorna`) is ever spelled. The derivation is **conditional on
727
+ the kind** — a `documentation` package has no asset root, and the third row is
728
+ refused there rather than rooted. So `icons/relic.svg` in a module's
662
729
  registry compiles to `modules/sohl-relics/assets/icons/relic.svg`, and the same
663
730
  string in the system's compiles to `systems/sohl/assets/icons/relic.svg`. An
664
731
  already-served `systems/sohl/assets/icons/…` passes through untouched — which is
@@ -977,9 +1044,10 @@ npx content-build lint some/tree # or a tree named outright
977
1044
  Checks the three rules every note's **identity** is authored against, and reports
978
1045
  each finding in the located form below:
979
1046
 
980
- - **Shape** — a `shortcode` is strictly ASCII-alphanumeric. It is the identity
981
- key referenced from saved world data, and half of the `type-shortcode`
982
- address, whose parse needs the separating hyphen to be the only hyphen.
1047
+ - **Shape** — a `shortcode` is strictly lowercase ASCII-alphanumeric. It is
1048
+ the identity key referenced from saved world data, and half of the
1049
+ `type-shortcode` address, whose parse needs the separating hyphen to be the
1050
+ only hyphen.
983
1051
  - **Uniqueness** — `(type, shortcode)` names one note. A document is addressed
984
1052
  across _every_ pack of its document type, so routing two same-address notes to
985
1053
  different packs with `pack:` does not separate them.
@@ -1149,7 +1217,7 @@ values. A `weapon` declares none — SoHL distinguishes a weapon's uses by strik
1149
1217
  mode rather than by kind — so `subType` on one is a finding; a `skill` declares
1150
1218
  ten, so `subType: crafte` is a finding naming `craft`.
1151
1219
 
1152
- **A `type` and a `subType` are both held to `^[A-Za-z0-9]+$`** — the same
1220
+ **A `type` and a `subType` are both held to `^[a-z0-9]+$`** — the same
1153
1221
  constant a `shortcode` is held to, read rather than restated. A type is a
1154
1222
  segment of every address — the first of the short form an author writes, the
1155
1223
  third of the canonical `package-system-type-shortcode` — so a hyphen in one is
@@ -1160,7 +1228,7 @@ rule is checked ahead of the closed-set check, which is what makes it reach a
1160
1228
  type whose values are declared but not yet enumerated:
1161
1229
 
1162
1230
  ```text
1163
- assets/content/Beings/Folk.md:3:1: error: `subType` "common-folk" is not a well-formed subType — a subType is letters and digits only (^[A-Za-z0-9]+$), the same charset a type, a shortcode and a contentPackage are held to. …
1231
+ assets/content/Beings/Folk.md:3:1: error: `subType` "common-folk" is not a well-formed subType — a subType is letters and digits only (^[a-z0-9]+$), the same charset a type, a shortcode and a contentPackage are held to. …
1164
1232
  ```
1165
1233
 
1166
1234
  One declared value broke that rule: a `doc`'s `user-guide`, now **`userguide`**.
@@ -1221,7 +1289,7 @@ that harmless: three segments name a _system_ whatever the first segment could
1221
1289
  also have meant, and four is the full form.
1222
1290
 
1223
1291
  **Parsing is plain segment counting**, the same rule the canonical key follows,
1224
- and it is sound because every segment is `^[A-Za-z0-9]+$` — so a hyphen is
1292
+ and it is sound because every segment is `^[a-z0-9]+$` — so a hyphen is
1225
1293
  purely a separator. A target with five segments is not a hyphenated shortcode;
1226
1294
  it is a name, and not an address.
1227
1295
 
@@ -2228,7 +2296,7 @@ Three things about the spelling, each of them load-bearing:
2228
2296
  site chose; a type and a subType are addresses. They need not agree, and on
2229
2297
  `sohl` they do not: the section is `user-guide`, because that is a published
2230
2298
  URL, while the subType is `userguide` because an address segment is
2231
- alphanumeric. Both values are checked against that charset here, so
2299
+ lowercase alphanumeric. Both values are checked against that charset here, so
2232
2300
  copying the section's name in is refused rather than quietly matching nothing.
2233
2301
  - **`listSubType` needs a `listType`.** A subType only tells pages apart within
2234
2302
  a type — `rules`, `userguide` and `reference` are all `doc` — so alone it
@@ -2316,6 +2384,131 @@ None of them exits the process from inside the library; the command decides. Tha
2316
2384
  is what makes them testable, which the consumer scripts' inline `process.exit`
2317
2385
  calls were not.
2318
2386
 
2387
+ ## Publishing a book
2388
+
2389
+ The third surface the content tree publishes, beside the compendium packs and
2390
+ the website: one PDF, built by `content-build pdf` and by `package-build release`.
2391
+
2392
+ ```bash
2393
+ npx content-build pdf # build it where `pdf.out` says
2394
+ npx content-build pdf --out build/book # somewhere else
2395
+ npx content-build pdf --no-compile # emit the Typst source and stop
2396
+ npx content-build pdf --version 1.4.0 # stamp a version on the title page
2397
+ ```
2398
+
2399
+ ### A book is a selection, not a rendering of everything
2400
+
2401
+ The packs and the website publish the _whole_ tree: every note becomes a
2402
+ document and a page, and the three surfaces agreeing about what the content is
2403
+ is the point. **A book is not that.** It is a declared structure whose leaves
2404
+ pick notes out of the corpus, interleaved with prose that need not live in the
2405
+ content tree at all.
2406
+
2407
+ `pdf.document` names that structure — a YAML file of nested sections, each
2408
+ holding some mixture of prose files and `filter:` clauses:
2409
+
2410
+ ```yaml
2411
+ contents:
2412
+ - sectionName: Gear
2413
+ contents:
2414
+ - file: prose/gear-preamble.md
2415
+ - filter: "type = 'weapongear'"
2416
+ - sectionName: Armour
2417
+ contents:
2418
+ - filter: "type = 'armorgear'"
2419
+ - sectionName: The Cast
2420
+ contents:
2421
+ - filter: "type = 'being' AND subType = 'npc'"
2422
+ ```
2423
+
2424
+ Three consequences follow, and each is behaviour rather than oversight:
2425
+
2426
+ - **A note no clause selects is not in the book.** A project decides what its
2427
+ own volume carries, so an omission is an editorial act and nothing reports it.
2428
+ - **A note several clauses select appears several times**, each occurrence its
2429
+ own page, outline node and anchor. Inbound wikilinks are pointed at the
2430
+ first, so `[[weapongear-dagger]]` reaches one place however often it prints.
2431
+ - **A filter that selects nothing _is_ reported.** A note nobody asked for is
2432
+ expected; a clause matching nothing is either wrong or left over from a
2433
+ structure that has moved on.
2434
+
2435
+ The `WHERE` clause runs against the same content index the SQL tables read, and
2436
+ the build owns the `SELECT … FROM notes` — so a filter cannot name a table, and
2437
+ cannot reach another package's notes.
2438
+
2439
+ ### What it is fenced by
2440
+
2441
+ **`publish.site` decides whether a book is built, and it is the only switch.**
2442
+ `content` builds one; `homepage` does not — the same fence that stops the tree
2443
+ being walked for pages stops it being walked for a book. A PDF of the content
2444
+ tree is a content surface by any reading, arguably the most portable one there
2445
+ is, so a package that publishes only a homepage publishes no book however its
2446
+ `pdf:` block is written. The command says so and exits 0.
2447
+
2448
+ That is also the opt-out: a package with no `pdf:` block, no content tree, or
2449
+ `publish.site: homepage` builds nothing and fails nothing.
2450
+
2451
+ ### Configuration
2452
+
2453
+ ```yaml
2454
+ pdf:
2455
+ title: The Hârn Ensemble # required
2456
+ subtitle: a roster of the ready-made
2457
+ document: book.yaml # required — the document tree above
2458
+ out: build/dist # default
2459
+ front: # prose before the contents
2460
+ - prose/colophon.md
2461
+ fonts:
2462
+ serif: Libertinus Serif
2463
+ sans: Libertinus Sans
2464
+ mono: DejaVu Sans Mono
2465
+ path: assets/fonts # where to find them, beyond the system's
2466
+ iconFonts: # icon family → the font carrying its glyphs
2467
+ fontawesome: assets/fonts/fa-solid-900.ttf
2468
+ binary: typst # when it is not simply `typst` on PATH
2469
+ ```
2470
+
2471
+ Nothing here is an address or a brand: the title, the front matter and the faces
2472
+ are the publishing repository's to choose, which is why they are configuration.
2473
+
2474
+ ### Typst is a binary, not a dependency
2475
+
2476
+ The compiler is an external program, found on `PATH` or named by `pdf.binary`.
2477
+ Bundling a native compiler would put a platform-specific artefact into the
2478
+ dependency tree of three repositories, only one of which is mostly a book, and
2479
+ it would have to resolve on every consumer's CI runner before any of them could
2480
+ install the toolchain at all.
2481
+
2482
+ A missing binary is a **finding**, not a failure: the `.typ` source is written
2483
+ anyway, which is both the diagnostic and the thing a consumer can compile by
2484
+ hand.
2485
+
2486
+ ### What the book gets right, and why each matters
2487
+
2488
+ | Property | How |
2489
+ | ------------------------ | ----------------------------------------------------------------------------- |
2490
+ | Searchable | Real text, not page images — a roster nobody can search for a name is no use. |
2491
+ | Bookmark outline | Every section and entry is a heading, so a viewer's sidebar is the way in. |
2492
+ | Page-numbered contents | `#outline()`, shallower than the bookmarks — 2,500 entries would be 40 pages. |
2493
+ | Repeating table headers | `table.header`, so a property table spilling a page keeps its column names. |
2494
+ | Internal cross-reference | A wikilink between two notes of the book becomes an internal destination. |
2495
+ | External cross-reference | A cross-package link, and a note the book did not select, stay URLs. |
2496
+
2497
+ `{#anchor}` on a heading becomes an internal destination namespaced by its
2498
+ entry, so `[[being-jaslyne#appearance]]` reaches the section and two notes may
2499
+ both declare `{#appearance}`. A reference to a destination the book does not
2500
+ carry falls back to the entry that would have held it, or to plain text, and is
2501
+ reported — because Typst treats a dangling reference as fatal, and one mistyped
2502
+ anchor should not take a thousand-page book down at the last step.
2503
+
2504
+ ### The charset is what makes a face choosable
2505
+
2506
+ A PDF embeds the faces it sets, so every character in the corpus is a claim on
2507
+ the book's typeface — and Typst does not warn about a missing glyph, it falls
2508
+ back and exits 0. That is why `content-build lint` holds content to a charset,
2509
+ and why `:icon-…:` names an icon rather than pasting one. Both exist for this
2510
+ surface. See _Prose: formatting and markdown_.
2511
+
2319
2512
  ## Diffing published addresses
2320
2513
 
2321
2514
  ```bash
package/README.md CHANGED
@@ -317,6 +317,13 @@ other's schema — they split by input, and the dependency runs one way.
317
317
  | the release artifact | `packageKind` — a system ships `system.json`, a module `module.json` |
318
318
  | the bundle entry | `packageId` — `<id>.mjs`, unless `packageBuild.bundle.entry` says otherwise |
319
319
 
320
+ **`packageKind: documentation` has no packaging half.** That kind publishes a
321
+ website and a book and installs into no Foundry data directory, so there is no
322
+ artifact to name and no id to derive one from: `manifest` refuses rather than
323
+ writing a manifest for a package Foundry never installs, and the whole of such a
324
+ package's build is `content-build`. See
325
+ [A package that compiles nothing](CONTENT.md#a-package-that-compiles-nothing).
326
+
320
327
  ## Command line
321
328
 
322
329
  ```
@@ -327,12 +334,23 @@ npx package-build lang check
327
334
  npx package-build lang coverage [--unused]
328
335
  npx package-build lang hardcoded
329
336
  npx package-build bundle check
330
- npx package-build release
337
+ npx package-build release [--no-pdf]
331
338
  npx package-build deploy <stage>
332
339
  npx package-build container <stage> <start|stop|restart|recreate|rm|status|logs|pull>
333
340
  npx package-build e2e <seed|run|open|fast|sweep>
334
341
  ```
335
342
 
343
+ `package-build release` writes `<artifact>.zip` and `<artifact>.json` into
344
+ `build/dist`, the content index the manifest advertises, and — when the package
345
+ publishes a content tree — the book that tree compiles to, as
346
+ `<artifact>-<version>.pdf`.
347
+
348
+ The book is **built by default and never fatal**. A package publishing only a
349
+ homepage, one with no `pdf:` block, and one with no content tree each build
350
+ none and say so; `--no-pdf` is for a release that has a tree and does not want
351
+ the book. What the book is and how it is configured is `CONTENT.md`, under
352
+ _Publishing a book_ — it is a content surface, and the content half owns it.
353
+
336
354
  Wrapped as npm scripts — SoHL spells them:
337
355
 
338
356
  ```json