@heroiclands/package-build 13.0.0 → 15.0.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,366 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 15.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 5c16c02: Let a note's art path say "unset" and "blank on purpose" with two different values.
8
+
9
+ `resolveImg` opened with `if (!raw) return ""`, and every caller then applied its
10
+ own default to the result with `||` — `resolveImg(fm.img) || itemArt(type)`. So
11
+ `""`, `null` and an absent key were one case: all three compiled to the type's
12
+ default art, and a note had no way to say _ship no image_ at all.
13
+
14
+ They are now three values with two meanings, the convention the project already
15
+ holds for an optional "not specified" DataModel string (`nullable, initial: null`,
16
+ so "unset" is one honest value rather than two):
17
+
18
+ | a note writes | it means | it compiles with |
19
+ | -------------- | ------------------------------------ | ---------------- |
20
+ | nothing at all | _unset_ — name me no art | the type default |
21
+ | `img: null` | the same thing, said out loud | the type default |
22
+ | `img: ""` | _blank on purpose_ — I want no image | no image |
23
+
24
+ **`resolveImg` returns `string | null`.** `null` for an unset path, `""` for a
25
+ deliberate blank, the translation half unchanged. Every caller pairs its default
26
+ with **nullish** coalescing: `sohl/items.mjs`, three in `sohl/actors.mjs` (`img`,
27
+ `portrait`, and the prototype token's `texture.src`), and `engine/macros.mjs`.
28
+ Not `||` — that collapses a deliberate blank back into the default and takes the
29
+ distinction away again, and it does so silently, because `""` is falsy.
30
+ `itemArt()` is unaffected: a registry entry with no art throws before the
31
+ translation, so its result is never the unset case.
32
+
33
+ **The default-art seam is a documented extension point, so this is the substance
34
+ of the change, not a detail.** A consuming repository that pairs art with its own
35
+ `itemBuilders` entry, or calls `resolveImg` from a builder of its own, gets the
36
+ new reading of `""` whether or not it asked for it — which is why the sweeps have
37
+ to come first. `sohl-thalorna` swept forty-five `img: ""` notes ahead of this
38
+ release (HeroicLands/sohl-thalorna#134) and `sohl-kethira-basic` eleven
39
+ `portrait: ""` beings (HeroicLands/sohl-kethira-basic#81); `sohl` authors neither,
40
+ and its 3,125 compiled documents are byte-identical across the change.
41
+
42
+ **Both art fields, because both go through `resolveImg`.** A being carries `img`
43
+ (its token art) and `portrait` (its sheet portrait) independently; `portrait` is
44
+ not a variant spelling of `img`, and the rule belongs to the translator rather
45
+ than to one of the keys reaching it. This is not theoretical: `sohl-kethira-basic`
46
+ writes `portrait: ""` on eleven beings and `img: ""` on none, so a change — or a
47
+ guard — keyed on `img` alone would have called that tree clean and dropped every
48
+ one of those portraits.
49
+
50
+ **A warning for the old spelling.** The frontmatter lint reports `img: ""` and
51
+ `portrait: ""` — in either authoring position — as the meaning-change they are, on
52
+ the pattern the `package:` and retired-alias sweeps set: a warning, because the
53
+ note still compiles, to a document that is merely iconless.
54
+
55
+ **`title` is deliberately not on this rule.** It reads as a general rule about
56
+ optional strings and it is not: on a `type: affiliation` note `title` is
57
+ _simultaneously_ a declared item field whose default is `""`
58
+ (`sohl/item-fields.mjs`), resolved from the very same shared top-level key the
59
+ site emitter reads as the page title. `title: null` therefore does not fall back —
60
+ it stringifies, and the compiled document ships the literal `"null"`. A `title` a
61
+ note does not want is written by omitting the key; the emitter's `fm.title ?? name`
62
+ is already correct and is untouched. The lint guard is `img`'s alone for the same
63
+ reason.
64
+
65
+ Delivers the `img` half of #218. The `title` half — the collision above — stays
66
+ open.
67
+ - 63dfcae: **A note's own `title` no longer fills an affiliation's `system.title`** (#218).
68
+
69
+ The two were never the same quantity. A note's top-level `title` is _the title
70
+ of the note_ — the heading its page is published under, which the site emitter
71
+ reads. An `affiliation` item's `system.title` is _the style of address the office
72
+ carries_ — Ajaw, Warden, a person's style within the body. They collided only in
73
+ spelling, and field resolution's third step, the shared top-level property, fed
74
+ the second from the first.
75
+
76
+ That step also answers **without** applying the field's default — only the
77
+ in-block step does — so an authored `title: null` reached the `String()` coercion
78
+ unguarded and compiled to the literal string `"null"`. Fifteen `sohl-thalorna`
79
+ notes shipped `"system": { "title": "null" }` that way.
80
+
81
+ **Nothing in any content tree relied on the fallback.** Across all three
82
+ consumers — 295 `affiliation` notes in `sohl-thalorna`, 28 in
83
+ `sohl-kethira-basic`, none in `sohl` — not one carries a non-empty top-level
84
+ `title`, and `content-build package compile` emits byte-identical
85
+ `build/packs-json` for all three. It is a major because the rule that decides a
86
+ consumer's compiled documents changed with no configuration to restore it, and
87
+ because the generated item-field reference moves (below).
88
+
89
+ **The field is still authorable**, at the two positions that describe the
90
+ document rather than the note: `sohl.system.title`, and the legacy in-block
91
+ `sohl.title` that most trees already write. A membership's title belongs on the
92
+ entry in a being's `sohl.items`, as its `system.title`. `data.title` is neither —
93
+ `title` is not a `data:` property any note type declares, so `content-build lint`
94
+ refuses it.
95
+
96
+ **Declaring the exemption:** a field in an `itemBuilders` `fields:` declaration
97
+ may now carry `topLevelMeans`, whose value is _what the note's top-level key of
98
+ that name means instead_. Declaring it removes the shared top-level position from
99
+ that field's resolution order, and the generated item-field reference prints the
100
+ reason beneath the type's table, so an author reading it learns that the
101
+ top-level key will not fill the field. A repository that commits that page should
102
+ regenerate it.
103
+
104
+ The value is the reason rather than a bare flag deliberately: a boolean would
105
+ record the decision and lose the case for it, and the next person adding a field
106
+ needs to know the question exists.
107
+ - a41b066: **A page states its address relative to the site root.** `site.base` is no
108
+ longer written into a page's Hugo `url:` front matter — only into the `href`s
109
+ this build renders and the base a link-manifest `path` is measured against:
110
+
111
+ ```yaml
112
+ # emitted page
113
+ url: /doc-rulesintro/ # was: /sohl/doc-rulesintro/
114
+ ```
115
+
116
+ `site.base` was two quantities wearing one name. It is correctly _where the
117
+ package is served_ — the prefix on every rendered `href`, and the base a
118
+ manifest `path` is stripped against — and `/<contentPackage>/` is the right
119
+ default for that. It was also written verbatim into each page's `url:`, and that
120
+ is a different quantity: Hugo resolves `url` against `baseURL`, whose path for
121
+ every consumer that exists **already is** the package base. So the prefix was
122
+ written twice, and every content page — plus the homepage — published one
123
+ package segment too deep:
124
+
125
+ ```text
126
+ /sohl/doc-rulesintro/ 404 /sohl/sohl/doc-rulesintro/ 200
127
+ /thalorna/being-afzndhprnzr/ 404 /thalorna/thalorna/being-afzndhprnzr/ 200
128
+ ```
129
+
130
+ Not one address a link manifest advertised resolved: **0 of `sohl`'s 2,988
131
+ entries**, and 0 of `thalorna`'s 2,585.
132
+
133
+ **One value fed two readers that need opposite framings**, which is why no
134
+ setting could fix it from a consumer: `site.base: "/"` bought the addresses and
135
+ short-changed the hrefs, and the default did the reverse. They are now separate,
136
+ and each reader gets the form it needs:
137
+
138
+ | Reader | Gets | Because |
139
+ | --------------------------------------------------- | --------------------------- | ------------------------------------------------------------------- |
140
+ | A page's own `url:` front matter | `/<type>-<shortcode>/` | Hugo prefixes the site's `baseURL` path to it |
141
+ | The address index a `[[wikilink]]` resolves through | `<base><type>-<shortcode>/` | A browser resolves the rendered `href` against nothing |
142
+ | A link-manifest `path` | `<type>-<shortcode>/` | Measured against `site.base` and stripped; unchanged, byte for byte |
143
+
144
+ The homepage moves with them: `homepageFrontmatter` states `/homepage-root/`,
145
+ and no longer takes a `base` (nor does `writeHomepages`' fourth argument, which
146
+ existed only to supply it). `trees` pages and section landings never stated a
147
+ `url:` and are untouched — they take their address from their path.
148
+
149
+ **Take this release and drop your `site.base`.** Both publishing consumers set
150
+ `site.base: "/"` as a stopgap
151
+ (HeroicLands/Song-of-Heroic-Lands-FoundryVTT#1813, HeroicLands/sohl-thalorna#131),
152
+ which bought the correct addresses at the cost of same-package body links
153
+ rendering `/doc-x/` rather than `/sohl/doc-x/` — dead either way, so nothing
154
+ regressed. With this release the stopgap is no longer needed and no longer
155
+ harmless: keeping it leaves those hrefs short. Delete the `base:` line and the
156
+ default is right for both halves.
157
+
158
+ **Verified against pristine `git archive origin/main` extractions of all three
159
+ consumers**, in both configurations, through `content-build site` **and** Hugo
160
+ 0.165:
161
+
162
+ | Consumer | `site.base` | Page `url:` | Rendered path | Manifest entries resolving |
163
+ | ---------- | ----------- | --------------------- | ------------------------------ | -------------------------- |
164
+ | `sohl` | unset | `/doc-rulesintro/` | `/sohl/doc-rulesintro/` | 2,988 of 2,988 (was 0) |
165
+ | `sohl` | `"/"` | `/doc-rulesintro/` | `/sohl/doc-rulesintro/` | 2,988 of 2,988 |
166
+ | `thalorna` | unset | `/being-afzndhprnzr/` | `/thalorna/being-afzndhprnzr/` | 2,585 of 2,585 (was 0) |
167
+ | `thalorna` | `"/"` | `/being-afzndhprnzr/` | `/thalorna/being-afzndhprnzr/` | 2,585 of 2,585 |
168
+
169
+ No `/sohl/sohl/` or `/thalorna/thalorna/` path exists in either built tree, the
170
+ section landings, the content mount and the `dev-docs` tree pages all still
171
+ resolve, and with the stopgap dropped a same-package body link renders
172
+ `<a href=/sohl/doc-skills/>` again. `lint` and `links` are identical line for
173
+ line in every combination — `sohl` green, `sohl-thalorna` exactly as red as its
174
+ own content gap leaves it (1,983 lint findings, 122 link findings).
175
+
176
+ **Exactly one line per page changes, and nothing else does.** Of `sohl`'s 1,671
177
+ emitted files, 1,606 differ — the 1,605 content pages and the homepage — each by
178
+ its `url:` alone; the 46 tree pages and 19 landings are byte-identical, and
179
+ `build/manifests/sohl.json` is byte-identical. `sohl-kethira-basic` publishes a
180
+ homepage and nothing else (`publish.site: homepage`) and declares no
181
+ `site.base`: its console output is identical, its one emitted page moves from
182
+ `/kethira/kethira/homepage-root/` to `/kethira/homepage-root/`, and its landing
183
+ at `/kethira/` is unaffected.
184
+
185
+ Closes #217
186
+
187
+ ## 14.0.0
188
+
189
+ ### Major Changes
190
+
191
+ - a0a113b: **`publish.address.landing` is deleted.** `prefix` is the whole address scheme:
192
+
193
+ ```yaml
194
+ publish:
195
+ address:
196
+ prefix: kb/ # default: "" — the package root
197
+ ```
198
+
199
+ The key named which note addressed a whole section rather than a page within
200
+ one. #202 retired the second of its two rules and #204 retired the concept both
201
+ rules chose between — a section is a Hugo content directory the note format does
202
+ not carry, a page's address names no directory, and so no note lands one. What
203
+ survived was the key itself: resolved, refused-by-name for the retired value,
204
+ checked against a one-element vocabulary, frozen into the configuration, and
205
+ read by nobody. `LANDING_RULES` said so in its own doc comment — _"Inert since
206
+ #204."_
207
+
208
+ **A configuration still declaring it is refused, at the line it is written on.**
209
+ Not reported as an unrecognized option, which names a spelling to correct and
210
+ leaves the author to work out that the mechanism is gone:
211
+
212
+ ```text
213
+ package-build.config.yaml:14:9: error: package-build config:
214
+ `publish.address.landing` is a retired option — delete it. It named which note
215
+ addressed a whole section rather than a page within one, and there are no
216
+ sections to address: a section is a Hugo content directory the note format does
217
+ not carry, so no note lands one and every page is addressed
218
+ `<type>-<shortcode>`. Nothing replaces it.
219
+ ```
220
+
221
+ Presence is the whole test, as it is for a retired frontmatter field: no value
222
+ makes declaring it right, so `readme` and the already-retired `collection` are
223
+ refused alike, by `RETIRED_ADDRESS_KEYS` — the configuration-side twin of
224
+ `engine/retired-fields.mjs`.
225
+
226
+ **Why the key outlived its mechanism by one release.** `content-config.mjs` has
227
+ no warning channel — every finding goes through `fail()`, which throws — so
228
+ while both publishing consumers still declared the then-true `landing: readme`
229
+ the only options were to break them over a correct statement or to accept the
230
+ key in silence, and silent acceptance is what this codebase refuses everywhere
231
+ else. So it took the three steps `package:` took (#56): retire the value, have
232
+ consumers drop the key, delete the key. No consumer declares it now.
233
+
234
+ **The plumbing goes with it.** `packageAddress` took an address scheme only to
235
+ validate the `landing` rule it then discarded — its own docstring already said
236
+ the `prefix` half never applied, because an address is `(type, shortcode)`, a
237
+ package-wide identity that takes no mount. It is now a function of the
238
+ frontmatter and nothing else, and `manifestContext` no longer carries a `scheme`
239
+ that nothing reads.
240
+
241
+ | Removed | Where |
242
+ | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------- |
243
+ | `LANDING_RULES`, `RETIRED_LANDING_RULES` | `content-config.mjs` |
244
+ | `DEFAULT_ADDRESS_SCHEME.landing`, `ADDRESS_KEYS`' second entry, `normalizePublish`'s resolve-and-check, `AddressSchemeInput.landing` | `content-config.mjs` |
245
+ | The `LANDING_RULES` re-export, `packageAddress`'s `{ scheme }` parameter and its landing check | `engine/content-address.mjs` |
246
+ | `manifestContext`'s `scheme`, and the argument `collectManifestEntries` passed on with it | `engine/manifest-emit.mjs` |
247
+
248
+ **Nothing a consumer emits moves.** Verified against pristine
249
+ `git archive origin/main` extractions of all three consumers, before and after:
250
+ `lint`, `links`, `manifest`, `package compile` and `site` produce **identical
251
+ console output, line for line**, and every emitted file is byte-identical —
252
+ 31,197 files across the three trees, with only LevelDB's own timestamped `LOG`
253
+ differing. `sohl` stays green (2,988 manifest entries, 3,125 pack documents,
254
+ 1,671 emitted pages); `sohl-thalorna` stays exactly as red as it was for its own
255
+ content gap (1,983 lint findings, 122 link findings); `sohl-kethira-basic` stays
256
+ green.
257
+
258
+ Closes #215
259
+ - 98ac362: **`subType: user-guide` is refused.** #206 renamed the `doc` subType
260
+ `user-guide` to `userguide` and held every `type` and `subType` to the address
261
+ charset, but shipped a **transitional acceptance** for the old spelling — a
262
+ warning naming the replacement rather than a refusal — because 43 `sohl` notes
263
+ authored it and no consumer can sweep ahead of the release that renames a value.
264
+ Every consumer tree has now swept: `sohl` **0**, `sohl-thalorna` **0**,
265
+ `sohl-kethira-basic` **0**, counted on a pristine extraction of each
266
+ `origin/main`. So the acceptance guards nothing, and this is the follow-up #207
267
+ named.
268
+
269
+ `RETIRED_SUBTYPES`, `retiredSubType()` and `retiredSubTypeMessage()` are gone
270
+ from `engine/note-vocabulary.mjs`, along with the retired-spelling branch that
271
+ ran ahead of the charset check in `checkSubType`. Nothing replaces them:
272
+ `user-guide` now falls through to the **charset** check and is refused as an
273
+ error, for the reason that always applied — it contains a hyphen. That is why
274
+ the acceptance could be deleted rather than promoted to an error: the permanent
275
+ rule already covers the case, so no retirement-specific code outlived the sweep.
276
+
277
+ **Breaking**, though the diff only removes code. A spelling that built at exit 0
278
+ one release ago now fails the build, and three exported symbols no longer exist.
279
+ A tree that has swept sees no change at all — which all three consumers have,
280
+ and each was verified unaffected.
281
+
282
+ **The subType charset diagnostic is reworded.** It justified the rule by "the
283
+ hyphen separates the segments of an address", true of a `subType` when #206
284
+ shipped — `sectionOf` returned a `doc`'s subType, so the value was a URL path
285
+ segment — and not true since #204 retired sections. The rule stands on its own
286
+ footing instead: a subType is a vocabulary term the whole toolchain keys on, one
287
+ closed set away from being an address segment again, and a charset holding for a
288
+ type, a shortcode and a `contentPackage` but not for a subType would be a rule
289
+ nobody could state in a sentence. `typeCharsetMessage` is untouched — a type
290
+ genuinely is the first segment of every address.
291
+
292
+ The same correction is applied to `assertVocabularyCharset`'s throw, which
293
+ carried the identical claim in a second place — _"A type and a subType are both
294
+ address segments"_ — where it would go unread until it fires, which is exactly
295
+ when it would be taken at face value. It now states the reason **per key**: the
296
+ address half for a type, the vocabulary-term half for a subType. The guard
297
+ itself is unchanged and stays where it is, running over `NOTE_VOCABULARY` as the
298
+ module loads.
299
+
300
+ Closes #210
301
+
302
+ ### Minor Changes
303
+
304
+ - 45b5bd0: **A section can say what it lists.** `site.sections` / `site.readmeSections`
305
+ take two more keys, `listType` and `listSubType`, and both reach the generated
306
+ `_index.md`:
307
+
308
+ ```yaml
309
+ sections:
310
+ weapongear: { title: Weapons, listType: weapongear }
311
+ user-guide: { title: User Guide, listType: doc, listSubType: userguide }
312
+ ```
313
+
314
+ Since #204 a content page is written flat under the mount, so a declared
315
+ section's directory holds nothing but the landing this build writes for it and a
316
+ layout reading Hugo's `.Pages` finds no members. The membership survives in the
317
+ `site.sections` map and in nothing a theme can read — not on the page, not on
318
+ the landing, not in any URL — so every section landing served by a generic list
319
+ layout renders empty. `sohl` was unaffected only because its eleven catalog
320
+ layouts already query `site.RegularPages` by `Params.type`; a consumer rendering
321
+ through the shared theme has no layout of its own to edit. The landing now
322
+ states that query and the theme runs it
323
+ (HeroicLands/heroiclands-hugo-theme#50).
324
+
325
+ **Two keys of their own, not `type` / `subType`.** On an `_index.md`, `type` is
326
+ Hugo's own layout selector: verified against Hugo 0.165, a section landing
327
+ carrying `type: doc` renders through `layouts/doc/list.html` rather than the
328
+ default list template — behaviour this build already relies on deliberately, for
329
+ the mount's own `landing`. Spelling the content type there would silently change
330
+ which template serves the landing.
331
+
332
+ **Two keys added to the closed set, not an open passthrough.** `site.landing` is
333
+ passed through unvalidated because it is written once, for the mount, in one
334
+ landing template's own vocabulary; a section entry is written fourteen to twenty
335
+ times per build against a contract every package and every section shares.
336
+ Unbounded there, a mistyped `listTpye:` would publish into front matter, list
337
+ nothing, and report no error — which is the bug being fixed, moved one step
338
+ downstream where no build can see it. `normalizeSectionMeta` stays the one place
339
+ the vocabulary is bounded, and the writers still name no keys.
340
+
341
+ **Both values are checked, because both ways of writing an inert declaration are
342
+ silent.** They name a content type and subType, so each must be an address
343
+ segment (`^[A-Za-z0-9]+$`), and a `listSubType` with no `listType` is refused —
344
+ a subType tells pages apart only within a type, so alone it names no query. The
345
+ charset check is the trap this came from: a section is named for a URL the site
346
+ chose and need not match the address (`/sohl/kb/user-guide/` is the section,
347
+ `userguide` the subType, #207), and copying the section's name in would match no
348
+ page at exit 0. All three refusals are located at the offending key:
349
+
350
+ ```text
351
+ package-build.config.yaml:504:90: error: package-build config:
352
+ `site.sections.user-guide.listSubType` is `user-guide`, which is not
353
+ alphanumeric. …
354
+ ```
355
+
356
+ **Additive.** A section that declares neither key emits exactly the bytes it
357
+ did before. Verified on a pristine `origin/main` extraction of `sohl`, the only
358
+ consumer running `content-build site` with declared sections: 1670 emitted files
359
+ byte-identical, and with the keys declared on two of its nineteen sections
360
+ exactly those two `_index.md` files change.
361
+
362
+ Closes #212
363
+
3
364
  ## 13.0.0
4
365
 
5
366
  ### Major Changes