@heroiclands/package-build 23.2.1 → 23.3.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 23.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ **Being notes**
8
+
9
+ - A being's eye, hair and skin colour and its complexion now take words from a fixed list, alongside gender and frame, and any other value fails `package-build lint` naming the accepted words.
10
+ - Variant spellings and empty values are refused too: write `grey` rather than `gray`, and leave a field out when it is unrecorded.
11
+ - A complexion takes one word or a list of them; distinguishing marks stay free text in `extra_features`.
12
+ - The format reference lists every word with what it means.
13
+
14
+ ### Patch Changes
15
+
16
+ A region can list its settlements in a table under `## Settlements` — name, market, people and holder, one row each — tagged with how its countryside is held: `held`, `endowed`, `registered`, `kin` or `polis`. `package-build lint` checks the table's columns, its market classes and figures, that a holder suits the region's model, and that every linked row agrees with the settlement's own note.
17
+
18
+ `e2e` now says when the Foundry licence is unsigned, and how to sign it, instead of timing out waiting for a world.
19
+
20
+ ## 23.2.2
21
+
22
+ ### Patch Changes
23
+
24
+ A book author can list leading words such as "Kingdom of" or "Ritual:" under `pdf.indexHeads`, and the index also files those notes under the rest of the name, so "Kingdom of Targud" is found under Targud.
25
+
26
+ A book whose Typst is killed, most often by the memory limit, now fails naming the signal instead of an empty reason, and a Typst that exits silently reports its exit code.
27
+
3
28
  ## 23.2.1
4
29
 
5
30
  ### Patch Changes
@@ -18,6 +18,7 @@ import { formatGenerated } from "../engine/format-generated.mjs";
18
18
  import { loadContentFormat } from "../engine/content-format.mjs";
