@heroiclands/package-build 17.1.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/CHANGELOG.md +1773 -0
  2. package/CONTENT.md +288 -72
  3. package/README.md +1 -1
  4. package/bin/content-build.mjs +218 -137
  5. package/bin/package-build.mjs +43 -0
  6. package/content-config.mjs +62 -67
  7. package/docs/content-format.md +768 -58
  8. package/engine/actor-compiler.mjs +586 -0
  9. package/engine/address-charset.mjs +11 -8
  10. package/engine/address-diff.mjs +266 -22
  11. package/engine/anchored-sections.mjs +83 -0
  12. package/engine/anchors.mjs +83 -0
  13. package/engine/base-compiler.mjs +148 -18
  14. package/engine/bundle-notes.mjs +276 -0
  15. package/engine/bundles.mjs +307 -0
  16. package/engine/code-fences.mjs +103 -0
  17. package/engine/compile-corpus.mjs +89 -0
  18. package/engine/content-address.mjs +267 -5
  19. package/engine/content-format-check.mjs +54 -11
  20. package/engine/content-format.mjs +37 -5
  21. package/engine/content-index.mjs +394 -111
  22. package/engine/content-links.mjs +185 -77
  23. package/engine/content-lint.mjs +232 -14
  24. package/engine/content-package.mjs +2 -1
  25. package/engine/content-tables.mjs +99 -3
  26. package/engine/document-subtypes.mjs +49 -6
  27. package/engine/field-reference.mjs +1 -1
  28. package/engine/field-spec.mjs +56 -6
  29. package/engine/folder-notes.mjs +470 -0
  30. package/engine/foreign-catalog.mjs +195 -4
  31. package/engine/{manifest-emit.mjs → foundry-entries.mjs} +78 -81
  32. package/engine/frontmatter-lint.mjs +245 -19
  33. package/engine/frontmatter.mjs +88 -8
  34. package/engine/generate.mjs +304 -13
  35. package/engine/helpers.mjs +293 -64
  36. package/engine/ids.mjs +137 -2
  37. package/engine/index-records.mjs +126 -0
  38. package/engine/index.mjs +23 -5
  39. package/engine/item-compiler.mjs +349 -0
  40. package/engine/item-docs.mjs +7 -1
  41. package/engine/item-registry.mjs +6 -0
  42. package/engine/journals.mjs +93 -27
  43. package/engine/macros.mjs +4 -2
  44. package/engine/metadata-index.mjs +495 -0
  45. package/engine/note-claims.mjs +146 -25
  46. package/engine/note-ids.mjs +112 -0
  47. package/engine/note-renames.mjs +134 -0
  48. package/engine/note-schemas.mjs +25 -0
  49. package/engine/note-vocabulary.mjs +110 -27
  50. package/engine/pack-config.mjs +4 -0
  51. package/engine/pack-router.mjs +23 -0
  52. package/engine/retired-fields.mjs +137 -3
  53. package/engine/scenes.mjs +66 -34
  54. package/engine/schema-check.mjs +58 -17
  55. package/engine/site-build.mjs +71 -12
  56. package/engine/site-index.mjs +20 -7
  57. package/engine/sql-tables.mjs +485 -0
  58. package/engine/subtype-registry.mjs +102 -0
  59. package/engine/system-block.mjs +96 -12
  60. package/engine/systems.mjs +201 -0
  61. package/engine/web-wikilinks.mjs +50 -20
  62. package/engine/wikilink-syntax.mjs +17 -8
  63. package/engine/wikilinks.mjs +124 -51
  64. package/engine/yaml-lint.mjs +282 -0
  65. package/hm3/actors.mjs +352 -0
  66. package/hm3/default-item-art.mjs +75 -0
  67. package/hm3/document-subtypes.mjs +134 -0
  68. package/hm3/index.mjs +56 -0
  69. package/hm3/item-builders.mjs +84 -0
  70. package/hm3/item-fields.mjs +180 -0
  71. package/hm3/items.mjs +96 -0
  72. package/hm3/template-priority.mjs +85 -0
  73. package/labels.mjs +191 -0
  74. package/manifest.mjs +43 -2
  75. package/package.json +19 -4
  76. package/release.mjs +62 -7
  77. package/sohl/actors.mjs +33 -487
  78. package/sohl/being-info.mjs +16 -7
  79. package/sohl/default-item-art.mjs +14 -3
  80. package/sohl/document-subtypes.mjs +16 -10
  81. package/sohl/item-builders.mjs +14 -5
  82. package/sohl/item-fields.mjs +68 -7
  83. package/sohl/items.mjs +44 -258
  84. package/sohl/note-schemas.mjs +2 -2
  85. package/types/content-config.d.mts +21 -42
  86. package/types/engine/actor-compiler.d.mts +204 -0
  87. package/types/engine/address-charset.d.mts +11 -8
  88. package/types/engine/address-diff.d.mts +53 -5
  89. package/types/engine/anchored-sections.d.mts +21 -0
  90. package/types/engine/anchors.d.mts +20 -0
  91. package/types/engine/base-compiler.d.mts +17 -17
  92. package/types/engine/bundle-notes.d.mts +173 -0
  93. package/types/engine/bundles.d.mts +60 -0
  94. package/types/engine/code-fences.d.mts +43 -0
  95. package/types/engine/compile-corpus.d.mts +32 -0
  96. package/types/engine/content-address.d.mts +205 -5
  97. package/types/engine/content-format-check.d.mts +6 -2
  98. package/types/engine/content-format.d.mts +57 -1
  99. package/types/engine/content-index.d.mts +64 -80
  100. package/types/engine/content-links.d.mts +52 -7
  101. package/types/engine/content-lint.d.mts +10 -1
  102. package/types/engine/content-package.d.mts +2 -1
  103. package/types/engine/content-tables.d.mts +21 -39
  104. package/types/engine/document-subtypes.d.mts +37 -3
  105. package/types/engine/field-spec.d.mts +76 -5
  106. package/types/engine/folder-notes.d.mts +159 -0
  107. package/types/engine/foreign-catalog.d.mts +53 -0
  108. package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +16 -38
  109. package/types/engine/frontmatter-lint.d.mts +10 -2
  110. package/types/engine/frontmatter.d.mts +64 -0
  111. package/types/engine/generate.d.mts +38 -0
  112. package/types/engine/helpers.d.mts +94 -30
  113. package/types/engine/ids.d.mts +96 -0
  114. package/types/engine/index-records.d.mts +68 -0
  115. package/types/engine/index.d.mts +9 -3
  116. package/types/engine/item-compiler.d.mts +131 -0
  117. package/types/engine/journals.d.mts +47 -9
  118. package/types/engine/metadata-index.d.mts +226 -0
  119. package/types/engine/note-claims.d.mts +57 -11
  120. package/types/engine/note-ids.d.mts +38 -0
  121. package/types/engine/note-renames.d.mts +102 -0
  122. package/types/engine/note-vocabulary.d.mts +44 -8
  123. package/types/engine/retired-fields.d.mts +75 -0
  124. package/types/engine/scenes.d.mts +3 -2
  125. package/types/engine/schema-check.d.mts +25 -4
  126. package/types/engine/site-build.d.mts +4 -4
  127. package/types/engine/site-index.d.mts +1 -1
  128. package/types/engine/sql-tables.d.mts +185 -0
  129. package/types/engine/subtype-registry.d.mts +49 -0
  130. package/types/engine/system-block.d.mts +40 -1
  131. package/types/engine/systems.d.mts +106 -0
  132. package/types/engine/web-wikilinks.d.mts +4 -2
  133. package/types/engine/wikilink-syntax.d.mts +10 -3
  134. package/types/engine/wikilinks.d.mts +41 -13
  135. package/types/engine/yaml-lint.d.mts +107 -0
  136. package/types/hm3/actors.d.mts +48 -0
  137. package/types/hm3/default-item-art.d.mts +42 -0
  138. package/types/hm3/document-subtypes.d.mts +24 -0
  139. package/types/hm3/index.d.mts +7 -0
  140. package/types/hm3/item-builders.d.mts +11 -0
  141. package/types/hm3/item-fields.d.mts +12 -0
  142. package/types/hm3/items.d.mts +23 -0
  143. package/types/hm3/template-priority.d.mts +21 -0
  144. package/types/manifest.d.mts +22 -1
  145. package/types/release.d.mts +9 -4
  146. package/types/sohl/actors.d.mts +7 -74
  147. package/types/sohl/being-info.d.mts +8 -27
  148. package/types/sohl/default-item-art.d.mts +5 -3
  149. package/types/sohl/items.d.mts +17 -32
  150. package/engine/foreign-manifests.mjs +0 -126
  151. package/engine/kb-manifest.mjs +0 -490
  152. package/types/engine/foreign-manifests.d.mts +0 -43
  153. package/types/engine/kb-manifest.d.mts +0 -241
