@heroiclands/package-build 20.3.1 → 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 (195) hide show
  1. package/CHANGELOG.md +351 -0
  2. package/CONTENT.md +322 -110
  3. package/README.md +21 -10
  4. package/bin/content-build.mjs +204 -92
  5. package/bin/package-build.mjs +48 -15
  6. package/config.mjs +6 -8
  7. package/content-config.mjs +518 -151
  8. package/docs/api.md +1352 -0
  9. package/docs/commands.md +1609 -0
  10. package/docs/configuration.md +1432 -0
  11. package/docs/content-format.md +73 -74
  12. package/docs/diagnostics.md +356 -0
  13. package/docs/getting-started.md +813 -0
  14. package/docs/project-setup.md +469 -0
  15. package/e2e.mjs +4 -4
  16. package/engine/actor-compiler.mjs +64 -61
  17. package/engine/address-charset.mjs +6 -6
  18. package/engine/address-diff.mjs +57 -52
  19. package/engine/anchored-sections.mjs +1 -1
  20. package/engine/anchors.mjs +1 -1
  21. package/engine/base-compiler.mjs +42 -38
  22. package/engine/bundle-notes.mjs +4 -4
  23. package/engine/bundles.mjs +17 -8
  24. package/engine/code-fences.mjs +3 -3
  25. package/engine/compendiums.mjs +8 -8
  26. package/engine/compile-corpus.mjs +4 -5
  27. package/engine/content-address.mjs +24 -24
  28. package/engine/content-charset.mjs +1 -1
  29. package/engine/content-format-check.mjs +24 -25
  30. package/engine/content-format.mjs +8 -8
  31. package/engine/content-html.mjs +202 -0
  32. package/engine/content-icons.mjs +196 -165
  33. package/engine/content-index.mjs +61 -40
  34. package/engine/content-links.mjs +74 -41
  35. package/engine/content-lint.mjs +64 -34
  36. package/engine/content-package.mjs +7 -9
  37. package/engine/content-slug.mjs +2 -2
  38. package/engine/content-tables.mjs +38 -34
  39. package/engine/content-tree.mjs +1 -1
  40. package/engine/derived-fields.mjs +174 -0
  41. package/engine/diagnostics.mjs +3 -4
  42. package/engine/document-subtypes.mjs +6 -7
  43. package/engine/field-reference.mjs +8 -8
  44. package/engine/field-spec.mjs +29 -30
  45. package/engine/folder-notes.mjs +18 -16
  46. package/engine/foreign-catalog.mjs +8 -9
  47. package/engine/foundry-entries.mjs +19 -20
  48. package/engine/frontmatter-lint.mjs +95 -103
  49. package/engine/frontmatter.mjs +11 -11
  50. package/engine/generate.mjs +44 -39
  51. package/engine/helpers.mjs +147 -84
  52. package/engine/homepage.mjs +20 -22
  53. package/engine/ids.mjs +15 -15
  54. package/engine/index-records.mjs +4 -4
  55. package/engine/index.mjs +26 -10
  56. package/engine/item-compiler.mjs +54 -29
  57. package/engine/item-docs.mjs +11 -13
  58. package/engine/item-registry.mjs +13 -13
  59. package/engine/journals.mjs +24 -15
  60. package/engine/macros.mjs +3 -3
  61. package/engine/map-notes.mjs +6 -6
  62. package/engine/metadata-index.mjs +5 -5
  63. package/engine/note-claims.mjs +51 -41
  64. package/engine/note-ids.mjs +5 -5
  65. package/engine/note-package.mjs +4 -4
  66. package/engine/note-renames.mjs +3 -3
  67. package/engine/note-schemas.mjs +3 -8
  68. package/engine/note-vocabulary.mjs +56 -54
  69. package/engine/pack-config.mjs +39 -25
  70. package/engine/pack-router.mjs +0 -0
  71. package/engine/pdf-build.mjs +464 -0
  72. package/engine/pdf-fonts.mjs +420 -0
  73. package/engine/pdf-render.mjs +798 -0
  74. package/engine/pdf-toc.mjs +525 -0
  75. package/engine/prose-config.mjs +9 -9
  76. package/engine/prose-lint.mjs +12 -13
  77. package/engine/region-events.mjs +2 -2
  78. package/engine/retired-fields.mjs +24 -22
  79. package/engine/runtime-only-fields.mjs +1 -1
  80. package/engine/scene-levels.mjs +1 -1
  81. package/engine/scenes.mjs +33 -25
  82. package/engine/schema-check.mjs +21 -21
  83. package/engine/schema-extract.mjs +1 -1
  84. package/engine/site-build.mjs +66 -50
  85. package/engine/site-index.mjs +17 -18
  86. package/engine/sql-tables.mjs +21 -13
  87. package/engine/subtype-registry.mjs +5 -4
  88. package/engine/system-block.mjs +18 -19
  89. package/engine/systems.mjs +2 -2
  90. package/engine/web-wikilinks.mjs +41 -38
  91. package/engine/wikilink-syntax.mjs +16 -16
  92. package/engine/wikilinks.mjs +40 -42
  93. package/hm3/actors.mjs +30 -22
  94. package/hm3/document-subtypes.mjs +5 -5
  95. package/hm3/index.mjs +2 -2
  96. package/hm3/item-builders.mjs +1 -1
  97. package/hm3/item-fields.mjs +3 -3
  98. package/hm3/items.mjs +28 -8
  99. package/hm3/template-priority.mjs +2 -2
  100. package/lang.mjs +3 -3
  101. package/manifest.mjs +17 -20
  102. package/package.json +1 -2
  103. package/release.mjs +66 -6
  104. package/sohl/actors.mjs +36 -28
  105. package/sohl/affiliation-standings.mjs +2 -2
  106. package/sohl/being-info.mjs +5 -5
  107. package/sohl/default-item-art.mjs +5 -5
  108. package/sohl/document-subtypes.mjs +5 -5
  109. package/sohl/index.mjs +3 -3
  110. package/sohl/item-builders.mjs +7 -7
  111. package/sohl/item-fields.mjs +8 -9
  112. package/sohl/items.mjs +28 -6
  113. package/sohl/kb-passes.mjs +5 -5
  114. package/sohl/note-schemas.mjs +12 -12
  115. package/sohl/skill-base.mjs +1 -1
  116. package/types/content-config.d.mts +91 -39
  117. package/types/e2e.d.mts +3 -3
  118. package/types/engine/actor-compiler.d.mts +45 -40
  119. package/types/engine/address-charset.d.mts +6 -6
  120. package/types/engine/address-diff.d.mts +60 -6
  121. package/types/engine/base-compiler.d.mts +27 -21
  122. package/types/engine/bundle-notes.d.mts +3 -3
  123. package/types/engine/bundles.d.mts +10 -1
  124. package/types/engine/code-fences.d.mts +3 -3
  125. package/types/engine/compendiums.d.mts +3 -3
  126. package/types/engine/compile-corpus.d.mts +1 -1
  127. package/types/engine/content-address.d.mts +20 -20
  128. package/types/engine/content-format-check.d.mts +6 -6
  129. package/types/engine/content-format.d.mts +2 -2
  130. package/types/engine/content-html.d.mts +78 -0
  131. package/types/engine/content-icons.d.mts +139 -92
  132. package/types/engine/content-index.d.mts +59 -15
  133. package/types/engine/content-links.d.mts +8 -8
  134. package/types/engine/content-lint.d.mts +8 -6
  135. package/types/engine/content-package.d.mts +6 -8
  136. package/types/engine/content-tables.d.mts +49 -18
  137. package/types/engine/derived-fields.d.mts +101 -0
  138. package/types/engine/diagnostics.d.mts +2 -2
  139. package/types/engine/document-subtypes.d.mts +3 -3
  140. package/types/engine/field-spec.d.mts +37 -39
  141. package/types/engine/folder-notes.d.mts +4 -5
  142. package/types/engine/foreign-catalog.d.mts +3 -3
  143. package/types/engine/foundry-entries.d.mts +6 -7
  144. package/types/engine/frontmatter-lint.d.mts +10 -9
  145. package/types/engine/frontmatter.d.mts +7 -7
  146. package/types/engine/generate.d.mts +7 -7
  147. package/types/engine/helpers.d.mts +128 -56
  148. package/types/engine/homepage.d.mts +16 -18
  149. package/types/engine/ids.d.mts +13 -13
  150. package/types/engine/index-records.d.mts +3 -3
  151. package/types/engine/index.d.mts +6 -0
  152. package/types/engine/item-compiler.d.mts +21 -5
  153. package/types/engine/item-docs.d.mts +2 -2
  154. package/types/engine/item-registry.d.mts +6 -6
  155. package/types/engine/journals.d.mts +12 -3
  156. package/types/engine/map-notes.d.mts +2 -2
  157. package/types/engine/metadata-index.d.mts +4 -4
  158. package/types/engine/note-claims.d.mts +30 -22
  159. package/types/engine/note-ids.d.mts +4 -4
  160. package/types/engine/note-package.d.mts +1 -1
  161. package/types/engine/note-renames.d.mts +3 -3
  162. package/types/engine/note-vocabulary.d.mts +31 -203
  163. package/types/engine/pack-config.d.mts +7 -7
  164. package/types/engine/pack-router.d.mts +1 -1
  165. package/types/engine/pdf-build.d.mts +42 -0
  166. package/types/engine/pdf-fonts.d.mts +30 -0
  167. package/types/engine/pdf-render.d.mts +144 -0
  168. package/types/engine/pdf-toc.d.mts +114 -0
  169. package/types/engine/prose-config.d.mts +9 -9
  170. package/types/engine/prose-lint.d.mts +3 -4
  171. package/types/engine/region-events.d.mts +2 -2
  172. package/types/engine/retired-fields.d.mts +10 -9
  173. package/types/engine/scenes.d.mts +10 -1
  174. package/types/engine/schema-check.d.mts +13 -13
  175. package/types/engine/site-build.d.mts +52 -25
  176. package/types/engine/site-index.d.mts +3 -4
  177. package/types/engine/sql-tables.d.mts +11 -5
  178. package/types/engine/subtype-registry.d.mts +3 -3
  179. package/types/engine/system-block.d.mts +3 -3
  180. package/types/engine/web-wikilinks.d.mts +7 -7
  181. package/types/engine/wikilink-syntax.d.mts +17 -17
  182. package/types/engine/wikilinks.d.mts +14 -14
  183. package/types/hm3/actors.d.mts +9 -1
  184. package/types/hm3/document-subtypes.d.mts +1 -1
  185. package/types/hm3/items.d.mts +9 -2
  186. package/types/hm3/template-priority.d.mts +1 -1
  187. package/types/manifest.d.mts +8 -8
  188. package/types/release.d.mts +15 -4
  189. package/types/sohl/actors.d.mts +12 -4
  190. package/types/sohl/affiliation-standings.d.mts +2 -2
  191. package/types/sohl/being-info.d.mts +2 -2
  192. package/types/sohl/document-subtypes.d.mts +1 -1
  193. package/types/sohl/items.d.mts +9 -2
  194. package/types/sohl/note-schemas.d.mts +1 -1
  195. package/MIGRATING.md +0 -608
