@heroiclands/package-build 20.3.0 → 20.4.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 (178) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/CONTENT.md +111 -92
  3. package/README.md +2 -9
  4. package/bin/content-build.mjs +70 -61
  5. package/bin/package-build.mjs +2 -2
  6. package/config.mjs +6 -8
  7. package/content-config.mjs +187 -64
  8. package/docs/content-format.md +57 -68
  9. package/e2e.mjs +4 -4
  10. package/engine/actor-compiler.mjs +36 -36
  11. package/engine/address-charset.mjs +6 -6
  12. package/engine/address-diff.mjs +12 -11
  13. package/engine/anchored-sections.mjs +1 -1
  14. package/engine/anchors.mjs +1 -1
  15. package/engine/base-compiler.mjs +36 -38
  16. package/engine/bundle-notes.mjs +4 -4
  17. package/engine/bundles.mjs +8 -8
  18. package/engine/code-fences.mjs +3 -3
  19. package/engine/compendiums.mjs +8 -8
  20. package/engine/compile-corpus.mjs +4 -5
  21. package/engine/content-address.mjs +15 -15
  22. package/engine/content-charset.mjs +1 -1
  23. package/engine/content-format-check.mjs +24 -25
  24. package/engine/content-format.mjs +8 -8
  25. package/engine/content-html.mjs +202 -0
  26. package/engine/content-icons.mjs +225 -98
  27. package/engine/content-index.mjs +17 -17
  28. package/engine/content-links.mjs +30 -30
  29. package/engine/content-lint.mjs +20 -24
  30. package/engine/content-package.mjs +7 -9
  31. package/engine/content-slug.mjs +2 -2
  32. package/engine/content-tables.mjs +7 -8
  33. package/engine/content-tree.mjs +1 -1
  34. package/engine/derived-fields.mjs +174 -0
  35. package/engine/diagnostics.mjs +3 -4
  36. package/engine/document-subtypes.mjs +6 -7
  37. package/engine/field-reference.mjs +8 -8
  38. package/engine/field-spec.mjs +29 -30
  39. package/engine/folder-notes.mjs +14 -14
  40. package/engine/foreign-catalog.mjs +8 -9
  41. package/engine/foundry-entries.mjs +19 -20
  42. package/engine/frontmatter-lint.mjs +64 -69
  43. package/engine/frontmatter.mjs +11 -11
  44. package/engine/generate.mjs +39 -39
  45. package/engine/helpers.mjs +63 -54
  46. package/engine/homepage.mjs +20 -22
  47. package/engine/ids.mjs +15 -15
  48. package/engine/index-records.mjs +4 -4
  49. package/engine/index.mjs +14 -8
  50. package/engine/item-compiler.mjs +54 -29
  51. package/engine/item-docs.mjs +11 -13
  52. package/engine/item-registry.mjs +13 -13
  53. package/engine/journals.mjs +15 -15
  54. package/engine/macros.mjs +3 -3
  55. package/engine/map-notes.mjs +6 -6
  56. package/engine/metadata-index.mjs +5 -5
  57. package/engine/note-claims.mjs +36 -34
  58. package/engine/note-ids.mjs +5 -5
  59. package/engine/note-package.mjs +4 -4
  60. package/engine/note-renames.mjs +3 -3
  61. package/engine/note-schemas.mjs +3 -3
  62. package/engine/note-vocabulary.mjs +27 -26
  63. package/engine/pack-config.mjs +13 -13
  64. package/engine/pack-router.mjs +0 -0
  65. package/engine/prose-config.mjs +9 -9
  66. package/engine/prose-lint.mjs +12 -13
  67. package/engine/region-events.mjs +2 -2
  68. package/engine/retired-fields.mjs +24 -22
  69. package/engine/runtime-only-fields.mjs +1 -1
  70. package/engine/scene-levels.mjs +1 -1
  71. package/engine/scenes.mjs +19 -20
  72. package/engine/schema-check.mjs +20 -20
  73. package/engine/schema-extract.mjs +1 -1
  74. package/engine/site-build.mjs +45 -47
  75. package/engine/site-index.mjs +17 -18
  76. package/engine/sql-tables.mjs +21 -13
  77. package/engine/subtype-registry.mjs +5 -4
  78. package/engine/system-block.mjs +18 -19
  79. package/engine/systems.mjs +2 -2
  80. package/engine/web-wikilinks.mjs +35 -35
  81. package/engine/wikilink-syntax.mjs +16 -16
  82. package/engine/wikilinks.mjs +39 -39
  83. package/hm3/actors.mjs +22 -22
  84. package/hm3/document-subtypes.mjs +5 -5
  85. package/hm3/index.mjs +2 -2
  86. package/hm3/item-builders.mjs +1 -1
  87. package/hm3/item-fields.mjs +3 -3
  88. package/hm3/items.mjs +20 -8
  89. package/hm3/template-priority.mjs +2 -2
  90. package/lang.mjs +3 -3
  91. package/manifest.mjs +17 -20
  92. package/package.json +1 -2
  93. package/release.mjs +3 -3
  94. package/sohl/actors.mjs +28 -28
  95. package/sohl/affiliation-standings.mjs +2 -2
  96. package/sohl/being-info.mjs +5 -5
  97. package/sohl/default-item-art.mjs +5 -5
  98. package/sohl/document-subtypes.mjs +5 -5
  99. package/sohl/index.mjs +3 -3
  100. package/sohl/item-builders.mjs +7 -7
  101. package/sohl/item-fields.mjs +8 -9
  102. package/sohl/items.mjs +20 -6
  103. package/sohl/kb-passes.mjs +5 -5
  104. package/sohl/note-schemas.mjs +7 -7
  105. package/sohl/skill-base.mjs +1 -1
  106. package/types/content-config.d.mts +25 -24
  107. package/types/e2e.d.mts +3 -3
  108. package/types/engine/actor-compiler.d.mts +14 -13
  109. package/types/engine/address-charset.d.mts +6 -6
  110. package/types/engine/address-diff.d.mts +3 -3
  111. package/types/engine/base-compiler.d.mts +17 -19
  112. package/types/engine/bundle-notes.d.mts +3 -3
  113. package/types/engine/bundles.d.mts +1 -1
  114. package/types/engine/code-fences.d.mts +3 -3
  115. package/types/engine/compendiums.d.mts +3 -3
  116. package/types/engine/compile-corpus.d.mts +1 -1
  117. package/types/engine/content-address.d.mts +11 -11
  118. package/types/engine/content-format-check.d.mts +6 -6
  119. package/types/engine/content-format.d.mts +2 -2
  120. package/types/engine/content-html.d.mts +78 -0
  121. package/types/engine/content-icons.d.mts +150 -62
  122. package/types/engine/content-index.d.mts +2 -2
  123. package/types/engine/content-links.d.mts +8 -8
  124. package/types/engine/content-lint.d.mts +2 -2
  125. package/types/engine/content-package.d.mts +6 -8
  126. package/types/engine/derived-fields.d.mts +101 -0
  127. package/types/engine/diagnostics.d.mts +2 -2
  128. package/types/engine/document-subtypes.d.mts +3 -3
  129. package/types/engine/field-spec.d.mts +37 -39
  130. package/types/engine/folder-notes.d.mts +4 -5
  131. package/types/engine/foreign-catalog.d.mts +3 -3
  132. package/types/engine/foundry-entries.d.mts +6 -7
  133. package/types/engine/frontmatter-lint.d.mts +7 -7
  134. package/types/engine/frontmatter.d.mts +7 -7
  135. package/types/engine/generate.d.mts +7 -7
  136. package/types/engine/helpers.d.mts +25 -27
  137. package/types/engine/homepage.d.mts +16 -18
  138. package/types/engine/ids.d.mts +13 -13
  139. package/types/engine/index-records.d.mts +3 -3
  140. package/types/engine/index.d.mts +2 -0
  141. package/types/engine/item-compiler.d.mts +21 -5
  142. package/types/engine/item-docs.d.mts +2 -2
  143. package/types/engine/item-registry.d.mts +6 -6
  144. package/types/engine/journals.d.mts +3 -3
  145. package/types/engine/map-notes.d.mts +2 -2
  146. package/types/engine/metadata-index.d.mts +4 -4
  147. package/types/engine/note-claims.d.mts +16 -15
  148. package/types/engine/note-ids.d.mts +4 -4
  149. package/types/engine/note-package.d.mts +1 -1
  150. package/types/engine/note-renames.d.mts +3 -3
  151. package/types/engine/note-vocabulary.d.mts +9 -8
  152. package/types/engine/pack-config.d.mts +3 -3
  153. package/types/engine/pack-router.d.mts +1 -1
  154. package/types/engine/prose-config.d.mts +9 -9
  155. package/types/engine/prose-lint.d.mts +3 -4
  156. package/types/engine/region-events.d.mts +2 -2
  157. package/types/engine/retired-fields.d.mts +10 -9
  158. package/types/engine/schema-check.d.mts +11 -11
  159. package/types/engine/site-build.d.mts +18 -19
  160. package/types/engine/site-index.d.mts +3 -4
  161. package/types/engine/sql-tables.d.mts +11 -5
  162. package/types/engine/subtype-registry.d.mts +3 -3
  163. package/types/engine/system-block.d.mts +3 -3
  164. package/types/engine/web-wikilinks.d.mts +7 -7
  165. package/types/engine/wikilink-syntax.d.mts +17 -17
  166. package/types/engine/wikilinks.d.mts +13 -12
  167. package/types/hm3/actors.d.mts +1 -1
  168. package/types/hm3/document-subtypes.d.mts +1 -1
  169. package/types/hm3/items.d.mts +1 -2
  170. package/types/hm3/template-priority.d.mts +1 -1
  171. package/types/manifest.d.mts +8 -8
  172. package/types/sohl/actors.d.mts +2 -2
  173. package/types/sohl/affiliation-standings.d.mts +2 -2
  174. package/types/sohl/being-info.d.mts +2 -2
  175. package/types/sohl/document-subtypes.d.mts +1 -1
  176. package/types/sohl/items.d.mts +1 -2
  177. package/types/sohl/note-schemas.d.mts +1 -1
  178. package/MIGRATING.md +0 -608
