@heroiclands/package-build 22.15.1 → 22.16.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 (221) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +39 -678
  3. package/bin/{content-build.mjs → content-commands.mjs} +197 -118
  4. package/bin/package-build.mjs +303 -42
  5. package/bin/report.mjs +1 -1
  6. package/ci/ci-docker.mjs +6 -3
  7. package/ci/ci-steps.mjs +6 -4
  8. package/ci/content-format-reference.mjs +106 -0
  9. package/config.mjs +22 -2
  10. package/content-config.mjs +74 -134
  11. package/docs/api.md +294 -234
  12. package/docs/authoring/assets.md +31 -0
  13. package/docs/authoring/dates-and-calendars.md +33 -0
  14. package/docs/authoring/first-note.md +48 -0
  15. package/docs/authoring/frontmatter.md +40 -0
  16. package/docs/authoring/links-and-markup.md +137 -0
  17. package/docs/commands.md +631 -162
  18. package/docs/configuration.md +176 -140
  19. package/docs/content-format.md +50 -4229
  20. package/docs/design.md +32 -0
  21. package/docs/diagnostics.md +29 -22
  22. package/docs/getting-started.md +21 -877
  23. package/docs/guides/book.md +153 -0
  24. package/docs/guides/e2e.md +143 -0
  25. package/docs/guides/icons.md +126 -0
  26. package/docs/guides/localization.md +120 -0
  27. package/docs/guides/module-project.md +915 -0
  28. package/docs/guides/packs.md +167 -0
  29. package/docs/guides/reference-pages.md +148 -0
  30. package/docs/guides/release-and-deploy.md +114 -0
  31. package/docs/guides/site.md +152 -0
  32. package/docs/guides/system-project.md +45 -0
  33. package/docs/index.md +16 -0
  34. package/docs/project-setup.md +54 -35
  35. package/docs/reference/format-details.md +4420 -0
  36. package/docs/reference/note-types.md +531 -0
  37. package/engine/address-render.mjs +26 -0
  38. package/engine/address.mjs +3 -20
  39. package/engine/anchors.mjs +15 -6
  40. package/engine/asset-bindings.mjs +6 -2
  41. package/engine/base-compiler.mjs +47 -11
  42. package/engine/being-measurements.mjs +70 -0
  43. package/engine/calendar-artifacts.mjs +1 -1
  44. package/engine/calendar-choice.mjs +32 -0
  45. package/engine/calendar-human.mjs +71 -0
  46. package/engine/calendar-notes.mjs +41 -3
  47. package/engine/calendars.mjs +8 -1
  48. package/engine/changelog-lint.mjs +2 -2
  49. package/engine/code-fences.mjs +23 -97
  50. package/engine/compile-corpus.mjs +6 -7
  51. package/engine/content-admonitions.mjs +114 -0
  52. package/engine/content-captions.mjs +181 -0
  53. package/engine/content-embeds.mjs +2 -2
  54. package/engine/content-footnotes.mjs +34 -0
  55. package/engine/content-format-check.mjs +2 -2
  56. package/engine/content-format.mjs +127 -73
  57. package/engine/content-format.yaml +745 -0
  58. package/engine/content-html.mjs +1 -1
  59. package/engine/content-icons.mjs +45 -92
  60. package/engine/content-images.mjs +22 -74
  61. package/engine/content-index.mjs +12 -3
  62. package/engine/content-links.mjs +21 -26
  63. package/engine/content-lint.mjs +1 -5
  64. package/engine/content-secrets.mjs +3 -1
  65. package/engine/content-tables.mjs +47 -1257
  66. package/engine/culture-choice.mjs +32 -0
  67. package/engine/date-conversion.mjs +47 -0
  68. package/engine/declaration-check.mjs +87 -0
  69. package/engine/docs-checks.mjs +140 -0
  70. package/engine/documentation-links.mjs +31 -0
  71. package/engine/exported-scene.mjs +1 -1
  72. package/engine/extension-attributes.mjs +74 -0
  73. package/engine/field-reference.mjs +1 -1
  74. package/engine/field-spec.mjs +4 -0
  75. package/engine/foreign-catalog.mjs +5 -5
  76. package/engine/format-generated.mjs +15 -0
  77. package/engine/foundry-entries.mjs +3 -1
  78. package/engine/frontmatter-lint.mjs +5 -1
  79. package/engine/frontmatter.mjs +10 -0
  80. package/engine/generate.mjs +2 -2
  81. package/engine/helpers.mjs +71 -117
  82. package/engine/homepage.mjs +2 -3
  83. package/engine/index-records.mjs +1 -0
  84. package/engine/index.mjs +18 -3
  85. package/engine/infobox-registry.mjs +3 -1
  86. package/engine/infobox.mjs +38 -20
  87. package/engine/item-compiler.mjs +10 -8
  88. package/engine/item-docs.mjs +7 -6
  89. package/engine/journals.mjs +51 -10
  90. package/engine/macros.mjs +1 -1
  91. package/engine/map-build.mjs +1 -1
  92. package/engine/map-dot.mjs +1 -1
  93. package/engine/markdown-expressions.mjs +117 -0
  94. package/engine/metadata-index.mjs +7 -3
  95. package/engine/note-addresses.mjs +1 -0
  96. package/engine/note-dates.mjs +283 -61
  97. package/engine/note-package.mjs +5 -25
  98. package/engine/note-state.mjs +6 -2
  99. package/engine/note-vocabulary.mjs +74 -19
  100. package/engine/number-words.mjs +75 -0
  101. package/engine/office-holders.mjs +188 -0
  102. package/engine/pack-config.mjs +2 -15
  103. package/engine/pack-router.mjs +24 -5
  104. package/engine/packages.mjs +3 -2
  105. package/engine/pathnames.mjs +21 -24
  106. package/engine/pdf-build.mjs +217 -55
  107. package/engine/pdf-fonts.mjs +2 -2
  108. package/engine/pdf-images.mjs +82 -0
  109. package/engine/pdf-render.mjs +174 -22
  110. package/engine/populations.mjs +3 -2
  111. package/engine/project-init.mjs +499 -0
  112. package/engine/prose-config.mjs +1 -1
  113. package/engine/prose-lint.mjs +7 -6
  114. package/engine/readability-lint.mjs +86 -0
  115. package/engine/reckoning-markers.mjs +205 -93
  116. package/engine/scenes.mjs +5 -2
  117. package/engine/schema-check.mjs +3 -3
  118. package/engine/site-build.mjs +87 -37
  119. package/engine/site-config.mjs +8 -8
  120. package/engine/site-index.mjs +4 -0
  121. package/engine/site-maps.mjs +1 -1
  122. package/engine/site-preview.mjs +19 -8
  123. package/engine/social-ties.mjs +5 -1
  124. package/engine/sql-tables.mjs +92 -22
  125. package/engine/web-wikilinks.mjs +15 -31
  126. package/engine/wikilink-syntax.mjs +4 -1
  127. package/engine/wikilinks.mjs +12 -14
  128. package/hm3/item-fields.mjs +1 -1
  129. package/hm3/items.mjs +12 -45
  130. package/index.mjs +2 -14
  131. package/manifest.mjs +31 -4
  132. package/markdownlint-config.mjs +1 -1
  133. package/package.json +32 -11
  134. package/prettier-config.mjs +1 -1
  135. package/sohl/being-info.mjs +3 -145
  136. package/sohl/index.mjs +1 -1
  137. package/sohl/infobox.mjs +2 -1
  138. package/sohl/item-builders.mjs +3 -27
  139. package/sohl/item-fields.mjs +61 -22
  140. package/sohl/kb-passes.mjs +2 -2
  141. package/sohl/note-schemas.mjs +0 -8
  142. package/stage.mjs +19 -0
  143. package/types/config.d.mts +12 -3
  144. package/types/content-config.d.mts +15 -81
  145. package/types/engine/address-render.d.mts +13 -0
  146. package/types/engine/address.d.mts +2 -11
  147. package/types/engine/anchors.d.mts +3 -4
  148. package/types/engine/asset-bindings.d.mts +9 -2
  149. package/types/engine/base-compiler.d.mts +1 -2
  150. package/types/engine/being-measurements.d.mts +16 -0
  151. package/types/engine/calendar-choice.d.mts +14 -0
  152. package/types/engine/calendar-human.d.mts +4 -0
  153. package/types/engine/calendar-notes.d.mts +2 -0
  154. package/types/engine/calendars.d.mts +5 -3
  155. package/types/engine/code-fences.d.mts +6 -72
  156. package/types/engine/compile-corpus.d.mts +1 -3
  157. package/types/engine/content-admonitions.d.mts +24 -0
  158. package/types/engine/content-captions.d.mts +45 -0
  159. package/types/engine/content-footnotes.d.mts +10 -0
  160. package/types/engine/content-format-check.d.mts +17 -4
  161. package/types/engine/content-format.d.mts +24 -11
  162. package/types/engine/content-icons.d.mts +14 -23
  163. package/types/engine/content-images.d.mts +2 -2
  164. package/types/engine/content-index.d.mts +9 -2
  165. package/types/engine/content-links.d.mts +3 -5
  166. package/types/engine/content-tables.d.mts +11 -140
  167. package/types/engine/culture-choice.d.mts +14 -0
  168. package/types/engine/date-conversion.d.mts +4 -0
  169. package/types/engine/declaration-check.d.mts +24 -0
  170. package/types/engine/docs-checks.d.mts +28 -0
  171. package/types/engine/documentation-links.d.mts +7 -0
  172. package/types/engine/exported-scene.d.mts +6 -2
  173. package/types/engine/extension-attributes.d.mts +14 -0
  174. package/types/engine/field-spec.d.mts +9 -0
  175. package/types/engine/format-generated.d.mts +7 -0
  176. package/types/engine/frontmatter.d.mts +14 -4
  177. package/types/engine/generate.d.mts +1 -1
  178. package/types/engine/helpers.d.mts +37 -92
  179. package/types/engine/homepage.d.mts +2 -3
  180. package/types/engine/index.d.mts +5 -0
  181. package/types/engine/infobox-registry.d.mts +3 -1
  182. package/types/engine/infobox.d.mts +17 -6
  183. package/types/engine/item-compiler.d.mts +4 -4
  184. package/types/engine/item-docs.d.mts +4 -3
  185. package/types/engine/journals.d.mts +10 -7
  186. package/types/engine/map-graphviz.d.mts +1 -1
  187. package/types/engine/markdown-expressions.d.mts +17 -0
  188. package/types/engine/note-dates.d.mts +47 -1
  189. package/types/engine/note-package.d.mts +3 -19
  190. package/types/engine/note-state.d.mts +2 -1
  191. package/types/engine/note-vocabulary.d.mts +9 -0
  192. package/types/engine/number-words.d.mts +4 -0
  193. package/types/engine/office-holders.d.mts +14 -0
  194. package/types/engine/pack-config.d.mts +1 -1
  195. package/types/engine/packages.d.mts +3 -2
  196. package/types/engine/pdf-images.d.mts +38 -0
  197. package/types/engine/populations.d.mts +2 -2
  198. package/types/engine/project-init.d.mts +17 -0
  199. package/types/engine/prose-config.d.mts +1 -1
  200. package/types/engine/prose-lint.d.mts +3 -2
  201. package/types/engine/readability-lint.d.mts +22 -0
  202. package/types/engine/reckoning-markers.d.mts +4 -2
  203. package/types/engine/schema-check.d.mts +3 -3
  204. package/types/engine/site-build.d.mts +38 -28
  205. package/types/engine/site-config.d.mts +2 -2
  206. package/types/engine/site-index.d.mts +4 -0
  207. package/types/engine/site-preview.d.mts +1 -1
  208. package/types/engine/social-ties.d.mts +8 -2
  209. package/types/engine/sql-tables.d.mts +13 -2
  210. package/types/engine/web-wikilinks.d.mts +2 -1
  211. package/types/engine/wikilinks.d.mts +7 -1
  212. package/types/hm3/items.d.mts +5 -6
  213. package/types/labels.d.mts +40 -0
  214. package/types/manifest.d.mts +3 -3
  215. package/types/sohl/actors.d.mts +2 -2
  216. package/types/sohl/being-info.d.mts +3 -43
  217. package/types/sohl/index.d.mts +1 -1
  218. package/types/sohl/infobox.d.mts +5 -2
  219. package/types/sohl/item-fields.d.mts +4 -13
  220. package/types/stage.d.mts +9 -0
  221. package/CONTENT.md +0 -3239