@@ -0,0 +1,469 @@
1
+ # Project setup
2
+
3
+ [`getting-started.md`](getting-started.md) builds a package. This document
4
+ describes the repository around it: the files every HeroicLands package carries
5
+ beyond `package-build.config.yaml`, what each npm script in the chain is for,
6
+ and the directory layout the build reads and writes.
7
+
8
+ None of it is required to compile a pack. All of it is required for a repository
9
+ someone else can contribute to, and the reason to write it down is that
10
+ otherwise each repository copies it from the last and the copies drift.
11
+
12
+ ## The layout
13
+
14
+ ```text
15
+ acme-bestiary/
16
+ ├── package.json # identity, and every script below
17
+ ├── package-lock.json # committed
18
+ ├── package-build.config.yaml # the build's single source
19
+ ├── prettier.config.js # re-exports the shared Prettier options
20
+ ├── .prettierignore # committed files Prettier must not rewrite
21
+ ├── .gitignore # what is generated
22
+ ├── .changeset/
23
+ │ ├── config.json
24
+ │ └── *.md # one per unreleased change
25
+ ├── .github/
26
+ │ ├── labels.yml # the closed label registry
27
+ │ ├── ISSUE_REPORTING.md # §3 lists the same labels
28
+ │ └── workflows/
29
+ ├── assets/
30
+ │ └── content/ # authored notes — an Obsidian vault
31
+ ├── lang/
32
+ │ └── en.json
33
+ ├── src/ # module JavaScript, if the package has any
34
+ ├── styles/
35
+ ├── README.md
36
+ ├── LICENSE.md
37
+ └── build/ # everything generated; gitignored
38
+ ```
39
+
40
+ `build/` is the only directory the toolchain writes into, and every path under
41
+ it is configurable through [`paths`](configuration.md#paths). The defaults:
42
+
43
+ | Path | Written by | Holds |
44
+ | ----------------------- | ------------------------------ | ------------------------------------------------------------- |
45
+ | `build/packs-json/` | `content-build package` | One JSON file per compiled document, before the pack is made. |
46
+ | `build/stage/packs/` | `content-build package` | The LevelDB compendium packs. |
47
+ | `build/stage/` | `package-build assets` | The staged package — manifest, packs, and the shipped files. |
48
+ | `build/content-index/` | `content-build content-index` | This package's published note index. |
49
+ | `build/cache/foreign/` | `content-build deps` | A dependency's unpacked item catalogue. |
50
+ | `build/cache/metadata/` | `content-build deps` | A dependency's fetched content index. |
51
+ | `build/tmp/packs/` | `content-build package unpack` | A compiled pack extracted back to JSON. |
52
+ | `build/dist/` | `package-build release` | The release archive, and the book when one is built. |
53
+
54
+ The two that matter when something is wrong are `build/packs-json/`, which is
55
+ the compiled document before Foundry ever sees it, and `build/stage/`, which is
56
+ exactly what ships.
57
+
58
+ **`assets/content/` is opened as an Obsidian vault.** There is no export step
59
+ and no vault environment variable: the notes in the repository are the source,
60
+ and a plain checkout builds. Dataview renders the content tables live while
61
+ authoring, which is why the tree is arranged for humans even though `type:`
62
+ rather than location is what routes a note.
63
+
64
+ ## `package.json`
65
+
66
+ Beyond the identity keys [`getting-started.md`](getting-started.md) covers,
67
+ `package.json` carries the whole command surface of the repository. Nobody types
68
+ `npx content-build package compile`; they type `npm run build`.
69
+
70
+ The scripts fall into six groups, and the grouping is the point — a contributor
71
+ who knows the groups can guess a name.
72
+
73
+ ### `prepare` — the git hooks
74
+
75
+ ```json
76
+ "prepare": "git config core.hooksPath node_modules/@heroiclands/package-build/githooks || true"
77
+ ```
78
+
79
+ npm runs `prepare` after every install, so this is what installs the hooks. It
80
+ points git at the hook directory this package ships, which means a repository
81
+ carries no hook files of its own and cannot drift from the others. `|| true` so
82
+ that an install outside a git checkout — a Docker build, a CI cache step —
83
+ does not fail on it.
84
+
85
+ Three hooks arrive with it, each with its own switch, read with git's normal
86
+ precedence: a plain `git config` sets one clone, `--global` sets a machine.
87
+
88
+ | Hook | What it does | Key | Default |
89
+ | -------------------------------- | --------------------------------------------------------------- | ------------------------------------ | ------- |
90
+ | `pre-commit`, `pre-merge-commit` | Refuse a commit on a protected branch. | `hooks.allowCommitOnMain` (inverted) | on |
91
+ | `commit-msg` | Refuse AI attribution in a commit message. | `hooks.noAttribution` | on |
92
+ | `pre-push` | Run this repository's own Build & Test workflow in a container. | `hooks.prePushCi` | off |
93
+
94
+ The defaults differ deliberately: a guard that costs nothing is on unless
95
+ refused, and one that runs a container for minutes is off unless asked for.
96
+
97
+ The branch guard is the one you meet first:
98
+
99
+ ```console
100
+ $ git commit -m "Add the bestiary content package"
101
+ pre-commit: refusing to commit on the protected branch 'master'.
102
+
103
+ 'master' is protected on GitHub, so this commit could never be pushed from
104
+ here. Move it onto a branch first — this keeps everything you have staged:
105
+
106
+ git switch -c <type>/<issue_#>_<slug>
107
+
108
+ To commit here anyway just this once, use 'git commit --no-verify'. To opt this
109
+ repository out permanently, 'git config hooks.allowCommitOnMain true'.
110
+ ```
111
+
112
+ `pre-push` reads the step list out of `.github/workflows/build.yml` rather than
113
+ holding its own copy, and runs it over a clean export of `HEAD` in a
114
+ `linux/amd64` container. That last detail is the one a Mac cannot reproduce any
115
+ other way: this filesystem is case-insensitive and the runner's is not, so a
116
+ wrong-case import passes locally and fails there.
117
+
118
+ ### `clean` — removing what the build wrote
119
+
120
+ ```json
121
+ "clean": "package-build clean",
122
+ "distclean": "package-build clean --distclean"
123
+ ```
124
+
125
+ `clean` removes the conventional build directories plus anything named in
126
+ `packageBuild.clean.extra` — a site's `content/`, `public/` and `resources/`,
127
+ say. `distclean` additionally removes `node_modules`. Both exit 0 whether or not
128
+ there was anything to remove.
129
+
130
+ ### `lint:*` — the checks, one per question
131
+
132
+ Six checks, each answering one question and blind to what the others see. They
133
+ are separate scripts rather than one because a failing chain stops at its first
134
+ failure, and knowing _which_ question failed is most of the diagnosis.
135
+
136
+ ```json
137
+ "lint": "run-s lint:format lint:markdown lint:addresses lint:content-links lint:lang lint:labels",
138
+ "lint:format": "content-build format",
139
+ "lint:markdown": "content-build markdown",
140
+ "lint:markdown:fix": "content-build markdown --fix",
141
+ "lint:addresses": "content-build lint",
142
+ "lint:content-links": "content-build links",
143
+ "lint:lang": "package-build lang check",
144
+ "lint:labels": "package-build labels check"
145
+ ```
146
+
147
+ - **`lint:format`** — is every file formatted to the shared Prettier options?
148
+ Reports without writing, which is what a CI gate wants; `npm run format`
149
+ is the writing form.
150
+ - **`lint:markdown`** — does the prose satisfy the shared markdownlint rule set?
151
+ `lint:markdown:fix` applies the fixes markdownlint can make.
152
+ - **`lint:addresses`** — is every note's frontmatter well-formed, is every
153
+ address unique, and does the tree hold exactly one homepage?
154
+ - **`lint:content-links`** — does every `[[…]]` resolve, does every `#anchor`
155
+ land, and is every cross-package reference reachable through a fetched
156
+ manifest?
157
+ - **`lint:lang`** — does every localization file survive
158
+ `foundry.utils.expandObject`? A dotted-prefix collision (`"a.b": 1` beside
159
+ `"a.b.c": 2`) makes Foundry drop the whole file silently, which is the quiet
160
+ failure this catches. A package with templates and scripts also wires
161
+ `package-build lang coverage` and `package-build lang hardcoded`; a
162
+ content-only package has nothing for those two to read, and each dies naming
163
+ the glob that matched nothing rather than passing vacuously.
164
+ - **`lint:labels`** — do `.github/labels.yml` and §3 of
165
+ `.github/ISSUE_REPORTING.md` still list the same labels? Neither derives from
166
+ the other, so nothing else notices when they drift.
167
+
168
+ `run-s` comes from `npm-run-all`, a devDependency. It runs scripts in sequence
169
+ and stops at the first failure, which is what makes a named chain readable.
170
+
171
+ ### `build:*` — the chain
172
+
173
+ ```json
174
+ "build": "npm ci && npm run build:noci",
175
+ "build:local": "npm i && npm run build:noci",
176
+ "build:noci": "run-s lint build:db build:module",
177
+ "build:db": "run-s build:content-index build:assets build:compiledb",
178
+ "build:content-index": "content-build content-index",
179
+ "build:assets": "package-build assets",
180
+ "build:compiledb": "content-build package compile",
181
+ "build:unpackdb": "content-build package unpack",
182
+ "build:module": "package-build manifest",
183
+ "build:pack-release": "package-build release"
184
+ ```
185
+
186
+ The split between `build` and `build:noci` is not cosmetic. **`build` installs
187
+ from the lockfile first**, so what it compiles is what CI compiles;
188
+ `build:local` installs without the lockfile, for a working tree mid-change; and
189
+ `build:noci` skips the install entirely, which is the one to reach for inside a
190
+ git worktree where `node_modules` is already correct.
191
+
192
+ Within the chain, two orderings are real:
193
+
194
+ - **`build:content-index` before `build:pack-release`.** The manifest advertises
195
+ the content index at a pinned address, and `package-build release` refuses an
196
+ archive whose manifest advertises an index the tree never wrote.
197
+ - **`lint` before any of it.** The checks read the tree; the compilers read the
198
+ tree and write. Finding a malformed address after the packs are built means
199
+ rebuilding them.
200
+
201
+ The three stage-writing steps — `build:assets`, `build:compiledb`,
202
+ `build:module` — each write their own part of `build/stage/` and none clobbers
203
+ another's, so their order among themselves is legibility rather than necessity.
204
+
205
+ `build:unpackdb` is the inverse of `build:compiledb`: it extracts a compiled
206
+ LevelDB pack back to JSON, which is how you read what actually shipped.
207
+
208
+ A package with a dependency adds `"build:deps": "content-build deps fetch"` at
209
+ the head of `build:db`. Fetching is its own step and never happens during a
210
+ compile, so a build never reaches the network silently — a cold cache fails
211
+ naming `deps fetch`.
212
+
213
+ ### `format` — writing rather than checking
214
+
215
+ ```json
216
+ "format": "content-build format --write",
217
+ "format:check": "content-build format"
218
+ ```
219
+
220
+ Two names for one command because two audiences want it: a contributor wants
221
+ the writing form, and CI wants the checking form. `format:check` and
222
+ `lint:format` are the same command; both names exist because the lint chain
223
+ reads better with one and a CI workflow reads better with the other.
224
+
225
+ ### `changeset:*` — the release mechanism
226
+
227
+ ```json
228
+ "changeset": "changeset",
229
+ "changeset:check": "changeset status --since=origin/main",
230
+ "changeset:version": "changeset version && npm install --package-lock-only"
231
+ ```
232
+
233
+ `changeset` adds one. `changeset:check` reports what is pending against `main`,
234
+ which is what a CI job gates on so that a behaviour change cannot merge without
235
+ declaring its bump. `changeset:version` consumes the pending changesets, writes
236
+ `CHANGELOG.md` and bumps `package.json` — and the `npm install --package-lock-only`
237
+ after it is what keeps `package-lock.json`'s recorded version in step, since
238
+ changesets does not touch the lockfile.
239
+
240
+ ### The site scripts
241
+
242
+ A package that publishes a website adds a further group. They are outside this
243
+ document's scope because they need Hugo and the shared theme submodule, but the
244
+ shape is worth knowing so the names are not surprising:
245
+ `build:site-content` writes the Hugo content tree with `content-build site`,
246
+ `build:site-html` runs Hugo over it, and `serve:site` does the first and then
247
+ `hugo server` for a live preview.
248
+
249
+ ### Deployment scripts
250
+
251
+ `push:dev`, `push:qa` and `push:prod` wrap `package-build deploy <stage>`, and
252
+ `deploy:*` chains a build in front of each. They read credentials from
253
+ `.env.local`, which is never committed. A fresh worktree has no `.env.local`, so
254
+ these exit without copying anything and the target keeps serving the previous
255
+ build — copy the file in before trusting a deployment from one.
256
+
257
+ ## `prettier.config.js`
258
+
259
+ ```js
260
+ /**
261
+ * The shared HeroicLands Prettier configuration.
262
+ *
263
+ * @type {import("prettier").Config}
264
+ */
265
+ export { default } from "@heroiclands/package-build/prettier";
266
+ ```
267
+
268
+ **Never restate an option here.** The options live in
269
+ `@heroiclands/package-build/prettier` so that every repository formats
270
+ identically, and a local override is how two repositories stop agreeing. The
271
+ file exists solely so that an editor and a bare `npx prettier` resolve the same
272
+ options the toolchain applies — without it they fall back to Prettier's own
273
+ defaults, and a print width that differs from the project's turns every
274
+ save into a diff the lint chain reverts.
275
+
276
+ `content-build markdown` needs no equivalent file: it hands the shared rule set
277
+ to markdownlint directly, and a repository that declares nothing gets it. A
278
+ `.markdownlint-cli2.jsonc` is for the editor's markdownlint extension, and
279
+ [`@heroiclands/package-build/markdownlint`](api.md) is the export it re-uses —
280
+ with one trap worth knowing before writing one. A consumer file overrides the
281
+ shared configuration **key by key, and each key wholesale**: declaring only
282
+ `ignores` keeps every rule intact but _replaces_ the shared ignore list rather
283
+ than extending it, so such a file must restate every shared entry it still
284
+ wants.
285
+
286
+ ## `.prettierignore`
287
+
288
+ `content-build format` already consults `.gitignore`, so generated trees named
289
+ there need no second entry. `.prettierignore` is for the opposite case: files
290
+ that are **committed** and must not be rewritten.
291
+
292
+ ```text
293
+ # Written by `changeset version`, not by hand. The Version Packages pull request
294
+ # runs no CI — it is opened by GITHUB_TOKEN — so a changelog the formatter has
295
+ # rewritten turns `main` red the moment that pull request merges.
296
+ CHANGELOG.md
297
+ ```
298
+
299
+ Two more entries are common, and both are the same principle:
300
+
301
+ - **A generated site tree.** Hugo layouts are Go templates in `.html` files,
302
+ which Prettier reflows as HTML — folding the template directives into prose.
303
+ - **Vendored files from another build.** A file copied verbatim from another
304
+ repository is reformatted into a permanent diff against its source, which
305
+ re-diffs on every re-vendor.
306
+
307
+ `package-lock.json` needs no entry when `package.json` is formatted, because npm
308
+ matches `package.json`'s indentation when it writes the lockfile — so a
309
+ formatted `package.json` produces a lockfile the formatter already agrees with.
310
+ A repository whose `package.json` is indented some other way ignores the
311
+ lockfile instead, or `npm install` and the formatter rewrite it in turn.
312
+
313
+ ## `.gitignore`
314
+
315
+ The build's own output, plus the local-only paths every repository has:
316
+
317
+ ```text
318
+ node_modules
319
+ /build/
320
+ /nogit/
321
+ /.env
322
+ /.env.local
323
+ /.claude/
324
+ CLAUDE.md
325
+ AI.md
326
+ *.tgz
327
+ .DS_Store
328
+ ```
329
+
330
+ Three of those carry rules rather than conventions.
331
+
332
+ **`nogit/` is local scratch** — design write-ups, working notes, downloaded
333
+ references. Nothing in it is ever committed.
334
+
335
+ **`.env.local` holds real credentials** for the deployment targets. Never
336
+ committed, never echoed.
337
+
338
+ **`node_modules` with no leading slash and no trailing slash.** The trailing
339
+ slash matches a directory only, so a worktree carrying `node_modules` as a
340
+ symlink is not ignored and a `git add -A` commits a symlink holding one
341
+ developer's absolute path. Unanchored so the same cannot happen one directory
342
+ down, which is where a nested worktree puts it.
343
+
344
+ Note also that `.gitignore` is what keeps `build/` out of the prose checks:
345
+ `content-build format` reads `.gitignore` and `.prettierignore` both, and
346
+ `content-build markdown` reads `.gitignore`.
347
+
348
+ ## `.changeset/`
349
+
350
+ ```json
351
+ {
352
+ "$schema": "https://unpkg.com/@changesets/config@4.0.0/schema.json",
353
+ "changelog": "@changesets/changelog-git",
354
+ "commit": true,
355
+ "baseBranch": "main",
356
+ "privatePackages": {
357
+ "version": true,
358
+ "tag": false
359
+ },
360
+ "updateInternalDependencies": "patch",
361
+ "ignore": []
362
+ }
363
+ ```
364
+
365
+ `npx changeset init` writes a starting point — it prompts once, for whether to
366
+ use the GitHub changelog integration, and the answer here is no. Four keys then
367
+ need setting, and each is a decision rather than a preference.
368
+
369
+ - **`changelog: "@changesets/changelog-git"`** attributes each entry to its
370
+ commit. The GitHub changelog generator wants a token and produces entries
371
+ naming pull requests, which is history the changelog does not need.
372
+ - **`privatePackages: {version: true, tag: false}`** is what makes changesets
373
+ work at all here. Every package in the constellation is `private`, because it
374
+ ships as a Foundry release rather than to npm; without `version: true`
375
+ changesets skips it entirely, and `tag: false` because the release tag is cut
376
+ by the release workflow rather than by changesets.
377
+ - **`commit: true`** so `changeset version` commits its own result.
378
+ - **`baseBranch: "main"`** is what `changeset status --since=origin/main`
379
+ compares against.
380
+
381
+ Then one `.changeset/*.md` per unreleased change:
382
+
383
+ ```markdown
384
+ ---
385
+ "acme-bestiary": minor
386
+ ---
387
+
388
+ **Creatures** — The marsh drake joins the bestiary, with its habits and the
389
+ stretch of water it holds.
390
+ ```
391
+
392
+ Three rules about the body, and they are enforced by review rather than by a
393
+ tool:
394
+
395
+ - **It describes the setting, not the work.** Write for someone who installs or
396
+ upgrades and wants to know what changed for them.
397
+ - **Bold labels, never `#` headings.** Changesets nests the whole summary inside
398
+ one bullet, so an `h2` written here becomes a real `<h2>` in the changelog,
399
+ outranking the `Minor Changes` heading above it and polluting the outline.
400
+ - **On a 0.x package a breaking change is `minor`, never `major`.** `major`
401
+ takes the package to 1.0.0, which is a product decision rather than a
402
+ description of a diff.
403
+
404
+ ## `.github/`
405
+
406
+ Two files the toolchain reads, and they must agree.
407
+
408
+ **`.github/labels.yml`** is the closed label registry — the machine-readable
409
+ source of truth, synced to GitHub by the org-wide `labels` action. A label on
410
+ GitHub that is not listed there is deleted on the next sync, which is what makes
411
+ the set closed and why nothing invents one.
412
+
413
+ ```yaml
414
+ - name: documentation
415
+ color: "0075ca"
416
+ description: Documentation about this repository — README, process, authoring guides.
417
+
418
+ - name: devops
419
+ color: "e07b31"
420
+ description: Build, tooling, CI, release, repo config.
421
+ ```
422
+
423
+ **`.github/ISSUE_REPORTING.md`** §3 is the same list in prose, for a human
424
+ filing an issue. `package-build labels check` compares the two and fails when
425
+ they disagree:
426
+
427
+ ```console
428
+ $ npx package-build labels check
429
+ package-build: registry and §3 agree (2 labels).
430
+ ```
431
+
432
+ Neither file derives from the other, so this check is the only thing standing
433
+ between them and a silent divergence. Editing the registry means editing both.
434
+
435
+ Labels are **subject matter only**. Work shape — bug, feature, epic, task, spike
436
+ — is an issue _type_, shared across the organization, so there is no `bug` label
437
+ and filtering on one returns nothing.
438
+
439
+ Neither file exists in a fresh repository, and `labels check` says so rather
440
+ than passing vacuously:
441
+
442
+ ```console
443
+ $ npx package-build labels check
444
+ package-build: labels check: .github/labels.yml does not exist.
445
+ ```
446
+
447
+ `.github/workflows/` then carries the repository's own CI. The org-wide
448
+ `HeroicLands/.github` repository supplies the shared composite actions those
449
+ workflows `uses:` — `labels`, `no-attribution` and `todos` — plus the reusable
450
+ site-deploy workflow, so a fix to any of them reaches every repository at once.
451
+
452
+ ## What the toolchain reads, and what it writes
453
+
454
+ A summary, because "where does this come from" is the question that recurs.
455
+
456
+ | Read | By |
457
+ | -------------------------------------------------- | ----------------------------------------------------------- |
458
+ | `package.json` | The Foundry package id, the version, the release addresses. |
459
+ | `package-build.config.yaml` | Everything else about the build. |
460
+ | `assets/content/**/*.md` | Every content command. |
461
+ | `.gitignore` | `content-build format`, `content-build markdown`. |
462
+ | `.prettierignore` | `content-build format`. |
463
+ | `.github/labels.yml`, `.github/ISSUE_REPORTING.md` | `package-build labels check`. |
464
+ | `lang/*.json` | `package-build lang`. |
465
+ | `.env.local` | `package-build deploy`. |
466
+ | `.github/workflows/build.yml` | The `pre-push` hook, for its step list. |
467
+
468
+ Everything written goes under `build/`. Nothing the toolchain generates is
469
+ committed.
package/e2e.mjs CHANGED
@@ -19,7 +19,7 @@
19
19
  * of labour here. Standing a licensed Foundry up, seeding a world whose
20
20
  * Gamemaster password is known, waiting for that world to be *active* rather
21
21
  * than merely reachable, tearing it all down again — none of that is one
22
- * repository's problem, and all of it used to live in one. What runs against
22
+ * repository's problem, and none of it lives in one. What runs against
23
23
  * the served world is named in `packageBuild.e2e.suite`, the same way an asset
24
24
  * transform or a manifest-flags module is named: the repository's code, the
25
25
  * toolchain's plumbing.
@@ -685,7 +685,7 @@ export function findExecutable(name, { cwd, env = process.env } = {}) {
685
685
  *
686
686
  * Asked twice per run, and the second asking is the point: an install running
687
687
  * alongside the suite can take the runner out from under it mid-flight, which
688
- * is precisely the failure that reported itself as green (#153).
688
+ * is precisely the failure that reported itself as green.
689
689
  *
690
690
  * @param {object} opts
691
691
  * @param {readonly string[]} opts.command - The program and its arguments.
@@ -766,7 +766,7 @@ export function freshResults({ paths, since, cwd }) {
766
766
  * exit status on its own cannot call a run green, because every way of stopping
767
767
  * a runner before it starts — a corrupt install, a missing browser, a killed
768
768
  * process, the concurrent `npm ci` that surfaced this — produces a run that
769
- * executed nothing, and nothing is not a pass (#153).
769
+ * executed nothing, and nothing is not a pass.
770
770
  *
771
771
  * This can only ever make a verdict worse. A suite that failed keeps its own
772
772
  * status; a suite that passed on no evidence loses the claim. Never the other
@@ -814,7 +814,7 @@ export function suiteVerdict({ status, vanished = [], declared = [], fresh = []
814
814
  * `MODULE_NOT_FOUND` naming nothing relevant.
815
815
  *
816
816
  * The suite is bracketed by checks rather than trusted on its exit status,
817
- * because a run that never started used to report as green (#153):
817
+ * because a run that never started used to report as green:
818
818
  *
819
819
  * - **Before.** Every executable the command needs is resolved, and a missing
820
820
  * one is an error naming it — rather than a container stood up, a world