package/CONTENT.md CHANGED
@@ -65,6 +65,35 @@ itemBuilders: sohl
65
65
  # Directory names the content walk ignores wherever they appear.
66
66
  skipDirectories: [Templates]
67
67
 
68
+ # Optional. The icon fonts this package ships and the names its notes draw from
69
+ # them. Nothing is supplied by default: an entry is a promise that a glyph will
70
+ # render, and only the package shipping the font can keep it.
71
+ #
72
+ # Either the registry itself, or a path to a file holding it — the file form is
73
+ # what a real package wants, because a registry is generated from what the
74
+ # interface draws and a generated document inlined here conflicts on every
75
+ # regeneration.
76
+ icons: assets/icon-registry.yaml
77
+ #
78
+ # The same, written inline:
79
+ #
80
+ # icons:
81
+ # families:
82
+ # # `class` is the stylesheet prefix; `styles` are the weights the font
83
+ # # ships, and `[]` means it has none.
84
+ # fontawesome: { class: fa, styles: [solid, regular, brands], describe: Font Awesome Free }
85
+ # game-icons: { class: ginf, styles: [], describe: the Game-Icons webfont }
86
+ # # Which family an entry with no `family` belongs to. Optional where exactly
87
+ # # one is declared, since then there is nothing to choose between.
88
+ # defaultFamily: fontawesome
89
+ # icons:
90
+ # # A name is what a note writes between the colons, in the charset an
91
+ # # address segment uses. `fixedWidth` asks for a full advance, which a
92
+ # # glyph like an ellipsis needs to sit in a column of controls.
93
+ # being: { style: solid, icon: user, label: being }
94
+ # context-menu: { style: solid, icon: ellipsis-vertical, fixedWidth: true, label: context menu }
95
+ # vehicle: { family: game-icons, icon: old-wagon, label: vehicle }
96
+
68
97
  # Optional; each path is relative to this file's directory and defaults to the
69
98
  # conventional layout shown here.
70
99
  paths:
@@ -349,7 +378,7 @@ pack: mysteries
349
378
  that type's default implicitly; a type with several designates one with
350
379
  `default: true`. Where several exist and none is marked, a declaration is
351
380
  **mandatory** and an undeclared note fails the build.