@@ -31,8 +31,6 @@
31
31
  * packageBuild:
32
32
  * assets:
33
33
  * - { from: assets/icons, to: assets/icons }
34
- * publish:
35
- * site: content
36
34
  * ```
37
35
  *
38
36
  * `defineConfig` is the whole of the contract: it validates the object, fills
@@ -65,6 +63,7 @@
65
63
 
66
64
  import fs from "node:fs";
67
65
  import path from "node:path";
66
+ import { fileURLToPath } from "node:url";
68
67
  import YAML from "yaml";
69
68
 
70
69
  // Leaves with no local imports of their own, so naming them here cannot close
@@ -246,59 +245,45 @@ export const DEFAULT_ADDRESS_SCHEME = Object.freeze({
246
245
  prefix: "",
247
246
  });
248
247
 
249
- /**
250
- * How much of a package reaches the web.
251
- *
252
- * Every HeroicLands package publishes something: a top-level, human-authored
253
- * homepage at `https://www.heroiclands.org/<contentPackage>/` saying what the
254
- * module is, which system it needs and how to install it. So there is no
255
- * value here meaning *no web presence at all* — homepage-only is the **floor**,
256
- * and the default.
257
- *
258
- * - `homepage` — the authored homepage, and **no other page**. The content tree
259
- * is not walked for pages, and nothing serves a page for its addresses.
260
- * - `content` — the homepage *plus* every page the content tree publishes, one
261
- * per note.
262
- *
263
- * **Homepage-only is a first-class mode, not an accommodation.**
264
- * `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
265
- * Fan Material Guidelines) and `harn-adventures` (HârnFanon under Lythia's
266
- * terms) must each publish a homepage and nothing beneath it — two packages
267
- * under two different fan-content licences. The boundary is **published
268
- * content**: journal text, artwork, item descriptions, compiled notes. A
269
- * human-authored page announcing the module discloses none of it. Because the
270
- * failure mode is silent — a `site:` block added later ships licensed content
271
- * with nobody noticing — the mode fences the content surfaces off rather than
272
- * trusting a configuration to stay empty.
273
- *
274
- * This was a boolean until 5.0.0, and `false` read as "no web presence", which
275
- * no longer describes any package. Both spellings are refused rather than
276
- * mapped: a value silently reinterpreted reads to its author as though it still
277
- * means what it said.
278
- *
279
- * @typedef {"homepage" | "content"} SiteMode
280
- */
281
-
282
- /**
283
- * The publishing modes {@link PublishSwitches.site} may name, floor first.
284
- *
285
- * @satisfies {readonly SiteMode[]}
286
- */
287
- export const SITE_MODES = /** @type {const} */ (["homepage", "content"]);
288
-
289
248
  /**
290
249
  * Whether this package publishes the pages its content tree compiles to.
291
250
  *
292
- * The one question every reader of the mode actually asks — the site build, to
293
- * decide whether to walk the tree at all, and the content index, to
294
- * decide whether an entry carries a web `path`. Written once here so the two
295
- * cannot come to disagree about what a mode means.
251
+ * Every package publishes an authored homepage. Other authored notes provide
252
+ * content pages; a tree containing only the homepage publishes only that page.
253
+ *
254
+ * Site, PDF, index, and Foundry address emission use the same source-tree
255
+ * decision. A file without note frontmatter is not a content page.
296
256
  *
297
- * @param {{publish: {site: SiteMode}}} config - A resolved configuration.
257
+ * @param {{paths: {content: string}, skipDirectories: readonly string[]}} config -
258
+ * A resolved configuration with content and skip paths.
298
259
  * @returns {boolean} Whether content pages are published.
299
260
  */
300
261
  export function publishesContentPages(config) {
301
- return config.publish.site === "content";
262
+ const root = config.paths.content;
263
+ if (!fs.existsSync(root)) return false;
264
+ const skipped = new Set(config.skipDirectories);
265
+ const stack = [root];
266
+ while (stack.length) {
267
+ const dir = stack.pop();
268
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
269
+ const file = path.join(dir, entry.name);
270
+ if (entry.isDirectory()) {
271
+ if (!skipped.has(entry.name)) stack.push(file);
272
+ continue;
273
+ }
274
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
275
+ const match = fs
276
+ .readFileSync(file, "utf8")
277
+ .match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
278
+ if (!match) continue;
279
+ try {
280
+ if (YAML.parse(match[1])?.type !== "homepage") return true;
281
+ } catch {
282
+ return true;
283
+ }
284
+ }
285
+ }
286
+ return false;
302
287
  }
303
288
 
304
289
  /**
@@ -447,8 +432,7 @@ export function publishesContentPages(config) {
447
432
 
448
433
  /**
449
434
  * @typedef {object} PublishSwitches
450
- * @property {SiteMode} site How much of this package reaches the web.
451
- * See {@link SITE_MODES}.
435
+ * @property {Readonly<{prefix: string}>} address Where content pages mount within the package.
452
436
  */
453
437
 
454
438
  /**
@@ -539,13 +523,12 @@ export function publishesContentPages(config) {
539
523
  *
540
524
  * @typedef {object} DocsSpec
541
525
  * @property {DocPageSpec} [itemFields] The item-frontmatter reference,
542
- * rendered by `content-build docs
526
+ * rendered by `package-build docs
543
527
  * item-fields`.