package/CONTENT.md CHANGED
@@ -22,7 +22,7 @@ A consuming repository declares one `package-build.config.yaml` at its root:
22
22
 
23
23
  ```yaml
24
24
  # The package this repository's content is published as: the first segment of
25
- # every canonical address, the name of the link manifest it emits, and the
25
+ # every canonical address, the name of the content index it emits, and the
26
26
  # package a cross-package wikilink writes to reach one of its notes.
27
27
  contentPackage: thalorna
28
28
  # Where Foundry installs it: "systems" or "modules". Also decides the served
@@ -186,11 +186,27 @@ something migrates on it.
186
186
  ### A note's package is the repository's, not the note's
187
187
 
188
188
  `contentPackage` is the **address namespace** every note in the tree is
189
- published under: the first segment of every canonical key (`sohl-skill-clmb`),
190
- the name of the link manifest this build emits (`sohl.json`), and the package a
191
- cross-package wikilink writes to reach one of these notes. It is the
192
- repository's identity in the address space — not a filter — and a note does not
193
- restate it.
189
+ published under: the first segment of every canonical key, the name of the link
190
+ manifest this build emits (`sohl.json`), and the package a cross-package
191
+ wikilink writes to reach one of these notes. It is the repository's identity in
192
+ the address space — not a filter — and a note does not restate it.
193
+
194
+ A canonical key has four segments, read by position:
195
+
196
+ ```text
197
+ <package>-<system>-<type>-<shortcode>
198
+ ```
199
+
200
+ `<system>` is a game system this toolchain compiles for — `sohl` or `hm3` — or
201
+ the literal `none` for a document no game system defines: a JournalEntry, a
202
+ Macro, a Scene, and an item's documentation journal, which is `none` however
203
+ many system blocks the item itself carries. The registry of permitted values is
204
+ `engine/systems.mjs`, and `none` is a **word** on purpose: `any` would read as a
205
+ wildcard, which is the opposite of what it says, and a YAML null (`null`, `~`,
206
+ an empty value) parses to an absent value and drops the segment altogether. So
207
+ `sohl-none-doc-gear`, `sohl-sohl-skill-clmb` — the package and the system are
208
+ independent slots that may hold the same word, because Foundry requires a system
209
+ package's id to _be_ its system id.
194
210
 
195
211
  Because it is a segment of an address, the value is **validated** rather than
196
212
  taken as written, and a violation fails the build naming the line it is on:
@@ -199,14 +215,20 @@ taken as written, and a violation fails the build naming the line it is on:
199
215
  hyphen-separated segments, so the hyphen has to be purely a separator — which
200
216
  is why `harn-adventures` is configured as `harnadventures`. This is the same
201
217
  rule `shortcode` is already held to, and the two are one constant.
202
- - **Not a note type.** The package and the type are adjacent segments, and the
203
- two vocabularies are kept disjoint so a reader never has to decide which slot
204
- a name is filling. `doc`, `being`, every map type, and every item type this
218
+ - **Not a note type.** A written address is a _partial_ one the shorter forms
219
+ drop segments from the left, so `skill-clmb` and `sohl-skill-clmb` are both
220
+ addresses and position alone therefore no longer says which vocabulary a
221
+ leading segment is drawn from. The reader settles that by asking whether the
222
+ name is a known package, so a name belonging to both vocabularies makes one
223
+ target readable two ways with no defensible pick. The package and the type are
224
+ no longer _adjacent_ segments now that the system sits between them, and that
225
+ changes nothing: the hazard was never adjacency, it is that a short form omits
226
+ the slots in between. `doc`, `being`, every map type, and every item type this
205
227
  repository declares — with its `doc`-prefixed documentation form — are
206
228
  refused.
207
229
 
208
230
  ```text
