@heroiclands/package-build 19.0.0 → 20.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 (65) hide show
  1. package/CHANGELOG.md +624 -0
  2. package/CONTENT.md +79 -8
  3. package/bin/content-build.mjs +7 -1
  4. package/content-config.mjs +10 -1
  5. package/docs/content-format.md +394 -72
  6. package/engine/actor-compiler.mjs +197 -7
  7. package/engine/address-charset.mjs +23 -5
  8. package/engine/base-compiler.mjs +63 -2
  9. package/engine/bundles.mjs +9 -0
  10. package/engine/content-address.mjs +92 -1
  11. package/engine/content-format.mjs +102 -0
  12. package/engine/content-index.mjs +11 -8
  13. package/engine/content-links.mjs +37 -21
  14. package/engine/field-reference.mjs +57 -5
  15. package/engine/field-spec.mjs +214 -7
  16. package/engine/folder-notes.mjs +24 -1
  17. package/engine/foreign-catalog.mjs +4 -1
  18. package/engine/foundry-entries.mjs +14 -0
  19. package/engine/frontmatter-lint.mjs +186 -27
  20. package/engine/frontmatter.mjs +11 -11
  21. package/engine/generate.mjs +67 -10
  22. package/engine/helpers.mjs +86 -9
  23. package/engine/index.mjs +3 -0
  24. package/engine/item-compiler.mjs +37 -0
  25. package/engine/journals.mjs +21 -4
  26. package/engine/macros.mjs +8 -0
  27. package/engine/map-notes.mjs +7 -7
  28. package/engine/note-ids.mjs +25 -1
  29. package/engine/note-vocabulary.mjs +76 -9
  30. package/engine/retired-fields.mjs +57 -16
  31. package/engine/runtime-only-fields.mjs +204 -0
  32. package/engine/scenes.mjs +12 -19
  33. package/engine/schema-check.mjs +23 -1
  34. package/engine/site-index.mjs +17 -0
  35. package/engine/subtype-registry.mjs +30 -0
  36. package/engine/system-block.mjs +81 -3
  37. package/engine/web-wikilinks.mjs +33 -27
  38. package/engine/wikilink-syntax.mjs +7 -0
  39. package/engine/wikilinks.mjs +74 -16
  40. package/hm3/actors.mjs +63 -13
  41. package/package.json +2 -2
  42. package/sohl/actors.mjs +106 -7
  43. package/sohl/item-fields.mjs +203 -0
  44. package/sohl/note-schemas.mjs +6 -3
  45. package/types/engine/actor-compiler.d.mts +83 -3
  46. package/types/engine/address-charset.d.mts +22 -4
  47. package/types/engine/base-compiler.d.mts +54 -3
  48. package/types/engine/content-address.d.mts +64 -0
  49. package/types/engine/content-format.d.mts +9 -0
  50. package/types/engine/field-spec.d.mts +271 -3
  51. package/types/engine/folder-notes.d.mts +20 -0
  52. package/types/engine/foundry-entries.d.mts +6 -0
  53. package/types/engine/frontmatter-lint.d.mts +18 -2
  54. package/types/engine/frontmatter.d.mts +11 -11
  55. package/types/engine/generate.d.mts +27 -0
  56. package/types/engine/helpers.d.mts +37 -9
  57. package/types/engine/index.d.mts +1 -0
  58. package/types/engine/map-notes.d.mts +2 -2
  59. package/types/engine/note-ids.d.mts +14 -0
  60. package/types/engine/retired-fields.d.mts +29 -13
  61. package/types/engine/runtime-only-fields.d.mts +102 -0
  62. package/types/engine/schema-check.d.mts +10 -1
  63. package/types/engine/subtype-registry.d.mts +21 -0
  64. package/types/engine/system-block.d.mts +28 -2
  65. package/types/sohl/actors.d.mts +3 -3
