@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
@@ -138,6 +138,18 @@ it _there_, and a system that disagrees is not in error. A weapon weighs what
138
138
  This is the same rule as `hm3.type` overriding a derived document type, applied
139
139
  to fields: derive from the shared source, and let the system state the exception.
140
140
 
141
+ **A shared source and the key a system block still carries are two
142
+ declarations.** The mapping tables name the shared source — `data.species` — and
143
+ the corpus writes the same fact inside the block it has always written it in —
144
+ `hm3.species`. Those are two positions for one field, and both are read while
145
+ the corpus moves, with the block winning. A field says so by naming each: its
146
+ shared source, and the legacy in-block key it is being swept off. Reading it
147
+ from the legacy position is _reported_, so the sweep has a progress signal, and
148
+ the note compiles to the identical document either way — the same read-both,
149
+ report-one shape every other retirement in this format uses. Until #305 the two
150
+ were one declaration, so a field could name only one of them, and a row this
151
+ table stated was reachable only by a note that had already moved.
152
+
141
153
  **A field whose spelling means something else at the note level has no shared
142
154
  source.** The fallback assumes the two vocabularies agree about what a name
143
155
  means, and they do not always: a note's top-level `title` is the heading its page
@@ -166,7 +178,7 @@ at all in any particular system.
166
178
 
167
179
  Each system then declares a map from the note's `(type, subType)` onto its own
168
180
  document type — and, for SoHL, its own `system.subType`. The map is **declared**,
169
- never inferred from a coincidence of names: `skill`, `weapongear`, `armorgear`,
181
+ never inferred from a coincidence of names: `skill`, `weapongear`, `armor`,
170
182
  `containergear` and `miscgear` exist in both systems with _different_ data
171
183
  models, so name-matching there would not fail, it would succeed wrongly.
172
184
 
@@ -174,6 +186,22 @@ Because the map derives the document type, `hm3.type` and `sohl.system.subType`
174
186
  are **overrides**, not required declarations. A note states them only when the
175
187
  map cannot decide, or decides wrongly.
176
188
 
189
+ **Four rows cannot decide, and there `hm3.type` is required.** HM3 splits four
190
+ of the note vocabulary's types across several documents — `mysticalability` into
191
+ a `psionic`, a `spell` or an `invocation`; `trauma` into an `injury` or a
192
+ `trait`; `weapongear` into a `weapongear` or a `missilegear`; `being` into a
193
+ `character` or a `creature`. Nothing in the note's own vocabulary partitions
194
+ cleanly onto any of those splits, so the note says which, in its own block, and
195
+ a note that says nothing is **an error naming the note and listing the permitted
196
+ values**. It is never defaulted: a default would pick one and be right about
197
+ half the time.
198
+
199
+ A consequence worth knowing before you author a being's embedded items: a
200
+ `(type, shortcode)` reference has no block of its own to read a discriminator
201
+ from, so it **cannot address a one-to-many type**. `[[weapongear-spear]]` names
202
+ no single HM3 document, and the reference is refused rather than resolved to
203
+ whichever came first.
204
+
177
205
  A note whose `(type, subType)` has no mapping for a system it carries a block
178
206
  for is an **error naming the note**, never a silent skip and never a guess at the
179
207
  first matching value. Where a mapping is missing for a whole class of note, that
@@ -186,8 +214,16 @@ Eight rows were identical in all sixteen tables below, so they are stated once
186
214
  here and omitted there. Each per-type table shows only what is particular to that
187
215
  type.
188
216
 
189
- | shared source | → sohl | → hm3 |
190
- | ------------- | ------ | ----- |
217
+ | shared source | → sohl | → hm3 |
218
+ | ----------------------- | ------------------------- | ---------------------------- |
219
+ | `name.full` | `name` | `name` |
220
+ | `img` | `img` | `img` |
221
+ | `id` | `_id` | `_id` |
222
+ | `packFolder` / `folder` | `folder` | `folder` |
223
+ | `shortcode` | `system.shortcode` | NA |
224
+ | `data.templatePriority` | `system.templatePriority` | `flags.hm3.templatePriority` |
225
+ | `actionDefs` | `system.actionDefs` | NA |
226
+ | `notes` | `system.notes` | `system.notes` |
191
227
 
192
228
  **A column reads NA wherever the type produces no document in that system.** An
193
229
  `affiliation` has no HM3 form, so its whole HM3 column is NA; `armorlocation` has
@@ -197,13 +233,377 @@ that matter.
197
233
 
198
234
  Actor types (`being`, `vehicle`) add one more:
199
235
 
200
- | shared source | → sohl | → hm3 |
201
- | ------------- | ------ | ----- |
236
+ | shared source | → sohl | → hm3 |
237
+ | --------------- | ----------------- | ----------------- |
238
+ | `data.portrait` | `system.portrait` | `system.bioImage` |
239
+
240
+ An actor carries `img` (its token art) and `portrait` (its sheet portrait)
241
+ independently, which is why this is a row of its own rather than a second
242
+ spelling of the one above. An Item has no second image, so the row applies to
243
+ actor types alone.
244
+
245
+ **Two of the eight are Item-only in SoHL.** `actionDefs` and `notes` are declared
246
+ on every SoHL Item subtype and on no SoHL Actor, so on a `being` or a `vehicle`
247
+ the SoHL column of both reads NA.
248
+
249
+ `notes` is also the one row that is emitted rather than mapped: SoHL writes
250
+ `system.notes` empty on every Item, and no note-level key fills it yet. The row
251
+ states where such a key would land, which is what makes `armorlocation`'s
252
+ exception below sayable at all.
253
+
254
+ **A third SoHL Item mapping has no shared source, so it is not a row.** SoHL
255
+ writes `system.docHtml` on every Item from the note's own prose — the UUID of the
256
+ JournalEntry that prose compiled into, which is derived rather than authored.
257
+ HM3's data model has nowhere to put such a pointer, so the same prose reaches an
258
+ HM3 item only as its own journal.
202
259
 
203
260
  **One exception.** HM3's `armorlocation` declares no `notes` — it is the one
204
261
  subtype that extends the Foundry base directly with no templates — so `notes` is
205
262
  NA on both sides for that type, and its table says so.
206
263
 