209
- package-build.config.yaml:1:1: error: package-build config: `contentPackage` is `harn-adventures`, which is not alphanumeric. It is the first segment of every address this package publishes (`harn-adventures-<type>-<shortcode>`), and an address is read by counting hyphen-separated segments — so anything outside `[A-Za-z0-9]` here makes those addresses unreadable rather than merely ugly. `harn-adventures` became `harnadventures`.
231
+ package-build.config.yaml:1:1: error: package-build config: `contentPackage` is `harn-adventures`, which is not alphanumeric. It is the first segment of every address this package publishes (`harn-adventures-<system>-<type>-<shortcode>`), and an address is read by counting hyphen-separated segments — so anything outside `[A-Za-z0-9]` here makes those addresses unreadable rather than merely ugly. `harn-adventures` became `harnadventures`.
210
232
  ```
211
233
 
212
234
  **`package:` in a note's frontmatter is retired, and declaring it fails the
@@ -220,7 +242,7 @@ assets/content/Gear/Axe.md:12:1: error: `package: sohl` is a retired frontmatter
220
242
  ```
221
243
 
222
244
  `content-build lint` reports every such note in one pass; `content-build
223
- package compile` and `content-build manifest` refuse the tree.
245
+ package compile` and `content-build content-index` refuse the tree.
224
246
 
225
247
  A generated table that scopes itself with `WHERE … and package = "<pkg>"` keeps
226
248
  working: the package is **synthesised** into what the table search sees,
@@ -400,6 +422,41 @@ system)` take the system that is asking; asking without one, for a type more
400
422
  than one registry declares, **throws** rather than answering with whichever was
401
423
  declared first.
402
424
 
425
+ **`hm3` is a real registry, and the rest of the pipeline follows the same
426
+ field.** Since #139 this package ships two system halves, `sohl/` and `hm3/`,
427
+ each with its own builders, its own default art and its own note-type →
428
+ document-subtype map; they share the engine between them and import nothing from
429
+ each other. A pack's `system:` is what selects among them — the Item and Actor
430
+ compilers, the item catalogue a being resolves against, the published
431
+ `schema.json` its emissions are checked against, and the `_stats` stamp all read
432
+ that one field — so declaring one Item pack and one Actor pack per system is the
433
+ whole of the configuration a dual-system tree needs:
434
+
435
+ ```yaml
436
+ itemBuilders: [sohl, hm3]
437
+ systems:
438
+ sohl: { compatibility: { verified: "0.9.0" } }
439
+ hm3: { compatibility: { verified: "1.6.3" } }
440
+ packs:
441
+ - { name: items-sohl, type: Item, system: sohl, default: true }
442
+ - { name: items-hm3, type: Item, system: hm3 }
443
+ - { name: actors-sohl, type: Actor, system: sohl, default: true }
444
+ - { name: actors-hm3, type: Actor, system: hm3 }
445
+ ```
446
+
447
+ A note carrying both a `sohl:` and an `hm3:` block then compiles **one document
448
+ in each system**, each shaped by its own builders and stamped with its own
449
+ system version. A note carrying only one block compiles only that system's
450
+ document: the other system's pass passes over it, rather than failing it for a
451
+ block it was never going to have.
452
+
453
+ Four of HM3's rows are **one-to-many** — `mysticalability` becomes a `psionic`,
454
+ a `spell` or an `invocation`; `trauma` an `injury` or a `trait`; `weapongear` a
455
+ `weapongear` or a `missilegear`; `being` a `character` or a `creature` — and the
456
+ note says which by writing `hm3.type`. Nothing is inferred from the note's own
457
+ `subType`, and a note that says nothing is an error naming the note and listing
458
+ the permitted values.
459
+
403
460
  **Configuration is the source, and the manifest is generated from it.** That
