@heroiclands/package-build 23.4.0 → 23.5.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 (48) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +29 -25
  3. package/assets/images/provenance.yaml +1 -0
  4. package/bin/package-build.mjs +259 -28
  5. package/ci/prettier.mjs +73 -0
  6. package/config.mjs +10 -0
  7. package/container.mjs +232 -1
  8. package/content-config.mjs +5 -1
  9. package/docs/api.md +100 -52
  10. package/docs/authoring/assets.md +16 -1
  11. package/docs/commands.md +154 -7
  12. package/docs/configuration.md +39 -2
  13. package/docs/content-format.md +30 -6
  14. package/docs/guides/e2e.md +38 -0
  15. package/docs/guides/icons.md +5 -0
  16. package/docs/guides/release-and-deploy.md +36 -1
  17. package/docs/reference/format-details.md +218 -50
  18. package/docs/reference/note-types.md +71 -71
  19. package/engine/asset-index.mjs +63 -22
  20. package/engine/asset-references.mjs +355 -0
  21. package/engine/asset-shipping.mjs +363 -0
  22. package/engine/asset-types.mjs +70 -0
  23. package/engine/base-compiler.mjs +24 -1
  24. package/engine/cdn-publish.mjs +618 -0
  25. package/engine/index.mjs +6 -0
  26. package/engine/note-vocabulary.mjs +40 -2
  27. package/engine/pathnames.mjs +21 -10
  28. package/engine/rank-ladder.mjs +40 -16
  29. package/engine/scenes.mjs +1 -1
  30. package/engine/standing-terms.mjs +3 -1
  31. package/package.json +4 -3
  32. package/packagebuild-metadata.jsonl +13 -13
  33. package/release.mjs +69 -7
  34. package/sohl/actors.mjs +12 -4
  35. package/sohl/default-item-art.mjs +23 -19
  36. package/types/config.d.mts +7 -0
  37. package/types/container.d.mts +137 -1
  38. package/types/engine/asset-index.d.mts +3 -2
  39. package/types/engine/asset-references.d.mts +136 -0
  40. package/types/engine/asset-shipping.d.mts +240 -0
  41. package/types/engine/asset-types.d.mts +36 -0
  42. package/types/engine/base-compiler.d.mts +7 -1
  43. package/types/engine/cdn-publish.d.mts +311 -0
  44. package/types/engine/index.d.mts +2 -0
  45. package/types/engine/pathnames.d.mts +2 -1
  46. package/types/engine/rank-ladder.d.mts +25 -6
  47. package/types/release.d.mts +5 -3
  48. package/types/sohl/default-item-art.d.mts +2 -1
@@ -917,7 +917,9 @@ of ownership** and every surface derives its own address from it.
917
917
 
918
918
  **The first segment names the package, when an `assets/` follows it.**
919
919
  Everything after that `assets/` is the _suffix_ — the path inside the package's
920
- shipped tree, and the one piece every derived form is built from. A pathname
920
+ `assets/` directory, and the one piece every derived form is built from. A
921
+ suffix under `icons/` or `images/` reaches Foundry and the website in the
922
+ deployed layout, by type root and basename alone. A pathname
921
923
  that does not open `<package>/assets/` belongs to the package being compiled,
922
924
  and the whole of it is the suffix.
923
925
 
@@ -931,7 +933,7 @@ ships as the Foundry module `harn-ensemble`) writing `images/map.webp`:
931
933
 
932
934
  | Surface | Address |
933
935
  | ----------- | ---------------------------------------------------------- |
934
- | **Foundry** | `modules/harn-ensemble/assets/images/map.webp` |
936
+ | **Foundry** | `modules/harn-ensemble/images/map.webp` |
935
937
  | **Local** | `assets/images/map.webp` |
936
938
  | **Web** | `https://cdn.heroiclands.org/harnensemble/images/map.webp` |
937
939
  | **Book** | `assets/images/map.webp`, staged beside the Typst source |
@@ -941,9 +943,9 @@ system ships and this repository does not hold:
941
943
 
942
944
  | Surface | Address |
