@heroiclands/package-build 11.1.0 → 13.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.
@@ -116,28 +116,45 @@ export const PACK_DOCUMENT_TYPES = /** @type {const} */ ([
116
116
  ]);
117
117
 
118
118
  /**
119
- * The landing-page rules a repository may route by.
119
+ * The landing-page rules a repository may route by. **Inert since #204.**
120
120
  *
121
- * A *landing page* is a note that addresses a whole section rather than a page
122
- * within one, so it has no slug of its own. Which note that is differs between
123
- * repositories, and both live rules are represented here because switching
124
- * either one on for the other repository moves addresses that are already
125
- * published:
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.
126
125
  *
127
- * - `readme` a `README.md` **is** its section's landing page. This is `sohl`'s
128
- * rule, and a `doc` note routes by its `category` like any other, so `sohl`'s
129
- * eleven `category: collection` notes publish under a literal `collection/`
130
- * section (`kb/collection/skills/`).
131
- * - `collection` a `doc` note whose `category` is `collection` addresses the
132
- * section it introduces, named by its authored `section`. This is `thalorna`'s
133
- * rule, under which the same note publishes at `creature/`.
134
- *
135
- * The two are not disjoint and cannot simply both apply: each tree holds notes
136
- * the other rule would move.
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.
137
133
  *
138
134
  * @type {readonly string[]}
139
135
  */
140
- export const LANDING_RULES = Object.freeze(["readme", "collection"]);
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.
146
+ *
147
+ * @type {Readonly<Record<string, string>>}
148
+ */
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",
157
+ });
141
158
 
142
159
  /**
143
160
  * A repository's address scheme, with the defaults an unconfigured one gets.
@@ -148,6 +165,8 @@ export const LANDING_RULES = Object.freeze(["readme", "collection"]);
148
165
  * own mount point: where the package itself is served is the consuming build's
149
166
  * knowledge, held in `PACKAGE_BASE` (`engine/kb-manifest.mjs`) and prefixed at
150
167
  * resolve time, so it is never recorded here (#1465).
168
+ *
169
+ * `landing` is inert — see {@link LANDING_RULES}.
151
170
  */
152
171
  export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
153
172
  prefix: "",
@@ -210,34 +229,6 @@ export function publishesContentPages(config) {
210
229
  return config.publish.site === "content";
211
230
  }
212
231
 
213
- /**
214
- * Every URL section this repository names, in declaration order (#197).
215
- *
216
- * A section is *declared* by describing it: `site.sections` for one whose
217
- * landing this build generates, `site.readmeSections` for one whose landing is
218
- * a `README`. Between them they are the open set of addresses a repository
219
- * says it publishes under — which is what a `README` landing's `subType` names,
220
- * since `sectionOf` reads that field as the section rather than as a genre.
221
- *
222
- * Read from the same two maps the site build renders each landing from, so the
223
- * set a note is checked against and the set a landing is written from cannot
224
- * come to disagree. A repository that describes no section declares none, and
225
- * the answer is empty rather than a guess assembled from the tree.
226
- *
227
- * @param {{site: {sections: object, readmeSections: object}}} config - A
228
- * resolved configuration.
229
- * @returns {readonly string[]} The section names, deduplicated.
230
- */
231
- export function declaredSections(config) {
232
- const site = config?.site ?? {};
233
- return Object.freeze([
234
- ...new Set([
235
- ...Object.keys(site.sections ?? {}),
236
- ...Object.keys(site.readmeSections ?? {}),
237
- ]),
238
- ]);
239
- }
240
-
241
232
  /**
242
233
  * @typedef {"systems" | "modules"} PackageKind
243
234
  */