@@ -161,6 +161,22 @@ document rather than the note. `title` is the one field this applies to; `subTyp
161
161
  is the other declared item field spelled like a note-level key, and there the two
162
162
  levels mean the same thing by design.
163
163
 
164
+ **Some of a system's fields are runtime state, and a note may not write any of
165
+ them.** A data model declares everything a document stores, and part of that is
166
+ what _play_ writes: an affliction's `onsetDate` is the world time its onset
167
+ fired at. Writing `<system>.system.<field>` reaches such a field as directly as
168
+ any other — the block is a verbatim passthrough, and the field really is in the
169
+ schema — so until #330 a note could stamp one, and the compiled pack shipped one
170
+ world's play state to every world that installed it.
171
+
172
+ So a field the document writes for itself is **declared as such**, and the
173
+ declaration says both halves of the fact: authoring it is an error naming the
174
+ note, the key and what the field holds, and the compiled document leaves the key
175
+ out entirely so the data model's own initial value stands. This is a property of
176
+ the field rather than a list of names, so it covers every such field a system
177
+ adds later. The per-type tables below never list one — they are the vocabulary
178
+ an author writes — and each type that has any names them under its table.
179
+
164
180
  **A `WikiLink` becomes a shortcode where the target field expects one.** SoHL
165
181
  stores cross-references as shortcode strings, which is what the `Code` suffix
166
182
  marks: `data.assocSkill` is a link to a skill note, and `system.assocSkillCode`
@@ -208,6 +224,16 @@ first matching value. Where a mapping is missing for a whole class of note, that
208
224
  is a gap in the vocabulary rather than something to write into every note: an
209
225
  override that thousands of notes need is a missing subType value.
210
226
 
227
+ **A type's `subType` values are stated in one shape**, so that they can be read
228
+ and compared to the vocabulary that enforces them (#345). Under the type's
229
+ heading, write `**subType**:` on a line of its own, then one bullet per value —
230
+ `- <value>` or `- <value>: <definition>` — and nothing else between the marker
231
+ and the list. A type that has no `subType`, or whose values are not enumerated
232
+ yet, writes no marker at all. Any other spelling is a build error naming the
233
+ line: five were in use, and a reader that accepted them all would keep accepting
234
+ the sixth by reading a section as declaring nothing, which is exactly the drift
235
+ the comparison exists to catch.
236
+
211
237
  ### Mappings every type shares
212
238
 
213
239
  Eight rows were identical in all sixteen tables below, so they are stated once
@@ -219,7 +245,7 @@ type.
219
245
  | `name.full` | `name` | `name` |
220
246
  | `img` | `img` | `img` |
221
247
  | `id` | `_id` | `_id` |
222
- | `packFolder` / `folder` | `folder` | `folder` |
248
+ | `packFolder` | `folder` | `folder` |
223
249
  | `shortcode` | `system.shortcode` | NA |
224
250
  | `data.templatePriority` | `system.templatePriority` | `flags.hm3.templatePriority` |
225
251
  | `actionDefs` | `system.actionDefs` | NA |
@@ -231,6 +257,30 @@ no SoHL form, so its SoHL column is. Nothing else varies — which is why these
231
257
  rows are worth stating once: repeated sixteen times they buried the differences
232
258
  that matter.
233
259
 
260
+ **`img` maps onto a document that has one, and not every type's does.** The row
261
+ is shared because the key is legal on every note whatever its type, not because
262
+ every document carries artwork: `doc`, `place`, `lore` and `scenario` compile
263
+ into a JournalEntry, which has no image of any kind, and a `folder` compiles into
264
+ a Foundry `Folder`, which has none either. A `homepage` compiles into no
265
+ compendium document at all. On any of those the mapping has no destination, so an
266
+ authored path is simply dropped — the note validates, the tree compiles, and the
267
+ value goes nowhere.
268
+
269
+ Since a dropped value looks exactly like a value never written, the frontmatter
270
+ lint **reports it**: an `img:` (or `portrait:`) authored on a type whose passes
271
+ emit neither is a warning naming the note and the key. A warning rather than an
272
+ error, because the note still compiles correctly and the key is not certainly
273
+ unwanted — a note's top level is the generated page's front matter as well, so a
274
+ site template may read there what no document carries. `img: null` is never
275
+ reported: that is the blessed way to say "this note names no art", and on a type
276
+ with no art it is a true and harmless thing to say.
277
+
278
+ Which types those are is **not a list**. It is asked of the passes: a type routes
279
+ to a document, a document to the pass that compiles it, and each pass declares
280
+ the art it writes (`emitsArt`). A second table of iconless types would be a table
281
+ free to drift from what is actually emitted, which is the defect rather than the
282
+ check.
283
+
234
284
  Actor types (`being`, `vehicle`) add one more:
235
285
 
236
286
  | shared source | → sohl | → hm3 |
@@ -240,7 +290,20 @@ Actor types (`being`, `vehicle`) add one more:
240
290
  An actor carries `img` (its token art) and `portrait` (its sheet portrait)
241
291
  independently, which is why this is a row of its own rather than a second
242
292
  spelling of the one above. An Item has no second image, so the row applies to
243
- actor types alone.
293
+ actor types alone. Note the asymmetry in where the two are authored: `img` stays
294
+ at the note's top level and `portrait` moved under `data:`, because a note's
295
+ token art is a fact about the _note as a published artefact_ while the portrait
296
+ is a fact about the _subject_.
297
+
298
+ **A `data:` source is still read at the top level, for now.** `data:` (#128) did
299
+ not invent the facts it holds — it gathered them out of the top level, where
300
+ `portrait:` sat beside `img:` — so every key it collected has a **pre-`data:`
301
+ spelling** that is read after the declared one and reported as retiring. Write
302
+ `data.portrait`; a tree still on `portrait:` compiles to the identical document
303
+ and gets a warning naming the line, until a later release removes the position.
304
+ This is the shared level's counterpart to the in-block `<system>.<key>`
305
+ retirement, and the two are separate: a note may have moved one and not the
306
+ other.
244
307
 
245
308
  **Two of the eight are Item-only in SoHL.** `actionDefs` and `notes` are declared
246
309
  on every SoHL Item subtype and on no SoHL Actor, so on a `being` or a `vehicle`
@@ -268,6 +331,74 @@ item compiled from a template note loses the fact that it is one
268
331
  (`HeroicLands/package-build#283`). The row states the mapping the format makes;
269
332
  the gap is in the pass, not in the table.
270
333
 