943
945
  | ----------- | ------------------------------------------------------------- |
944
- | **Foundry** | `systems/sohl/assets/icons/noun/shieldnn.svg` |
946
+ | **Foundry** | `systems/sohl/icons/shieldnn.svg` |
945
947
  | **Local** | `assets/icons/noun/shieldnn.svg`, in the `sohl` repository |
946
- | **Web** | `https://cdn.heroiclands.org/sohl/icons/noun/shieldnn.svg` |
948
+ | **Web** | `https://cdn.heroiclands.org/sohl/icons/shieldnn.svg` |
947
949
  | **Book** | not carried, unless a declared replacement carries it instead |
948
950
 
949
951
  **The content package name and Foundry id are separate values.**
@@ -1863,7 +1865,7 @@ nothing about itself, compiles into no document, and publishes no page, so the
1863
1865
  | `path` | the walk | Where the file sits inside the emitting package's asset directory. |
1864
1866
  | `attribution` | provenance | The person holding the rights, to whom attribution is legally due. |
1865
1867
  | `source` | provenance | Where it came from — a URL, or a sentence. |
1866
- | `ai` | provenance | Whether the file is machine-generated — strictly the YAML boolean `true` or `false`; any other value is a finding, never a stringified record. Unstated resolves to `false`. |
1868
+ | `ai` | provenance | Whether the file is machine-generated — strictly the YAML boolean `true` or `false`; any other value is a finding, never a stringified record. Required: a record that leaves it out is a finding, never read as `false`. |
1867
1869
  | `license` | provenance | The licence it is used under — an SPDX identifier, or terms. |
1868
1870
  | `notes` | provenance | Anything else a person reading the attribution needs. |
1869
1871
  | `role` | provenance | What the picture is for — `portrait`, `emblem`, `banner`, `plate` or `map`. Absent for an ordinary picture; a value outside that set is a finding. The `image` type only — a value on an `icon` address is a finding and is dropped. |
@@ -1874,17 +1876,21 @@ Every field is present on every record, blank where nothing states one. A fixed
1874
1876
  shape is what lets a consumer read `asset.license` without first asking whether
1875
1877
  the package happened to record one.
1876
1878
 