@@ -481,7 +472,9 @@ export function declaredSections(config) {
481
472
  /**
482
473
  * @typedef {object} AddressSchemeInput
483
474
  * @property {string} [prefix] Where the content tree mounts inside the package.
484
- * @property {string} [landing] Which note addresses a whole section.
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}.
485
478
  */
486
479
 
487
480
  /**
@@ -1120,10 +1113,11 @@ function normalizeDocs(value) {
1120
1113
  * One section's landing metadata — what a section says about itself on the
1121
1114
  * `_index.md` this build generates for it.
1122
1115
  *
1123
- * A generated landing is the *only* place a section can speak: a content
1124
- * package has no authored `_index.md` for `weapongear` or `affliction`, so the
1125
- * file the theme reads is the one the site build writes. This is therefore the
1126
- * whole vocabulary, and it is deliberately a **closed** one.
1116
+ * A generated landing is the *only* place a section can speak, and since #204 it
1117
+ * is the only place a section **exists**: a content page is addressed
1118
+ * `(type, shortcode)` and written flat under the mount, so no page creates a
1119
+ * directory and nothing else makes `<prefix><section>/` answer. This is
1120
+ * therefore the whole vocabulary, and it is deliberately a **closed** one.
1127
1121
  *
1128
1122
  * The alternative — passing whatever a section declared straight through, as
1129
1123
  * `site.landing` does — was weighed and refused. `landing` is written once, for
@@ -1759,6 +1753,13 @@ function normalizePublish(value) {
1759
1753
  address.landing === undefined ?
1760
1754
  DEFAULT_ADDRESS_SCHEME.landing
1761
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
+ }
1762
1763
  if (!LANDING_RULES.includes(landing)) {
1763
1764
  fail("publish.address.landing", `must be one of ${LANDING_RULES.join(", ")}`);
1764
1765
  }
@@ -233,10 +233,33 @@ field to supply it, so a bare shortcode is resolved across types and must match
233
233
  exactly one; an ambiguity is an error naming the candidates.
234
234
 
235
235
  **Parsing is positional counting from the right, and nothing else.** Every
236
- segment is alphanumeric — shortcodes are `^[A-Za-z0-9]+$`, types are bare words,
237
- systems come from a closed registry, and `contentPackage` is alphanumeric — so
238
- the hyphen is purely a separator. There is no longest-match against a roster and
239
- no vocabulary check before splitting.
236
+ segment is alphanumeric — shortcodes, **types** and **subTypes** are all
237
+ `^[A-Za-z0-9]+$`, systems come from a closed registry, and `contentPackage` is
238
+ alphanumeric — so the hyphen is purely a separator. There is no longest-match
239
+ against a roster and no vocabulary check before splitting.
240
+
241
+ **`type` and `subType` are held to that charset, not merely expected to meet
242
+ it** (#206). A type is the first segment of every address, so a hyphen in one is
243
+ read back as a segment boundary that was never meant as one. A `subType` reaches
244
+ no address since #204 retired sections, but it is held to the same rule all the
245
+ same: it is a vocabulary term the whole toolchain keys on, one closed set away
246
+ from being an address again, and a charset that holds for two of the three
247
+ segments and half of a fourth is a rule nobody can state. Both are checked
248
+ against the same constant a shortcode is checked against, and a note carrying a
249
+ hyphenated value is reported where it wrote it:
250
+
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
253
+ ```
254
+
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
+ ```
240
263
 
241
264
  That is a guarantee rather than an observation, and it holds: of **4,456 distinct
242
265
  shortcodes** across the four content trees, not one contains a character outside
@@ -267,7 +290,7 @@ was measured before it was retired, and the namespace was empty in practice:
267
290
  across 8,305 wikilinks in three content trees, **not one** bare link resolved to
268
291
  a note. What the index behind it did do was fold every note's `name.full` into
269
292
  itself, so two notes of one type could not share a display name — a rules page
270
- and a user-guide page both called "Gear" were a build failure whose every
293
+ and a user guide page both called "Gear" were a build failure whose every
271
294
  available fix moved a published URL (#179).
272
295
 
273
296
  The top-level `aliases:` that fed it is **retired** and refused. The nested
@@ -1356,29 +1379,30 @@ subType:
1356
1379
  subType:
1357
1380
 
1358
1381
  - rules: The rules of the game, independent of medium — valid at a table with paper and dice.
1359
- - user-guide: How to operate the Foundry implementation to play by the rules.
1382
+ - userguide: How to operate the Foundry implementation to play by the rules.
1360
1383
  - reference: Out-of-world lookup material about the setting or system — correspondences, conversions, glossaries.
1361
- - collection: A note whose content is **derived from the tree** — a roster, a table of
1362
- settlements, an index of languages, almost always a query. It differs from `reference` in
1363
- where its content comes from, not in who reads it: a reference **states** facts of its own,
1364
- and a collection **derives** them. The author's test is whether the page would still say
1365
- anything if every other note vanished. A collection can therefore collect nothing and still
1366
- render an empty table looks exactly like a full one until you read it — which is the
1367
- failure this value exists to make nameable.
1368
-
1369
- | `data` property | Values | Description |
1370
- | --------------- | -------- | --------------------------------------------------------------------------------------------------------- |
1371
- | `section` | `string` | For a `collection` only: the URL segment of the section it heads. Omit it and the note's own slug is used |
1372
-
1373
- **`section` is the segment, not a label.** A collection is a section's landing
1374
- page, so what it names is where that section lives and two collections may not
1375
- claim the same one. A collection that lists a _subset_ of a section therefore
1376
- names none: five of `sohl-thalorna`'s list places and three list affiliations, and
1377
- each falls back to its own slug rather than fighting over `/place/`.
1378
-
1379
- It is authored rather than derived because the note's own title is presentation:
1380
- a collection called "Creatures" heads the `being` section, and slugging the title
1381
- would put it somewhere else.
1384
+
1385
+ A `doc` declares no properties of its own.
1386
+
1387
+ **A page that introduces a type is an ordinary note, named by convention.**
1388
+ Write `type: doc`, `subType: reference`, `shortcode: <type>` so the
1389
+ affiliations introduction is `doc-affiliation`, addressed and linked like
1390
+ anything else, and typically carrying a generated table of what it introduces.
1391
+ It has no build path of its own; the package's own front page already works this
1392
+ way (`homepage-root`).
1393
+
1394
+ There is no landing page and no section. A `README.md` used to _be_ its
1395
+ section's landing, and a `subType: collection` note with a top-level `section:`
1396
+ key was a second way to say the same thing. All of it is retired the second
1397
+ rule in #202, the first in #204 because a section appears in **no address**: a
1398
+ page publishes at `/<package>/<type>-<shortcode>/`, which names no directory. A
1399
+ section is what Hugo calls a content directory, and the note format does not
1400
+ carry one.
1401
+
1402
+ So a `doc`'s `subType` is a **genre** and nothing else, closed to the three
1403
+ values above. It briefly had to accept a content type as well, because a
1404
+ landing's `subType` named the section it addressed; with no landings, one field
1405
+ has one reading again.
1382
1406
 
1383
1407
  ### type: macro
1384
1408
 
@@ -77,7 +77,11 @@ import {
77
77
  } from "./helpers.mjs";
78
78
  import { emitDiagnostic } from "./diagnostics.mjs";
79
79
  import { assertNoDeclaredPackage } from "./note-package.mjs";
80
- import { assertNoAliasesField, assertNoDraftField } from "./retired-fields.mjs";
80
+ import {
81
+ assertNoAliasesField,
82
+ assertNoDraftField,
83
+ assertNoSectionField,
84
+ } from "./retired-fields.mjs";
81
85
  import { assertTypeNotRetired, packForType } from "./ids.mjs";
82
86
  import { carriesSystemBlock } from "./system-block.mjs";
83
87
  import { checkAuthoredSystemData, checkEmittedSystemData } from "./schema-check.mjs";
@@ -784,6 +788,7 @@ export class BasePackCompiler {
784
788
  assertNoDeclaredPackage(fm, { absPath });
785
789
  assertNoDraftField(fm, { absPath });
786
790
  assertNoAliasesField(fm, { absPath });
791
+ assertNoSectionField(fm, { absPath });
787
792
  } catch (err) {
788
793
  stats.declined++;
789
794
  this.errorCount++;
@@ -42,20 +42,6 @@ export { DEFAULT_ADDRESS_SCHEME, LANDING_RULES };
42
42
  /** The knowledgebase's mount within this package's site (#1470). */
43
43
  export const KB_PREFIX = "kb/";
44
44
 
45
- /**
46
- * The URL section a note routes to.
47
- *
48
- * A `doc` is narrative content whose only identity is its subtype label, so it
49
- * routes by `subType`; every other type names its own section.
50
- *
51
- * @param {object} fm - Parsed frontmatter.
52
- * @returns {string|undefined} The section, or `undefined` when the note has
53
- * none — a `doc` with no subtype has no address and is not published.
54
- */
55
- export function sectionOf(fm) {
56
- return fm.type === "doc" ? fm.subType : fm.type;
57
- }
58
-
59
45
  /**
60
46
  * The single path segment a note is addressed by: `type-shortcode`.
61
47
  *
@@ -95,40 +81,17 @@ export function addressSlug(fm) {
95
81
  /**
96
82
  * A note's address below the knowledgebase mount, e.g. `affliction-aconite/`.
97
83
  *
98
- * A `README.md` **is** its section's landing page rather than a page within it,
99
- * so it addresses the section itself and has no address of its own.
84
+ * Every note, without exception. A `README.md` used to be its section's landing
85
+ * page and to address the section instead of itself; a section is a Hugo
86
+ * directory concept the note format no longer carries (#204), so a file's name
87
+ * decides nothing about where it publishes.
100
88
  *
101
89
  * @param {object} fm - Parsed frontmatter.
102
- * @param {boolean} isReadme - Whether the file is a `README.md`.
103
90
  * @returns {string} The mount-relative address, with a trailing slash.
104
91
  * @throws {Error} When the note has no address.
105
92
  */
106
- export function contentAddress(fm, isReadme) {
107
- return isReadme ? `${sectionOf(fm)}/` : `${addressSlug(fm)}/`;
108
- }
109
-
110
- /**
111
- * Whether a note is a landing page under a scheme, and what it lands at.
112
- *
113
- * @param {object} fm - Parsed frontmatter.
114
- * @param {boolean} isReadme - Whether the file is a `README.md`.
115
- * @param {string} landing - The landing rule, one of {@link LANDING_RULES}.
116
- * @returns {{landing: true, segment: string}|{landing: false}|{landing: true, segment: undefined}}
117
- * `segment` is the single path segment the note addresses. A `collection`
118
- * note that declares no `section` is a landing page with no segment — an
119
- * error rather than a page, since it names nowhere to land.
120
- */
121
- function landingOf(fm, isReadme, landing) {
122
- if (landing === "readme") {
123
- return isReadme ? { landing: true, segment: sectionOf(fm) } : { landing: false };
124
- }
125
- // `collection`. The section is authored rather than derived: it is the
126
- // identity of the section being introduced, and the note's own title
127
- // ("Creatures") is presentation, which would slug to something else.
128
- if (fm.type === "doc" && fm.subType === "collection") {
129
- return { landing: true, segment: fm.section || fm.slug };
130
- }
131
- return { landing: false };
93
+ export function contentAddress(fm) {
94
+ return `${addressSlug(fm)}/`;
132
95
  }
133
96
 
134
97
  /**
@@ -139,52 +102,40 @@ function landingOf(fm, isReadme, landing) {
139
102
  * address the site does not publish resolves at build time and 404s for the
140
103
  * reader, which is the failure this module exists to prevent.
141
104
  *
105
+ * **It is a pure function of the frontmatter.** Nothing about the file the note
106
+ * was read from reaches it: the `README.md` convention that made one note
107
+ * address a whole section is retired with the section itself (#204), so there
108
+ * is one rule and no branch.
109
+ *
142
110
  * **The prefix does not apply to a page's own address.** `prefix` says where the
143
- * content tree *mounts inside the package* — where its section directories and
144
- * their landing pages live — and a landing page is addressed by that mount
145
- * (`kb/rules/`). An ordinary page is addressed by `(type, shortcode)`, which is
146
- * a package-wide identity and takes no mount: `sohl` publishes
147
- * `/sohl/affliction-aconite/` while its section landings stay at
148
- * `/sohl/kb/affliction/`. The `type-` half is what keeps that flat namespace
149
- * clear of the package's fixed mounts — `/<package>/` for the landing,
111
+ * content tree *mounts inside the package* — the Hugo directory its pages are
112
+ * written under — and an address is `(type, shortcode)`, a package-wide identity
113
+ * that takes no mount: `sohl` publishes `/sohl/affliction-aconite/` from a file
114
+ * written under `kb/`. The `type-` half is what keeps that flat namespace clear
115
+ * of the package's fixed mounts `/<package>/` for the landing,
150
116
  * `/<package>/api/` for generated API docs, neither of which contains a hyphen
151
117
  * or names a type.
152
118
  *
153
- * **The section still decides where the *file* is written**, which is why a
154
- * note without one still has no address: Hugo derives a section from a page's
155
- * directory rather than from its URL, so a page with nowhere to be filed is a
156
- * page with no section landing, no `.CurrentSection` and no per-section layout.
157
- *
158
119
  * @param {object} fm - Parsed frontmatter.
159
120
  * @param {object} [options] - Options.
160
- * @param {boolean} [options.isReadme] - Whether the file is a `README.md`.
161
121
  * @param {{prefix?: string, landing?: string}} [options.scheme] - The
162
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.
163
126
  * @returns {string} The package-relative address, with a trailing slash and no
164
127
  * leading one.
165
- * @throws {Error} When the note has no address no section, a landing page
166
- * naming no section, or no shortcode to be addressed by. Each is a note that
167
- * is not published, and inventing an address for one would put a dead entry
168
- * in the manifest.
128
+ * @throws {Error} When the note has no type or no shortcode to be addressed by.
129
+ * Such a note is not published, and inventing an address for one would put a
130
+ * dead entry in the manifest.
169
131
  */
170
- export function packageAddress(fm, { isReadme = false, scheme } = {}) {
171
- const { prefix, landing } = { ...DEFAULT_ADDRESS_SCHEME, ...scheme };
132
+ export function packageAddress(fm, { scheme } = {}) {
133
+ const { landing } = { ...DEFAULT_ADDRESS_SCHEME, ...scheme };
172
134
  if (!LANDING_RULES.includes(landing)) {
173
135
  throw new Error(
174
136
  `unknown landing rule ${JSON.stringify(landing)} — expected one ` +
175
137
  `of ${LANDING_RULES.join(", ")}`,
176
138
  );
177
139
  }
178
- const land = landingOf(fm, isReadme, landing);
179
- if (land.landing) {
180
- if (typeof land.segment !== "string" || !land.segment) {
181
- throw new Error(`landing note declares no section, so it lands nowhere`);
182
- }
183
- return `${prefix}${land.segment}/`;
184
- }
185
- const sec = sectionOf(fm);
186
- if (typeof sec !== "string" || !sec) {
187
- throw new Error(`type "${fm.type}" has no section`);
188
- }
189
140
  return `${addressSlug(fm)}/`;
190
141
  }