@heroiclands/package-build 22.19.0 → 22.21.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.
@@ -62,7 +62,6 @@
62
62
  pass, which reads them exactly as it reads a local pack.
63
63
 
64
64
  Three things it does deliberately:
65
-
66
65
  - **Pins, rather than following `latest`.** A published
67
66
  `releases/latest/download/…` URL is rewritten to the declared
68
67
  `compatibility.verified` version, so a build names one particular dependency
@@ -177,7 +176,6 @@
177
176
 
178
177
  `content-build lint` now checks frontmatter as well as addresses. Five classes,
179
178
  each previously reported somewhere other than where it was made, or not at all:
180
-
181
179
  - **Unknown or retired type**, told what replaced it.
182
180
  - **Missing required property** — `dimensions` on a map, `subType` on a skill.
183
181
  - **Wrong value shape** — `weight: heavy` where a number belongs.
@@ -215,7 +213,6 @@
215
213
  configures (#69).
216
214
 
217
215
  Two new commands:
218
-
219
216
  - `content-build format [paths..] [--write]` — Prettier, with the shared
220
217
  configuration.
221
218
  - `content-build markdown [paths..] [--fix]` — markdownlint, with a narrow,
@@ -310,7 +307,6 @@ version` regenerates it in every repository here.
310
307
  entries) exactly as the scripts it replaces emit them, on the same toolchain.
311
308
 
312
309
  Also new:
313
-
314
310
  - `paths.manifestOut` (default `build/manifests`) — where the manifest is
315
311
  written. Deliberately not `paths.manifests`, which is the _inbound_ directory
316
312
  of vendored foreign manifests that `links` consumes.
@@ -377,7 +373,6 @@ compile`. Publishing the _same tree_ as a website was a script each consumer
377
373
  exotic, so neither is left to care.
378
374
 
379
375
  Also new:
380
-
381
376
  - `engine/site-build.mjs` exports each stage (`collectContentPages`,
382
377
  `collectTreePages`, `siteGates`, `renderPages`, `writeSectionLandings`) for a
383
378
  consumer that needs a step rather than the whole command.
@@ -422,7 +417,6 @@ compile`. Publishing the _same tree_ as a website was a script each consumer
422
417
  stale by `--check` on every clean checkout — the two undoing each other forever.
423
418
  Three things were making that happen, and all three are fixed at the source
424
419
  rather than by adding a formatting pass:
425
-
426
420
  - Table columns are padded to their widest cell, which is what Prettier's
427
421
  alignment comes to for this content.
428
422
  - The worked example's fence said `yaml`, but the block is a whole note —
@@ -632,7 +626,6 @@ compile`. Publishing the _same tree_ as a website was a script each consumer
632
626
  hide the next drift instead of surfacing it.
633
627
 
634
628
  Two deliberate differences from the code it replaces:
635
-
636
629
  - **The `corpus` derivation is dropped.** `corpus` is not a registered item
637
630
  type, so nothing can compile to one and the branch matched nothing — the same
638
631
  class of dead code as the gate that caused the bug. It was in SoHL's copy and
@@ -654,7 +647,6 @@ compile`. Publishing the _same tree_ as a website was a script each consumer
654
647
  `gh release create`, because cutting the Release is what published. Nothing
655
648
  enforced the last step, so on 2026-08-21 `main` carried 0.5.1 while npm served
656
649
  0.4.0: two versions merged and never published, with no check red.
657
-
658
650
  - Every pull request now declares its bump as a `.changeset/*.md` file, and CI's
659
651
  **Changeset declared** job fails one that does not. `npx changeset add --empty`
660
652
  is how a change says it needs no release — explicitly, rather than by omission.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ **Standing in a body** — A being's `affiliations` is now a map keyed by the body, and each entry says what the being is there: `rank`, a level on that body's own ladder, and `office`, one of the posts it names. A page prints the standing closing on the body it belongs to — "War Chief, Hárár, of Vrystwald Tribes" — and an office links to the description the body already carries. In SoHL a being now arrives carrying one affiliation item per body it belongs to, standing included.
8
+
9
+ **Offices read the right way round** — An affiliation's offices each take their own row, with the post's name as the label and its description beside it, and each row can be linked to directly.
10
+
11
+ ### Patch Changes
12
+
13
+ - Icons drawn with outlines follow the colour scheme instead of keeping a black rim in dark mode.
14
+ - An explicitly black outline also moves to ink in light mode, matching the icon's filled shapes.
15
+
16
+ ## 22.20.0
17
+
18
+ ### Minor Changes
19
+
20
+ - Content checks require characters and NPCs to name an archetype and reject unrecognized archetypes; creatures may omit them.
21
+ - Names with unsupported fields now fail validation, and aliases appear beneath the name in infoboxes across published formats.
22
+
3
23
  ## 22.19.0
4
24
 
5
25
  ### Minor Changes
@@ -7,7 +7,11 @@ import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { formatGenerated } from "../engine/format-generated.mjs";
9
9
  import { loadContentFormat } from "../engine/content-format.mjs";
10
- import { NOTE_VOCABULARY, SHARED_DATA_FIELDS } from "../engine/note-vocabulary.mjs";
10
+ import {
11
+ BEING_ARCHETYPES,
12
+ NOTE_VOCABULARY,
13
+ SHARED_DATA_FIELDS,
14
+ } from "../engine/note-vocabulary.mjs";
11
15
 
12
16
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13
17
  const output = path.join(root, "docs/reference/note-types.md");
@@ -104,3 +108,29 @@ if (process.argv.includes("--check")) {
104
108
  } else {
105
109
  process.stdout.write(result);
106
110
  }
111
+
112
+ const detailsPath = path.join(root, "docs/reference/format-details.md");
113
+ const details = fs.readFileSync(detailsPath, "utf8");
114
+ if (!/<!-- archetypes:start -->[\s\S]*?<!-- archetypes:end -->/.test(details)) {
115
+ throw new Error("The archetype reference requires its generation markers");
116
+ }
117
+ const archetypes = Object.entries(BEING_ARCHETYPES)
118
+ .map(([name, meaning]) => `- ${name}: ${meaning}`)
119
+ .join("\n");
120
+ const updatedDetails = await formatGenerated(
121
+ details.replace(
122
+ /<!-- archetypes:start -->[\s\S]*?<!-- archetypes:end -->/,
123
+ `<!-- archetypes:start -->\n${archetypes}\n<!-- archetypes:end -->`,
124
+ ),
125
+ detailsPath,
126
+ );
127
+ if (process.argv.includes("--check")) {
128
+ if (details !== updatedDetails) {
129
+ console.error(
130
+ "docs/reference/format-details.md: error: run node ci/content-format-reference.mjs to refresh the archetype reference",
131
+ );
132
+ process.exitCode = 1;
133
+ }
134
+ } else {
135
+ fs.writeFileSync(detailsPath, updatedDetails);
136
+ }
package/docs/api.md CHANGED
@@ -353,6 +353,7 @@ An addressed content note accepts exactly these top-level keys, in this order: `
353
353
  | ------------------------- | -------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------- |
354
354
  | `DRAFT_TAG` | `const DRAFT_TAG` | — | naming the declared tag that marks a note as unfinished |
355
355
  | `GM_TAG` | `const GM_TAG` | — | naming the declared tag that marks a GM-only note |
356
+ | `BEING_ARCHETYPES` | `const BEING_ARCHETYPES` | `Readonly<Record<string, string>>` | looking up the case-sensitive being archetypes and their meanings |
356
357
  | `NOTE_VOCABULARY` | `const NOTE_VOCABULARY` | — | looking up, per content type, its `data:` field list and its closed `subType` values |
357
358
  | `SHARED_DATA_FIELDS` | `const SHARED_DATA_FIELDS` | `readonly DataFieldSpec[]` | reading the `data:` keys every type accepts, whatever it is — the art slots legal everywhere |
358
359
  | `DECLARED_TAGS` | `const DECLARED_TAGS` | — | looking up the tags a note type may declare, grouped |
@@ -1287,15 +1288,15 @@ The content tree, built into a book. The I/O half of the PDF surface: it reads t
1287
1288
 
1288
1289
  Journals pack compiler — produces JSON pack files for the "journals" Foundry compendium from markdown notes in the `assets/content/` tree. The content root (`contentBase`) is walked recursively; any `.md` file whose frontmatter declares either `type: doc` or a **doc-carrying type** ({@link sohl.utils.packs.docEntryTypes} — every item type, plus `macro`) is compiled into one JournalEntry document. Each note's body is split on top-level H1 headings; the optional content before the first H1 becomes a lead page, and each subsequent H1 starts a new page named after its heading text. All page bodies are rendered to HTML.
1289
1290
 
1290
- | Export | Signature | Returns | Use it when |
1291
- | --------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
1292
- | `splitPages` | `function splitPages(body, leadName` | — | Splits a markdown body into pages by top-level H1 headings. |
1293
- | `assertUniquePages` | `function assertUniquePages(rawPages, noteName)` | — | Two pages in one note that would derive the same id, which the LevelDB packer reports only as an opaque duplicate-key collision. |
1294
- | `assertUniqueAnchors` | `function assertUniqueAnchors(rawPages, noteName)` | — | The anchor half of {@link assertUniquePages}, under its former name. |
1295
- | `journalPageId` | `function journalPageId(entryId, page)` | {string} A 16-character Foundry id. | The id of one page within its entry. |
1296
- | `buildPages` | `function buildPages(rawPages, entryId, noteName)` | {Array<{_id: string, name: string, type: string, title: {show: boolean, level: number}, text: {format: number, content: string}, _key: string}>} The page documents, in order. | Compile split pages into JournalEntryPage documents. |
1297
- | `buildJournalEntry` | `buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats })` | {object} The JournalEntry document, keyed for the pack. | Assemble one JournalEntry document from a note's converted markdown. |
1298
- | `Journals` | `class Journals extends BasePackCompiler` | — | not called directly — imported and driven by `engine/generate.mjs` as the "journals" pack compiler |
1291
+ | Export | Signature | Returns | Use it when |
1292
+ | --------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
1293
+ | `splitPages` | `function splitPages(body, leadName` | — | Splits a markdown body into pages by top-level H1 headings. |
1294
+ | `assertUniquePages` | `function assertUniquePages(rawPages, noteName)` | — | Two pages in one note that would derive the same id, which the LevelDB packer reports only as an opaque duplicate-key collision. |
1295
+ | `assertUniqueAnchors` | `function assertUniqueAnchors(rawPages, noteName)` | — | The anchor half of {@link assertUniquePages}, under its former name. |
1296
+ | `journalPageId` | `function journalPageId(entryId, page)` | {string} A 16-character Foundry id. | The id of one page within its entry. |
1297
+ | `buildPages` | `function buildPages(rawPages, entryId, noteName)` | {Array<{\_id: string, name: string, type: string, title: {show: boolean, level: number}, text: {format: number, content: string}, \_key: string}>} The page documents, in order. | Compile split pages into JournalEntryPage documents. |
1298
+ | `buildJournalEntry` | `buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats })` | {object} The JournalEntry document, keyed for the pack. | Assemble one JournalEntry document from a note's converted markdown. |
1299
+ | `Journals` | `class Journals extends BasePackCompiler` | — | not called directly — imported and driven by `engine/generate.mjs` as the "journals" pack compiler |
1299
1300
 
1300
1301
  ### `engine.macros`
1301
1302
 
@@ -1360,7 +1361,7 @@ Bundles, as notes — the Foundry `Adventure` a `type: bundle` note compiles int
1360
1361
  | `BUNDLE_TYPE` | `const BUNDLE_TYPE` | — | The note type a bundle is authored as. |
1361
1362
  | `ADVENTURE_CONTENT_FIELD` | `const ADVENTURE_CONTENT_FIELD` | — | Foundry's `Adventure.contentFields`, keyed by the document class each holds. |
1362
1363
  | `CONTENTS_FIELD` | `const CONTENTS_FIELD` | — | The `data:` key a bundle lists its members under. |
1363
- | `stripAdventureKeys` | `function stripAdventureKeys(value)` | {*} The same shape with every `_key` removed. | Strip the LevelDB keys from a document tree. |
1364
+ | `stripAdventureKeys` | `function stripAdventureKeys(value)` | {\*} The same shape with every `_key` removed. | Strip the LevelDB keys from a document tree. |
1364
1365
  | `bareAddress` | `function bareAddress(value)` | {string\|null} The bare address, or `null` for a blank entry. | An authored address with any wikilink brackets and label stripped. |
1365
1366
  | `bundleContents` | `function bundleContents(fm)` | {string[]} The bare addresses. | The addresses a bundle note names, in the order it names them. |
1366
1367
  | `missingMemberVerdict` | `function missingMemberVerdict(packSystem)` | {"omit"\|"fail"} What to do about a member the sources lack. | What a pass should do about a member its sources do not hold. |
@@ -21,7 +21,9 @@ data: {}
21
21
  ---
22
22
  ```
23
23
 
24
- `name.full` is the displayed name; `name.aliases` helps search. `description` is the short page summary. `tags` hold draft state and descriptive labels. The note type's classification belongs in `subType` where it is declared. `data` holds facts about the subject shared by systems. A YAML comment is the place for an author-only note: commented values remain valid YAML but do not enter the content index or generated documents.
24
+ `name` is a map with a required, nonempty `full` display name. Every note may also use `aliases` for an ordered list of other names. The note infobox lists aliases directly beneath the full name. For a `being` with `subType: character`, `given` and `clan` are optional name components. The first alias of a character or NPC is its nickname. Omit optional keys when they do not apply; `given`, `clan`, and alias entries must be nonempty strings when present. No other keys are accepted under `name`.
25
+
26
+ `description` is the short page summary. `tags` hold draft state and descriptive labels. The note type's classification belongs in `subType` where it is declared. `data` holds facts about the subject shared by systems. A YAML comment is the place for an author-only note: commented values remain valid YAML but do not enter the content index or generated documents.
25
27
 
26
28
  Use `tags: [gm]` for a note intended only for the GM. It is absent from the public website and book. Foundry includes it only when its document routes to a pack with `private: true`. A note with `gm` that also creates a prose JournalEntry needs a private JournalEntry pack. Links from untagged notes to GM notes are errors; GM notes may link to one another.
27
29
 
@@ -117,7 +117,8 @@ Item link.
117
117
  order** — the very table each `### type:` section below states. There is no
118
118
  second list, so a key added to a type appears in the book, on the website and
119
119
  in a compendium journal with no further edit. A `Name` row stands first on
120
- every note, because every note has one.
120
+ every note, because every note has one. An `Aliases` row follows it when the
121
+ note names any aliases, preserving their authored order.
121
122
 
122
123
  A few keys carry no row, and the reason in each case is one of exactly two —
123
124
  the key is **machinery**, steering a build or an interface rather than
@@ -333,6 +334,13 @@ sohl: {}
333
334
  Any other top-level key is an error. To keep an author-only note beside the
334
335
  content, comment out its YAML lines with `#`. Comments remain valid YAML text
335
336
  and do not become data in the JSONL index, generated pages, or Foundry documents.
337
+
338
+ `name` is a map with a required nonempty `full` string when present. It may
339
+ also contain `aliases`, an ordered list of nonempty strings. A being with
340
+ `subType: character` may additionally contain nonempty `given` and `clan`
341
+ strings. Other keys under `name` are errors. For a character or NPC, the first
342
+ alias is its nickname. A being's homes belong in `data.homes`.
343
+
336
344
  There is no custom site metadata. `description` supplies the page's short
337
345
  description; `tags` carries publication and classification tags.
338
346
 
@@ -2432,6 +2440,8 @@ For JournalEntries, the following rules apply:
2432
2440
 
2433
2441
  **Archetypes** describe characters in broad terms and help match them to an adventure. The list is:
2434
2442
 
2443
+ <!-- archetypes:start -->
2444
+
2435
2445
  - warrior: Can hold a line and win a fight.
2436
2446
  - skirmisher: Fights light — ambush, missile, mobility.
2437
2447
  - infiltrator: Gets in unseen — locks, stealth, disguise.
@@ -2448,6 +2458,10 @@ For JournalEntries, the following rules apply:
2448
2458
  - entertainer: Performs for an audience through acting, music, comedy, or similar arts.
2449
2459
  - guildsperson: Has professional training, standing, or connections that open doors.
2450
2460
 
2461
+ <!-- archetypes:end -->
2462
+
2463
+ `character` and `npc` beings require at least one archetype. `creature` beings may omit archetypes or write an empty array. Values use the exact lowercase spellings above.
2464
+
2451
2465
  Note that archetypes are descriptive, not proscriptive, and a character may be described by multiple archetypes at once.
2452
2466
  `commoner` stands alone: a being with another fitting archetype does not also carry `commoner`.
2453
2467
  Guild membership alone does not require `guildsperson`; the being's training, standing, or connections must matter to the role.
@@ -2462,34 +2476,34 @@ An Item can mark its readable description with `# ... {#description}`. That head
2462
2476
 
2463
2477
  Generates a living (or undead, or spirit) being.
2464
2478
 
2465
- | `data` property | Values | Description |
2466
- | --------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------ |
2467
- | `tokenIcon` | `Address` | What a token on the canvas wears — an `icon` address; defaults to `icon` |
2468
- | `templatePriority` | `number` | Template priority, _null_ = not a template |
2469
- | `archetypes` | `Archetype[]` | What sort of character this is. **Always an array** — `[]` where none apply; `null` is an error. |
2470
- | `occupation` | `string` | Name of the character's occupation |
2471
- | `social` | `Map<string, unknown>` | The being's social profile |
2472
- | `stations` | `Address[]` | Name of the stations the character belongs to |
2473
- | `lore` | `Address[]` | Other lore concerning this being, such as social standing or law |
2474
- | `culture` | `Address` | Primary culture, naming a lore note with `subType: culture` |
2475
- | `homes` | `Address[]` | Place the being calls home |
2476
- | `affiliations` | `Address[]` | Affilliations (e.g., arcane/divine traditions, polities, etc) |
2477
- | `socialTies` | `Map<Address, Tie>` | Defining support and opposition, directed from this being to others |
2478
- | `gender` | `male \| female \| other` | Gender of the character |
2479
- | `species` | `Address` | Being's species (lore) |
2480
- | `born` | `date \| unknown` | When the being was born; absent, it was never born |
2481
- | `calendar` | `Address` | Calendar note used to print the being's dates |
2482
- | `died` | `date \| unknown` | When the being died; absent, it is alive |
2483
- | `age` | `34 \| ~34` | Age in years, stated only to override what `born` says; `~` marks an estimate |
2484
- | `ageYears` | `number` | Written by the compiler beside an `age` estimate — the `~` stripped; a note never authors this |
2485
- | `height` | `1.91m \| 6' 3"` | Height in metres or feet and inches |
2486
- | `weight` | `85kg \| 187 lbs` | Body weight in kilograms or pounds |
2487
- | `frame` | `scant \| light \| medium \| large \| massive` | Relative frame size |
2488
- | `appearance.eye_color` | `string` | Eye color |
2489
- | `appearance.hair_color` | `string` | Hair color |
2490
- | `appearance.skin_color` | `string` | Skin color |
2491
- | `appearance.complexion` | `string` | Complexion |
2492
- | `appearance.extra_features` | `string[]` | Extra features |
2479
+ | `data` property | Values | Description |
2480
+ | --------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
2481
+ | `tokenIcon` | `Address` | What a token on the canvas wears — an `icon` address; defaults to `icon` |
2482
+ | `templatePriority` | `number` | Template priority, _null_ = not a template |
2483
+ | `archetypes` | `Archetype[]` | What sort of character this is. **Always an array** — character and npc require at least one; creature may omit it or write `[]`; `null` is an error. |
2484
+ | `occupation` | `string` | Name of the character's occupation |
2485
+ | `social` | `Map<string, unknown>` | The being's social profile |
2486
+ | `stations` | `Address[]` | Name of the stations the character belongs to |
2487
+ | `lore` | `Address[]` | Other lore concerning this being, such as social standing or law |
2488
+ | `culture` | `Address` | Primary culture, naming a lore note with `subType: culture` |
2489
+ | `homes` | `Address[]` | Place the being calls home |
2490
+ | `affiliations` | `Map<Address, Membership>` | Bodies the being belongs to, keyed by Address, each entry the standing it holds there |
2491
+ | `socialTies` | `Map<Address, Tie>` | Defining support and opposition, directed from this being to others |
2492
+ | `gender` | `male \| female \| other` | Gender of the character |
2493
+ | `species` | `Address` | Being's species (lore) |
2494
+ | `born` | `date \| unknown` | When the being was born; absent, it was never born |
2495
+ | `calendar` | `Address` | Calendar note used to print the being's dates |
2496
+ | `died` | `date \| unknown` | When the being died; absent, it is alive |
2497
+ | `age` | `34 \| ~34` | Age in years, stated only to override what `born` says; `~` marks an estimate |
2498
+ | `ageYears` | `number` | Written by the compiler beside an `age` estimate — the `~` stripped; a note never authors this |
2499
+ | `height` | `1.91m \| 6' 3"` | Height in metres or feet and inches |
2500
+ | `weight` | `85kg \| 187 lbs` | Body weight in kilograms or pounds |
2501
+ | `frame` | `scant \| light \| medium \| large \| massive` | Relative frame size |
2502
+ | `appearance.eye_color` | `string` | Eye color |
2503
+ | `appearance.hair_color` | `string` | Hair color |
2504
+ | `appearance.skin_color` | `string` | Skin color |
2505
+ | `appearance.complexion` | `string` | Complexion |
2506
+ | `appearance.extra_features` | `string[]` | Extra features |
2493
2507
 
2494
2508
  Author `data.height` as a string of decimal metres (`1.91m` or `1.91 m`) or
2495
2509
  whole feet with optional inches (`6'`, `6'3"`, or `6' 3"`). Inches must be
@@ -2503,15 +2517,60 @@ describe a being's body. Item gear weight remains a numeric `data.weight`.
2503
2517
  `data.culture` names the culture in which a character belongs. It is one
2504
2518
  Address to a `type: lore`, `subType: culture` note. A named character with a
2505
2519
  known culture states it here; generic beings and templates can omit it.
2506
- `data.lore` holds other lore concerning the being, such as social standing or
2507
- law. Record the primary culture in `data.culture` only.
2520
+ `data.lore` holds the law the being lives under, the customs it is subject to
2521
+ and the traditions it was raised in. Record the primary culture in
2522
+ `data.culture` only, and a standing in `data.affiliations`.
2508
2523
 
2509
2524
  ```yaml
2510
2525
  data:
2511
2526
  culture: lore-vedyariclt
2512
- lore: [lore-commonerrnk]
2527
+ lore: [lore-marchlaw]
2528
+ ```
2529
+
2530
+ #### The bodies a being belongs to, and its standing in each
2531
+
2532
+ **Membership**: `{rank: number, office: string}`, both optional.
2533
+
2534
+ `data.affiliations` is a **map keyed by the body's Address**, and each entry is
2535
+ the standing the being holds in that body. The key is what makes the pair
2536
+ expressible: a being in two bodies holds a standing in each, and two bodies may
2537
+ call a rung the same word.
2538
+
2539
+ An entry carries two optional keys and nothing else:
2540
+
2541
+ - **`rank`** — a **number**, the level on that body's own `governance.ranks`
2542
+ ladder. The body declares `{level, title, description}` for every rung, so a
2543
+ number indexes into it and the body stays the single source for what its
2544
+ level 5 is called.
2545
+ - **`office`** — a **string**, matched against the keys of that body's
2546
+ `governance.offices` map.
2547
+
2548
+ An entry with neither is valid and is the ordinary shape: it says the being
2549
+ belongs to the body and nothing more.
2550
+
2551
+ ```yaml
2552
+ data:
2553
+ affiliations:
2554
+ affiliation-vrystwldtrbs: { rank: 5, office: War Chief }
2555
+ affiliation-greenwardens: { rank: 1 }
2556
+ affiliation-silverguild: {}
2513
2557
  ```
2514
2558
 
2559
+ Both values are checked against the body the entry is keyed by. A `rank` that
2560
+ is not a level the body confers and an `office` that is not one of its keys are
2561
+ each an error naming the file, line and column — so a renamed rung or a renamed
2562
+ post fails on every being pointing at it. Naming one body twice, by its short
2563
+ and full Address, still names one body and is an error.
2564
+
2565
+ The infobox prints the standing closing on its body — `War Chief, Hárár, of
2566
+ Vrystwald Tribes` — and links an office to the description the body itself
2567
+ declares, so recording the post needs no note about the post.
2568
+
2569
+ For SoHL, a being embeds one affiliation item per entry: `rank` **is** that
2570
+ item's `system.level` and `office` its `system.office`. A standing is a fact
2571
+ about the setting rather than about one system, so the `sohl:` block states
2572
+ none of it.
2573
+
2515
2574
  #### The people who matter to a being
2516
2575
 
2517
2576
  `data.socialTies` records relationships that define the subject's dossier, not
@@ -2544,8 +2603,8 @@ an error.
2544
2603
  Each tie is one-sided. The target need not name the subject back, even when
2545
2604
  `patron` and `dependent` would describe opposite ends of the same support.
2546
2605
  Kinship, household membership, organizational membership, rank, and office are
2547
- distinct facts. Their relevant effects may appear as ties, while their details
2548
- belong in prose, `data.affiliations`, or governance fields. A tie has no
2606
+ distinct facts. Their relevant effects may appear as ties, while a membership
2607
+ and the standing held in it belong in `data.affiliations`. A tie has no
2549
2608
  mechanical modifier.
2550
2609
 
2551
2610
  #### When a being was born, when it died, and how old it is
@@ -30,34 +30,34 @@ These fields are accepted by every note type. A field's value can still be irrel
30
30
 
31
31
  **Subtypes:** `npc`, `character`, `creature`.
32
32
 
33
- | Field | Shape | Meaning |
34
- | -------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
- | `data.social` | map | The being's social profile. |
36
- | `data.tokenIcon` | an Address | What a token on the canvas wears — an `icon` address; unset, it follows `icon`. |
37
- | `data.templatePriority` | number | Template priority; unset means the note is not a template. |
38
- | `data.archetypes` | list | Archetypal behaviours the being fits. |
39
- | `data.occupation` | string | What the being does for a living. |
40
- | `data.stations` | list of Addresses | Stations the being holds. |
41
- | `data.lore` | list of Addresses | Lore concerning this being, such as the standing it holds or the law it lives under. |
42
- | `data.culture` | an Address | The being's primary culture, as a culture lore note. |
43
- | `data.homes` | list of Addresses | Places the being calls home. |
44
- | `data.affiliations` | list of Addresses | Affiliations the being belongs to — traditions, polities, and the rest. |
45
- | `data.socialTies` | a map keyed by Address | Defining ties directed from this being to others: `patron` (Supports the subject from greater power, wealth, or status.); `friend` (Supports the subject out of goodwill.); `dependent` (Relies on the subject's support or protection.); `acquaintance` (Knows the subject without a strong disposition.); `rival` (Opposes the subject without implacable hostility.); `nemesis` (Opposes the subject personally and implacably.) |
46
- | `data.gender` | string | `male`, `female` or `other`. |
47
- | `data.species` | an Address | The being's species, as a lore note. |
48
- | `data.born` | string | When the being was born — a date, or `unknown` where the birth is unrecorded. Absent, the being was never born. |
49
- | `data.calendar` | an Address | Calendar used to display this being's dates. |
50
- | `data.died` | string | When the being died — a date, or `unknown` where the death is unrecorded. Absent, the being is alive. |
51
- | `data.age` | string | Age in years, stated only to override what `born` says — `34`, or `~34` for an estimate. Unstated beside a dated `born` it is computed; unstated beside an unknown or absent `born` the age is unknown. |
52
- | `data.ageYears` | number | Written by the compiler beside an `age` estimate — the `~` stripped, the magnitude alone. A note never authors this. |
53
- | `data.height` | metres or feet and inches | Height in metres or feet and inches. |
54
- | `data.weight` | kilograms or pounds | Body weight in kilograms or pounds. |
55
- | `data.frame` | string | Relative frame — `scant`, `light`, `medium`, `large` or `massive`. |
56
- | `data.appearance.eye_color` | string | Eye colour. |
57
- | `data.appearance.hair_color` | string | Hair colour. |
58
- | `data.appearance.skin_color` | string | Skin colour. |
59
- | `data.appearance.complexion` | string | Complexion. |
60
- | `data.appearance.extra_features` | list | Anything else a stranger would notice. |
33
+ | Field | Shape | Meaning |
34
+ | -------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
35
+ | `data.social` | map | The being's social profile. |
36
+ | `data.tokenIcon` | an Address | What a token on the canvas wears — an `icon` address; unset, it follows `icon`. |
37
+ | `data.templatePriority` | number | Template priority; unset means the note is not a template. |
38
+ | `data.archetypes` | list | Archetypal behaviours the being fits. |
39
+ | `data.occupation` | string | What the being does for a living. |
40
+ | `data.stations` | list of Addresses | Stations the being holds. |
41
+ | `data.lore` | list of Addresses | Lore concerning this being — the law it lives under, the customs it is subject to, the traditions it was raised in. |
42
+ | `data.culture` | an Address | The being's primary culture, as a culture lore note. |
43
+ | `data.homes` | list of Addresses | Places the being calls home. |
44
+ | `data.affiliations` | a standing map keyed by Address, or a list of Addresses | Bodies the being belongs to, keyed by Address, each entry holding the standing it holds there — `rank`, a level on that body's own ladder, and `office`, a post that body names. |
45
+ | `data.socialTies` | a map keyed by Address | Defining ties directed from this being to others: `patron` (Supports the subject from greater power, wealth, or status.); `friend` (Supports the subject out of goodwill.); `dependent` (Relies on the subject's support or protection.); `acquaintance` (Knows the subject without a strong disposition.); `rival` (Opposes the subject without implacable hostility.); `nemesis` (Opposes the subject personally and implacably.) |
46
+ | `data.gender` | string | `male`, `female` or `other`. |
47
+ | `data.species` | an Address | The being's species, as a lore note. |
48
+ | `data.born` | string | When the being was born — a date, or `unknown` where the birth is unrecorded. Absent, the being was never born. |
49
+ | `data.calendar` | an Address | Calendar used to display this being's dates. |
50
+ | `data.died` | string | When the being died — a date, or `unknown` where the death is unrecorded. Absent, the being is alive. |
51
+ | `data.age` | string | Age in years, stated only to override what `born` says — `34`, or `~34` for an estimate. Unstated beside a dated `born` it is computed; unstated beside an unknown or absent `born` the age is unknown. |
52
+ | `data.ageYears` | number | Written by the compiler beside an `age` estimate — the `~` stripped, the magnitude alone. A note never authors this. |
53
+ | `data.height` | metres or feet and inches | Height in metres or feet and inches. |
54
+ | `data.weight` | kilograms or pounds | Body weight in kilograms or pounds. |
55
+ | `data.frame` | string | Relative frame — `scant`, `light`, `medium`, `large` or `massive`. |
56
+ | `data.appearance.eye_color` | string | Eye colour. |
57
+ | `data.appearance.hair_color` | string | Hair colour. |
58
+ | `data.appearance.skin_color` | string | Skin colour. |
59
+ | `data.appearance.complexion` | string | Complexion. |
60
+ | `data.appearance.extra_features` | list | Anything else a stranger would notice. |
61
61
 
62
62
  **System mappings**
63
63
 
@@ -1133,8 +1133,8 @@ export class BasePackCompiler {
1133
1133
  // - `aliases:`: it fed the alias index, which the bare
1134
1134
  // `[[Alias]]` form was looked up in; the form is retired, so the
1135
1135
  // list has no reader left. The nested `name.aliases` is a
1136
- // different field and is **not** refused — it is reserved, and
1137
- // deliberately neither read nor validated.
1136
+ // different field: the infobox displays it, and the note name
1137
+ // check validates it.
1138
1138
  //
1139
1139
  // Both are reported and counted — never skipped, which is how a
1140
1140
  // tree naming a package nothing answers to used to compile zero