@heroiclands/package-build 3.4.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/CONTENT.md +100 -22
  3. package/MIGRATING.md +120 -0
  4. package/bin/content-build.mjs +6 -2
  5. package/content-config.mjs +109 -6
  6. package/engine/base-compiler.mjs +33 -36
  7. package/engine/content-links.mjs +15 -13
  8. package/engine/content-package.mjs +3 -2
  9. package/engine/field-reference.mjs +2 -2
  10. package/engine/frontmatter-lint.mjs +26 -0
  11. package/engine/helpers.mjs +10 -12
  12. package/engine/homepage.mjs +150 -0
  13. package/engine/index.mjs +10 -1
  14. package/engine/journals.mjs +2 -3
  15. package/engine/macros.mjs +2 -3
  16. package/engine/manifest-emit.mjs +17 -12
  17. package/engine/note-package.mjs +75 -68
  18. package/engine/note-schemas.mjs +44 -0
  19. package/engine/pack-router.mjs +3 -3
  20. package/engine/retired-fields.mjs +123 -0
  21. package/engine/scenes.mjs +7 -11
  22. package/engine/site-build.mjs +151 -22
  23. package/engine/site-index.mjs +5 -5
  24. package/package.json +1 -1
  25. package/sohl/actors.mjs +2 -3
  26. package/sohl/items.mjs +2 -3
  27. package/sohl/note-schemas.mjs +8 -0
  28. package/types/content-config.d.mts +105 -4
  29. package/types/engine/base-compiler.d.mts +12 -17
  30. package/types/engine/content-package.d.mts +3 -2
  31. package/types/engine/helpers.d.mts +5 -8
  32. package/types/engine/homepage.d.mts +118 -0
  33. package/types/engine/index.d.mts +3 -0
  34. package/types/engine/manifest-emit.d.mts +7 -8
  35. package/types/engine/note-package.d.mts +29 -34
  36. package/types/engine/note-schemas.d.mts +6 -0
  37. package/types/engine/pack-router.d.mts +3 -3
  38. package/types/engine/retired-fields.d.mts +54 -0
  39. package/types/engine/site-build.d.mts +52 -4
  40. package/types/sohl/note-schemas.d.mts +6 -0
@@ -62,7 +62,14 @@ import {
62
62
  } from "./foreign-manifests.mjs";
63
63
  import { deriveBeingInfo, isBeing } from "../sohl/being-info.mjs";
64
64
  import { loadPackConfig } from "./pack-config.mjs";
65
- import { notePackage, searchableFrontmatter } from "./note-package.mjs";
65
+ import { searchableFrontmatter } from "./note-package.mjs";
66
+ import {
67
+ HOMEPAGE_DESTINATION,
68
+ homepageFrontmatter,
69
+ homepageTitle,
70
+ isHomepage,
71
+ } from "./homepage.mjs";
72
+ import { publishesContentPages } from "../content-config.mjs";
66
73
 
67
74
  const require = createRequire(import.meta.url);
68
75
 
