@heroiclands/package-build 22.15.1 → 22.16.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 (221) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +39 -678
  3. package/bin/{content-build.mjs → content-commands.mjs} +197 -118
  4. package/bin/package-build.mjs +303 -42
  5. package/bin/report.mjs +1 -1
  6. package/ci/ci-docker.mjs +6 -3
  7. package/ci/ci-steps.mjs +6 -4
  8. package/ci/content-format-reference.mjs +106 -0
  9. package/config.mjs +22 -2
  10. package/content-config.mjs +74 -134
  11. package/docs/api.md +294 -234
  12. package/docs/authoring/assets.md +31 -0
  13. package/docs/authoring/dates-and-calendars.md +33 -0
  14. package/docs/authoring/first-note.md +48 -0
  15. package/docs/authoring/frontmatter.md +40 -0
  16. package/docs/authoring/links-and-markup.md +137 -0
  17. package/docs/commands.md +631 -162
  18. package/docs/configuration.md +176 -140
  19. package/docs/content-format.md +50 -4229
  20. package/docs/design.md +32 -0
  21. package/docs/diagnostics.md +29 -22
  22. package/docs/getting-started.md +21 -877
  23. package/docs/guides/book.md +153 -0
  24. package/docs/guides/e2e.md +143 -0
  25. package/docs/guides/icons.md +126 -0
  26. package/docs/guides/localization.md +120 -0
  27. package/docs/guides/module-project.md +915 -0
  28. package/docs/guides/packs.md +167 -0
  29. package/docs/guides/reference-pages.md +148 -0
  30. package/docs/guides/release-and-deploy.md +114 -0
  31. package/docs/guides/site.md +152 -0
  32. package/docs/guides/system-project.md +45 -0
  33. package/docs/index.md +16 -0
  34. package/docs/project-setup.md +54 -35
  35. package/docs/reference/format-details.md +4420 -0
  36. package/docs/reference/note-types.md +531 -0
  37. package/engine/address-render.mjs +26 -0
  38. package/engine/address.mjs +3 -20
  39. package/engine/anchors.mjs +15 -6
  40. package/engine/asset-bindings.mjs +6 -2
  41. package/engine/base-compiler.mjs +47 -11
  42. package/engine/being-measurements.mjs +70 -0
  43. package/engine/calendar-artifacts.mjs +1 -1
  44. package/engine/calendar-choice.mjs +32 -0
  45. package/engine/calendar-human.mjs +71 -0
  46. package/engine/calendar-notes.mjs +41 -3
  47. package/engine/calendars.mjs +8 -1
  48. package/engine/changelog-lint.mjs +2 -2
  49. package/engine/code-fences.mjs +23 -97
  50. package/engine/compile-corpus.mjs +6 -7
  51. package/engine/content-admonitions.mjs +114 -0
  52. package/engine/content-captions.mjs +181 -0
  53. package/engine/content-embeds.mjs +2 -2
  54. package/engine/content-footnotes.mjs +34 -0
  55. package/engine/content-format-check.mjs +2 -2
  56. package/engine/content-format.mjs +127 -73
  57. package/engine/content-format.yaml +745 -0
  58. package/engine/content-html.mjs +1 -1
  59. package/engine/content-icons.mjs +45 -92
  60. package/engine/content-images.mjs +22 -74
  61. package/engine/content-index.mjs +12 -3
  62. package/engine/content-links.mjs +21 -26
  63. package/engine/content-lint.mjs +1 -5
  64. package/engine/content-secrets.mjs +3 -1
  65. package/engine/content-tables.mjs +47 -1257
  66. package/engine/culture-choice.mjs +32 -0
  67. package/engine/date-conversion.mjs +47 -0
  68. package/engine/declaration-check.mjs +87 -0
  69. package/engine/docs-checks.mjs +140 -0
  70. package/engine/documentation-links.mjs +31 -0
  71. package/engine/exported-scene.mjs +1 -1
  72. package/engine/extension-attributes.mjs +74 -0
  73. package/engine/field-reference.mjs +1 -1
  74. package/engine/field-spec.mjs +4 -0
  75. package/engine/foreign-catalog.mjs +5 -5
  76. package/engine/format-generated.mjs +15 -0
  77. package/engine/foundry-entries.mjs +3 -1
  78. package/engine/frontmatter-lint.mjs +5 -1
  79. package/engine/frontmatter.mjs +10 -0
  80. package/engine/generate.mjs +2 -2
  81. package/engine/helpers.mjs +71 -117
  82. package/engine/homepage.mjs +2 -3
  83. package/engine/index-records.mjs +1 -0
  84. package/engine/index.mjs +18 -3
  85. package/engine/infobox-registry.mjs +3 -1
  86. package/engine/infobox.mjs +38 -20
  87. package/engine/item-compiler.mjs +10 -8
  88. package/engine/item-docs.mjs +7 -6
  89. package/engine/journals.mjs +51 -10
  90. package/engine/macros.mjs +1 -1
  91. package/engine/map-build.mjs +1 -1
  92. package/engine/map-dot.mjs +1 -1
  93. package/engine/markdown-expressions.mjs +117 -0
  94. package/engine/metadata-index.mjs +7 -3
  95. package/engine/note-addresses.mjs +1 -0
  96. package/engine/note-dates.mjs +283 -61
  97. package/engine/note-package.mjs +5 -25
  98. package/engine/note-state.mjs +6 -2
  99. package/engine/note-vocabulary.mjs +74 -19
  100. package/engine/number-words.mjs +75 -0
  101. package/engine/office-holders.mjs +188 -0
  102. package/engine/pack-config.mjs +2 -15
  103. package/engine/pack-router.mjs +24 -5
  104. package/engine/packages.mjs +3 -2
  105. package/engine/pathnames.mjs +21 -24
  106. package/engine/pdf-build.mjs +217 -55
  107. package/engine/pdf-fonts.mjs +2 -2
  108. package/engine/pdf-images.mjs +82 -0
  109. package/engine/pdf-render.mjs +174 -22
  110. package/engine/populations.mjs +3 -2
  111. package/engine/project-init.mjs +499 -0
  112. package/engine/prose-config.mjs +1 -1
  113. package/engine/prose-lint.mjs +7 -6
  114. package/engine/readability-lint.mjs +86 -0
  115. package/engine/reckoning-markers.mjs +205 -93
  116. package/engine/scenes.mjs +5 -2
  117. package/engine/schema-check.mjs +3 -3
  118. package/engine/site-build.mjs +87 -37
  119. package/engine/site-config.mjs +8 -8
  120. package/engine/site-index.mjs +4 -0
  121. package/engine/site-maps.mjs +1 -1
  122. package/engine/site-preview.mjs +19 -8
  123. package/engine/social-ties.mjs +5 -1
  124. package/engine/sql-tables.mjs +92 -22
  125. package/engine/web-wikilinks.mjs +15 -31
  126. package/engine/wikilink-syntax.mjs +4 -1
  127. package/engine/wikilinks.mjs +12 -14
  128. package/hm3/item-fields.mjs +1 -1
  129. package/hm3/items.mjs +12 -45
  130. package/index.mjs +2 -14
  131. package/manifest.mjs +31 -4
  132. package/markdownlint-config.mjs +1 -1
  133. package/package.json +32 -11
  134. package/prettier-config.mjs +1 -1
  135. package/sohl/being-info.mjs +3 -145
  136. package/sohl/index.mjs +1 -1
  137. package/sohl/infobox.mjs +2 -1
  138. package/sohl/item-builders.mjs +3 -27
  139. package/sohl/item-fields.mjs +61 -22
  140. package/sohl/kb-passes.mjs +2 -2
  141. package/sohl/note-schemas.mjs +0 -8
  142. package/stage.mjs +19 -0
  143. package/types/config.d.mts +12 -3
  144. package/types/content-config.d.mts +15 -81
  145. package/types/engine/address-render.d.mts +13 -0
  146. package/types/engine/address.d.mts +2 -11
  147. package/types/engine/anchors.d.mts +3 -4
  148. package/types/engine/asset-bindings.d.mts +9 -2
  149. package/types/engine/base-compiler.d.mts +1 -2
  150. package/types/engine/being-measurements.d.mts +16 -0
  151. package/types/engine/calendar-choice.d.mts +14 -0
  152. package/types/engine/calendar-human.d.mts +4 -0
  153. package/types/engine/calendar-notes.d.mts +2 -0
  154. package/types/engine/calendars.d.mts +5 -3
  155. package/types/engine/code-fences.d.mts +6 -72
  156. package/types/engine/compile-corpus.d.mts +1 -3
  157. package/types/engine/content-admonitions.d.mts +24 -0
  158. package/types/engine/content-captions.d.mts +45 -0
  159. package/types/engine/content-footnotes.d.mts +10 -0
  160. package/types/engine/content-format-check.d.mts +17 -4
  161. package/types/engine/content-format.d.mts +24 -11
  162. package/types/engine/content-icons.d.mts +14 -23
  163. package/types/engine/content-images.d.mts +2 -2
  164. package/types/engine/content-index.d.mts +9 -2
  165. package/types/engine/content-links.d.mts +3 -5
  166. package/types/engine/content-tables.d.mts +11 -140
  167. package/types/engine/culture-choice.d.mts +14 -0
  168. package/types/engine/date-conversion.d.mts +4 -0
  169. package/types/engine/declaration-check.d.mts +24 -0
  170. package/types/engine/docs-checks.d.mts +28 -0
  171. package/types/engine/documentation-links.d.mts +7 -0
  172. package/types/engine/exported-scene.d.mts +6 -2
  173. package/types/engine/extension-attributes.d.mts +14 -0
  174. package/types/engine/field-spec.d.mts +9 -0
  175. package/types/engine/format-generated.d.mts +7 -0
  176. package/types/engine/frontmatter.d.mts +14 -4
  177. package/types/engine/generate.d.mts +1 -1
  178. package/types/engine/helpers.d.mts +37 -92
  179. package/types/engine/homepage.d.mts +2 -3
  180. package/types/engine/index.d.mts +5 -0
  181. package/types/engine/infobox-registry.d.mts +3 -1
  182. package/types/engine/infobox.d.mts +17 -6
  183. package/types/engine/item-compiler.d.mts +4 -4
  184. package/types/engine/item-docs.d.mts +4 -3
  185. package/types/engine/journals.d.mts +10 -7
  186. package/types/engine/map-graphviz.d.mts +1 -1
  187. package/types/engine/markdown-expressions.d.mts +17 -0
  188. package/types/engine/note-dates.d.mts +47 -1
  189. package/types/engine/note-package.d.mts +3 -19
  190. package/types/engine/note-state.d.mts +2 -1
  191. package/types/engine/note-vocabulary.d.mts +9 -0
  192. package/types/engine/number-words.d.mts +4 -0
  193. package/types/engine/office-holders.d.mts +14 -0
  194. package/types/engine/pack-config.d.mts +1 -1
  195. package/types/engine/packages.d.mts +3 -2
  196. package/types/engine/pdf-images.d.mts +38 -0
  197. package/types/engine/populations.d.mts +2 -2
  198. package/types/engine/project-init.d.mts +17 -0
  199. package/types/engine/prose-config.d.mts +1 -1
  200. package/types/engine/prose-lint.d.mts +3 -2
  201. package/types/engine/readability-lint.d.mts +22 -0
  202. package/types/engine/reckoning-markers.d.mts +4 -2
  203. package/types/engine/schema-check.d.mts +3 -3
  204. package/types/engine/site-build.d.mts +38 -28
  205. package/types/engine/site-config.d.mts +2 -2
  206. package/types/engine/site-index.d.mts +4 -0
  207. package/types/engine/site-preview.d.mts +1 -1
  208. package/types/engine/social-ties.d.mts +8 -2
  209. package/types/engine/sql-tables.d.mts +13 -2
  210. package/types/engine/web-wikilinks.d.mts +2 -1
  211. package/types/engine/wikilinks.d.mts +7 -1
  212. package/types/hm3/items.d.mts +5 -6
  213. package/types/labels.d.mts +40 -0
  214. package/types/manifest.d.mts +3 -3
  215. package/types/sohl/actors.d.mts +2 -2
  216. package/types/sohl/being-info.d.mts +3 -43
  217. package/types/sohl/index.d.mts +1 -1
  218. package/types/sohl/infobox.d.mts +5 -2
  219. package/types/sohl/item-fields.d.mts +4 -13
  220. package/types/stage.d.mts +9 -0
  221. package/CONTENT.md +0 -3239