404
461
  arrow used to point the other way: `paths.packageManifest` said where a
405
462
  hand-authored `system.template.json` lived, and the package-id guard and the
@@ -584,16 +641,24 @@ toolchain vocabulary, and the _generators_ `items` and `attributes` expand into
584
641
  embedded documents rather than mapping anywhere, so neither has a `system` path
585
642
  to be written at.
586
643
 
587
- `archetype` is authored there too, and is the one such key that _is_ a field:
588
- the builder writes it to `system.archetype` a number is an archetype at that
589
- priority, `null` is not an archetype — exactly as `portrait` reaches
590
- `sohl.system.portrait` from a shared top-level property. Its authored position
591
- is unchanged (#126).
644
+ `templatePriority` is the one key of this shape that _is_ a field, and it is
645
+ **shared rather than per-system**: its home is `data.templatePriority`, and it
646
+ reaches `sohl.system.templatePriority` and `hm3.flags.hm3.templatePriority`
647
+ exactly as `portrait` reaches two differently-named fields from one shared
648
+ property. A number is a template at that priority, `null` is not a template, and
649
+ absent is an authoring error (#126, #266). The legacy in-block and top-level
650
+ positions are still read, in that order after `data:`, so a tree sweeps on its
651
+ own schedule; `archetype` is the retiring spelling of the same field, still read
652
+ last but **refused by the frontmatter linter** — a priority and the `archetypes`
653
+ a being fits are different things, and one letter is not enough to tell them
654
+ apart.
592
655
 
593
656
  ```yaml
594
657
  type: being # the content type — system-agnostic
595
658
  pack: actors # shared: unless a block says otherwise
596
659
  portrait: kaldor.webp # shared: reaches both systems' fields, differently named
660
+ data:
661
+ templatePriority: 1 # shared: → sohl `system.`, hm3 `flags.hm3.`
597
662
  hm3:
598
663
  type: character # this system's document subtype
599
664
  pack: actors-hm3 # overrides the shared one, for HM3 only
@@ -605,7 +670,6 @@ sohl:
605
670
  type: being
606
671
  system:
607
672
  currentMoveMedium: walk
608
- archetype: 1
609
673
  ```
610
674
 
611
675
  **The shared fallback is declared, not name-matched.** `sohl.system.portrait`
@@ -676,7 +740,7 @@ value goes through one coercion rather than two.
676
740
 
677
741
  **What the compiler writes on its own is checked too.** A compiled document
678
742
  carries keys no field declaration and no note ever names — `shortcode`,
679
- `archetype`, `actionDefs`, `notes`, `docHtml` — because the pass writes them
743
+ `templatePriority`, `actionDefs`, `notes`, `docHtml` — because the pass writes them
680
744
  itself, and they were compared against nothing: the declaration-derived check
681
745
  reads `itemBuilders`, the note-side check reads `<system>.system`, and these are
682
746
  in neither. A compile now reads the `system` block each pass **assembled** and
@@ -715,7 +779,6 @@ npx content-build content-format notes [root] [--strict]
715
779
  npx content-build links [root] [--manifests <dir>]
716
780
  npx content-build format [paths..] [--write]
717
781
  npx content-build markdown [paths..] [--fix]
718
- npx content-build manifest [root] [--out <dir>]
719
782
  npx content-build content-index [root] [--out <dir>]
720
783
  npx content-build site [--out <dir>]
721
784
  npx content-build reachability <dir> [file] [--index <shortcode>]
@@ -731,7 +794,6 @@ npx content-build addresses diff --from <zip|dir> [--strict]
731
794
  | `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. |
732
795
  | `format` | Prettier, with the shared configuration. See [Prose: formatting and markdown](#prose-formatting-and-markdown). |
733
796
  | `markdown` | markdownlint, with the shared rule set — the structure Prettier is indifferent to. |
734
- | `manifest` | Emit this package's cross-package link manifest. See [Publishing a link manifest](#publishing-a-link-manifest). |
735
797
  | `content-index` | Emit this package's note index as JSON Lines. See [Publishing a content index](#publishing-a-content-index). |
736
798
  | `site` | Publish the content tree as a website. See [Publishing a website](#publishing-a-website). |
737
799
  | `reachability` | Walk outward from an index note and report what no path reaches, for a tree meant to be navigable from one entry point. |
@@ -894,10 +956,11 @@ mode rather than by kind — so `subType` on one is a finding; a `skill` declare
894
956
  ten, so `subType: crafte` is a finding naming `craft`.
895
957
 
896
958
  **A `type` and a `subType` are both held to `^[A-Za-z0-9]+$`** (#206) — the same
897
- constant a `shortcode` is held to, read rather than restated. A type is the
898
- first segment of every address, so a hyphen in one is read back as a segment
899
- boundary nobody wrote. A `subType` reaches no address since #204 retired
900
- sections, and keeps the rule anyway: it is a vocabulary term the toolchain keys
959
+ constant a `shortcode` is held to, read rather than restated. A type is a
960
+ segment of every address the first of the short form an author writes, the
961
+ third of the canonical `package-system-type-shortcode` so a hyphen in one is
962
+ read back as a segment boundary nobody wrote. A `subType` reaches no address
963
+ since #204 retired sections, and keeps the rule anyway: it is a vocabulary term the toolchain keys
901
964
  on, and one charset that holds for every term is a rule an author can state. The
902
965
  rule is checked ahead of the closed-set check, which is what makes it reach a
903
966
  type whose values are declared but not yet enumerated:
@@ -935,6 +998,43 @@ folded in every note's `name.full` and so decided what a note could be named
935
998
  `[[type-shortcode|Text]]`, and declaring `aliases:` is refused naming the file
936
999
  and the line.
937
1000
 
1001
+ ### Writing a link: the address grammar
1002
+
1003
+ Every wikilink is an address, and omission runs **strictly left to right**:
1004
+
1005
+ ```text
1006
+ [[[[<package>-]<system>-]<type>-]<shortcode>]
1007
+ ```
1008
+
1009
+ So the written forms are exactly the suffixes of the canonical address:
1010
+
1011
+ | Form | Example | Means |
1012
+ | ------------------------------- | ------------------------------------ | --------------------------------- |
1013
+ | `type-shortcode` | `[[skill-clmb\|Climbing]]` | This package, any system. |
1014
+ | `system-type-shortcode` | `[[sohl-skill-clmb\|Climbing]]` | This package, the `sohl` system. |
1015
+ | `package-system-type-shortcode` | `[[thalorna-sohl-being-grod\|Grod]]` | Another package, fully qualified. |
1016
+
1017
+ **There is no `package-type-shortcode`.** Naming a package means naming the
1018
+ system before the type, because the segments are positional rather than tagged.
1019
+ A link into another package must therefore be fully qualified — which is the
1020
+ price of a grammar that needs no vocabulary to parse.
1021
+
1022
+ **An omitted system is a wildcard; an omitted package is a default.** Most links
1023
+ target items, which belong to a system, so a target naming none matches a note
1024
+ under any of them and exactly one hit is required — two claimants is an
1025
+ _ambiguity_, a different finding with a different fix from resolving nowhere. A
1026
+ target naming no package means the citing note's own, so an unqualified link
1027
+ resolves locally and only locally.
1028
+
1029
+ **`sohl` is both a package and a system**, and positional counting is what makes
1030
+ that harmless: three segments name a _system_ whatever the first segment could
1031
+ also have meant, and four is the full form.
1032
+
1033
+ **Parsing is plain segment counting**, the same rule the canonical key follows,
1034
+ and it is sound because every segment is `^[A-Za-z0-9]+$` — so a hyphen is
1035
+ purely a separator. A target with five segments is not a hyphenated shortcode;
1036
+ it is a name, and not an address.
1037
+
938
1038
  **`name.aliases` is kept, and is read by nothing.** It fed the same index and
939
1039
  lost the same reader, but unlike the top-level list it is **reserved** — held
940
1040
  for a use that does not exist yet. So it is the one field in the format that is
@@ -988,10 +1088,10 @@ the Foundry document id a compendium UUID is built from, and a homepage compiles
988
1088
  into **no document**.
989
1089
 
990
1090
  ```text
991
- assets/content/homepage.md:4:1: error: `id` decides nothing on a `type: homepage` note: it is the Foundry document id a compendium UUID is built from, and a homepage compiles into no document — it appears in no pack and in no link manifest. Delete it
1091
+ assets/content/homepage.md:4:1: error: `id` decides nothing on a `type: homepage` note: it is the Foundry document id a compendium UUID is built from, and a homepage compiles into no document — it appears in no pack and states no Foundry address. Delete it
992
1092
  ```
993
1093
 
994
- That is also why a homepage stays **out of the link manifest**, now that a
1094
+ That is also why a homepage states **no Foundry address**, now that a
995
1095
  shortcode alone would put it in. A manifest entry is how another package
996
1096
  resolves a _document_; a cross-package link to a package's front page is its
997
1097
  bare `/<package>/` address, which needs no index.
@@ -1128,7 +1228,7 @@ attempted:
1128
1228
  documentation, hand-authored Hugo sections — so this build does not hold the
1129
1229
  set of published pages and would report a working link as dead. A bare
1130
1230
  `https://www.heroiclands.org/<package>/` is left alone for the same reason it
1131
- cannot be improved: a package homepage is in no link manifest, so there is no
1231
+ cannot be improved: a package homepage compiles into no document, so there is no
1132
1232
  better form to write.
1133
1233
 
1134
1234
  ## The content format specification
@@ -1260,44 +1360,145 @@ import shared from "@heroiclands/package-build/markdownlint";
1260
1360
  export default { ...shared, config: { ...shared.config, MD013: true } };
1261
1361
  ```
1262
1362
 
1263
- ## Publishing a link manifest
1363
+ ## YAML: frontmatter, and every YAML file
1364
+
1365
+ ```bash
1366
+ npx package-build yaml # every YAML file git would consider
1367
+ npx package-build yaml assets/content # or just these paths
1368
+ ```
1369
+
1370
+ Frontmatter carries a note's type, its shortcode, its address and the system
1371
+ blocks a document is compiled from — and until this command existed nothing
1372
+ checked it _as YAML_.
1373
+
1374
+ **Worse than unchecked: a parse failure unmade the note.** `parseMarkdownFile`
1375
+ caught the error, logged it at `warn`, and returned `{frontmatter: null}` — which
1376
+ is not a note with bad frontmatter but, to every pass downstream, _a file with no
1377
+ frontmatter_. It was skipped by the compiler, the linter, the link checker and
1378
+ the index, and the build reported success. A duplicate key did not fail anything;
1379
+ it removed a note from the corpus. The parser had detected it all along.
1380
+
1381
+ **Frontmatter is linted through an ESLint processor** — the mechanism
1382
+ `eslint-plugin-markdown` uses for fenced code blocks. Frontmatter is its easy
1383
+ case: the block is always at the top of the file, so a finding maps back to the
1384
+ line it came from with a constant `+1` for the opening `---`, and nothing after
1385
+ the closing `---` is read as YAML.
1386
+
1387
+ The rule set is **deliberately narrow**, for the same reason `markdown`'s is.
1388
+ Prettier already owns YAML's whitespace, quoting and line breaks — including
1389
+ inside a frontmatter fence — so a rule about any of those would duplicate the
1390
+ formatter or fight it. What is left is the class a formatter cannot see: text
1391
+ that parses to something other than what it looks like.
1392
+
1393
+ | Reported | Why it is not a matter of taste |
1394
+ | ------------------------------------- | -------------------------------------------------------------- |
1395
+ | a parse error | A duplicate key, a tab indent, mis-aligned mapping items. |
1396
+ | `yml/no-empty-mapping-value` | `folder:` and `folder: null` are one value and two statements. |
1397
+ | `yml/no-irregular-whitespace` | A non-breaking space is invisible and part of the value. |
1398
+ | `yml/no-empty-key`, `-empty-document` | A fence or a file that parses to nothing at all. |
1399
+
1400
+ `folder:` and `folder: null` read as opposites — a decision, or a key somebody
1401
+ began and did not finish — so the distinction is drawn where the text still
1402
+ exists. **A key with a block under it is not empty**: `name:` followed by an
1403
+ indented mapping, or by a sequence at its own indent, is an ordinary container.
1404
+
1405
+ **GitHub workflows are exempt from the empty-value rule.** `on:`, `push:` and
1406
+ `workflow_dispatch:` carry their meaning by being present, and writing
1407
+ `push: null` to satisfy a linter would be worse YAML, not better. A real error in
1408
+ a workflow is still reported.
1409
+
1410
+ **The files are the ones git would consider** — `--cached --others
1411
+ --exclude-standard`, tracked plus untracked-and-not-ignored, the same set
1412
+ `gitignore: true` gives the markdown linter. Untracked is included so a note is
1413
+ linted while it is being written rather than only once it has been staged.
1414
+
1415
+ **A consumer needs no ESLint.** This ships as a command, not as a configuration
1416
+ to adopt: there is no `eslint` dependency to add, no `eslint.config.js` and no
1417
+ rules to declare. A repository that _has_ an ESLint of its own keeps it untouched
1418
+ and unconsulted — the run sets `overrideConfigFile: true`, so no config file is
1419
+ looked for at all.
1420
+
1421
+ ## Publishing the content index
1422
+
1423
+ Every package emits one file naming every note it publishes:
1264
1424
 
1265
1425
  ```bash
1266
- npx content-build manifest # the configured tree and output directory
1267
- npx content-build manifest --out tmp/ # or somewhere else
1426
+ npx content-build content-index # the configured tree and output directory
1268
1427
  ```
1269
1428
 
1270
- Writes `<contentPackage>.json` naming every note this package publishes, keyed by
1271
- the canonical `package-type-shortcode` address and valued with every address that
1272
- note has: a `path` on the web, a `uuid` in Foundry, the `anchors` its named
1273
- sections compiled to, and a `doc` pointer where an item's prose compiles into a
1274
- JournalEntry of its own. A consuming build vendors the file into its own
1275
- `paths.manifests` and resolves cross-package links through it — the counterpart
1276
- of `links`, which consumes what this emits.
1277
-
1278
- It reads its whole input from configuration and takes nothing else:
1279
-
1280
- | Setting | What it decides |
1281
- | --------------------------- | ------------------------------------------------------------- |
1282
- | `contentPackage` | The package emitted, which every note belongs to. |
1283
- | `foundryPackage` | The package every emitted `uuid` names. |
1284
- | `paths.content` | The tree walked. |
1285
- | `paths.manifestOut` | Where the file lands (`build/manifests` by default). |
1286
- | `publish.manifests.publish` | Whether this repository publishes one at all. |
1287
- | `publish.site` | Whether entries carry a `path` — see below. |
1288
- | `publish.address` | The address scheme those paths are derived under — see below. |
1429
+ `<contentPackage>-metadata.jsonl` holds one JSON record per line, keyed by the
1430
+ canonical `package-system-type-shortcode` address and carrying every address
1431
+ that note has: its `address.slug` on the web, a `foundry.<system>.uuid` in
1432
+ Foundry, the `anchors` its named sections compiled to, and a `documentation`
1433
+ pointer where an item's prose compiles into a JournalEntry of its own.
1434
+
1435
+ **This is the artifact other packages resolve your addresses through.** It is
1436
+ advertised in the emitted `system.json` / `module.json` as
1437
+ `flags.metadataUrl` derived, version-pinned, and not something you write down
1438
+ — and published as a release asset beside the manifest and the `.zip`.
1439
+
1440
+ | Setting | What it decides |
1441
+ | -------------------- | --------------------------------------------- |
1442
+ | `contentPackage` | The package emitted, and the file's name. |
1443
+ | `foundryPackage` | The package every emitted `uuid` names. |
1444
+ | `paths.content` | The tree walked. |
1445
+ | `paths.contentIndex` | Where the file lands (`build/content-index`). |
1289
1446
 
1290
1447
  **Both addresses are optional, independently.** A note that compiles into no
1291
- document has no `uuid`, and a package that ships compendiums and publishes only
1292
- a homepage (`publish.site: homepage`) has no `path` on any entry its notes are
1293
- not pages. Neither is an error, and neither is guessed: inventing the missing one
1294
- asserts a target that does not exist, which is the silent dead link the manifest
1295
- exists to prevent.
1448
+ document states no `foundry` block, and a package that ships compendiums and
1449
+ publishes only a homepage serves no page at its addresses. Neither is an error,
1450
+ and neither is guessed: inventing the missing one asserts a target that does not
1451
+ exist, which is the silent dead link this whole mechanism exists to prevent.
1452
+
1453
+ ## Resolving another package's addresses
1454
+
1455
+ Declare what you depend on, and fetch it:
1456
+
1457
+ ```bash
1458
+ npx content-build deps fetch # every declared dependency
1459
+ npx content-build deps fetch --from ../sohl # from a local build, unreleased
1460
+ ```
1296
1461
 
1297
- **`publish.manifests.publish` is a declaration, not a preference.** The file is
1298
- vendored by other repositories and read as authoritative, so emitting one is a
1299
- statement about this package. With the switch off the command fails rather than
1300
- writing.
1462
+ ```yaml
1463
+ relationships:
1464
+ systems:
1465
+ - id: sohl
1466
+ type: system
1467
+ manifest: https://github.com/HeroicLands/Song-of-Heroic-Lands-FoundryVTT/releases/latest/download/system.json
1468
+ compatibility: { minimum: "0.8.0", verified: "0.8.2" }
1469
+ ```
1470
+
1471
+ `deps fetch` reads that manifest, takes the `flags.metadataUrl` it advertises,
1472
+ and pulls the index into `build/cache/metadata`. The whole chain is declared, so
1473
+ nothing here holds an address of its own and a dependency that moves its release
1474
+ assets does not break its consumers.
1475
+
1476
+ **You may cite what you depend on, and nothing else.** The set is every entry in
1477
+ `relationships.systems` and `relationships.requires`; `recommends` and
1478
+ `conflicts` are declarations _about_ other packages rather than dependencies on
1479
+ them, so a link into one is a defect in the citing note. An address into a
1480
+ package you have not declared resolves nowhere and fails the build.
1481
+
1482
+ **A build never reaches the network.** A declared dependency whose index has not
1483
+ been fetched is an error naming `deps fetch`, rather than a download nobody
1484
+ asked for — a build that downloads silently is not reproducible and fails
1485
+ strangely offline. The cache is keyed by version, so changing the pinned version
1486
+ is a miss rather than a silent overwrite.
1487
+
1488
+ **`--from` is for two packages changing together.** It fills the cache from a
1489
+ locally built artifact — a package zip or the directory it was built from — so a
1490
+ consumer can be built against a dependency that has not shipped. Without it,
1491
+ testing a dependency change against its consumers would cost a release
1492
+ round-trip.
1493
+
1494
+ > **This replaced a vendored link manifest**, which every repository committed a
1495
+ > copy of every other repository's file into. A copy went stale silently and one
1496
+ > did — 2,101 entries in `Song-of-Heroic-Lands-FoundryVTT` pointed at URLs the
1497
+ > `thalorna` site had stopped publishing, at the current format version, so the
1498
+ > version gate saw nothing. Mutual vendoring also deadlocked: each package had to
1499
+ > read the other's file before it could publish its own. A fetched artifact
1500
+ > cannot drift from its producer, and a consumer reads one the producer has
1501
+ > already shipped.
1301
1502
 
1302
1503
  ### A page's URL is its address
1303
1504
 
@@ -1310,6 +1511,14 @@ writing.
1310
1511
  is no collision check behind it, there never can be one to fail, and renaming a
1311
1512
  note changes nothing: no part of the address comes from a display string.
1312
1513
 
1514
+ **A URL carries no `<system>` segment**, though the canonical address does. That
1515
+ is deliberate rather than an omission: a note publishes one page however many
1516
+ systems' documents it compiles into, so the segment would have nothing to
1517
+ distinguish and would only split one page's URL in two. The canonical address
1518
+ names a _document_; a URL names a _page_. So a consumer deriving a page address
1519
+ from a manifest key drops the package **and** the system, not the package
1520
+ alone.
1521
+
1313
1522
  It used to come from `name.full`. That made a display name load-bearing three
1314
1523
  ways at once — a rename silently 404'd every inbound link, two notes in one
1315
1524
  section could derive the same URL so a uniqueness gate had to run, and long names
@@ -1462,7 +1671,7 @@ A record states the address a wikilink writes to reach the note, and every
1462
1671
 
1463
1672
  ```json
1464
1673
  {
1465
- "address": { "slug": "being-aurochs", "canonical": "sohl-being-aurochs" },
1674
+ "address": { "slug": "being-aurochs", "canonical": "sohl-sohl-being-aurochs" },
1466
1675
  "file": { "path": "Bestiary/Animal/Aurochs.md", "folder": "Bestiary/Animal", "name": "Aurochs" },
1467
1676
  "anchors": [
1468
1677
  {
@@ -1524,18 +1733,22 @@ first. An entry that is not a non-empty string is dropped rather than left as a
1524
1733
  hole, since the array is a set of names to match and a null is not one.
1525
1734
 
1526
1735
  `address.slug` is what goes inside `[[…]]` within the package; `address.canonical`
1527
- is the package-qualified key the link manifest files the note under. Both are
1528
- `null` for a note with no type or no shortcode, which has no address at all — the
1529
- record says so rather than leaving each reader to rediscover the rule.
1530
-
1531
- **Neither is new information** both derive from `type` and `shortcode`, which
1532
- every record already carries. What the fields add is the _rule_: the lowercasing
1736
+ is the fully qualified key the content index files the note under, carrying the
1737
+ package and the system as well. The slug is the canonical key's **last two
1738
+ segments**, not its whole tail: a page has no system to name, so the two forms
1739
+ diverge by that segment rather than one trailing the other. Both are `null` for a
1740
+ note with no type or no shortcode, which has no address at all — the record says
1741
+ so rather than leaving each reader to rediscover the rule.
1742
+
1743
+ **Neither is new information** — the slug derives from `type` and `shortcode`,
1744
+ which every record already carries, and the canonical key adds only the system
1745
+ those two already imply. What the fields add is the _rule_: the lowercasing
1533
1746
  and the hyphen join live in one place, derived by the same `addressSlug` and
1534
1747
  `canonicalKey` the manifest and the site build use, so an index cannot disagree
1535
1748
  with either about where a note lives. A consumer that reimplements the join
1536
1749
  slightly differently gets a lookup matching nothing and no explanation — which is
1537
1750
  exactly how a resolver keyed on a bare `type/shortcode` silently misses every
1538
- canonical `pkg-type-shortcode` entry.
1751
+ canonical `pkg-system-type-shortcode` entry.
1539
1752
 
1540
1753
  **Anchors make a link checkable without a build.** Because the index states every
1541
1754
  anchor a note defines, `[[being-aurochs#dossier]]` can be confirmed — or shown
@@ -1666,9 +1879,12 @@ configuration to stay empty: in `homepage` mode the tree is never walked for
1666
1879
  pages, and `sections`, `trees`, `landing` and `backfillSections` emit nothing
1667
1880
  even when they are declared.
1668
1881
 
1669
- That is separate from `publish.manifests.publish`, which stays off for both for
1670
- an unrelated reason: a link manifest is the dependency edge that would stop the
1671
- module being withdrawable, and a homepage is one row in a routing table.
1882
+ That is separate from the **dependency** edge, which such a module also
1883
+ declines: being cited by another package is what would stop it being
1884
+ withdrawable, and a homepage is one row in a routing table rather than an
1885
+ address anyone links to. A package publishes its content index regardless — the
1886
+ licensing constraint is against publishing _pages_, not against the artifact
1887
+ existing — but nothing may declare it as a dependency.
1672
1888
 
1673
1889
  The homepage's file is written at the root of `site.out` — the package's own
1674
1890
  site root, one level above the content mount, which is where
@@ -1678,7 +1894,7 @@ decides where it publishes, and states it relative to the site root — `site.ba
1678
1894
  does not reach it (#217).
1679
1895
 
1680
1896
  **What it does not do is decide addresses.** Those come from `publish.address`,
1681
- the same setting the link manifest reads, so a page and its manifest entry cannot
1897
+ the same setting the content index reads, so a page and its index record cannot
1682
1898
  disagree about where the page is. Everything under `site:` is _framing_ —
1683
1899
  where the tree is written, what a section is called, which extra trees are
1684
1900
  published beside the content:
@@ -1951,7 +2167,7 @@ form every C-family compiler, `tsc` and ESLint already use, so an editor, a CI
1951
2167
  annotator or a `grep` parses it with no knowledge of this build:
1952
2168
 
1953
2169
  ```text
1954
- assets/content/Regions/Capital_Nome.md:43:635: error: address [[place-kenbetpat]] resolves to no note — no package publishes it. Fix the shortcode, or vendor the link manifest of the package that does — in "The Capital Nome".
2170
+ assets/content/Regions/Capital_Nome.md:43:635: error: address [[place-kenbetpat]] resolves to no note — no package publishes it. Fix the shortcode, or declare the package that does as a dependency and run `content-build deps fetch` — in "The Capital Nome".
1955
2171
  ```
1956
2172
 
1957
2173
  `file:line:column: severity: message`. The path is relative to the working
@@ -2000,7 +2216,7 @@ authored character to point at.
2000
2216
  ## Layout
2001
2217
 
2002
2218
  - **`@heroiclands/package-build/engine`** — package-agnostic machinery: the
2003
- content walk, frontmatter, tables, wikilinks, ids, folders, the link manifest
2219
+ content walk, frontmatter, tables, wikilinks, ids, folders, the content index
2004
2220
  and the web-address rule, `BasePackCompiler`, and the generic Foundry document
2005
2221
  compilers.
2006
2222
  - **`@heroiclands/package-build/sohl`** — Song of Heroic Lands data-model
package/README.md CHANGED
@@ -7,7 +7,7 @@ It has two halves, split by **input**:
7
7
 
8
8
  | Half | Reads | Produces |
9
9
  | ------------- | ------------------------------------------------------------ | -------------------------------------------------------------- |
10
- | **content** | `assets/content/**` | compendium packs, site content, link manifest |
10
+ | **content** | `assets/content/**` | compendium packs, site content, content index |
11
11
  | **packaging** | `lang/`, `styles/`, `src/`, `assets/`, the manifest template | `system.json` / `module.json`, styles, bundle, release archive |
12
12
 
13
13
  The content half is documented separately in **[CONTENT.md](CONTENT.md)** — the