@heroiclands/package-build 20.4.0 → 20.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +288 -0
  2. package/CONTENT.md +213 -20
  3. package/README.md +19 -1
  4. package/bin/content-build.mjs +135 -32
  5. package/bin/package-build.mjs +46 -13
  6. package/content-config.mjs +345 -101
  7. package/docs/api.md +1352 -0
  8. package/docs/commands.md +1609 -0
  9. package/docs/configuration.md +1432 -0
  10. package/docs/content-format.md +16 -6
  11. package/docs/diagnostics.md +356 -0
  12. package/docs/getting-started.md +813 -0
  13. package/docs/project-setup.md +469 -0
  14. package/engine/actor-compiler.mjs +30 -27
  15. package/engine/address-diff.mjs +45 -41
  16. package/engine/base-compiler.mjs +6 -0
  17. package/engine/bundles.mjs +9 -0
  18. package/engine/content-address.mjs +9 -9
  19. package/engine/content-index.mjs +44 -23
  20. package/engine/content-links.mjs +44 -11
  21. package/engine/content-lint.mjs +44 -10
  22. package/engine/content-tables.mjs +32 -27
  23. package/engine/folder-notes.mjs +4 -2
  24. package/engine/frontmatter-lint.mjs +35 -38
  25. package/engine/generate.mjs +5 -0
  26. package/engine/helpers.mjs +86 -32
  27. package/engine/index.mjs +12 -2
  28. package/engine/journals.mjs +9 -0
  29. package/engine/note-claims.mjs +18 -10
  30. package/engine/note-schemas.mjs +0 -5
  31. package/engine/note-vocabulary.mjs +32 -31
  32. package/engine/pack-config.mjs +26 -12
  33. package/engine/pack-router.mjs +0 -0
  34. package/engine/pdf-build.mjs +464 -0
  35. package/engine/pdf-fonts.mjs +420 -0
  36. package/engine/pdf-render.mjs +876 -0
  37. package/engine/pdf-toc.mjs +525 -0
  38. package/engine/scenes.mjs +14 -5
  39. package/engine/schema-check.mjs +1 -1
  40. package/engine/site-build.mjs +21 -3
  41. package/engine/web-wikilinks.mjs +6 -3
  42. package/engine/wikilinks.mjs +2 -4
  43. package/hm3/actors.mjs +8 -0
  44. package/hm3/items.mjs +8 -0
  45. package/package.json +1 -1
  46. package/release.mjs +63 -3
  47. package/sohl/actors.mjs +8 -0
  48. package/sohl/items.mjs +8 -0
  49. package/sohl/note-schemas.mjs +5 -5
  50. package/types/content-config.d.mts +66 -15
  51. package/types/engine/actor-compiler.d.mts +34 -30
  52. package/types/engine/address-diff.d.mts +57 -3
  53. package/types/engine/base-compiler.d.mts +10 -2
  54. package/types/engine/bundles.d.mts +9 -0
  55. package/types/engine/content-address.d.mts +9 -9
  56. package/types/engine/content-index.d.mts +57 -13
  57. package/types/engine/content-lint.d.mts +6 -4
  58. package/types/engine/content-tables.d.mts +49 -18
  59. package/types/engine/frontmatter-lint.d.mts +3 -2
  60. package/types/engine/helpers.d.mts +105 -31
  61. package/types/engine/index.d.mts +4 -0
  62. package/types/engine/journals.d.mts +9 -0
  63. package/types/engine/note-claims.d.mts +17 -10
  64. package/types/engine/note-vocabulary.d.mts +23 -196
  65. package/types/engine/pack-config.d.mts +4 -4
  66. package/types/engine/pdf-build.d.mts +42 -0
  67. package/types/engine/pdf-fonts.d.mts +30 -0
  68. package/types/engine/pdf-render.d.mts +156 -0
  69. package/types/engine/pdf-toc.d.mts +114 -0
  70. package/types/engine/scenes.d.mts +10 -1
  71. package/types/engine/schema-check.d.mts +2 -2
  72. package/types/engine/site-build.d.mts +34 -6
  73. package/types/engine/wikilinks.d.mts +2 -3
  74. package/types/hm3/actors.d.mts +8 -0
  75. package/types/hm3/items.d.mts +8 -0
  76. package/types/release.d.mts +15 -4
  77. package/types/sohl/actors.d.mts +10 -2
  78. package/types/sohl/items.d.mts +8 -0