1877
- **`path` is what makes resolution one step.** The record is emitted by the
1878
- package holding the bytes, so the path is that package's path and each consumer
1879
- joins its own root onto it:
1880
-
1881
- | Consumer | `thalorna-none-image-thorn` resolves to |
1882
- | -------- | ------------------------------------------------------------- |
1883
- | Web | `<cdn base>/thalorna/images/beings/characters/thorn.webp` |
1884
- | Book | `<asset base>/thalorna/images/beings/characters/thorn.webp` |
1885
- | Foundry | `modules/thalorna/assets/images/beings/characters/thorn.webp` |
1886
-
1887
- Foundry paths use the **Foundry package id**. The two values are both `thalorna`
1879
+ **`path` is the source path.** The record is emitted by the package holding
1880
+ the bytes, so the path is where the file sits in that package's repository. The
1881
+ book stages from it; Foundry and the website serve the **deployed layout**, the
1882
+ type root and the basename alone:
1883
+
1884
+ | Consumer | `thalorna-none-image-thorn` resolves to |
1885
+ | -------- | ----------------------------------------------------------- |
1886
+ | Web | `<cdn base>/thalorna/images/thorn.webp` |
1887
+ | Book | `<asset base>/thalorna/images/beings/characters/thorn.webp` |
1888
+ | Foundry | `modules/thalorna/images/thorn.webp` |
1889
+
1890
+ Foundry and the website serve a picture under `icons/` or `images/` flat, by
1891
+ type root and basename — see [what a package ships](#what-a-package-ships);
1892
+ every other file keeps its path under `assets/`, and the local and book forms
1893
+ keep the repository's arrangement. Foundry paths use the **Foundry package id**. The two values are both `thalorna`
1888
1894
  for this image; `harnensemble` content uses `harn-ensemble` in its Foundry paths.
1889
1895
 
1890
1896
  ##### What a role declares
@@ -1987,19 +1993,131 @@ One address resolves its record in this order:
1987
1993
  2. Otherwise the nearest `provenance.yaml`, searching the file's own directory
1988
1994
  and then its ancestors, **stopping at the type root**. A record above the root
1989
1995
  would speak for trees it says nothing about.
1990
- 3. Otherwise nothing, and the provenance fields are blank. A package that
1991
- records no attribution is a fact to state, not a walk to fail.
1996
+ 3. Otherwise nothing, and that is a **finding at the file**: every asset is
1997
+ covered by a record, because the stage decides from the record whether the
1998
+ file ships.
1999
+
2000
+ Resolution is per file. One directory routinely holds files its own
2001
+ `provenance.yaml` covers beside files a sidecar answers for, and each is
2002
+ resolved on its own.
1992
2003
 
1993
2004
  **The nearest record is the whole answer.** It is never merged with the one
1994
2005
  above it, so a record states every key it means to claim — which is what lets a
1995
2006
  reader open one file and know what the tree beneath it carries, instead of
1996
2007
  resolving a chain by hand.
1997
2008
 
1998
- That is also why `attribution` and `license` are **required in any record that
1999
- exists**: a key left out is not inherited from above, it is simply absent, so a
2000
- record stating neither leaves every file it covers with no rights holder and no
2001
- terms. Omitting one is a finding. `source`, `ai`, `notes` and `role` stay optional,
2002
- because a blank is a truthful answer for each of them.
2009
+ That is also why `attribution`, `license` and `ai` are **required in every
2010
+ record**: a key left out is not inherited from above, it is simply absent, so a
2011
+ record stating none leaves every file it covers with no rights holder, no terms,
2012
+ or no statement of whether a machine made it. Omitting one is a finding at the
2013
+ record, which an empty file is held to as well. An unstated `ai` is never read as
2014
+ `false`. `source`, `notes` and `role` stay optional, because a blank is a
2015
+ truthful answer for each of them.
2016
+
2017
+ ##### What a package ships
2018
+
2019
+ The repository keeps every picture; the Foundry package carries a filtered copy.
2020
+ `package-build assets` stages a file from `assets/icons/` or `assets/images/`
2021
+ only when its resolved record says `ai: false` **and** its `license` is one of:
2022
+
2023
+ | Licence | Spelled |
2024
+ | ---------------------------- | ---------------------------- |
2025
+ | Public domain | `LicenseRef-PublicDomain` |
2026
+ | Creative Commons Zero | `CC0-1.0`, or `CC0` |
2027
+ | Creative Commons Attribution | `CC-BY-3.0`, `CC-BY-4.0` |
2028
+ | Attribution-ShareAlike | `CC-BY-SA-4.0` |
2029
+ | Noun Project Pro | `LicenseRef-NounProject-Pro` |
2030
+
2031
+ The spelling is matched exactly; nothing is normalised. A package adds licences
2032
+ to this list with `packageBuild.shippingLicenses`, and ships under the list
2033
+ together with its additions. `ai: true` is never shipped, whatever the
2034
+ additions say. Audio is not filtered
2035
+ and not laid out flat — a package stages it with an ordinary
2036
+ `packageBuild.assets` row.
2037
+
2038
+ **The deployed layout is flat.** A shipped picture sits at the package root as
2039
+ `icons/<basename>` or `images/<basename>`, whatever directories hold it in the
2040
+ repository. The root is the type and the basename is the shortcode with its
2041
+ extension, so the path follows from the address alone, and a root's shortcodes
2042
+ being one namespace is what keeps the flattening collision-free. The same
2043
+ layout serves Foundry (`modules/thalorna/images/thorn.webp`), the website's CDN
2044
+ (`<cdn base>/thalorna/images/thorn.webp`), and every location a world copies
2045
+ the pictures to.
2046
+
2047
+ **The package's asset set**, `asset-sets/default.json` under the package
2048
+ root, lists what it ships. The name is fixed, so a world always finds the
2049
+ package's own set there:
2050
+
2051
+ ```json
2052
+ {
2053
+ "name": "Thalorna — shipped art",
2054
+ "package": "thalorna",
2055
+ "assets": [
2056
+ {
2057
+ "address": "thalorna-none-icon-anvil",
2058
+ "source": "../icons/anvil.svg",
2059
+ "target": "icons/anvil.svg",
2060
+ "size": 412
2061
+ }
2062
+ ]
2063
+ }
2064
+ ```
2065
+
2066
+ | Key | What it says |
2067
+ | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2068
+ | `name` | The set's display name: the package's manifest title with ` — shipped art` appended, or the content package without a title. |
2069
+ | `package` | The content package — the addresses' first segment, not the Foundry id. |
2070
+ | `assets` | One entry per file, in `target` order. |
2071
+ | `address` | The file's canonical address. |
2072
+ | `target` | The file's path relative to the location's root, in the deployed layout, extension included. |
2073
+ | `source` | Where the file is fetched from, resolved against the URL of the set file holding it — `new URL(source, setUrl)`. The package's own set sits in `asset-sets/`, so its `source` is `../<target>`. |
2074
+ | `size` | The staged file's size in bytes. |
2075
+
2076
+ `target` is never the record's source `path`. Each release attaches
2077
+ `icons.zip` and `images.zip` beside the package archive: one picture root of
2078
+ the stage each, with `asset-sets/default.json`, for a GM placing the files by
2079
+ hand.
2080
+
2081
+ ##### Every reference states its address
2082
+
2083
+ A compiled document names a picture by its Foundry path, and beside each
2084
+ reference it states the picture's canonical address, so a world can rebuild the
2085
+ path for wherever it keeps the file:
2086
+
2087
+ - **HTML.** Each `<img>` whose `src` names an asset carries
2088
+ `data-asset="<address>"`.
2089
+ - **Path fields.** The address sits in `flags.<system>.assets` on the document
2090
+ holding the field, as nested objects mirroring the field's path — never a
2091
+ dotted key, which Foundry's `expandObject` splits. `<system>` is the system
2092
+ the document is compiled for, its `_stats.systemId`.
2093
+
2094
+ | Document | Flag on | Keys |
2095
+ | ------------------------------------------- | ---------------------- | ------------- |
2096
+ | Actor, Item, ActiveEffect, Macro, RollTable | the document | `img` |
2097
+ | Actor token art | `prototypeToken.flags` | `texture.src` |
2098
+ | Table result | the result | `img` |
2099
+ | Journal image page | the page | `src` |
2100
+
2101
+ ```json
2102
+ "img": "modules/thalorna/icons/anvil.svg",
2103
+ "flags": { "sohl": { "assets": { "img": "thalorna-none-icon-anvil" } } },
2104
+ "prototypeToken": {
2105
+ "texture": { "src": "modules/thalorna/icons/anvil.svg" },
2106
+ "flags": { "sohl": { "assets": { "texture": { "src": "thalorna-none-icon-anvil" } } } }
2107
+ }
2108
+ ```
2109
+
2110
+ Embedded documents — an actor's items and effects, a journal's pages, an
2111
+ Adventure's contents — each carry their own. A path or an `<img>` naming no
2112
+ asset is left as it is.
2113
+
2114
+ **A picture the package does not ship draws a stand-in.** A reference to an
2115
+ excluded picture compiles to its package's own stand-in,
2116
+ `<package>-none-icon-missing` or `<package>-none-image-missing` — a picture
2117
+ under `icons/` or `images/` named `missing`, human-made and licensed to ship —
2118
+ and keeps the excluded picture's address in its flag or `data-asset`. A
2119
+ package that leaves out a picture a note references and ships no stand-in of
2120
+ that type fails the build with an error at the referencing note.
2003
2121
 
2004
2122
  ##### A declared replacement resolves first
2005
2123
 
@@ -3688,6 +3806,13 @@ tattoo, scar or brand may show gear or a gesture, as in `a tattoo of a knight's
3688
3806
  helmet`, and stands; the voice, impression, handedness, sterility and age rules
3689
3807
  hold even for a mark.
3690
3808
 
3809
+ A condition is a feature only through what it shows. Its visible sign is a mark
3810
+ — `trembling hands`, `one eye missing`, `a withered left arm` — and a skin sign
3811
+ the complexion words cover goes in `complexion`, so healed pox is `pockmarked`
3812
+ and acne scarring `scarred`. A condition with no visible sign, such as epilepsy,
3813
+ haemophilia or a parasite, stays in the being's prose and is not an appearance
3814
+ fact at all.
3815
+
3691
3816
  ```yaml
3692
3817
  data:
3693
3818
  gender: female
@@ -3735,29 +3860,44 @@ The words, and what each means:
3735
3860
  | `blue` | Blue | Unmixed blue, from pale ice to deep sea. |
3736
3861
  | `brown` | Brown | Mid-brown, the iris pattern still legible in ordinary light. |
3737
3862
  | `dark_brown` | Dark Brown | Brown so deep the iris barely separates from the pupil. |
3863
+ | `gold` | Gold | A bright metallic yellow iris, brighter and colder than `amber`. |
3738
3864
  | `grey` | Grey | Grey or grey-blue, cool and without green. |
3739
3865
  | `green` | Green | Green dominant, with no brown ring worth naming. |
3740
3866
  | `hazel` | Hazel | Brown and green together, often shifting with the light; the commonest mixed eye. |
3867
+ | `red` | Red | An iris that is red, as in some undead and elder folk. |
3868
+ | `pink` | Pink | A pale pink or rose iris, as in albinism. |
3869
+ | `silver` | Silver | A grey iris with a bright metallic sheen rather than a dull cast. |
3741
3870
  | `violet` | Violet | The pink-violet of an iris carrying no pigment. |
3871
+ | `white` | White | An iris white or near-white, so that the eye reads pale throughout. |
3872
+ | `yellow` | Yellow | A pale, flat yellow iris without the warmth of `amber`. |
3742
3873
 
3743
3874
  **`data.appearance.hair_color`**
3744
3875
 
3745
- | Word | Label | Meaning |
3746
- | ------------------ | ---------------- | ------------------------------------------------------------------------- |
3747
- | `auburn` | Auburn | Red-brown with the red dominant. |
3748
- | `black` | Black | True black, with no brown cast in sunlight. |
3749
- | `blonde` | Blonde | Light yellow, from near-white to wheat. |
3750
- | `brown` | Brown | Mid-brown, neither dark nor reddish. |
3751
- | `chestnut` | Chestnut | Mid-brown with a red cast, the brown dominant — between brown and auburn. |
3752
- | `dark_blonde` | Dark Blonde | Blonde darkened towards light brown. |
3753
- | `dark_brown` | Dark Brown | Brown approaching black, still brown in sunlight. |
3754
- | `grey` | Grey | Grey throughout, with nothing of the original colour left to read. |
3755
- | `greying_black` | Greying Black | Black still dominant, grey coming in. |
3756
- | `greying_brown` | Greying Brown | Brown still dominant, grey coming in. |
3757
- | `red` | Red | Ginger or copper, the red unmixed with brown. |
3758
- | `silver` | Silver | Grey with a bright metallic sheen rather than a dull cast. |
3759
- | `strawberry_blond` | Strawberry Blond | Pale red-gold: blonde with a red cast, lighter than red. |
3760
- | `white` | White | Wholly white, whether from age or from carrying no pigment. |
3876
+ | Word | Label | Meaning |
3877
+ | -------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
3878
+ | `auburn` | Auburn | Red-brown with the red dominant. |
3879
+ | `black` | Black | True black, with no brown cast in sunlight. |
3880
+ | `blonde` | Blonde | Light yellow, from near-white to wheat. |
3881
+ | `brown` | Brown | Mid-brown, neither dark nor reddish. |
3882
+ | `chestnut` | Chestnut | Mid-brown with a red cast, the brown dominant — between brown and auburn. |
3883
+ | `dark_blonde` | Dark Blonde | Blonde darkened towards light brown. |
3884
+ | `dark_brown` | Dark Brown | Brown approaching black, still brown in sunlight. |
3885
+ | `grey` | Grey | Grey throughout, with nothing of the original colour left to read. |
3886
+ | `greying_auburn` | Greying Auburn | Auburn still dominant, grey coming in. |
3887
+ | `greying_black` | Greying Black | Black still dominant, grey coming in. |
3888
+ | `greying_blonde` | Greying Blonde | Blonde still dominant, grey coming in. |
3889
+ | `greying_brown` | Greying Brown | Brown still dominant, grey coming in. |
3890
+ | `greying_chestnut` | Greying Chestnut | Chestnut still dominant, grey coming in. |
3891
+ | `greying_dark_blonde` | Greying Dark Blonde | Dark Blonde still dominant, grey coming in. |
3892
+ | `greying_dark_brown` | Greying Dark Brown | Dark Brown still dominant, grey coming in. |
3893
+ | `greying_light_brown` | Greying Light Brown | Light Brown still dominant, grey coming in. |
3894
+ | `light_brown` | Light Brown | Brown lightened toward dark blonde: paler than `brown`, darker than `dark_blonde`. |
3895
+ | `greying_red` | Greying Red | Red still dominant, grey coming in. |
3896
+ | `greying_strawberry_blond` | Greying Strawberry Blond | Strawberry Blond still dominant, grey coming in. |
3897
+ | `red` | Red | Ginger or copper, the red unmixed with brown. |
3898
+ | `silver` | Silver | A bright metallic pale between white and grey: the silver of great age, or the natural silver some elder folk are born with. |
3899
+ | `strawberry_blond` | Strawberry Blond | Pale red-gold: blonde with a red cast, lighter than red. |
3900
+ | `white` | White | Wholly white, whether from age or from carrying no pigment. |
3761
3901
 
3762
3902
  **`data.appearance.skin_color`**
3763
3903
 
@@ -4397,18 +4537,19 @@ a propertied or senatorial order supplies nearly everyone who holds them, it is
4397
4537
  **Democracy**.
4398
4538
 
4399
4539
  The distinction is not academic: it is the difference between a ladder whose top
4400
- rungs are a class one is born or bought into, and one whose top rungs are an
4401
- office one is voted into and then vacates. A republic's ladder therefore carries
4402
- **both** — the civic status (Citizen) and the standing in the governing body above
4403
- it (Senator) — because a citizen is not a member of the Senate, and conflating the
4404
- two is the commonest way to get a non-monarchy wrong.
4540
+ rungs are a class one is born or bought into, and a body whose highest posts are
4541
+ offices one is voted into and then vacates. A republic's ladder therefore carries
4542
+ **both** standings — the civic status (Citizen) and the standing in the governing
4543
+ body above it (Senator) — because a citizen is not a member of the Senate, and
4544
+ conflating the two is the commonest way to get a non-monarchy wrong. The
4545
+ magistracies held for a term are offices, not rungs.
4405
4546
 
4406
4547
  **Rank**
4407
4548
  Definition of a level within the organization (e.g., Priest, Layperson, Member, Gang Leader, Master, etc.)
4408
4549
 
4409
4550
  | Property | Required | Values | Description |
4410
4551
  | ------------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------ |
4411
- | `level` | yes | integer | The ranking within the affiliation, increasing values starting with 1, 0 indicates intentional exclusion (expulsion/excommunication) |
4552
+ | `level` | yes | integer | The ranking within the affiliation: 0 those it excludes (expelled, excommunicated), 1 its ordinary members, higher values above them |
4412
4553
  | `title` | yes | string | Title associated with the Rank |
4413
4554
  | `description` | yes | string | Description of the Rank |
4414
4555
  | `lore` | no | `Address` | The standing this rank _is_ — a `lore` note of subType `law`, shared with every other body that confers the same thing |
@@ -4429,11 +4570,37 @@ moving two lines renames the standing. A body that confers two titles at one
4429
4570
  standing writes them as one rung — `title: Knight or Dame` — or gives each its
4430
4571
  own level.
4431
4572
 
4573
+ **Every ladder has rung 0 and rung 1.** Level 1 is the ordinary member, and level
4574
+ 0 names those the body excludes. A body that can name its members can name those
4575
+ it shuts out, whether or not it ever does, so an affiliation whose
4576
+ `governance.ranks` lacks either is an **error** at the `ranks` key, as is one
4577
+ with no ladder at all. The level-0 title is the body's own word for the excluded
4578
+ — Outlaw, Apostate, Níðing — never a generic "Excluded".
4579
+
4580
+ Those two rungs are the minimum. Add rungs above level 1 only where the body
4581
+ truly has them, generally no more than five. **A rung is a standing, what a member
4582
+ is; an office is a post, what a member does.** Leadership is normally an office
4583
+ held by the top rank rather than a rank above it, and a rank never falls when an
4584
+ office is left: a bishop stays a bishop after being Presiding Bishop.
4585
+
4586
+ ```yaml
4587
+ data:
4588
+ governance:
4589
+ ranks:
4590
+ - { level: 0, title: Apostate, description: Put out of the communion. }
4591
+ - { level: 1, title: Layman, description: A baptised member of the faith. }
4592
+ - { level: 2, title: Priest, description: Ordained to serve a parish. }
4593
+ - { level: 3, title: Bishop, description: Consecrated to oversee a diocese. }
4594
+ offices:
4595
+ Presiding Bishop: Chairs the synod of bishops for a term.
4596
+ ```
4597
+
4432
4598
  ```yaml
4433
4599
  data:
4434
4600
  governance:
4435
4601
  ranks:
4436
4602
  - { level: 0, title: Vrystrith, description: Kinless and owed nothing. }
4603
+ - { level: 1, title: Kinsman, description: Born or adopted into a household. }
4437
4604
  - { level: 4, title: Fródrád, description: A householder whose word carries. }
4438
4605
  - level: 5
4439
4606
  title: Hárár
@@ -4469,7 +4636,7 @@ rank names the standing, and the standing says.
4469
4636
  | `symbol` | `string` | Its emblem in words: a feather atop a golden scale, a chisel carving a star |
4470
4637
  | `governance.model` | `GovernanceModel` | Type of government structure, if applicable |
4471
4638
  | `governance.summary` | `string` | summary of the governance situation |
4472
- | `governance.ranks` | `Rank[]` | The ranks available to members of the affiliation |
4639
+ | `governance.ranks` | `Rank[]` | The ranks the affiliation confers; level 0 and level 1 are required |
4473
4640
  | `governance.ranks[].level` | `integer`, required | The rung's position on this body's ladder; a member's `rank` names it |
4474
4641
  | `governance.ranks[].title` | `string`, required | What the standing is called |
4475
4642
  | `governance.ranks[].description` | `string`, required | What the standing is |
@@ -4519,14 +4686,15 @@ the ranks and offices that exist, and a membership names which of them it holds.
4519
4686
  **Rank is the organisation's, not the member's.** A bare `level: 4` on a
4520
4687
  membership says nothing on its own; it means _Knight_ only because the polity
4521
4688
  declared that rung. So the ladder is authored once, on the body that confers it,
4522
- and a member's rank is an index into it. Level 0 is reserved for the excluded —
4523
- outlawed, expelled, excommunicated — which is a standing the organisation still
4524
- recognises, and so still has to define.
4689
+ and a member's rank is an index into it. Level 0 is the excluded — outlawed,
4690
+ expelled, excommunicated — which is a standing the organisation still
4691
+ recognises, and so every ladder defines it beside level 1.
4525
4692
 
4526
4693
  **Offices are named, not ranked.** A Chancellor and a Marshal are both great
4527
4694
  officers and neither is above the other, so an office is a key with a
4528
4695
  description rather than a rung — and a being may hold an office at any rank, or a
4529
- rank with no office at all.
4696
+ rank with no office at all. The head of a body is usually an office of this kind,
4697
+ held by a member of the top rank, who keeps that rank when the term ends.
4530
4698
 
4531
4699
  An office may remain a string description or declare a `description` and a
4532
4700
  `holders` list. Each holder names a `being` Address; optional `start` and `end`