@heroiclands/package-build 22.1.1 → 22.3.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 (42) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/CONTENT.md +27 -32
  3. package/bin/content-build.mjs +0 -3
  4. package/bin/package-build.mjs +9 -26
  5. package/config.mjs +7 -2
  6. package/content-config.mjs +73 -41
  7. package/docs/api.md +26 -25
  8. package/docs/commands.md +22 -22
  9. package/docs/configuration.md +78 -46
  10. package/docs/content-format.md +36 -6
  11. package/engine/foundry-entries.mjs +12 -1
  12. package/engine/frontmatter-lint.mjs +2 -3
  13. package/engine/helpers.mjs +4 -1
  14. package/engine/ids.mjs +12 -0
  15. package/engine/note-claims.mjs +21 -0
  16. package/engine/pack-config.mjs +14 -1
  17. package/engine/pack-router.mjs +82 -5
  18. package/engine/pdf-build.mjs +2 -2
  19. package/engine/retired-fields.mjs +2 -3
  20. package/engine/schema-check.mjs +6 -6
  21. package/engine/site-build.mjs +59 -218
  22. package/engine/site-config.mjs +22 -22
  23. package/engine/site-index.mjs +18 -62
  24. package/engine/web-wikilinks.mjs +2 -16
  25. package/engine/wikilinks.mjs +35 -14
  26. package/manifest.mjs +16 -10
  27. package/package.json +1 -1
  28. package/release.mjs +41 -8
  29. package/sohl/kb-passes.mjs +14 -85
  30. package/types/config.d.mts +17 -0
  31. package/types/content-config.d.mts +6 -6
  32. package/types/engine/ids.d.mts +11 -0
  33. package/types/engine/pack-router.d.mts +20 -2
  34. package/types/engine/schema-check.d.mts +5 -5
  35. package/types/engine/site-build.d.mts +12 -62
  36. package/types/engine/site-config.d.mts +6 -8
  37. package/types/engine/site-index.d.mts +3 -28
  38. package/types/engine/web-wikilinks.d.mts +1 -1
  39. package/types/engine/wikilinks.d.mts +6 -3
  40. package/types/manifest.d.mts +5 -3
  41. package/types/release.d.mts +7 -6
  42. package/types/sohl/kb-passes.d.mts +5 -36
package/docs/commands.md CHANGED
@@ -166,7 +166,7 @@ Publish this package's DataModel field sets as `schema.json`.
166
166
  **SYNOPSIS**
167
167
 
168
168
  ```
169
- package-build schema [--check]
169
+ package-build schema
170
170
  ```
171
171
 
172
172
  **DESCRIPTION**
@@ -174,23 +174,20 @@ package-build schema [--check]
174
174
  Publishes the registries named in `packageBuild.schema` as `schema.json`, read
175
175
  by `content-build content-format schema` and, in a consuming package, by
176
176
  `content-build lint`'s emitted-versus-declared check. A repository that
177
- declares no registries has nothing to publish. `--check` compares the
178
- committed file against what the source would produce now instead of
179
- rewriting it, so CI can gate on the file being current. Reads the registries
180
- `packageBuild.schema` names; writes (or checks) `schema.json` at the
181
- repository root.
177
+ declares no registries has nothing to publish. Reads the registries
178
+ `packageBuild.schema` names; writes `build/schema.json`. A release publishes
179
+ it beside the archive and the manifest when `packageBuild.assets` names
180
+ `build/schema.json` (see `package-build release`); a module's
181
+ `content-build deps fetch` keeps the copy from the archive it downloads.
182
182
 
183
183
  **OPTIONS**
184
184
 
185
- | Option | Type | Default | Description |
186
- | --------- | ------- | ------- | ------------------------------------------------------------------------------ |
187
- | `--check` | boolean | `false` | Fail when the committed `schema.json` is out of date rather than rewriting it. |
185
+ None.
188
186
 
189
187
  **EXIT STATUS**
190
188
 
191
- 1 when `--check` finds the committed file does not match what the source
192
- would produce. 1 on any other thrown error. Otherwise 0 — including when
193
- `packageBuild.schema` is empty, which logs and publishes nothing.
189
+ 1 on any thrown error. Otherwise 0 including when `packageBuild.schema` is
190
+ empty, which logs and publishes nothing.
194
191
 
195
192
  **EXAMPLES**
196
193
 
@@ -202,7 +199,7 @@ package-build: no `packageBuild.schema` declared; nothing to publish.
202
199
  **SEE ALSO**
203
200
 
204
201
  `content-build content-format schema`, `content-build lint [root]`,
205
- [Configuration](configuration.md).
202
+ `package-build release`, [Configuration](configuration.md).
206
203
 
207
204
  ### `package-build manifest`
208
205
 
