@heroiclands/package-build 10.0.0 → 11.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +306 -0
  2. package/CONTENT.md +218 -71
  3. package/MIGRATING.md +64 -0
  4. package/bin/content-build.mjs +59 -75
  5. package/docs/content-format.md +90 -67
  6. package/engine/base-compiler.mjs +7 -1
  7. package/engine/content-address.mjs +71 -18
  8. package/engine/content-format-check.mjs +1 -1
  9. package/engine/content-links.mjs +93 -112
  10. package/engine/content-lint.mjs +14 -10
  11. package/engine/content-slug.mjs +39 -105
  12. package/engine/diagnostics.mjs +16 -2
  13. package/engine/frontmatter-lint.mjs +27 -7
  14. package/engine/helpers.mjs +31 -68
  15. package/engine/homepage.mjs +131 -86
  16. package/engine/index.mjs +2 -5
  17. package/engine/manifest-emit.mjs +23 -4
  18. package/engine/note-vocabulary.mjs +58 -1
  19. package/engine/retired-fields.mjs +117 -6
  20. package/engine/site-build.mjs +182 -59
  21. package/engine/site-index.mjs +57 -102
  22. package/engine/web-wikilinks.mjs +183 -127
  23. package/engine/wikilink-syntax.mjs +174 -34
  24. package/engine/wikilinks.mjs +159 -117
  25. package/package.json +1 -1
  26. package/types/engine/base-compiler.d.mts +1 -1
  27. package/types/engine/content-address.d.mts +46 -14
  28. package/types/engine/content-links.d.mts +13 -17
  29. package/types/engine/content-slug.d.mts +11 -48
  30. package/types/engine/diagnostics.d.mts +14 -1
  31. package/types/engine/helpers.d.mts +4 -3
  32. package/types/engine/homepage.d.mts +96 -60
  33. package/types/engine/index.d.mts +0 -1
  34. package/types/engine/note-vocabulary.d.mts +43 -0
  35. package/types/engine/retired-fields.d.mts +78 -1
  36. package/types/engine/site-build.d.mts +70 -17
  37. package/types/engine/site-index.d.mts +19 -21
  38. package/types/engine/web-wikilinks.d.mts +29 -28
  39. package/types/engine/wikilink-syntax.d.mts +126 -40
  40. package/types/engine/wikilinks.d.mts +29 -24
  41. package/engine/abbreviations.mjs +0 -0
  42. package/engine/alias-index.mjs +0 -153
  43. package/types/engine/abbreviations.d.mts +0 -44
  44. package/types/engine/alias-index.d.mts +0 -122