@@ -132,12 +139,15 @@ export function collectContentPages(contentBase, ctx) {
132
139
  const note = readNote(file);
133
140
  if (!note) continue;
134
141
  const { fm, body } = note;
135
- // Derived rather than read: `package:` is optional, and a note that
136
- // declares nothing belongs to the package this repository compiles
137
- // (#56). Once the field is retired outright this collapses to
138
- // `ctx.contentPackage` and the set membership becomes a formality.
139
- const pkg = notePackage(fm, ctx.contentPackage);
142
+ // The configuration's, never a note's: `package:` is retired, so every
143
+ // note in the tree belongs to the package this repository compiles
144
+ // (#56).
145
+ const pkg = ctx.contentPackage;
140
146
  if (!ctx.packages.has(pkg) || !fm.type) continue;
147
+ // A homepage is addressed by the *package*, not by its own name, so it
148
+ // never takes a section and a slug (#51). {@link collectHomepages}
149
+ // gathers it instead.
150
+ if (isHomepage(fm)) continue;
141
151
 
142
152
  for (const hit of frontmatterWikilinks(fm)) {
143
153
  fmLinkFindings.push({ file, ...hit });
@@ -162,9 +172,9 @@ export function collectContentPages(contentBase, ctx) {
162
172
  pages.push({
163
173
  kind: "content",
164
174
  fm,
165
- // The page's package, resolved once here so every consumer — the
175
+ // The page's package, recorded once here so every consumer — the
166
176
  // index's canonical keys, the table universe, the local-package set
167
- // — reads one derived value instead of frontmatter (#56).
177
+ // — reads one configured value and never frontmatter (#56).
168
178
  pkg,
169
179
  body,
170
180
  name,
@@ -250,6 +260,62 @@ export function collectTreePages(tree, ctx) {
250
260
  return { pages, fmLinkFindings };
251
261
  }
252
262
 
263
+ /**
264
+ * The package's homepage notes — the authored page at `/<contentPackage>/`.
265
+ *
266
+ * A separate walk from {@link collectContentPages} rather than a branch inside
267
+ * it, because in homepage-only mode it is the **whole** of the site build: the
268
+ * content tree is never read for pages at all, so the licensing constraint two
269
+ * packages ship under is a property of the code path rather than of a
270
+ * configuration that happens to be empty (#55).
271
+ *
272
+ * Returned as a list rather than as the one note there should be. Requiring
273
+ * exactly one is #52's, and it is a separate decision — this reports what it
274
+ * found so a count is visible either way.
275
+ *
276
+ * @param {string} contentBase - Absolute path to the content tree.
277
+ * @param {object} ctx - `{ skipDirectories }`.
278
+ * @returns {{pages: object[]}} The homepage notes, in walk order.
279
+ */
280
+ export function collectHomepages(contentBase, ctx) {
281
+ const pages = [];
282
+ for (const file of walkSiteTree(contentBase, ctx.skipDirectories)) {
283
+ const note = readNote(file);
284
+ if (!note || !isHomepage(note.fm)) continue;
285
+ pages.push({ kind: "homepage", file, fm: note.fm, body: note.body });
286
+ }
287
+ return { pages };
288
+ }
289
+
290
+ /**
291
+ * Writes each homepage at the package's own root.
292
+ *
293
+ * Its own writer, deliberately small. A homepage is authored markdown published
294
+ * verbatim — no table expansion, no section landing, and (until #54) no link
295
+ * resolution — so routing it through {@link renderPages} would buy it a pipeline
296
+ * it has no input for, and would make homepage-only mode depend on the index,
297
+ * the foreign manifests and the table universe that mode exists to not build.
298
+ *
299
+ * @param {string} outRoot - The package's site root — the configured `site.out`,
300
+ * one level above the content mount.
301
+ * @param {readonly object[]} pages - From {@link collectHomepages}.
302
+ * @param {object} config - The resolved configuration, for the package name and
303
+ * the default title.
304
+ * @returns {number} How many pages were written.
305
+ */
306
+ export function writeHomepages(outRoot, pages, config) {
307
+ for (const page of pages) {
308
+ const data = homepageFrontmatter(page.fm, {
309
+ contentPackage: config.contentPackage,
310
+ title: homepageTitle(page.fm, config),
311
+ });
312
+ const dest = path.join(outRoot, HOMEPAGE_DESTINATION);
313
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
314
+ fs.writeFileSync(dest, matter.stringify(page.body, data));
315
+ }
316
+ return pages.length;
317
+ }
318
+
253
319
  /**
254
320
  * The integrity gates a site build runs before it writes anything.
255
321
  *
@@ -319,6 +385,30 @@ export function siteGates(pages, findings, { manifestDir }) {
319
385
  return out;
320
386
  }
321
387
 
388
+ /**
389
+ * The gate result of a build that ran none of them.
390
+ *
391
+ * Homepage-only publishes one authored page and resolves nothing, so every gate
392
+ * here is about a surface that mode does not have. The shape is returned all the
393
+ * same, because a caller reads the same fields whichever mode ran and a `null`
394
+ * would make each of them a special case.
395
+ *
396
+ * @returns {object} An all-clear gate result.
397
+ */
398
+ export function emptyGates() {
399
+ return {
400
+ frontmatterLinks: [],
401
+ slugErrors: [],
402
+ collisions: [],
403
+ staleManifests: [],
404
+ unaddressable: [],
405
+ conflicts: [],
406
+ index: null,
407
+ foreign: null,
408
+ manifests: null,
409
+ };
410
+ }
411
+
322
412
  /** Whether any gate produced a finding. */
323
413
  export function gatesFailed(gates) {
324
414
  return Boolean(
@@ -347,7 +437,8 @@ export function tableUniverse(pages) {
347
437
  const pkg = p.pkg;
348
438
  if (!byPackage.has(pkg)) byPackage.set(pkg, []);
349
439
  byPackage.get(pkg).push({
350
- // Package present however the note spells it see
440
+ // Package present for a `WHERE package = "…"` clause,
441
+ // synthesised rather than authored — see
351
442
  // {@link searchableFrontmatter} (#56).
352
443
  fm: searchableFrontmatter(p.fm, pkg),
353
444
  path: p.relPath,
@@ -367,10 +458,10 @@ export function tableUniverse(pages) {
367
458
  * redirect stub at each name. They are dropped, and this build emits no
368
459
  * redirects of its own.
369
460
  *
370
- * A content page carries the package the build **derived**, whether or not the
371
- * note declared one (#65). `package:` became optional in 3.3.0, so a swept tree
372
- * declares none and the note's frontmatter alone would then publish a page
373
- * that does not say which package it belongs to. The emitted page is what a
461
+ * A content page carries the package the build **derived** (#65). No note
462
+ * declares one `package:` is retired (#56) so the note's frontmatter alone
463
+ * would publish a page that does not say which package it belongs to. The
464
+ * emitted page is what a
374
465
  * theme reads: `breadcrumbs.html` builds its middle crumb from
375
466
  * `.Params.package`, so without it that crumb degrades from a linked, labelled
376
467
  * section to a bare type slug. Writing the derived value keeps a page
@@ -387,10 +478,8 @@ export function pageFrontmatter(page, { readmeSections = {}, decorate }) {
387
478
  if (page.kind === "content") {
388
479
  data = {
389
480
  ...fm,
390
- // Spread after the note's own frontmatter, so a note that declares
391
- // the field keeps its authored position and value and an unswept
392
- // tree emits byte-identically. Guarded because `package: undefined`
393
- // is not a value YAML can carry.
481
+ // Spread after the note's own frontmatter. Guarded because
482
+ // `package: undefined` is not a value YAML can carry.
394
483
  ...(page.pkg ? { package: page.pkg } : {}),
395
484
  slug,
396
485
  title: fm.title ?? name,
@@ -708,6 +797,10 @@ export function buildSite({ config, outRoot } = {}) {
708
797
  const resolved = config ?? loadPackConfig();
709
798
  const site = resolved.site;
710
799
  const scheme = resolved.publish.address;
800
+ // Homepage-only or homepage-plus-content (#55). The floor is the homepage,
801
+ // so this decides whether the *content* surfaces are published, never
802
+ // whether anything is.
803
+ const publishesContent = publishesContentPages(resolved);
711
804
 
712
805
  // Where the package is served, and where its content mounts inside it. The
713
806
  // two are separate facts: `base` is the package's own address on the site
@@ -724,9 +817,16 @@ export function buildSite({ config, outRoot } = {}) {
724
817
  // directory it was launched from (#1508).
725
818
  const outBase = resolveOutputRoot(resolved.rootDir, site.out);
726
819
  const out =
727
- outRoot ?
728
- path.resolve(outRoot)
729
- : path.join(outBase, scheme.prefix.replace(/\/$/, ""));
820
+ outRoot ? path.resolve(outRoot)
821
+ : publishesContent ?
822
+ path.join(outBase, scheme.prefix.replace(/\/$/, ""))
823
+ // Homepage-only has no content mount, so the package's root *is*
824
+ // the output root and `--out` redirects the whole of it.
825
+ : outBase;
826
+ // The homepage publishes at `/<contentPackage>/`, which is the package's
827
+ // own root — one level above the content mount, and the same directory in
828
+ // homepage-only mode.
829
+ const homeRoot = publishesContent ? outBase : out;
730
830
 
731
831
  const packages = new Set(
732
832
  site.packages.length ? site.packages : [resolved.contentPackage],
@@ -734,7 +834,8 @@ export function buildSite({ config, outRoot } = {}) {
734
834
 
735
835
  const ctx = {
736
836
  packages,
737
- // What a note that declares no `package:` belongs to (#56).
837
+ // The package every note in the tree belongs to. `package:` is
838
+ // retired, so this is the only source of it (#56).
738
839
  contentPackage: resolved.contentPackage,
739
840
  skipDirectories: resolved.skipDirectories,
740
841
  mount,
@@ -745,6 +846,25 @@ export function buildSite({ config, outRoot } = {}) {
745
846
  // note was deleted or renamed would otherwise linger and keep publishing.
746
847
  fs.rmSync(outBase, { recursive: true, force: true });
747
848
 
849
+ const homepages = collectHomepages(resolved.paths.content, ctx).pages;
850
+
851
+ // Homepage-only stops here, and stopping is the point: nothing below reads
852
+ // the content tree for pages, so `sohl-kethira-basic` and `harn-adventures`
853
+ // cannot publish one whatever else their `site:` block declares (#55).
854
+ if (!publishesContent) {
855
+ return {
856
+ gates: emptyGates(),
857
+ manifests: null,
858
+ tableErrors: [],
859
+ wikiErrors: [],
860
+ stats: {
861
+ homepages: writeHomepages(homeRoot, homepages, resolved),
862
+ landings: 0,
863
+ out: homeRoot,
864
+ },
865
+ };
866
+ }
867
+
748
868
  const content = collectContentPages(resolved.paths.content, ctx);
749
869
  const pages = [...content.pages];
750
870
  const fmLinkFindings = [...content.fmLinkFindings];
@@ -805,12 +925,21 @@ export function buildSite({ config, outRoot } = {}) {
805
925
  sectionTitle: site.backfillSections ? pluralTitle : null,
806
926
  });
807
927
 
928
+ // Last, and outside the mount: the package's front page is not part of the
929
+ // content tree it introduces.
930
+ const homepagesWritten = writeHomepages(homeRoot, homepages, resolved);
931
+
808
932
  return {
809
933
  gates,
810
934
  manifests: gates.manifests,
811
935
  tableErrors: rendered.tableErrors,
812
936
  wikiErrors: rendered.wikiErrors,
813
- stats: { ...rendered.byKind, landings, out },
937
+ stats: {
938
+ ...rendered.byKind,
939
+ homepages: homepagesWritten,
940
+ landings,
941
+ out,
942
+ },
814
943
  };
815
944
  }
816
945
 
@@ -53,7 +53,7 @@ import path from "node:path";
53
53
 
54
54
  import { canonicalKey, readCanonicalKey } from "./kb-manifest.mjs";
55
55
  import { hasDocEntry } from "./item-docs.mjs";
56
- import { notePackage } from "./note-package.mjs";
56
+ import { contentPackage } from "./content-package.mjs";
57
57
 
58
58
  /**
59
59
  * One page the site will publish, as the index needs to see it.
@@ -239,11 +239,11 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
239
239
  // stays because a bare `[[skill-lang]]` defaults to the citing
240
240
  // note's own package and must keep resolving unchanged; the
241
241
  // canonical form is what cross-package links use (#1499).
242
- // The page's package is derived — the site collection resolves it
243
- // and records it as `pkg` rather than read out of frontmatter,
244
- // where `package:` is optional and on its way out (#56).
242
+ // The page's package is the configured one — the site collection
243
+ // resolves it and records it as `pkg`. Never read out of
244
+ // frontmatter: `package:` is retired (#56).
245
245
  index.set(
246
- canonicalKey(e.pkg ?? notePackage(e.fm), type, shortcode),
246
+ canonicalKey(e.pkg ?? contentPackage(), type, shortcode),
247
247
  value,
248
248
  );
249
249
  // In Foundry an item and its documentation are two documents, so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "3.4.0",
3
+ "version": "5.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",
package/sohl/actors.mjs CHANGED
@@ -34,9 +34,8 @@
34
34
  * driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`). Must run
35
35
  * after the items pass, since it reads the items pack's generated JSON tree.
36
36
  *
37
- * The walk itself — filtering by package and type, skipping drafts,
38
- * expanding tables, converting wikilinks, writing the JSON and counting
39
- * errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
37
+ * The walk itself — filtering by type, expanding tables, converting
38
+ * wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
40
39
  * states only what makes this pass its own (#1509).
41
40
  */
42
41
 
package/sohl/items.mjs CHANGED
@@ -29,9 +29,8 @@
29
29
  * Not a standalone script — exports the `Items` compiler class, imported and
30
30
  * driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
31
31
  *
32
- * The walk itself — filtering by package and type, skipping drafts,
33
- * expanding tables, converting wikilinks, writing the JSON and counting
34
- * errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
32
+ * The walk itself — filtering by type, expanding tables, converting
33
+ * wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
35
34
  * states only what makes this pass its own (#1509).
36
35
  */
37
36
 
@@ -38,6 +38,7 @@
38
38
  */
39
39
 
40
40
  import { AS_AUTHORED, NUMBER, STRING } from "../engine/field-spec.mjs";
41
+ import { ENGINE_NOTE_SCHEMAS } from "../engine/note-schemas.mjs";
41
42
  import { ITEM_FIELDS } from "./item-fields.mjs";
42
43
 
43
44
  /** A map-valued property, whose entries the compiler walks by key. */
@@ -302,9 +303,16 @@ const PRESENTATION_FIELDS = Object.freeze({
302
303
  * Every content type this package compiles, and what a note of that type may
303
304
  * write.
304
305
  *
306
+ * The engine's own types are merged in first, so a SoHL tree is checked against
307
+ * one vocabulary rather than two. They are declared there rather than here
308
+ * because they are note-format knowledge — a `homepage` carries no `system`
309
+ * block and would mean the same thing for a game system that is not SoHL — and
310
+ * because a package declaring no `itemBuilders` never reaches this file (#51).
311
+ *
305
312
  * @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
306
313
  */
307
314
  export const NOTE_SCHEMAS = Object.freeze({
315
+ ...ENGINE_NOTE_SCHEMAS,
308
316
  ...Object.fromEntries(
309
317
  Object.entries(ITEM_FIELDS).map(([type, fields]) => [
310
318
  type,
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Whether this package publishes the pages its content tree compiles to.
3
+ *
4
+ * The one question every reader of the mode actually asks — the site build, to
5
+ * decide whether to walk the tree at all, and the link-manifest emitter, to
6
+ * decide whether an entry carries a web `path`. Written once here so the two
7
+ * cannot come to disagree about what a mode means.
8
+ *
9
+ * @param {{publish: {site: SiteMode}}} config - A resolved configuration.
10
+ * @returns {boolean} Whether content pages are published.
11
+ */
12
+ export function publishesContentPages(config: {
13
+ publish: {
14
+ site: SiteMode;
15
+ };
16
+ }): boolean;
1
17
  /**
2
18
  * Validate and normalize a content configuration.
3
19
  *
@@ -76,6 +92,77 @@ export const DEFAULT_ADDRESS_SCHEME: Readonly<{
76
92
  prefix: "";
77
93
  landing: "readme";
78
94
  }>;
95
+ /**
96
+ * How much of a package reaches the web.
97
+ *
98
+ * Every HeroicLands package publishes something: a top-level, human-authored
99
+ * homepage at `https://www.heroiclands.org/<contentPackage>/` saying what the
100
+ * module is, which system it needs and how to install it (#50). So there is no
101
+ * value here meaning *no web presence at all* — homepage-only is the **floor**,
102
+ * and the default.
103
+ *
104
+ * - `homepage` — the authored homepage, and **no other page**. The content tree
105
+ * is not walked for pages, `site.sections` / `site.trees` / `site.landing`
106
+ * emit nothing, and link-manifest entries carry no web `path`.
107
+ * - `content` — the homepage *plus* every page the content tree publishes: the
108
+ * knowledgebase, the extra trees, the section landings.
109
+ *
110
+ * **Homepage-only is a first-class mode, not an accommodation.**
111
+ * `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
112
+ * Fan Material Guidelines) and `harn-adventures` (HârnFanon under Lythia's
113
+ * terms) must each publish a homepage and nothing beneath it — two packages
114
+ * under two different fan-content licences. The boundary is **published
115
+ * content**: journal text, artwork, item descriptions, compiled notes. A
116
+ * human-authored page announcing the module discloses none of it. Because the
117
+ * failure mode is silent — a `site:` block added later ships licensed content
118
+ * with nobody noticing — the mode fences the content surfaces off rather than
119
+ * trusting a configuration to stay empty.
120
+ *
121
+ * This was a boolean until 5.0.0, and `false` read as "no web presence", which
122
+ * no longer describes any package. Both spellings are refused rather than
123
+ * mapped: a value silently reinterpreted reads to its author as though it still
124
+ * means what it said.
125
+ *
126
+ * @typedef {"homepage" | "content"} SiteMode
127
+ */
128
+ /**
129
+ * The publishing modes {@link PublishSwitches.site} may name, floor first.
130
+ *
131
+ * @satisfies {readonly SiteMode[]}
132
+ */
133
+ export const SITE_MODES: readonly ["homepage", "content"];
134
+ /**
135
+ * How much of a package reaches the web.
136
+ *
137
+ * Every HeroicLands package publishes something: a top-level, human-authored
138
+ * homepage at `https://www.heroiclands.org/<contentPackage>/` saying what the
139
+ * module is, which system it needs and how to install it (#50). So there is no
140
+ * value here meaning *no web presence at all* — homepage-only is the **floor**,
141
+ * and the default.
142
+ *
143
+ * - `homepage` — the authored homepage, and **no other page**. The content tree
144
+ * is not walked for pages, `site.sections` / `site.trees` / `site.landing`
145
+ * emit nothing, and link-manifest entries carry no web `path`.
146
+ * - `content` — the homepage *plus* every page the content tree publishes: the
147
+ * knowledgebase, the extra trees, the section landings.
148
+ *
149
+ * **Homepage-only is a first-class mode, not an accommodation.**
150
+ * `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
151
+ * Fan Material Guidelines) and `harn-adventures` (HârnFanon under Lythia's
152
+ * terms) must each publish a homepage and nothing beneath it — two packages
153
+ * under two different fan-content licences. The boundary is **published
154
+ * content**: journal text, artwork, item descriptions, compiled notes. A
155
+ * human-authored page announcing the module discloses none of it. Because the
156
+ * failure mode is silent — a `site:` block added later ships licensed content
157
+ * with nobody noticing — the mode fences the content surfaces off rather than
158
+ * trusting a configuration to stay empty.
159
+ *
160
+ * This was a boolean until 5.0.0, and `false` read as "no web presence", which
161
+ * no longer describes any package. Both spellings are refused rather than
162
+ * mapped: a value silently reinterpreted reads to its author as though it still
163
+ * means what it said.
164
+ */
165
+ export type SiteMode = "homepage" | "content";
79
166
  export type PackageKind = "systems" | "modules";
80
167
  export type PackDocumentType = "Actor" | "Adventure" | "Item" | "JournalEntry" | "Macro" | "Scene";
81
168
  /**
@@ -272,9 +359,10 @@ export type ManifestSwitches = {
272
359
  };
273
360
  export type PublishSwitches = {
274
361
  /**
275
- * Render this package's knowledgebase/site pages.
362
+ * How much of this package reaches the web.
363
+ * See {@link SITE_MODES}.
276
364
  */
277
- site: boolean;
365
+ site: SiteMode;
278
366
  manifests: ManifestSwitches;
279
367
  };
280
368
  export type ManifestSwitchesInput = {
@@ -394,8 +482,19 @@ export type DocsSpec = {
394
482
  itemFields?: DocPageSpec | undefined;
395
483
  };
396
484
  export type PublishSwitchesInput = {
397
- site?: boolean | undefined;
485
+ site?: SiteMode | undefined;
398
486
  manifests?: ManifestSwitchesInput | undefined;
487
+ address?: AddressSchemeInput | undefined;
488
+ };
489
+ export type AddressSchemeInput = {
490
+ /**
491
+ * Where the content tree mounts inside the package.
492
+ */
493
+ prefix?: string | undefined;
494
+ /**
495
+ * Which note addresses a whole section.
496
+ */
497
+ landing?: string | undefined;
399
498
  };
400
499
  /**
401
500
  * One entry of a consumer's `itemBuilders` registry.
@@ -502,7 +601,9 @@ export type ContentBuildConfigInput = {
502
601
  */
503
602
  relationships?: Relationships | undefined;
504
603
  /**
505
- * Publishing switches. Each defaults to off.
604
+ * Publishing switches. The manifest
605
+ * switches default to off; `site`
606
+ * defaults to `homepage`, the floor.
506
607
  */
507
608
  publish?: PublishSwitchesInput | undefined;
508
609
  };
@@ -2,18 +2,17 @@
2
2
  * The tallies one pass accumulates while walking the tree.
3
3
  *
4
4
  * `declined` and `skippedOther` are deliberately separate numbers. A declined
5
- * note is one this build **refused** — it names a package this repository does
6
- * not compile — and it is an error; a skipped one legitimately belongs to
7
- * another pass, and there are thousands of those. Folding the first into the
8
- * second is what let a whole tree be filtered out in silence (#56).
5
+ * note is one this build **refused** — it declares a retired frontmatter field
6
+ * — and it is an error; a skipped one legitimately belongs to another pass, and
7
+ * there are thousands of those. Folding the first into the second is what let a
8
+ * whole tree be filtered out in silence (#56).
9
9
  *
10
10
  * @typedef {object} PassStats
11
11
  * @property {number} compiled - Notes that became a document.
12
- * @property {number} skippedDraft - Notes marked `draft: true`.
13
12
  * @property {number} skippedNoId - Notes with no `id`, where that is tolerated.
14
13
  * @property {number} skippedOther - Notes this pass does not claim.
15
- * @property {number} declined - Notes refused because they declare another
16
- * package. Counted as errors, never as skips.
14
+ * @property {number} declined - Notes refused because they declare a retired
15
+ * frontmatter field. Counted as errors, never as skips.
17
16
  */
18
17
  /**
19
18
  * The shared walk → filter → expand → convert → build → write → count loop.
@@ -310,20 +309,16 @@ export class BasePackCompiler {
310
309
  * The tallies one pass accumulates while walking the tree.
311
310
  *
312
311
  * `declined` and `skippedOther` are deliberately separate numbers. A declined
313
- * note is one this build **refused** — it names a package this repository does
314
- * not compile — and it is an error; a skipped one legitimately belongs to
315
- * another pass, and there are thousands of those. Folding the first into the
316
- * second is what let a whole tree be filtered out in silence (#56).
312
+ * note is one this build **refused** — it declares a retired frontmatter field
313
+ * — and it is an error; a skipped one legitimately belongs to another pass, and
314
+ * there are thousands of those. Folding the first into the second is what let a
315
+ * whole tree be filtered out in silence (#56).
317
316
  */
318
317
  export type PassStats = {
319
318
  /**
320
319
  * - Notes that became a document.
321
320
  */
322
321
  compiled: number;
323
- /**
324
- * - Notes marked `draft: true`.
325
- */
326
- skippedDraft: number;
327
322
  /**
328
323
  * - Notes with no `id`, where that is tolerated.
329
324
  */
@@ -333,8 +328,8 @@ export type PassStats = {
333
328
  */
334
329
  skippedOther: number;
335
330
  /**
336
- * - Notes refused because they declare another
337
- * package. Counted as errors, never as skips.
331
+ * - Notes refused because they declare a retired
332
+ * frontmatter field. Counted as errors, never as skips.
338
333
  */
339
334
  declined: number;
340
335
  };
@@ -12,8 +12,9 @@
12
12
  * `package:` frontmatter and the compilers kept the ones that matched. Every
13
13
  * content tree is single-package — each is single-sourced in the repository that
14
14
  * ships it — so the field restated this constant once per note while a value
15
- * that matched nothing filtered the whole tree out in silence. The field is
16
- * being retired; the value stays, here, where it is declared once.
15
+ * that matched nothing filtered the whole tree out in silence. That field is
16
+ * retired and declaring it now fails the build; this value stays, here, where
17
+ * it is declared once.
17
18
  *
18
19
  * Stable across compilation targets. If this content were ever compiled for a
19
20
  * second game system, it would still be published as `sohl` — only the Foundry
@@ -274,17 +274,15 @@ export function collectContentDocs(contentBase: string): Array<{
274
274
  * Expand the fenced `dataview` tables in one note's markdown, before wikilinks
275
275
  * are resolved — so a generated cell may itself be a wikilink.
276
276
  *
277
- * A table searches only notes of the source note's own package, so a SoHL page
278
- * never tabulates setting-package content (and vice versa). Each candidate's
279
- * package is **derived** rather than read out of its frontmatter: `package:` is
280
- * optional, and comparing a declared value with an absent one would drop every
281
- * unswept — or every swept — note from the table (#56).
277
+ * A table searches the whole tree, which is one package's notes and nothing
278
+ * else so there is no longer a package to scope on. It used to filter, back
279
+ * when a tree could hold several packages' notes and `package:` said which was
280
+ * which; that field is retired and the filter with it (#56).
282
281
  *
283
282
  * @param {string} body - The note's markdown body.
284
283
  * @param {object} ctx
285
284
  * @param {Array<object>} ctx.docs - From {@link collectContentDocs}.
286
285
  * @param {string} ctx.name - The note, for the error message.
287
- * @param {string} [ctx.pkg] - The source note's package.
288
286
  * @param {object} [ctx.fm] - The source note's frontmatter, which is what a
289
287
  * query's `this` reads. Its entry in `docs` supplies the path as well.
290
288
  * @param {number} [ctx.bodyLine] - 1-based file line of the body's first line,
@@ -297,10 +295,9 @@ export function collectContentDocs(contentBase: string): Array<{
297
295
  * compile rather than shipping a table-shaped hole. The error carries
298
296
  * `position`, the directive's own line.
299
297
  */
300
- export function expandNoteTables(body: string, { docs, name, pkg, fm, bodyLine }: {
298
+ export function expandNoteTables(body: string, { docs, name, fm, bodyLine }: {
301
299
  docs: Array<object>;
302
300
  name: string;
303
- pkg?: string | undefined;
304
301
  fm?: object | undefined;
305
302
  bodyLine?: number | undefined;
306
303
  }): {