@@ -670,14 +667,17 @@ package-build release [--no-pdf]
670
667
 
671
668
  The artifact name comes from `packageKind` — a system ships as
672
669
  `system.json`'s sibling, a module as `module.json`'s — so no repository
673
- states it a second time. When the package publishes content
674
- (`publish.site: content`), it also builds the content-tree book (see
675
- `content-build pdf`) and reports it alongside the archive; `--no-pdf` skips
676
- that step for a release that has a tree but does not want the book this
677
- time. A book that fails to build is reported, never fatal — the archive
678
- above is the release regardless. Reads the staged package and (for the book)
679
- the content tree; writes `<artifact>.zip` and, unless skipped, the book,
680
- both under `build/dist`.
670
+ states it a second time. It also publishes the content index the manifest
671
+ advertises (`flags.metadataUrl`) and, when the stage carries one,
672
+ `schema.json` a repository that names `build/schema.json` in
673
+ `packageBuild.assets` gets it released beside the archive; one that does not
674
+ gets none. When the package publishes content (`publish.site: content`), it
675
+ also builds the content-tree book (see `content-build pdf`) and reports it
676
+ alongside the archive; `--no-pdf` skips that step for a release that has a
677
+ tree but does not want the book this time. A book that fails to build is
678
+ reported, never fatal — the archive above is the release regardless. Reads
679
+ the staged package and (for the book) the content tree; writes
680
+ `<artifact>.zip` and, unless skipped, the book, both under `build/dist`.
681
681
 
682
682
  **OPTIONS**
683
683
 
@@ -701,7 +701,7 @@ $ package-build release
701
701
 
702
702
  **SEE ALSO**
703
703
 
704
- `content-build pdf`, [Configuration](configuration.md).
704
+ `content-build pdf`, `package-build schema`, [Configuration](configuration.md).
705
705
 
706
706
  ### `package-build deploy <stage>`
707
707
 
@@ -752,28 +752,36 @@ Any other key under `docs.itemFields` is refused:
752
752
 
753
753
  **Type:** object · **Optional** · every key defaults to nothing published:
754
754
 
755
- | Key | Type | Default |
756
- | ----------------------- | -------- | --------------------- |
757
- | `site.base` | string | `""` |
758
- | `site.assets` | string | `""` |
759
- | `site.packages` | string[] | `[]` |
760
- | `site.sections` | object | `{}` |
761
- | `site.readmeSections` | object | `{}` |
762
- | `site.landing` | object | `null` |
763
- | `site.trees` | array | `[]` |
764
- | `site.pass` | string | `""` |
765
- | `site.passOptions` | object | `{}` |
766
- | `site.backfillSections` | boolean | `false` |
767
- | `site.list` | object | `{shortcodes: false}` |
768
- | `site.notfound` | object | `null` |
769
- | `site.hugo` | object | `{}` |
755
+ | Key | Type | Default |
756
+ | ----------------------- | -------- | ------------------------------------------- |
757
+ | `site.base` | string | `""` |
758
+ | `site.assets` | string | `""`, but required for `content-build site` |
759
+ | `site.description` | string | `""`, but required for `content-build site` |
760
+ | `site.packages` | string[] | `[]` |
761
+ | `site.sections` | object | `{}` |
762
+ | `site.landing` | object | `null` |
763
+ | `site.pass` | string | `""` |
764
+ | `site.passOptions` | object | `{}` |
765
+ | `site.backfillSections` | boolean | `false` |
766
+ | `site.list` | object | `{shortcodes: false}` |
767
+ | `site.notfound` | object | `null` |
768
+ | `site.hugo` | object | `{}` |
770
769
 
771
770
  How much of a package reaches the web at all is **not** here — it is
