@heroiclands/package-build 4.0.0 → 6.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/CONTENT.md CHANGED
@@ -77,8 +77,10 @@ paths:
77
77
  stage: build/stage/packs
78
78
  unpack: build/tmp/packs
79
79
 
80
- # The one pack list. Order is load-bearing where one pass reads another's
81
- # output, and `packDirectories` is derived from it.
80
+ # The one pack list. `packDirectories` and the manifest's `packs` array are both
81
+ # derived from it, so order it for a reader browsing compendiums — the compile
82
+ # order is worked out separately, from what each pass reads (see "Declaration
83
+ # order is presentation" below).
82
84
  packs:
83
85
  - { name: items, type: Item, label: Items, folders: item-folders.yaml }
84
86
  - { name: journals, type: JournalEntry, label: Journals }
@@ -128,9 +130,11 @@ packageBuild:
128
130
  - { from: assets/icons, to: assets/icons }
129
131
 
130
132
  # Three independent switches — every combination is real — plus the address
131
- # scheme both `manifest` and `site` derive addresses under.
133
+ # scheme both `manifest` and `site` derive addresses under. `site` is a mode,
134
+ # not a boolean: `homepage` (the default) publishes the authored homepage and
135
+ # no other page; `content` publishes it plus every page the tree compiles to.
132
136
  publish:
133
- site: true
137
+ site: content
134
138
  manifests: { publish: true, consume: true }
135
139
  address:
136
140
  prefix: kb/
@@ -147,8 +151,8 @@ site:
147
151
 
148
152
  The loader validates the document, resolves every path against the directory
149
153
  the file sits in, fills the optional halves with their defaults
150
- (`skipDirectories: []`, `packageBuild: {}`, the conventional `paths`, every
151
- publishing switch off),
154
+ (`skipDirectories: []`, `packageBuild: {}`, the conventional `paths`, both
155
+ manifest switches off and `publish.site` at its `homepage` floor),
152
156
  derives `assetRoot`, `packDirectories`, `itemTypes` and `docEntryTypes`, and
153
157
  freezes the result. A malformed configuration throws a `TypeError` naming the
154
158
  offending field, so it fails at load rather than as an empty pack much later.
@@ -355,6 +359,55 @@ hand-authored `system.template.json` lived, and the package-id guard and the
355
359
  top-level `compatibility.minimum`, the id is derived from `package.json`
356
360
  `name`, and `@heroiclands/package-build` writes the manifest from this file.
357
361
 
