@heroiclands/package-build 13.0.0 → 14.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,182 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 14.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - a0a113b: **`publish.address.landing` is deleted.** `prefix` is the whole address scheme:
8
+
9
+ ```yaml
10
+ publish:
11
+ address:
12
+ prefix: kb/ # default: "" — the package root
13
+ ```
14
+
15
+ The key named which note addressed a whole section rather than a page within
16
+ one. #202 retired the second of its two rules and #204 retired the concept both
17
+ rules chose between — a section is a Hugo content directory the note format does
18
+ not carry, a page's address names no directory, and so no note lands one. What
19
+ survived was the key itself: resolved, refused-by-name for the retired value,
20
+ checked against a one-element vocabulary, frozen into the configuration, and
21
+ read by nobody. `LANDING_RULES` said so in its own doc comment — _"Inert since
22
+ #204."_
23
+
24
+ **A configuration still declaring it is refused, at the line it is written on.**
25
+ Not reported as an unrecognized option, which names a spelling to correct and
26
+ leaves the author to work out that the mechanism is gone:
27
+
28
+ ```text
29
+ package-build.config.yaml:14:9: error: package-build config:
30
+ `publish.address.landing` is a retired option — delete it. It named which note
31
+ addressed a whole section rather than a page within one, and there are no
32
+ sections to address: a section is a Hugo content directory the note format does
33
+ not carry, so no note lands one and every page is addressed
34
+ `<type>-<shortcode>`. Nothing replaces it.
35
+ ```
36
+
37
+ Presence is the whole test, as it is for a retired frontmatter field: no value
38
+ makes declaring it right, so `readme` and the already-retired `collection` are
39
+ refused alike, by `RETIRED_ADDRESS_KEYS` — the configuration-side twin of
40
+ `engine/retired-fields.mjs`.
41
+
42
+ **Why the key outlived its mechanism by one release.** `content-config.mjs` has
43
+ no warning channel — every finding goes through `fail()`, which throws — so
44
+ while both publishing consumers still declared the then-true `landing: readme`
45
+ the only options were to break them over a correct statement or to accept the
46
+ key in silence, and silent acceptance is what this codebase refuses everywhere
47
+ else. So it took the three steps `package:` took (#56): retire the value, have
48
+ consumers drop the key, delete the key. No consumer declares it now.
49
+
50
+ **The plumbing goes with it.** `packageAddress` took an address scheme only to
51
+ validate the `landing` rule it then discarded — its own docstring already said
52
+ the `prefix` half never applied, because an address is `(type, shortcode)`, a
53
+ package-wide identity that takes no mount. It is now a function of the
54
+ frontmatter and nothing else, and `manifestContext` no longer carries a `scheme`
55
+ that nothing reads.
56
+
57
+ | Removed | Where |
58
+ | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------- |
59
+ | `LANDING_RULES`, `RETIRED_LANDING_RULES` | `content-config.mjs` |
60
+ | `DEFAULT_ADDRESS_SCHEME.landing`, `ADDRESS_KEYS`' second entry, `normalizePublish`'s resolve-and-check, `AddressSchemeInput.landing` | `content-config.mjs` |
61
+ | The `LANDING_RULES` re-export, `packageAddress`'s `{ scheme }` parameter and its landing check | `engine/content-address.mjs` |
62
+ | `manifestContext`'s `scheme`, and the argument `collectManifestEntries` passed on with it | `engine/manifest-emit.mjs` |
63
+
64
+ **Nothing a consumer emits moves.** Verified against pristine
65
+ `git archive origin/main` extractions of all three consumers, before and after:
66
+ `lint`, `links`, `manifest`, `package compile` and `site` produce **identical
67
+ console output, line for line**, and every emitted file is byte-identical —
68
+ 31,197 files across the three trees, with only LevelDB's own timestamped `LOG`
69
+ differing. `sohl` stays green (2,988 manifest entries, 3,125 pack documents,
70
+ 1,671 emitted pages); `sohl-thalorna` stays exactly as red as it was for its own
71
+ content gap (1,983 lint findings, 122 link findings); `sohl-kethira-basic` stays
72
+ green.
73
+
74
+ Closes #215
75
+ - 98ac362: **`subType: user-guide` is refused.** #206 renamed the `doc` subType
76
+ `user-guide` to `userguide` and held every `type` and `subType` to the address
77
+ charset, but shipped a **transitional acceptance** for the old spelling — a
78
+ warning naming the replacement rather than a refusal — because 43 `sohl` notes
79
+ authored it and no consumer can sweep ahead of the release that renames a value.
80
+ Every consumer tree has now swept: `sohl` **0**, `sohl-thalorna` **0**,
81
+ `sohl-kethira-basic` **0**, counted on a pristine extraction of each
82
+ `origin/main`. So the acceptance guards nothing, and this is the follow-up #207
83
+ named.
84
+
85
+ `RETIRED_SUBTYPES`, `retiredSubType()` and `retiredSubTypeMessage()` are gone
86
+ from `engine/note-vocabulary.mjs`, along with the retired-spelling branch that
87
+ ran ahead of the charset check in `checkSubType`. Nothing replaces them:
88
+ `user-guide` now falls through to the **charset** check and is refused as an
89
+ error, for the reason that always applied — it contains a hyphen. That is why
90
+ the acceptance could be deleted rather than promoted to an error: the permanent
91
+ rule already covers the case, so no retirement-specific code outlived the sweep.
92
+
93
+ **Breaking**, though the diff only removes code. A spelling that built at exit 0
94
+ one release ago now fails the build, and three exported symbols no longer exist.
95
+ A tree that has swept sees no change at all — which all three consumers have,
96
+ and each was verified unaffected.
97
+
98
+ **The subType charset diagnostic is reworded.** It justified the rule by "the
99
+ hyphen separates the segments of an address", true of a `subType` when #206
100
+ shipped — `sectionOf` returned a `doc`'s subType, so the value was a URL path
101
+ segment — and not true since #204 retired sections. The rule stands on its own
102
+ footing instead: a subType is a vocabulary term the whole toolchain keys on, one
103
+ closed set away from being an address segment again, and a charset holding for a
104
+ type, a shortcode and a `contentPackage` but not for a subType would be a rule
105
+ nobody could state in a sentence. `typeCharsetMessage` is untouched — a type
106
+ genuinely is the first segment of every address.
107
+
108
+ The same correction is applied to `assertVocabularyCharset`'s throw, which
109
+ carried the identical claim in a second place — _"A type and a subType are both
110
+ address segments"_ — where it would go unread until it fires, which is exactly
111
+ when it would be taken at face value. It now states the reason **per key**: the
112
+ address half for a type, the vocabulary-term half for a subType. The guard
113
+ itself is unchanged and stays where it is, running over `NOTE_VOCABULARY` as the
114
+ module loads.
115
+
116
+ Closes #210
117
+
118
+ ### Minor Changes
119
+
120
+ - 45b5bd0: **A section can say what it lists.** `site.sections` / `site.readmeSections`
121
+ take two more keys, `listType` and `listSubType`, and both reach the generated
122
+ `_index.md`:
123
+
124
+ ```yaml
125
+ sections:
126
+ weapongear: { title: Weapons, listType: weapongear }
127
+ user-guide: { title: User Guide, listType: doc, listSubType: userguide }
128
+ ```
129
+
130
+ Since #204 a content page is written flat under the mount, so a declared
131
+ section's directory holds nothing but the landing this build writes for it and a
132
+ layout reading Hugo's `.Pages` finds no members. The membership survives in the
133
+ `site.sections` map and in nothing a theme can read — not on the page, not on
134
+ the landing, not in any URL — so every section landing served by a generic list
135
+ layout renders empty. `sohl` was unaffected only because its eleven catalog
136
+ layouts already query `site.RegularPages` by `Params.type`; a consumer rendering
137
+ through the shared theme has no layout of its own to edit. The landing now
138
+ states that query and the theme runs it
139
+ (HeroicLands/heroiclands-hugo-theme#50).
140
+
141
+ **Two keys of their own, not `type` / `subType`.** On an `_index.md`, `type` is
142
+ Hugo's own layout selector: verified against Hugo 0.165, a section landing
143
+ carrying `type: doc` renders through `layouts/doc/list.html` rather than the
144
+ default list template — behaviour this build already relies on deliberately, for
145
+ the mount's own `landing`. Spelling the content type there would silently change
146
+ which template serves the landing.
147
+
148
+ **Two keys added to the closed set, not an open passthrough.** `site.landing` is
149
+ passed through unvalidated because it is written once, for the mount, in one
150
+ landing template's own vocabulary; a section entry is written fourteen to twenty
151
+ times per build against a contract every package and every section shares.
152
+ Unbounded there, a mistyped `listTpye:` would publish into front matter, list
153
+ nothing, and report no error — which is the bug being fixed, moved one step
154
+ downstream where no build can see it. `normalizeSectionMeta` stays the one place
155
+ the vocabulary is bounded, and the writers still name no keys.
156
+
157
+ **Both values are checked, because both ways of writing an inert declaration are
158
+ silent.** They name a content type and subType, so each must be an address
159
+ segment (`^[A-Za-z0-9]+$`), and a `listSubType` with no `listType` is refused —
160
+ a subType tells pages apart only within a type, so alone it names no query. The
161
+ charset check is the trap this came from: a section is named for a URL the site
162
+ chose and need not match the address (`/sohl/kb/user-guide/` is the section,
163
+ `userguide` the subType, #207), and copying the section's name in would match no
164
+ page at exit 0. All three refusals are located at the offending key:
165
+
166
+ ```text
167
+ package-build.config.yaml:504:90: error: package-build config:
168
+ `site.sections.user-guide.listSubType` is `user-guide`, which is not
169
+ alphanumeric. …
170
+ ```
171
+
172
+ **Additive.** A section that declares neither key emits exactly the bytes it
173
+ did before. Verified on a pristine `origin/main` extraction of `sohl`, the only
174
+ consumer running `content-build site` with declared sections: 1670 emitted files
175
+ byte-identical, and with the keys declared on two of its nineteen sections
176
+ exactly those two `_index.md` files change.
177
+
178
+ Closes #212
179
+
3
180
  ## 13.0.0
4
181
 
5
182
  ### Major Changes
package/CONTENT.md CHANGED
@@ -138,7 +138,6 @@ publish:
138
138
  manifests: { publish: true, consume: true }
139
139
  address:
140
140
  prefix: kb/
141
- landing: readme
142
141
 
143
142
  # How this repository frames the website `content-build site` publishes.
144
143
  # Framing only: addresses come from `publish.address` above.
@@ -806,15 +805,15 @@ rule is checked ahead of the closed-set check, which is what makes it reach a
806
805
  type whose values are declared but not yet enumerated:
807
806
 
808
807
  ```text
809
- assets/content/Beings/Folk.md:3:1: error: `subType` "common-folk" is not an address segment — a subType is letters and digits only (^[A-Za-z0-9]+$), the same charset a shortcode is held to. …
808
+ assets/content/Beings/Folk.md:3:1: error: `subType` "common-folk" is not a well-formed subType — a subType is letters and digits only (^[A-Za-z0-9]+$), the same charset a type, a shortcode and a contentPackage are held to. …
810
809
  ```
811
810
 
812
811
  One declared value broke that rule: a `doc`'s `user-guide`, now **`userguide`**.
813
- The old spelling is accepted for one transitional release and reported as a
814
- **warning** naming the replacement an error would red every tree that took the
815
- release before it had a chance to sweep, and the note still compiles to the
816
- correct page. A later release removes the acceptance, and the old spelling then
817
- falls through to the ordinary undeclared-value error.
812
+ The old spelling was accepted for one transitional release, as a warning naming
813
+ the replacement, because an error would have redded every tree that took the
814
+ release before it had a chance to sweep. Every tree has swept, so the acceptance
815
+ is gone (#210): `user-guide` is refused by the charset check like any other
816
+ hyphenated value, and nothing retirement-specific was left to remove.
818
817
 
819
818
  The vocabulary lives in `engine/note-vocabulary.mjs`, one entry per note type,
820
819
  taken from the content-format specification. It is note-format knowledge rather
@@ -1247,9 +1246,8 @@ per section, and that is now the _only_ thing that makes one exist — see
1247
1246
 
1248
1247
  ### The address scheme
1249
1248
 
1250
- Where the content tree mounts _inside the package_, and which note addresses a
1251
- whole section rather than a page within one, differ between repositories and are
1252
- both load-bearing. They are one setting, read by this command **and** by the page
1249
+ Where the content tree mounts _inside the package_ differs between repositories
1250
+ and is load-bearing. It is one setting, read by this command **and** by the page
1253
1251
  emitter, so the address a manifest publishes is the address a page is emitted at
1254
1252
  — stating it twice is how a manifest comes to assert a URL that resolves at build
1255
1253
  time and 404s for the reader.
@@ -1260,7 +1258,6 @@ publish:
1260
1258
  manifests: { publish: true, consume: true }
1261
1259
  address:
1262
1260
  prefix: kb/ # default: "" — the package root
1263
- landing: readme # inert since #204; still accepted
1264
1261
  ```
1265
1262
 
1266
1263
  - **`prefix`** — the content tree's mount within the package: the Hugo directory
@@ -1270,14 +1267,9 @@ publish:
1270
1267
  content, so it has no prefix. It must end in a slash and must not begin with
1271
1268
  one — where the _package_ is mounted is the consuming build's knowledge and is
1272
1269
  never recorded here.
1273
- - **`landing`** — **inert, and accepted only so it keeps loading.** It named
1274
- which note addressed a whole section rather than a page within one. There are
1275
- no sections in the note format (#204) and so no landings, so it selects
1276
- nothing; it is still accepted because both publishing consumers declare
1277
- `landing: readme` and refusing a correct statement would break them. It is
1278
- removed once no configuration writes it. The retired second rule, `collection`,
1279
- is still refused by name (#202), along with the `collection` subtype and the
1280
- top-level `section:` key.
1270
+
1271
+ `prefix` is the whole scheme. The `collection` subtype and the top-level
1272
+ `section:` key are refused by name (#202).
1281
1273
 
1282
1274
  A note's `subType` is checked against the values its type declares, and only
1283
1275
  those. It briefly had a second reading — a `README` landing's `subType` was the
@@ -1431,7 +1423,8 @@ Two consequences follow, and neither is optional:
1431
1423
  `_index.md`, so a layout reading `.Pages` finds nothing. A layout that queries
1432
1424
  `site.RegularPages` by `Params.type` is unaffected, and that is the shape a
1433
1425
  content catalog wants anyway — it groups by what a page _is_, not by where its
1434
- file happened to be written.
1426
+ file happened to be written. A section that wants that query run for it says
1427
+ so with `listType` — see below.
1435
1428
 
1436
1429
  Whatever the entry may carry is the whole of what the section can say.
1437
1430
 
@@ -1440,18 +1433,63 @@ Whatever the entry may carry is the whole of what the section can say.
1440
1433
  | `title` | yes | The landing's heading, so it matches the card that links to it. |
1441
1434
  | `banner` | no | The hero image, resolved as a CDN asset like any other `banner:`. |
1442
1435
  | `description` | no | The hero standfirst under the heading, and the blurb a landing card uses. |
1436
+ | `listType` | no | The content **type** whose pages this section lists. |
1437
+ | `listSubType` | no | Narrows that to one **subType**. Only with a `listType`. |
1443
1438
 
1444
- `readmeSections` takes the same three, for a **`trees`** entry: those pages keep
1439
+ `readmeSections` takes the same keys, for a **`trees`** entry: those pages keep
1445
1440
  their source layout below a named section, so the tree's own `README.md` is that
1446
1441
  section's landing. What the section declares wins over what the `README` happens
1447
1442
  to carry — the landing has to match the card that links to it. No content note
1448
1443
  reaches this map any more.
1449
1444
 
1445
+ #### Saying what a section lists
1446
+
1447
+ A generic list layout has nothing to render, because the membership a section
1448
+ landing used to get free from Hugo's page tree no longer exists anywhere the
1449
+ theme can read: not on the page, not on the landing, not in any URL — only here.
1450
+ So a section states its own query, and a layout substitutes it when `.Pages` is
1451
+ empty:
1452
+
1453
+ ```yaml
1454
+ sections:
1455
+ being: { title: Beings, listType: being }
1456
+ rules: { title: Rules, listType: doc, listSubType: rules }
1457
+ user-guide: { title: User Guide, listType: doc, listSubType: userguide }
1458
+ ```
1459
+
1460
+ emits, for the last of those, `user-guide/_index.md`:
1461
+
1462
+ ```yaml
1463
+ ---
1464
+ title: User Guide
1465
+ listType: doc
1466
+ listSubType: userguide
1467
+ ---
1468
+ ```
1469
+
1470
+ Three things about the spelling, each of them load-bearing:
1471
+
1472
+ - **Not `type`.** On an `_index.md` that is Hugo's own layout selector — a
1473
+ landing carrying `type: doc` is rendered by `layouts/doc/list.html` rather
1474
+ than the default list template — so writing the content type there would
1475
+ silently change which template serves the landing. This build already relies
1476
+ on that behaviour for the mount's own `landing`.
1477
+ - **Not inferred from the section's name.** A section is named for a URL the
1478
+ site chose; a type and a subType are addresses. They need not agree, and on
1479
+ `sohl` they do not: the section is `user-guide`, because that is a published
1480
+ URL, while the subType is `userguide` because an address segment is
1481
+ alphanumeric (#207). Both values are checked against that charset here, so
1482
+ copying the section's name in is refused rather than quietly matching nothing.
1483
+ - **`listSubType` needs a `listType`.** A subType only tells pages apart within
1484
+ a type — `rules`, `userguide` and `reference` are all `doc` — so alone it
1485
+ names no query.
1486
+
1450
1487
  **The vocabulary is closed, and a key outside it is refused by name:**
1451
1488
 
1452
1489
  ```text
1453
1490
  package-build config: `site.sections.affliction.descrption` is not a
1454
- recognized option (expected one of: title, banner, description).
1491
+ recognized option (expected one of: title, banner, description, listType,
1492
+ listSubType).
1455
1493
  ```
1456
1494
 
1457
1495
  That refusal is the point. `landing` is passed through unvalidated because it is
@@ -1460,7 +1498,10 @@ entry is written fourteen to twenty times per build against a contract every
1460
1498
  package and every section shares, so an unbounded one would let a mistyped
1461
1499
  `descrption:` publish into front matter, be read by nobody, and say nothing to
1462
1500
  anyone. Refusing it costs one line here when the vocabulary genuinely grows, and
1463
- buys a build that cannot quietly emit a key no theme reads.
1501
+ buys a build that cannot quietly emit a key no theme reads. `listType` and
1502
+ `listSubType` are that growth: two named keys, checked, rather than an open
1503
+ passthrough in which `listTpye:` would publish and no landing would list
1504
+ anything.
1464
1505
 
1465
1506
  The **writers** name no keys: a section's `_index.md` is whatever the entry
1466
1507
  resolved to, `title` first. So extending the vocabulary is a change to the
@@ -1624,7 +1665,7 @@ path now rides on the error, and the loader that read the file resolves it
1624
1665
  against the YAML, so all of them come out located:
1625
1666
 
1626
1667
  ```text
1627
- package-build.config.yaml:382:64: error: package-build config: `site.sections.being.descrption` is not a recognized option (expected one of: title, banner).
1668
+ package-build.config.yaml:382:64: error: package-build config: `site.sections.being.descrption` is not a recognized option (expected one of: title, banner, description, listType, listSubType).
1628
1669
  ```
1629
1670
 
1630
1671
  The same two rules apply. A key the file never declares — a required one that is
package/MIGRATING.md CHANGED
@@ -49,12 +49,18 @@ That is the shape a content catalog wants regardless — it groups by what a pag
49
49
  _is_ rather than by where its file happened to be written — and it is what
50
50
  `sohl`'s catalog layouts already do.
51
51
 
52
- ## 4. `publish.address.landing` is inert, and still accepted
52
+ A site rendering through a **shared theme** has no layout of its own to edit. As
53
+ of 13.1.0 the section declares its query instead, and the theme runs it:
53
54
 
54
- It named which note addressed a whole section. There are no landings, so it
55
- selects nothing; `landing: readme` keeps loading because it stated something true
56
- when it was written, and the key is removed once no configuration declares it.
57
- `landing: collection` is still refused by name (#202).
55
+ ```yaml
56
+ sections:
57
+ weapongear: { title: Weapons, listType: weapongear }
58
+ user-guide: { title: User Guide, listType: doc, listSubType: userguide }
59
+ ```
60
+
61
+ `listType` is the content type, not the section's name — the two need not agree,
62
+ and `listSubType` narrows a type whose genres share it. See
63
+ [What a section may declare](CONTENT.md#what-a-section-may-declare).
58
64
 
59
65
  ## What did not change
60
66
 
@@ -68,7 +68,7 @@ import path from "node:path";
68
68
 
69
69
  // Leaves with no local imports of their own, so naming them here cannot close
70
70
  // a cycle around a consumer's config file (see `engine/pack-config.mjs`).
71
- import { isAddressSegment } from "./engine/address-charset.mjs";
71
+ import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./engine/address-charset.mjs";
72
72
  import { MAP_TYPES, PACK_BY_TYPE } from "./engine/ids.mjs";
73
73
 
74
74
  /**
@@ -116,44 +116,37 @@ export const PACK_DOCUMENT_TYPES = /** @type {const} */ ([
116
116
  ]);
117
117
 
118
118
  /**
119
- * The landing-page rules a repository may route by. **Inert since #204.**
120
- *
121
- * A *landing page* was a note that addressed a whole section rather than a page
122
- * within one, so it had no slug of its own. There are no sections in the note
123
- * format any more a section is a Hugo content directory, and a page's address
124
- * names no directory so there are no landings and this selects nothing.
125
- *
126
- * The key survives its own mechanism on purpose. Both publishing consumers
127
- * declare `landing: readme`, which stated something true when they wrote it;
128
- * refusing it now would break them over a correct statement, and silently
129
- * ignoring an unknown value would be worse. So `readme` stays accepted, the
130
- * retired `collection` stays refused by name (below), and the key is deleted
131
- * once no configuration writes it `content-config.mjs` has no warning channel
132
- * with which to say "accepted, and does nothing" in between.
133
- *
134
- * @type {readonly string[]}
135
- */
136
- export const LANDING_RULES = Object.freeze(["readme"]);
137
-
138
- /**
139
- * What a configuration naming the retired `collection` landing rule is told.
140
- *
141
- * A retired *value* is refused the way a retired *field* is (see
142
- * `engine/retired-fields.mjs`): left merely unrecognized it would be reported
143
- * as a bad value, which names something to correct and leaves the author to
144
- * work out for themselves that the mechanism is gone. The message says the rule
145
- * is retired, what lands a section instead, and what to do with the key.
119
+ * Address-scheme keys a configuration may no longer declare.
120
+ *
121
+ * A retired key has exactly two possible fates, and only one of them is honest
122
+ * the same reasoning `engine/retired-fields.mjs` applies to a retired
123
+ * frontmatter field. Left honoured, it keeps doing whatever it did, which is
124
+ * why it was retired. Left *ignored*, it reads to its author as though it still
125
+ * works: the configuration says one thing and the build does another, and
126
+ * nothing says so. This module has no third option, because it has no warning
127
+ * channel every finding goes through `fail()`, which throws. So a retired
128
+ * key is **refused**, at the line it was written on, with a message that says
129
+ * the mechanism is gone rather than naming a value to correct.
130
+ *
131
+ * **What `landing` did (#204).** It named which note addressed a whole section
132
+ * rather than a page within one a *landing page*, which therefore had no slug
133
+ * of its own. #203 retired the second of its two rules and #204 retired the
134
+ * concept both rules chose between: a section is a Hugo content directory that
135
+ * the note format does not carry, a page's address names no directory, and so
136
+ * no note lands anything. The key outlived its mechanism by one release only
137
+ * because both publishing consumers still declared the then-true
138
+ * `landing: readme`, and neither breaking them over a correct statement nor
139
+ * accepting the key in silence was acceptable. Neither declares it now.
146
140
  *
147
141
  * @type {Readonly<Record<string, string>>}
148
142
  */
149
- export const RETIRED_LANDING_RULES = Object.freeze({
150
- collection:
151
- "the `collection` landing rule is retired, and so is the mechanism it " +
152
- "chose between: a section is a Hugo directory the note format does not " +
153
- "carry, so no note lands one. Delete this key. A page that introduces " +
154
- "the notes of a type is an ordinary note `type: doc`, " +
155
- "`subType: reference`, `shortcode: <type>` addressed `doc-<type>`; " +
156
- "the `section:` frontmatter key the rule read is retired with it",
143
+ export const RETIRED_ADDRESS_KEYS = Object.freeze({
144
+ landing:
145
+ "is a retired option delete it. It named which note addressed a " +
146
+ "whole section rather than a page within one, and there are no " +
147
+ "sections to address: a section is a Hugo content directory the note " +
148
+ "format does not carry, so no note lands one and every page is " +
149
+ "addressed `<type>-<shortcode>`. Nothing replaces it",
157
150
  });
158
151
 
159
152
  /**
@@ -166,11 +159,12 @@ export const RETIRED_LANDING_RULES = Object.freeze({
166
159
  * knowledge, held in `PACKAGE_BASE` (`engine/kb-manifest.mjs`) and prefixed at
167
160
  * resolve time, so it is never recorded here (#1465).
168
161
  *
169
- * `landing` is inert see {@link LANDING_RULES}.
162
+ * It is the whole scheme: `landing`, the key that named which note addressed a
163
+ * whole section, is retired with the sections themselves — see
164
+ * {@link RETIRED_ADDRESS_KEYS}.
170
165
  */
171
166
  export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
172
167
  prefix: "",
173
- landing: "readme",
174
168
  });
175
169
 
176
170
  /**
@@ -471,10 +465,7 @@ export function publishesContentPages(config) {
471
465
 
472
466
  /**
473
467
  * @typedef {object} AddressSchemeInput
474
- * @property {string} [prefix] Where the content tree mounts inside the package.
475
- * @property {string} [landing] Which note addressed a whole section. Inert
476
- * since #204 retired sections from the note format — see
477
- * {@link LANDING_RULES}.
468
+ * @property {string} [prefix] Where the content tree mounts inside the package.
478
469
  */
479
470
 
480
471
  /**
@@ -658,7 +649,7 @@ const SITE_KEYS = [
658
649
  "backfillSections",
659
650
  ];
660
651
  const SITE_TREE_KEYS = ["from", "section"];
661
- const SECTION_META_KEYS = ["title", "banner", "description"];
652
+ const SECTION_META_KEYS = ["title", "banner", "description", "listType", "listSubType"];
662
653
  const DOC_PAGE_KEYS = ["title", "out", "preamble"];
663
654
  const RELATIONSHIP_KINDS = ["systems", "requires", "recommends", "conflicts"];
664
655
  const RELATIONSHIP_KEYS = ["id", "type", "manifest", "compatibility", "itemCatalog"];
@@ -695,7 +686,7 @@ const STATS_KEYS = ["lastModifiedBy"];
695
686
  export const DERIVED_SYSTEM_VERSION = Symbol.for("package-build.derivedSystemVersion");
696
687
  const PUBLISH_KEYS = ["site", "manifests", "address"];
697
688
  const MANIFEST_KEYS = ["publish", "consume"];
698
- const ADDRESS_KEYS = ["prefix", "landing"];
689
+ const ADDRESS_KEYS = ["prefix"];
699
690
 
700
691
  /** @param {unknown} value */
701
692
  function isPlainObject(value) {
@@ -1134,9 +1125,36 @@ function normalizeDocs(value) {
1134
1125
  * add to its title. Each is left off entirely rather than written as
1135
1126
  * `undefined`, which is not a value YAML can carry.
1136
1127
  *
1128
+ * **`listType` / `listSubType` say what the section lists**
1129
+ * (heroiclands-hugo-theme#50). Since #204 a section's directory holds nothing
1130
+ * but the `_index.md` written here, so a layout reading Hugo's `.Pages` finds
1131
+ * no members and renders an empty landing. The membership survives in this map
1132
+ * and nowhere a theme can reach it, so the landing states it and a layout
1133
+ * substitutes the equivalent `site.RegularPages` query — the same one `sohl`'s
1134
+ * catalog layouts already run, which is why `sohl`'s landings never broke.
1135
+ *
1136
+ * They are two keys of their own rather than `type` / `subType` because `type`
1137
+ * on an `_index.md` is **Hugo's own layout selector**: verified against Hugo
1138
+ * 0.165, a section landing carrying `type: doc` renders through
1139
+ * `layouts/doc/list.html` rather than the default list template, so spelling
1140
+ * the content type there would silently change which template serves the
1141
+ * landing. (This build already uses that behaviour deliberately, for the
1142
+ * mount's own landing.)
1143
+ *
1144
+ * Both are checked as **address segments**, which is the trap this came from:
1145
+ * a section is named for the URL a consumer chose and a subType is an address
1146
+ * segment, and the two need not agree — `/sohl/kb/user-guide/` is the section,
1147
+ * `userguide` the subType (#207). Copying the section's name into the
1148
+ * declaration would select no page at all, and an empty landing reported by
1149
+ * nobody is the failure being fixed. A `listSubType` with no `listType` is
1150
+ * refused for the same reason: a subType is only distinguishing *within* a
1151
+ * type — `rules`, `userguide` and `reference` are all `doc` — so alone it names
1152
+ * no query.
1153
+ *
1137
1154
  * @param {unknown} value - The declared entry.
1138
1155
  * @param {string} where - Dotted path, for the error.
1139
- * @returns {Readonly<{title: string, banner?: string, description?: string}>}
1156
+ * @returns {Readonly<{title: string, banner?: string, description?: string,
1157
+ * listType?: string, listSubType?: string}>}
1140
1158
  */
1141
1159
  function normalizeSectionMeta(value, where) {
1142
1160
  if (!isPlainObject(value)) fail(where, "must be a mapping");
@@ -1149,6 +1167,32 @@ function normalizeSectionMeta(value, where) {
1149
1167
  if (input.description !== undefined) {
1150
1168
  out.description = requireNonEmptyString(input.description, `${where}.description`);
1151
1169
  }
1170
+ for (const key of ["listType", "listSubType"]) {
1171
+ if (input[key] === undefined) continue;
1172
+ const segment = requireNonEmptyString(input[key], `${where}.${key}`);
1173
+ if (!isAddressSegment(segment)) {
1174
+ fail(
1175
+ `${where}.${key}`,
1176
+ `is \`${segment}\`, which is not alphanumeric. It names a ` +
1177
+ "content type or subType, and those are address segments " +
1178
+ `(${ADDRESS_SEGMENT_PATTERN.source}) — not the section's ` +
1179
+ "own name, which is a URL this site chose and need not " +
1180
+ "match (`user-guide` is the section, `userguide` the " +
1181
+ "subType). A value no page carries selects nothing and " +
1182
+ "leaves the landing empty",
1183
+ );
1184
+ }
1185
+ out[key] = segment;
1186
+ }
1187
+ if (out.listSubType !== undefined && out.listType === undefined) {
1188
+ fail(
1189
+ `${where}.listSubType`,
1190
+ "is declared without a `listType`. A subType tells pages apart " +
1191
+ "only within a type — `rules`, `userguide` and `reference` " +
1192
+ "are all `doc` — so on its own it names no query for a layout " +
1193
+ "to run",
1194
+ );
1195
+ }
1152
1196
  return Object.freeze(out);
1153
1197
  }