544
528
  */
545
529
 
546
530
  /**
547
531
  * @typedef {object} PublishSwitchesInput
548
- * @property {SiteMode} [site]
549
532
  * @property {AddressSchemeInput} [address]
550
533
  */
551
534
 
@@ -764,6 +747,7 @@ const DOCS_KEYS = ["itemFields"];
764
747
  const CHANGELOG_KEYS = ["labels"];
765
748
  const SITE_KEYS = [
766
749
  "base",
750
+ "title",
767
751
  "assets",
768
752
  "description",
769
753
  "packages",
@@ -821,7 +805,7 @@ const STATS_KEYS = ["lastModifiedBy"];
821
805
  * @type {symbol}
822
806
  */
823
807
  export const DERIVED_SYSTEM_VERSION = Symbol.for("package-build.derivedSystemVersion");
824
- const PUBLISH_KEYS = ["site", "address"];
808
+ const PUBLISH_KEYS = ["address"];
825
809
  const ADDRESS_KEYS = ["prefix"];
826
810
 
827
811
  /** @param {unknown} value */
@@ -906,9 +890,10 @@ function requireNonEmptyString(value, field) {
906
890
  * package's id to *be* its system id, and `sohl-sohl-skill-clmb` is the
907
891
  * honest address that results — which is the reason to prevent the ones that
908
892
  * are avoidable.
909
- * 3. _Not reserved_. `packagebuild` addresses the files this toolchain ships
910
- * itself, so a repository claiming the name would publish addresses that
911
- * collide with them — see {@link module:engine/packages}.
893
+ * 3. _Not reserved for another repository_. `packagebuild` addresses assets
894
+ * and documentation this toolchain ships itself. Only this repository's
895
+ * documentation build can use that namespace — see
896
+ * {@link module:engine/packages}.
912
897
  *
913
898
  * The type vocabulary rule reaches the **asset** types too: `icon`, `image` and
914
899
  * `audio` are types an address names exactly as it names a being, so a package
@@ -919,9 +904,11 @@ function requireNonEmptyString(value, field) {
919
904
  * to a documentation entry: the item types plus `macro` and the map types.
920
905
  * With {@link PACK_BY_TYPE}, {@link NOTE_VOCABULARY} and the `doc`-prefixed
921
906
  * forms, this is the whole type vocabulary an address may write.
907
+ * @param {{rootDir: string, packageKind: string}} scope - The source claiming
908
+ * the namespace; this toolchain owns `packagebuild` for its documentation.
922
909
  * @returns {string} The value, unchanged.
923
910
  */
924
- function requireContentPackage(value, docEntryTypes) {
911
+ function requireContentPackage(value, docEntryTypes, { rootDir, packageKind }) {
925
912
  const pkg = requireNonEmptyString(value, "contentPackage");
926
913
  if (!isAddressSegment(pkg)) {
927
914
  fail(
@@ -935,7 +922,12 @@ function requireContentPackage(value, docEntryTypes) {
935
922
  "than merely ugly. `harn-adventures` became `harnadventures`",
936
923
  );
937
924
  }
938
- if (isReservedPackage(pkg)) {
925
+ // This toolchain's own documentation shares the asset namespace it owns.
926
+ // Other repositories cannot claim that namespace.
927
+ const ownDocumentation =
928
+ packageKind === DOCUMENTATION_KIND &&
929
+ path.resolve(rootDir) === path.dirname(fileURLToPath(import.meta.url));
930
+ if (isReservedPackage(pkg) && !ownDocumentation) {
939
931
  fail(
940
932
  "contentPackage",
941
933
  `is \`${pkg}\`, which is a reserved package name. ` +
@@ -1270,7 +1262,7 @@ function normalizeIcons(value, rootDir) {
1270
1262
  if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
1271
1263
  fail(
1272
1264
  `${where}.icons.${name}`,
1273
- "is not a name a note can write — `:icon-…:` takes lowercase " +
1265
+ "is not a name a note can write — `:icon …:` takes lowercase " +
1274
1266
  "letters, digits and hyphens, the charset an address segment uses",
1275
1267
  );
1276
1268
  }
@@ -1567,7 +1559,7 @@ function normalizeSiteDescription(value) {
1567
1559
  */
1568
1560
  export const DERIVED_HUGO_KEYS = Object.freeze({
1569
1561
  baseURL: "package.json `homepage`",
1570
- title: "`packageBuild.manifest.title`",
1562
+ title: "`site.title` or `packageBuild.manifest.title`",
1571
1563
  locale: "the organisation's locale, in `engine/site-config.mjs`",
1572
1564
  publishDir: "`contentPackage`, under the deployment root `build/site`",
1573
1565
  contentDir: "the fixed content mount, `build/hugo/content`",
@@ -1588,7 +1580,7 @@ export const DERIVED_HUGO_KEYS = Object.freeze({
1588
1580
  "params.brand": "the organisation's brand links, in `engine/site-config.mjs`",
1589
1581
  "params.notfound": "`site.notfound`",
1590
1582
  "markup.goldmark.renderer.unsafe": "the toolchain, whose pages carry raw HTML",
1591
- menu: "the navigation `content-build deps fetch` caches from heroiclands.org",
1583
+ menu: "the navigation `package-build deps fetch` caches from heroiclands.org",
1592
1584
  });
1593
1585
 
1594
1586
  /**
@@ -1706,7 +1698,7 @@ const SITE_INDEX_MESSAGE =
1706
1698
  * Everything here is *framing*: which named pass bundle supplies the
1707
1699
  * repository's own body rewrites, and the residue of the generated Hugo
1708
1700
  * configuration that is genuinely this repository's own. Where the Hugo tree
1709
- * is written is not a choice: `content-build site` writes it under
1701
+ * is written is not a choice: `package-build site` writes it under
1710
1702
  * `build/hugo/`, and a `site.out` is refused by name. How a page gets its
1711
1703
  * **address** is deliberately not here either — that is `publish.address`,
1712
1704
  * shared with the link manifest so the two cannot disagree about where a
@@ -1714,7 +1706,7 @@ const SITE_INDEX_MESSAGE =
1714
1706
  *
1715
1707
  * Two switches sit here, both on by default. `search` is whether
1716
1708
  * `package-build site-root` writes a search index beside the rendered pages.
1717
- * `maps` is whether `content-build site` draws the map from each place that
1709
+ * `maps` is whether `package-build site` draws the map from each place that
1718
1710
  * states a border or a route and writes it into that place's page — a site
1719
1711
  * carries both unless it says otherwise.
1720
1712
  *
@@ -1734,6 +1726,7 @@ const SITE_INDEX_MESSAGE =
1734
1726
  function normalizeSite(value) {
1735
1727
  const empty = Object.freeze({
1736
1728
  base: "",
1729
+ title: "",
1737
1730
  assets: "",
1738
1731
  description: "",
1739
1732
  packages: Object.freeze([]),
@@ -1788,6 +1781,7 @@ function normalizeSite(value) {
1788
1781
 
1789
1782
  return Object.freeze({
1790
1783
  base: input.base === undefined ? "" : requireNonEmptyString(input.base, "site.base"),
1784
+ title: input.title === undefined ? "" : requireNonEmptyString(input.title, "site.title"),
1791
1785
  assets: normalizeSiteAssets(input.assets),
1792
1786
  description: normalizeSiteDescription(input.description),
1793
1787
  packages: Object.freeze(packages),
@@ -1818,10 +1812,8 @@ function normalizeSite(value) {
1818
1812
  * choose, which is the whole reason they are configuration: the engine that
1819
1813
  * sets the book must be able to set somebody else's book.
1820
1814
  *
1821
- * **Declaring the block is not the switch.** Whether a PDF is built at all is
1822
- * `publish.site` — `content` builds one, `homepage` does not — so a package
1823
- * cannot end up with two switches that disagree about whether it publishes its
1824
- * content tree. See {@link publishesContentPages}.
1815
+ * A PDF is built when the authored tree contains content pages and the `pdf`
1816
+ * block names a document tree. See {@link publishesContentPages}.
1825
1817
  *
1826
1818
  * @param {unknown} value - The `pdf` block, or `undefined`.
1827
1819
  * @param {string} rootDir - The repository root configured paths resolve against.
@@ -1874,7 +1866,7 @@ function normalizePdf(value, rootDir) {
1874
1866
  });
1875
1867
  }
1876
1868
 
1877
- // Family name to the font file carrying its glyphs, for `:icon-…:`. A file
1869
+ // Family name to the font file carrying its glyphs, for `:icon …:`. A file
1878
1870
  // rather than a codepoint, because the font's own tables are the only
1879
1871
  // trustworthy source of which glyph a name resolves to — see
1880
1872
  // {@link module:engine/content-icons}, which states the style and the name
@@ -2103,8 +2095,8 @@ function normalizeRelationships(value) {
2103
2095
  };
2104
2096
  // What the other package's *content* is called, where that
2105
2097
  // differs from its Foundry id. A note addresses a file by the
2106
- // content package that owns it — `thalorna/assets/…` — and the
2107
- // Foundry id (`sohl-thalorna`) appears only in the install
2098
+ // content package that owns it — `harnensemble/assets/…` — and the
2099
+ // Foundry id (`harn-ensemble`) appears only in the install
2108
2100
  // path this derives. Omitted where the two are the same word,
2109
2101
  // which they are for every system.
2110
2102
  if (rel.contentPackage !== undefined) {
@@ -2397,43 +2389,6 @@ function normalizeItemBuilders(value) {
2397
2389
  };
2398
2390
  }
2399
2391
 
2400
- /**
2401
- * The publishing mode, refusing a boolean.
2402
- *
2403
- * A boolean is refused rather than mapped onto the nearest mode, because the
2404
- * reading `false` invited — *this package has no web presence* — is exactly the
2405
- * belief the change exists to correct, and a value quietly reinterpreted reads
2406
- * to its author as though it still means what it said. So the message names the
2407
- * mode to write instead of the value to fix.
2408
- *
2409
- * @param {unknown} value - The authored `publish.site`.
2410
- * @returns {SiteMode} The mode.
2411
- */
2412
- function normalizeSiteMode(value) {
2413
- if (value === undefined) return "homepage";
2414
- if (typeof value === "boolean") {
2415
- fail(
2416
- "publish.site",
2417
- `is no longer a boolean — write \`site: ${value ? "content" : "homepage"}\`. ` +
2418
- `Every package publishes an authored homepage at ` +
2419
- `/<contentPackage>/, so no value means "no web presence": ` +
2420
- `\`homepage\` publishes that page and nothing else, and ` +
2421
- `\`content\` publishes it plus every page the content tree ` +
2422
- `compiles to`,
2423
- );
2424
- }
2425
- if (
2426
- typeof value !== "string" ||
2427
- !(/** @type {readonly string[]} */ (SITE_MODES).includes(value))
2428
- ) {
2429
- fail(
2430
- "publish.site",
2431
- `must be one of ${SITE_MODES.join(", ")} (got ${JSON.stringify(value)})`,
2432
- );
2433
- }
2434
- return /** @type {SiteMode} */ (value);
2435
- }
2436
-
2437
2392
  /**
2438
2393
  * @param {unknown} value
2439
2394
  * @returns {Readonly<PublishSwitches>}
@@ -2441,12 +2396,14 @@ function normalizeSiteMode(value) {
2441
2396
  function normalizePublish(value) {
2442
2397
  if (value === undefined) {
2443
2398
  return Object.freeze({
2444
- site: "homepage",
2445
2399
  address: Object.freeze({ ...DEFAULT_ADDRESS_SCHEME }),
2446
2400
  });
2447
2401
  }
2448
2402
  if (!isPlainObject(value)) fail("publish", "must be an object");
2449
2403
  const publish = /** @type {Record<string, unknown>} */ (value);
2404
+ if (Object.hasOwn(publish, "site")) {
2405
+ fail("publish.site", "is not configured; publishing follows the authored content tree");
2406
+ }
2450
2407
  rejectUnknownKeys(publish, PUBLISH_KEYS, "publish.");
2451
2408
 
2452
2409
  const addressInput = publish.address;
@@ -2482,7 +2439,6 @@ function normalizePublish(value) {
2482
2439
  }
2483
2440
 
2484
2441
  return Object.freeze({
2485
- site: normalizeSiteMode(publish.site),
2486
2442
  address: Object.freeze({ prefix }),
2487
2443
  });
2488
2444
  }
@@ -2534,28 +2490,6 @@ export function defineConfig(config) {
2534
2490
  if (input[key] === undefined) continue;
2535
2491
  fail(key, `is refused in a \`${DOCUMENTATION_KIND}\` package, which ${why}`);
2536
2492
  }
2537
- // Publishing is what a documentation package is *for*, so the floor
2538
- // every other package may sit at is not available to it: `homepage`
2539
- // would leave a package that publishes one authored page, builds no
2540
- // book, and compiles nothing at all.
2541
- if (!isPlainObject(input.publish)) {
2542
- fail(
2543
- "publish",
2544
- `is required in a \`${DOCUMENTATION_KIND}\` package: publishing ` +
2545
- "the content tree is the whole of what it does. Write " +
2546
- "`publish: {site: content}`",
2547
- );
2548
- }
2549
- const mode = /** @type {Record<string, unknown>} */ (input.publish).site;
2550
- if (mode !== "content") {
2551
- fail(
2552
- "publish.site",
2553
- `must be \`content\` in a \`${DOCUMENTATION_KIND}\` package — ` +
2554
- "`homepage` fences the content surfaces off, and a package " +
2555
- "that compiles nothing and publishes nothing from its tree " +
2556
- "would produce a single authored page and no book",
2557
- );
2558
- }
2559
2493
  }
2560
2494
 
2561
2495
  // A Foundry package need not compile anything. A module may ship assets and
@@ -2718,10 +2652,16 @@ export function defineConfig(config) {
2718
2652
  const docEntryTypes = Object.freeze(
2719
2653
  new Set([...itemTypes, ...ACTOR_TYPES, "macro", ...MAP_TYPES]),
2720
2654
  );
2655
+ const compatibility = normalizeCompatibility(input.compatibility, "compatibility");
2656
+ if (!documentation && compatibility === null)
2657
+ fail("compatibility.minimum", "is required for a Foundry package");
2721
2658
 
2722
2659
  return Object.freeze({
2723
2660
  rootDir,
2724
- contentPackage: requireContentPackage(input.contentPackage, docEntryTypes),
2661
+ contentPackage: requireContentPackage(input.contentPackage, docEntryTypes, {
2662
+ rootDir,
2663
+ packageKind,
2664
+ }),
2725
2665
  foundryPackage,
2726
2666
  // `package.json`'s own address and byline. `homepage` is checked by
2727
2667
  // `checkHomepage` in `config.mjs`.
@@ -2730,7 +2670,7 @@ export function defineConfig(config) {
2730
2670
  author: normalizeAuthor(input.author),
2731
2671
  packageKind: /** @type {PackageKind} */ (packageKind),
2732
2672
  // Foundry serves a package's files from `<kind>/<id>/`, so this is the
2733
- // one place `systems/sohl` (or `modules/sohl-thalorna`) is spelled.
2673
+ // one place `systems/sohl` (or `modules/harn-ensemble`) is spelled.
2734
2674
  //
2735
2675
  // **Conditional on the kind.** `documentation` names no directory
2736
2676
  // Foundry serves, and there is no package id to put under one either, so
@@ -2797,7 +2737,7 @@ export function defineConfig(config) {
2797
2737
  docs: normalizeDocs(input.docs),
2798
2738
  site: normalizeSite(input.site),
2799
2739
  pdf: normalizePdf(input.pdf, rootDir),
2800
- compatibility: normalizeCompatibility(input.compatibility, "compatibility"),
2740
+ compatibility,
2801
2741
  relationships: normalizeRelationships(input.relationships),
2802
2742
  systems,
2803
2743
  requiresSystem,