362
+ ### Declaration order is presentation, not compile order
363
+
364
+ `packs:` is the manifest's `packs` array as well, so a consumer orders it for a
365
+ reader browsing compendiums. It is **not** the order the passes run in, and it
366
+ does not have to be: the compile order is derived from what each pass reads.
367
+
368
+ One pass reads another's output today. The actors pass resolves each being's
369
+ embedded items against the JSON the item passes wrote — a being names an item by
370
+ `(type, shortcode)` and never by the pack it ships in, so **every** Item pack has
371
+ to be compiled before the Actor pass, not merely the first. A compiler states
372
+ that on itself:
373
+
374
+ ```js
375
+ export class Actors extends BasePackCompiler {
376
+ static readsPackOutputOf = Object.freeze(["Item"]);
377
+ }
378
+ ```
379
+
380
+ The generator schedules each pass after the packs of every type it names, and
381
+ does so with the **smallest** reordering that works — the earliest declared pass
382
+ whose dependencies have all run goes next. A list already in a workable order is
383
+ therefore compiled exactly as declared, and one that is not moves only the
384
+ passes that had to move. When the two orders differ the build says so:
385
+
386
+ ```text
387
+ [INFO]: Pass order: characteristics, mysteries, characters — a pass that reads
388
+ another's output compiles after it, whatever order `packs:` declares.
389
+ ```
390
+
391
+ This used to be the author's problem, and a nasty one: an Actor pack declared
392
+ first compiled only where an earlier run had already left `build/packs-json`
393
+ populated. `build/` is gitignored, so it was green on every local tree that had
394
+ built once and exit 1 on every fresh checkout and CI runner, over a message that
395
+ named a missing directory rather than the ordering that caused it (#73). A
396
+ consumer registering a compiler of its own declares its dependencies the same
397
+ way; a type no pack of which is declared is simply not waited for.
398
+
399
+ **Compiling one pack by name is the case ordering cannot answer.**
400
+ `content-build package compile <name>` runs the pass you asked for and no other,
401
+ so a dependency that is neither in the run nor already on disk is reported
402
+ rather than ordered around:
403
+
404
+ ```text
405
+ error: pack "characters" (Actor) reads the compiled output of the Item pack
406
+ "characteristics", which this run does not compile and which
407
+ build/packs-json/characteristics does not hold — compile the whole
408
+ package, or compile "characteristics" first
409
+ ```
410
+
358
411
  ### An item type's default art
359
412
 
360
413
  A note that carries no `img:` gets its type's **default art**, and a type
@@ -423,17 +476,17 @@ npx content-build site [--out <dir>]
423
476
  npx content-build reachability <dir> [file] [--index <shortcode>]
424
477
  ```
425
478
 
426
- | Command | What it does |
427
- | -------------- | ----------------------------------------------------------------------------------------------------------------------------- |
428
- | `package` | Compile the content tree into LevelDB packs, unpack a shipped pack back to JSON, or clean one. See [Install](#install). |
429
- | `docs` | Render a generated reference from the configured registries. `item-fields` is the item-frontmatter page. |
430
- | `lint` | Check a content tree's addresses and its frontmatter. See [Linting a content tree](#linting-a-content-tree). |
431
- | `links` | Check that every link in the tree lands: dead anchors, dead qualified addresses, wikilinks in frontmatter, drifted manifests. |
432
- | `format` | Prettier, with the shared configuration. See [Prose: formatting and markdown](#prose-formatting-and-markdown). |
433
- | `markdown` | markdownlint, with the shared rule set — the structure Prettier is indifferent to. |
434
- | `manifest` | Emit this package's cross-package link manifest. See [Publishing a link manifest](#publishing-a-link-manifest). |
435
- | `site` | Publish the content tree as a website. See [Publishing a website](#publishing-a-website). |
436
- | `reachability` | Walk outward from an index note and report what no path reaches, for a tree meant to be navigable from one entry point. |
479
+ | Command | What it does |
480
+ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
481
+ | `package` | Compile the content tree into LevelDB packs, unpack a shipped pack back to JSON, or clean one. See [Install](#install). |
482
+ | `docs` | Render a generated reference from the configured registries. `item-fields` is the item-frontmatter page. |
483
+ | `lint` | Check a content tree's addresses and its frontmatter. See [Linting a content tree](#linting-a-content-tree). |
484
+ | `links` | Check that every link in the tree lands: dead anchors, dead qualified addresses, wikilinks in frontmatter, drifted manifests, and the package homepage's own addresses. |
485
+ | `format` | Prettier, with the shared configuration. See [Prose: formatting and markdown](#prose-formatting-and-markdown). |
486
+ | `markdown` | markdownlint, with the shared rule set — the structure Prettier is indifferent to. |
487
+ | `manifest` | Emit this package's cross-package link manifest. See [Publishing a link manifest](#publishing-a-link-manifest). |
488
+ | `site` | Publish the content tree as a website. See [Publishing a website](#publishing-a-website). |
489
+ | `reachability` | Walk outward from an index note and report what no path reaches, for a tree meant to be navigable from one entry point. |
437
490
 
438
491
  Every path, pack name and root it needs comes from the consuming repository's
439
492
  `package-build.config.yaml`, so the usual invocation takes no arguments beyond
@@ -467,6 +520,13 @@ in about a second and can gate a commit. An empty or untyped tree **fails**
467
520
  rather than passing: "every one of nothing is unique" is a vacuous pass, and it
468
521
  is exactly what a tree that failed to check out produces.
469
522
 
523
+ What that guard reports is an **empty walk**, not an empty set of addresses. A
524
+ note may be keyless by design — a homepage carries no `shortcode`, because it is
525
+ addressed by the package rather than by a slug — so a package in
526
+ `publish.site: homepage` mode has a content tree that is populated, correct and
527
+ permanently unkeyed. That tree passes; a tree holding no notes at all still
528
+ fails.
529
+
470
530
  ### Frontmatter, against the schema its type declares
471
531
 
472
532
  The same command also checks that each note's `sohl:` block is what its **type**
@@ -510,6 +570,61 @@ does not exist. Removing it was verified output-neutral first: across 1,735
510
570
  stripped notes, `package compile` produced byte-identical `build/packs-json` and
511
571
  the site build byte-identical `site/content`.
512
572
 
573
+ ### The homepage's own links
574
+
575
+ The homepage is the page a reader arrives at, and until #54 it was the one page
576
+ nothing checked. SoHL's landing pointed at `kb/creature/` and `kb/character/`
577
+ from the day those two types merged into `being` — two 404s on the package's
578
+ front page, through every build, because a landing's links went through no
579
+ checker at all.
580
+
581
+ `links` therefore audits a `type: homepage` note as well, and it reads **both**
582
+ halves of it. Of the six homepages authored today four carry every link in the
583
+ body as ordinary markdown and two carry them in `landing:` — and the one whose
584
+ dead links prompted this has an _empty body_. A dead link in a card is exactly
585
+ as broken as one in a paragraph, so `landing.install.url`, every
586
+ `cards…​.url` / `.href`, the markdown links inside the prose fields (`lead`,
587
+ `closing`, `install.intro`, `install.note`, a card's `description`, a link's
588
+ `note`) and the body's own markdown links are all read.
589
+
590
+ **`url` and `href` are not the same address and are not checked the same way.**
591
+ The theme resolves a `url` against the site with `relURL`, so a package writes
592
+ `kb/rules/` and is served `/sohl/kb/rules/` without naming its own prefix; an
593
+ `href` is an address that is _already_ resolved and is used verbatim, which is
594
+ what `cards.source: sections` fills in. A leading `/` is therefore a defect in a
595
+ `url` — Hugo prefixes it a second time — and correct in an `href`.
596
+
597
+ Four findings, and each one names the form to write instead:
598
+
599
+ | Finding | Why |
600
+ | ---------------------------- | --------------------------------------------------------------------------------- |
601
+ | A **retired content type** | `kb/creature/` when `creature` became `being`. The engine knows what was retired. |
602
+ | A **hardcoded absolute URL** | Into this package's own prefix, or into one a vendored manifest names. |
603
+ | A **root-relative `url:`** | `relURL` prefixes it again. `href:` is exempt — verbatim is what it means. |
604
+ | A **wikilink** | Nothing resolves one here: a homepage is published verbatim in every mode. |
605
+
606
+ That last one is why a homepage does **not** get the wikilink resolution every
607
+ other note body gets. In `homepage` mode the content tree is never walked, so
608
+ there is no index for a wikilink to resolve against — and giving the page one
609
+ would make the mode depend on exactly the machinery its licensing fence exists
610
+ to not build. So a landing addresses the web the way the web does, and a
611
+ wikilink on one is reported rather than resolved.
612
+
613
+ **What is checkable, and what is not.** Only an address into this site is, and
614
+ only against facts the build already holds — the retired-type table and the
615
+ package prefixes a vendored manifest names. Two things are deliberately not
616
+ attempted:
617
+
618
+ - **Whether an external URL answers.** There is no network at build time, and a
619
+ build must not go red because a third party is down.
620
+ - **Whether a live in-site address names a page that exists.** Several surfaces
621
+ a landing routes to are produced by other tools entirely — generated API
622
+ documentation, hand-authored Hugo sections — so this build does not hold the
623
+ set of published pages and would report a working link as dead. A bare
624
+ `https://www.heroiclands.org/<package>/` is left alone for the same reason it
625
+ cannot be improved: a package homepage is in no link manifest, so there is no
626
+ better form to write.
627
+
513
628
  ## Prose: formatting and markdown
514
629
 
515
630
  ```bash
@@ -594,10 +709,11 @@ It reads its whole input from configuration and takes nothing else:
594
709
  | `publish.address` | The address scheme those paths are derived under — see below. |
595
710
 
596
711
  **Both addresses are optional, independently.** A note that compiles into no
597
- document has no `uuid`, and a package that ships compendiums and publishes no
598
- site (`publish.site: false`) has no `path` on any entry. Neither is an error, and
599
- neither is guessed: inventing the missing one asserts a target that does not
600
- exist, which is the silent dead link the manifest exists to prevent.
712
+ document has no `uuid`, and a package that ships compendiums and publishes only
713
+ a homepage (`publish.site: homepage`) has no `path` on any entry its notes are
714
+ not pages. Neither is an error, and neither is guessed: inventing the missing one
715
+ asserts a target that does not exist, which is the silent dead link the manifest
716
+ exists to prevent.
601
717
 
602
718
  **`publish.manifests.publish` is a declaration, not a preference.** The file is
603
719
  vendored by other repositories and read as authoritative, so emitting one is a
@@ -615,7 +731,7 @@ time and 404s for the reader.
615
731
 
616
732
  ```yaml
617
733
  publish:
618
- site: true
734
+ site: content
619
735
  manifests: { publish: true, consume: true }
620
736
  address:
621
737
  prefix: kb/ # default: "" — the package root
@@ -658,6 +774,64 @@ address derivation, the address index, table expansion, wikilink resolution,
658
774
  code-fence protection, the foreign-manifest merge, the page emission and the
659
775
  section-landing backfill.
660
776
 
777
+ ### The homepage, and how much else is published
778
+
779
+ Every package is reachable at `https://www.heroiclands.org/<contentPackage>/`,
780
+ and what a reader finds there is a note in the content tree — one markdown file,
781
+ written by a person:
782
+
783
+ ```markdown
784
+ ---
785
+ type: homepage
786
+ title: HârnMaster Kethira Basic # optional; defaults to packageBuild.manifest.title
787
+ ---
788
+
789
+ What the module is, which system it needs, how to install it.
790
+ ```
791
+
792
+ That is the whole envelope. A homepage **compiles into no compendium
793
+ document**, appears in no pack and in no link manifest, and is addressed by the
794
+ _package_ rather than by its own name — so `name.full`, `shortcode` and `id`
795
+ decide nothing on it. It is dispatched on `type` like every other note, not on a
796
+ filename: `README.md` is already a section landing under `landing: readme`, and
797
+ in `sohl-thalorna` it is a developer explainer about the source tree.
798
+
799
+ `type: homepage` is declared by the **engine**, not by the `sohl` item registry,
800
+ so a package that configures no `itemBuilders` at all — `HarnMaster-3-FoundryVTT`
801
+ and every HM3 module — can author one. The `engine/` ÷ `sohl/` line is
802
+ note-format knowledge against game-system knowledge, and a homepage carries no
803
+ `system` block.
804
+
805
+ `publish.site` then says how much _else_ is published:
806
+
807
+ | Mode | What is published |
808
+ | ---------- | ------------------------------------------------------------------------------- |
809
+ | `homepage` | The authored homepage, and no other page. **The default, and the floor.** |
810
+ | `content` | The homepage plus every page the content tree compiles to, and its extra trees. |
811
+
812
+ There is no value meaning "no web presence": every package publishes its
813
+ homepage. It was a boolean until 5.0.0, and both spellings are now refused
814
+ naming the mode to write instead — see [MIGRATING.md](MIGRATING.md).
815
+
816
+ **Homepage-only is a first-class mode, not an accommodation.**
817
+ `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
818
+ Fan Material Guidelines) and `harn-adventures` (HârnFanon under Lythia's terms)
819
+ must each publish a homepage and nothing beneath it. The boundary is _published
820
+ content_ — journal text, artwork, item descriptions, compiled notes — and a page
821
+ announcing the module discloses none of it. Because the failure mode is silent,
822
+ the mode **fences the content surfaces off** rather than trusting a
823
+ configuration to stay empty: in `homepage` mode the tree is never walked for
824
+ pages, and `sections`, `trees`, `landing` and `backfillSections` emit nothing
825
+ even when they are declared.
826
+
827
+ That is separate from `publish.manifests.publish`, which stays off for both for
828
+ an unrelated reason: a link manifest is the dependency edge that would stop the
829
+ module being withdrawable, and a homepage is one row in a routing table.
830
+
831
+ The homepage is written at the root of `site.out` — the package's own address —
832
+ one level above the content mount, which is where `publish.address.prefix` puts
833
+ everything else.
834
+
661
835
  **What it does not do is decide addresses.** Those come from `publish.address`,
662
836
  the same setting the link manifest reads, so a page and its manifest entry cannot
663
837
  disagree about where the page is. Everything under `site:` is _framing_ —
@@ -686,7 +860,7 @@ site:
686
860
 
687
861
  | Key | What it decides |
688
862
  | ------------------ | ------------------------------------------------------------------------------------------------ |
689
- | `out` | The Hugo content root. **Required**, and wiped on every run — see the safety note below. |
863
+ | `out` | The Hugo content root. **Required** in both modes, and wiped on every run — see below. |
690
864
  | `base` | Where the package is served. Defaults to `/<contentPackage>/`. |
691
865
  | `packages` | Which content packages this site renders. Defaults to its own. |
692
866
  | `sections` | Landing title and hero per section, so a landing matches the card that links to it. |
@@ -717,6 +891,16 @@ rewrites repository-relative links in the developer docs to their published or
717
891
  GitHub addresses. Neither rewrite can fail a build; an unknown `{@link}` degrades
718
892
  to a code span.
719
893
 
894
+ `symbolMap` is resolved **against the repository root**, not the process cwd, so
895
+ `content-build site` reads the same map whatever directory it was invoked from.
896
+ Leaving it unset is the legitimate empty case — every `{@link}` degrades, and
897
+ nothing is reported. Setting it to a path that cannot be read, cannot be parsed,
898
+ or does not hold a name → page object **fails the build**, naming the file and
899
+ the reason: those were all indistinguishable from "no symbols" until #75, so a
900
+ site could publish 224 dead `{@link}` tags at exit 0. A map that is read reports
901
+ its symbol count at info level, which is the only way to tell a map that loaded
902
+ from one that loaded empty without reading the emitted HTML.
903
+
720
904
  A bundle supplies up to two hooks, and their order around the shared work is the
721
905
  point:
722
906
 
package/MIGRATING.md CHANGED
@@ -1,3 +1,117 @@
1
+ # Migrating to `@heroiclands/package-build` 6.0.0
2
+
3
+ **No configuration change to make, and one build check that may now fail.**
4
+ `packageBuild.manifest.packFolders` is compared against the `packs[]` the build
5
+ derives, and a folder naming a pack the package does not ship is an error.
6
+
7
+ ## 1. Check what `package-build manifest` says
8
+
9
+ Nothing to edit up front — run it, and the build names anything wrong:
10
+
11
+ ```text
12
+ package-build.config.yaml:164:23: error: packFolders: folder "HârnMaster 3 System" names pack "character", which this package does not ship (packs: items, system-help)
13
+ ```
14
+
15
+ Every name in a folder's `packs` must appear in the top-level `packs:` list —
16
+ companions included, since Foundry sees no difference. Delete a name that no
17
+ longer resolves, or correct it. An error stops the manifest being written, so
18
+ nothing half-right reaches the stage.
19
+
20
+ ## 2. The advisory needs no action
21
+
22
+ A pack no folder names is a **warning**, and the build continues:
23
+
24
+ ```text
25
+ package-build.config.yaml:162:13: warning: packFolders: pack "items" is named by no folder, so it ships outside every folder this package declares
26
+ ```
27
+
28
+ Shipping one pack at the root can be deliberate, so this is not an error. But a
29
+ package that bothered to declare a folder rarely meant to leave one out — that
30
+ is exactly how `HarnMaster-3-FoundryVTT` shipped 1,577 of 1,597 documents loose
31
+ beside its folder. Add the pack to a folder, or leave it and take the advisory.
32
+
33
+ A package that declares no `packFolders` at all is unaffected, and says nothing.
34
+
35
+ ## 3. Nothing else
36
+
37
+ - **No configuration key changed**, and no CLI command, flag or exit code beyond
38
+ `manifest` failing on the error above.
39
+ - `writeManifest` takes an optional `configFile`, so a finding names the line it
40
+ is about. Omitting it costs the position, not the finding.
41
+
42
+ # Migrating to `@heroiclands/package-build` 5.0.0
43
+
44
+ **One configuration change: `publish.site` is a mode, not a boolean.** And one
45
+ new authoring capability that needs no migration: a `type: homepage` note.
46
+
47
+ ## 1. Respell `publish.site`
48
+
49
+ ```yaml
50
+ publish:
51
+ site: content # was `site: true`
52
+ ```
53
+
54
+ ```yaml
55
+ publish:
56
+ site: homepage # was `site: false`, or absent
57
+ ```
58
+
59
+ `homepage` is the default, so a repository that never set the key needs no edit.
60
+ A repository that set it to either boolean gets a `TypeError` at load naming the
61
+ mode to write:
62
+
63
+ ```text
64
+ package-build config: `publish.site` is no longer a boolean — write `site: content`. Every package publishes an authored homepage at /<contentPackage>/, so no value means "no web presence": `homepage` publishes that page and nothing else, and `content` publishes it plus every page the content tree compiles to.
65
+ ```
66
+
67
+ Both spellings are **refused rather than mapped** onto the nearest mode. `false`
68
+ read as _this package has no web presence_, which now describes no package at
69
+ all, and a value silently reinterpreted reads to its author as though it still
70
+ means what it said.
71
+
72
+ Nothing else about publishing moved: `publish.address`, `publish.manifests` and
73
+ the whole `site:` block are unchanged, and every address `sohl` and `thalorna`
74
+ already publish is byte-identical across the upgrade.
75
+
76
+ ## 2. Author a homepage (optional here, required by #52)
77
+
78
+ Every package is reachable at `https://www.heroiclands.org/<contentPackage>/`,
79
+ and the page there is a note in the content tree:
80
+
81
+ ```markdown
82
+ ---
83
+ type: homepage
84
+ title: HârnMaster Kethira Basic # optional; defaults to packageBuild.manifest.title
85
+ ---
86
+
87
+ What the module is, which system it needs, how to install it.
88
+ ```
89
+
90
+ It compiles into no compendium document, appears in no pack and in no link
91
+ manifest, and is addressed by the package rather than by its own name. It is
92
+ written to the root of `site.out`, one level above the content mount.
93
+
94
+ A repository with no homepage note publishes none, and the site build says so in
95
+ its count. Requiring exactly one is a separate change (#52).
96
+
97
+ ## 3. What homepage-only means
98
+
99
+ `homepage` mode does not merely leave the content configuration unused — it
100
+ **fences the content surfaces off**. The tree is never walked for pages, and
101
+ `site.sections`, `site.trees`, `site.landing` and `site.backfillSections` emit
102
+ nothing even when they are declared.
103
+
104
+ That is deliberate, and it is a licensing requirement rather than a preference.
105
+ `sohl-kethira-basic` (Keléstia Productions' Fan Material Guidelines) and
106
+ `harn-adventures` (HârnFanon under Lythia's terms) publish a homepage and no
107
+ other page; the failure mode is silent — a `site:` block added later ships
108
+ licensed content with nobody noticing — so the property is asserted by the code
109
+ path rather than left to configuration.
110
+
111
+ `publish.manifests.publish` is a separate decision and stays `false` for both: a
112
+ link manifest is the dependency edge that would stop a module being withdrawable,
113
+ and a homepage is not.
114
+
1
115
  # Migrating to `@heroiclands/package-build` 4.0.0
2
116
 
3
117
  **One authoring change: delete `package:` from every content note.** A note's
package/README.md CHANGED
@@ -255,6 +255,36 @@ comes from.
255
255
  a companion is only a pack written by another pass rather than one of its own.
256
256
  Give each pack the `label` you want Foundry to show.
257
257
 
258
+ #### `packFolders` is checked against the packs you ship
259
+
260
+ `packFolders` is the one **declared** key that names something the build
261
+ **derives**. Every other declared key states a fact about the package (`title`,
262
+ `socket`, `grid`) or addresses a staged file (`esmodules`, `styles`,
263
+ `languages`) — a staged file being a different relation, answered against the
264
+ stage rather than against configuration. So it is the one place a declaration
265
+ can quietly go stale against a value the build already computed, and it did:
266
+ `HarnMaster-3-FoundryVTT` shipped a folder naming four packs, three of which had
267
+ not existed since its compendium was consolidated, while `items` — 1,577 of
268
+ 1,597 documents — sat in no folder at all, with the build reporting nothing.
269
+
270
+ `package-build manifest` now compares the two, descending through nested folders
271
+ (Foundry allows three levels), and reports in the usual
272
+ `file:line:column: severity: message` form:
273
+
274
+ | Finding | Severity | Why |
275
+ | ----------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------- |
276
+ | a folder names a pack the package does not ship | **error** | Foundry silently skips a name it cannot resolve, so the declaration does nothing; no arrangement intends it |
277
+ | a pack no folder names | **warning** | legal, and a root-level pack can be deliberate — but a package that declared a folder rarely meant to leave one out |
278
+ | no `packFolders` declared at all | nothing | everything at the root is an arrangement, not an omission |
279
+
280
+ An error **stops the write**: a manifest already known to describe packs that do
281
+ not exist should not reach the stage, where the next command would deploy it.
282
+
283
+ ```text
284
+ package-build.config.yaml:164:23: error: packFolders: folder "HârnMaster 3 System" names pack "character", which this package does not ship (packs: items, system-help)
285
+ package-build.config.yaml:162:13: warning: packFolders: pack "items" is named by no folder, so it ships outside every folder this package declares
286
+ ```
287
+
258
288
  `compatibility` and `relationships` are read from the **top level** of the
259
289
  shared configuration, not from this section — content-build consumes them
260
290
  (`supportedCoreVersion`, and a module'''s `stats.systemVersion`) and the
@@ -76,6 +76,9 @@ import { lintFrontmatter } from "../engine/frontmatter-lint.mjs";
76
76
  // set — an adventure module ships skills, beings and magic swords — so no
77
77
  // consumer gets a subset (#19, #20).
78
78
  import { NOTE_SCHEMAS } from "../sohl/note-schemas.mjs";
79
+ // The engine's own types, merged under the registry's so the vocabulary stands
80
+ // in a package that configures no `itemBuilders` at all (#51).
81
+ import { ENGINE_NOTE_SCHEMAS } from "../engine/note-schemas.mjs";
79
82
  import { checkFormatting, lintMarkdown } from "../engine/prose-lint.mjs";
80
83
  import { emitLinkManifest } from "../engine/manifest-emit.mjs";
81
84
  import {
@@ -346,7 +349,7 @@ function lintCommand() {
346
349
  skipDirectories: config.skipDirectories,
347
350
  });
348
351
  const frontmatter = lintFrontmatter(index, {
349
- schemas: NOTE_SCHEMAS,
352
+ schemas: { ...ENGINE_NOTE_SCHEMAS, ...NOTE_SCHEMAS },
350
353
  references: argv.references,
351
354
  });
352
355
 
@@ -363,7 +366,8 @@ function lintCommand() {
363
366
  } else {
364
367
  log.info(
365
368
  `Addresses and frontmatter are well-formed ` +
366
- `(${addresses.keys} across ${addresses.notes} note(s)).`,
369
+ `(${addresses.keys} address(es) across ` +
370
+ `${addresses.notes} note(s)).`,
367
371
  );
368
372
  }
369
373
  } catch (err) {
@@ -579,6 +583,7 @@ function linksCommand() {
579
583
  deadAnchors,
580
584
  deadAddresses,
581
585
  frontmatterLinks,
586
+ homepageLinks,
582
587
  usedManifest,
583
588
  } = auditLinks(index);
584
589
 
@@ -611,10 +616,24 @@ function linksCommand() {
611
616
  });
612
617
  }
613
618
 
619
+ // The package homepage. Its addresses are markdown links and
620
+ // `landing:` url/href fields rather than wikilinks — it is
621
+ // published verbatim, so nothing resolves a wikilink on it —
622
+ // and until #54 nothing looked at them at all.
623
+ for (const h of homepageLinks) {
624
+ emitDiagnostic({
625
+ file: h.note.file,
626
+ ...positionOfLiteral(h.note.raw, h.text, h.occurrence),
627
+ severity: "error",
628
+ message: `${h.field}: ${h.message}`,
629
+ });
630
+ }
631
+
614
632
  const failures =
615
633
  deadAnchors.length +
616
634
  deadAddresses.length +
617
- frontmatterLinks.length;
635
+ frontmatterLinks.length +
636
+ homepageLinks.length;
618
637
  if (failures) {
619
638
  log.error(
620
639
  `${failures} link problem(s) across ${index.notes.length} note(s).`,
@@ -625,7 +644,8 @@ function linksCommand() {
625
644
  `${index.notes.length} notes: every anchor link lands ` +
626
645
  `and every qualified address resolves ` +
627
646
  `(${usedManifest.size} cross-package reference(s) ` +
628
- `via manifest), no wikilink in frontmatter.`,
647
+ `via manifest), no wikilink in frontmatter, ` +
648
+ `every homepage address resolvable.`,
629
649
  );
630
650
  }
631
651
  } catch (err) {
@@ -823,7 +843,8 @@ function siteCommand() {
823
843
 
824
844
  const s = result.stats;
825
845
  log.info(
826
- `wrote ${s.content ?? 0} content page(s) + ` +
846
+ `wrote ${s.homepages ?? 0} homepage(s) + ` +
847
+ `${s.content ?? 0} content page(s) + ` +
827
848
  `${s.tree ?? 0} tree page(s) + ${s.landings} ` +
828
849
  `landing(s) to ${path.relative(process.cwd(), s.out)}`,
829
850
  );
@@ -78,7 +78,7 @@ import yargs from "yargs";
78
78
  import { hideBin } from "yargs/helpers";
79
79
 
80
80
  import { loadPackageBuildConfig } from "../config.mjs";
81
- import { loadPackConfig } from "../engine/pack-config.mjs";
81
+ import { loadPackConfig, packConfigPath } from "../engine/pack-config.mjs";
82
82
  import { cleanBuildArtifacts, stageAssets } from "../stage.mjs";
83
83
  import { validateLangSource } from "../lang.mjs";
84
84
  import {
@@ -354,6 +354,8 @@ function manifestCommand() {
354
354
  artifact: config.artifact,
355
355
  outDir: path.join(config.rootDir, config.stageDir),
356
356
  flags,
357
+ // So a `packFolders` finding names the line it is about.
358
+ configFile: packConfigPath(),
357
359
  });
358
360
  console.log(
359
361
  `✅ Wrote ${path.relative(config.rootDir, written)} ` +