334
+ #### An asset path's first segment says which package owns it
335
+
336
+ `img` and `portrait` are paths, and a path has to say **which package holds the
337
+ file** — because a module's content routinely cites the system's art, while the
338
+ system's content never cites the module's. The first segment answers that, and
339
+ there are exactly three answers:
340
+
341
+ | Authored path starts with | Owner | Emitted |
342
+ | ------------------------- | --------------------- | -------------------- |
343
+ | `systems/` | a separate **system** | unchanged |
344
+ | `modules/` | a separate **module** | unchanged |
345
+ | anything else | **this package** | `<assetRoot>/<path>` |
346
+
347
+ `<assetRoot>` is `<packageKind>/<foundryPackage>/assets`, derived from the
348
+ configuration — `systems/sohl/assets` for the system,
349
+ `modules/sohl-thalorna/assets` for that module. So one authored
350
+ `icons/relic.svg` means "my own `assets/icons/relic.svg`" in whichever package
351
+ writes it, while an authored `systems/sohl/assets/icons/noun/shield.svg` names
352
+ the system's file and is left exactly as written wherever it appears. That
353
+ second case is not hypothetical: every default this toolchain pairs with an item
354
+ type is a `systems/sohl/…` path, so a module's compiled documents carry it
355
+ verbatim.
356
+
357
+ **The third row is "anything else", not a list of directories.** It is a rule
358
+ about ownership: a package owns its whole `assets/` tree, and the directory
359
+ names inside it are that package's business. `sohl-kethira-basic` keeps art
360
+ under `assets/artwork/`, and `artwork/deity.webp` is rooted under its assets by
361
+ the same rule that roots `icons/…` and `images/…` there.
362
+
363
+ An address naming **no** package passes through untouched, which is that same
364
+ rule rather than an exception — an absolute URL, a `data:` URI and a `/`-rooted
365
+ path each already address something no package owns, so prefixing any of them
366
+ would break an address that was already correct.
367
+
368
+ `worlds/` is deliberately **not** exempt. A package may not ship art out of a
369
+ world, so a note writing one has made a mistake; prefixing it yields a plainly
370
+ broken path rather than a plausible one that 404s in Foundry with nothing
371
+ reporting it.
372
+
373
+ #### `banner:` addresses the CDN, not the Foundry install
374
+
375
+ **`banner:` is a path, and it does not follow the rule above.** It is worth
376
+ stating plainly, because the two fields look alike and a value written for one
377
+ resolves somewhere else entirely under the other.
378
+
379
+ `banner:` never reaches a compiled document — searching a built `packs-json`
380
+ tree for it turns up nothing. It is a top-level key, so it passes through to the
381
+ generated page, and its only consumer is the Hugo theme, whose
382
+ `partials/banner-url.html` applies its own rule: an absolute URL passes through,
383
+ and **anything else is prefixed with `images/`** and joined onto
384
+ `params.cdnBaseURL`. A `banner:` written to the package rule therefore resolves
385
+ to a doubled path:
386
+
387
+ ```text
388
+ banner: systems/sohl/assets/images/banners/lore.webp
389
+ → <cdnBaseURL>/images/systems/sohl/assets/images/banners/lore.webp
390
+ ```
391
+
392
+ That can be made to work by mirroring the path on the CDN, and one consumer
393
+ does exactly that — but it is not what the author meant.
394
+
395
+ **The two are not reconciled, because they are not two spellings of one thing.**
396
+ `img:` addresses a file inside a Foundry install, where the package that holds
397
+ it is the question worth asking. `banner:` addresses a file on a CDN, where
398
+ there are no packages at all. Write a `banner:` relative to the CDN's `images/`
399
+ root — `banners/lore.webp`, not `images/banners/lore.webp` and not a
400
+ package-rooted path.
401
+
271
402
  #### The pack a note compiles into
272
403
 
273
404
  `pack` names which configured compendium receives the note's document.