772
771
  [`publish.site`](#publish). `site` is framing: what a section is called,
773
- which extra trees are published beside the content, which named pass bundle
774
- supplies the repository's own body rewrites, and the residue of the
775
- [generated Hugo configuration](#the-generated-hugo-configuration) that is
776
- genuinely this repository's own.
772
+ which named pass bundle supplies the repository's own body rewrites, and the
773
+ residue of the [generated Hugo configuration](#the-generated-hugo-configuration)
774
+ that is genuinely this repository's own.
775
+
776
+ What the site publishes is the content tree, and nothing beside it. A page of
777
+ documentation is a note — `type: doc`, addressed by its shortcode, and
778
+ `pack: none` where it compiles into no Foundry document — so there is no
779
+ second mechanism for mounting a directory of markdown, and a configuration
780
+ that names one is refused with a message saying where the page goes instead:
781
+
782
+ > ``package-build config: `site.trees` is retired — a page is a note in the content tree. Give each page `type: doc`, a `shortcode` and `pack: none`, file it under `assets/content/`, and declare the section that lists it under `site.sections`.``
783
+
784
+ > ``package-build config: `site.readmeSections` is retired — a page is a note in the content tree. Give each page `type: doc`, a `shortcode` and `pack: none`, file it under `assets/content/`, and declare the section that lists it under `site.sections`.``
777
785
 
778
786
  Where the Hugo tree is written is not a choice. `content-build site` writes
779
787
  the whole Hugo source tree under `build/hugo/` — the generated `hugo.toml`,
@@ -785,7 +793,7 @@ published. A `site.out` is refused by name:
785
793
 
786
794
  > ``package-build config: `site` must be a mapping.``
787
795
 
788
- > ``package-build config: `site.<key>` is not a recognized option (expected one of: base, assets, packages, sections, readmeSections, landing, trees, pass, passOptions, backfillSections, list, notfound, hugo).``
796
+ > ``package-build config: `site.<key>` is not a recognized option (expected one of: base, assets, description, packages, sections, landing, pass, passOptions, backfillSections, list, notfound, hugo).``
789
797
 
790
798
  `site.assets` is the host every package's imagery is served from, and it is
791
799
  the one address in this file that is not this repository's own. A note names
@@ -798,11 +806,30 @@ Absolute, and the trailing slash is trimmed:
798
806
 
799
807
  > ``package-build config: `site.assets` must be an absolute `http://` or `https://` address — it is the host every package's imagery is served from, and a relative value resolves against whichever page happens to carry the image.``
800
808
 
809
+ `content-build site` refuses to generate a configuration with no
810
+ `site.assets` at all — there is no defensible default, because the theme
811
+ resolves every relative asset against it:
812
+
813
+ > ``package-build config: `site.assets` is not declared, and a site build needs one — it is the host every package's imagery is served from, and the theme resolves every relative asset against it.``
814
+
801
815
  The generated Hugo configuration carries the same host as
802
816
  `params.cdnBaseURL`, which the theme resolves a relative asset path against.
803
817
  The two are one value read by two readers: the toolchain emits it into a
804
818
  page, and the theme joins it onto anything the toolchain left relative.
805
819
 
820
+ `site.description` is the site's `<meta name="description">` — one plain
821
+ sentence, distinct from the Foundry package browser's pitch
822
+ ([`packageBuild.manifest.descriptionHtml`](#packagebuildmanifest), which
823
+ allows HTML). Required for `content-build site`, the way `packageBuild.manifest.title`
824
+ is:
825
+
826
+ > ``package-build config: `site.description` is not declared, and the site's `<meta name="description">` reads from it.``
827
+
828
+ Markup belongs in `descriptionHtml`, not here — a value containing `<` is
829
+ refused:
830
+
831
+ > ``package-build config: `site.description` contains `<` — this is plain text for the site's `<meta name="description">`; markup belongs in `packageBuild.manifest.descriptionHtml`.``
832
+
806
833
  `site.packages` names which content packages' notes the site walks, beyond
807
834
  this one's own; `site.pass` names a repository's own body-rewrite bundle
808
835
  (the one part of the site contract that is code, exactly as `itemBuilders`
@@ -812,25 +839,8 @@ names a registry):
812
839
 
813
840
  > ``package-build config: `site.packages[<index>]` must be a non-empty string.``
814
841
 
815
- `site.trees` are the extra trees published beside the content, each naming
816
- where it comes from and which section it publishes into:
817
-
818
- | Key (under `site.trees[]`) | Type | Required |
819
- | -------------------------- | ------ | -------- |
820
- | `site.trees[].from` | string | yes |
821
- | `site.trees[].section` | string | yes |
822
-
823
- > ``package-build config: `site.trees` must be a list.``
824
-
825
- > ``package-build config: `site.trees[<index>]` must be a mapping.``
826
-
827
- > ``package-build config: `site.trees[<index>].from` must be a non-empty string.``
828
-
829
- > ``package-build config: `site.trees[<index>].<key>` is not a recognized option (expected one of: from, section).``
830
-
831
- `site.sections` (and `site.readmeSections`, the same shape) is a closed
832
- vocabulary — a section's _only_ place to speak, since it exists solely as
833
- the generated `_index.md` this build writes for it:
842
+ `site.sections` is a closed vocabulary a section's _only_ place to speak,
843
+ since it exists solely as the generated `_index.md` this build writes for it:
834
844
 
835
845
  | Key (under `site.sections.<name>`) | Type | Required | Default |
836
846
  | ---------------------------------- | -------------------------- | -------- | ------- |
@@ -968,9 +978,9 @@ in it has one source, and that source is where it is edited:
968
978
  | `disableKinds` | whether any note in the tree carries `tags:`, which the site walk discovers: `["taxonomy", "term", "RSS"]` when none does, `["RSS"]` when at least one does |
969
979
  | `taxonomies` | the same fact — written as `{ tag = "tags" }` when at least one note carries `tags:`, absent otherwise |
970
980
  | `outputs` | the same fact — written as `{ taxonomy = ["HTML"], term = ["HTML"] }` when at least one note carries `tags:`, absent otherwise |
971
- | `params.description` | `package.json` `description`; absent when the package declares none |
981
+ | `params.description` | `site.description`, which is required |
972
982
  | `params.author` | `package.json` `author`, its `name`; absent when the package declares none |
973
- | `params.cdnBaseURL` | `site.assets`; absent when unset |
983
+ | `params.cdnBaseURL` | `site.assets`, which is required |
974
984
  | `params.brand` | the organisation's brand links — `logo`, `licenseURL`, `discordURL` — in `engine/site-config.mjs` |
975
985
  | `params.list` | `site.list` |
976
986
  | `params.notfound` | `site.notfound`; absent when undeclared |
@@ -991,6 +1001,17 @@ none fails the site build:
991
1001
 
992
1002
  > ``package-build config: `packageBuild.manifest.title` is not declared, and the site's `title` reads from it.``
993
1003
 
1004
+ `params.description` reads from `site.description` the same way, and fails
1005
+ the same way when it is absent:
1006
+
1007
+ > ``package-build config: `site.description` is not declared, and the site's `<meta name="description">` reads from it.``
1008
+
1009
+ `params.cdnBaseURL` reads from `site.assets`, and the theme resolves every
1010
+ relative asset against it, so a configuration declaring none fails the site
1011
+ build the same way:
1012
+
1013
+ > ``package-build config: `site.assets` is not declared, and a site build needs one — it is the host every package's imagery is served from, and the theme resolves every relative asset against it.``
1014
+
994
1015
  Nothing else is emitted. A site whose notes carry no `tags:` publishes no
995
1016
  taxonomy pages — `[taxonomies]` and `[outputs]` go unwritten, and Hugo's
996
1017
  defaults never apply because `taxonomy` and `term` are disabled kinds. A site
@@ -1305,9 +1326,9 @@ its content tree's addresses mount inside the package.
1305
1326
  Every HeroicLands package publishes at least an authored homepage at
1306
1327
  `https://www.heroiclands.org/<contentPackage>/` — there is no value meaning
1307
1328
  _no web presence at all_. `homepage` is the floor: the authored homepage
1308
- and nothing else, no content-tree walk, no `site.sections` / `site.trees` /
1309
- `site.landing` output. `content` is the homepage plus every page the
1310
- content tree publishes. `publishesContentPages(config)`, exported from
1329
+ and nothing else, no content-tree walk, no `site.sections` / `site.landing`
1330
+ output. `content` is the homepage plus every page the content tree
1331
+ publishes. `publishesContentPages(config)`, exported from
1311
1332
  `content-config.mjs` alongside [`compilesFoundryDocuments`](#packagekind),
1312
1333
  answers the one question every reader of the mode actually asks — the site
1313
1334
  build, to decide whether to walk the tree at all, and the content index, to
@@ -1436,11 +1457,22 @@ that has a wrong answer rather than an unknown one: a key the build
1436
1457
  silently overwritten and the two would be free to disagree with nothing to
1437
1458
  say so.
1438
1459
 
1460
+ `packageBuild.manifest.descriptionHtml` is the exception worth calling out on
1461
+ its own: it is not forbidden, it **is** how `description` is authored. It is
1462
+ the pitch Foundry's package browser shows — HTML allowed, any length — and it
1463
+ is emitted into the generated manifest as `description`; the key itself never
1464
+ survives into the manifest under its own name. `package.json`'s own
1465
+ `description` is read by neither this nor the site (see
1466
+ [`site.description`](#site)) — a declared one is reported as a warning naming
1467
+ both real keys, so it cannot drift back into use:
1468
+
1469
+ > `package.json: warning: \`description\` is read by nothing; the Foundry pitch is \`packageBuild.manifest.descriptionHtml\` and the site's is \`site.description\`` — a JSON manifest carries no line to point at, so only the file is named.
1470
+
1439
1471
  | Forbidden key | Derived from |
1440
1472
  | ------------------------------------- | ---------------------------------------------------------------- |
1441
1473
  | `packageBuild.manifest.id` | `foundryPackage`, itself derived from `package.json` `name` |
1442
1474
  | `packageBuild.manifest.version` | `package.json` `version` |
1443
- | `packageBuild.manifest.description` | `package.json` `description` |
1475
+ | `packageBuild.manifest.description` | `packageBuild.manifest.descriptionHtml` |
1444
1476
  | `packageBuild.manifest.url` | `package.json` `repository` |
1445
1477
  | `packageBuild.manifest.bugs` | `package.json` `repository` |
1446
1478
  | `packageBuild.manifest.manifest` | `package.json` `repository` and the release tag |
@@ -1451,7 +1483,7 @@ say so.
1451
1483
 
1452
1484
  > ``package-build config: `packageBuild.manifest.version` is derived from package.json `version` and must not be declared — it would be overwritten, and the two would disagree with nothing to say so.``
1453
1485
 
1454
- > ``package-build config: `packageBuild.manifest.description` is derived from package.json `description` and must not be declared — it would be overwritten, and the two would disagree with nothing to say so.``
1486
+ > ``package-build config: `packageBuild.manifest.description` is derived from `packageBuild.manifest.descriptionHtml` and must not be declared — it would be overwritten, and the two would disagree with nothing to say so.``
1455
1487
 
1456
1488
  > ``package-build config: `packageBuild.manifest` must be a mapping.``
1457
1489
 
@@ -801,13 +801,43 @@ hm3:
801
801
  Where no pack of that type is the default, the build refuses rather than
802
802
  guessing, and names the candidates.
803
803
 
804
- Three declarations are refused, each with the reason:
804
+ **`pack: none` compiles the note into no document.** The note is walked,
805
+ published as a page, present in the content index with an address and no
806
+ Foundry UUID, and addressable by wikilink like any other; every pack compiler
807
+ passes over it without a finding, and the check that reports a note nothing
808
+ compiles says nothing about it. Write it on a page that belongs on the website
809
+ and in no compendium — a page of developer documentation, a page about the
810
+ package itself:
805
811
 
806
- | written | why it is refused |
807
- | ----------------------------------- | --------------------------------------------------------------------------------- |
808
- | a **companion** pack | A companion is written by another pack's pass, so no note may be routed into one. |
809
- | a pack **nothing answers to** | The message lists the configured packs of that document type. |
810
- | a pack of **another document type** | A note's `pack:` names a pack of its own document type. |
812
+ ```yaml
813
+ type: doc
814
+ subType: concept
815
+ shortcode: architecture
816
+ pack: none
817
+ ```
818
+
819
+ A link to such a note resolves everywhere the note is addressable. On the web
820
+ it is an ordinary link to the page. In a compiled journal there is no document
821
+ to open, so the reader gets the link's text as prose and no `@UUID` — the
822
+ mirror of a link into a package that publishes documents and no pages.
823
+
824
+ It is accepted only on a type whose **sole document is the JournalEntry its
825
+ prose becomes** — `doc`, `place`, `lore` and `scenario`. On a type that
826
+ compiles an Item, an Actor, a Macro, a Scene or an Adventure it is refused by
827
+ name, because there it would drop the document the type exists to produce;
828
+ the message names that document. It is read per system like any other
829
+ `pack:`, so `<system>.pack: none` withholds one system's document while the
830
+ shared declaration names a pack for the rest, and a configured pack may not
831
+ be called `none`.
832
+
833
+ Four declarations are refused, each with the reason:
834
+
835
+ | written | why it is refused |
836
+ | ------------------------------------------- | -------------------------------------------------------------------------------------------- |
837
+ | a **companion** pack | A companion is written by another pack's pass, so no note may be routed into one. |
838
+ | a pack **nothing answers to** | The message lists the configured packs of that document type. |
839
+ | a pack of **another document type** | A note's `pack:` names a pack of its own document type. |
840
+ | **`none`** on a type with a second document | The Item, Actor, Macro, Scene or Adventure the type compiles into would be dropped, by name. |
811
841
 
812
842
  #### Template priority: which template wins
813
843
 
@@ -70,6 +70,7 @@ import { resolveNoteId } from "./note-ids.mjs";
70
70
  import { compendiumUuid, currentType, packForType, pageUuid } from "./ids.mjs";
71
71
  import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
72
72
  import { isHomepage } from "./homepage.mjs";
73
+ import { declaresNoPack } from "./pack-router.mjs";
73
74
  import { assertNoDeclaredPackage } from "./note-package.mjs";
74
75
  import { assertNoDeclaredFolder } from "./folder-notes.mjs";
75
76
  import {
@@ -179,7 +180,17 @@ export function entriesForNote(fm, name, address, body, ctx) {
179
180
  // and its id is hashed under the `folder` namespace against its own
180
181
  // address rather than under `document`. Emitting one would publish an
181
182
  // `Item` UUID for a `Folder`, at an id no document carries.
182
- id && !NEVER_PACKED_TYPES.has(String(type)) && !DERIVED_PACKED_TYPES.has(String(type)) ?
183
+ //
184
+ // And a note declaring **`pack: none`** is in no pack by its own
185
+ // statement: it publishes a page and compiles into nothing, so it has
186
+ // an address and no UUID. The shared declaration is what is read,
187
+ // because an entry names one document and this pass routes no system.
188
+ (
189
+ id &&
190
+ !NEVER_PACKED_TYPES.has(String(type)) &&
191
+ !DERIVED_PACKED_TYPES.has(String(type)) &&
192
+ !(routeFm && declaresNoPack(routeFm))
193
+ ) ?
183
194
  compendiumUuid(
184
195
  foundryPackageId,
185
196
  type,
@@ -1265,9 +1265,8 @@ export function lintNote(
1265
1265
  message: draftRetiredMessage(),
1266
1266
  });
1267
1267
  }
1268
- // Anchored at column 1 for the same reason `aliases` is: `section` names a
1269
- // configuration key too (`site.trees[].section`), and a nested one under
1270
- // some other block is not this field.
1268
+ // Anchored at column 1 for the same reason `aliases` is: a nested
1269
+ // `section` under some other block is not this field.
1271
1270
  if (Object.hasOwn(fm, "section")) {
1272
1271
  findings.push({
1273
1272
  file: note.file,
@@ -38,7 +38,7 @@ import { foundryAddressProblem, pathnameProblem, resolvePathname } from "./pathn
38
38
  import log from "loglevel";
39
39
 
40
40
  import { loadPackConfig } from "./pack-config.mjs";
41
- import { packRouter } from "./pack-router.mjs";
41
+ import { declaresNoPack, packRouter } from "./pack-router.mjs";
42
42
  import { contentPackage, foundryPackageId } from "./content-package.mjs";
43
43
  import { searchableFrontmatter } from "./note-package.mjs";
44
44
  import { PACKAGE_BASE } from "./content-address.mjs";
@@ -741,6 +741,9 @@ export function buildContentLinkIndex(
741
741
  // its prose compiles into lands — two documents, two packs.
742
742
  pack: router.resolveOrNull(fm, packForType(fm.type).docType),
743
743
  docPack: router.resolveOrNull(fm, "JournalEntry"),
744
+ // Whether the note declares `pack: none` and so has no document
745
+ // at all: a link to it names a page, never a compendium entry.
746
+ none: declaresNoPack(fm),
744
747
  shortcode: fm.shortcode ?? null,
745
748
  // What the note *is*, carried so a caller resolving a reference
746
749
  // can group by the family its target declares rather than only by
package/engine/ids.mjs CHANGED
@@ -86,6 +86,18 @@ export const MAP_SUBTYPES = Object.freeze(["battlemap", "localmap", "regionalmap
86
86
  */
87
87
  export const JOURNAL_TYPES = Object.freeze(new Set(["doc", "place", "lore", "scenario"]));
88
88
 
89
+ /**
90
+ * The `pack:` value that routes a note's document into no compendium.
91
+ *
92
+ * One reading, on the key that already decides which compendium receives a
93
+ * note: the note publishes to the site and compiles into nothing. Declared
94
+ * here, beside the pack table, so the configuration can refuse a pack of this
95
+ * name and the router can read the value from one spelling.
96
+ *
97
+ * @type {string}
98
+ */
99
+ export const NO_PACK = "none";
100
+
89
101
  /**
90
102
  * Content type → the pack its documents compile into, and the document type
91
103
  * that pack holds.
@@ -91,6 +91,7 @@ import { JOURNAL_TYPES, MAP_TYPES, PACK_BY_TYPE, RETIRED_TYPES, currentType } fr
91
91
  import { itemTypes } from "./item-registry.mjs";
92
92
  import { docEntryTypes } from "./item-docs.mjs";
93
93
  import { loadPackConfig } from "./pack-config.mjs";
94
+ import { declaresNoPack } from "./pack-router.mjs";
94
95
  import { locateFrontmatterKey } from "./retired-fields.mjs";
95
96
  import { noteTypesFor, subtypeRow } from "./document-subtypes.mjs";
96
97
  import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
@@ -641,6 +642,12 @@ export function unclaimedNoteFindings(config = loadPackConfig(), sources, { reco
641
642
  // {@link claimedNoteTypes} states: its JSON is checked in, it has no pass,
642
643
  // and no note is routed into it.
643
644
  const configured = new Set((config.packs ?? []).filter((p) => !p.prebuilt).map((p) => p.type));
645
+ // Every system a pack declares, plus the shared position: a note that
646
+ // answers `pack: none` for all of them compiles into nothing anywhere.
647
+ const routed = [
648
+ undefined,
649
+ ...new Set((config.packs ?? []).map((p) => p.system).filter(Boolean)),
650
+ ];
644
651
 
645
652
  // The corpus this compile derived once, required rather than
646
653
  // derived here: this module is imported *by* the content index, so it
@@ -681,6 +688,20 @@ export function unclaimedNoteFindings(config = loadPackConfig(), sources, { reco
681
688
  // Every document it produces has somewhere to go.
682
689
  if (produces.length && !missing.length) continue;
683
690
 
691
+ // `pack: none`, on a type whose only document is the JournalEntry its
692
+ // prose becomes: the note compiles into nothing **by declaration**, so
693
+ // a missing JournalEntry pack drops nothing it meant to keep. A type
694
+ // that also produces an Item or an Actor is not excused here — the
695
+ // router refuses that declaration by name when a pass asks it, and
696
+ // where no pass does, the finding below still says nothing claims the
697
+ // note.
698
+ if (
699
+ produces.every((docType) => docType === "JournalEntry") &&
700
+ routed.every((system) => declaresNoPack(fm, system))
701
+ ) {
702
+ continue;
703
+ }
704
+
684
705
  // Some do and some do not: the note compiles, and one of its documents
685
706
  // is dropped in silence. A type nothing produces at all falls past this
686
707
  // to the unclaimed messages below, where `produces` being empty is itself
@@ -570,9 +570,22 @@ export function configFromData(data, configPath) {
570
570
  );
571
571
  }
572
572
  }
573
- const { pkg } = readPackageJson(rootDir);
573
+ const { manifestPath, pkg } = readPackageJson(rootDir);
574
574
  if (pkg.homepage !== undefined) input.homepage = pkg.homepage;
575
575
  if (pkg.author !== undefined) input.author = pkg.author;
576
+ // `description` is npm metadata nothing displays for a private package —
577
+ // read by neither the manifest nor the site, so a declared one is a
578
+ // warning rather than a silent no-op that looks like it did something.
579
+ if (pkg.description !== undefined) {
580
+ emitDiagnostic({
581
+ file: manifestPath,
582
+ severity: "warning",
583
+ message:
584
+ "`description` is read by nothing; the Foundry pitch is " +
585
+ "`packageBuild.manifest.descriptionHtml` and the site's is " +
586
+ "`site.description`",
587
+ });
588
+ }
576
589
 
577
590
  if (input.itemBuilders !== undefined) {
578
591
  const declared = input.itemBuilders;
@@ -65,11 +65,22 @@
65
65
  * a shared value that cannot describe a system-specific pack, so it does not
66
66
  * answer and that system falls through to its own default. Both were silent
67
67
  * losses of one system's whole document set.
68
+ * - **`pack: none` routes the document nowhere, on purpose.** The note is
69
+ * walked, indexed, published and linkable, and no compendium receives it:
70
+ * {@link createPackRouter}'s `resolve` answers `undefined`, which no pass's
71
+ * name equals, so every pass passes over the note as one it does not own.
72
+ * It is accepted only on a type whose sole document is the JournalEntry its
73
+ * prose becomes — `doc` and the other journal-only types. On a type that
74
+ * compiles an Item, an Actor, a Macro, a Scene or an Adventure it is refused
75
+ * by name, because there the declaration would drop the document the type
76
+ * exists to produce. Read per system like any other `pack:`, so a block may
77
+ * say `none` for one system and the shared declaration may name a pack for
78
+ * the rest.
68
79
  *
69
80
  * @module
70
81
  */
71
82
 
72
- import { packForType } from "./ids.mjs";
83
+ import { NO_PACK, packForType } from "./ids.mjs";
73
84
  import { loadPackConfig } from "./pack-config.mjs";
74
85
  import { blockProperty, systemBlock } from "./system-block.mjs";
75
86
 
@@ -95,6 +106,35 @@ export class PackRoutingError extends Error {
95
106
  */
96
107
  export const PACK_FIELD = "pack";
97
108
 
109
+ /**
110
+ * The `pack:` value that routes a note's document into no compendium — see
111
+ * {@link module:engine/ids.NO_PACK}. Re-exported here because this is the
112
+ * module that reads it; a configured pack may not take the name, so the value
113
+ * can never be mistaken for one.
114
+ */
115
+ export { NO_PACK };
116
+
117
+ /**
118
+ * Whether a note declares `pack: none` for one system's document.
119
+ *
120
+ * The same reading {@link createPackRouter}'s `resolve` applies: the system's
121
+ * block wins where it declares a pack, and the shared top-level value stands
122
+ * otherwise. Asked with no system, it reads the shared value alone, which is
123
+ * what every single-system build and the content index read.
124
+ *
125
+ * Pure, so the readers that never route — the Foundry-address pass, the link
126
+ * index, the unclaimed-type check — can ask it without a router.
127
+ *
128
+ * @param {object} fm - The note's frontmatter.
129
+ * @param {string} [system] - The system whose document is asked about.
130
+ * @returns {boolean} True when the answer for that document is `none`.
131
+ */
132
+ export function declaresNoPack(fm, system) {
133
+ const declared =
134
+ system === undefined ? fm?.[PACK_FIELD] : blockProperty(fm, system, PACK_FIELD);
135
+ return declared === NO_PACK;
136
+ }
137
+
98
138
  /**
99
139
  * Build the router for one configured pack list.
100
140
  *
@@ -103,7 +143,7 @@ export const PACK_FIELD = "pack";
103
143
  *
104
144
  * @param {readonly object[]} packs - The resolved `packs` list from
105
145
  * `defineConfig`.
106
- * @returns {{resolve: (fm: object, docType: string, system?: string) => string,
146
+ * @returns {{resolve: (fm: object, docType: string, system?: string) => string|undefined,
107
147
  * resolveOrNull: (fm: object, docType: string, system?: string) => string|undefined,
108
148
  * packsOfType: (docType: string) => string[],
109
149
  * defaultOf: (docType: string) => string|undefined}} The router.
@@ -179,6 +219,14 @@ export function createPackRouter(packs) {
179
219
  /** @param {object} fm */
180
220
  const noteLabel = (fm) => fm?.name?.full ?? fm?.shortcode ?? fm?.id ?? "a note";
181
221
 
222
+ /**
223
+ * `"a"` or `"an"`, so a document class reads as English in a message.
224
+ *
225
+ * @param {string} word - The word the article precedes.
226
+ * @returns {string} The article.
227
+ */
228
+ const article = (word) => (/^[AEIOUaeiou]/.test(word) ? "an" : "a");
229
+
182
230
  /**
183
231
  * The pack a note declaring none is routed to.
184
232
  *
@@ -212,8 +260,11 @@ export function createPackRouter(packs) {
212
260
  * @param {string} [system] - The system whose document is being routed. Its
213
261
  * block's `pack:` wins over the shared one; without it only the shared
214
262
  * declaration is read, which is every single-system build.
215
- * @returns {string} The pack name.
216
- * @throws {PackRoutingError} When the note routes nowhere.
263
+ * @returns {string|undefined} The pack name — or `undefined` where the
264
+ * note declares `pack: none`, which no pass's name equals, so every pass
265
+ * passes over the note as one it does not own.
266
+ * @throws {PackRoutingError} When the note routes nowhere, or declares
267
+ * `pack: none` on a type that compiles a document beside its prose.
217
268
  */
218
269
  function resolve(fm, docType, system) {
219
270
  const inBlock = system === undefined ? undefined : systemBlock(fm, system)?.[PACK_FIELD];
@@ -225,6 +276,29 @@ export function createPackRouter(packs) {
225
276
  // JournalEntry — is not what the author was addressing.
226
277
  const ownDocType = packForType(fm?.type).docType;
227
278
 
279
+ // `none`: the note compiles into no document. Only a type whose own
280
+ // document *is* the JournalEntry may say so — for every other type
281
+ // the declaration would drop the Item, Actor, Macro, Scene or
282
+ // Adventure the type exists to produce, and its prose with it. The
283
+ // document class comes from the same table every pass routes by, so
284
+ // the refusal names what the compile would have written. Asked only
285
+ // by the pass that writes the note's own document, as every `pack:`
286
+ // is, so the refusal is reported once rather than once per pass.
287
+ if (declared === NO_PACK && docType === ownDocType) {
288
+ if (ownDocType !== "JournalEntry") {
289
+ const spelled = authoredInBlock ? `${system}.pack: ${NO_PACK}` : `pack: ${NO_PACK}`;
290
+ throw new PackRoutingError(
291
+ `${noteLabel(fm)} declares \`${spelled}\`, but a ${fm?.type} note ` +
292
+ `compiles into ${article(ownDocType)} ${ownDocType}, and ` +
293
+ `\`pack: ${NO_PACK}\` would drop it. Only a type whose sole ` +
294
+ `document is the JournalEntry its prose becomes may ` +
295
+ `declare it — name the pack the ${ownDocType} goes to, or ` +
296
+ `leave \`pack:\` unset for the default.`,
297
+ );
298
+ }
299
+ return undefined;
300
+ }
301
+
228
302
  // **A declaration naming another system's pack is not this system's
229
303
  // answer**, and which of two things that means depends on where
230
304
  // it was written.
@@ -328,7 +402,10 @@ export function createPackRouter(packs) {
328
402
  * @param {object} fm - The note's frontmatter.
329
403
  * @param {string} docType - The document type being addressed.
330
404
  * @param {string} [system] - The system whose document is addressed.
331
- * @returns {string|undefined} The pack name, or `undefined`.
405
+ * @returns {string|undefined} The pack name, or `undefined` — for a
406
+ * note that routes nowhere and for one declaring `pack: none`
407
+ * alike. A caller that must tell the two apart asks
408
+ * {@link declaresNoPack}.
332
409
  */
333
410
  resolveOrNull(fm, docType, system) {
334
411
  try {