352
- - **A default is per system, not merely per type** (#58). The rule above counts
381
+ - **A default is per system, not merely per type**. The rule above counts
353
382
  every pack of a type together, so a tree shipping one Actor pack per system has
354
383
  two and would need a flag — except that asked _per system_ the layout is
355
384
  unambiguous, one pack each, for the same reason a single-pack type needs no
@@ -373,11 +402,11 @@ pack: mysteries
373
402
  it — an item's or a macro's prose, which compiles into a JournalEntry of its
374
403
  own — lands in the default pack of _that_ type.
375
404
  - **Every document a note produces needs a pack, and one that has none is a
376
- finding** (#152). A note produces more than one document as a matter of
405
+ finding**. A note produces more than one document as a matter of
377
406
  course: an item note an Item and the JournalEntry its prose becomes, a map
378
407
  note a Scene and a JournalEntry, an actor note an Actor and a JournalEntry
379
- since #337. Where the configuration declares no pack for one of them, that
380
- document used to be dropped while the rest of the note compiled into a pack
408
+ too. Where the configuration declares no pack for one of them, that
409
+ document would be dropped while the rest of the note compiled into a pack
381
410
  that does exist — a build that succeeds and ships half of what was written.
382
411
  The finding names the note, the class with no pack, and the class that did
383
412
  compile, which is what distinguishes it from a note **nothing** claims: that
@@ -388,7 +417,7 @@ pack: mysteries
388
417
  no doc — so a tree of deliberately description-less items loses nothing by
389
418
  having no JournalEntry pack, and is told nothing. And it names no system, so a
390
419
  type one system maps and another does not stays silent for the system that
391
- declines it, per #79.
420
+ declines it.
392
421
 
393
422
  **The configuration is found by walking up from the working directory, and from
394
423
  the installed package only if that finds nothing.** `engine/pack-config.mjs`
@@ -402,7 +431,7 @@ with **no `node_modules` of its own** resolves `@heroiclands/package-build` out
402
431
  of the parent's, because Node's resolution walks parent directories. Climbing
403
432
  from the module then landed on the _parent's_ configuration, and the build
404
433
  compiled the parent's content tree into the parent's `build/` and exited 0
405
- (#364). When both walks find a configuration and they disagree, the working
434
+ . When both walks find a configuration and they disagree, the working
406
435
  directory's is read and the ignored one is named in a warning — that
407
436
  disagreement is also the cheapest signal that this tree is building on another
408
437
  checkout's `node_modules`. Run `npm ci` in the worktree to give it its own.
@@ -435,7 +464,7 @@ resolved set at runtime; the compilers and the link-manifest emitter cannot come
435
464
  to disagree about which notes carry documentation.
436
465
 
437
466
  **Every note compiling into a system-bearing document carries documentation, and
438
- that includes actors** (#337). `docEntryTypes` is `itemTypes` plus the actor
467
+ that includes actors**. `docEntryTypes` is `itemTypes` plus the actor
439
468
  types the shipped subtype maps declare (`ACTOR_TYPES`, derived from them rather
440
469
  than listed again), plus `macro` and the map types. Only `doc` is outside it, for
441
470
  the reason that actually applies to it: its single document _is_ the prose.
@@ -487,7 +516,7 @@ than one registry declares, **throws** rather than answering with whichever was
487
516
  declared first.
488
517
 
489
518
  **`hm3` is a real registry, and the rest of the pipeline follows the same
490
- field.** Since #139 this package ships two system halves, `sohl/` and `hm3/`,
519
+ field.** This package ships two system halves, `sohl/` and `hm3/`,
491
520
  each with its own builders, its own default art and its own note-type →
492
521
  document-subtype map; they share the engine between them and import nothing from
493
522
  each other. A pack's `system:` is what selects among them — the Item and Actor
@@ -513,10 +542,9 @@ what its documents are stamped `_stats.systemId` and `systemVersion` from, and
513
542
  the version can only come from that block — or, for a package whose packs are
514
543
  all for its own system, from the package-wide stats. A pack naming a system that
515
544
  resolves to neither is refused at configuration time, naming the pack and the
516
- entry to add. It used to fall through and stamp `null` for both, which is the
517
- plausible lie #43 was about arriving by the one path the check did not cover:
518
- `harn-ensemble` shipped 2,513 compiled actors that way, out of a pack whose
519
- configuration says `system: sohl` on the line above.
545
+ entry to add. Falling through and stamping `null` for both is a plausible lie:
546
+ a pack whose configuration says `system: sohl` on the line above ships every
547
+ compiled actor with no system at all.
520
548
 
521
549
  No `default: true` anywhere, because each system has exactly one pack of each
522
550
  type and a default is resolved per system. Marking one is still allowed and
@@ -536,12 +564,12 @@ note says which by writing `hm3.type`. Nothing is inferred from the note's own
536
564
  `subType`, and a note that says nothing is an error naming the note and listing
537
565
  the permitted values.
538
566
 
539
- **Configuration is the source, and the manifest is generated from it.** That
540
- arrow used to point the other way: `paths.packageManifest` said where a
541
- hand-authored `system.template.json` lived, and the package-id guard and the
542
- `_stats.coreVersion` stamp both read out of it. Both are gone — the floor is the
543
- top-level `compatibility.minimum`, the id is derived from `package.json`
544
- `name`, and `@heroiclands/package-build` writes the manifest from this file.
567
+ **Configuration is the source, and the manifest is generated from it.** The
568
+ floor is the top-level `compatibility.minimum`, the id is derived from
569
+ `package.json` `name`, and `@heroiclands/package-build` writes the manifest from
570
+ this file. There is no `paths.packageManifest` pointing at a hand-authored
571
+ `system.template.json` for a package-id guard or a `_stats.coreVersion` stamp to
572
+ read out of.
545
573
 
546
574
  ### Declaration order is presentation, not compile order
547
575
 
@@ -572,11 +600,11 @@ passes that had to move. When the two orders differ the build says so:
572
600
  another's output compiles after it, whatever order `packs:` declares.
573
601
  ```
574
602
 
575
- This used to be the author's problem, and a nasty one: an Actor pack declared
603
+ This is not the author's problem, and it would be a nasty one: an Actor pack declaring
576
604
  first compiled only where an earlier run had already left `build/packs-json`
577
605
  populated. `build/` is gitignored, so it was green on every local tree that had
578
606
  built once and exit 1 on every fresh checkout and CI runner, over a message that
579
- named a missing directory rather than the ordering that caused it (#73). A
607
+ named a missing directory rather than the ordering that caused it. A
580
608
  consumer registering a compiler of its own declares its dependencies the same
581
609
  way; a type no pack of which is declared is simply not waited for.
582
610
 
@@ -654,7 +682,7 @@ address different places — `img:` a file Foundry serves, `banner:` a file the
654
682
  CDN serves — so they are stated apart rather than reconciled. See the
655
683
  [content format specification](docs/content-format.md#banner-addresses-the-cdn-not-the-foundry-install).
656
684
 
657
- #### "Names no art" and "wants no art" are different (#218)
685
+ #### "Names no art" and "wants no art" are different
658
686
 
659
687
  A note has two ways to leave `img:` empty, and they mean opposite things:
660
688
 
@@ -668,7 +696,7 @@ A note has two ways to leave `img:` empty, and they mean opposite things:
668
696
  `resolveImg` returns `null` for the first two and `""` for the third, and every
669
697
  caller pairs its default with **nullish** coalescing — `resolveImg(fm.img) ?? itemArt(type)`.
670
698
  Never `||`: that collapses a deliberate blank back into the default and takes the
671
- distinction away again, which is exactly what the function used to do.
699
+ distinction away again.
672
700
 
673
701
  **`portrait` is the same field twice over.** A being carries `img` (its token
674
702
  art) and `portrait` (its sheet portrait) independently, and both resolve through
@@ -681,13 +709,11 @@ rather than two.
681
709
  > **The rule is `img`'s, and does not extend to `title`.** `title` is not art
682
710
  > and never reaches `resolveImg`, so nothing here applies to it.
683
711
  >
684
- > The reason used to be sharper, and is no longer true: a note's top-level
685
- > `title` was _also_ the shared source for an `affiliation` item's `system.title`
686
- > (`sohl/item-fields.mjs`, "the style of address the office carries"), so one
687
- > authored key fed two unrelated destinations that disagreed about what empty
688
- > means — and `title: null` stringified into the compiled document as the literal
689
- > `"null"`. The field declares `topLevelMeans` now, so the top-level key is no
690
- > longer a source for it (#218).
712
+ > The field declares `topLevelMeans`, so the top-level key is not a source for
713
+ > an `affiliation` item's `system.title`. Were it one, a single authored key
714
+ > would feed two unrelated destinations that disagree about what empty means,
715
+ > and `title: null` would stringify into the compiled document as the literal
716
+ > `"null"`.
691
717
  >
692
718
  > So `title: null` is a note declining to state a heading, and the site emitter's
693
719
  > `fm.title ?? name` falls back to `name.full`. `title: ""` publishes a
@@ -699,9 +725,9 @@ rather than two.
699
725
  > not answer for the page's heading. Twenty-eight `sohl-kethira-basic`
700
726
  > affiliations write `sohl.title: ""`, meaning an office with no style of
701
727
  > address, and every one of them was reported as publishing a blank heading
702
- > until the check honoured the declaration (#312).
728
+ > until the check honoured the declaration.
703
729
 
704
- Because `""` used to mean "unset", a note still carrying that spelling has
730
+ Because `""` reads as "unset" elsewhere, a note carrying that spelling has
705
731
  quietly changed meaning, and the frontmatter lint says so — for either art
706
732
  field:
707
733
 
@@ -736,7 +762,7 @@ one failed the build with an error naming a module in someone else's package.
736
762
 
737
763
  Widening that map was not the fix. It is deliberately SoHL data, shared with the
738
764
  runtime's `SohlItem.getDefaultArtwork` so that the build-time and runtime
739
- defaults are one list and cannot drift (SoHL#932/#1510). Pairing art with the
765
+ defaults are one list and cannot drift. Pairing art with the
740
766
  builder instead moves it onto the seam a type is _already_ declared through, and
741
767
  costs the `sohl` package nothing: `ITEM_BUILDERS` reads each entry's image out of
742
768
  that same map, so there is still exactly one map — and the drift a test used to
@@ -770,7 +796,7 @@ to be written at.
770
796
  reaches `sohl.system.templatePriority` and `hm3.flags.hm3.templatePriority` —
771
797
  exactly as `portrait` reaches two differently-named fields from one shared
772
798
  property. A number is a template at that priority, `null` is not a template, and
773
- absent is an authoring error (#126, #266). The legacy in-block and top-level
799
+ absent is an authoring error. The legacy in-block and top-level
774
800
  positions are still read, in that order after `data:`, so a tree sweeps on its
775
801
  own schedule; `archetype` is the retiring spelling of the same field, still read
776
802
  last but **refused by the frontmatter linter** — a priority and the `archetypes`
@@ -809,7 +835,7 @@ system `S` is:
809
835
  a **dotted path** (`data.portrait`) rather than a sibling key;
810
836
  4. the field's own default.
811
837
 
812
- `FieldSpec.name` is that declared source. It used to mean "frontmatter key under
838
+ `FieldSpec.name` is that declared source. Read as "frontmatter key under
813
839
  `sohl:`", which is the degenerate case where source and destination happen to
814
840
  share a name.
815
841
 
@@ -821,7 +847,7 @@ is _the title of the note_ — the heading its page publishes under, which the
821
847
  site emitter reads. An `affiliation` item's `system.title` is _the style of
822
848
  address the office carries_ — Ajaw, Warden, a person's style within the body.
823
849
  They are unrelated quantities, and step 3 used to feed the second from the first
824
- (#218).
850
+ .
825
851
 
826
852
  That was not merely untidy, because **step 3 answers without applying
827
853
  `field.default`** — only step 2 does — so an authored `title: null` reached the
@@ -841,7 +867,7 @@ side too.** If the two positions hold unrelated quantities, then the _in-block_
841
867
  position is not the note-level field either — so a check about a note-level field
842
868
  (the page's heading, an art path) reads past a block key the note's own type
843
869
  claims for something else. Declaring `topLevelMeans` settles both directions at
844
- once; it was read for the emitted field alone until #312, which is how an
870
+ once. Reading it for the emitted field alone is how an
845
871
  affiliation's office style came to answer for its page heading.
846
872
 
847
873
  **An exempted field is still authorable**, at the two positions that describe the
@@ -968,14 +994,14 @@ is exactly what a tree that failed to check out produces.
968
994
  What that guard reports is an **empty walk**, not an empty set of addresses. A
969
995
  note may be keyless — a folder document carries no `shortcode` — so a tree of
970
996
  them is populated, correct and unkeyed. That tree passes; a tree holding no
971
- notes at all still fails. (The homepage used to be the headline example, because
997
+ notes at all still fails. (The homepage is not the headline example, though it was once
972
998
  it was addressed by the package rather than by a slug. It carries an address
973
- like every other note now (#182); the guard is unchanged, because what it reads
999
+ like every other note now; the guard is unchanged, because what it reads
974
1000
  was never the key count.)
975
1001
 
976
1002
  ### Exactly one homepage
977
1003
 
978
- A content tree declares **exactly one** `type: homepage` note (#52). Zero is an
1004
+ A content tree declares **exactly one** `type: homepage` note. Zero is an
979
1005
  error and two is an error, at the same severity, because they are one defect: a
980
1006
  package whose front page is not the page a person chose.
981
1007
 
@@ -983,10 +1009,8 @@ package whose front page is not the page a person chose.
983
1009
  authored homepage exists to prevent, and a silent one: the site build reports
984
1010
  `wrote 0 homepage(s)` and exits 0.
985
1011
  - _Two_ and it serves a page nobody chose. **This is a cardinality rule, and
986
- since #182 it is only that.** It used to rest on the fixed destination every
987
- homepage shared the second overwrote the first — so the address rule
988
- enforced it as a side effect. A homepage is written at its own address now, so
989
- two of them publish two pages and collide over nothing: the duplicate-address
1012
+ only that.** A homepage is written at its own address, so two of them publish
1013
+ two pages and collide over nothing: the duplicate-address
990
1014
  check catches only the pair that happen to share a shortcode, and says nothing
991
1015
  at all about a `homepage-root` beside a `homepage-front`. Which of the two the
992
1016
  redirect at `/<package>/` should name is a question nothing here can answer,
@@ -1022,7 +1046,7 @@ assets/content/homepage.md:3:7: error: duplicate `type: homepage` note, also dec
1022
1046
  ### Frontmatter, against the schema its type declares
1023
1047
 
1024
1048
  The same command also checks that each note's `sohl:` block is what its **type**
1025
- allows (#19). Five classes, all of them mistakes that were previously reported
1049
+ allows. Five classes, all of them mistakes that were previously reported
1026
1050
  somewhere other than where they were made, or not at all:
1027
1051
 
1028
1052
  - **Unknown or retired type** — a note on a retired spelling is told what
@@ -1055,7 +1079,7 @@ Nothing here writes. A check reports and an author fixes.
1055
1079
 
1056
1080
  ### The `data:` container is closed; the top level is not
1057
1081
 
1058
- A note's frontmatter has three regions, and only one of them is open (#128):
1082
+ A note's frontmatter has three regions, and only one of them is open:
1059
1083
 
1060
1084
  | region | describes | an unknown key is |
1061
1085
  | ---------------- | -------------------------------------------- | ------------------ |
@@ -1071,7 +1095,7 @@ the page's description.
1071
1095
 
1072
1096
  **`data:` is deliberately closed**, and that is the point of having it. The
1073
1097
  type-specific facts about a subject — a weapon's weight, an affliction's
1074
- transmission, a being's species — used to sit at the top level, where the
1098
+ transmission, a being's species — do not sit at the top level, where the
1075
1099
  pass-through rule applied to them too. So a misspelled `wieght` became a theme
1076
1100
  parameter rather than a finding, indistinguishable from a weapon that weighs
1077
1101
  nothing. Under `data:` the same key is reported where it was written, with the
@@ -1082,14 +1106,14 @@ assets/content/Gear/Axe.md:14:5: error: "wieght" is not a `data:` property of a
1082
1106
  ```
1083
1107
 
1084
1108
  **A system block is closed too, and which blocks exist is the configuration's
1085
- answer** (#58). A package is held to the blocks named after the systems it
1109
+ answer**. A package is held to the blocks named after the systems it
1086
1110
  declares it ships for, read from the three places that already declare them:
1087
1111
  `systems:`, a pack's own `system:`, and `stats.systemId` where neither is
1088
1112
  written. So a package shipping for HM3 has its `hm3:` block checked and a
1089
- package shipping for SoHL its `sohl:`. It used to be a constant, and the
1090
- constant was `sohl`: an `hm3:` block was never read at all, so every key in it
1091
- was discarded at compile without a word, while the block that _was_ checked was
1092
- named after a system the package does not ship for.
1113
+ package shipping for SoHL its `sohl:`. Held constant at `sohl`, an `hm3:` block
1114
+ would never be read at all: every key in it would be discarded at compile
1115
+ without a word, while the block that _was_ checked would be named after a system
1116
+ the package does not ship for.
1093
1117
 
1094
1118
  A pack's `system:` counts because it is already authoritative at compile — a
1095
1119
  note routed to a pack declaring one and carrying no such block fails the build —
@@ -1125,12 +1149,12 @@ values. A `weapon` declares none — SoHL distinguishes a weapon's uses by strik
1125
1149
  mode rather than by kind — so `subType` on one is a finding; a `skill` declares
1126
1150
  ten, so `subType: crafte` is a finding naming `craft`.
1127
1151
 
1128
- **A `type` and a `subType` are both held to `^[A-Za-z0-9]+$`** (#206) — the same
1152
+ **A `type` and a `subType` are both held to `^[A-Za-z0-9]+$`** — the same
1129
1153
  constant a `shortcode` is held to, read rather than restated. A type is a
1130
1154
  segment of every address — the first of the short form an author writes, the
1131
1155
  third of the canonical `package-system-type-shortcode` — so a hyphen in one is
1132
1156
  read back as a segment boundary nobody wrote. A `subType` reaches no address
1133
- since #204 retired sections, and keeps the rule anyway: it is a vocabulary term the toolchain keys
1157
+ sections are retired and keeps the rule anyway: it is a vocabulary term the toolchain keys
1134
1158
  on, and one charset that holds for every term is a rule an author can state. The
1135
1159
  rule is checked ahead of the closed-set check, which is what makes it reach a
1136
1160
  type whose values are declared but not yet enumerated:
@@ -1143,7 +1167,7 @@ One declared value broke that rule: a `doc`'s `user-guide`, now **`userguide`**.
1143
1167
  The old spelling was accepted for one transitional release, as a warning naming
1144
1168
  the replacement, because an error would have redded every tree that took the
1145
1169
  release before it had a chance to sweep. Every tree has swept, so the acceptance
1146
- is gone (#210): `user-guide` is refused by the charset check like any other
1170
+ is gone: `user-guide` is refused by the charset check like any other
1147
1171
  hyphenated value, and nothing retirement-specific was left to remove.
1148
1172
 
1149
1173
  The vocabulary lives in `engine/note-vocabulary.mjs`, one entry per note type,
@@ -1151,20 +1175,16 @@ taken from the content-format specification. It is note-format knowledge rather
1151
1175
  than any system's: `data:` holds what is true of the thing, and what a system
1152
1176
  makes of that value is declared in that system's own half.
1153
1177
 
1154
- **A third rule was retired (#79).** Every note used to be required to repeat its
1155
- own `type-shortcode` address in `aliases:`. That served one reader — Obsidian,
1156
- so `[[type-shortcode]]` resolved in the editor — and no build ever read it: both
1157
- resolvers parse the hyphen qualifier themselves. The project no longer authors
1158
- in Obsidian, so the rule cost a line of frontmatter per note for a reader that
1159
- does not exist. Removing it was verified output-neutral first: across 1,735
1160
- stripped notes, `package compile` produced byte-identical `build/packs-json` and
1161
- the site build byte-identical `site/content`.
1178
+ **There is deliberately no third rule** requiring every note to repeat its own
1179
+ `type-shortcode` address in `aliases:`. It would serve one reader — Obsidian, so
1180
+ `[[type-shortcode]]` resolves in the editor — and no build reads it: both
1181
+ resolvers parse the hyphen qualifier themselves. The field is retired.
1162
1182
 
1163
- **And the top-level field itself is now retired (#180).** `aliases:` fed the
1183
+ **And the top-level field itself is now retired.** `aliases:` fed the
1164
1184
  alias index, which is what a bare `[[Alias]]` was looked up in. That form
1165
1185
  resolved to nothing anywhere in the corpus, while the collision rule guarding it
1166
1186
  folded in every note's `name.full` and so decided what a note could be named
1167
- (#179). Both are gone: every wikilink is an address, written
1187
+ . Both are gone: every wikilink is an address, written
1168
1188
  `[[type-shortcode|Text]]`, and declaring `aliases:` is refused naming the file
1169
1189
  and the line.
1170
1190
 
@@ -1220,12 +1240,12 @@ nothing today will read it.
1220
1240
 
1221
1241
  A homepage declares a `shortcode` — conventionally `root` — and publishes at its
1222
1242
  address, `/<package>/homepage-root/`, written by the same rule as everything
1223
- else (#182). So `[[homepage-root|Read the introduction]]` is an ordinary
1243
+ else. So `[[homepage-root|Read the introduction]]` is an ordinary
1224
1244
  wikilink, resolving to the page the build actually writes.
1225
1245
 
1226
1246
  It did not use to be. A page's URL derived from `name.full` while a homepage's
1227
1247
  destination was fixed at `_index.md`, so `content-build lint` **refused** `name`
1228
- and `shortcode` on one (#53) — not out of tidiness, but because they were not
1248
+ and `shortcode` on one — not out of tidiness, but because they were not
1229
1249
  inert. A `shortcode` put the note in the address index, so
1230
1250
  `[[homepage-<shortcode>]]` resolved _green_ to a page the site build never
1231
1251
  wrote, and a build reporting a live link to a 404 is worse than one saying
@@ -1274,7 +1294,7 @@ is emitted into the published page, so an unrecognised key is a Hugo or theme
1274
1294
  parameter this build has never heard of and has no standing to reject; a closed
1275
1295
  list would make every new theme parameter wait on a package-build release.
1276
1296
  `aliases` is not in the class either — it is a retired field now, refused on
1277
- every note whatever its type (#180).
1297
+ every note whatever its type.
1278
1298
 
1279
1299
  **Where it fires: `content-build lint` only.** Unlike a rule about the shape of
1280
1300
  the _tree_, which the site build has its own reason to gate on, this is a
@@ -1348,7 +1368,7 @@ serves.
1348
1368
 
1349
1369
  ### The homepage's own links
1350
1370
 
1351
- The homepage is the page a reader arrives at, and until #54 it was the one page
1371
+ The homepage is the page a reader arrives at, and it is the one page
1352
1372
  nothing checked. SoHL's landing pointed at `kb/creature/` and `kb/character/`
1353
1373
  from the day those two types merged into `being` — two 404s on the package's
1354
1374
  front page, through every build, because a landing's links went through no
@@ -1484,7 +1504,7 @@ npx content-build markdown --fix # apply the fixes markdownlint can make
1484
1504
  ```
1485
1505
 
1486
1506
  Two conventions every content repository writes to, declared once here so a note
1487
- formatted in one is formatted the same way in the next (#69):
1507
+ formatted in one is formatted the same way in the next:
1488
1508
 
1489
1509
  - **`format`** runs Prettier. Same values SoHL has always used, so a module or a
1490
1510
  note moving between repositories does not reformat on arrival.
@@ -1518,7 +1538,7 @@ alone: a `prettier.config.mjs` that spreads `PRETTIER_BASE` without the `**/*.md
1518
1538
  override reindents every note at 4, and a partial `.prettierrc` such as
1519
1539
  `{"tabWidth": 2}` silently discards `printWidth: 100`, `trailingComma` and the
1520
1540
  rest. So every `format` run first names, as warnings, each shared convention this
1521
- repository resolves differently (#133):
1541
+ repository resolves differently:
1522
1542
 
1523
1543
  ```text
1524
1544
  prettier.config.mjs: warning: markdown `tabWidth` is 4 here; the shared configuration says 2
@@ -1722,20 +1742,20 @@ names a _document_; a URL names a _page_. So a consumer deriving a page address
1722
1742
  from a manifest key drops the package **and** the system, not the package
1723
1743
  alone.
1724
1744
 
1725
- It used to come from `name.full`. That made a display name load-bearing three
1745
+ It does not come from `name.full`. That would make a display name load-bearing three
1726
1746
  ways at once — a rename silently 404'd every inbound link, two notes in one
1727
1747
  section could derive the same URL so a uniqueness gate had to run, and long names
1728
1748
  had to be shortened through a table of 200 abbreviations. The header of the
1729
1749
  module doing it justified the cost by promising redirects "every change appends
1730
1750
  to the legacy-URL map" — and no such map was ever written, here or in any
1731
- consumer. All of it is gone (#181).
1751
+ consumer. All of it is gone.
1732
1752
 
1733
1753
  The `type-` half earns its place: it keeps every content address clear of the
1734
1754
  package's fixed mounts (`/<package>/` for the landing page, `/<package>/api/` for
1735
1755
  generated API docs), neither of which contains a hyphen or names a type. So the
1736
1756
  namespace is provably disjoint rather than conventionally so.
1737
1757
 
1738
- **A page is written flat, named by its address** (#204). It used to be filed
1758
+ **A page is written flat, named by its address**, not filed
1739
1759
  into `<section>/`, because Hugo derives a page's section from where the file is
1740
1760
  written and a section gave it a landing page, `.CurrentSection` and a per-section
1741
1761
  layout lookup. But a section appears in no address, so the note format was
@@ -1745,7 +1765,7 @@ order to satisfy a rendering engine's directory semantics. The file is now
1745
1765
  the package root, one level above.
1746
1766
 
1747
1767
  **A page states its address without the package base; everything pointing _at_
1748
- it composes one** (#217). They read as one quantity and are two:
1768
+ it composes one**. They read as one quantity and are two:
1749
1769
 
1750
1770
  | Written | Form | Because |
1751
1771
  | ------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------- |
@@ -1754,7 +1774,7 @@ it composes one** (#217). They read as one quantity and are two:
1754
1774
  | A link-manifest `path` | `<type>-<shortcode>/` | Measured against `site.base` and stripped; a consumer prefixes its own |
1755
1775
 
1756
1776
  `site.base` is the second and third of those and reaches the first not at all.
1757
- It used to be written into the `url:` as well, so every consumer's Hugo prefixed
1777
+ Writing it into the `url:` as well makes every consumer's Hugo prefix
1758
1778
  its own base to a value that already carried one and published every content
1759
1779
  page a segment too deep — `/sohl/sohl/doc-rulesintro/`, 404 at the address the
1760
1780
  manifest, the sitemap and every inbound link named.
@@ -1763,7 +1783,7 @@ manifest, the sitemap and every inbound link named.
1763
1783
  addressed the section itself; that is retired with the section. A page that
1764
1784
  introduces the notes of a type is an ordinary note addressed `doc-<type>`, with
1765
1785
  no build path of its own — exactly as the package's own front page is
1766
- `homepage-root` (#182).
1786
+ `homepage-root`.
1767
1787
 
1768
1788
  **Sections stay, as configuration.** `site.sections` still writes an `_index.md`
1769
1789
  per section, and that is now the _only_ thing that makes one exist — see
@@ -1794,13 +1814,12 @@ publish:
1794
1814
  never recorded here.
1795
1815
 
1796
1816
  `prefix` is the whole scheme. The `collection` subtype and the top-level
1797
- `section:` key are refused by name (#202).
1817
+ `section:` key are refused by name.
1798
1818
 
1799
1819
  A note's `subType` is checked against the values its type declares, and only
1800
1820
  those. It briefly had a second reading — a `README` landing's `subType` was the
1801
- _address_ it landed at, so the closed genre list could not answer for it (#197,
1802
- #198, #200, #201) and #204 removed the cause rather than widening the
1803
- vocabulary again.
1821
+ _address_ it landed at, so the closed genre list could not answer for it. The
1822
+ cause is removed rather than the vocabulary widened again.
1804
1823
 
1805
1824
  The only note the scheme yields no address for is one carrying no `shortcode`.
1806
1825
  It is **reported and omitted**, never guessed: the command prints one located
@@ -2046,7 +2065,7 @@ That is the whole envelope. A homepage **compiles into no compendium
2046
2065
  document**, and so appears in no pack and in no link manifest — which is why it
2047
2066
  still refuses `id`. Everything else about its address is ordinary: it declares a
2048
2067
  `shortcode`, publishes at `/<contentPackage>/homepage-root/`, and is cited as
2049
- `[[homepage-root|Text]]` (#182 — see
2068
+ `[[homepage-root|Text]]` (see
2050
2069
  [The homepage is addressed like every other note](#the-homepage-is-addressed-like-every-other-note)).
2051
2070
  `/<contentPackage>/` itself is a redirect the package authors, not a page this
2052
2071
  build writes. It is dispatched on `type` like every other note, not on a
@@ -2068,8 +2087,8 @@ note-format knowledge against game-system knowledge, and a homepage carries no
2068
2087
  | `content` | The homepage plus every page the content tree compiles to, and its extra trees. |
2069
2088
 
2070
2089
  There is no value meaning "no web presence": every package publishes its
2071
- homepage. It was a boolean until 5.0.0, and both spellings are now refused
2072
- naming the mode to write instead — see [MIGRATING.md](MIGRATING.md).
2090
+ homepage. A boolean is refused, with a message naming the mode to write
2091
+ instead.
2073
2092
 
2074
2093
  **Homepage-only is a first-class mode, not an accommodation.**
2075
2094
  `sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
@@ -2094,7 +2113,7 @@ site root, one level above the content mount, which is where
2094
2113
  `publish.address.prefix` puts everything else — under the name its address gives
2095
2114
  it, `homepage-root.md`. As with every other page, the front matter's `url`
2096
2115
  decides where it publishes, and states it relative to the site root — `site.base`
2097
- does not reach it (#217).
2116
+ does not reach it.
2098
2117
 
2099
2118
  **What it does not do is decide addresses.** Those come from `publish.address`,
2100
2119
  the same setting the content index reads, so a page and its index record cannot
@@ -2140,7 +2159,7 @@ site:
2140
2159
 
2141
2160
  ### What a section may declare
2142
2161
 
2143
- **`sections` is what a section _is_ now** (#204). A content page is addressed
2162
+ **`sections` is what a section _is_ now**. A content page is addressed
2144
2163
  `(type, shortcode)` and written flat under the mount, so no page creates a
2145
2164
  directory and nothing else makes `/<package>/<prefix><section>/` answer at all.
2146
2165
  A site that wants that address says so here, and this build writes the
@@ -2209,7 +2228,7 @@ Three things about the spelling, each of them load-bearing:
2209
2228
  site chose; a type and a subType are addresses. They need not agree, and on
2210
2229
  `sohl` they do not: the section is `user-guide`, because that is a published
2211
2230
  URL, while the subType is `userguide` because an address segment is
2212
- alphanumeric (#207). Both values are checked against that charset here, so
2231
+ alphanumeric. Both values are checked against that charset here, so
2213
2232
  copying the section's name in is refused rather than quietly matching nothing.
2214
2233
  - **`listSubType` needs a `listType`.** A subType only tells pages apart within
2215
2234
  a type — `rules`, `userguide` and `reference` are all `doc` — so alone it
@@ -2263,8 +2282,8 @@ to a code span.
2263
2282
  Leaving it unset is the legitimate empty case — every `{@link}` degrades, and
2264
2283
  nothing is reported. Setting it to a path that cannot be read, cannot be parsed,
2265
2284
  or does not hold a name → page object **fails the build**, naming the file and
2266
- the reason: those were all indistinguishable from "no symbols" until #75, so a
2267
- site could publish 224 dead `{@link}` tags at exit 0. A map that is read reports
2285
+ the reason. Left unreported they are indistinguishable from "no symbols", and
2286
+ a site publishes dead `{@link}` tags at exit 0. A map that is read reports
2268
2287
  its symbol count at info level, which is the only way to tell a map that loaded
2269
2288
  from one that loaded empty without reading the emitted HTML.
2270
2289
 
@@ -2442,7 +2461,7 @@ affiliation standings. Each has its own entry point —
2442
2461
  `.../engine/region-events`, `.../sohl/affiliation-standings` — so a client bundle
2443
2462
  reaches the constant without importing a barrel that grows to hold compilers
2444
2463
  reading the filesystem. Keeping one copy of each is the point: the build-time and
2445
- runtime values cannot disagree, which is the drift that produced #932.
2464
+ runtime values cannot disagree.
2446
2465
 
2447
2466
  `@heroiclands/package-build/content-config` exposes the configuration contract's own
2448
2467
  module, so a consumer can name its types (`ContentBuildConfig`, `PackSpec`) from
@@ -2479,7 +2498,7 @@ was the system repository, because the package was vendored inside it (#1).
2479
2498
  shipping side. This package spent its first six changes as a workspace inside
2480
2499
  the Song of Heroic Lands repository, where npm hoisted the root's
2481
2500
  `devDependencies` into the workspace root: an import this package never declared
2482
- still resolved, and failed nowhere but a consumer's install (#1557). The test
2501
+ still resolved, and failed nowhere but a consumer's install. The test
2483
2502
  walks every module named by the `files` field and holds each bare specifier to
2484
2503
  one of three cases — a Node builtin, this package addressing itself, or a
2485
2504
  declared `dependency` — and checks the converse: nothing shipped may import a
@@ -2497,7 +2516,7 @@ the copy before the rest run. A module that hoisted a configured value to import
2497
2516
  time fails there, and only there.
2498
2517
 
2499
2518
  `tests/config-from-working-directory.test.ts` describes the resolution order
2500
- itself (#364). It builds the shape no unit test can fake — a repository with the
2519
+ itself. It builds the shape no unit test can fake — a repository with the
2501
2520
  toolchain installed under it, and a second checkout nested inside that
2502
2521
  repository with its own configuration and no `node_modules` — and asserts which
2503
2522
  configuration a build run in each place reads. The nested case is the one that
@@ -2522,7 +2541,7 @@ bump and the rewritten `CHANGELOG.md`. That pull request _is_ the pending
2522
2541
  release: as long as something is merged but unpublished, there is an open pull
2523
2542
  request saying so. This is the whole point of the pipeline — the previous,
2524
2543
  hand-driven process failed by leaving _nothing_ behind when the final step was
2525
- forgotten, and on 2026-08-21 it did exactly that for two versions (#15).
2544
+ forgotten, and on 2026-08-21 it did exactly that for two versions.
2526
2545
 
2527
2546
  **Merging that publishes.** `changeset publish` puts the version on npm through
2528
2547
  Trusted Publishing (OIDC — there is no `NPM_TOKEN`), tags the commit `v<version>`
package/README.md CHANGED
@@ -14,13 +14,6 @@ The content half is documented separately in **[CONTENT.md](CONTENT.md)** — th
14
14
  note format, the pack pipeline, and the configuration contract a content tree
15
15
  declares itself with.
16
16
 
17
- > **This package was two.** Until 3.0.0 the content half shipped as
18
- > `@heroiclands/content-build`. No consumer ever installed one without the
19
- > other, and the packaging half depended on the content half besides, so the
20
- > boundary bought nothing and cost a configuration file with two owners and a
21
- > two-repository dance for single changes. See
22
- > [MIGRATING.md](MIGRATING.md) to move a consumer from 1.x.
23
-
24
17
  ## Install
25
18
 
26
19
  ```
@@ -234,7 +227,7 @@ packageBuild:
234
227
  ### The manifest is generated, not stamped
235
228
 
236
229
  `package-build manifest` writes `system.json` / `module.json` into the stage.
237
- **There is no template file.** A manifest used to be hand-authored JSON that the
230
+ **There is no template file.** A hand-authored JSON manifest is one the
238
231
  build stamped a few fields into — the one build input still written by hand, per
239
232
  repository, with no schema and nothing checking it. It also declared facts the
240
233
  configuration already declared: the pack list twice, in two formats, with
@@ -495,7 +488,7 @@ own to call a run green. Every way of stopping a runner before it starts — a
495
488
  corrupt install, a missing browser, a killed process, an `npm ci` racing the
496
489
  run and taking `node_modules` with it — produces a run that executed nothing,
497
490
  and a harness that reports that as 0 makes the evidence unfalsifiable in the
498
- one direction that matters (#153).
491
+ one direction that matters.
499
492
 
500
493
  So the suite is bracketed rather than trusted:
501
494