@@ -0,0 +1,915 @@
1
+ ---
2
+ shortcode: guidesmoduleproject
3
+ name: { full: "Building a Foundry module" }
4
+ type: doc
5
+ subType: howto
6
+ ---
7
+
8
+ # Building a Foundry module
9
+
10
+ This is the path from an empty directory to a HeroicLands Foundry module with content, website, and book builds. A Foundry package also compiles notes into compendium
11
+ packs and generates an installable manifest.
12
+
13
+ ## Create a project
14
+
15
+ Run `init` from a global installation or use npm to fetch the scoped package
16
+ for one invocation:
17
+
18
+ ```bash
19
+ npm exec --package=@heroiclands/package-build -- package-build init acme-bestiary --kind modules --name acme-bestiary --title "Acme Bestiary" --description "Creatures of the reed flats." --author "Acme" --license original
20
+ cd acme-bestiary
21
+ npm install
22
+ npm run lint
23
+ ```
24
+
25
+ The generated project includes a homepage, an introduction, a website build,
26
+ and a book build. Run `npm run build:site` to produce the site,
27
+ `npm run serve:site` to view it locally, or `npm run build:book` for a PDF.
28
+ Those commands require Hugo Extended and Typst respectively. Run
29
+ `package-build init --check` to inspect the project without changing it. See
30
+ [`init`](../commands.md#package-build-init-directory) for the package kinds,
31
+ interactive prompts, and collision rules.
32
+
33
+ The steps below explain each generated file and how to configure it by hand.
34
+
35
+ It assumes you know the constellation's conventions — `~/dev/HeroicLands`, the
36
+ branch and pull request rules, the shared Prettier configuration, the git hooks
37
+ — and assumes nothing about this toolchain. Everything specific to
38
+ `@heroiclands/package-build` is stated here or linked.
39
+
40
+ The worked example builds a **module content package**: a Foundry module whose
41
+ whole substance is a content tree. That is the common case, and the shortest
42
+ path that exercises every stage of the build — the note format, the pack
43
+ compilers, the content index, the manifest and the release archive. The [system project guide](system-project.md) covers the separate system path. A documentation package is a different shape and is covered at the end.
44
+
45
+ You will need Node 24 or newer and npm. Nothing else: no Foundry install, no
46
+ credentials, no container.
47
+
48
+ ## Where the reference material is
49
+
50
+ This document is about **order and motivation** — what to do first, and why the
51
+ next step needs the one before it. The detail lives in the references beside it,
52
+ and this tutorial links to them rather than repeating them:
53
+
54
+ | Document | What it answers |
55
+ | ------------------------------------------- | ------------------------------------------------------------------------------- |
56
+ | [`commands.md`](../commands.md) | What each command does, its options, its exit codes. |
57
+ | [`configuration.md`](../configuration.md) | Every key of `package-build.config.yaml`, its type, its default, its refusals. |
58
+ | [`content-format.md`](../content-format.md) | What a note may declare, per note type, and what it compiles into. |
59
+ | [`diagnostics.md`](../diagnostics.md) | How to read the `file:line:column: severity: message` output. |
60
+ | [`project-setup.md`](../project-setup.md) | The files a repository carries beyond the configuration, and the script wiring. |
61
+ | [`api.md`](../api.md) | The programmatic surface, for a repository with a build script of its own. |
62
+
63
+ An editor can render one note from its current buffer through the
64
+ [`engine.sitePreview` API](../api.md#enginesitepreview). The context reads saved
65
+ notes and dependency indexes once, while each render overlays the active note's
66
+ text and frontmatter. It returns generated Markdown and frontmatter for an HTML
67
+ preview, or located findings if the page cannot be rendered. The editor keeps
68
+ its own preview artifact and calls `refresh()` after a save.
69
+
70
+ ## The shape of what you are building
71
+
72
+ Nine files, and a directory the build writes:
73
+
74
+ ```text
75
+ acme-bestiary/
76
+ ├── package.json # identity: the Foundry package id and the release addresses
77
+ ├── package-build.config.yaml # the build: what this package is, and what it compiles
78
+ ├── prettier.config.js # the shared formatting options
79
+ ├── .gitignore # what is generated, so the checks skip it
80
+ ├── README.md # shipped into the package
81
+ ├── LICENSE.md # shipped into the package
82
+ ├── lang/en.json # shipped into the package
83
+ ├── assets/content/ # the notes — the only hand-authored content
84
+ │ ├── homepage.md
85
+ │ └── Bestiary/Marsh_Drake.md
86
+ └── build/ # everything the build writes; never committed
87
+ ```
88
+
89
+ Two facts govern the whole arrangement, and both are worth holding on to before
90
+ the first command.
91
+
92
+ **`package.json` is the package's identity.** The Foundry package id is its
93
+ `name`, verbatim; the manifest, download and bug addresses are derived from its
94
+ `repository.url`; a system's stamped version is its `version`. None of those is
95
+ transcribed into the build configuration, because a transcribed copy is free to
96
+ drift from what it copied.
97
+
98
+ **`package-build.config.yaml` is the build's single source.** The pack list, the
99
+ compatibility range, the manifest and the site layout are all declared there
100
+ once, and the generated `module.json` is derived from it. There is no
101
+ hand-authored manifest to keep in step.
102
+
103
+ **One thing about the examples.** Every block below is a real file or a real
104
+ transcript. Fenced examples render at the two-space indentation markdown itself
105
+ uses, which is not the four the shared Prettier configuration gives a `.json` or
106
+ `.yaml` file — so copy the content and let `package-build format --write` settle
107
+ the whitespace, rather than transcribing it. Step 6 is where that becomes part
108
+ of the routine.
109
+
110
+ ## Step 1 — `package.json`
111
+
112
+ Create the directory and write `package.json` first, because the toolchain reads
113
+ it from the moment it is installed.
114
+
115
+ ```bash
116
+ mkdir acme-bestiary
117
+ cd acme-bestiary
118
+ ```
119
+
120
+ ```json
121
+ {
122
+ "name": "acme-bestiary",
123
+ "private": true,
124
+ "version": "0.1.0",
125
+ "type": "module",
126
+ "license": "GPL-3.0-or-later AND CC-BY-SA-4.0",
127
+ "repository": {
128
+ "type": "git",
129
+ "url": "https://github.com/HeroicLands/acme-bestiary"
130
+ },
131
+ "engines": {
132
+ "node": ">=24.0.0"
133
+ },
134
+ "scripts": {}
135
+ }
136
+ ```
137
+
138
+ Five of those keys are load-bearing:
139
+
140
+ - **`name`** becomes the Foundry package id, read verbatim. It is what appears
141
+ as `id` in the generated `module.json`, what a compendium UUID is addressed
142
+ through (`Compendium.acme-bestiary.journals.…`), and what the served asset
143
+ root is built from. Choose it once; renaming it later invalidates every UUID
144
+ anything has stored.
145
+ - **`version`** is the release version. It appears in the manifest and in the
146
+ pinned `download` address.
147
+ - **`repository.url`** is what the manifest's `url`, `bugs`, `manifest` and
148
+ `download` addresses are derived from. Without it, `package-build manifest`
149
+ refuses to write anything.
150
+ - **`type: "module"`** lets `prettier.config.js` be written as an ES module,
151
+ which is how the shared configuration is re-exported.
152
+ - **`private: true`** because a package built by this toolchain ships as a
153
+ Foundry release archive, not to npm.
154
+
155
+ Let `package-build format --write` set the indentation once this file exists:
156
+ npm matches `package.json`'s existing indentation when it writes
157
+ `package-lock.json`, so a formatted `package.json` produces a formatted
158
+ lockfile, and neither needs a `.prettierignore` entry for the life of the
159
+ repository.
160
+
161
+ ## Step 2 — install the toolchain
162
+
163
+ ```bash
164
+ npm install --save-dev @heroiclands/package-build
165
+ ```
166
+
167
+ That installs the `package-build` executable into `node_modules/.bin`. Run it
168
+ through `npx`, or wire it into npm scripts as
169
+ [`project-setup.md`](../project-setup.md) describes.
170
+
171
+ **How it worked:** `npx package-build --version` prints the installed version.
172
+ `--version` and `--help` need no configuration. Commands that build a package
173
+ resolve `package-build.config.yaml` by walking up from the working directory
174
+ and report when it is missing:
175
+
176
+ ```console
177
+ $ npx package-build package compile
178
+ [ERROR]: package-build: no package-build.config.yaml or package-build.config.yml or package-build.config.mjs found at or above /path/to/acme-bestiary, nor at or above /path/to/acme-bestiary/node_modules/@heroiclands/package-build/engine. A consuming repository declares its build in one file at its root; set PACKAGE_BUILD_CONFIG to name it elsewhere.
179
+ ```
180
+
181
+ That is the next step.
182
+
183
+ ## Step 3 — `.gitignore` and `prettier.config.js`
184
+
185
+ These come before the first check rather than after it, because the checks read
186
+ them.
187
+
188
+ `.gitignore` is what keeps generated trees out of the prose checks: both
189
+ `package-build format` and `package-build markdown` consult a repository's
190
+ ignore files, so a `build/` that is not named here is a build whose own output
191
+ gets reported as unformatted.
192
+
193
+ ```text
194
+ node_modules
195
+ /build/
196
+ /nogit/
197
+ /.env.local
198
+ ```
199
+
200
+ `node_modules` carries neither a leading nor a trailing slash, for a reason
201
+ [`project-setup.md`](../project-setup.md) gives in full: a trailing slash matches a
202
+ directory only, and a worktree whose `node_modules` is a symlink then goes
203
+ unignored.
204
+
205
+ `prettier.config.js` re-exports the shared options:
206
+
207
+ ```js
208
+ /**
209
+ * The shared HeroicLands Prettier configuration.
210
+ *
211
+ * Every route to Prettier — the toolchain, an editor integration, a bare
212
+ * `npx prettier` — resolves a config file, so re-exporting the shared options
213
+ * here is what keeps those three from formatting the same tree three ways.
214
+ *
215
+ * @type {import("prettier").Config}
216
+ */
217
+ export { default } from "@heroiclands/package-build/prettier";
218
+ ```
219
+
220
+ `package-build format` applies the shared options whether or not this file
221
+ exists, so the lint chain is correct without it. Nothing else is: an editor's
222
+ format-on-save and a bare `npx prettier --write .` resolve a _config file_, and
223
+ finding none they fall back to Prettier's own defaults — a different print width
224
+ against the same tree, so the editor and the lint chain take turns rewriting
225
+ each other's work. The toolchain says so when the file is missing:
226
+
227
+ ```console
228
+ $ npx package-build format
229
+ warning: this repository declares no Prettier configuration, so `package-build format` applies the shared conventions while an editor and a bare `npx prettier` apply Prettier's own to the same tree; declare them in a prettier.config.mjs — export { default } from "@heroiclands/package-build/prettier";
230
+ ```
231
+
232
+ Any filename Prettier resolves works, `prettier.config.js` included; this is the
233
+ one every repository in the family carries.
234
+
235
+ ## Step 4 — `package-build.config.yaml`
236
+
237
+ The build's declaration. This is the smallest one that compiles a module content
238
+ package:
239
+
240
+ ```yaml
241
+ contentPackage: bestiary
242
+ packageKind: modules
243
+
244
+ compatibility:
245
+ minimum: "14.359"
246
+ verified: "14.364"
247
+
248
+ stats:
249
+ lastModifiedBy: acmebuilder00000
250
+
251
+ packs:
252
+ - { name: journals, label: Journals, type: JournalEntry }
253
+ ```
254
+
255
+ Five keys, and each answers a question the build cannot answer for itself:
256
+
257
+ - **`contentPackage`** names the content package this repository single-sources
258
+ — `bestiary` here, as `thalorna` and `kethira` name theirs. It is what a note's
259
+ canonical address is scoped by and what the published content index is named
260
+ for. **A note never declares its own package**; it belongs to this one, and a
261
+ `package:` key in a note is a hard error.
262
+ - **`packageKind`** is `modules`, `systems` or `documentation`. It decides
263
+ whether the manifest is written as `module.json` or `system.json`, where the
264
+ served asset root is rooted, and where the stamped system version comes from.
265
+ - **`compatibility.minimum`** is the Foundry core version every compiled
266
+ document is stamped with, and `verified` is the version the package is tested
267
+ against. There is no default: a guessed floor is stamped into every document
268
+ in the pack and stays invisible until something migrates on it.
269
+ - **`stats.lastModifiedBy`** is the sixteen-character Foundry user id stamped
270
+ into every document's `_stats` block.
271
+ - **`packs`** is the compendium list, and it is the _only_ place packs are
272
+ declared — the manifest's `packs` array is derived from it, so the two cannot
273
+ disagree.
274
+
275
+ Everything omitted has a default, and the defaults are the conventional
276
+ HeroicLands layout: the content tree at `assets/content`, the compiled packs at
277
+ `build/stage/packs`, the intermediates and caches elsewhere under `build/`.
278
+ [`configuration.md`](../configuration.md) documents all eighteen top-level keys,
279
+ what each refusal message means, and the five values that are derived rather
280
+ than authored.
281
+
282
+ **How it worked:** run a content command and watch the complaint move from the
283
+ configuration to the content tree.
284
+
285
+ ```console
286
+ $ npx package-build package compile
287
+ [ERROR]: Content tree not found at /path/to/acme-bestiary/assets/content.
288
+ [ERROR]: Pack JSON generation reported 1 error(s); refusing to compile packs from incomplete output.
289
+ ```
290
+
291
+ The configuration validated. There is simply nothing to compile yet.
292
+
293
+ ## Step 5 — the content tree
294
+
295
+ Two notes: the package homepage, and one piece of content.
296
+
297
+ **Every package's tree holds exactly one `type: homepage` note.** It is the
298
+ package's front page, authored rather than generated, and `package-build lint`
299
+ requires it. It compiles to a page and to no Foundry document.
300
+
301
+ `assets/content/homepage.md`:
302
+
303
+ ```markdown
304
+ ---
305
+ shortcode: root
306
+ name: { full: The Acme Bestiary }
307
+ type: homepage
308
+ description: Creatures of the reed flats, their habits and their hides.
309
+ ---
310
+
311
+ # The Acme Bestiary
312
+
313
+ What lives in the reed flats, what it eats, and what it is worth to the people
314
+ who hunt it.
315
+ ```
316
+
317
+ `assets/content/Bestiary/Marsh_Drake.md`:
318
+
319
+ ```markdown
320
+ ---
321
+ shortcode: marshdrake
322
+ name: { full: Marsh Drake }
323
+ type: lore
324
+ subType: bestiary
325
+ description: A wingless drake of the reed flats, hunted for its hide and feared for its patience.
326
+ ---
327
+
328
+ # Marsh Drake
329
+
330
+ The marsh drake is a wingless reptile of the reed flats, grown to the length of
331
+ a river barge. It hunts by stillness: it lies half-submerged for a day at a
332
+ time and takes whatever wades within reach.
333
+
334
+ ## Habits
335
+
336
+ Drakes hold a stretch of water and defend it against their own kind. A stretch
337
+ that falls vacant is claimed within a season.
338
+ ```
339
+
340
+ Four things about that frontmatter are the note format in miniature, and they
341
+ are worth reading closely because everything else in the tree is a variation on
342
+ them.
343
+
344
+ **`type:` routes the note, not its location.** `Bestiary/` is a folder for a
345
+ human's benefit. What makes this note a journal is `type: lore`; a note's
346
+ directory has no bearing on which pack it lands in. `subType:` narrows it —
347
+ `bestiary` is "a kind of creature that is not a people" — and the pair
348
+ `(type, subType)` is what each game system maps onto its own document type.
349
+
350
+ **`shortcode:` is the note's address.** Cross-references are written
351
+ `[[lore-marshdrake|Marsh Drake]]` — the type, shortcode, and readable label — and resolve to whatever
352
+ that note compiles into, in whatever system is being compiled. A shortcode is
353
+ identity: renaming one breaks every link into it.
354
+
355
+ **`name.full` is the document name and the published URL**, derived by one
356
+ shared rule. There is no authored slug anywhere in this toolchain.
357
+
358
+ **Frontmatter has three closed regions.** Top-level keys identify the note and
359
+ describe its publication; `data:` holds shared facts; `sohl:` and `hm3:` hold
360
+ system-specific facts. An unrecognised key is an error rather than a site
361
+ parameter. The formatter keeps the allowed top-level keys in their required
362
+ order.
363
+ [the content authoring guide](../content-format.md) explains the note model and links to the detailed reference.
364
+
365
+ **A setting guide is a `doc` that orients a reader to a place or setting.** It
366
+ gives a usable overview of areas, peoples, institutions, and daily life, then
367
+ links to the detailed notes. Use `subType: settingguide` for that entry point;
368
+ `concept` explains one subject, `reference` serves lookup, and `userguide`
369
+ explains the Foundry controls. For example, a guide at
370
+ `assets/content/Guides/Reed_Flats.md` can begin:
371
+
372
+ ```markdown
373
+ ---
374
+ shortcode: reedflatsguide
375
+ name: { full: The Reed Flats Guide }
376
+ type: doc
377
+ subType: settingguide
378
+ description: A starting point for adventures in the Reed Flats.
379
+ ---
380
+
381
+ The river trade links the settlements of the Reed Flats. Their routes and
382
+ customs give visitors a way into the region. See [[lore-reedfolk|Reed Folk]]
383
+ for the people who guide those journeys.
384
+ ```
385
+
386
+ The guide can include SQL tables for lists drawn from indexed notes and
387
+ `:::secret` blocks for GM passages. Its address is
388
+ `doc-reedflatsguide`, following the same rule as every `doc` note.
389
+
390
+ An image stands in its own paragraph. Its directive accepts a named size and a
391
+ position together:
392
+
393
+ ```markdown
394
+ ![[icon-anubis|Anubis]]{size=medium float=top-left}
395
+ ```
396
+
397
+ The accepted sizes are `auto`, `small`, `medium`, `large`, `xlarge`, and
398
+ `full-width`. Omitting `size` means `auto`, which uses the image's natural
399
+ dimensions within the available measure. The other names set maximum display
400
+ widths for the website, Foundry, and book. See
401
+ [Images](../authoring/assets.md) for the surface sizes and layout rules.
402
+
403
+ ## Step 6 — check the tree
404
+
405
+ Four checks, and they are quick enough to run continuously while authoring.
406
+
407
+ ```bash
408
+ npx package-build lint # addresses and frontmatter
409
+ npx package-build links # every wikilink resolves
410
+ npx package-build format # the shared Prettier options
411
+ npx package-build markdown # the shared markdownlint rule set
412
+ ```
413
+
414
+ **How it worked:**
415
+
416
+ ```console
417
+ $ npx package-build lint
418
+ [INFO]: Addresses and frontmatter are well-formed (2 address(es) across 2 note(s)).
419
+
420
+ $ npx package-build links
421
+ [INFO]: 2 notes: every link is a labelled address, every anchor link lands and every address resolves (0 cross-package reference(s) via manifest), no wikilink in frontmatter, every homepage address resolvable.
422
+
423
+ $ npx package-build markdown
424
+ [INFO]: Markdown is clean.
425
+ ```
426
+
427
+ `package-build format` reports rather than fixes, which is what a CI gate wants;
428
+ `--write` is the fixing form. Transcribe a file's whitespace by hand and the
429
+ reporting form names it:
430
+
431
+ ```console
432
+ $ npx package-build format
433
+ package-build.config.yaml: error: is not formatted; run `package-build format --write` to fix it
434
+ [ERROR]: 1 of 6 file(s) are not formatted.
435
+
436
+ $ npx package-build format --write
437
+ [INFO]: Formatted 1 of 6 file(s).
438
+
439
+ $ npx package-build format
440
+ [INFO]: Formatting is clean (6 file(s)).
441
+ ```
442
+
443
+ Both forms walk the whole repository, not only the content tree — the
444
+ configuration, the scripts and the prose are all held to the same options.
445
+
446
+ Every finding these commands emit starts with the path of the file that is
447
+ wrong, followed by a line, a column, a severity and a message — the contract
448
+ [`diagnostics.md`](../diagnostics.md) describes. A field that cannot be known is
449
+ dropped rather than guessed, so a finding about a whole file names only the file.
450
+
451
+ Forget the homepage and `package-build lint` says so by name:
452
+
453
+ ```console
454
+ $ npx package-build lint
455
+ assets/content: error: holds no `type: homepage` note, so package "bestiary" publishes nothing at its own address /bestiary/ — a package's front page is one authored note in this tree, routed by `type:` rather than by filename
456
+ [ERROR]: 1 finding(s) across 1 note(s).
457
+ ```
458
+
459
+ ## Step 7 — compile the packs
460
+
461
+ ```bash
462
+ npx package-build package compile
463
+ ```
464
+
465
+ ```console
466
+ [INFO]: Content tree: 2 note(s) at /path/to/acme-bestiary/assets/content
467
+ [INFO]: Pack journals: /path/to/acme-bestiary/assets/content → /path/to/acme-bestiary/build/packs-json/journals
468
+ [INFO]: Compiled 1 journal entry (0 documentation entries)
469
+ [INFO]: Pack journals: compiling to LevelDB at /path/to/acme-bestiary/build/stage/packs/journals
470
+ [INFO]: Pack compilation complete.
471
+ ```
472
+
473
+ Two notes, one journal entry: the homepage compiles to a page and to no Foundry
474
+ document, which is why the counts differ.
475
+
476
+ The compile runs in two passes, and the intermediate is worth knowing about
477
+ because it is where you look when a document is not what you expected.
478
+ `build/packs-json/journals/Marsh_Drake_72d497c6a8e57f57.json` is the compiled
479
+ document as JSON, before it is written into the LevelDB pack — shown here with
480
+ the page's HTML elided, and otherwise entire:
481
+
482
+ ```json
483
+ {
484
+ "name": "Marsh Drake",
485
+ "pages": [
486
+ {
487
+ "_id": "2ecce29fd1c5578f",
488
+ "name": "Marsh Drake",
489
+ "type": "text",
490
+ "title": { "show": true, "level": 1 },
491
+ "text": { "format": 1, "content": "…" },
492
+ "_key": "!journal.pages!72d497c6a8e57f57.2ecce29fd1c5578f"
493
+ }
494
+ ],
495
+ "folder": null,
496
+ "sort": 0,
497
+ "ownership": { "default": 0 },
498
+ "flags": {},
499
+ "_id": "72d497c6a8e57f57",
500
+ "_stats": {
501
+ "systemId": null,
502
+ "systemVersion": null,
503
+ "coreVersion": "14.359",
504
+ "createdTime": 0,
505
+ "modifiedTime": 0,
506
+ "lastModifiedBy": "acmebuilder00000"
507
+ },
508
+ "_key": "!journal!72d497c6a8e57f57"
509
+ }
510
+ ```
511
+
512
+ The body has become one text page, because a journal's pages are cut at its `#`
513
+ headings: this note has one, so the whole body is one page named for it. A
514
+ second `#` would produce a second page; anything before the first becomes a
515
+ leading page named "Introduction". A heading at any level carrying an
516
+ `{#anchor}` suffix also starts a page, because a Foundry UUID can address a page
517
+ and nothing smaller — which is how a link to a section inside a note resolves at
518
+ all. `_key` is the LevelDB key the entry is stored under, which is why the
519
+ intermediate carries it.
520
+
521
+ `coreVersion` is the `compatibility.minimum` from step 4 and `lastModifiedBy` is
522
+ the `stats.lastModifiedBy`. `systemId` and `systemVersion` are `null` because
523
+ this module declares no game system: it ships journals, which every system can
524
+ read. A module shipping Actors or Items declares the system it ships for, and
525
+ those two fields are stamped from it — see
526
+ [`systems`](../configuration.md#systems).
527
+
528
+ `_id` is derived from the note's canonical address, so it is stable across
529
+ builds: recompiling does not renumber anything, and a world that imported
530
+ yesterday's pack still resolves against today's.
531
+
532
+ **A build that exits 0 and produces the wrong output is a bug.** The compile is
533
+ the stage where that matters most, so read the counts: "2 note(s)" and "Compiled
534
+ 1 journal entry" are the two numbers that say what the tree held and what came
535
+ out of it.
536
+
537
+ ### Cross-references, and what they compile into
538
+
539
+ Add a second note and link to it. `assets/content/Bestiary/Fen_Adder.md`:
540
+
541
+ ```markdown
542
+ ---
543
+ type: lore
544
+ subType: bestiary
545
+ name:
546
+ full: Fen Adder
547
+ shortcode: fenadder
548
+ description: A small venomous snake of the standing water, more feared than the drake.
549
+ ---
550
+
551
+ # Fen Adder
552
+
553
+ A hand-long snake that lies under the surface scum. Its venom kills slowly and
554
+ reliably, which is why the reed-cutters fear it more than the drake.
555
+ ```
556
+
557
+ Then, in `Marsh_Drake.md`:
558
+
559
+ ```markdown
560
+ Drakes hold a stretch of water and defend it against their own kind. A stretch
561
+ that falls vacant is claimed within a season. Reed-cutters working a drake's
562
+ water fear the [[lore-fenadder|fen adder]] more.
563
+ ```
564
+
565
+ ```console
566
+ $ npx package-build links
567
+ [INFO]: 3 notes: every link is a labelled address, every anchor link lands and every address resolves (0 cross-package reference(s) via manifest), no wikilink in frontmatter, every homepage address resolvable.
568
+
569
+ $ npx package-build package compile
570
+ [INFO]: Compiled 2 journal entries (0 documentation entries)
571
+ ```
572
+
573
+ And in the compiled journal, the wikilink has become a Foundry reference:
574
+
575
+ ```text
576
+ @UUID[Compendium.acme-bestiary.journals.JournalEntry.c7c3488c3e2a9282]{fen adder}
577
+ ```
578
+
579
+ That is the whole point of addressing notes by shortcode rather than by
580
+ filename or by id: one authored link, resolved at build time into the reference
581
+ each surface needs — a `@UUID` in a compendium journal, a relative URL on the
582
+ website. A link that resolves to nothing is an error naming the note, never a
583
+ blank.
584
+
585
+ ## Step 8 — what the package ships
586
+
587
+ The packs are compiled but the package is not yet assembled. A Foundry package
588
+ is a staged directory — a manifest, the packs, and whatever files the manifest
589
+ points at — and the configuration declares both halves.
590
+
591
+ Write the three files the manifest will name:
592
+
593
+ `README.md`:
594
+
595
+ ```markdown
596
+ # Acme Bestiary
597
+
598
+ Creatures of the reed flats, as compendium journals for Foundry VTT.
599
+ ```
600
+
601
+ `LICENSE.md`:
602
+
603
+ ```markdown
604
+ # License
605
+
606
+ Code: GPL-3.0-or-later. Content: CC-BY-SA-4.0.
607
+ ```
608
+
609
+ `lang/en.json`:
610
+
611
+ ```json
612
+ {
613
+ "ACMEBESTIARY": {
614
+ "Title": "Acme Bestiary"
615
+ }
616
+ }
617
+ ```
618
+
619
+ Then add the `packageBuild:` section to `package-build.config.yaml`. It is the
620
+ packaging half's reserved section — validated separately from everything above
621
+ it, and documented under
622
+ [the `packageBuild` section](../configuration.md#the-packagebuild-section):
623
+
624
+ ```yaml
625
+ packageBuild:
626
+ assets:
627
+ - { from: README.md, to: README.md }
628
+ - { from: LICENSE.md, to: LICENSE.md }
629
+ - { from: lang, to: lang }
630
+
631
+ manifest:
632
+ title: Acme Bestiary
633
+ description: Creatures of the reed flats, as compendium journals.
634
+ authors:
635
+ - name: Acme
636
+ license: LICENSE.md
637
+ readme: README.md
638
+ languages:
639
+ - lang: en
640
+ name: English
641
+ path: lang/en.json
642
+ packFolders:
643
+ - name: Acme Bestiary
644
+ sorting: m
645
+ packs:
646
+ - journals
647
+ ```
648
+
649
+ `assets:` is a copy list, from the repository into the stage. `manifest:` is
650
+ everything about the manifest that cannot be derived from `package.json` or from
651
+ the keys above — the human-facing identity, and the files Foundry links to.
652
+ `title` is required by Foundry and has no derivation; supply it.
653
+
654
+ `packFolders` may name only packs this package ships, and the build compares the
655
+ two lists rather than trusting them to agree:
656
+
657
+ ```console
658
+ $ npx package-build manifest
659
+ package-build.config.yaml:36:21: error: packFolders: folder "Acme Bestiary" names pack "items", which this package does not ship (packs: journals)
660
+ package-build: packFolders names 1 pack this package does not ship (reported above). Foundry skips a name it cannot resolve, so the folder would ship missing those packs — correct `packageBuild.manifest.packFolders`.
661
+ ```
662
+
663
+ That is the shape of the checks throughout: Foundry's own behaviour on bad input
664
+ is to skip it silently, so the build refuses to emit input Foundry would skip.
665
+
666
+ Stage the files:
667
+
668
+ ```console
669
+ $ npx package-build assets
670
+ ✅ Static assets staged (3 entries, 3 files).
671
+ ```
672
+
673
+ Name a path that is not there and it says so before copying anything:
674
+
675
+ ```console
676
+ $ npx package-build assets
677
+ package-build: Cannot stage assets — these paths do not exist:
678
+ README.md
679
+ LICENSE.md
680
+ lang
681
+ ```
682
+
683
+ ## Step 9 — generate the manifest
684
+
685
+ ```console
686
+ $ npx package-build manifest
687
+ ✅ Wrote build/stage/module.json (16 keys, 1 packs).
688
+ ```
689
+
690
+ `build/stage/module.json` is the file Foundry installs:
691
+
692
+ ```json
693
+ {
694
+ "id": "acme-bestiary",
695
+ "title": "Acme Bestiary",
696
+ "description": "Creatures of the reed flats, as compendium journals.",
697
+ "version": "0.1.0",
698
+ "authors": [{ "name": "Acme" }],
699
+ "license": "LICENSE.md",
700
+ "readme": "README.md",
701
+ "flags": {
702
+ "metadataUrl": "https://github.com/HeroicLands/acme-bestiary/releases/download/v0.1.0/bestiary-metadata.jsonl"
703
+ },
704
+ "compatibility": { "minimum": "14.359", "verified": "14.364" },
705
+ "languages": [{ "lang": "en", "name": "English", "path": "lang/en.json" }],
706
+ "packFolders": [{ "name": "Acme Bestiary", "sorting": "m", "packs": ["journals"] }],
707
+ "packs": [
708
+ {
709
+ "label": "Journals",
710
+ "type": "JournalEntry",
711
+ "name": "journals",
712
+ "path": "packs/journals",
713
+ "private": false
714
+ }
715
+ ],
716
+ "url": "https://github.com/HeroicLands/acme-bestiary",
717
+ "bugs": "https://github.com/HeroicLands/acme-bestiary/issues",
718
+ "manifest": "https://github.com/HeroicLands/acme-bestiary/releases/latest/download/module.json",
719
+ "download": "https://github.com/HeroicLands/acme-bestiary/releases/download/v0.1.0/module.zip"
720
+ }
721
+ ```
722
+
723
+ Read it against what you authored, because the derivations are the part worth
724
+ checking:
725
+
726
+ - `id` is `package.json`'s `name`, verbatim.
727
+ - `version` and the pinned `download` address are its `version`.
728
+ - `url`, `bugs`, `manifest` and `download` are all built from its
729
+ `repository.url`. Omit that and the command refuses rather than writing an
730
+ unpublishable manifest:
731
+
732
+ ```console
733
+ $ npx package-build manifest
734
+ package-build: package.json declares no `repository.url`, so the manifest has no release addresses to advertise. Add it.
735
+ ```
736
+
737
+ - `packs` is the `packs:` list from step 4, with `path` and `private` filled in.
738
+ It is never declared twice.
739
+ - `manifest` points at `releases/latest`, while `download` and `flags.metadataUrl`
740
+ are pinned to this version. That asymmetry is deliberate: the manifest address
741
+ must keep resolving as new releases land, and the download must not move under
742
+ a world that installed it.
743
+
744
+ ## Step 10 — publish the content index
745
+
746
+ ```console
747
+ $ npx package-build content-index
748
+ [INFO]: bestiary → build/content-index/bestiary-metadata.jsonl (3 notes, 1 KiB)
749
+ ```
750
+
751
+ This is the file `flags.metadataUrl` advertises, and it is how packages address
752
+ each other. One JSON object per note, carrying the note's canonical address, its
753
+ name, its description and the Foundry UUID it compiles to:
754
+
755
+ ```text
756
+ {"address":{"canonical":"bestiary-none-lore-marshdrake","slug":"lore-marshdrake"},"aliasesAscii":[],"anchors":[],"description":"A wingless drake of the reed flats, hunted for its hide and feared for its patience.","documentation":null,"file":{"folder":"Bestiary","name":"Marsh_Drake","path":"Bestiary/Marsh_Drake.md"},"foundry":{"none":{"uuid":"Compendium.acme-bestiary.journals.JournalEntry.72d497c6a8e57f57"}},"id":"72d497c6a8e57f57","name":{"full":"Marsh Drake"},"nameAscii":"Marsh Drake","package":"bestiary","shortcode":"marshdrake","subType":"bestiary","type":"lore"}
757
+ ```
758
+
759
+ A downstream package that declares a dependency on this one fetches this file
760
+ for the release it pins, and `[[lore-marshdrake]]` written in _its_ tree resolves
761
+ through it to that UUID. The homepage's entry carries `"foundry": null` — it
762
+ compiles to a page, so there is no document to address.
763
+
764
+ The index is derived and disposable; it is written under `build/` and rebuilt
765
+ from the tree every time.
766
+
767
+ ### Calendar files for a setting module
768
+
769
+ A content package with `lore` notes of `subType: calendar` can emit Foundry
770
+ calendar definitions after the content index is built:
771
+
772
+ ```console
773
+ $ npx package-build calendars --calendaria-version 1.4.2
774
+ ✅ 5 calendars emitted (10 files).
775
+ ```
776
+
777
+ The command writes `build/calendars/<shortcode>.json` and
778
+ `build/calendars/<shortcode>.calendaria.json` for each calendar. The first is a
779
+ calendar definition; the second wraps the same definition for import through
780
+ Calendaria's settings. The version argument names the Calendaria import format
781
+ the package targets. The envelope uses a fixed `exportedAt` sentinel because
782
+ these files are reproducible build artifacts. A package with no calendars gets
783
+ an empty `build/calendars/` directory.
784
+
785
+ To ship the files in a Foundry package, add
786
+ `{ from: build/calendars, to: calendars }` to `packageBuild.assets` and run the
787
+ calendar command before `package-build assets`. Running the command requires
788
+ the current content index; regenerate it after changing calendar notes.
789
+
790
+ ## Step 11 — the release archive
791
+
792
+ ```console
793
+ $ npx package-build release
794
+ ✅ Packaged 0.1.0 for release: build/dist/module.zip (0.0 MB)
795
+ ```
796
+
797
+ `build/dist/` then holds the three files a GitHub release needs, and the archive
798
+ is named from `packageKind` — a module ships `module.zip`, a system
799
+ `system.zip` — so no repository states it a second time:
800
+
801
+ ```console
802
+ $ ls build/dist
803
+ bestiary-metadata.jsonl
804
+ module.json
805
+ module.zip
806
+ ```
807
+
808
+ The manifest and the content index sit beside the archive rather than only
809
+ inside it, because that is where the addresses in the manifest point: a
810
+ consumer reads `module.json` from the release without downloading the zip, and
811
+ then fetches the index it names.
812
+
813
+ The archive itself is `build/stage/` verbatim — the manifest at the root, the
814
+ LevelDB packs under `packs/`, and the files the assets step put there. `unzip
815
+ -l build/dist/module.zip` is worth reading once, because it is the only view of
816
+ what a player actually installs.
817
+
818
+ That is the whole build. From an empty directory: identity, configuration,
819
+ notes, checks, packs, stage, manifest, index, archive.
820
+
821
+ ## Putting it in order
822
+
823
+ The eleven steps above are the order you need them the _first_ time. Thereafter
824
+ the build is one chain, and this is the order it runs in:
825
+
826
+ ```bash
827
+ npx package-build format # the prose checks
828
+ npx package-build markdown
829
+ npx package-build lint # the content checks
830
+ npx package-build links
831
+ npx package-build content-index # the index the manifest advertises
832
+ npx package-build assets # the stage: the shipped files
833
+ npx package-build package compile
834
+ npx package-build manifest # the stage: the manifest
835
+ npx package-build release # the archive
836
+ ```
837
+
838
+ Only one of those orderings is enforced, and it is worth knowing which.
839
+ `package-build release` refuses to pack an archive whose manifest advertises a
840
+ content index the tree never wrote:
841
+
842
+ ```console
843
+ $ npx package-build release
844
+ package-build: the manifest advertises bestiary-metadata.jsonl as `flags.metadataUrl` but no such file exists — looked in …/build/stage/bestiary-metadata.jsonl and …/build/content-index/bestiary-metadata.jsonl. Build the content index before packing the release.
845
+ ```
846
+
847
+ The rest of the chain is order-independent: `assets`, `compile` and `manifest`
848
+ each write their own part of `build/stage/` and none clobbers another's. They
849
+ are written in this order because it is the order that reads as an assembly —
850
+ the files, then the packs, then the manifest describing both — and because
851
+ everything that reads the tree runs before everything that writes the stage.
852
+
853
+ [`project-setup.md`](../project-setup.md) turns this into npm scripts, and covers
854
+ everything a repository carries beyond the build itself: the git hooks, the
855
+ changeset directory, the label registry, and what each script in the chain is
856
+ for.
857
+
858
+ ## What comes next
859
+
860
+ Four capabilities are configuration away, and each has its own guide material.
861
+ None of them is needed to build a package.
862
+
863
+ **A website.** Every package publishes one — at the least, the homepage note
864
+ from step 5 — at `https://www.heroiclands.org/<contentPackage>/`. Add that
865
+ address to `package.json` as `homepage`, with a `description` and an `author`
866
+ beside it; add `@heroiclands/hugo-theme` under `devDependencies`; and add a
867
+ `packageBuild.manifest.title`. Then `package-build deps fetch` caches the
868
+ organisation's navigation, and `package-build site` writes the whole Hugo
869
+ source tree under `build/hugo/` — the configuration generated from those
870
+ values, and the content mount — for `hugo --source build/hugo` to render
871
+ into `build/site/<contentPackage>/`. There is no Hugo configuration to
872
+ write: the file is generated on every run, and what is genuinely the
873
+ package's own — the wording of its "page not found" page — goes in the
874
+ `site:` block as `site.notfound`. The same command publishes the content tree's
875
+ every page beside the homepage — one
876
+ page per note, and nothing generated between them; an index of what the
877
+ package publishes is a `doc` note carrying a content table.
878
+ [`project-setup.md`](../project-setup.md) gives the npm scripts.
879
+
880
+ **Another package's content.** Declare a dependency under `relationships`, and
881
+ `package-build deps fetch` caches that release's published content index so
882
+ `[[…]]` links into it resolve. A relationship marked `itemCatalog: true` also
883
+ caches the release's Item packs, which is what lets a being embed items by
884
+ `(type, shortcode)`. Fetching never happens during a compile: a cold cache
885
+ fails naming `deps fetch` rather than reaching the network.
886
+
887
+ **A game system's documents.** A module shipping Actors or Items names its
888
+ `itemBuilders` registry — `sohl` or `hm3` — and declares the system under
889
+ `systems:`. That is what maps a note's `(type, subType)` onto a system's own
890
+ document type, and what supplies the `_stats.systemVersion` every document is
891
+ stamped with.
892
+
893
+ **Deploying and testing.** `package-build deploy <stage>` pushes a staged
894
+ package to a Foundry data directory or a remote host; `package-build container`
895
+ runs a licensed Foundry in Docker; `package-build e2e` drives the Cypress suite
896
+ against it. All three need a Foundry install, credentials, or both — see
897
+ [`commands.md`](../commands.md).
898
+
899
+ ## A documentation package
900
+
901
+ `packageKind: documentation` is the third kind, and it is a different shape
902
+ rather than a smaller one: a package that publishes a content tree as a website
903
+ and a book, and compiles no Foundry documents at all.
904
+
905
+ Everything in steps 4 and 8 that exists to describe a Foundry package is
906
+ **refused** there rather than ignored, each with a message saying why:
907
+ `foundryPackage`, `stats`, `packs`, `itemBuilders`, `compatibility`,
908
+ `relationships`, `systems`, `requiresSystem` and `docs`. The authored content
909
+ tree supplies its site pages and book entries.
910
+
911
+ So a documentation package's configuration is steps 1 through 6 with a different
912
+ `packageKind`, plus a `site:` block, and then `package-build site` in place of
913
+ steps 7 through 11. [`configuration.md`](../configuration.md) carries the refusal
914
+ message for every key; [`commands.md`](../commands.md) covers the two commands such
915
+ a package lives on, `package-build site` and `package-build pdf`.