package/CHANGELOG.md CHANGED
@@ -1,5 +1,311 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 11.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 51f9b4f: **The package homepage becomes an ordinary addressed note** (#182).
8
+
9
+ A homepage declares a `shortcode` — conventionally `root` — and publishes at
10
+ its address, `/<package>/homepage-root/`, written by the same rule as every
11
+ other page. `[[homepage-root|Read the introduction]]` is an ordinary wikilink
12
+ now, resolving to the page the build actually writes.
13
+
14
+ **Why it was the exception, and why it is not one any more.** A page's URL used
15
+ to derive from `name.full` while a homepage's destination was fixed at
16
+ `_index.md`, so a `shortcode` on one put the note in the address index and
17
+ `[[homepage-<shortcode>]]` resolved _green_ to a page nothing wrote. That is
18
+ what `HOMEPAGE_REFUSED_FIELDS` refused, and the reason was entirely an artifact
19
+ of name-derived URLs. #181 removed the premise: the address a `shortcode`
20
+ computes is the address the build publishes.
21
+
22
+ **Breaking, two ways.**
23
+
24
+ - **Every homepage note must now declare a `shortcode`.** Without one the note
25
+ has no address, and both `content-build lint` and `content-build site` refuse
26
+ it, located at the `type:` value that makes it necessary. All six trees need
27
+ the same one-line edit: `shortcode: root`.
28
+ - **The landing moves** from `/<package>/` to `/<package>/homepage-root/`.
29
+ `/<package>/` becomes a `301` the package authors in its own `_redirects`,
30
+ with a pinned `Cache-Control: max-age=3600` in `_headers` — Cloudflare Pages
31
+ sets no `Cache-Control` on a redirect it generates, and an unpinned 301 is
32
+ cacheable indefinitely under RFC 9111. See `CONTENT.md`.
33
+
34
+ | Field | Was | Now |
35
+ | ----------- | ------- | -------------------------------------------- |
36
+ | `shortcode` | refused | **required** |
37
+ | `name` | refused | permitted, like any other note's |
38
+ | `id` | refused | refused — a homepage compiles to no document |
39
+
40
+ `id` is unaffected because its reason is: a homepage appears in no pack, and it
41
+ stays out of the **link manifest** for the same reason, now that a shortcode
42
+ alone would put it in.
43
+
44
+ **What is deleted.** `HOMEPAGE_DESTINATION`, and the comment describing the
45
+ homepage as "the one page for which neither addressing rule holds" — it no
46
+ longer is. `homepageDestination(fm)` replaces the constant, and
47
+ `homepageFrontmatter` takes a `base` so the emitted page can state its `url`.
48
+
49
+ **Singularity is now stated as a cardinality rule.** It used to rest on the
50
+ fixed destination every homepage shared — the second silently overwrote the
51
+ first — so the address rule enforced it as a side effect. Two homepages publish
52
+ two pages now and collide over nothing, so `checkHomepageCount` says what it
53
+ means: a package has one front page, and nothing here can decide which of two
54
+ `/<package>/` should redirect to.
55
+ - 6291dec: **A published page's URL is its address, not its display name** (#181).
56
+
57
+ Every content page now serves at `/<package>/<type>-<shortcode>/`. `(type,
58
+ shortcode)` names one note within a package — the rule `content-build lint`
59
+ already enforces — so the URL is unique _by construction_: there is no
60
+ collision check behind it, and renaming a note moves no URL, because no part of
61
+ the address comes from a display string.
62
+
63
+ **Every published URL moves**, which is what makes this a major. `sohl`'s
64
+ `/sohl/kb/rules/shock/` becomes `/sohl/doc-shock/`; `thalorna`'s
65
+ `/thalorna/affiliation/the-aerarium-imperii/` becomes
66
+ `/thalorna/affiliation-aerarium/`. Measured across the three live trees: 1,595
67
+ `sohl` pages, 1,848 `thalorna`, 370 `kethira` — every one of them unique, and no
68
+ content edit required in any repository.
69
+
70
+ **What it replaces.** The URL derived from `name.full`, which made a display
71
+ name load-bearing three ways at once: a rename silently 404'd every inbound
72
+ link, two notes in one section could derive one URL so a uniqueness gate had to
73
+ run, and long names were shortened through a table of 200 abbreviations. The
74
+ module doing it justified the cost by promising redirects — _"every change
75
+ appends to the legacy-URL map"_ — and no such map was ever written, here or in
76
+ any consumer. An address needs none of it.
77
+
78
+ **Sections stay, as directories.** Hugo derives a page's section from where its
79
+ file is written, not from its URL, and that section is what supplies the section
80
+ landing pages, `.CurrentSection` and per-section layout lookup. So a page is
81
+ still written into `<section>/` and now carries a front-matter `url:` publishing
82
+ it at its address. A **landing page** is the one exception: it _is_ its section,
83
+ so it still addresses the section under the configured `publish.address.prefix`
84
+ (`kb/rules/`), which is why an entry's `path` is still written to the manifest
85
+ rather than left for a consumer to compute.
86
+
87
+ **The `type-` prefix is deliberate.** Flattening to `<shortcode>` would put
88
+ content in the same namespace as a package's fixed mounts — `/<package>/` for
89
+ the landing page, `/<package>/api/` for generated API docs — neither of which
90
+ contains a hyphen or names a type. With it the namespace is provably disjoint.
91
+
92
+ **Removed**
93
+
94
+ | Gone | Why |
95
+ | ----------------------------------------------- | -------------------------------------------- |
96
+ | `contentSlug` | Nothing derives a URL from a name. |
97
+ | `findSlugCollisions`, and the site build's gate | Two addresses cannot collide. |
98
+ | `ABBREVIATIONS` / `abbreviateTokens` | They only ever shortened a name-derived URL. |
99
+
100
+ `slugify` stays, unchanged and un-abbreviated, in the two places it was always
101
+ right for: heading anchors and pack filenames.
102
+
103
+ **Breaking, for a consumer calling the engine directly**
104
+
105
+ - `packageAddress(fm, name, options)` → `packageAddress(fm, options)`, and
106
+ `contentAddress(fm, name, isReadme)` → `contentAddress(fm, isReadme)`. The
107
+ name was the input the address no longer has.
108
+ - `addressSlug(fm)` is new: the `type-shortcode` segment, lowercased, so it is
109
+ exactly the tail of the canonical key.
110
+ - The site build's gate result renames `slugErrors` to `addressErrors` and drops
111
+ `collisions` entirely. A note with no `shortcode`, or no section to be filed
112
+ under, is reported there rather than published.
113
+ - `engine/abbreviations.mjs` is deleted.
114
+ - a9fb0fc: **Retire the bare `[[Alias]]` wikilink form and the alias index** (#180, resolving #179).
115
+
116
+ Every wikilink is now an **address**, and every wikilink carries a **label**. The
117
+ pipe no longer selects between two namespaces — there is only one — so a link
118
+ written without one addresses nothing and is a finding wherever it is met:
119
+ `content-build links` fails on it, the pack compilers fail the note, and the site
120
+ resolver reports it. The correction is always the same, and the message says so:
121
+ write `[[type-shortcode|Text]]`. `[[#slug|Text]]` still resolves; the link part
122
+ may be an anchor, it is the label that is required.
123
+
124
+ **Why.** The alias namespace was empty in practice. Across 8,305 wikilinks in the
125
+ three content trees, **not one** bare `[[Alias]]` resolved to a note. What the
126
+ index behind it did do was fold every note's `name.full` into itself, so two
127
+ notes of one type could not share a display name — five `doc` notes in the `sohl`
128
+ tree collided, and every available fix moved a published URL (#179). The rule had
129
+ never prevented a broken link; it had only ever forbidden a name.
130
+
131
+ **The top-level `aliases:` is a retired field.** It fed nothing else, so it is
132
+ now **refused** naming the file and the line, the same way `draft:` and
133
+ `package:` are, and reported by the frontmatter lint as well as at compile.
134
+
135
+ **`name.aliases` is kept, and is read by nothing.** It fed the same index and
136
+ lost the same reader, but it is **reserved** — held for a use that does not
137
+ exist yet — so it is deliberately neither retired nor consulted. No index folds
138
+ it in, no rule validates it, nothing derives a name, address or URL from it, and
139
+ the refusal above never mentions it. A note carrying one compiles, resolves and
140
+ addresses exactly as the same note without it, and `tests/name-aliases-reserved.test.ts`
141
+ pins that equivalence across the pack compile, both wikilink resolvers, the link
142
+ manifest and the site index, so a reader cannot be reintroduced unnoticed.
143
+
144
+ **Removed.** `engine/alias-index.mjs` in its entirety — `aliasesOf`, `aliasKey`,
145
+ `indexAliases`, and the `engine.aliasIndex` namespace export — along with
146
+ `resolvesAsAddress` from `engine/wikilink-syntax.mjs`, replaced by
147
+ `unlabelledLinkMessage`, which is the one place that states the rule for both
148
+ builds.
149
+
150
+ **API changes** for anything importing the engine directly:
151
+
152
+ | was | now |
153
+ | ------------------------------------------------------ | ------------------------------ |
154
+ | `index.resolve(note, target, labelled)` | `index.resolve(target)` |
155
+ | `index.resolveAlias`, `aliasClaims`, `aliasCollisions` | gone |
156
+ | `auditLinks().deadAliases` / `.aliasCollisions` | `auditLinks().unlabelledLinks` |
157
+ | `buildWikilinkIndex().byAlias` / `.aliasClaims` | gone |
158
+ | `buildSiteIndex().typeAlias` / `.typeCollide` | gone |
159
+ | `wikiContext()`'s `typeAlias` / `typeCollide` | gone |
160
+
161
+ `buildSiteIndex` no longer indexes a page by its **name**, filename or bare slug
162
+ — those were the collision-aware fallbacks the bare form was looked up in, and
163
+ nothing consults them now. `ambiguous` / `collide` becomes the set of short
164
+ `type/shortcode` addresses two **foreign packages** both publish, which the web
165
+ resolver now reports as ambiguous rather than as merely broken.
166
+
167
+ **Consumer impact, measured.** Every tree needs a mechanical frontmatter sweep
168
+ of the **top-level** field, which is authored almost everywhere: 1,609 notes in
169
+ `sohl`, 1,850 in `thalorna`, 370 in `kethira`. A `name.aliases:` is left exactly
170
+ where it is — the sweep must delete the top-level list only. Links are cheaper — `sohl` has **0** unlabelled
171
+ links and `kethira` has none at all; `thalorna` carries 70 (68 bare links and 2
172
+ pipe-less anchors), which is content work in its own repository. The five `sohl`
173
+ alias collisions and `thalorna`'s thirteen cease to exist with no note renamed
174
+ and no published URL moved.
175
+ - ffb1b04: **An address that resolves to no note fails every build** (#184).
176
+
177
+ A wikilink whose address names no document was a **warning** in `content-build
178
+ links`, a **failure** in the pack compilers, and — in the site build — _nothing
179
+ at all_ while any linkable package had no vendored manifest. One authored link,
180
+ three verdicts. This makes it an error everywhere, and makes the three resolvers
181
+ name and word every class of link failure identically.
182
+
183
+ **Why the tolerance is spent.** It existed because a bare `[[Sunless Vault]]`
184
+ might be a worldbuilding placeholder for a note nobody had written. That was a
185
+ property of the bare form, which #180 retired, and the intent behind it now has
186
+ a real spelling: a note tagged `draft` exists, resolves, compiles, publishes,
187
+ and renders its inbound links marked (#183). An address naming no note is a typo
188
+ or an omission, and both want fixing.
189
+
190
+ **One vocabulary, one message.** `engine/wikilink-syntax.mjs` — which already
191
+ held the syntax the three resolvers share — now also holds the closed set of
192
+ failure classes (`LINK_FINDING_REASONS`) and the message each reports through
193
+ (`linkFindingMessage`, `unresolvedAddressMessage`, `ambiguousAddressMessage`).
194
+ An author meets whichever build ran first, and a consumer switching on a
195
+ `reason` should not be switching on which build produced it.
196
+
197
+ | finding | checker | pack build | site build |
198
+ | ---------------- | ------- | ---------- | ---------- |
199
+ | `unlabelled` | error | error | error |
200
+ | `not-an-address` | error | error | error |
201
+ | `unknown-type` | error | error | error |
202
+ | `unresolved` | error | error | **error** |
203
+ | `ambiguous` | error | error | error |
204
+
205
+ **Breaking changes.**
206
+
207
+ - _The site build fails an unresolved address unconditionally._
208
+ `wikiContext()` no longer takes `manifestsComplete`, and `resolveWebWikilinks`
209
+ ignores one on the context. A missing manifest is now advice inside the
210
+ message rather than a reason to let the link through.
211
+ - _Two reason strings were renamed into the shared vocabulary._ The pack
212
+ build's `"unknown"` and the site build's `"broken type/shortcode"` are both
213
+ `"unresolved"`. A slash-qualified target naming no known type reports as
214
+ `"unknown-type"` on the site build too, matching the checker.
215
+ - _`ambiguous` is a class in all three._ An address more than one package
216
+ publishes was reported by the checker and the pack build as though nothing
217
+ published it. The finding carries the claiming `packages`, and the message
218
+ names them.
219
+ - _Site-build wikilink findings are compiler-parseable._ They were
220
+ `log.error("bad wikilink [[x]]: reason (file)")` — a `loglevel` timestamp
221
+ sitting exactly where a parser reads the path from. They are now
222
+ `file:line:column: error: message`, located by the authored link's own
223
+ position in the source note, like every other diagnostic.
224
+ - _Pack-build failure messages are reworded_ by the shared table, and name the
225
+ address rather than the whole authored link.
226
+
227
+ **Consumer impact: none measured.** Across the three content trees — `sohl`
228
+ (1,607 notes / 3,618 links), `thalorna` (1,849 / 7,913) and `kethira` (371 / 0)
229
+ — the promotion produces **zero** new findings. `thalorna`'s 66 dead addresses
230
+ are all `not-an-address`, which was already an error, and its 70 unlabelled
231
+ links are #180's. No tree carries an address that resolves nowhere.
232
+
233
+ ### Minor Changes
234
+
235
+ - ba51273: Mark a link whose target is tagged `draft` (#183).
236
+
237
+ A note that exists only so a link is not dead is now visibly distinct from one
238
+ that is written. Both builds wrap such a link in
239
+ `<span class="sohl-draft-link" title="Draft — not yet written">…</span>` —
240
+ byte-identically, as `unresolvedLink` already does, so one authored link carries
241
+ the same cue in a compiled journal and on the website. The link inside is
242
+ untouched: Foundry enriches inside HTML and Goldmark parses markdown inside an
243
+ inline span, so it is still a live link either way.
244
+
245
+ **The note stays in the graph.** This marks at presentation and nothing else.
246
+ Resolution, validation, pack compilation and manifest membership are unchanged
247
+ for a draft note, which is what separates the tag from the retired `draft:`
248
+ field — that field moved a note from _published_ to unresolvable without saying
249
+ so, and suppressed the build failures the note carried. Nothing here reinstates
250
+ any of it, and declaring the field is still refused by name.
251
+
252
+ **Read from the tag vocabulary, not respelt.** `draft` is declared in
253
+ `DECLARED_TAGS.state` (#172) and exported as `DRAFT_TAG`, with `isDraftNote()`
254
+ as its one reader — so the declared tag and the thing that acts on it cannot
255
+ drift apart. This is the first thing in either build to read `tags`.
256
+
257
+ **For consumers.** The class carries no appearance of its own. A Foundry system
258
+ supplies it in an SCSS partial beside `_unresolved-link.scss`; a site supplies
259
+ it in its theme. Until one does, a draft link renders exactly as it did before.
260
+
261
+ ### Patch Changes
262
+
263
+ - 743b303: **`collectContentPages` refuses a missing `base` instead of publishing to `undefined/`.**
264
+
265
+ A page's URL is built as `` `${ctx.base}${slug}/` `` since _a page URL is its
266
+ address_ (#181). When `base` was absent the template still ran, so every
267
+ non-landing page in that build published at `undefineddoc-<shortcode>/` with no
268
+ diagnostic. The function already guards the section immediately below, on the
269
+ stated grounds that a note with none "is reported rather than written to
270
+ `undefined/`" — the same reasoning applies to `base`, which affects _every_ page
271
+ rather than one.
272
+
273
+ It is a caller contract rather than a note defect, so it throws rather than being
274
+ collected as a finding.
275
+
276
+ Fixes the two in-repo callers that still supplied the pre-#181 options and had
277
+ gone unnoticed: the end-to-end draft-link case (#183) and a homepage case, whose
278
+ combination with #181 left `main` red — neither pull request failed alone.
279
+
280
+ Closes #195
281
+
282
+ ## 10.0.1
283
+
284
+ ### Patch Changes
285
+
286
+ - 5bc08b6: **A frontmatter reference resolves as an address, never as an alias.**
287
+
288
+ The resolver's third argument chooses the namespace and has no fallback (#131,
289
+ #144). The frontmatter lint's `ref:` check omitted it, so every reference was
290
+ looked up as an _alias_ — and `type-shortcode` is never an alias, so every
291
+ value a `ref:` field carried was reported as naming a note nothing declares:
292
+
293
+ ```
294
+ Spirit.md:7:60: error: `sohl.assocSkillCode` names skill "spirit", and no note
295
+ or vendored manifest declares it
296
+ ```
297
+
298
+ The skill existed, in the same tree, with exactly that shortcode.
299
+
300
+ A frontmatter reference is a bare address by construction — there is no pipe to
301
+ read intent from, and the field supplies the type — so the check now says so.
302
+
303
+ The suite stayed green because its index stub ignored the argument and resolved
304
+ whatever it was handed. It no longer does, which is the part that keeps this
305
+ fixed.
306
+
307
+ Closes #176
308
+
3
309
  ## 10.0.0
4
310
 
5
311
  ### Major Changes