1154
1198
 
@@ -1731,6 +1775,15 @@ function normalizePublish(value) {
1731
1775
  fail("publish.address", "must be an object");
1732
1776
  }
1733
1777
  const address = /** @type {Record<string, unknown>} */ (addressInput ?? {});
1778
+ // A retired key is refused by name, ahead of the vocabulary check: reported
1779
+ // as merely unrecognized it would read as a misspelling of the one key that
1780
+ // survives, and the author would correct the spelling rather than learn
1781
+ // that the mechanism is gone (#215).
1782
+ for (const key of Object.keys(address)) {
1783
+ if (Object.hasOwn(RETIRED_ADDRESS_KEYS, key)) {
1784
+ fail(`publish.address.${key}`, RETIRED_ADDRESS_KEYS[key]);
1785
+ }
1786
+ }
1734
1787
  rejectUnknownKeys(address, ADDRESS_KEYS, "publish.address.");
1735
1788
 
1736
1789
  const prefix =
@@ -1749,24 +1802,9 @@ function normalizePublish(value) {
1749
1802
  fail("publish.address.prefix", "must not begin with a slash");
1750
1803
  }
1751
1804
 
1752
- const landing =
1753
- address.landing === undefined ?
1754
- DEFAULT_ADDRESS_SCHEME.landing
1755
- : optionalString(address.landing, "publish.address.landing");
1756
- // A retired rule is refused by name, before the vocabulary check: reported
1757
- // as merely unrecognized it would read as a misspelling of the one that
1758
- // survives, and the author would correct the value rather than learn that
1759
- // the mechanism is gone (#202).
1760
- if (Object.hasOwn(RETIRED_LANDING_RULES, landing)) {
1761
- fail("publish.address.landing", RETIRED_LANDING_RULES[landing]);
1762
- }
1763
- if (!LANDING_RULES.includes(landing)) {
1764
- fail("publish.address.landing", `must be one of ${LANDING_RULES.join(", ")}`);
1765
- }
1766
-
1767
1805
  return Object.freeze({
1768
1806
  site: normalizeSiteMode(publish.site),
1769
- address: Object.freeze({ prefix, landing }),
1807
+ address: Object.freeze({ prefix }),
1770
1808
  manifests: Object.freeze({
1771
1809
  publish: optionalBoolean(manifests.publish, "publish.manifests.publish", false),
1772
1810
  consume: optionalBoolean(manifests.consume, "publish.manifests.consume", false),
@@ -249,17 +249,16 @@ against the same constant a shortcode is checked against, and a note carrying a
249
249
  hyphenated value is reported where it wrote it:
250
250
 
251
251
  ```text
252
- Trauma/Blood_Loss.md:3:1: error: `subType` "blood-loss" is not an address segment — a subType is letters and digits only (^[A-Za-z0-9]+$), the same charset a shortcode is held to. The hyphen separates the segments of an address, so a value containing one is read back as two segments and resolves to nothing
252
+ Trauma/Blood_Loss.md:3:1: error: `subType` "blood-loss" is not a well-formed subType — a subType is letters and digits only (^[A-Za-z0-9]+$), the same charset a type, a shortcode and a contentPackage are held to. It is a vocabulary term the whole toolchain keys on, and one closed set away from being an address segment again, so a charset that held for every term but this one would be a rule nobody could state in a sentence
253
253
  ```
254
254
 
255
255
  One declared value broke the rule and has been renamed: a `doc`'s `user-guide`
256
- is now **`userguide`**. The old spelling is accepted for one transitional
257
- release and reported as a **warning** naming the replacement, so a tree that has
258
- not yet swept still builds:
259
-
260
- ```text
261
- User_Guide/Actions.md:3:1: warning: `subType` "user-guide" is a retired spelling of "userguide" on a doc; write "userguide".
262
- ```
256
+ is now **`userguide`**. The old spelling was accepted for one transitional
257
+ release, as a warning naming the replacement, so the 43 `sohl` notes authoring
258
+ it were not invalidated by the release that renamed them. Every consumer tree
259
+ has swept, so the acceptance is gone (#210) and `user-guide` is refused by the
260
+ charset check — it contains a hyphen, which is the reason that always applied.
261
+ No retirement-specific code outlived the sweep.
263
262
 
264
263
  That is a guarantee rather than an observation, and it holds: of **4,456 distinct
265
264
  shortcodes** across the four content trees, not one contains a character outside
@@ -32,12 +32,12 @@
32
32
  // The scheme vocabulary is part of the configuration contract — a
33
33
  // repository names its scheme in `package-build.config.yaml` — so it is
34
34
  // declared beside the rest of that vocabulary rather than here, and this
35
- // module reads it. `config.mjs` is the leaf entry point and imports nothing
36
- // but `node:path` and `engine/ids.mjs`, so the direction cannot close a
37
- // cycle (see `engine/pack-config.mjs`).
38
- import { DEFAULT_ADDRESS_SCHEME, LANDING_RULES } from "../content-config.mjs";
35
+ // module re-exports it beside the addresses it derives. `config.mjs` is the
36
+ // leaf entry point and imports nothing but `node:path` and `engine/ids.mjs`,
37
+ // so the direction cannot close a cycle (see `engine/pack-config.mjs`).
38
+ import { DEFAULT_ADDRESS_SCHEME } from "../content-config.mjs";
39
39
 
40
- export { DEFAULT_ADDRESS_SCHEME, LANDING_RULES };
40
+ export { DEFAULT_ADDRESS_SCHEME };
41
41
 
42
42
  /** The knowledgebase's mount within this package's site (#1470). */
43
43
  export const KB_PREFIX = "kb/";
@@ -116,26 +116,18 @@ export function contentAddress(fm) {
116
116
  * `/<package>/api/` for generated API docs, neither of which contains a hyphen
117
117
  * or names a type.
118
118
  *
119
+ * **It takes no address scheme.** It took one until #215, to validate the
120
+ * `landing` rule it then discarded; with that key retired, `prefix` was the
121
+ * only thing left in the scheme and the paragraph above is the reason it never
122
+ * applied. A parameter read by nothing is the defect this deletion is about.
123
+ *
119
124
  * @param {object} fm - Parsed frontmatter.
120
- * @param {object} [options] - Options.
121
- * @param {{prefix?: string, landing?: string}} [options.scheme] - The
122
- * repository's address scheme; defaults to {@link DEFAULT_ADDRESS_SCHEME}.
123
- * `landing` is validated against {@link LANDING_RULES} and selects nothing —
124
- * it is accepted so a configuration declaring the still-true `landing: readme`
125
- * keeps loading, and is removed once none does.
126
125
  * @returns {string} The package-relative address, with a trailing slash and no
127
126
  * leading one.
128
127
  * @throws {Error} When the note has no type or no shortcode to be addressed by.
129
128
  * Such a note is not published, and inventing an address for one would put a
130
129
  * dead entry in the manifest.
131
130
  */
132
- export function packageAddress(fm, { scheme } = {}) {
133
- const { landing } = { ...DEFAULT_ADDRESS_SCHEME, ...scheme };
134
- if (!LANDING_RULES.includes(landing)) {
135
- throw new Error(
136
- `unknown landing rule ${JSON.stringify(landing)} — expected one ` +
137
- `of ${LANDING_RULES.join(", ")}`,
138
- );
139
- }
131
+ export function packageAddress(fm) {
140
132
  return `${addressSlug(fm)}/`;
141
133
  }
@@ -61,13 +61,7 @@ import { positionInFrontmatter, positionOfFrontmatterPath } from "./diagnostics.
61
61
  import { checkHomepageAddressFields } from "./homepage.mjs";
62
62
  import { RETIRED_TYPES } from "./ids.mjs";
63
63
  import { isAddressSegment } from "./address-charset.mjs";
64
- import {
65
- declaredTags,
66
- retiredSubType,
67
- retiredSubTypeMessage,
68
- subTypeCharsetMessage,
69
- typeCharsetMessage,
70
- } from "./note-vocabulary.mjs";
64
+ import { declaredTags, subTypeCharsetMessage, typeCharsetMessage } from "./note-vocabulary.mjs";
71
65
  import {
72
66
  RETIRED_FIELD_ALIASES,
73
67
  declaresRetiredAlias,
@@ -342,11 +336,18 @@ function checkDataContainer(note, { type, fields }) {
342
336
  * called, and `rules`, `userguide`, `reference` mean three genres and nothing
343
337
  * else.
344
338
  *
345
- * **Three checks, in this order** — retired spelling, then charset, then the
346
- * closed set (#206, #204). Each is ahead of the next because it is the more
347
- * specific statement about the same value: a retired spelling has a named
348
- * replacement, a hyphenated value is unaddressable whatever the type declares,
349
- * and only then is the type's own list the reason.
339
+ * **Two checks, in this order** — the charset, then the closed set (#206,
340
+ * #204). The charset is first because it is the more general statement about
341
+ * the same value: a value outside `^[A-Za-z0-9]+$` is refused whatever the type
342
+ * declares, and only once it is a well-formed term is the type's own list the
343
+ * reason to refuse it.
344
+ *
345
+ * There were three. #206 ran a retired-spelling check ahead of both, accepting
346
+ * `user-guide` as a warning naming `userguide`, so the 43 `sohl` notes
347
+ * authoring it were not invalidated by the release that renamed it. Every
348
+ * consumer tree has swept, so the acceptance guarded nothing and is gone: the
349
+ * old spelling now falls through to the charset check, which refuses it for the
350
+ * reason that always applied — it contains a hyphen (#210).
350
351
  *
351
352
  * @param {object} note - The note.
352
353
  * @param {object} opts
@@ -374,26 +375,7 @@ function checkSubType(note, { type, entry }) {
374
375
  ];
375
376
  }
376
377
 
377
- // A retired spelling is **accepted**, and said out loud (#206). Checked
378
- // before the charset, because the note is not wrong about the charset in
379
- // some general way — it is wrong about one value, and naming the
380
- // replacement is the whole of what the author needs. A warning rather than
381
- // an error for the same reason the retired field aliases below are: the
382
- // note compiles to the correct page, and erroring would red every tree the
383
- // moment it took this release, ahead of any chance to sweep.
384
- const replacement = retiredSubType(type, value);
385
- if (replacement) {
386
- return [
387
- {
388
- file: note.file,
389
- ...at,
390
- severity: "warning",
391
- message: retiredSubTypeMessage(type, value, replacement),
392
- },
393
- ];
394
- }
395
-
396
- // The charset, before the closed set: a hyphenated value is unaddressable
378
+ // The charset, before the closed set: a value outside it is refused
397
379
  // whatever the type declares, and the type's list is not the reason it is
398
380
  // refused. Reported here rather than only for an enumerated type, so a
399
381
  // `subTypes: null` type — whose values nothing may yet check — is still
@@ -210,8 +210,7 @@ export function entriesForNote(fm, name, address, body, ctx) {
210
210
  * exist.
211
211
  *
212
212
  * @param {string} contentBase - Absolute path to the content tree.
213
- * @param {object} ctx - `{ contentPackage, foundryPackageId, packRouter,
214
- * scheme }`.
213
+ * @param {object} ctx - `{ contentPackage, foundryPackageId, packRouter }`.
215
214
  * @returns {{entries: Array<object>, notes: number,
216
215
  * skipped: Array<{file: string, reason: string}>}}
217
216
  */
@@ -249,7 +248,7 @@ export function collectManifestEntries(contentBase, ctx) {
249
248
 
250
249
  let address;
251
250
  try {
252
- address = packageAddress(fm, { scheme: ctx.scheme });
251
+ address = packageAddress(fm);
253
252
  } catch (err) {
254
253
  skipped.push({ file: rel, reason: err.message });
255
254
  continue;
@@ -261,7 +260,7 @@ export function collectManifestEntries(contentBase, ctx) {
261
260
  }
262
261
 
263
262
  /**
264
- * The identities and scheme an emission runs against, from configuration.
263
+ * The identities an emission runs against, from configuration.
265
264
  *
266
265
  * Resolved in one place and passed down, rather than read at each use, so the
267
266
  * pass itself is a pure function of its context and a test can drive it without
@@ -269,15 +268,13 @@ export function collectManifestEntries(contentBase, ctx) {
269
268
  *
270
269
  * @param {object} [config] - A resolved configuration; loaded when omitted.
271
270
  * @returns {{contentPackage: string, foundryPackageId: string, packRouter: object,
272
- * scheme: {prefix: string, landing: string}, web: boolean,
273
- * skipDirectories: readonly string[]}}
271
+ * web: boolean, skipDirectories: readonly string[]}}
274
272
  */
275
273
  export function manifestContext(config = loadPackConfig()) {
276
274
  return {
277
275
  contentPackage: config.contentPackage,
278
276
  foundryPackageId: config.foundryPackage,
279
277
  packRouter: routerFor(config),
280
- scheme: config.publish.address,
281
278
  web: publishesContentPages(config),
282
279
  // The walk's own configuration, threaded through rather than left to
283
280
  // its default, so a caller that passes a config drives every read.
@@ -663,11 +663,11 @@ export const NOTE_VOCABULARY = Object.freeze({
663
663
  /* ----- core documents ------------------------------------------- */
664
664
 
665
665
  doc: Object.freeze({
666
- // `userguide`, not `user-guide`: a `doc` routes by its subType, so the
667
- // value is a path segment, and a segment carries no hyphen (#206). The
668
- // old spelling is accepted transitionally see {@link RETIRED_SUBTYPES}
669
- // but it is not declared here, because this list is what the format
670
- // says a note *should* write.
666
+ // `userguide`, not `user-guide`: a subType is held to the address
667
+ // charset, and a segment carries no hyphen (#206). The old spelling was
668
+ // accepted transitionally for one release so the consumer trees could
669
+ // sweep; they have, so it is refused by the charset check now, with no
670
+ // retirement-specific code left over (#210).
671
671
  subTypes: Object.freeze(["rules", "userguide", "reference"]),
672
672
  data: Object.freeze([]),
673
673
  }),
@@ -732,83 +732,32 @@ export const NOTE_VOCABULARY = Object.freeze({
732
732
  });
733
733
 
734
734
  /**
735
- * The retired spelling of a subType a type declares what to write now (#206).
736
- *
737
- * Keyed by type, because a retirement is a statement about *that type's*
738
- * vocabulary: `user-guide` on a `doc` is the old spelling of `userguide`, while
739
- * the same string on any other type is nothing but a charset violation, and
740
- * saying "did you mean userguide" there would be a guess dressed as a fact.
741
- *
742
- * **Recorded here rather than left in `subTypes`** so the declared list stays
743
- * the list of values a note *should* write. A retired value is accepted, not
744
- * declared — the difference is exactly what makes the finding possible.
745
- *
746
- * **Deliberately not the shape of a type rename** ({@link
747
- * import("./ids.mjs").RETIRED_TYPES}), which is an error: a retired type routes
748
- * a note to the wrong pack, whereas a retired subType still compiles to the
749
- * correct page. The sweep is the consumer's, and the ordering is the reverse of
750
- * the usual — the acceptance ships *first*, because declaring only the new
751
- * spelling while 43 `sohl` notes still author the old one would invalidate all
752
- * 43 with a release they had no chance to sweep ahead of. A later change
753
- * removes this map, and the old spelling then falls through to the ordinary
754
- * undeclared-value error with no code left to remove.
755
- *
756
- * @type {Readonly<Record<string, Readonly<Record<string, string>>>>}
757
- */
758
- export const RETIRED_SUBTYPES = Object.freeze({
759
- doc: Object.freeze({ "user-guide": "userguide" }),
760
- });
761
-
762
- /**
763
- * What to write in place of a retired subType value, if it is one.
735
+ * What a note carrying a subType outside the address charset is told.
764
736
  *
765
- * @param {string} type - The note's `type`.
766
- * @param {string} value - The authored `subType`.
767
- * @param {Readonly<Record<string, Readonly<Record<string, string>>>>} [retired]
768
- * The map to read, defaulting to {@link RETIRED_SUBTYPES}.
769
- * @returns {string|undefined} The current spelling, or `undefined` when the
770
- * value is not a retired one which is not the same as it being valid.
771
- */
772
- export function retiredSubType(type, value, retired = RETIRED_SUBTYPES) {
773
- const forType = retired?.[type];
774
- if (!forType || !Object.hasOwn(forType, value)) return undefined;
775
- return forType[value];
776
- }
777
-
778
- /**
779
- * What a note carrying a retired subType is told.
737
+ * **Why the charset holds for a subType, which reaches no address.** #206 said
738
+ * "the hyphen separates the segments of an address", and that was true of a
739
+ * subType when it shipped: `sectionOf` returned a `doc`'s subType, so the value
740
+ * was a URL path segment. #204 retired sections and it is not one now. The rule
741
+ * stays, on its own footing: a subType is a vocabulary term the whole toolchain
742
+ * keys on, and it is one closed set away from being an address segment again
743
+ * so the reason to spell it in the address charset is that a charset holding
744
+ * for a type, a shortcode and a `contentPackage` but not for a subType is a
745
+ * rule nobody can state in a sentence.
780
746
  *
781
- * One message, so the lint and any later refusal cannot describe the same
782
- * retirement differently.
783
- *
784
- * @param {string} type - The note's `type`.
785
- * @param {string} value - The retired spelling the note carries.
786
- * @param {string} replacement - What to write instead.
787
- * @returns {string} The message.
788
- */
789
- export function retiredSubTypeMessage(type, value, replacement) {
790
- return (
791
- `\`subType\` "${value}" is a retired spelling of "${replacement}" on a ` +
792
- `${type}; write "${replacement}". A subType is an address segment, and ` +
793
- `a segment is ${ADDRESS_SEGMENT_PATTERN.source} — the hyphen separates ` +
794
- `segments, so it can never occur inside one. The old spelling is still ` +
795
- `accepted, and will stop being accepted once the trees have swept`
796
- );
797
- }
798
-
799
- /**
800
- * What a note carrying a subType outside the address charset is told.
747
+ * Contrast {@link typeCharsetMessage}, which keeps the address reasoning
748
+ * because a type genuinely is the first segment of every address.
801
749
  *
802
750
  * @param {string} value - The authored `subType`.
803
751
  * @returns {string} The message.
804
752
  */
805
753
  export function subTypeCharsetMessage(value) {
806
754
  return (
807
- `\`subType\` "${value}" is not an address segment — a subType is ` +
755
+ `\`subType\` "${value}" is not a well-formed subType — a subType is ` +
808
756
  `letters and digits only (${ADDRESS_SEGMENT_PATTERN.source}), the same ` +
809
- `charset a shortcode is held to. The hyphen separates the segments of ` +
810
- `an address, so a value containing one is read back as two segments ` +
811
- `and resolves to nothing`
757
+ `charset a type, a shortcode and a contentPackage are held to. It is a ` +
758
+ `vocabulary term the whole toolchain keys on, and one closed set away ` +
759
+ `from being an address segment again, so a charset that held for every ` +
760
+ `term but this one would be a rule nobody could state in a sentence`
812
761
  );
813
762
  }
814
763
 
@@ -836,6 +785,11 @@ export function typeCharsetMessage(type) {
836
785
  * a note's bad value is one author's mistake and belongs in a report, while a
837
786
  * bad *declaration* would tell every author to write something unaddressable.
838
787
  *
788
+ * The message states the reason **per key**, as {@link typeCharsetMessage} and
789
+ * {@link subTypeCharsetMessage} do: a type is an address segment, and a subType
790
+ * has not been one since #204 retired sections, so a single claim covering both
791
+ * would be half wrong (#210).
792
+ *
839
793
  * @param {Readonly<Record<string, TypeVocabulary>>} vocabulary - The registry.
840
794
  * @param {string} [where] - What declares it, for the message.
841
795
  * @throws {Error} Naming every offending type and subType at once, rather than
@@ -854,9 +808,13 @@ export function assertVocabularyCharset(vocabulary, where = "the note vocabulary
854
808
  if (!bad.length) return;
855
809
  throw new Error(
856
810
  `${where} declares ${bad.join(", ")}, which ${bad.length === 1 ? "is" : "are"} ` +
857
- `not ${ADDRESS_SEGMENT_PATTERN.source}. A type and a subType are both ` +
858
- `address segments, and the hyphen separates segments rather than ` +
859
- `occurring inside one.`,
811
+ `not ${ADDRESS_SEGMENT_PATTERN.source}. A type is an address segment, ` +
812
+ `and the hyphen separates segments rather than occurring inside one. ` +
813
+ `A subType reaches no address since #204 retired sections, and is ` +
814
+ `held to the same charset anyway: it is a vocabulary term the whole ` +
815
+ `toolchain keys on, one closed set away from being a segment again, ` +
816
+ `and a charset holding for every term but that one would be a rule ` +
817
+ `nobody could state in a sentence.`,
860
818
  );
861
819
  }
862
820
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "13.0.0",
3
+ "version": "14.0.0",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -56,36 +56,31 @@ export namespace DEFAULT_PATHS {
56
56
  */
57
57
  export const PACK_DOCUMENT_TYPES: readonly ["Actor", "Adventure", "Item", "JournalEntry", "Macro", "Scene"];
58
58
  /**
59
- * The landing-page rules a repository may route by. **Inert since #204.**
60
- *
61
- * A *landing page* was a note that addressed a whole section rather than a page
62
- * within one, so it had no slug of its own. There are no sections in the note
63
- * format any more a section is a Hugo content directory, and a page's address
64
- * names no directory so there are no landings and this selects nothing.
65
- *
66
- * The key survives its own mechanism on purpose. Both publishing consumers
67
- * declare `landing: readme`, which stated something true when they wrote it;
68
- * refusing it now would break them over a correct statement, and silently
69
- * ignoring an unknown value would be worse. So `readme` stays accepted, the
70
- * retired `collection` stays refused by name (below), and the key is deleted
71
- * once no configuration writes it `content-config.mjs` has no warning channel
72
- * with which to say "accepted, and does nothing" in between.
73
- *
74
- * @type {readonly string[]}
75
- */
76
- export const LANDING_RULES: readonly string[];
77
- /**
78
- * What a configuration naming the retired `collection` landing rule is told.
79
- *
80
- * A retired *value* is refused the way a retired *field* is (see
81
- * `engine/retired-fields.mjs`): left merely unrecognized it would be reported
82
- * as a bad value, which names something to correct and leaves the author to
83
- * work out for themselves that the mechanism is gone. The message says the rule
84
- * is retired, what lands a section instead, and what to do with the key.
59
+ * Address-scheme keys a configuration may no longer declare.
60
+ *
61
+ * A retired key has exactly two possible fates, and only one of them is honest
62
+ * the same reasoning `engine/retired-fields.mjs` applies to a retired
63
+ * frontmatter field. Left honoured, it keeps doing whatever it did, which is
64
+ * why it was retired. Left *ignored*, it reads to its author as though it still
65
+ * works: the configuration says one thing and the build does another, and
66
+ * nothing says so. This module has no third option, because it has no warning
67
+ * channel every finding goes through `fail()`, which throws. So a retired
68
+ * key is **refused**, at the line it was written on, with a message that says
69
+ * the mechanism is gone rather than naming a value to correct.
70
+ *
71
+ * **What `landing` did (#204).** It named which note addressed a whole section
72
+ * rather than a page within one a *landing page*, which therefore had no slug
73
+ * of its own. #203 retired the second of its two rules and #204 retired the
74
+ * concept both rules chose between: a section is a Hugo content directory that
75
+ * the note format does not carry, a page's address names no directory, and so
76
+ * no note lands anything. The key outlived its mechanism by one release only
77
+ * because both publishing consumers still declared the then-true
78
+ * `landing: readme`, and neither breaking them over a correct statement nor
79
+ * accepting the key in silence was acceptable. Neither declares it now.
85
80
  *
86
81
  * @type {Readonly<Record<string, string>>}
87
82
  */
88
- export const RETIRED_LANDING_RULES: Readonly<Record<string, string>>;
83
+ export const RETIRED_ADDRESS_KEYS: Readonly<Record<string, string>>;
89
84
  /**
90
85
  * A repository's address scheme, with the defaults an unconfigured one gets.
91
86
  *
@@ -96,11 +91,12 @@ export const RETIRED_LANDING_RULES: Readonly<Record<string, string>>;
96
91
  * knowledge, held in `PACKAGE_BASE` (`engine/kb-manifest.mjs`) and prefixed at
97
92
  * resolve time, so it is never recorded here (#1465).
98
93
  *
99
- * `landing` is inert see {@link LANDING_RULES}.
94
+ * It is the whole scheme: `landing`, the key that named which note addressed a
95
+ * whole section, is retired with the sections themselves — see
96
+ * {@link RETIRED_ADDRESS_KEYS}.
100
97
  */
101
98
  export const DEFAULT_ADDRESS_SCHEME: Readonly<{
102
99
  prefix: "";
103
- landing: "readme";
104
100
  }>;
105
101
  /**
106
102
  * How much of a package reaches the web.
@@ -515,12 +511,6 @@ export type AddressSchemeInput = {
515
511
  * Where the content tree mounts inside the package.
516
512
  */
517
513
  prefix?: string | undefined;
518
- /**
519
- * Which note addressed a whole section. Inert
520
- * since #204 retired sections from the note format — see
521
- * {@link LANDING_RULES}.
522
- */
523
- landing?: string | undefined;
524
514
  };
525
515
  /**
526
516
  * One entry of a consumer's `itemBuilders` registry.
@@ -55,27 +55,20 @@ export function contentAddress(fm: object): string;
55
55
  * `/<package>/api/` for generated API docs, neither of which contains a hyphen
56
56
  * or names a type.
57
57
  *
58
+ * **It takes no address scheme.** It took one until #215, to validate the
59
+ * `landing` rule it then discarded; with that key retired, `prefix` was the
60
+ * only thing left in the scheme and the paragraph above is the reason it never
61
+ * applied. A parameter read by nothing is the defect this deletion is about.
62
+ *
58
63
  * @param {object} fm - Parsed frontmatter.
59
- * @param {object} [options] - Options.
60
- * @param {{prefix?: string, landing?: string}} [options.scheme] - The
61
- * repository's address scheme; defaults to {@link DEFAULT_ADDRESS_SCHEME}.
62
- * `landing` is validated against {@link LANDING_RULES} and selects nothing —
63
- * it is accepted so a configuration declaring the still-true `landing: readme`
64
- * keeps loading, and is removed once none does.
65
64
  * @returns {string} The package-relative address, with a trailing slash and no
66
65
  * leading one.
67
66
  * @throws {Error} When the note has no type or no shortcode to be addressed by.
68
67
  * Such a note is not published, and inventing an address for one would put a
69
68
  * dead entry in the manifest.
70
69
  */
71
- export function packageAddress(fm: object, { scheme }?: {
72
- scheme?: {
73
- prefix?: string;
74
- landing?: string;
75
- } | undefined;
76
- }): string;
70
+ export function packageAddress(fm: object): string;
71
+ export { DEFAULT_ADDRESS_SCHEME };
77
72
  /** The knowledgebase's mount within this package's site (#1470). */
78
73
  export const KB_PREFIX: "kb/";
79
74
  import { DEFAULT_ADDRESS_SCHEME } from "../content-config.mjs";
80
- import { LANDING_RULES } from "../content-config.mjs";
81
- export { DEFAULT_ADDRESS_SCHEME, LANDING_RULES };
@@ -51,8 +51,7 @@ export function entriesForNote(fm: object, name: string, address: string, body:
51
51
  * exist.
52
52
  *
53
53
  * @param {string} contentBase - Absolute path to the content tree.
54
- * @param {object} ctx - `{ contentPackage, foundryPackageId, packRouter,
55
- * scheme }`.
54
+ * @param {object} ctx - `{ contentPackage, foundryPackageId, packRouter }`.
56
55
  * @returns {{entries: Array<object>, notes: number,
57
56
  * skipped: Array<{file: string, reason: string}>}}
58
57
  */
@@ -65,7 +64,7 @@ export function collectManifestEntries(contentBase: string, ctx: object): {
65
64
  }>;
66
65
  };
67
66
  /**
68
- * The identities and scheme an emission runs against, from configuration.
67
+ * The identities an emission runs against, from configuration.
69
68
  *
70
69
  * Resolved in one place and passed down, rather than read at each use, so the
71
70
  * pass itself is a pure function of its context and a test can drive it without
@@ -73,17 +72,12 @@ export function collectManifestEntries(contentBase: string, ctx: object): {
73
72
  *
74
73
  * @param {object} [config] - A resolved configuration; loaded when omitted.
75
74
  * @returns {{contentPackage: string, foundryPackageId: string, packRouter: object,
76
- * scheme: {prefix: string, landing: string}, web: boolean,
77
- * skipDirectories: readonly string[]}}
75
+ * web: boolean, skipDirectories: readonly string[]}}
78
76
  */
79
77
  export function manifestContext(config?: object): {
80
78
  contentPackage: string;
81
79
  foundryPackageId: string;
82
80
  packRouter: object;
83
- scheme: {
84
- prefix: string;
85
- landing: string;
86
- };
87
81
  web: boolean;
88
82
  skipDirectories: readonly string[];
89
83
  };
@@ -36,30 +36,20 @@ export function hasTag(fm: object | null | undefined, tag: string): boolean;
36
36
  */
37
37
  export function isDraftNote(fm: object | null | undefined): boolean;
38
38
  /**
39
- * What to write in place of a retired subType value, if it is one.
40
- *
41
- * @param {string} type - The note's `type`.
42
- * @param {string} value - The authored `subType`.
43
- * @param {Readonly<Record<string, Readonly<Record<string, string>>>>} [retired]
44
- * The map to read, defaulting to {@link RETIRED_SUBTYPES}.
45
- * @returns {string|undefined} The current spelling, or `undefined` when the
46
- * value is not a retired one — which is not the same as it being valid.
47
- */
48
- export function retiredSubType(type: string, value: string, retired?: Readonly<Record<string, Readonly<Record<string, string>>>>): string | undefined;
49
- /**
50
- * What a note carrying a retired subType is told.
39
+ * What a note carrying a subType outside the address charset is told.
51
40
  *
52
- * One message, so the lint and any later refusal cannot describe the same
53
- * retirement differently.
41
+ * **Why the charset holds for a subType, which reaches no address.** #206 said
42
+ * "the hyphen separates the segments of an address", and that was true of a
43
+ * subType when it shipped: `sectionOf` returned a `doc`'s subType, so the value
44
+ * was a URL path segment. #204 retired sections and it is not one now. The rule
45
+ * stays, on its own footing: a subType is a vocabulary term the whole toolchain
46
+ * keys on, and it is one closed set away from being an address segment again —
47
+ * so the reason to spell it in the address charset is that a charset holding
48
+ * for a type, a shortcode and a `contentPackage` but not for a subType is a
49
+ * rule nobody can state in a sentence.
54
50
  *
55
- * @param {string} type - The note's `type`.
56
- * @param {string} value - The retired spelling the note carries.
57
- * @param {string} replacement - What to write instead.
58
- * @returns {string} The message.
59
- */
60
- export function retiredSubTypeMessage(type: string, value: string, replacement: string): string;
61
- /**
62
- * What a note carrying a subType outside the address charset is told.
51
+ * Contrast {@link typeCharsetMessage}, which keeps the address reasoning
52
+ * because a type genuinely is the first segment of every address.
63
53
  *
64
54
  * @param {string} value - The authored `subType`.
65
55
  * @returns {string} The message.
@@ -80,6 +70,11 @@ export function typeCharsetMessage(type: string): string;
80
70
  * a note's bad value is one author's mistake and belongs in a report, while a
81
71
  * bad *declaration* would tell every author to write something unaddressable.
82
72
  *
73
+ * The message states the reason **per key**, as {@link typeCharsetMessage} and
74
+ * {@link subTypeCharsetMessage} do: a type is an address segment, and a subType
75
+ * has not been one since #204 retired sections, so a single claim covering both
76
+ * would be half wrong (#210).
77
+ *
83
78
  * @param {Readonly<Record<string, TypeVocabulary>>} vocabulary - The registry.
84
79
  * @param {string} [where] - What declares it, for the message.
85
80
  * @throws {Error} Naming every offending type and subType at once, rather than
@@ -288,31 +283,6 @@ export const NOTE_VOCABULARY: Readonly<{
288
283
  })[];
289
284
  }>;
290
285
  }>;
291
- /**
292
- * The retired spelling of a subType a type declares → what to write now (#206).
293
- *
294
- * Keyed by type, because a retirement is a statement about *that type's*
295
- * vocabulary: `user-guide` on a `doc` is the old spelling of `userguide`, while
296
- * the same string on any other type is nothing but a charset violation, and
297
- * saying "did you mean userguide" there would be a guess dressed as a fact.
298
- *
299
- * **Recorded here rather than left in `subTypes`** so the declared list stays
300
- * the list of values a note *should* write. A retired value is accepted, not
301
- * declared — the difference is exactly what makes the finding possible.
302
- *
303
- * **Deliberately not the shape of a type rename** ({@link
304
- * import("./ids.mjs").RETIRED_TYPES}), which is an error: a retired type routes
305
- * a note to the wrong pack, whereas a retired subType still compiles to the
306
- * correct page. The sweep is the consumer's, and the ordering is the reverse of
307
- * the usual — the acceptance ships *first*, because declaring only the new
308
- * spelling while 43 `sohl` notes still author the old one would invalidate all
309
- * 43 with a release they had no chance to sweep ahead of. A later change
310
- * removes this map, and the old spelling then falls through to the ordinary
311
- * undeclared-value error with no code left to remove.
312
- *
313
- * @type {Readonly<Record<string, Readonly<Record<string, string>>>>}
314
- */
315
- export const RETIRED_SUBTYPES: Readonly<Record<string, Readonly<Record<string, string>>>>;
316
286
  /**
317
287
  * One `data:` key a note type may carry.
318
288
  *