@@ -0,0 +1,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.
@@ -576,33 +576,6 @@ export class SystemActorCompiler extends BasePackCompiler {
576
576
  return referencedSubtype(this.documentSubtypes, type, "Item");
577
577
  }
578
578
 
579
- /**
580
- * Resolve one embedded item from a `(type, shortcode?, overlay)`
581
- * descriptor. If `shortcode` is given, the predefined item is fetched
582
- * from `itemsMap` and the overlay deep-merged on top. If absent, the
583
- * descriptor must carry enough fields to stand alone. The embedded
584
- * item's `_id` is regenerated deterministically from
585
- * `(actorId, subType, shortcode, indexKey)` so re-exports are stable —
586
- * from the **document subtype**, so that renaming a note type leaves
587
- * every embedded id exactly where it was.
588
- * Returns null if the descriptor cannot be resolved.
589
- *
590
- * @param {Map<string, object>} itemsMap - The predefined items, by address.
591
- * @param {string} actorId - The owning actor's id, seeding embedded ids.
592
- * @param {string} type - The **note** type the reference names.
593
- * @param {string|null} shortcode - The referenced item's shortcode, or
594
- * `null` for a stand-alone entry.
595
- * @param {object} [overlay] - The entry's remaining properties.
596
- * @param {string} indexKey - Where the reference sits, for a diagnostic.
597
- * It no longer reaches the id — it names the entry in a message.
598
- * @param {string} ctx - Diagnostic context (the actor's label).
599
- * @param {object} [at] - Where to locate a finding.
600
- * @param {string} [at.fmKey] - The frontmatter key the reference sits
601
- * under, so an unresolved one is reported at the reference rather than
602
- * at the note.
603
- * @returns {object|null} The embedded item, or null when it resolved to
604
- * nothing — always with a finding emitted.
605
- */
606
579
  /**
607
580
  * Read an entry's `model:` — the address of the item it is a copy of.
608
581
  *
@@ -653,6 +626,36 @@ export class SystemActorCompiler extends BasePackCompiler {
653
626
  return { type: read.type, shortcode: read.shortcode, package: read.package ?? null };
654
627
  }
655
628
 
629
+ /**
630
+ * Resolve one embedded item from a `(type, shortcode?, overlay)`
631
+ * descriptor. If `shortcode` is given, the predefined item is fetched
632
+ * from `itemsMap` and the overlay deep-merged on top. If absent, the
633
+ * descriptor must carry enough fields to stand alone. The embedded
634
+ * item's `_id` is regenerated deterministically from
635
+ * `(actorId, subType, shortcode, indexKey)` so re-exports are stable —
636
+ * from the **document subtype**, so that renaming a note type leaves
637
+ * every embedded id exactly where it was.
638
+ * Returns null if the descriptor cannot be resolved.
639
+ *
640
+ * @param {Map<string, object>} itemsMap - The predefined items, by address.
641
+ * @param {string} actorId - The owning actor's id, seeding embedded ids.
642
+ * @param {string} type - The **note** type the reference names.
643
+ * @param {string|null} shortcode - The referenced item's shortcode, or
644
+ * `null` for a stand-alone entry.
645
+ * @param {object} overlay - The entry's remaining properties.
646
+ * @param {string} indexKey - Where the reference sits, for a diagnostic.
647
+ * It names the entry in a message and does not reach the id.
648
+ * @param {string} ctx - Diagnostic context (the actor's label).
649
+ * @param {object} [at] - Where to locate a finding.
650
+ * @param {string} [at.fmKey] - The frontmatter key the reference sits
651
+ * under, so an unresolved one is reported at the reference rather than
652
+ * at the note.
653
+ * @param {string|null} [at.modelPackage] - The package a `model:` names,
654
+ * where the template comes from another package's catalogue. Given one,
655
+ * the packaged address is what resolves and nothing local shadows it.
656
+ * @returns {object|null} The embedded item, or null when it resolved to
657
+ * nothing — always with a finding emitted.
658
+ */
656
659
  resolveEmbedded(
657
660
  itemsMap,
658
661
  actorId,
@@ -173,47 +173,6 @@ export function readItemAddresses(dirs) {
173
173
  return space;
174
174
  }
175
175
 
176
- /**
177
- * Every rename the tree's notes **declare**, as old address → where it went.
178
- *
179
- * Read from the content tree rather than from compiled output, because a
180
- * declaration is authored and the compiled document does not carry it: nothing
181
- * downstream consumes `renamedFrom:`, so emitting it into every pack to let one
182
- * diagnostic read it back would put a build-time note in shipped data forever.
183
- * The tree is already read by this module for the same reason
184
- * ({@link noteFilesById}) — to place a finding where its author can fix it.
185
- *
186
- * **A declaration is keyed by document subtype, not by note type.** The address
187
- * space is the one consumers resolve against, and it is spelled in compiled
188
- * documents: `hm3` compiles a `projectile` note into a `missilegear` item,
189
- * so that is the address a rename of it moves. {@link referencedSubtype} is the
190
- * function that already answers this for a being's embedded `(type, shortcode)`
191
- * references, so both sides read the same rule rather than a second copy of it.
192
- *
193
- * **An entry is emitted for every system that maps the type**, whether or not
194
- * the note declares that system's block. Over-emitting is inert — the diff uses
195
- * an entry only when the baseline published the old address *and* this build
196
- * publishes the new one, and a system the note does not compile for satisfies
197
- * neither — while asking which blocks a note declares would put a second,
198
- * subtly different answer to that question in a third place.
199
- *
200
- * **First claim wins on a collision.** Two notes naming one predecessor is a
201
- * contradiction — an address has one successor — and it is reported as an error
202
- * by `engine/content-lint.mjs`, where both notes are in hand and can both be
203
- * named. Picking one here keeps this a map; it is not a resolution, and nothing
204
- * rests on which one it picked.
205
- *
206
- * @param {string} contentBase - Root of the content tree.
207
- * @param {object} opts
208
- * @param {readonly string[]} [opts.skipDirectories] - The corpus scope. Stated
209
- * by the caller, never defaulted — see {@link addressCorpus}.
210
- * @param {readonly object[]} [opts.maps] - The document-subtype maps.
211
- * @param {object} [opts.config] - The resolved build configuration.
212
- * @param {readonly object[]} [opts.records] - Index records the caller already
213
- * derived, shared with {@link noteFilesById} so one command reads one corpus.
214
- * @returns {Map<string, {to: string, file: string, shortcode: string}>} Old
215
- * address → the address the declaring note publishes at now, and that note.
216
- */
217
176
  /**
218
177
  * The corpus both reads below share, as content-index records.
219
178
  *
@@ -258,6 +217,49 @@ function addressCorpus(contentBase, { skipDirectories, config, records, problems
258
217
  return indexRecordsFor({ contentBase, config, skipDirectories, problems });
259
218
  }
260
219
 
220
+ /**
221
+ * Every rename the tree's notes **declare**, as old address → where it went.
222
+ *
223
+ * Read from the content tree rather than from compiled output, because a
224
+ * declaration is authored and the compiled document does not carry it: nothing
225
+ * downstream consumes `renamedFrom:`, so emitting it into every pack to let one
226
+ * diagnostic read it back would put a build-time note in shipped data forever.
227
+ * The tree is already read by this module for the same reason
228
+ * ({@link noteFilesById}) — to place a finding where its author can fix it.
229
+ *
230
+ * **A declaration is keyed by document subtype, not by note type.** The address
231
+ * space is the one consumers resolve against, and it is spelled in compiled
232
+ * documents: `hm3` compiles a `projectile` note into a `missilegear` item,
233
+ * so that is the address a rename of it moves. {@link referencedSubtype} is the
234
+ * function that already answers this for a being's embedded `(type, shortcode)`
235
+ * references, so both sides read the same rule rather than a second copy of it.
236
+ *
237
+ * **An entry is emitted for every system that maps the type**, whether or not
238
+ * the note declares that system's block. Over-emitting is inert — the diff uses
239
+ * an entry only when the baseline published the old address *and* this build
240
+ * publishes the new one, and a system the note does not compile for satisfies
241
+ * neither — while asking which blocks a note declares would put a second,
242
+ * subtly different answer to that question in a third place.
243
+ *
244
+ * **First claim wins on a collision.** Two notes naming one predecessor is a
245
+ * contradiction — an address has one successor — and it is reported as an error
246
+ * by `engine/content-lint.mjs`, where both notes are in hand and can both be
247
+ * named. Picking one here keeps this a map; it is not a resolution, and nothing
248
+ * rests on which one it picked.
249
+ *
250
+ * @param {string} contentBase - Root of the content tree.
251
+ * @param {object} opts
252
+ * @param {readonly string[]} [opts.skipDirectories] - The corpus scope. Stated
253
+ * by the caller, never defaulted — see {@link addressCorpus}.
254
+ * @param {readonly object[]} [opts.maps] - The document-subtype maps.
255
+ * @param {object} [opts.config] - The resolved build configuration.
256
+ * @param {readonly object[]} [opts.records] - Index records the caller already
257
+ * derived, shared with {@link noteFilesById} so one command reads one corpus.
258
+ * @param {object[]} [opts.problems] - Collects the notes the index cannot
259
+ * record, so one of them does not abort the diff before it reports.
260
+ * @returns {Map<string, {to: string, file: string, shortcode: string}>} Old
261
+ * address → the address the declaring note publishes at now, and that note.
262
+ */
261
263
  export function declaredPredecessors(
262
264
  contentBase,
263
265
  { skipDirectories, maps = KNOWN_DOCUMENT_SUBTYPE_MAPS, config, records, problems } = {},
@@ -394,6 +396,8 @@ export function diffItemAddresses(baseline, current, { baseline: label, predeces
394
396
  * id is derived against. See {@link addressCorpus} for why that matters.
395
397
  * @param {readonly object[]} [opts.records] - Index records the caller already
396
398
  * derived, shared with {@link declaredPredecessors}.
399
+ * @param {object[]} [opts.problems] - Collects the notes the index cannot
400
+ * record, so one of them does not abort the diff before it reports.
397
401
  * @returns {Map<string, string>} Document id → the note's absolute path.
398
402
  */
399
403
  export function noteFilesById(contentBase, { skipDirectories, config, records, problems } = {}) {
@@ -303,11 +303,17 @@ export class BasePackCompiler {
303
303
  * Resolves a `packFolder` — a folder note's address — to the Foundry
304
304
  * folder id it materialises as in this pack.
305
305
  * @param {string} [options.packName] - The pack this pass writes.
306
+ * @param {string|null} [options.packSystem] - The system that pack declares,
307
+ * which decides the `_stats` it stamps and the system block a note must
308
+ * carry to reach it. `null` for a pack that declares none.
306
309
  * @param {string} [options.docType] - The Foundry document type it holds.
307
310
  * @param {{resolve: Function}} [options.router] - The pack router. Omit it
308
311
  * — as the unit suite does — and every claimed note is compiled here.
309
312
  * @param {boolean} [options.routingReporter] - Whether this pass reports a
310
313
  * note of its type that routes nowhere.
314
+ * @param {object} [options.corpus] - The corpus this compile is running
315
+ * over, derived once and shared by every pass. A pass handed none derives
316
+ * its own in `prepare`, and reports that corpus's problems itself.
311
317
  */
312
318
  constructor({
313
319
  contentBase,