@@ -396,10 +527,23 @@ _id = makeId("document", "<package>-<system>-<type>-<shortcode>")
396
527
  ```
397
528
 
398
529
  — 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.
530
+ characters a Foundry id is. Nothing else feeds it.
531
+
532
+ **One type hashes under a different namespace, and a consumer must not assume
533
+ otherwise.** A `Folder` is a document of its own class, and its id is
534
+ `makeId("folder", <address>)` — a separate namespace so that a folder and an
535
+ item sharing a shortcode cannot derive one id, which Foundry would not report
536
+ because it keys folders and documents in separate collections. See
537
+ [`type: folder`](#type-folder).
538
+
539
+ **So the id is transported, not recomputed.** The content index publishes an
540
+ `id` on every entry it gives an identity to, beside the `uuid` that identity
541
+ ends in, and both are computed by whatever owns that entry's derivation — the
542
+ folder pass for a folder, the journals pass for a documentation journal, this
543
+ rule for everything else. Re-deriving one from the `canonical` key alone reads
544
+ correctly and is wrong for a folder, and wrong in a way nothing outside the
545
+ build can detect: the result is a plausible 16-character id that resolves to
546
+ nothing (#310).
403
547
 
404
548
  **Why the address and not an authored string.** A note used to declare an
405
549
  opaque 16-character `id` — 6,343 of them across the four content trees — which
@@ -409,7 +553,7 @@ every pack of a document type, which is exactly the scope a primary document's
409
553
  id must be unique within, and it said nothing at all about a duplicate `id`. So
410
554
  the derived id inherits a guard that already exists, where the authored one had
411
555
  none. It is the same principle that turned `folder: ONXsqZAIZr2qzxTb` into
412
- `packFolder: <path>` above: an opaque derived identity does not belong in
556
+ `packFolder: <address>` above: an opaque derived identity does not belong in
413
557
  authored content.
414
558
 
415
559
  **An authored `id` still wins**, and that is how a document's identity is
@@ -629,22 +773,58 @@ The full address is the unambiguous form, and almost nothing uses it — 92 of
629
773
  12,056 links in the current trees. The rest rely on shorter forms, each dropping
630
774
  segments from the left:
631
775
 
632
- | form | resolves by |
633
- | ------------------------------- | -------------------------------------------- |
634
- | `package-system-type-shortcode` | exactly, always |
635
- | `package-type-shortcode` | `(type, shortcode)` within the named package |
636
- | `type-shortcode` | `(type, shortcode)` within this package |
637
- | `shortcode` | `shortcode` within this package see below |
638
-
639
- The last row is the everyday case, and it means two different things by
640
- context. In a **frontmatter field** the type segment is supplied by the field's
641
- own declaration, so `tashal` is a complete address. In **body prose** there is no
642
- field to supply it, so a bare shortcode is resolved across types and must match
643
- exactly one; an ambiguity is an error naming the candidates.
776
+ | form | expands to |
777
+ | ------------------------------- | -------------------------------------------------------- |
778
+ | `package-system-type-shortcode` | itself |
779
+ | `system-type-shortcode` | `<this package>-system-type-shortcode` |
780
+ | `type-shortcode` | `<this package>-<this block's system>-type-shortcode` |
781
+ | `shortcode` | as above, with the type from the field's own declaration |
782
+
783
+ There is no `package-type-shortcode`: the forms are exactly the **suffixes** of
784
+ the canonical address, so naming another package means naming its system too.
785
+ `kethira-place-tashal` is three segments, which reads as system `kethira`, and
786
+ fails.
787
+
788
+ #### An omitted segment defaults from where the link is written
789
+
790
+ It is **not** a wildcard and resolution is not a search. Every short form expands
791
+ to exactly one canonical address before anything is looked up, so a lookup either
792
+ finds one entry or none — there is no candidate set, and a cross-package
793
+ ambiguity is impossible by construction.
794
+
795
+ - **package** omitted → the current package. A short address therefore names
796
+ _this_ package and never falls through to a dependency; reaching another one is
797
+ the fully qualified form's job.
798
+ - **system** omitted → **the system block the link is written under**. Anywhere
799
+ under `sohl:` is `sohl`; anywhere under `hm3:` is `hm3`; **anywhere else** —
800
+ top-level frontmatter, `data:`, and body prose — is `none`. The enclosing block
801
+ decides at any depth, so `sohl.items[3].model` and `sohl.system.body.structure`
802
+ default alike; the field has no say.
803
+
804
+ **Under `none`, a system-bearing type addresses its documentation.** A note's
805
+ `none` address _is_ its `doc<type>` journal — the Item is the one with a system —
806
+ so a prose `[[affiliation-sirvadar|Sirvadar]]` names the page, which is what
807
+ prose almost always means. A prose link that means the **Item** states the
808
+ system: `[[sohl-affiliation-sirvadar|…]]`.
809
+
810
+ Only a type whose own document carries a system is redirected this way. A
811
+ `macro` and the map types have documentation journals too, but their own
812
+ documents are core ones already at `none`, so `macro-autoattack` names the Macro
813
+ and `docmacro-autoattack` its journal — two live addresses.
814
+
815
+ **An address is lowercase throughout** (#340). Every segment — package,
816
+ system, type and shortcode alike — is `^[a-z0-9]+$`, so a capital anywhere in an
817
+ address is an error naming the lowercase form.
818
+
819
+ The shortcode was the exception until #340: it was case-sensitive and routinely
820
+ mixed (`Clb`, `LtShoe`, `HsTunic`) while the address built from it was lowercased
821
+ wholesale, so the authored name and its address disagreed. Two names differing
822
+ only in case are two names nobody can tell apart, and they collapsed onto one
823
+ address, one `_id` and one URL with nothing to report it.
644
824
 
645
825
  **Parsing is positional counting from the right, and nothing else.** Every
646
826
  segment is alphanumeric — shortcodes, **types** and **subTypes** are all
647
- `^[A-Za-z0-9]+$`, systems come from a closed registry, and `contentPackage` is
827
+ `^[a-z0-9]+$`, systems come from a closed registry, and `contentPackage` is
648
828
  alphanumeric — so the hyphen is purely a separator. There is no longest-match
649
829
  against a roster and no vocabulary check before splitting.
650
830
 
@@ -659,7 +839,7 @@ against the same constant a shortcode is checked against, and a note carrying a
659
839
  hyphenated value is reported where it wrote it:
660
840
 
661
841
  ```text
662
- Trauma/Blood_Loss.md:3:1: error: `subType` "blood-loss" is not a well-formed subType — a subType is letters and digits only (^[A-Za-z0-9]+$), the same charset a type, a shortcode and a contentPackage are held to. It is a vocabulary term the whole toolchain keys on, and one closed set away from being an address segment again, so a charset that held for every term but this one would be a rule nobody could state in a sentence
842
+ Trauma/Blood_Loss.md:3:1: error: `subType` "blood-loss" is not a well-formed subType — a subType is lowercase letters and digits only (^[a-z0-9]+$), the same charset a type, a shortcode and a contentPackage are held to. It is a vocabulary term the whole toolchain keys on, and one closed set away from being an address segment again, so a charset that held for every term but this one would be a rule nobody could state in a sentence
663
843
  ```
664
844
 
665
845
  One declared value broke the rule and has been renamed: a `doc`'s `user-guide`
@@ -672,7 +852,7 @@ No retirement-specific code outlived the sweep.
672
852
 
673
853
  That is a guarantee rather than an observation, and it holds: of **4,456 distinct
674
854
  shortcodes** across the four content trees, not one contains a character outside
675
- `[A-Za-z0-9]`. It is load-bearing, so relaxing the charset later would break
855
+ `[a-z0-9]`. It is load-bearing, so relaxing the charset later would break
676
856
  resolution with nothing to say so.
677
857
 
678
858
  `type/shortcode` with a slash is the legacy form, still resolved so links written
@@ -735,14 +915,21 @@ nowhere is a typo or an omission, and both want fixing.
735
915
  There are six ways a link can fail, and each is one **error** with one message
736
916
  wherever it is met:
737
917
 
738
- | finding | what it means | the fix |
739
- | ---------------- | ---------------------------------------------- | ----------------------------------------- |
740
- | `unlabelled` | no `\|`, so the link addresses nothing | write `[[type-shortcode\|Text]]` |
741
- | `not-an-address` | labelled, but the target is not an address | write the address, not the name |
742
- | `unknown-type` | qualified, but names no type this build knows | correct the type segment |
743
- | `unresolved` | parses as an address; nothing publishes it | fix the shortcode, or vendor the manifest |
744
- | `ambiguous` | more than one package publishes the short form | write `[[package-type-shortcode\|Text]]` |
745
- | `unknown-anchor` | the address resolves; the `#section` does not | correct the anchor |
918
+ | finding | what it means | the fix |
919
+ | ---------------- | ----------------------------------------------- | ------------------------------------------------------ |
920
+ | `unlabelled` | no `\|`, so the link addresses nothing | write `[[type-shortcode\|Text]]` |
921
+ | `not-an-address` | labelled, but the target is not an address | write the address, not the name |
922
+ | `not-lowercase` | any segment of the address is capitalised | lowercase it every segment is lowercase |
923
+ | `unknown-type` | qualified, but names no type this build knows | correct the type segment |
924
+ | `unresolved` | parses as an address; nothing publishes it | fix the shortcode, or qualify to reach another package |
925
+ | `ambiguous` | _unreachable since #336; kept for the manifest_ | |
926
+ | `unknown-anchor` | the address resolves; the `#section` does not | correct the anchor |
927
+
928
+ `ambiguous` no longer fires. An omitted segment defaults rather than wildcarding,
929
+ so a written target expands to one canonical address and a lookup returns one
930
+ entry or none — two claimants is a state the grammar can no longer reach. The
931
+ reason is retained so a consumer switching on it does not break, and because a
932
+ vendored manifest built by an older toolchain may still carry the finding.
746
933
 
747
934
  The vocabulary and the messages live in one module (`engine/wikilink-syntax.mjs`)
748
935
  precisely because an author meets whichever build ran first. Three resolvers read
@@ -813,19 +1000,6 @@ Brackets belong in prose, where a link sits inside a sentence and needs marking
813
1000
  off from the words around it. A frontmatter value has nothing to be marked off
814
1001
  from.
815
1002
 
816
- #### Not yet implemented
817
-
818
- One rule in this section is settled but unbuilt, and describes the target rather
819
- than current behaviour:
820
-
821
- - **The `<system>` segment.** `readQualifier` reads package, type and shortcode;
822
- there is no system segment. A four-segment target today parses as
823
- `package-type-shortcode` with a hyphenated shortcode, or fails.
824
- The corpus is close to the rule already. Of 12,056 links, 10,413 are
825
- `[[type-shortcode|Label]]`, which is correct as written. Two authored links use
826
- an unpiped multi-segment target, and 69 use a pipe with a note name where an
827
- address belongs; those are the migration.
828
-
829
1003
  ### What a note produces
830
1004
 
831
1005
  Note types fall into two groups, and only the first has a mapping table.
@@ -847,6 +1021,21 @@ having nothing worth summarising.
847
1021
  Every note in **both** groups still produces its JournalEntry and its web page.
848
1022
  The difference is only whether a system Actor or Item is created as well.
849
1023
 
1024
+ That has been true of actors only since #337. A being used to produce its Actor
1025
+ and nothing else, which left it the one system-bearing note with no address at
1026
+ `none` — so a prose link naming it had nowhere to land. It now carries a
1027
+ documentation journal like every other such note, addressed
1028
+ `<package>-none-docbeing-<shortcode>` beside the Actor's
1029
+ `<package>-<system>-being-<shortcode>`.
1030
+
1031
+ **A being keeps its prose inline as well.** `system.appearance` and
1032
+ `system.dossier` are still the rendered text, where an item's description is an
1033
+ `@UUID` pointer into its journal. The difference is deliberate and is about
1034
+ size: one item is embedded across hundreds of beings, so baking its description
1035
+ into every copy bloats the compendium by the length of the prose times the
1036
+ number of carriers, and the pointer buys that back. An actor is singular, so the
1037
+ same indirection would cost a reader a click and save nothing.
1038
+
850
1039
  ### One note is at most one document per system
851
1040
 
852
1041
  A note produces **at most one document in each system**. That constraint is worth
@@ -1112,6 +1301,62 @@ Generates a living (or undead, or spirit) being.
1112
1301
  | `appearance.complexion` | `string` | Complexion |
1113
1302
  | `appearance.extra_features` | `string[]` | Extra features |
1114
1303
 
1304
+ #### A being's embedded items
1305
+
1306
+ `<system>.items` is a list, and each entry compiles into one embedded Item. An
1307
+ entry takes one of two shapes.
1308
+
1309
+ **A copy of a catalogue item** names it with `model:` — an address, read by the
1310
+ same grammar every wikilink is. The entry's remaining keys are merged over the
1311
+ model, so it carries the model's values except where it says otherwise.
1312
+
1313
+ ```yaml
1314
+ sohl:
1315
+ items:
1316
+ - { model: skill-wpnc, system: { masteryLevelBase: 52 } }
1317
+ - { model: sohl-sohl-weapongear-whmr }
1318
+ - { model: sohl-sohl-weapongear-dgr, system: { shortcode: dgr2, name: Offhand dagger } }
1319
+ ```
1320
+
1321
+ | key | required | meaning |
1322
+ | -------- | -------- | ------------------------------------------------------------- |
1323
+ | `model` | yes | The address of the item this entry is a copy of |
1324
+ | `system` | no | Values that override the model's |
1325
+ | `name` | no | A name of this entry's own, where it differs from the model's |
1326
+
1327
+ **`type:` is not written beside a `model`.** The address already names the type,
1328
+ so a second statement of it is a place to be wrong, and it is refused.
1329
+
1330
+ **The address is written at whatever length says what it means.** Within this
1331
+ package `type-shortcode` is enough; reaching another package means the full
1332
+ `package-system-type-shortcode`, since the forms are suffixes of the canonical
1333
+ address. The system segment defaults from the block the entry sits in — an entry
1334
+ under `sohl.items` defaults to `sohl` — which is why the short form names an
1335
+ **Item** here while the same string in body prose names a page.
1336
+
1337
+ **A custom item** — one that copies nothing — states `name`, `type` **and**
1338
+ `system.shortcode`, all three required, plus whatever else its data model needs.
1339
+ It is written in **block form**, never inline, so the two kinds of entry are
1340
+ distinguishable at a glance:
1341
+
1342
+ ```yaml
1343
+ sohl:
1344
+ items:
1345
+ - name: Whetstone
1346
+ type: miscgear
1347
+ system:
1348
+ shortcode: whetstone
1349
+ weight: 1
1350
+ value: 5
1351
+ durability: 0
1352
+ ```
1353
+
1354
+ **A top-level `shortcode:` is retired** (#334). It selected a template, while the
1355
+ `system.shortcode` beside it was the compiled item's identity — one word for two
1356
+ things — and it could not say which package a template came from, so an address
1357
+ resolved into a dependency only because no local pack claimed it and would have
1358
+ retargeted silently the day one did.
1359
+
1115
1360
  #### Identifying a being's embedded items
1116
1361
 
1117
1362
  Each entry in `sohl.items` compiles into one embedded Item, and its `_id` is
@@ -1121,28 +1366,26 @@ derived from **what the entry is**, never from where it sits in the list:
1121
1366
  _id = makeId(<the actor's id>, "<subType>:<system.shortcode>")
1122
1367
  ```
1123
1368
 
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:
1369
+ An entry's identity is its **own `system.shortcode`**. The `model:` is not it —
1370
+ a model names the item this entry is a _copy of_, and is never written to the
1371
+ document so two entries may share one model and are then two embodiments, each
1372
+ stating its own identity:
1128
1373
 
1129
1374
  ```yaml
1130
1375
  sohl:
1131
1376
  items:
1132
- - shortcode: dgr # selects the catalogue's dagger
1133
- type: weapongear
1377
+ - model: weapongear-dgr # the catalogue's dagger
1134
1378
  name: Dagger 1
1135
1379
  system:
1136
1380
  shortcode: dgr1 # this dagger's own identity
1137
- - shortcode: dgr
1138
- type: weapongear
1381
+ - model: weapongear-dgr
1139
1382
  name: Dagger 2
1140
1383
  system:
1141
1384
  shortcode: dgr2
1142
1385
  ```
1143
1386
 
1144
1387
  **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`,
1388
+ their own `system.shortcode`, both daggers above carry the model's `dgr`,
1146
1389
  which makes them the same entity to everything that resolves by `(type,
1147
1390
  shortcode)` — compendium/world reconciliation, template shadowing, cohort
1148
1391
  membership, effect and expression references. A `name` cannot stand in: it is
@@ -1210,7 +1453,7 @@ the divine, magic, the spirit world — and that partition is load-bearing rathe
1210
1453
  than descriptive: a system filtering which mystical practices may associate with
1211
1454
  an affiliation can only be as precise as the distinction it filters on.
1212
1455
 
1213
- subType
1456
+ **subType**:
1214
1457
 
1215
1458
  - guild: A sworn association of craftsmen holding monopoly over a trade within a locality.
1216
1459
  - order: A body of members bound by vows or a rule of life to a shared purpose.
@@ -1377,7 +1620,7 @@ one, at `sohl.system.title`.
1377
1620
 
1378
1621
  Represents an affliction.
1379
1622
 
1380
- **subType**
1623
+ **subType**:
1381
1624
 
1382
1625
  - disease: A biological affliction: an illness or parasite that infects the body or mind (e.g. typhoid, tuberculosis, river blindness)
1383
1626
  - poisontoxin: A chemical affliction: a toxic substance or venom that impairs or kills the host (e.g. hemotoxin, mandrake, wasp venom)
@@ -1406,8 +1649,11 @@ Represents an affliction.
1406
1649
  | `contagionIndex` | `number` | how contagious the disease is |
1407
1650
  | `outcomeTraumas` | `SafeExpression` | Expression returning traumas that result from affliction recovery |
1408
1651
  | `onsetDurationFormula` | `RollFormula` | Formula to calculate duration until onset after contracting affliction |
1652
+ | `onsetDurationBase` | `number` | That duration in seconds, stated outright instead of rolled |
1409
1653
  | `healingCheckDurationFormula` | `RollFormula` | Formula to calculate duration until next healing check (measured from last healing check) |
1654
+ | `healingCheckDurationBase` | `number` | That duration in seconds, stated outright instead of rolled |
1410
1655
  | `resolutionDurationFormula` | `RollFormula` | Formula to calculate duration after onset to resolution |
1656
+ | `resolutionDurationBase` | `number` | That duration in seconds, stated outright instead of rolled |
1411
1657
 
1412
1658
  If `sohl` is present, this becomes an `affliction` item.
1413
1659
 
@@ -1420,8 +1666,41 @@ If `sohl` is present, this becomes an `affliction` item.
1420
1666
  | `data.contagionIndex` | `system.contagionIndexBase` | NA |
1421
1667
  | `data.outcomeTraumas` | `system.outcomeTraumas` | NA |
1422
1668
  | `data.onsetDurationFormula` | `system.onsetDurationFormula` | NA |
1669
+ | `data.onsetDurationBase` | `system.onsetDurationBase` | NA |
1423
1670
  | `data.healingCheckDurationFormula` | `system.healingCheckDurationFormula` | NA |
1671
+ | `data.healingCheckDurationBase` | `system.healingCheckDurationBase` | NA |
1424
1672
  | `data.resolutionDurationFormula` | `system.resolutionDurationFormula` | NA |
1673
+ | `data.resolutionDurationBase` | `system.resolutionDurationBase` | NA |
1674
+
1675
+ **A timed phase is a triplet, and a note writes two thirds of it.** SoHL stores
1676
+ each phase as `{…DurationFormula, …DurationBase, …Date}`, and the three are
1677
+ authored differently:
1678
+
1679
+ | third | who writes it | what it says |
1680
+ | ------------------ | ------------- | -------------------------------------------------------- |
1681
+ | `…DurationFormula` | the note | what is **rolled** to get the interval |
1682
+ | `…DurationBase` | the note | the interval **outright**, in seconds, instead of a roll |
1683
+ | `…Date` | play | _when the phase actually fired_ |
1684
+
1685
+ Write the **formula** for a phase whose length varies — `"2d6*86400"` for an
1686
+ incubation of two-to-twelve days — and the **base** for one that does not. Both
1687
+ are intervals in **seconds**, and a bare number is a valid formula, so
1688
+ `onsetDurationFormula: 86400` and `onsetDurationBase: 86400` differ only in
1689
+ whether a roll is attempted.
1690
+
1691
+ **Omitting either omits the key**, rather than writing a default over it. That
1692
+ matters because the data model's own `null` is what several phases fall back on:
1693
+ a trauma that sets no `healingCheckDuration*` takes the world's configured
1694
+ healing-check interval, and one that sets no `bloodLossAdvanceDurationBase` does
1695
+ not bleed at all. A compile-time `null` is still a value, and would answer those
1696
+ questions for every note in every world.
1697
+
1698
+ **The `…Date` third is never authored.** It records when the phase fired, which
1699
+ only play can know, so `system.contractDate`, `system.onsetDate`,
1700
+ `system.treatmentDate` and `system.resolutionDate` are runtime state and a note
1701
+ that writes one fails the build. They are world times, and `0` is a valid one,
1702
+ so there is no blank a note could write either; leave them out and the data
1703
+ model's `null` stands.
1425
1704
 
1426
1705
  ### type: armorgear
1427
1706
 
@@ -1624,7 +1903,7 @@ If an `hm3` property is present, an HM3 item is created, and `hm3.type` states w
1624
1903
 
1625
1904
  ### type: projectilegear
1626
1905
 
1627
- **subTypes**:
1906
+ **subType**:
1628
1907
 
1629
1908
  - none
1630
1909
  - arrow
@@ -1661,7 +1940,7 @@ HM3 side while remaining distinct on the SoHL side.
1661
1940
 
1662
1941
  ### type: skill
1663
1942
 
1664
- **subTypes**:
1943
+ **subType**:
1665
1944
 
1666
1945
  - social
1667
1946
  - nature
@@ -1708,17 +1987,42 @@ Note: `hm3.system.type` (skill types) use the values "Craft", "Physical", "Commu
1708
1987
  - shock: A prolonged physiological state of shock lasting hours or days, following severe trauma or blood loss — distinct from the transient combat-shock states.
1709
1988
  - coma: A prolonged state of unconsciousness.
1710
1989
 
1711
- | `data` property | Values | Description |
1712
- | ------------------ | -------- | ------------------------------------------ |
1713
- | `templatePriority` | `number` | Template priority, _null_ = not a template |
1990
+ | `data` property | Values | Description |
1991
+ | --------------------------------- | ------------- | ---------------------------------------------------------------------- |
1992
+ | `templatePriority` | `number` | Template priority, _null_ = not a template |
1993
+ | `healingCheckDurationFormula` | `RollFormula` | Formula for the interval between healing checks |
1994
+ | `healingCheckDurationBase` | `number` | That interval in seconds, stated outright instead of rolled |
1995
+ | `bloodLossAdvanceDurationFormula` | `RollFormula` | Formula for the interval between blood-loss advances |
1996
+ | `bloodLossAdvanceDurationBase` | `number` | That interval in seconds. Setting it is what makes the wound bleed |
1997
+ | `courseDurationFormula` | `RollFormula` | Formula for the interval between course tests — shock, coma, infection |
1998
+ | `courseDurationBase` | `number` | That interval in seconds, stated outright instead of rolled |
1714
1999
 
1715
2000
  if a `sohl` property is present, a SoHL item of type "trauma" will be created.
1716
2001
 
1717
2002
  If an `hm3` property is present, an HM3 item is created. `hm3.type` must be specified as either `injury` or `trait`.
1718
2003
 
1719
- | shared source | → sohl | → hm3 |
1720
- | ------------- | ---------------- | ----- |
1721
- | `subType` | `system.subType` | NA |
2004
+ | shared source | → sohl | → hm3 |
2005
+ | -------------------------------------- | ---------------------------------------- | ----- |
2006
+ | `subType` | `system.subType` | NA |
2007
+ | `data.healingCheckDurationFormula` | `system.healingCheckDurationFormula` | NA |
2008
+ | `data.healingCheckDurationBase` | `system.healingCheckDurationBase` | NA |
2009
+ | `data.bloodLossAdvanceDurationFormula` | `system.bloodLossAdvanceDurationFormula` | NA |
2010
+ | `data.bloodLossAdvanceDurationBase` | `system.bloodLossAdvanceDurationBase` | NA |
2011
+ | `data.courseDurationFormula` | `system.courseDurationFormula` | NA |
2012
+ | `data.courseDurationBase` | `system.courseDurationBase` | NA |
2013
+
2014
+ **A trauma's three timed phases follow the triplet rule** stated under
2015
+ `affliction`: write the formula or the base, both in seconds, and omitting each
2016
+ leaves the key out so the data model answers. Two of them fall back to a **world
2017
+ setting** when the trauma sets neither half — the healing check and the course —
2018
+ which is why a default written here would be wrong rather than merely
2019
+ redundant. `bloodLossAdvanceDurationBase` is the switch that makes a wound
2020
+ bleed: a trauma that sets it bleeds, one that leaves it unset does not.
2021
+
2022
+ **`system.contractDate` and `system.treatmentDate` are never authored.** They
2023
+ are the world times the injury was taken and last treated — runtime state, for
2024
+ the reason `affliction`'s four dates are — so a note that writes one fails the
2025
+ build.
1722
2026
 
1723
2027
  ### type: weapongear
1724
2028
 
@@ -1749,7 +2053,7 @@ If an `hm3` property is present, an HM3 item is created, and `hm3.type` states w
1749
2053
 
1750
2054
  In-world information about people, places, or concepts.
1751
2055
 
1752
- subType:
2056
+ **subType**:
1753
2057
 
1754
2058
  - cosmology: The structure of reality — planes, realms, creation, and the ordering of what exists.
1755
2059
  - deity: Individual gods and their attributed natures, domains, epithets, and aspects.
@@ -1765,6 +2069,12 @@ subType:
1765
2069
  - culture: A social grouping of individuals with common beliefs, mores, and values.
1766
2070
  - bestiary: A kind of creature that is not a people — beasts, monsters, and the made things
1767
2071
  that were never born. What `folk` covers for the sapient, this covers for everything else.
2072
+ - gathering: A scheduled public occasion people travel to — a tournament or martial games, a
2073
+ great market or fair, a religious festival, a ceremony or rite. What these share is assembly
2074
+ on a cycle: a place, a time, and something contested or observed. Held apart from `calendar`,
2075
+ which covers the _reckoning_ — a festival's date is calendar and the festival is not, and a
2076
+ tournament is not a matter of time-reckoning at all — and from `culture`, which is a grouping
2077
+ of people rather than an occasion they attend.
1768
2078
 
1769
2079
  | `data` property | Values | Description |
1770
2080
  | --------------- | ------ | ----------- |
@@ -1792,7 +2102,7 @@ the place from the map is the end that cannot, because a map is written once and
1792
2102
  depicts what it depicts. A place's maps are therefore derived — every map whose
1793
2103
  `place` is this one — and the relation exists in exactly one place.
1794
2104
 
1795
- subType:
2105
+ **subType**:
1796
2106
 
1797
2107
  - battlemap: Tactical scale, for a scene played out square by square.
1798
2108
  - localmap: Roughly a kilometre across — a settlement, a holding, a small valley.
@@ -1851,11 +2161,16 @@ single Level is synthesised from `img`, `overlay`, `levelName` and
1851
2161
  > system would want the identical art — so the field sits beside every other
1852
2162
  > note's `img` rather than inside a system block.
1853
2163
  >
1854
- > `image` is **retired in favour of it**, in the three steps `package:` took
1855
- > (#56), and this is the first: both spellings are read, `img` wins where a note
1856
- > carries both, and a note still writing `image` gets a located **warning**
1857
- > rather than a refusal. It compiles to the byte-identical document, so failing
1858
- > a build over it would red a tree that has done nothing wrong.
2164
+ > `image` was **retired in favour of it**, in the three steps `package:` took
2165
+ > (#56), and all three have now run
2166
+ > ([package-build#149](https://github.com/HeroicLands/package-build/issues/149)).
2167
+ > Through the window both spellings were read, `img` won where a note carried
2168
+ > both, and a note still writing `image` got a located **warning** rather than a
2169
+ > refusal — it compiled to the byte-identical document, so failing a build over
2170
+ > it would have redded a tree that had done nothing wrong. The trees were then
2171
+ > swept, and the alias dropped. `image` is now simply not a key a map has: in a
2172
+ > `sohl:` block it is reported as unknown, and either way the note is refused
2173
+ > for the `img` it never declared.
1859
2174
 
1860
2175
  **Two unit conventions, deliberately.** Geometry — walls, doors, lights, tiles,
1861
2176
  sounds, region shapes — is authored in **pixels**, Foundry's native storage,
@@ -1895,7 +2210,7 @@ three maps, and none of those maps needs to know it is a keep.
1895
2210
 
1896
2211
  ### type: place
1897
2212
 
1898
- subType:
2213
+ **subType**:
1899
2214
 
1900
2215
  - world: A self-contained whole in which places exist — a planet, plane, or realm.
1901
2216
  - region: A bounded division of a world or larger region — continents, marches, uplands, provinces.
@@ -1938,7 +2253,7 @@ place it depicts — see `type: map` below.
1938
2253
 
1939
2254
  Content prepared to be played — a situation with its cast, places, and possible outcomes.
1940
2255
 
1941
- subType:
2256
+ **subType**:
1942
2257
 
1943
2258
  - campaign: A long arc toward a goal, spanning many adventures — carries standing cast, factions, and its own timeline.
1944
2259
  - adventure: A self-contained undertaking with a specific objective, playable in a few sessions.
@@ -1957,7 +2272,7 @@ subType:
1957
2272
 
1958
2273
  ### type: doc
1959
2274
 
1960
- subType:
2275
+ **subType**:
1961
2276
 
1962
2277
  - rules: The rules of the game, independent of medium — valid at a table with paper and dice.
1963
2278
  - userguide: How to operate the Foundry implementation to play by the rules.
@@ -2198,3 +2513,10 @@ folder needs no invented id. An authored `id` is kept where one is present —
2198
2513
  which is what lets a tree sweep its folder YAML into notes without a world that
2199
2514
  already holds those folders losing them. Two folders claiming one id is a build
2200
2515
  error.
2516
+
2517
+ The derivation is `makeId("folder", "<package>-none-folder-<shortcode>")`, and
2518
+ the namespace is **`folder`**, not the `document` one [every other note hashes
2519
+ under](#the-document-id): a folder and an item sharing a shortcode would
2520
+ otherwise derive the same id, and Foundry keys the two in separate collections
2521
+ so neither would complain. The content index publishes this value, so a consumer
2522
+ reads a folder's id rather than recomputing one (#310).