19
19
  import {
20
20
  BEING_ARCHETYPES,
21
+ BEING_LOOK_VOCABULARY,
21
22
  NOTE_TOP_LEVEL_FIELDS,
22
23
  NOTE_VOCABULARY,
23
24
  SHARED_DATA_FIELDS,
@@ -236,20 +237,42 @@ const details = fs.readFileSync(detailsPath, "utf8");
236
237
  if (!/<!-- archetypes:start -->[\s\S]*?<!-- archetypes:end -->/.test(details)) {
237
238
  throw new Error("The archetype reference requires its generation markers");
238
239
  }
240
+ if (!/<!-- being-look:start -->[\s\S]*?<!-- being-look:end -->/.test(details)) {
241
+ throw new Error("The being-look reference requires its generation markers");
242
+ }
239
243
  const archetypes = Object.entries(BEING_ARCHETYPES)
240
244
  .map(([name, meaning]) => `- ${name}: ${meaning}`)
241
245
  .join("\n");
246
+ // One table per field of a being's look, every word with its label and meaning,
247
+ // written from the declaration the lint closes each field to.
248
+ const beingLook = Object.entries(BEING_LOOK_VOCABULARY)
249
+ .flatMap(([field, words]) => [
250
+ `**\`data.${field}\`**`,
251
+ "",
252
+ "| Word | Label | Meaning |",
253
+ "| --- | --- | --- |",
254
+ ...words.map(
255
+ ({ value, label, describe }) => `| \`${value}\` | ${cell(label)} | ${cell(describe)} |`,
256
+ ),
257
+ "",
258
+ ])
259
+ .join("\n");
242
260
  const updatedDetails = await formatGenerated(
243
- details.replace(
244
- /<!-- archetypes:start -->[\s\S]*?<!-- archetypes:end -->/,
245
- `<!-- archetypes:start -->\n${archetypes}\n<!-- archetypes:end -->`,
246
- ),
261
+ details
262
+ .replace(
263
+ /<!-- archetypes:start -->[\s\S]*?<!-- archetypes:end -->/,
264
+ `<!-- archetypes:start -->\n${archetypes}\n<!-- archetypes:end -->`,
265
+ )
266
+ .replace(
267
+ /<!-- being-look:start -->[\s\S]*?<!-- being-look:end -->/,
268
+ `<!-- being-look:start -->\n\n${beingLook}\n<!-- being-look:end -->`,
269
+ ),
247
270
  detailsPath,
248
271
  );
249
272
  if (process.argv.includes("--check")) {
250
273
  if (details !== updatedDetails) {
251
274
  console.error(
252
- "docs/reference/format-details.md: error: run node ci/content-format-reference.mjs to refresh the archetype reference",
275
+ "docs/reference/format-details.md: error: run node ci/content-format-reference.mjs to refresh the archetype and being-look references",
253
276
  );
254
277
  process.exitCode = 1;
255
278
  }
@@ -776,6 +776,7 @@ const PDF_KEYS = [
776
776
  "unprinted",
777
777
  "coverage",
778
778
  "indexSeeAlso",
779
+ "indexHeads",
779
780
  "copyright",
780
781
  "license",
781
782
  ];
@@ -1920,6 +1921,17 @@ function normalizePdf(value, rootDir, contentPackage) {
1920
1921
  fail("pdf.copyright", "must be one line — the licence statement is the one that runs on");
1921
1922
  }
1922
1923
 
1924
+ /** @type {string[]} */
1925
+ const indexHeads = [];
1926
+ if (input.indexHeads !== undefined) {
1927
+ if (!Array.isArray(input.indexHeads)) fail("pdf.indexHeads", "must be a list of strings");
1928
+ input.indexHeads.forEach((head, i) => {
1929
+ const field = `pdf.indexHeads[${i}]`;
1930
+ if (typeof head !== "string" || !head.trim()) fail(field, "must be a non-empty string");
1931
+ indexHeads.push(head);
1932
+ });
1933
+ }
1934
+
1923
1935
  return Object.freeze({
1924
1936
  books,
1925
1937
  out: input.out === undefined ? "" : requireNonEmptyString(input.out, "pdf.out"),
@@ -1939,6 +1951,9 @@ function normalizePdf(value, rootDir, contentPackage) {
1939
1951
  // Whether an index names a note another book prints, as
1940
1952
  // `Name: Book title, p. N`.
1941
1953
  indexSeeAlso: optionalBoolean(input.indexSeeAlso, "pdf.indexSeeAlso", false),
1954
+ // Leading words an index inverts: `Kingdom of ` files `Kingdom of
1955
+ // Targud` again as `Targud, Kingdom of`.
1956
+ indexHeads: Object.freeze(indexHeads),
1942
1957
  copyright: copyright.trim(),
1943
1958
  license:
1944
1959
  input.license === undefined ?
package/docs/api.md CHANGED
@@ -323,29 +323,30 @@ The note types the engine itself declares — vocabulary that is a fact about th
323
323
 
324
324
  An addressed content note accepts exactly these top-level keys, in this order: `shortcode`, `name`, `type`, `subType`, `description`, `tags`, `data`, `dnd5e`, `hm3`, `sohl`. A key outside this list is an error at its own line. The system blocks are one per system the address vocabulary recognizes, sorted by name after the fixed keys, so a further system is a change to `SYSTEM_IDS` alone. No dnd5e-specific document types are declared. Within that shape, the closed vocabularies are the `data:` container and each type's `subType`.
325
325
 
326
- | Export | Signature | Returns | Use it when |
327
- | ------------------------- | -------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------- |
328
- | `DRAFT_TAG` | `const DRAFT_TAG` | — | naming the declared tag that marks a note as unfinished |
329
- | `GM_TAG` | `const GM_TAG` | — | naming the declared tag that marks a GM-only note |
330
- | `BEING_ARCHETYPES` | `const BEING_ARCHETYPES` | `Readonly<Record<string, string>>` | looking up the case-sensitive being archetypes and their meanings |
331
- | `NOTE_VOCABULARY` | `const NOTE_VOCABULARY` | — | looking up, per content type, its `data:` field list and its closed `subType` values |
332
- | `NOTE_TOP_LEVEL_FIELDS` | `const NOTE_TOP_LEVEL_FIELDS` | `readonly TopLevelFieldSpec[]` | reading the top-level keys a note accepts, in formatted order, with what each one means |
333
- | `NOTE_TOP_LEVEL_KEYS` | `const NOTE_TOP_LEVEL_KEYS` | `readonly string[]` | reading just the key names, in the order the formatter writes them |
334
- | `NOTE_TOP_LEVEL_KEY_SET` | `const NOTE_TOP_LEVEL_KEY_SET` | `ReadonlySet<string>` | testing whether a top-level key is one the note format declares |
335
- | `SHARED_DATA_FIELDS` | `const SHARED_DATA_FIELDS` | `readonly DataFieldSpec[]` | reading the `data:` keys every type accepts, whatever it is — the art slots legal everywhere |
336
- | `EVENTS_FIELD` | `const EVENTS_FIELD` | `DataFieldSpec` | reading the one `events` declaration that `lore`, `place` and `affiliation` share, with its check |
337
- | `DECLARED_TAGS` | `const DECLARED_TAGS` | — | looking up the tags a note type may declare, grouped |
338
- | `declaredTags` | `declaredTags(type, groups)` | `readonly string[]` | reading the declared tags a note of a type may carry, flattened |
339
- | `applicableTagGroups` | `applicableTagGroups(type, groups)` | `object[]` | reading the declared tag groups that apply to a note type |
340
- | `exclusiveTagGroups` | `exclusiveTagGroups(type, groups)` | `Array<{slot, tags}>` | reading single-valued tag slots declared for a note type |
341
- | `hasTag` | `hasTag(fm, tag)` | `boolean` | checking whether a note carries a given tag, whatever scalar-or-list form it was authored in |
342
- | `isDraftNote` | `isDraftNote(fm)` | `boolean` | checking whether a note is tagged as an unfinished draft |
343
- | `isGmNote` | `isGmNote(fm)` | `boolean` | checking whether a note is restricted to GM-facing output |
344
- | `subTypeCharsetMessage` | `subTypeCharsetMessage(value)` | `string` | building the message for a `subType` outside the address charset |
345
- | `typeCharsetMessage` | `typeCharsetMessage(type)` | `string` | building the message for a `type` outside the address charset |
346
- | `assertVocabularyCharset` | `assertVocabularyCharset(vocabulary, where)` | throws | refusing a vocabulary declaration whose type or subType breaks the address charset |
347
- | `dataFields` | `dataFields(type, vocabulary)` | `readonly DataFieldSpec[] \| undefined` | looking up the `data:` keys a note type may carry |
348
- | `subTypes` | `subTypes(type, vocabulary)` | `readonly string[] \| null \| undefined` | looking up the closed `subType` values a note type declares |
326
+ | Export | Signature | Returns | Use it when |
327
+ | ------------------------- | -------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
328
+ | `DRAFT_TAG` | `const DRAFT_TAG` | — | naming the declared tag that marks a note as unfinished |
329
+ | `GM_TAG` | `const GM_TAG` | — | naming the declared tag that marks a GM-only note |
330
+ | `BEING_ARCHETYPES` | `const BEING_ARCHETYPES` | `Readonly<Record<string, string>>` | looking up the case-sensitive being archetypes and their meanings |
331
+ | `BEING_LOOK_VOCABULARY` | `const BEING_LOOK_VOCABULARY` | `Readonly<Record<string, readonly LookWord[]>>` | reading the closed words a being's gender, frame and appearance take, each with its label and meaning, keyed by `data:` field |
332
+ | `NOTE_VOCABULARY` | `const NOTE_VOCABULARY` | — | looking up, per content type, its `data:` field list and its closed `subType` values |
333
+ | `NOTE_TOP_LEVEL_FIELDS` | `const NOTE_TOP_LEVEL_FIELDS` | `readonly TopLevelFieldSpec[]` | reading the top-level keys a note accepts, in formatted order, with what each one means |
334
+ | `NOTE_TOP_LEVEL_KEYS` | `const NOTE_TOP_LEVEL_KEYS` | `readonly string[]` | reading just the key names, in the order the formatter writes them |
335
+ | `NOTE_TOP_LEVEL_KEY_SET` | `const NOTE_TOP_LEVEL_KEY_SET` | `ReadonlySet<string>` | testing whether a top-level key is one the note format declares |
336
+ | `SHARED_DATA_FIELDS` | `const SHARED_DATA_FIELDS` | `readonly DataFieldSpec[]` | reading the `data:` keys every type accepts, whatever it is — the art slots legal everywhere |
337
+ | `EVENTS_FIELD` | `const EVENTS_FIELD` | `DataFieldSpec` | reading the one `events` declaration that `lore`, `place` and `affiliation` share, with its check |
338
+ | `DECLARED_TAGS` | `const DECLARED_TAGS` | — | looking up the tags a note type may declare, grouped |
339
+ | `declaredTags` | `declaredTags(type, groups)` | `readonly string[]` | reading the declared tags a note of a type may carry, flattened |
340
+ | `applicableTagGroups` | `applicableTagGroups(type, groups)` | `object[]` | reading the declared tag groups that apply to a note type |
341
+ | `exclusiveTagGroups` | `exclusiveTagGroups(type, groups)` | `Array<{slot, tags}>` | reading single-valued tag slots declared for a note type |
342
+ | `hasTag` | `hasTag(fm, tag)` | `boolean` | checking whether a note carries a given tag, whatever scalar-or-list form it was authored in |
343
+ | `isDraftNote` | `isDraftNote(fm)` | `boolean` | checking whether a note is tagged as an unfinished draft |
344
+ | `isGmNote` | `isGmNote(fm)` | `boolean` | checking whether a note is restricted to GM-facing output |
345
+ | `subTypeCharsetMessage` | `subTypeCharsetMessage(value)` | `string` | building the message for a `subType` outside the address charset |
346
+ | `typeCharsetMessage` | `typeCharsetMessage(type)` | `string` | building the message for a `type` outside the address charset |
347
+ | `assertVocabularyCharset` | `assertVocabularyCharset(vocabulary, where)` | throws | refusing a vocabulary declaration whose type or subType breaks the address charset |
348
+ | `dataFields` | `dataFields(type, vocabulary)` | `readonly DataFieldSpec[] \| undefined` | looking up the `data:` keys a note type may carry |
349
+ | `subTypes` | `subTypes(type, vocabulary)` | `readonly string[] \| null \| undefined` | looking up the closed `subType` values a note type declares |
349
350
 
350
351
  ### `engine.infobox`
351
352
 
@@ -507,6 +508,19 @@ What trade a settlement supports (`data.market`): the six-step scale, stated onc
507
508
  | `marketClass` | `marketClass(value)` | `object\|undefined` | the class a value names, for rendering what a number means |
508
509
  | `checkMarket` | `checkMarket(note)` | `object[]` | linting a place's `market` — a value off the scale, located at the value that states it |
509
510
 
511
+ ### `engine.settlementTables`
512
+
513
+ A region's settlements table — a markdown table under `## Settlements` on a `place` note, one row per settlement with its market, its people and its holder — held to its declared columns, to the holding model the region's tags name, and to the notes its rows link. `package-build lint` runs it as part of the `place` check; a note with no such table is not read.
514
+
515
+ | Export | Signature | Returns | Use it when |
516
+ | ------------------------ | ---------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
517
+ | `HOLDING_MODELS` | `const HOLDING_MODELS` | `readonly object[]` | the holding models in order, each its `tag`, the `holder` its Held by cell names, and whether a `person` may hold |
518
+ | `SETTLEMENT_COLUMNS` | `const SETTLEMENT_COLUMNS` | `readonly object[]` | the table's columns in header order, each its `name` and whether it is `required` |
519
+ | `SETTLEMENTS_HEADING` | `const SETTLEMENTS_HEADING` | `string` | the level-two heading text a settlements table stands under |
520
+ | `SETTLEMENT_TABLE_RULES` | `const SETTLEMENT_TABLE_RULES` | `readonly object[]` | the rules in order, each its `name` — the prefix its findings carry — and what it `describe`s |
521
+ | `findSettlementTables` | `findSettlementTables(raw)` | `{ heading: number, tables: object[] }` | reading a note's settlements tables, each with its header, its rows' cells and their 1-based lines and columns |
522
+ | `checkSettlementTables` | `checkSettlementTables(note, { index })` | `object[]` | linting a place note's settlements tables against the corpus the index holds |
523
+
510
524
  ### `engine.mapPlaces`
511
525
 
512
526
  The places a map is drawn from — this package's index records and every fetched dependency's entries, read through one shape — and what their `parents` say: the world, the continents, each place's continent, and everything the containment tree can get wrong, reported as findings located at the entry that states it.
@@ -35,6 +35,8 @@ The [note-type reference](../reference/note-types.md) lists every field accepted
35
35
 
36
36
  A being's `data.archetypes` names the roles it can fill in an adventure. Choose every specific role that fits, such as `entertainer` for a performer or `guildsperson` for someone whose professional training or connections matter. Use `[commoner]` when no specific archetype fits. `commoner` cannot share the list with another value. The [archetype reference](../reference/format-details.md#actors) defines the available roles.
37
37
 
38
+ A being's look is stated in a closed vocabulary. `data.gender` and `data.frame` take one word each, as do `data.appearance.eye_color`, `hair_color` and `skin_color`; `data.appearance.complexion` takes one word or a list of them. A value outside a field's list — a variant spelling such as `gray`, a word from another field's list, or an empty string — is an error naming the words the field takes. Leave a field out when the fact is unrecorded. `data.appearance.extra_features` is a free-text list for the marks no word covers. The [look reference](../reference/format-details.md#a-beings-look) gives every word and what it means.
39
+
38
40
  A `sohl:`, `hm3:` or `dnd5e:` block holds values for that Foundry system. Its `system:` child contains values sent to the system's DataModel. The system block can override shared routing and art where the declared field permits it. `data.pack` and `data.packFolder` are shared routes; a system-specific route belongs in that system block. The [detailed mapping reference](../reference/format-details.md#mappings-every-type-shares) and [system-block explanation](../reference/format-details.md#frontmatter-has-three-regions) cover precedence and emitted fields.
39
41
 
40
42
  One body describes the subject for both systems. A being's `{#appearance}` and `{#dossier}` sections feed SoHL Actor `system.appearance` and `system.dossier`, and HM3 Actor `system.description` and `system.biography`. An Item's `{#description}` page becomes a shared JournalEntryPage. Its resolved `@UUID[...]` pointer is stored in SoHL Item `system.docHtml` and HM3 Item `system.description`. An Item with no `{#description}` anchor uses its first page. Other H1 pages remain linkable.
@@ -1161,6 +1161,7 @@ The keys of `pdf` itself are shared by every book:
1161
1161
  | `pdf.unprinted` | string (`WHERE` clause) | no | `""` |
1162
1162
  | `pdf.coverage` | `warning` \| `error` \| `off` | no | `warning` |
1163
1163
  | `pdf.indexSeeAlso` | boolean | no | `false` |
1164
+ | `pdf.indexHeads` | string[] | no | `[]` — none inverted |
1164
1165
  | `pdf.copyright` | string (one line) | no | `""` |
1165
1166
  | `pdf.license` | string (one or more lines) | no | `""` |
1166
1167
 
@@ -1185,6 +1186,15 @@ The keys of `pdf` itself are shared by every book:
1185
1186
  `Name: _Book title_, p. N` with the page the note's entry starts on in its
1186
1187
  home book. It is `false` by default, and the index then lists only the notes
1187
1188
  the book is home to. See [The index](guides/book.md#the-index).
1189
+ - `pdf.indexHeads` lists leading words that open many names, such as
1190
+ `Kingdom of ` or `Ritual:`. Each is matched literally against the start of
1191
+ a note's `name.full`, so a head that ends in a space includes it. A note
1192
+ whose name opens with one is also filed in the index under the rest of its
1193
+ name, followed by the head without its trailing space or colon: `Kingdom of
1194
+ Targud` is also listed as `Targud, Kingdom of`, with the same pages as the
1195
+ note's own entry. Where several heads match, the longest applies. A blank
1196
+ entry, or a value that is not a list of strings, is an error at the offending
1197
+ key. See [The index](guides/book.md#the-index).
1188
1198
  - A note several books select prints in each of them. Its **home book** is the
1189
1199
  first book in `pdf.books` that selects it; a note a book selects twice is
1190
1200
  still one book's, and prints at each position. After the books are built the
@@ -90,6 +90,10 @@ The [first-note guide](authoring/first-note.md) shows the build commands and how
90
90
 
91
91
  Place notes may declare a governing affiliation with `data.government`, or explicit `null` for complete anarchy. Positive population with an omitted government produces an advisory. See [place governments](authoring/frontmatter.md#place-governments).
92
92
 
93
+ A region may list its settlements in a table under `## Settlements` — name, market, people and holder, one row each — tagged with the holding model it is written in. A row links the settlement's note where one exists, and `package-build lint` holds the two to agreement. See [a region's settlements table](reference/format-details.md#a-regions-settlements-table).
94
+
95
+ A being states its gender, frame and appearance in words from one closed vocabulary: `data.gender`, `data.frame`, and `data.appearance.eye_color`, `hair_color`, `skin_color` and `complexion` each take only the words listed for them, and any other value is an error at the value. `data.appearance.extra_features` stays free text. See [a being's look](reference/format-details.md#a-beings-look).
96
+
93
97
  Places, affiliations, lore and beings may name their culture in `data.culture`, and a `doc` of `subType: settingguide` must. A place or affiliation that names none inherits the culture of its parents. See [culture in the index](reference/format-details.md#culture-in-the-index).
94
98
 
95
99
  Blockquote alerts use `[!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, or `[!]`, with optional attributes after the marker. They replace the former info and warn div fences; secret blocks remain supported. See [alerts](authoring/links-and-markup.md#alerts).
@@ -409,9 +409,19 @@ notes the book prints.
409
409
  | Term | Source | Shown as |
410
410
  | ------------------------ | --------------------- | ---------------------------------------------------------------------------------- |
411
411
  | A note's name | `name.full` | the page of the note's entry in **bold**, then the pages of every authored mention |
412
+ | A name without its head | `name.full` | the same pages as the note's entry, filed as `Targud, Kingdom of` |
412
413
  | An alias | `name.aliases` | _see_ the note's name |
413
414
  | An event's in-world name | `data.events[].names` | _see_ the name of the note that holds the event |
414
415
 
416
+ A name that opens with a generic word a reader would not look under, such as
417
+ `Kingdom of Targud` or `Ritual: Blessing`, is also filed under the rest of the
418
+ name when [`pdf.indexHeads`](../configuration.md) lists that opening: with
419
+ `indexHeads: ["Kingdom of ", "Ritual:"]` the index lists `Targud, Kingdom of`
420
+ and `Blessing, Ritual`, each with the pages of the note's entry. No alias is
421
+ needed to find the note under its distinctive word. An inverted entry that would
422
+ print the same as another note's term is told apart by the note's subtype, then
423
+ its parent place, as any two equal terms are.
424
+
415
425
  To get a term into the index, give the note a `name.full`, add an alias or an
416
426
  event name, or link to the note from prose with a wikilink. A wikilink to a note
417
427
  the book prints adds the page it appears on. A term mentioned several times on
@@ -91,7 +91,14 @@ image made for that build. The first run downloads the release, which needs
91
91
  `FOUNDRYVTT_CACHE`, or in `container_cache` beneath the data root when that is
92
92
  unset. Once the archive is cached and the stage has a licence, later runs
93
93
  start without signing in to foundryvtt.com, so its rate limit never stops a
94
- run. Under rootless Docker, set
94
+ run. A key alone does not sign the licence. The image applies
95
+ `FOUNDRYVTT_TEST_LICENSE_KEY` and nothing more; the signature comes from
96
+ accepting the licence agreement in Foundry's own page, once per data root, and
97
+ then persists in `Config/license.json` there. On a fresh data root, start the
98
+ stage with no world (`package-build container test start`), open it in a
99
+ browser and accept the agreement. A run against an unsigned licence fails at
100
+ once saying so, naming the data root, instead of waiting for a world that never
101
+ activates. Under rootless Docker, set
95
102
  `FOUNDRYVTT_CONTAINER_USER=0:0`: container root is then the host user, so every
96
103
  file under the data root belongs to them, where the image would otherwise run
97
104
  Foundry as user 1000. With the local settings in place, seed the world:
@@ -3505,7 +3505,7 @@ See [a being's subtype describes the subject](#a-beings-subtype-describes-the-su
3505
3505
  | `affiliations.<Address>.office` | `string` | A post that body names in its `governance.offices`, where the being holds one |
3506
3506
  | `affiliations.<Address>.societies` | `string[]` | The societies the membership is held in, by name; SoHL carries the first as `system.society` |
3507
3507
  | `socialTies` | `Map<Address, Tie>` | Defining support and opposition, directed from this being to others |
3508
- | `gender` | `male \| female \| other` | Gender of the character |
3508
+ | `gender` | `male \| female \| nonbinary \| none \| other` | Gender of the being — see [a being's look](#a-beings-look) |
3509
3509
  | `species` | `Address` | Being's species (lore) |
3510
3510
  | `born` | `date \| unknown` | When the being was born; absent, it was never born |
3511
3511
  | `calendar` | `Address` | Calendar note that prints the being's dates |
@@ -3514,12 +3514,12 @@ See [a being's subtype describes the subject](#a-beings-subtype-describes-the-su
3514
3514
  | `ageYears` | `number` | Written by the compiler beside an `age` estimate — the `~` stripped; a note never authors this |
3515
3515
  | `height` | `1.91m \| 6' 3"` | Height in metres or feet and inches |
3516
3516
  | `weight` | `85kg \| 187 lbs` | Body weight in kilograms or pounds |
3517
- | `frame` | `scant \| light \| medium \| large \| massive` | Relative frame size |
3518
- | `appearance.eye_color` | `string` | Eye color |
3519
- | `appearance.hair_color` | `string` | Hair color |
3520
- | `appearance.skin_color` | `string` | Skin color |
3521
- | `appearance.complexion` | `string` | Complexion |
3522
- | `appearance.extra_features` | `string[]` | Extra features |
3517
+ | `frame` | `scant \| light \| medium \| heavy \| massive` | Relative frame — see [a being's look](#a-beings-look) |
3518
+ | `appearance.eye_color` | a word | Eye colour — see [a being's look](#a-beings-look) |
3519
+ | `appearance.hair_color` | a word | Hair colour — see [a being's look](#a-beings-look) |
3520
+ | `appearance.skin_color` | a word | Skin colour — see [a being's look](#a-beings-look) |
3521
+ | `appearance.complexion` | `word \| word[]` | The skin's condition, one word or several — see [a being's look](#a-beings-look) |
3522
+ | `appearance.extra_features` | `string[]` | Anything else a stranger would notice, in free text |
3523
3523
 
3524
3524
  Author `data.height` as a string of decimal metres (`1.91m` or `1.91 m`) or
3525
3525
  whole feet with optional inches (`6'`, `6'3"`, or `6' 3"`). Inches must be
@@ -3543,6 +3543,142 @@ data:
3543
3543
  lore: [lore-marchlaw]
3544
3544
  ```
3545
3545
 
3546
+ #### A being's look
3547
+
3548
+ **A being's gender, frame and appearance are stated in words from one closed
3549
+ vocabulary**, so every package describes a face in the same terms and a
3550
+ misspelt or invented word is caught. `data.gender`, `data.frame`,
3551
+ `data.appearance.eye_color`, `data.appearance.hair_color` and
3552
+ `data.appearance.skin_color` each take one word from their own list.
3553
+ `data.appearance.complexion` takes one word or a list of them, since a face
3554
+ carries more than one condition at once; a single word means a list of one.
3555
+ Any other value — a word from another field's list, a variant spelling such as
3556
+ `gray` for `grey`, or an empty string — is an error at the value, naming the
3557
+ words the field takes. Leave a field out when the fact is unrecorded.
3558
+
3559
+ `data.appearance.extra_features` is not closed. It is a list of free-text
3560
+ marks — a scar and where it came from, a missing finger, a limp — for what no
3561
+ word in the vocabulary covers.
3562
+
3563
+ ```yaml
3564
+ data:
3565
+ gender: female
3566
+ frame: light
3567
+ appearance:
3568
+ eye_color: dark_brown
3569
+ hair_color: greying_black
3570
+ skin_color: olive
3571
+ complexion: [weathered, freckled]
3572
+ extra_features:
3573
+ - a burn scar across the back of the left hand
3574
+ ```
3575
+
3576
+ The words, and what each means:
3577
+
3578
+ <!-- being-look:start -->
3579
+
3580
+ **`data.gender`**
3581
+
3582
+ | Word | Label | Meaning |
3583
+ | ----------- | --------- | ------------------------------------------------------------------ |
3584
+ | `female` | Female | Female. |
3585
+ | `male` | Male | Male. |
3586
+ | `nonbinary` | Nonbinary | Neither male nor female. |
3587
+ | `none` | None | The being has no gender, as against one that is simply unrecorded. |
3588
+ | `other` | Other | A gender none of these words fits. |
3589
+
3590
+ **`data.frame`**
3591
+
3592
+ | Word | Label | Meaning |
3593
+ | --------- | ------- | --------------------------------------------- |
3594
+ | `scant` | Scant | Very thin, with little flesh over the bone. |
3595
+ | `light` | Light | Slender, narrow through shoulder and hip. |
3596
+ | `medium` | Medium | The ordinary build, neither slight nor heavy. |
3597
+ | `heavy` | Heavy | Broad and solid, in muscle or in flesh. |
3598
+ | `massive` | Massive | Exceptionally large in frame. |
3599
+
3600
+ **`data.appearance.eye_color`**
3601
+
3602
+ | Word | Label | Meaning |
3603
+ | ------------ | ---------- | ---------------------------------------------------------------------------------- |
3604
+ | `amber` | Amber | Golden or coppery, with no grey or green in it; the honey-toned eye. |
3605
+ | `black` | Black | An iris that cannot be told from the pupil in any light, darker than `dark_brown`. |
3606
+ | `blue` | Blue | Unmixed blue, from pale ice to deep sea. |
3607
+ | `brown` | Brown | Mid-brown, the iris pattern still legible in ordinary light. |
3608
+ | `dark_brown` | Dark Brown | Brown so deep the iris barely separates from the pupil. |
3609
+ | `grey` | Grey | Grey or grey-blue, cool and without green. |
3610
+ | `green` | Green | Green dominant, with no brown ring worth naming. |
3611
+ | `hazel` | Hazel | Brown and green together, often shifting with the light; the commonest mixed eye. |
3612
+ | `violet` | Violet | The pink-violet of an iris carrying no pigment. |
3613
+
3614
+ **`data.appearance.hair_color`**
3615
+
3616
+ | Word | Label | Meaning |
3617
+ | ------------------ | ---------------- | ------------------------------------------------------------------------- |
3618
+ | `auburn` | Auburn | Red-brown with the red dominant. |
3619
+ | `black` | Black | True black, with no brown cast in sunlight. |
3620
+ | `blonde` | Blonde | Light yellow, from near-white to wheat. |
3621
+ | `brown` | Brown | Mid-brown, neither dark nor reddish. |
3622
+ | `chestnut` | Chestnut | Mid-brown with a red cast, the brown dominant — between brown and auburn. |
3623
+ | `dark_blonde` | Dark Blonde | Blonde darkened towards light brown. |
3624
+ | `dark_brown` | Dark Brown | Brown approaching black, still brown in sunlight. |
3625
+ | `grey` | Grey | Grey throughout, with nothing of the original colour left to read. |
3626
+ | `greying_black` | Greying Black | Black still dominant, grey coming in. |
3627
+ | `greying_brown` | Greying Brown | Brown still dominant, grey coming in. |
3628
+ | `red` | Red | Ginger or copper, the red unmixed with brown. |
3629
+ | `silver` | Silver | Grey with a bright metallic sheen rather than a dull cast. |
3630
+ | `strawberry_blond` | Strawberry Blond | Pale red-gold: blonde with a red cast, lighter than red. |
3631
+ | `white` | White | Wholly white, whether from age or from carrying no pigment. |
3632
+
3633
+ **`data.appearance.skin_color`**
3634
+
3635
+ | Word | Label | Meaning |
3636
+ | ------------ | ---------- | ------------------------------------------------------------------------------------------- |
3637
+ | `pale` | Pale | The lightest skin, which burns rather than browns; also skin carrying no pigment. |
3638
+ | `fair` | Fair | Light with a pink cast, burning before it browns. |
3639
+ | `light` | Light | Light and neutral, browning without burning readily. |
3640
+ | `medium` | Medium | Neither light nor dark, with no strong cast either way. |
3641
+ | `olive` | Olive | Medium with a green-grey cast. |
3642
+ | `tawny` | Tawny | Light brown, warm rather than olive. |
3643
+ | `golden` | Golden | Light to medium with a yellow cast. |
3644
+ | `tanned` | Tanned | Medium, darkened by sun rather than by birth. |
3645
+ | `swarthy` | Swarthy | Dark with an olive or sallow cast, between olive and brown. |
3646
+ | `brown` | Brown | Mid-brown. |
3647
+ | `dark` | Dark | Darker than brown with no particular cast; the general word where no finer shade is stated. |
3648
+ | `dark_brown` | Dark Brown | Deep brown. |
3649
+ | `ebony` | Ebony | The darkest skin, near-black. |
3650
+
3651
+ **`data.appearance.complexion`**
3652
+
3653
+ | Word | Label | Meaning |
3654
+ | ------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
3655
+ | `ashen` | Ashen | Grey and drained; the skin of serious illness, shock or terror. |
3656
+ | `blotchy` | Blotchy | Uneven in colour, patched red or pale. |
3657
+ | `chapped` | Chapped | Reddened and cracked by cold and wind. |
3658
+ | `clear` | Clear | Free of blemish, mark or broken vessel. |
3659
+ | `freckled` | Freckled | Freckled, in patches or across the whole face. |
3660
+ | `jaundiced` | Jaundiced | Yellowed, the colour a failing liver gives. |
3661
+ | `leathery` | Leathery | Thickened and toughened by years of exposure. |
3662
+ | `oily` | Oily | Carrying a greasy sheen. |
3663
+ | `pasty` | Pasty | Pale and doughy; the skin of indoor work and poor feeding. |
3664
+ | `pimpled` | Pimpled | Presently broken out, with pimples at brow, cheek or jaw. |
3665
+ | `pockmarked` | Pockmarked | Pitted by pox, or by deep pimples long since healed. |
3666
+ | `rough` | Rough | Coarse in texture, without the wear of outdoor work. |
3667
+ | `ruddy` | Ruddy | High colour at cheek and nose, from weather, drink or constitution. |
3668
+ | `sallow` | Sallow | Yellowish and bloodless. |
3669
+ | `scabrous` | Scabrous | Rough and scaling, the skin of a lasting complaint such as eczema. |
3670
+ | `scarred` | Scarred | Marked by healed wounds. `extra_features` says where and from what. |
3671
+ | `smooth` | Smooth | Even in texture, whatever marks it carries. |
3672
+ | `sun_kissed` | Sun-Kissed | Browned by sun on skin not naturally that dark. |
3673
+ | `sunburnt` | Sunburnt | Presently burned, red and peeling. |
3674
+ | `translucent` | Translucent | So pale the vessels show through. |
3675
+ | `vitiligo` | Vitiligo | Patched with white where the skin has lost its colour, most often at face, hands and mouth. The tone it has lost is still what `skin_color` states. |
3676
+ | `wan` | Wan | Pale and drained; the skin of exhaustion or convalescence. |
3677
+ | `weathered` | Weathered | Worn and lined by sun and wind; the skin of outdoor work. |
3678
+ | `wrinkled` | Wrinkled | Deeply lined by age. |
3679
+
3680
+ <!-- being-look:end -->
3681
+
3546
3682
  #### The bodies a being belongs to, and its standing in each
3547
3683
 
3548
3684
  **Membership**: `{rank: number, office: string, societies: string[]}` — `rank`
@@ -5294,6 +5430,123 @@ The place's `lore` links cover its peoples, calendar, law and history. Political
5294
5430
  languages belong to a polity's `sohl.system.commonSkills`, while the note's
5295
5431
  `description` provides its page summary.
5296
5432
 
5433
+ #### A region's settlements table
5434
+
5435
+ A region lists its settlements in a table on its own note: one row for each
5436
+ settlement, stating what its market is, how many people it holds and who holds
5437
+ it. **A row is the whole record of a settlement that has no note.** Where a
5438
+ note exists the row links it, and the two must agree. A package that writes no
5439
+ such table is not affected by anything in this section.
5440
+
5441
+ The table is a markdown table under a level-two `## Settlements` heading on the
5442
+ region's `place` note. Its header is exactly these columns, in this order. `For`
5443
+ may be left out; the header carries no other column:
5444
+
5445
+ | Name | Market | People | Held by | For |
5446
+ | ---- | ------ | ------ | ------- | --- |
5447
+
5448
+ - **Name** — the settlement. Where it has a note, a link to it with the pipe
5449
+ escaped for the table, `[[place-ashford\|Ashford]]`; otherwise plain text.
5450
+ - **Market** — the market class as its number and its name, lower case:
5451
+ `1 hamlet`, `2 village`, `3 town`, `4 market town`, `5 city`, `6 great city`.
5452
+ The scale is the one [`data.market`](#what-a-settlements-market-supports)
5453
+ states.
5454
+ - **People** — the population, a whole number, grouped with commas or not:
5455
+ `60`, `420`, `3,400`.
5456
+ - **Held by** — who holds it, written in the region's holding model. Link the
5457
+ holder where it has a note.
5458
+ - **For** — a short phrase saying why the place is there: the ford, the mine,
5459
+ the shrine.
5460
+
5461
+ At least one sentence of prose follows the table, before the next heading,
5462
+ saying what the table leaves out. A section filled by an `sql` fence is
5463
+ generated from the notes and is not read as a settlements table.
5464
+
5465
+ **The region names its holding model as a tag.** A note carrying a settlements
5466
+ table carries at least one of these tags; a region whose countryside is held
5467
+ two ways carries both. Tags stay open, so a misspelled model is not reported as
5468
+ such: the table's note is reported for naming no model.
5469
+
5470
+ | `holdingModel` value | the Held by cell names |
5471
+ | -------------------- | ---------------------- |
5472
+ | `held` | a person or a house |
5473
+ | `endowed` | an institution |
5474
+ | `registered` | an office |
5475
+ | `kin` | a kindred |
5476
+ | `polis` | the city |
5477
+
5478
+ A linked holder is an `affiliation` or a `being`, and a `being` is a person: it
5479
+ may hold land only in a region tagged `held`.
5480
+
5481
+ **A settlement belongs to one table** — that of the nearest place above it, by
5482
+ `parents`, that carries a settlements table. Every `settlement` note that table
5483
+ covers has a row there and nowhere else.
5484
+
5485
+ A worked example. `Vale.md`:
5486
+
5487
+ ```markdown
5488
+ ---
5489
+ shortcode: vale
5490
+ name: { full: The Vale }
5491
+ type: place
5492
+ subType: region
5493
+ tags: [region, held]
5494
+ data:
5495
+ population: 100000
5496
+ government: crown
5497
+ ---
5498
+
5499
+ ## Settlements
5500
+
5501
+ | Name | Market | People | Held by | For |
5502
+ | -------------------------- | ------------- | -----: | ---------------------------------------- | -------------------------- |
5503
+ | [[place-ashford\|Ashford]] | 4 market town | 3,400 | [[affiliation-crown\|the Crown]] | the ford on the drove road |
5504
+ | Weyshott | 2 village | 420 | [[being-aldwyn\|Sir Aldwyn of Brynwold]] | the manor's mill |
5505
+ | Cold Hatch | 1 hamlet | 60 | Sir Aldwyn of Brynwold | the sheepwalk |
5506
+
5507
+ Some four hundred further villages lie in the vale, none of them named here.
5508
+ ```
5509
+
5510
+ `Ashford.md` states `parents: [vale]`, `population: 3400` and `market: 4`, so its
5511
+ row agrees with it. Weyshott and Cold Hatch have no notes; their rows are their
5512
+ whole record. Change Ashford's row to `3,000` and `package-build lint` reports:
5513
+
5514
+ ```text
5515
+ assets/content/Vale.md:16:49: error: figure disagrees: Ashford is carried at 3,000 here and 3,400 on its own note
5516
+ ```
5517
+
5518
+ `package-build lint` reports each of these as an error, each named by the
5519
+ phrase its message begins with:
5520
+
5521
+ - **`settlements columns`** — the header is not the declared columns in
5522
+ order. The table's rows are not read until it is.
5523
+ - **`no holding model`** — the note carries a settlements table and none of
5524
+ the holding-model tags. Reported at its `tags`.
5525
+ - **`market class`** — a Market cell is not a class written as its number and
5526
+ its name.
5527
+ - **`people figure`** — a People cell is not a positive whole number.
5528
+ - **`no holder`** — a Held by cell is empty.
5529
+ - **`not a holder`** — a Held by link names a note that is neither an
5530
+ affiliation nor a being.
5531
+ - **`holder out of model`** — a Held by link names a being in a region not
5532
+ tagged `held`.
5533
+ - **`not a settlement`** — a Name link names a note that is not a place.
5534
+ - **`figure disagrees`** — a linked row's People differs from its note's
5535
+ `data.population`, or the note states none.
5536
+ - **`market disagrees`** — a linked row's Market differs from its note's
5537
+ `data.market`, or the note states none.
5538
+ - **`unlinked settlement`** — an unlinked Name is the name or an alias of a
5539
+ settlement note the table covers. The finding gives the link to write.
5540
+ - **`missing row`** — a settlement note the table covers has no row. Reported
5541
+ at the table's header.
5542
+ - **`duplicate settlement`** — a settlement has a second row in one table, or
5543
+ a row in a table other than the one covering it.
5544
+ - **`no tail`** — no sentence follows the table before the next heading.
5545
+ Reported at the table's last row.
5546
+
5547
+ A cell's finding is located at the cell. A link in a cell that resolves to no
5548
+ note is reported by the link check, once, and not again here.
5549
+
5297
5550
  ### type: scenario
5298
5551
 
5299
5552
  Content prepared to be played — a situation with its cast, places, and possible outcomes.