264
+ **And one divergence, tracked rather than specified away.** HM3 records a
265
+ template priority on an Actor and not on an Item: `hm3/actors.mjs` writes
266
+ `flags.hm3.templatePriority`, and HM3's Item pass writes no equivalent, so an HM3
267
+ item compiled from a template note loses the fact that it is one
268
+ (`HeroicLands/package-build#283`). The row states the mapping the format makes;
269
+ the gap is in the pass, not in the table.
270
+
271
+ #### The pack a note compiles into
272
+
273
+ `pack` names which configured compendium receives the note's document.
274
+
275
+ ```yaml
276
+ pack: items-hm3
277
+ ```
278
+
279
+ It is deliberately close to the retired `package:` and deliberately not the same
280
+ word: `package:` said which _distribution_ owned a note — now the repository's
281
+ `contentPackage`, and no longer authorable — while `pack:` says which
282
+ _compendium_ receives its document.
283
+
284
+ **It names the pack for the note's _own_ document.** A document derived from it
285
+ — an item's prose compiling into a `JournalEntry` of its own — is not what the
286
+ author was addressing, and is routed by the pass that produces it.
287
+
288
+ **`<system>.pack` overrides it for one system.** A note that compiles into two
289
+ systems can send each document to its own pack:
290
+
291
+ ```yaml
292
+ pack: items-sohl
293
+ hm3:
294
+ pack: items-hm3
295
+ ```
296
+
297
+ **Unstated, the document goes to the pack of its type marked `default: true`.**
298
+ Where no pack of that type is the default, the build refuses rather than
299
+ guessing, and names the candidates.
300
+
301
+ Three declarations are refused, each with the reason:
302
+
303
+ | written | why it is refused |
304
+ | ----------------------------------- | --------------------------------------------------------------------------------- |
305
+ | a **companion** pack | A companion is written by another pack's pass, so no note may be routed into one. |
306
+ | a pack **nothing answers to** | The message lists the configured packs of that document type. |
307
+ | a pack of **another document type** | A note's `pack:` names a pack of its own document type. |
308
+
309
+ #### Template priority: which template wins
310
+
311
+ A note can mark its document as a **starting template** the Create dialog offers
312
+ to clone from, so a new being or item is born populated rather than blank. The
313
+ value is a **priority**, and the priority is the whole mechanism — it decides
314
+ which of several competing templates a player is actually offered.
315
+
316
+ The shared mapping table above names it `data.templatePriority`, targeting
317
+ `system.templatePriority` in SoHL and `flags.hm3.templatePriority` in HM3.
318
+
319
+ > **`archetype` is the retiring spelling.** It is still read, in the `sohl:`
320
+ > block and at the top level, so a tree sweeps on its own schedule
321
+ > (`HeroicLands/package-build#266`) — but the frontmatter linter refuses it, and
322
+ > what is compiled and emitted is `templatePriority` on all three sides.
323
+ >
324
+ > It is more than a rename, and the collision is **already live** rather than
325
+ > pending: `archetypes` is specified above as the _sort_ a character is, and a
326
+ > being's row declares it. So a number deciding which template wins and a list of
327
+ > what sort of character this is would otherwise be distinguished **only by a
328
+ > plural `s`**.
329
+ >
330
+ > A note carrying both spellings with **different** values is refused rather than
331
+ > resolved quietly — `templatePriority: null` and `archetype: 0` say opposite
332
+ > things, and picking either silently would decide it on the author's behalf.
333
+
334
+ ```yaml
335
+ data:
336
+ templatePriority: 0 # a template, at the priority SoHL's own ship at
337
+ ```
338
+
339
+ ```yaml
340
+ data:
341
+ templatePriority: null # not a template
342
+ ```
343
+
344
+ **Every note SoHL compiles into an Item or an Actor must state it.** Absent, the
345
+ build refuses: "not a template" has to be _said_, not left out, or an omission
346
+ and a decision look identical. The value is a number or `null`, and **`0` is a
347
+ real priority** — the one SoHL's own templates ship at — not an absence.
348
+
349
+ **Where it lands differs by system, because HM3's data model has no field for
350
+ it.** SoHL records it in `system`; HM3 keeps it under its own flag scope,
351
+ `flags.hm3`, and a note that is not a template writes nothing there rather than a
352
+ `null` nothing reads. Both of HM3's passes write it — an Item's flag was missing
353
+ until `HeroicLands/package-build#283`, which made an item note's priority reach
354
+ SoHL and stop at HM3, with nothing said on either side.
355
+
356
+ **How a winner is chosen.** Opening a Create dialog gathers every candidate
357
+ across the world and every matching compendium, _including other modules'_. Those
358
+ are filtered to the `(type, subType)` being created, deduped by **`shortcode`** —
359
+ a template's stable identity, where the name is only presentation — and one
360
+ winner is taken per shortcode:
361
+
362
+ 1. the highest priority;
363
+ 2. then the nearest source — **world**, then **system**, then **module** — so a
364
+ GM's own copy shadows a shipped one at equal priority;
365
+ 3. then a stable UUID, so the answer never depends on load order.
366
+
367
+ **The reserved ranges make a collision predictable.** Two packages can easily
368
+ ship a template under one shortcode, and the number says which yields:
369
+
370
+ | priority | reserved for |
371
+ | ---------- | -------------------------- |
372
+ | `0`–`98` | SoHL and HM3 themselves |
373
+ | `99`–`999` | other HeroicLands packages |
374
+ | `1000`+ | everyone else |
375
+
376
+ HeroicLands reserves everything below `1000`. Since the highest priority wins,
377
+ **anyone else's template always beats content shipped from here** — which is the
378
+ point: a module author can override a standard template without coordinating with
379
+ anybody, and be certain it takes effect.
380
+
381
+ #### The document id
382
+
383
+ A note's Foundry `_id` is **derived from its canonical address**, and no note
384
+ needs to write one:
385
+
386
+ ```yaml
387
+ type: miscgear
388
+ shortcode: bowlcer
389
+ # no `id:` — the document is filed under makeId("document", "sohl-none-miscgear-bowlcer")
390
+ ```
391
+
392
+ The derivation is exactly:
393
+
394
+ ```
395
+ _id = makeId("document", "<package>-<system>-<type>-<shortcode>")
396
+ ```
397
+
398
+ — the note's canonical address, hashed with SHA-1 and truncated to the 16 hex
399
+ characters a Foundry id is. Nothing else feeds it. A consumer holding a
400
+ content-index entry can therefore recompute a document's id, and so its
401
+ compendium UUID, from the `canonical` key alone; it is not a value the index has
402
+ to transport.
403
+
404
+ **Why the address and not an authored string.** A note used to declare an
405
+ opaque 16-character `id` — 6,343 of them across the four content trees — which
406
+ said nothing its address did not, could not be read or reviewed, and was
407
+ guaranteed by nothing: `content-lint` refuses a **duplicate address** across
408
+ every pack of a document type, which is exactly the scope a primary document's
409
+ id must be unique within, and it said nothing at all about a duplicate `id`. So
410
+ the derived id inherits a guard that already exists, where the authored one had
411
+ none. It is the same principle that turned `folder: ONXsqZAIZr2qzxTb` into
412
+ `packFolder: <path>` above: an opaque derived identity does not belong in
413
+ authored content.
414
+
415
+ **An authored `id` still wins**, and that is how a document's identity is
416
+ **pinned**:
417
+
418
+ ```yaml
419
+ type: miscgear
420
+ shortcode: bowlcer
421
+ id: plaiQQm2T5zVK5mO # pinned: this document keeps this id
422
+ ```
423
+
424
+ A blank `id:` is not a pin — it is a deleted value with the key left behind, and
425
+ is treated as absent.
426
+
427
+ **A rename moves the id, and that is the trade.** The address carries the
428
+ shortcode, so renaming a shortcode gives the document a new `_id`. Two things
429
+ make that acceptable: a shortcode rename already breaks every wikilink to the
430
+ note, so it is a breaking change either way; and a note that must keep its
431
+ identity across one pins its `id`, which is what the pin is for. One diagnostic
432
+ narrows — `content-build` tells a **rename** from a **withdrawal** by matching
433
+ document ids across releases, and for an unpinned note both sides now move
434
+ together. It never reports a _wrong_ successor, and it stays exact for a pinned
435
+ note; for the rest, the note **declares** the rename (below).
436
+
437
+ **A note with no address gets no id, and no document.** `type` and `shortcode`
438
+ are what a note is addressed by, so a note missing either cannot be filed and
439
+ the build refuses it by that name rather than by a missing `id:`.
440
+
441
+ #### Declaring a rename
442
+
443
+ A note names the shortcode it used to be published under:
444
+
445
+ ```yaml
446
+ type: weapongear
447
+ shortcode: Taburi
448
+ renamedFrom: Tabri
449
+ ```
450
+
451
+ `(type, shortcode)` is a **published interface** — every satellite that declares
452
+ `itemCatalog: true` assembles its beings out of those addresses — so renaming a
453
+ shortcode breaks other repositories, and `content-build addresses diff` exists to
454
+ say so before a release does. To be useful it has to name where the address
455
+ _went_, and since ids are derived it can no longer work that out for an unpinned
456
+ note: both sides of the match move together, and the rename reads as a
457
+ withdrawal.
458
+
459
+ **Pinning an `id` needs foresight; a declaration needs only hindsight.** A pin
460
+ has to be written _before_ the rename, by an author who does not yet know they
461
+ will make one. An author who has just renamed a shortcode knows exactly what the
462
+ old one was, and that is the only moment anyone does — so this is the key to
463
+ reach for, and `id:` stays what it is for: keeping a document's identity across
464
+ the rename, which is a different question from explaining it.
465
+
466
+ **It takes one shortcode or a list**, because renames chain: the diff runs
467
+ against a released baseline, and a shortcode may have been renamed more than once
468
+ since. List every name the baseline might still know it by.
469
+
470
+ ```yaml
471
+ shortcode: Taburin
472
+ renamedFrom:
473
+ - Tabri
474
+ - Taburi
475
+ ```
476
+
477
+ **It is transient.** Once every baseline a build is compared against post-dates
478
+ the rename, the declaration has nothing left to say and should be deleted. That
479
+ is what separates it from an `id:` pin, which is permanent.
480
+
481
+ **One key per note, at the top level**, however many systems the note compiles
482
+ into: a shortcode is the note's rather than a system block's, so a note carrying
483
+ `sohl:` and `hm3:` blocks compiles two documents that share one shortcode, and
484
+ one declaration covers both.
485
+
486
+ **What a declaration changes is what the diagnostic can say, not what it says
487
+ about you.** A finding reports which of the two joins it had, because they are
488
+ not equally checkable — a matched id is a fact a reader can verify in both
489
+ artefacts, while a declaration is the author's word:
490
+
491
+ ```text
492
+ since sohl@0.8.2, weapongear:Tabri is no longer published; the note now
493
+ published as weapongear:Taburi declares it was renamed from Tabri. Every
494
+ package that resolves weapongear:Tabri breaks when it moves past sohl@0.8.2
495
+ ```
496
+
497
+ A rename that is neither pinned nor declared is still reported as a
498
+ **withdrawal**. Nothing infers a successor from a similar-looking string: a wrong
499
+ one sends the reader to the wrong fix, which is worse than saying nothing.
500
+
501
+ `content-lint` holds a declaration to the same rules a current address is held
502
+ to. An entry must be a well-formed shortcode, must not be the note's own, and
503
+ must name an address the package actually **vacated** — an entry naming an
504
+ address some note still publishes is refused, as are two notes claiming one
505
+ predecessor, since an address had one holder and so has one successor. A repeat
506
+ of the same entry is a warning; the declaration still works.
507
+
508
+ #### The compendium folder
509
+
510
+ A note says which folder of its pack it lands in. Two spellings are read, and
511
+ `packFolder` wins where both are present:
512
+
513
+ ```yaml
514
+ packFolder: poisonsandtoxins # a folder note's address
515
+ folder: ONXsqZAIZr2qzxTb # a Foundry id
516
+ ```
517
+
518
+ **`packFolder` is a folder note's address** — an ordinary address, resolved the
519
+ way every other reference is, and written in any form [the grammar
520
+ admits](#shorter-forms). The field supplies the type, so a bare shortcode is a
521
+ complete address here; `folder-poisonsandtoxins` and the fully qualified
522
+ `sohl-none-folder-poisonsandtoxins` name the same folder. An address no folder
523
+ note answers to is a build error naming the folders the package does declare.
524
+
525
+ **`folder` is a Foundry id**, and is unchanged: a note that names one is read,
526
+ resolved and emitted exactly as before.
527
+
528
+ **Which one a value is comes from the field it was written in, never from the
529
+ string.** Both are alphanumeric, so there is nothing in the value to tell them
530
+ apart.
531
+
532
+ Note this is the _pack_ folder, not the note's directory. The directory is
533
+ `file.path` / `file.folder`, which a content table reads separately.
534
+
535
+ **Where a folder materialises is derived from what references it.** Every pack
536
+ holding a document that names a folder gets that folder, and its ancestors with
537
+ it — so a documentation journal is filed beside the item it describes without
538
+ the journals pack having to declare anything. A folder nothing references
539
+ materialises nowhere.
540
+
541
+ That derivation is what makes a whole class of defect unrepresentable. The
542
+ folder used to be declared twice, once per pack, in two files free to disagree:
543
+ `sohl-thalorna` was missing 57 of its item folders from its journal folder file
544
+ and `sohl-kethira-basic` had no journal folder file at all, so both emitted
545
+ documentation journals into folders their own pack never declared — silently.
546
+ With one folder note and one address there is no second file to disagree with
547
+ the first.
548
+
549
+ > **`packFolder` was a path** for one release (`Possessions/Misc_Gear/Cooking`).
550
+ > A path encoded the hierarchy in the value, so reparenting a folder made every
551
+ > note naming it wrong — a structural edit became a corpus-wide rewrite. The
552
+ > path form is **removed**, not deprecated: nothing authored it yet, which is
553
+ > the whole reason the change was cheap enough to make.
554
+
555
+ #### The knowledgebase category
556
+
557
+ `kbcat` names the group a note is listed under on the knowledgebase and the
558
+ website. It is written in the system block:
559
+
560
+ ```yaml
561
+ sohl:
562
+ kbcat: poisontoxin
563
+ ```
564
+
565
+ **It compiles into no document.** No pack compiler reads it and no `system`
566
+ field receives it. It reaches a published page because a note's frontmatter is
567
+ copied onto that page, where a list layout groups by `sohl.kbcat` — so `kbcat`
568
+ is the one key in this section that answers _where does this appear_ for the
569
+ web rather than for Foundry. `pack` and `packFolder` place a document in a
570
+ compendium; `kbcat` places a page in a list.
571
+
572
+ That is also why it is specified here rather than in a type's table. A type's
573
+ fields say what the **builder** compiles, and `kbcat` is never compiled — but
574
+ what a note **may write** is broader than what any one consumer reads, and a
575
+ check that equated the two reported thousands of correctly authored properties
576
+ as unknown.
577
+
578
+ **It is editorial, and deliberately independent of `subType`.** The two are not
579
+ alternative spellings of one classification and neither is derived from the
580
+ other. `kbcat` both _subdivides_ a subtype — `trauma`/`physcond` is listed as
581
+ `physdisability`, `physfeature` or `physprivations` — and _renames_ one for
582
+ display, as `trauma`/`fear` listed under `phobias`. Most notes that carry a
583
+ `kbcat` declare no `subType` at all. So the two are stated separately where both
584
+ apply, and a reviewer should not read a disagreement between them as an error.
585
+
586
+ **The value is free-form, and nothing validates it.** There is no configured
587
+ list of categories. The frontmatter check knows `kbcat` is a key every type may
588
+ write and says nothing whatever about its value. A layout supplies display
589
+ titles and an explicit order for the values it knows about, and appends any
590
+ other value as its own group, titled by humanizing it.
591
+
592
+ The consequence is worth stating plainly, because it is the failure mode this
593
+ key has: **a misspelled category is not a build error and is not dropped — it
594
+ silently becomes a group of one**, sorted in after the known ones.
595
+
596
+ **A note that writes none is dropped from the list entirely.** Grouping is by
597
+ the key, so a page carrying no value falls in no group and is absent from the
598
+ list page — not listed last, not listed under a fallback heading, absent, with
599
+ nothing reported at either build. Every note of a listed type in SoHL's tree
600
+ carries one today, and nothing in this package enforces that; the content index
601
+ is where the question _which notes carry no `kbcat`?_ is answered.
602
+
603
+ **It is also what a content table sections on.** `sohl.kbcat AS _section` in a
604
+ `sql` fence is the ordinary case of _Content tables_ below, and the same
605
+ free-form value decides the headings there.
606
+
207
607
  ### WikiLinks
208
608
 
209
609
  Twenty-seven fields in the tables below take a `WikiLink`, and a link is written
@@ -477,11 +877,11 @@ objects with a SoHL counterpart — `Arrow (Longbow)`, `Bolt (Crossbow)`,
477
877
  both blocks. Only four are companions: `Javelin (thrown)`, `Shorkana (thrown)`,
478
878
  `Spear (thrown)`, `Taburi (thrown)`.
479
879
 
480
- **A companion note requires its system's type.** With one block and no
481
- counterpart, nothing can derive whether `Spear (thrown)` is a `weapongear` or a
482
- `missilegear` — there is no other system to infer from and no subType to read. So
483
- `hm3.type` is _required_ on a single-block note, and is an override only on notes
484
- that carry both.
880
+ **Every note carrying an `hm3:` block states its type.** Nothing can derive
881
+ whether `Spear (thrown)` is a `weapongear` or a `missilegear` — there is no other
882
+ system to infer from and no subType to read — and the same is true of the note
883
+ carrying both blocks, whose SoHL strike modes describe every usage at once. So
884
+ `hm3.type` is _required_ on both, not only on the companion.
485
885
 
486
886
  The following special markdown sequences are recognized:
487
887
 
@@ -495,6 +895,133 @@ Any header can include curly braces. Inside the curly braces:
495
895
  - `.class1` represents a CSS class named `class1` (any number of classes allowed)
496
896
  - `attr="value"` represents an HTML attribute named `attr` whose value is `value` (any number of attr/value pairs allowed)
497
897
 
898
+ #### Content tables
899
+
900
+ A fenced `dataview` block is replaced by the table its query selects:
901
+
902
+ ````markdown
903
+ ```dataview
904
+ TABLE WITHOUT ID name.full AS "Name", shortcode AS "Code"
905
+ WHERE type = "armor"
906
+ ```
907
+ ````
908
+
909
+ **A query that selects nothing is a build error.** A zero-row table publishes as
910
+ a bare header and a rule, and a stale query — a renamed type, a retired
911
+ category, a typo'd path — is then indistinguishable from a category that is
912
+ legitimately empty. Eight tables in one note published that way for months after
913
+ a type rename, and no build said a word.
914
+
915
+ Where a table is _meant_ to be empty, say so on the fence:
916
+
917
+ ````markdown
918
+ ```dataview allow-empty
919
+ TABLE WITHOUT ID name.full AS "Name"
920
+ WHERE type = "affliction" AND sohl.kbcat = "not-written-yet"
921
+ ```
922
+ ````
923
+
924
+ The opt-in is on the fence rather than in the query because it is a statement
925
+ about this directive, not part of the query language. Either way the table is
926
+ still rendered — the finding is the point, not withholding the output.
927
+
928
+ ##### In SQL, over the content index
929
+
930
+ `dataview` is being replaced by **SQL**, queried over the content index, and both
931
+ fences work while the corpus is converted (#246). The query is real SQL, run by
932
+ DuckDB — not a dialect maintained by this package.
933
+
934
+ ````markdown
935
+ ```sql
936
+ SELECT address.slug AS _ref,
937
+ sohl.kbcat AS _section,
938
+ name.full AS "Name",
939
+ sohl.weight AS "Weight"
940
+ FROM notes
941
+ WHERE type = 'miscgear'
942
+ ORDER BY sohl.kbcat, name.full
943
+ ```
944
+ ````
945
+
946
+ **`FROM notes`** is the content index: one row per note, plus one per
947
+ documentation entry, so `type = 'miscgear'` selects the items and never their
948
+ journals. A nested field is addressed exactly as a note authors it —
949
+ `sohl.weight`, `name.full`, `file.path` — because the index is read as JSON and
950
+ every nested object is inferred as a struct. A field a note type does not carry
951
+ reads `NULL` rather than failing.
952
+
953
+ **Two aliases are read by the renderer rather than printed**, because which
954
+ column links and where a section breaks are decisions about output, not
955
+ relational operations:
956
+
957
+ | Alias | What it does |
958
+ | ---------- | ---------------------------------------------------------------------- |
959
+ | `_ref` | Makes the row's **first** rendered column a wikilink to that address. |
960
+ | `_section` | Emits a headed table per distinct value, in the order the rows arrive. |
961
+
962
+ `_section` is why one query replaces the forty near-identical blocks a grouped
963
+ table used to need: the authored `ORDER BY` decides the section order too.
964
+
965
+ **Beware `folder`.** It is a note's _pack_ folder, not its directory — the
966
+ directory is `file.folder`.
967
+
968
+ ###### Reading another package's notes
969
+
970
+ Each package this one **depends on** is attached as a schema named after it, so
971
+ a satellite can tabulate what it builds on:
972
+
973
+ ````markdown
974
+ ```sql
975
+ SELECT name.full AS "Name", sohl.skillBase AS "Base"
976
+ FROM sohl.notes
977
+ WHERE type = 'skill'
978
+ ORDER BY name.full
979
+ ```
980
+ ````
981
+
982
+ This package's own notes stay at the unqualified `notes`, and a query may read
983
+ both at once — joining your beings against the skills they cite is one `FROM`
984
+ clause. It needs no fetch and no configuration: a dependency's published index
985
+ is already cached when a compile starts, because resolving addresses across
986
+ packages needs it.
987
+
988
+ Which dataset a query reads is `FROM`'s job rather than a fence property. A
989
+ fence naming a file would write a build artifact's path into the corpus, so
990
+ renaming the artifact would mean sweeping every note that cited it.
991
+
992
+ ###### Header arguments
993
+
994
+ Statements _about the directive_ — as opposed to the query — are written after
995
+ the language as **org-babel header arguments**:
996
+
997
+ ````markdown
998
+ ```sql :section-level 3 :allow-empty
999
+ SELECT name.full AS "Name", sohl.kbcat AS _section
1000
+ FROM notes WHERE type = 'affliction'
1001
+ ```
1002
+ ````
1003
+
1004
+ The language word stays first and stays plain, so GitHub, Prettier and every
1005
+ other markdown reader still highlight the block as SQL and simply ignore what
1006
+ follows.
1007
+
1008
+ | Argument | What it does |
1009
+ | ---------------------- | ---------------------------------------------------------------------------------------- |
1010
+ | `:allow-empty` | A table selecting nothing is intended, not a stale query. Without it, empty is an error. |
1011
+ | `:section-level <1-6>` | The heading level `_section` emits. Default `2`. |
1012
+
1013
+ The grammar is org's, so it extends without inventing a spelling per property:
1014
+
1015
+ - a key is `:name` **starting a word**, so a colon inside or ending one is text
1016
+ — `:caption Gear: the tables` is a single argument;
1017
+ - a value runs to the next key or the end of the line, spaces included;
1018
+ - a key with no value means `true`, which is what `:allow-empty` is;
1019
+ - a value may be `"quoted"` to hold a word that would otherwise read as a key;
1020
+ - a repeated key takes its last value.
1021
+
1022
+ `dataview` keeps its own bare `allow-empty`; it is the retiring language and its
1023
+ grammar is frozen.
1024
+
498
1025
  ```
499
1026
  :::secret
500
1027
  This is secret text
@@ -562,42 +1089,84 @@ The following H1 headers are treated specially:
562
1089
 
563
1090
  Generates a living (or undead, or spirit) being.
564
1091
 
565
- | `data` property | Values | Description |
566
- | --------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------- |
567
- | `portrait` | `string` | File path to the portrait image |
568
- | `templatePriority` | `number` | Template priority, _null_ = not a template |
569
- | `archetypes` | `Archetype[]` | List of archtypical behaviors |
570
- | `occupation` | `string` | Name of the character's occupation |
571
- | `stations` | `WikiLink[]` | Name of the stations the character belongs to |
572
- | `lore` | `WikiLink[]` | Lore concerning this being — the people it is of, the standing it holds, the law it lives under |
573
- | `homes` | `WikiLink[]` | Place the being calls home |
574
- | `affiliations` | `WikiLink[]` | Affilliations (e.g., arcane/divine traditions, polities, etc) |
575
- | `gender` | `male \| female \| other` | Gender of the character |
576
- | `species` | `WikiLink` | Being's species (lore) |
577
- | `age` | `number` | Age of the character |
578
- | `birthday` | `YYYY/MM/DD` | Date of birth of the character |
579
- | `height` | `number` | Height in meters |
580
- | `weight` | `number` | Weight in kilograms |
581
- | `frame` | `scant \| light \| medium \| large \| massive` | Relative frame size |
582
- | `appearance.eye_color` | `string` | Eye color |
583
- | `appearance.hair_color` | `string` | Hair color |
584
- | `appearance.skin_color` | `string` | Skin color |
585
- | `appearance.complexion` | `string` | Complexion |
586
- | `appearance.extra_features` | `string[]` | Extra features |
1092
+ | `data` property | Values | Description |
1093
+ | --------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------ |
1094
+ | `portrait` | `string` | File path to the portrait image |
1095
+ | `templatePriority` | `number` | Template priority, _null_ = not a template |
1096
+ | `archetypes` | `Archetype[]` | What sort of character this is. **Always an array** — `[]` where none apply; `null` is an error. |
1097
+ | `occupation` | `string` | Name of the character's occupation |
1098
+ | `stations` | `WikiLink[]` | Name of the stations the character belongs to |
1099
+ | `lore` | `WikiLink[]` | Lore concerning this being — the people it is of, the standing it holds, the law it lives under |
1100
+ | `homes` | `WikiLink[]` | Place the being calls home |
1101
+ | `affiliations` | `WikiLink[]` | Affilliations (e.g., arcane/divine traditions, polities, etc) |
1102
+ | `gender` | `male \| female \| other` | Gender of the character |
1103
+ | `species` | `WikiLink` | Being's species (lore) |
1104
+ | `age` | `number` | Age of the character |
1105
+ | `birthday` | `YYYY/MM/DD` | Date of birth of the character |
1106
+ | `height` | `number` | Height in meters |
1107
+ | `weight` | `number` | Weight in kilograms |
1108
+ | `frame` | `scant \| light \| medium \| large \| massive` | Relative frame size |
1109
+ | `appearance.eye_color` | `string` | Eye color |
1110
+ | `appearance.hair_color` | `string` | Hair color |
1111
+ | `appearance.skin_color` | `string` | Skin color |
1112
+ | `appearance.complexion` | `string` | Complexion |
1113
+ | `appearance.extra_features` | `string[]` | Extra features |
1114
+
1115
+ #### Identifying a being's embedded items
1116
+
1117
+ Each entry in `sohl.items` compiles into one embedded Item, and its `_id` is
1118
+ derived from **what the entry is**, never from where it sits in the list:
1119
+
1120
+ ```
1121
+ _id = makeId(<the actor's id>, "<subType>:<system.shortcode>")
1122
+ ```
1123
+
1124
+ An entry's identity is its **own `system.shortcode`**. The entry's _top-level_
1125
+ `shortcode` is a **selector** — it names the catalogue template the entry is
1126
+ written from and is never written to the document — so two entries may share
1127
+ one:
1128
+
1129
+ ```yaml
1130
+ sohl:
1131
+ items:
1132
+ - shortcode: dgr # selects the catalogue's dagger
1133
+ type: weapongear
1134
+ name: Dagger 1
1135
+ system:
1136
+ shortcode: dgr1 # this dagger's own identity
1137
+ - shortcode: dgr
1138
+ type: weapongear
1139
+ name: Dagger 2
1140
+ system:
1141
+ shortcode: dgr2
1142
+ ```
1143
+
1144
+ **Two entries resolving to one identity are a build error naming both.** Without
1145
+ their own `system.shortcode`, both daggers above carry the catalogue's `dgr`,
1146
+ which makes them the same entity to everything that resolves by `(type,
1147
+ shortcode)` — compendium/world reconciliation, template shadowing, cohort
1148
+ membership, effect and expression references. A `name` cannot stand in: it is
1149
+ presentation, free to be localized or to diverge.
1150
+
1151
+ **Reordering the list moves no id.** The key used to carry the entry's position,
1152
+ so inserting an item renumbered every id after it and a re-import created new
1153
+ documents beside the old ones — while nothing about those documents had changed,
1154
+ only their neighbours. The same is now true of a note's journal pages: an
1155
+ unanchored page is keyed on its heading, so inserting a heading leaves every
1156
+ other page's id where it was. Two sibling pages sharing a heading is likewise a
1157
+ build error, matching the `MD024` lint rule that already refuses it.
587
1158
 
588
1159
  If a `sohl` property is present, a SoHL actor of type "being" will be created.
589
1160
 
590
- If an `hm3` property is present, an HM3 actor is created. Its document type is derived from the note's `subType`; `hm3.type` overrides that, and must be `character` or `creature`.
1161
+ If an `hm3` property is present, an HM3 actor is created. Its document type is **not** derived: `hm3.type` states it, and must be `character` or `creature`. A note that omits it is an error naming the note — see _The note vocabulary, and how it maps_.
591
1162
 
592
1163
  A SoHL "being" document will be created, as will an "HM3" document.
593
1164
 
594
- | shared source | → sohl | → hm3 |
595
- | ----------------------- | ----------------- | ---------------------------- |
596
- | `data.portrait` | `system.portrait` | `system.bioImage` |
597
- | `data.templatePriority` | `system.template` | `flags.hm3.templatePriority` |
598
- | `data.species` | NA | `system.species` |
599
- | `data.gender` | NA | `system.gender` |
600
- | `data.occupation` | NA | `system.occupation` |
1165
+ | shared source | → sohl | → hm3 |
1166
+ | ----------------- | ------ | ------------------- |
1167
+ | `data.species` | NA | `system.species` |
1168
+ | `data.gender` | NA | `system.gender` |
1169
+ | `data.occupation` | NA | `system.occupation` |
601
1170
 
602
1171
  ### type: homepage
603
1172
 
@@ -618,10 +1187,8 @@ Represents a conveyance able to hold goods and people moving from one place to a
618
1187
 
619
1188
  If `sohl` is present, this becomes a `vehicle` actor.
620
1189
 
621
- | shared source | sohl | hm3 |
622
- | ----------------------- | ----------------- | ----- |
623
- | `data.portrait` | `system.portrait` | NA |
624
- | `data.templatePriority` | `system.template` | NA |
1190
+ It maps nothing beyond the shared rows above, actor row included: a vehicle
1191
+ carries a portrait and a template priority and no field of its own.
625
1192
 
626
1193
  ### type: affiliation
627
1194
 
@@ -856,7 +1423,7 @@ If `sohl` is present, this becomes an `affliction` item.
856
1423
  | `data.healingCheckDurationFormula` | `system.healingCheckDurationFormula` | NA |
857
1424
  | `data.resolutionDurationFormula` | `system.resolutionDurationFormula` | NA |
858
1425
 
859
- ### type: armorgear
1426
+ ### type: armor
860
1427
 
861
1428
  Note: `data.quantity` may not be specified. Quantity is always 1.
862
1429
 
@@ -870,7 +1437,9 @@ Note: `data.quantity` may not be specified. Quantity is always 1.
870
1437
 
871
1438
  If a `sohl` property is present, a SoHL item of type "armorgear" will be created.
872
1439
 
873
- if a `hm3` property is present, an HM3 item of type "armorgear" will be created.
1440
+ if a `hm3` property is present, an HM3 item of type "armorgear" will be created. The
1441
+ note type is `armor` in both cases: the `gear` suffix named a document subtype rather
1442
+ than the thing the note is about.
874
1443
 
875
1444
  | shared source | → sohl | → hm3 |
876
1445
  | ----------------- | ----------------------- | --------------- |
@@ -902,7 +1471,7 @@ if a `sohl` property is present, a SoHL item of type "attribute" will be created
902
1471
  | shared source | → sohl | → hm3 |
903
1472
  | ------------- | ------ | ----- |
904
1473
 
905
- ### type: concoctiongear
1474
+ ### type: concoction
906
1475
 
907
1476
  **subType**:
908
1477
 
@@ -1043,13 +1612,7 @@ if a `sohl` property is present, a SoHL item of type "mystery" will be created.
1043
1612
 
1044
1613
  if a `sohl` property is present, a SoHL item of type "mysticalability" will be created.
1045
1614
 
1046
- If an `hm3` property is present, an HM3 item is created. The following mappings are performed:
1047
-
1048
- - if `subType` === `arcaneinvocation`, `hm3.type` = `spell`
1049
- - if `subType` === `divineinvocation`, `hm3.type` = `invocation`
1050
- - if `subType` === `arcanetalent`, `hm3.type` = `psionic`
1051
-
1052
- If `hm3.type` is specified, it must be `psionic`, `spell` or `invocation`.
1615
+ If an `hm3` property is present, an HM3 item is created, and `hm3.type` states which `psionic`, `spell` or `invocation`. It is **authored, not derived from `subType`**: the ten mystical-ability subtypes do not partition onto HM3's three documents (a `spiritrite`, an `alchemy` and a `divination` each answer to none of them), so a derivation would be a guess with a plausible shape. A note that omits it is an error naming the note.
1053
1616
 
1054
1617
  | shared source | → sohl | → hm3 |
1055
1618
  | ----------------------- | ----------------------------- | ------------- |
@@ -1061,7 +1624,7 @@ If `hm3.type` is specified, it must be `psionic`, `spell` or `invocation`.
1061
1624
  | `data.charges.value` | `system.charges.value` | NA |
1062
1625
  | `data.charges.max` | `system.charges.max` | NA |
1063
1626
 
1064
- ### type: projectilegear
1627
+ ### type: projectile
1065
1628
 
1066
1629
  **subTypes**:
1067
1630
 
@@ -1175,7 +1738,7 @@ if a `sohl` property is present, a SoHL item of type "weapongear" will be create
1175
1738
  carrying every strike mode the weapon has — melee and missile alike — on
1176
1739
  `system.strikeModes`.
1177
1740
 
1178
- If an `hm3` property is present, an HM3 item is created. **`weapon` has no `subType`**: SoHL distinguishes a weapon's uses with strike modes rather than by kind, and HM3's document type follows from which of those the note describes. A note carrying both blocks is a `weapongear` in each; a companion note carrying only `hm3:` states `hm3.type` itself, and is usually a `missilegear` — see _One note is at most one document per system_.
1741
+ If an `hm3` property is present, an HM3 item is created, and `hm3.type` states whether it is a `weapongear` or a `missilegear`. **`weapon` has no `subType`**: SoHL distinguishes a weapon's uses with strike modes rather than by kind, and HM3 has one document per usage, so nothing but the note can say which usage it describes. Every note carrying an `hm3:` block states it — the one carrying both blocks as well as the companion carrying only `hm3:`, which is usually a `missilegear` — see _One note is at most one document per system_.
1179
1742
 
1180
1743
  | shared source | → sohl | → hm3 |
1181
1744
  | ----------------- | ----------------------- | --------------- |
@@ -1310,8 +1873,8 @@ Foundry, so the build refuses rather than resolving.
1310
1873
  > system-specific, and HM3 would want the identical Scene. Authoring it under
1311
1874
  > `sohl:` means a map produces nothing for a system-agnostic build and carries a
1312
1875
  > SoHL infobox implying a specificity it does not have. It is the same class of
1313
- > mistake as storing `archetype` in flags: the data went where the only available
1314
- > container was, rather than where it belongs. Three notes carry it today.
1876
+ > mistake as storing the template priority in flags: the data went where the only
1877
+ > available container was, rather than where it belongs. Three notes carry it today.
1315
1878
 
1316
1879
  **A map is always a leaf.** Its frontmatter references nothing outside itself.
1317
1880
  `notes:` is a list of `[anchor, GridLocation]`, and each anchor names a heading in
@@ -1489,3 +2052,150 @@ need an authoring convention of their own.
1489
2052
 
1490
2053
  The note's `img` is a content-relative path resolved the way every other note's
1491
2054
  is; a note that authors none takes Foundry's own `icons/svg/dice-target.svg`.
2055
+
2056
+ ### type: bundle
2057
+
2058
+ A bundle of notes to be taken as a single unit — an `Adventure` in Foundry VTT.
2059
+
2060
+ | `data` property | Values | Description |
2061
+ | --------------- | ------------ | ------------------------------------------------------ |
2062
+ | `contents` | `WikiLink[]` | The documents the Adventure holds; `[]` when unstated. |
2063
+
2064
+ ```yaml
2065
+ ---
2066
+ type: bundle
2067
+ shortcode: hegovynvale
2068
+ name:
2069
+ full: The Hegóvyn Vale
2070
+ data:
2071
+ contents:
2072
+ - map-hegovynvale
2073
+ - miscgear-bowlcer
2074
+ - being-aurochs
2075
+ ---
2076
+ Prose describing what the bundle is for.
2077
+ ```
2078
+
2079
+ An `Adventure` carries **copies** of what it holds, not references: importing one
2080
+ creates or updates each document in the world, after which they live
2081
+ independently. So a bundle is not a folder — a folder is a live grouping that
2082
+ persists in the pack.
2083
+
2084
+ **Each address names the note's own document.** That is the same rule `pack:`
2085
+ follows, so there is one answer and not two. A note that compiles into _two_
2086
+ documents — an item and the JournalEntry its prose became — puts the second in a
2087
+ bundle only when the bundle names it by its own `doc…` address:
2088
+ `miscgear-bowlcer` is the item, `docmiscgear-bowlcer` its description page.
2089
+
2090
+ **An address that resolves to nothing fails the build.** A `folder` address is
2091
+ refused with a message of its own: a folder materialises in every pack holding
2092
+ something filed in it, so it belongs to no one pack and there is no single copy
2093
+ to take.
2094
+
2095
+ **The note's prose becomes the Adventure's `description`**, which is what
2096
+ Foundry renders on the import card. A bundle is something you hand someone, so
2097
+ its prose belongs on the document itself — which is why, unlike an item, a
2098
+ bundle earns no separate documentation journal.
2099
+
2100
+ Each Adventure is written to the pack the note's `pack` names — the shared
2101
+ routing field every type uses, not one of the bundle's own — defaulting to the
2102
+ configured `Adventure` pack, conventionally `adventures`. `<system>.pack`
2103
+ overrides it for that system, as it does everywhere else.
2104
+
2105
+ **It cannot be the `adventures` companion**, though, where a repository also
2106
+ compiles map notes: that pack is written by the scenes pass, and a companion is
2107
+ written by its parent pass rather than routed to. A repository that authors
2108
+ bundles declares an Adventure pack of its own, and one that declares none is
2109
+ told so by name.
2110
+
2111
+ **A pack's `system:` constrains what its Adventures may hold.** An `Adventure`
2112
+ has no `system` field, so a bundle spanning two systems cannot be one document
2113
+ that knows it spans them: it is one Adventure per system, and the pack each is
2114
+ written to is what carries the system. A pack declaring `system: hm3` sees the
2115
+ HM3 packs and the system-neutral ones, so a member that publishes no HM3
2116
+ document is **left out rather than failing** — and named, because an installer
2117
+ that quietly ships half its contents is worse than one that fails. A pack
2118
+ declaring no system scopes nothing away, and a member it cannot find is a dead
2119
+ address.
2120
+
2121
+ **The bundles pass runs last**, after every pass producing what a bundle can
2122
+ hold — Item, Actor, JournalEntry, Macro and Scene. That ordering is derived from
2123
+ what the pass declares it reads, not from the order `packs:` happens to list, so
2124
+ an Adventure pack declared first still compiles last.
2125
+
2126
+ ### type: folder
2127
+
2128
+ Foundry's `Folder` — the grouping documents are filed in, and the last document
2129
+ this package compiled from bespoke configuration (`*-folders.yaml`, five files
2130
+ per tree) rather than from a note.
2131
+
2132
+ ```yaml
2133
+ ---
2134
+ type: folder
2135
+ shortcode: possessionscooking
2136
+ name:
2137
+ full: Cooking
2138
+ data:
2139
+ parent: possessionsmiscgear
2140
+ color: "#7a4b2a"
2141
+ ---
2142
+ ```
2143
+
2144
+ | `data` property | Values | Description |
2145
+ | --------------- | ------------------------------------------ | ------------------------------------------------------------------------------ |
2146
+ | `parent` | `WikiLink`, or a map of them keyed by pack | The folder this one sits in — one address, or one per pack. Unset at the root. |
2147
+ | `color` | `"#RRGGBB"`, a string | The folder's colour. Unset for Foundry's default. |
2148
+
2149
+ A folder is addressed `<package>-none-folder-<shortcode>` — **`none`**, because a
2150
+ `Folder` is a core Foundry document like a `JournalEntry` or a `Scene`, not a
2151
+ system's. Its shortcode is [an address segment](#the-canonical-address) like
2152
+ every other, so it is strictly alphanumeric: `possessionscooking`, never
2153
+ `possessions-cooking`, which would read as two segments and resolve to nothing.
2154
+
2155
+ **`color` must be quoted**, and YAML gives no third option: `color: #7a4b2a`
2156
+ parses as `null` (a `#` after a space opens a comment) and `color: 000000` parses
2157
+ as the number `0`. All 639 colour values across the five trees are already
2158
+ written `"#RRGGBB"`.
2159
+
2160
+ `parent` is an address, so a dangling one is an ordinary dead-address finding
2161
+ rather than a special-cased `Unknown folder id`, and a cycle is refused. Both are
2162
+ reported when the tree is read, not when something happens to reference the
2163
+ folder that carries them.
2164
+
2165
+ **`parent` may be a map keyed by pack**, because a folder's _identity_ is one
2166
+ thing and its _hierarchy_ is another. The same folder is deliberately filed
2167
+ under different parents in different packs: an item compendium is browsed by
2168
+ kind, a journal compendium is read by subject.
2169
+
2170
+ ```yaml
2171
+ data:
2172
+ parent:
2173
+ default: ~ # at the root of the items pack
2174
+ journals: descriptions # under Rules/Descriptions in the journals pack
2175
+ ```
2176
+
2177
+ `default` is every pack that is not named; an explicit `~` under a pack key means
2178
+ _at the root there_, which is a different statement from saying nothing. A plain
2179
+ scalar — the everyday spelling, and the right one wherever the hierarchies agree
2180
+ — is exactly `{ default: <value> }`. The folder keeps **one id** across every
2181
+ pack it materialises in, which is what files a documentation journal beside the
2182
+ item it describes; only its parent differs.
2183
+
2184
+ Every key but `default` names a **pack the package declares**, and one that names
2185
+ none is a finding of its own. It cannot be a harmless surplus: the compile asks
2186
+ the map for the pack it is writing and falls back to `default` when there is no
2187
+ such key, so a mistyped `journal:` files the folder wherever the default puts it
2188
+ — exactly the hierarchy the key was written to override, and silently.
2189
+
2190
+ **A folder note carries no prose.** It is structure, not content, so it produces
2191
+ no documentation journal and takes no part in `docEntryTypes`.
2192
+
2193
+ **It declares no pack.** Which packs a folder materialises in is derived from
2194
+ [what references it](#the-compendium-folder), and its ancestors materialise with
2195
+ it; a folder nothing references materialises nowhere.
2196
+
2197
+ **Its Foundry `_id` is derived from its address**, stable across runs, so a new
2198
+ folder needs no invented id. An authored `id` is kept where one is present —
2199
+ which is what lets a tree sweep its folder YAML into notes without a world that
2200
+ already holds those folders losing them. Two folders claiming one id is a build
2201
+ error.