@heroiclands/package-build 5.0.0 → 6.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,508 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 6.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - dc424d7: **`content-build addresses diff` reports every published address a build has
8
+ stopped publishing, and tells a rename from a removal** (#66).
9
+
10
+ A package's `(type, shortcode)` addresses are a published interface — every
11
+ satellite declaring `itemCatalog: true` assembles its beings out of them — but
12
+ nothing compared one release's addresses against the next, so renaming a
13
+ shortcode cost nothing and produced no signal. The check that got made instead
14
+ was a repository-local grep, which cannot see the other repositories and reports
15
+ the reassuring answer. `sohl` renamed `weapongear:Tabri` to `weapongear:Taburi`
16
+ two days after the `v0.8.2` tag both satellites pin, stating that nothing
17
+ referenced the old value; five lookups across two repositories do, and they fail
18
+ the moment either pin moves.
19
+
20
+ ```bash
21
+ gh release download v0.8.2 -p system.zip -D build/baseline
22
+ npx content-build addresses diff --from build/baseline/system.zip
23
+ ```
24
+
25
+ Run against `sohl` today that reports 20 findings — 8 renames and 12
26
+ withdrawals — including the `Tabri` one, at the line of the note that made it:
27
+
28
+ ```text
29
+ assets/content/Weapons/Melee/Taburi.md:12:1: warning: since sohl@0.8.2, weapongear:Tabri is no longer published; the same document (s5D6QJbw7ZbETxdN) is now published as weapongear:Taburi. Every package that resolves weapongear:Tabri breaks when it moves past sohl@0.8.2
30
+ ```
31
+
32
+ **A rename is told from a removal by the document id, which is an identity match
33
+ rather than an inference.** A note authors its `_id` in frontmatter and it is not
34
+ derived from the shortcode, so it survives a rename. Where the id is published
35
+ under no address at all, the finding says only that — _withdrawn_, naming no
36
+ successor. A split, a deletion and a merge are indistinguishable at that point,
37
+ and a "did you mean" guessed from string similarity would send the reader to the
38
+ wrong fix.
39
+
40
+ Both findings are warnings and neither fails a build: retiring content is
41
+ legitimate, and so is renaming — the shortcode charset rule forces some. What a
42
+ rename must not do is happen in silence. `--strict` reports both as errors and
43
+ exits non-zero, for a release workflow that wants a gate.
44
+
45
+ Additive: nothing runs unless the command is invoked, and no existing behaviour
46
+ changed.
47
+ - f28f653: A section can now describe itself: `site.sections` and `site.readmeSections`
48
+ take a `description`, and it reaches the generated `_index.md` (#91).
49
+
50
+ A generated section landing is the only place a section can speak — a content
51
+ package authors no `_index.md` for `weapongear` or `affliction`, so the file the
52
+ theme reads is the one this build writes. Its vocabulary was two keys, and
53
+ `partials/hero-banner.html` has always rendered `description` as the hero
54
+ standfirst, so every generated landing rendered a heading with no standfirst and
55
+ could not be given one by any consumer, at any level.
56
+
57
+ **The pair was the defect, not a passthrough.** The vocabulary lived in two
58
+ places that had to be kept in step by hand: the schema that admits a key, and
59
+ the two writers that each transcribed `title` and `banner` by name. A key added
60
+ to the schema alone validated cleanly and then reached no page. So the writers
61
+ stop naming keys and emit what the section resolved to, `title` first, and the
62
+ schema is now the single place a section's vocabulary is decided.
63
+
64
+ **The bound stays.** An unrecognised key under a section is refused at config
65
+ load, by name, exactly as before — `site.sections.affliction.descrption is not a
66
+ recognized option (expected one of: title, banner, description)`. Passing
67
+ sections through unvalidated the way `site.landing` is was weighed and refused:
68
+ `landing` is written once, for the mount, in one landing template's vocabulary,
69
+ where a section entry is written fourteen to twenty times per build against a
70
+ contract every package shares. Unbounded, a mistyped `descrption:` would publish
71
+ into front matter, be read by nobody, and say nothing to anyone — which is the
72
+ failure this issue was filed about, one step downstream where no build can see
73
+ it.
74
+
75
+ Additive. `description` is optional and no consuming package declares one — none
76
+ could, since the loader has always refused it — so every generated landing is
77
+ byte-identical, verified across all six consuming packages.
78
+ - 43a9de4: Refuse `name`, `shortcode` and `id` on a `type: homepage` note (#53).
79
+
80
+ A note's URL derives from `name.full` and its identity from `(type, shortcode)`.
81
+ The homepage is the one page for which neither holds — it publishes at
82
+ `/<package>/`, fixed by the package id — so an author fluent in the conventions
83
+ writes them there expecting exactly what they do everywhere else, and gets none
84
+ of it. Until now `content-build lint` ignored all three: a homepage carrying
85
+ `shortcode`, `id` and `name.full` passed at exit 0.
86
+
87
+ **They were never inert, which is why ignoring them was the wrong answer.** A
88
+ `shortcode` puts the note in the address index and in the `dataview` link
89
+ universe, so `[[homepage-<shortcode>]]` resolves _green_ — to `homepage/<slug>/`,
90
+ an address derived from `name.full` and published by nothing, because a homepage
91
+ is written to `_index.md` at the package root. A build that reports a live link
92
+ to a 404 is worse than one that says nothing.
93
+
94
+ **The inflated address tally is the same defect, not a second one.** The same
95
+ note was counted as an address it does not publish, so the lint and the link
96
+ manifest disagreed about what the package ships: SoHL's tree reports
97
+ `1607 address(es) across 1607 note(s)` with a `shortcode` on its landing and
98
+ `1606 address(es) across 1607 note(s)` without one. The tally is only ever
99
+ printed on a clean run, so refusing the field is what makes the count honest —
100
+ `lintContentTree` needed no change, and got none.
101
+
102
+ **A named class, not an allow-list, and that boundary is the decision.** An
103
+ unknown top-level key is deliberately still accepted. A homepage's frontmatter is
104
+ emitted into the published page, so an unrecognised key is a Hugo or theme
105
+ parameter this build has never heard of and has no standing to reject, and a
106
+ closed list would make every new theme parameter wait on a package-build release.
107
+ What is refused is the class that makes a false claim about _where this page is_.
108
+ `aliases` is not in it either — it is already dropped from every emitted page, so
109
+ authoring one here is the same no-op it is anywhere else. This departs from the
110
+ issue's third acceptance criterion, deliberately: over-strictness here breaks
111
+ authoring on a page whose whole frontmatter is pass-through.
112
+
113
+ **Where it fires: `content-build lint` only.** This is a frontmatter-schema rule,
114
+ and `content-build site` runs none of them; wiring one type's field rule in there
115
+ would have the site build refuse `shortcode` on a homepage while accepting
116
+ `weight: heavy` on a weapon. The gap that leaves is `HarnMaster-3-FoundryVTT`,
117
+ which runs no `content-build lint` at all and so receives no frontmatter finding
118
+ of any kind — a missing script in that repository rather than a rule to duplicate
119
+ one at a time.
120
+
121
+ Each finding is located at the offending key and says what the field would have
122
+ decided:
123
+
124
+ ```text
125
+ assets/content/homepage.md:26:1: error: `shortcode` decides nothing on a `type: homepage` note: this page's address is the package's own, `/<package>/`, fixed by the package id. It is not ignored either — it puts the note in the address index, so `[[homepage-<shortcode>]]` resolves to a page the site build never writes. Delete it
126
+ ```
127
+
128
+ **Minor rather than major, measured rather than assumed.** A new hard error is
129
+ breaking only if it fails a previously-passing consumer. All six HeroicLands
130
+ content packages were linted at their default branch, before and after — `sohl`,
131
+ `hm3`, `thalorna`, `kethira`, `harnensemble` and `harnadventures` — and every one
132
+ produces byte-identical findings and the same exit code with the rule as without
133
+ it. None of the six authors any of the three fields on its homepage.
134
+ - d1166d0: Require exactly one `type: homepage` note per package (#52).
135
+
136
+ Every package is reachable at `/<package>/` and what a reader finds there is one
137
+ authored note in its content tree — but nothing required a package to have one,
138
+ so the failure mode of the whole arrangement was a package that builds green and
139
+ serves nothing at its own address.
140
+
141
+ **Zero and two are the same defect, at the same severity.** Neither is a warning,
142
+ because a build that proceeds past either publishes the wrong front page while
143
+ reporting success — which is exactly what a warning tolerates.
144
+
145
+ | Count | What ships |
146
+ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
147
+ | **Zero** | Nothing at `/<package>/`, silently: the site build reports `wrote 0 homepage(s)` and exits 0. |
148
+ | **Two** | A page nobody chose. Both are written to the same `_index.md`, so the last one walked wins — the front page decided by _filename_, on a type whose point is frontmatter routing. |
149
+
150
+ **It fires in `content-build lint` and in `content-build site`, because neither
151
+ one reaches every package.** `HarnMaster-3-FoundryVTT` runs `site` and no `lint`;
152
+ `sohl-thalorna` runs `lint` and its own site builder. A rule in one of them is a
153
+ rule two of the six packages do not have. Both call the same function, so this is
154
+ one rule with two call sites rather than two rules that can drift. In the site
155
+ build it runs _before the output tree is cleared_, so a failing gate cannot
156
+ destroy a good site to report a bad tree.
157
+
158
+ **It does not vary by `publish.site`.** That setting chooses whether the
159
+ _content_ surfaces are published; the homepage is the floor beneath both modes.
160
+ The lint call site reads no `site:` block at all, so it could not vary by mode
161
+ even if the rule wanted to.
162
+
163
+ **Zero has no file to name, and none is invented.** The locator is the content
164
+ root — a real path, and the directory the note has to be added to — with no line
165
+ and no column, as the diagnostic rules require. Two is reported once per note,
166
+ located at its own `type:` value and naming the other, because each note is a
167
+ place an author has to open and edit:
168
+
169
+ ```text
170
+ assets/content: error: holds no `type: homepage` note, so package "sohl" publishes nothing at its own address /sohl/ — a package's front page is one authored note in this tree, routed by `type:` rather than by filename
171
+ assets/content/homepage.md:3:7: error: duplicate `type: homepage` note, also declared by assets/content/Landing.md; a package has one front page, at /sohl/, and every homepage is written to the same `_index.md` — so the one the walk reaches last silently overwrites the rest
172
+ ```
173
+
174
+ **Minor rather than major, measured rather than assumed.** A new hard error is
175
+ breaking only if it fails a previously-passing consumer. All six HeroicLands
176
+ content packages were run at their default branch, before and after: `sohl`,
177
+ `hm3`, `thalorna`, `kethira`, `harnensemble` and `harnadventures` each carry
178
+ exactly one homepage note, and every one produces byte-identical findings and the
179
+ same exit code with the check as without it.
180
+
181
+ The issue's sequencing — ship it inert, flip it to an error later — was written
182
+ when the count was ~45 repositories and two homepages existed only in unmerged
183
+ pull requests. Both have since merged, the real count is six, and every one
184
+ passes today, so the warning window would protect nobody and the flip would be a
185
+ second pull request for no reason.
186
+
187
+ ## 6.0.0
188
+
189
+ ### Major Changes
190
+
191
+ - 5538fdf: Check `packFolders` against the derived `packs[]` (#81).
192
+
193
+ `packageBuild.manifest.packFolders` is the one **declared** manifest key that
194
+ names something the build **derives**. Every other declared key states a fact
195
+ about the package (`title`, `socket`, `grid`) or addresses a staged file
196
+ (`esmodules`, `styles`, `languages`) — a staged file being a different relation,
197
+ answered against the stage rather than against configuration. Surveyed across
198
+ all six HeroicLands packages, no other declared key names a derived value, so
199
+ this is the only place the two halves could drift.
200
+
201
+ They did. `HarnMaster-3-FoundryVTT` shipped a folder naming `character`,
202
+ `possessions`, `esoteric` and `system-help`; three had not existed since its
203
+ compendium was consolidated into one `items` pack, and `items` — 1,577 of 1,597
204
+ documents — was named by no folder at all. Foundry rendered the folder holding
205
+ one journal pack with the entire item compendium loose beside it, and the build
206
+ reported nothing at any point (HarnMaster-3-FoundryVTT#420).
207
+
208
+ **Two findings, deliberately different severities**
209
+
210
+ | Finding | Severity | Why |
211
+ | ----------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------- |
212
+ | a folder names a pack the package does not ship | **error** | Foundry silently skips a name it cannot resolve, so the declaration does nothing; no arrangement intends it |
213
+ | a pack no folder names | **warning** | legal, and a root-level pack can be deliberate — but a package that declared a folder rarely meant to leave one out |
214
+ | no `packFolders` declared at all | nothing | everything at the root is an arrangement, not an omission |
215
+
216
+ Giving the two one severity gets one of them wrong: erroring on an ungrouped
217
+ pack fails working packages over a matter of taste, and warning on an
218
+ unresolvable name reproduces the defect this exists to catch.
219
+
220
+ The comparison descends through nested folders — Foundry's
221
+ `PackageCompendiumFolder` re-declares itself while `depth < 4` — so a nested
222
+ name is checked in both directions rather than being missed and then reported as
223
+ ungrouped.
224
+
225
+ **Reported where the reader can open it**
226
+
227
+ Findings carry the config key path of the offending scalar, which
228
+ `positionOfYamlPath` (new, in `engine/diagnostics.mjs`) resolves against the
229
+ configuration file. A position that cannot be established honestly — an `.mjs`
230
+ configuration, an unreadable file — is dropped rather than guessed, so the line
231
+ degrades from `file:line:column:` to `file:` to no locator:
232
+
233
+ ```text
234
+ package-build.config.yaml:164:23: error: packFolders: folder "HârnMaster 3 System" names pack "character", which this package does not ship (packs: items, system-help)
235
+ package-build.config.yaml:162:13: warning: packFolders: pack "items" is named by no folder, so it ships outside every folder this package declares
236
+ ```
237
+
238
+ An error **stops the write**: a manifest already known to describe packs that do
239
+ not exist should not reach the stage, where the next command would deploy it.
240
+
241
+ **Why major**
242
+
243
+ It newly fails a build that passes today. Measured against every real consumer
244
+ as each stands: `sohl`, `sohl-kethira-basic`, `harn-ensemble` and
245
+ `harn-adventures` are clean; `sohl-thalorna` warns once, for an `actors` pack no
246
+ folder names, and still builds; and `HarnMaster-3-FoundryVTT`'s `main` fails
247
+ with the three errors above, which is HarnMaster-3-FoundryVTT#420 — filed, and
248
+ fixed in its open PR #426. That is a real defect reported rather than
249
+ accommodated, but it is still a red build a consumer would meet on a blind
250
+ upgrade, so it takes the major.
251
+
252
+ Also new and exported: `packFolderFindings` (the rule, pure) from
253
+ `@heroiclands/package-build/manifest`, `positionOfYamlPath` from
254
+ `@heroiclands/package-build/engine/diagnostics`, and `packConfigPath` from
255
+ `@heroiclands/package-build/engine/pack-config` — the file `loadPackConfig`
256
+ actually read, so a finding about a configured value names it rather than
257
+ re-deriving a path free to disagree. `writeManifest` takes an optional
258
+ `configFile`; omitting it costs the position, not the finding.
259
+
260
+ ### Minor Changes
261
+
262
+ - 46e0c10: Check the package homepage's own links (#54).
263
+
264
+ The homepage is the page a reader arrives at, and it was the one page nothing
265
+ checked. Every other note addresses the corpus with wikilinks, which
266
+ `content-build links` resolves; a landing addresses the web the way the web does
267
+ — markdown links and `landing:` `url` / `href` fields — and none of those went
268
+ through a checker at all. SoHL's landing pointed at `kb/creature/` and
269
+ `kb/character/` from the day those two types merged into `being`: two 404s on
270
+ the package's front page, surviving every build, found only by a person reading
271
+ the page.
272
+
273
+ **Both halves of the note are in scope, and the real pages are why.** Of the six
274
+ homepages authored today, four carry every link in the body as ordinary markdown
275
+ and two carry them in `landing:` front matter — and the one whose dead links
276
+ prompted this has an _empty body_. A body-only check would have found nothing on
277
+ the page it was written for. So `landing.install.url`, every card and card-link
278
+ `url` / `href`, the markdown links inside the prose fields (`lead`, `closing`,
279
+ `install.intro`, `install.note`, a card's `description`, a link's `note`) and the
280
+ body's own markdown links are all read.
281
+
282
+ **`url` and `href` are not the same address.** The theme resolves a `url`
283
+ against the site with `relURL`, so a package writes `kb/rules/` and is served
284
+ `/sohl/kb/rules/` without naming its own prefix; an `href` is an address that is
285
+ _already_ resolved and is used verbatim, which is what `cards.source: sections`
286
+ fills in. A leading `/` is therefore a defect in a `url` — Hugo prefixes it a
287
+ second time — and correct in an `href`, so the two are not checked the same way.
288
+
289
+ **What is reported**, each finding naming the form to write instead:
290
+
291
+ | Finding | Why |
292
+ | ---------------------------- | -------------------------------------------------------------------------- |
293
+ | A **retired content type** | `kb/creature/` after `creature` became `being` — the engine knows. |
294
+ | A **hardcoded absolute URL** | Into this package's own prefix, or into one a vendored manifest names. |
295
+ | A **root-relative `url:`** | `relURL` prefixes it again. `href:` is exempt — verbatim is what it means. |
296
+ | A **wikilink** | Nothing resolves one here: a homepage is published verbatim in every mode. |
297
+
298
+ That last one settles a question rather than deferring it. A homepage does
299
+ **not** get the wikilink resolution every other note body gets, because in
300
+ `homepage` mode the content tree is never walked — there is no index for a
301
+ wikilink to resolve against, and giving the page one would make the mode depend
302
+ on exactly the machinery its licensing fence exists to not build. A wikilink on
303
+ a landing is therefore reported, not resolved.
304
+
305
+ **What is deliberately not attempted.** Whether an external URL answers: there is
306
+ no network at build time and a build must not go red because a third party is
307
+ down. And whether a live in-site address names a page that exists: several
308
+ surfaces a landing routes to are produced by other tools entirely — generated API
309
+ documentation, hand-authored Hugo sections — so this build does not hold the set
310
+ of published pages and would report a working link as dead. A bare
311
+ `https://www.heroiclands.org/<package>/` is left alone for the same reason it
312
+ cannot be improved: a package homepage is in no link manifest, so there is no
313
+ better form to write.
314
+
315
+ **Minor rather than major, measured rather than assumed.** A new lint error that
316
+ fails a previously-passing consumer would be breaking. All six HeroicLands
317
+ content packages were run against it — `sohl`, `hm3`, `thalorna`, `kethira`,
318
+ `harnensemble`, `harnadventures`, including the two homepages that exist only in
319
+ open pull requests — and every one is clean; the four whose trees are checked out
320
+ in full pass `links` end to end. Run against SoHL's landing as it stood _before_
321
+ the port, the check reports both dead links, at their line and column.
322
+
323
+ It rides in the existing pass rather than beside it: no new command, no second
324
+ walk, and a consumer that already runs `content-build links` gets it with no
325
+ change.
326
+ - d8ce7b3: Derive the compile order from what each pass reads, instead of trusting the
327
+ order `packs:` happens to declare (#73).
328
+
329
+ `generatePacksJson` ran its passes in declaration order, but the actors pass
330
+ resolves each being's embedded items against the **output** of the item passes —
331
+ the JSON under `build/packs-json/`, not the content tree. A package declaring its
332
+ Actor pack first therefore compiled only where an earlier run had already left
333
+ that directory populated: green on every local tree that had built once, exit 1
334
+ on a cold one, over a message naming a missing directory rather than the ordering
335
+ that caused it.
336
+
337
+ `build/` is gitignored, so **every fresh checkout and every CI runner is cold**.
338
+ `sohl-kethira-basic` shipped exactly that list and its release path was broken;
339
+ the failure had not fired only because an unrelated lint failure exited first.
340
+
341
+ **What changed**
342
+
343
+ - A compiler declares the document types whose compiled output it reads —
344
+ `static readsPackOutputOf` on `BasePackCompiler`, `["Item"]` on `Actors`. A
345
+ consumer registering a compiler of its own declares its dependencies the same
346
+ way.
347
+ - `orderPassesByDependency` (exported from `engine/generate.mjs`) schedules each
348
+ pass after **every** pack of every type it names — a being addresses an item by
349
+ `(type, shortcode)` without knowing which Item pack ships it, so waiting for
350
+ one of several would resolve some beings and silently fail others. The
351
+ reordering is the smallest one that works: the earliest declared pass whose
352
+ dependencies have all run goes next, so a list already in a workable order is
353
+ compiled exactly as declared. The build logs the derived order only when it
354
+ differs from the declared one.
355
+ - **The declared list is untouched**, which is the point: it is also the
356
+ manifest's `packs` array, and a consumer orders that for a reader browsing
357
+ compendiums. The two are now allowed to disagree, so fixing a cold build no
358
+ longer means reordering the shipped manifest away from its `packFolders`.
359
+ - The case ordering cannot answer — `content-build package compile <name>`, which
360
+ runs one pass and no other — is now reported in this project's diagnostic form,
361
+ naming the pack that waits, the pack it waits on and the fix, instead of
362
+ throwing about a directory:
363
+
364
+ ```text
365
+ error: pack "characters" (Actor) reads the compiled output of the Item pack
366
+ "characteristics", which this run does not compile and which
367
+ build/packs-json/characteristics does not hold — compile the whole
368
+ package, or compile "characteristics" first
369
+ ```
370
+
371
+ **Bump**
372
+
373
+ _Minor, not patch, and not major._ Minor because it adds public surface: two
374
+ exports on `engine/generate.mjs` and a third documented static switch on
375
+ `BasePackCompiler`, which is the registration point for a consumer's own
376
+ compiler.
377
+
378
+ **No previously-passing consumer build starts failing.** The change is strictly
379
+ permissive — configurations that failed now succeed, and configurations that
380
+ succeeded compile the same documents. Of the four HeroicLands packages, three
381
+ (`sohl`, `sohl-thalorna`, `HarnMaster-3-FoundryVTT`) declare an order the
382
+ derivation returns unchanged; `sohl-kethira-basic` is the one that moves, and its
383
+ `build/packs-json` is **byte-identical** across the two orders — 385 documents,
384
+ `diff -r` exit 0. The new single-pack diagnostic replaces a throw on exactly the
385
+ runs that already failed.
386
+ - f716902: Stop emitting `isEquipped` on a compiled gear item (#68).
387
+
388
+ `GEAR_COMMON` emitted `isEquipped: false` on every gear item, and no SoHL
389
+ DataModel declares the field. `GearDataModel` declares `isCarried`,
390
+ `containerId` and `sharedWithCohortIds` as its possession state, and nothing
391
+ else — Foundry discards the extra key when the document is constructed, so
392
+ every gear item in every consuming pack shipped a value that was thrown away at
393
+ load, with nothing at compile or load time saying so.
394
+
395
+ `GEAR_COMMON` is spread into all six gear types (`armorgear`, `concoctiongear`,
396
+ `containergear`, `miscgear`, `projectilegear`, `weapongear`), so this reached
397
+ every gear item of every consuming package.
398
+
399
+ This is the second instance of #35's defect and has the same root cause:
400
+ nothing compares a builder's emitted `system` block against the DataModel that
401
+ receives it. The general check is #60.
402
+
403
+ **The field was retired, not renamed.**
404
+ Song-of-Heroic-Lands-FoundryVTT#662 made the worn/equipped concept armour-only:
405
+ it removed `system.isEquipped` from the shared gear data model and gave
406
+ `ArmorGearDataModel` its own `system.isWorn`. That shipped in SoHL 0.8.0, so no
407
+ released system has read the key since. `isWorn` belongs to armour alone and is
408
+ not a target this declaration can be retargeted at — whether an `armorgear`
409
+ note should be able to author one is a separate content question, left open on
410
+ #68.
411
+
412
+ **Nothing to sweep.** Unlike `assocMysteryCode`, this was never authorable: the
413
+ declaration carried a `to` and a `value` but no `name`, so `readField` never
414
+ consulted the frontmatter and no note in any package could set it. It was
415
+ already absent from `authoredFields`, so the author-facing field reference is
416
+ unchanged. A consumer has no line to delete.
417
+
418
+ **This changes emitted documents**, so a consumer wants a rebuild rather than a
419
+ silent upgrade — though nothing downstream can have depended on the value.
420
+ Verified by recompiling two consumer trees at `main` before and after, comparing
421
+ every emitted document key-ordered:
422
+
423
+ - `sohl` — removes exactly 1019 `"isEquipped": false` keys from 1012 of its 3126
424
+ compiled documents: 1010 items (465 `miscgear`, 331 `armorgear`, 114
425
+ `containergear`, 82 `weapongear`, 18 `projectilegear`) and 9 more embedded in
426
+ gear-carrying actors. No other difference, and no document added or removed.
427
+ - `sohl-thalorna` — removes 97 of 2018 across 2555 documents (96 items: 71
428
+ `concoctiongear`, 25 `weapongear`). No other difference.
429
+
430
+ **A consumer that embeds a foreign item catalogue keeps the key until its
431
+ upstream republishes.** The 1921 `sohl-thalorna` occurrences this does not
432
+ remove are not emitted by this build at all: they are inherited verbatim from
433
+ the pinned `sohl@0.8.2` release pack its actors resolve against, which was
434
+ compiled by an earlier package-build. They clear when `sohl` cuts a release
435
+ built with this version, not before — so a consumer grepping its own output
436
+ after upgrading should expect the catalogue's share to remain.
437
+ - 07944a2: Resolve the `sohlKb` TypeDoc symbol map against the repository root, and stop
438
+ swallowing every failure to read it (#75).
439
+
440
+ `site.passOptions.symbolMap` is authored repo-relative, but `readSymbolMap` read
441
+ it against the process cwd and wrapped the read in a bare `catch` that returned
442
+ `{}`. A missing file, a malformed one, a permissions error, a path typo and a
443
+ correctly configured build with no symbols were all indistinguishable — and the
444
+ build exited 0 either way, publishing every `{@link}` as a code span instead of
445
+ a link into the API documentation. Driving `content-build site` from outside the
446
+ tree through `PACKAGE_BUILD_CONFIG` — how #51 was verified — silently dropped
447
+ 224 API links across 25 pages of the `sohl` knowledgebase, and nothing at any
448
+ stage reported it.
449
+
450
+ **What changed**
451
+
452
+ | State | Before | Now |
453
+ | -------------------------------------- | -------------------- | -------------------------------------------- |
454
+ | `symbolMap` unset | `{}`, silent | `{}`, silent — unchanged |
455
+ | Configured, readable | works from repo root | works from **any** directory |
456
+ | Configured, missing / unreadable | `{}`, exit 0 | build fails, naming the path and `errno` |
457
+ | Configured, malformed JSON | `{}`, exit 0 | build fails, naming the path and the JSON |
458
+ | Configured, JSON that is not an object | `{}`, exit 0 | build fails, naming the path |
459
+ | Configured, read | nothing | `resolved N API symbols from <path>` at info |
460
+
461
+ The count is reported because a map that loaded and a map that loaded _empty_
462
+ are otherwise indistinguishable without reading the emitted HTML, and an empty
463
+ one degrades every tag exactly as a missing one used to.
464
+
465
+ **Bump**
466
+
467
+ _Minor, not patch._ No key, export, or flag changed shape, and a consumer whose
468
+ map is where its configuration says it is sees only the new info line. But a
469
+ build that previously exited 0 can now fail — deliberately — which reverses the
470
+ module's own documented licence to run the knowledgebase before `npm run docs`
471
+ and publish degraded tags. A consumer that orders its pipeline that way must
472
+ either generate the map first or leave `symbolMap` unset. No known consumer is
473
+ affected: `Song-of-Heroic-Lands-FoundryVTT` commits `kb/data/api-symbols.json`.
474
+
475
+ ### Patch Changes
476
+
477
+ - bb08713: Stop `content-build lint` failing a homepage-only content tree (#77).
478
+
479
+ A package in `publish.site: homepage` mode may hold exactly one note, and a
480
+ homepage carries no `shortcode` **by design** — it is addressed by the package
481
+ rather than by a slug, so `HOMEPAGE_FIELDS` is empty. The vacuous-tree guard
482
+ keyed off the address map, so that tree produced no keys and was reported as a
483
+ missing checkout:
484
+
485
+ ```text
486
+ assets/content: error: holds no keyed content, so every rule here is vacuous — check that the content tree is present and that this is its root
487
+ ```
488
+
489
+ The tree was present, it was the root, and it held the one note the package is
490
+ meant to have. `harn-adventures` and `sohl-kethira-basic` both ship in that
491
+ mode, so for them the failure was permanent — and an expected failure trains its
492
+ author to stop reading the output, which is the one thing this guard needs them
493
+ to do.
494
+
495
+ The guard now reports an **empty walk** rather than an empty key set: a tree
496
+ holding notes is a tree, whatever they are keyed on, and only a tree holding
497
+ none is the absent one. Nothing about its strength changes — an empty tree, a
498
+ tree of untyped scaffolding, and a path that is not the content root each still
499
+ fail with the same diagnostic, now worded "holds no content notes".
500
+
501
+ `patch`, not `minor`: no tree that lints today reports anything different. The
502
+ only behaviour that changes is a false failure becoming a pass. The success line
503
+ gains its missing noun — `(0 address(es) across 1 note(s))` — since a
504
+ homepage-only pass is the first time it prints a zero.
505
+
3
506
  ## 5.0.0
4
507
 
5
508
  ### Major Changes