@heroiclands/package-build 25.5.5 → 25.6.1

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 (74) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/assets/styles/content-base.css +1 -1
  3. package/docs/api.md +24 -16
  4. package/docs/authoring/frontmatter.md +1 -1
  5. package/docs/authoring/links-and-markup.md +10 -9
  6. package/docs/content-format.md +40 -1
  7. package/docs/diagnostics.md +2 -2
  8. package/docs/encoding-core.md +43 -5
  9. package/docs/guides/reference-pages.md +4 -1
  10. package/docs/guides/site.md +1 -1
  11. package/docs/reference/format-details.md +239 -194
  12. package/docs/reference/note-types.md +187 -174
  13. package/docs/reference/theme-pages.md +1 -0
  14. package/engine/content-format.yaml +9 -7
  15. package/engine/content-html.mjs +3 -2
  16. package/engine/content-index.mjs +23 -0
  17. package/engine/content-links.mjs +8 -0
  18. package/engine/content-render.mjs +2 -2
  19. package/engine/content-tables.mjs +12 -5
  20. package/engine/culture-reach.mjs +3 -0
  21. package/engine/declared-sections.mjs +100 -0
  22. package/engine/derived-sections.mjs +36 -2
  23. package/engine/dictionary-rows.mjs +2 -1
  24. package/engine/encoding-template.mjs +28 -0
  25. package/engine/encoding-terms.mjs +20 -12
  26. package/engine/encoding.mjs +147 -17
  27. package/engine/generated-sections.mjs +2 -0
  28. package/engine/holdings.mjs +107 -3
  29. package/engine/index-records.mjs +3 -1
  30. package/engine/index.mjs +1 -1
  31. package/engine/language-data.mjs +0 -1
  32. package/engine/language-rows.mjs +1 -3
  33. package/engine/language-terms.mjs +7 -10
  34. package/engine/map-dot.mjs +22 -0
  35. package/engine/markdown-blocks.mjs +23 -8
  36. package/engine/metadata-index.mjs +1 -0
  37. package/engine/note-addresses.mjs +16 -0
  38. package/engine/note-vocabulary.mjs +19 -8
  39. package/engine/office-holders.mjs +76 -0
  40. package/engine/page-breaks.mjs +36 -8
  41. package/engine/pdf-render.mjs +4 -3
  42. package/engine/place-kinds.mjs +106 -0
  43. package/engine/settlement-tables.mjs +410 -675
  44. package/engine/settlement-terms.mjs +118 -0
  45. package/engine/site-build.mjs +2 -4
  46. package/engine/sql-tables.mjs +101 -7
  47. package/engine/standing-terms.mjs +9 -5
  48. package/engine/standings.mjs +83 -31
  49. package/engine/unique-items.mjs +85 -0
  50. package/package.json +9 -4
  51. package/site-theme/data/banner-kinds.yaml +2 -0
  52. package/sohl/actors.mjs +4 -3
  53. package/types/engine/content-html.d.mts +2 -1
  54. package/types/engine/content-scripts.d.mts +6 -0
  55. package/types/engine/culture-reach.d.mts +3 -0
  56. package/types/engine/declared-sections.d.mts +30 -0
  57. package/types/engine/derived-sections.d.mts +5 -1
  58. package/types/engine/encoding-lexicon.d.mts +6 -0
  59. package/types/engine/encoding-template.d.mts +6 -0
  60. package/types/engine/encoding.d.mts +12 -0
  61. package/types/engine/holdings.d.mts +34 -2
  62. package/types/engine/index-records.d.mts +2 -1
  63. package/types/engine/language-terms.d.mts +5 -1
  64. package/types/engine/markdown-blocks.d.mts +5 -1
  65. package/types/engine/office-holders.d.mts +33 -0
  66. package/types/engine/page-breaks.d.mts +8 -0
  67. package/types/engine/place-kinds.d.mts +18 -0
  68. package/types/engine/settlement-tables.d.mts +25 -158
  69. package/types/engine/settlement-terms.d.mts +53 -0
  70. package/types/engine/sql-tables.d.mts +35 -3
  71. package/types/engine/standing-terms.d.mts +3 -2
  72. package/types/engine/standings.d.mts +14 -2
  73. package/types/engine/unique-items.d.mts +12 -0
  74. package/vendor/markdownlint.mjs +855 -569
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 25.6.1
4
+
5
+ **Languages and scripts**
6
+
7
+ - Scripts that allow diaeresis can read back the marks in split vowel groups when their writing keeps joined and separate vowels distinct.
8
+ - Language notes and spelling guides can declare pronunciation marks for rising or falling pitch with `kind: tone`.
9
+
10
+ ## 25.6.0
11
+
12
+ **Changes to act on**
13
+
14
+ - Note queries and a language note's `scope` select notes by property, tag or address; a query that reads `file.path`, `file.folder` or `file.name`, or a scope that lists `paths`, is an error at its position.
15
+ - A scenario no longer takes `data.status`, and a note that still writes it fails the build; mark an unfinished scenario with the `draft` tag instead.
16
+ - A region's settlement table comes from its frontmatter on the website, in Foundry and in books, and a table still written in the body must move there. It needs no holding-model tag, and shows a Held by column only where a row names its holder by address.
17
+
18
+ **New**
19
+
20
+ - Places can be districts, such as a city's quarter or a kingdom's shire, standing within a settlement or a region and falling under its government unless they name their own.
21
+ - Places can be routes, such as a road, a pass or a sea lane, with the journeys along them.
22
+ - An affiliation's page lists who holds each of its offices, drawn from the living beings that name the office, and the content index carries the same list.
23
+ - A being can hold several posts in one body by listing them under `office`.
24
+ - Gear can be marked `unique: true` for a one-of-a-kind item; such an item's note must link a place or a being.
25
+ - A language or spelling guide can declare a written mark of kind `quality`, for a mark that changes which vowel is said rather than how long it is held.
26
+ - A quotation typed with `<<` and `>>` draws no raw-HTML warning.
27
+
3
28
  ## 25.5.5
4
29
 
5
30
  **Languages and scripts**
@@ -17,7 +17,7 @@
17
17
  *
18
18
  * It carries what every package's compiled content needs to read in Foundry as
19
19
  * it does in the preview and on the site: definition lists, verse, alerts,
20
- * inline spoilers, marked names, unresolved links, captioned blocks and
20
+ * inline spoilers, unresolved links, captioned blocks and
21
21
  * highlighted code. Every
22
22
  * class the Foundry renderer emits is styled here or exempted by name in
23
23
  * `tests/base-styles.test.ts`. A package keeps its own stylesheet for its own
package/docs/api.md CHANGED
@@ -496,13 +496,15 @@ What a place is next to (`data.borders`) and reachable from (`data.routes`): the
496
496
 
497
497
  What lies within a place and which affiliation explicitly governs it — read off `data.parents` and resolved explicit `data.government` across this package and every fetched index, inverted into the lists a note's generated **Within**, **Governed by** and **Governed places** sections show — and the population-based government advisory `package-build lint` runs on a place.
498
498
 
499
- | Export | Signature | Returns | Use it when |
500
- | ---------------------- | -------------------------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
501
- | `holdingsNode` | `holdingsNode(fm, { title, url, package, system, canonical })` | `HoldingsNode\|null` | reading a local note as a node of the derivation, or `null` for one that is neither a place nor an affiliation |
502
- | `foreignHoldingsNodes` | `foreignHoldingsNodes(foreignIndex)` | `HoldingsNode[]` | reading a fetched index's places and affiliations as nodes |
503
- | `holdingsPages` | `holdingsPages(nodes, { governmentNodes })` | `Map<string, Holdings>` | inverting `parents` and resolved explicit `government` into each node's sorted lists, keyed by its `url`, for every node with a `url` and an entry |
504
- | `shortcodesOf` | `shortcodesOf(value)` | `string[]` | the shortcodes a `parents` value names — bare or qualified Addresses — empties dropped and repeats collapsed |
505
- | `checkGovernment` | `checkGovernment(note)` | `object[]` | warning at `data.population` for positive population with no own `data.government` key; explicit null means anarchy |
499
+ | Export | Signature | Returns | Use it when |
500
+ | ---------------------------- | -------------------------------------------------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
501
+ | `holdingsNode` | `holdingsNode(fm, { title, url, package, system, canonical })` | `HoldingsNode\|null` | reading a local note as a node of the derivation, or `null` for one that is neither a place nor an affiliation |
502
+ | `foreignHoldingsNodes` | `foreignHoldingsNodes(foreignIndex)` | `HoldingsNode[]` | reading a fetched index's places and affiliations as nodes |
503
+ | `holdingsPages` | `holdingsPages(nodes, { governmentNodes })` | `Map<string, Holdings>` | inverting `parents` and resolved explicit `government` into each node's sorted lists, keyed by its `url`, for every node with a `url` and an entry |
504
+ | `shortcodesOf` | `shortcodesOf(value)` | `string[]` | the shortcodes a `parents` value names — bare or qualified Addresses — empties dropped and repeats collapsed |
505
+ | `checkGovernment` | `checkGovernment(note)` | `object[]` | warning at `data.population` for positive population with no `data.government` key, own or inherited; explicit null means anarchy |
506
+ | `inheritDistrictGovernments` | `inheritDistrictGovernments(records, { foreignRecord })` | `void` | giving each district record that states no `data.government` the value of its nearest parent that states one, as the content index does |
507
+ | `governmentInherited` | `governmentInherited(record)` | `boolean` | asking whether a district record's `data.government` came from a parent rather than its own note |
506
508
 
507
509
  An entry carries the listed note's `title`, its `address` in the `note` system, its `type` and `subType`, and its `url` where the note has one. A node with no `url` — a stub — is listed by others and carries no list of its own. Records passed as `governmentNodes` take part in government alone, never in geography.
508
510
 
@@ -531,16 +533,18 @@ What trade a settlement supports (`data.market`): the six-step scale, stated onc
531
533
 
532
534
  ### `engine.settlementTables`
533
535
 
534
- 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.
536
+ A region declares its rows in indexed `data.settlements`. The guard checks populations, markets, holders and regional coverage, and refuses authored body tables. Every publishing surface uses the same generated table, retaining the canonical `#settlements` heading or filling the author's existing section.
535
537
 
536
- | Export | Signature | Returns | Use it when |
537
- | ------------------------ | ---------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
538
- | `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 |
539
- | `SETTLEMENT_COLUMNS` | `const SETTLEMENT_COLUMNS` | `readonly object[]` | the table's columns in header order, each its `name` and whether it is `required` |
540
- | `SETTLEMENTS_HEADING` | `const SETTLEMENTS_HEADING` | `string` | the level-two heading text a settlements table stands under |
541
- | `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 |
542
- | `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 |
543
- | `checkSettlementTables` | `checkSettlementTables(note, { index })` | `object[]` | linting a place note's settlements tables against the corpus the index holds |
538
+ | Export | Signature | Returns | Use it when |
539
+ | ------------------------- | ------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
540
+ | `SETTLEMENT_COLUMNS` | `const SETTLEMENT_COLUMNS` | `readonly object[]` | the table's columns in header order, each its `name` and whether it is `required`; an optional one appears when a row states it |
541
+ | `SETTLEMENTS_HEADING` | `const SETTLEMENTS_HEADING` | `string` | the generated settlements heading |
542
+ | `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 |
543
+ | `settlementMarkdown` | `settlementMarkdown(record)` | `string` | rendering indexed rows as the shared table |
544
+ | `assertSettlementRecords` | `assertSettlementRecords(records, { fileOf, rawOf, foreignIndex })` | `void` | refusing invalid indexed rows before publication |
545
+ | `SETTLEMENTS_FIELD` | `const SETTLEMENTS_FIELD` | `object` | the vocabulary declaration for rows and tail |
546
+ | `SETTLEMENT_ROW_FIELDS` | `const SETTLEMENT_ROW_FIELDS` | `readonly object[]` | the closed row declaration |
547
+ | `checkSettlementTables` | `checkSettlementTables(note, { index })` | `object[]` | linting a place note's settlements tables against the corpus the index holds |
544
548
 
545
549
  ### `engine.mapPlaces`
546
550
 
@@ -1519,6 +1523,10 @@ Naming an interface icon in a note, without drawing it there. The user guide des
1519
1523
 
1520
1524
  Raw HTML in a note's prose, reported. **A note is markdown.** What markdown cannot say, a note does not say — it gets a construct every surface can render, the way `:icon …:` replaces a pasted glyph.
1521
1525
 
1526
+ `HTML_TAG` excludes the doubled `<<` that opens a typed quotation. A quotation
1527
+ written with `<<` and `>>` draws no raw-HTML warning; adjacent real tags retain
1528
+ their findings and positions.
1529
+
1522
1530
  | Export | Signature | Returns | Use it when |
1523
1531
  | ----------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
1524
1532
  | `HTML_TAG` | `const HTML_TAG` | — | A raw HTML tag, opening, closing or self-closing. |
@@ -117,7 +117,7 @@ Write `government: null` for **complete anarchy**. Omitting the key leaves gover
117
117
  | Any | Explicit `null` | None; complete anarchy |
118
118
  | Any | Affiliation Address | None; the reference must resolve |
119
119
 
120
- These rules apply to every place subtype, including settlements, sites, structures, features, regions and worlds. Empty strings, numbers, lists, maps, references to another note type and unresolved references are errors. They do not declare anarchy. `parents`, containment and capitals do not supply a government. The generated **Governed by** and **Governed places** sections use only explicit government references.
120
+ These rules apply to every place subtype, including settlements, sites, structures, features, regions and worlds. Empty strings, numbers, lists, maps, references to another note type and unresolved references are errors. They do not declare anarchy. `parents`, containment and capitals do not supply a government, with one exception: a `district` that omits the key falls under the government of its nearest parent that states one, and is treated as stating that value, so it receives no advisory. An explicit value on the district, `null` included, is its own. The generated **Governed by** and **Governed places** sections use the explicit references and these inherited ones.
121
121
 
122
122
  ## Publication and summaries
123
123
 
@@ -258,8 +258,8 @@ as literal text.
258
258
  Only `---` accepts `.page-break`; `*** {.page-break}` and `___ {.page-break}`
259
259
  are findings. Other classes, ids and attributes on an attributed `---` line
260
260
  are findings too. A page break stands between blocks at the top level of a
261
- note: putting one inside a list, block quote, alert, secret block or fenced
262
- div is a finding at the marker's line and column.
261
+ note: putting one inside a list, block quote, alert, footnote definition,
262
+ secret block or fenced div is a finding at the marker's line and column.
263
263
 
264
264
  ## Captions and numbered references
265
265
 
@@ -1538,13 +1538,14 @@ Nested frontmatter is addressed exactly as a note writes it: `name.full`, `sohl.
1538
1538
 
1539
1539
  The build adds sections to the end of a note, after everything you wrote, listing what other notes say about it. Each appears only where it has something to list, under a fixed anchor:
1540
1540
 
1541
- | Section | Anchor | Lists |
1542
- | ----------------- | ---------------- | -------------------------------------------------------------------- |
1543
- | Within | `within` | on a place, the places whose `parents` name it, grouped by kind |
1544
- | Governed by | `governedby` | on a place, the affiliation its `government` names |
1545
- | Governed places | `governedplaces` | on an affiliation, the places naming it as their `government` |
1546
- | In song and story | `insongandstory` | the works of literature whose `subjects` name the note or its events |
1547
- | From here | `fromhere` | on a place with borders or routes, the map drawn from it |
1541
+ | Section | Anchor | Lists |
1542
+ | ----------------- | ---------------- | ----------------------------------------------------------------------------- |
1543
+ | Within | `within` | on a place, the places whose `parents` name it, grouped by kind |
1544
+ | Governed by | `governedby` | on a place, the affiliation its `government` names |
1545
+ | Governed places | `governedplaces` | on an affiliation, the places naming it as their `government` |
1546
+ | Office holders | `officeholders` | on an affiliation, each office's living holders, from the beings that name it |
1547
+ | In song and story | `insongandstory` | the works of literature whose `subjects` name the note or its events |
1548
+ | From here | `fromhere` | on a place with borders or routes, the map drawn from it |
1548
1549
 
1549
1550
  The event sections below sit among them, and the [format reference](../reference/format-details.md#generated-sections) gives the order. Each entry links to the note it names; a note with no page of its own is named in plain text. Replace a section the way you replace an event view, by writing your own with the same anchor:
1550
1551
 
@@ -96,6 +96,8 @@ markers scoped to each note. Definition lists use a term followed by one or
96
96
  more `: definition` lines. See [footnotes and definition lists](authoring/links-and-markup.md#footnotes-and-definition-lists)
97
97
  for examples and placement in journals, web pages, and books.
98
98
 
99
+ A query selects notes by property, tag or address, never by where a note's file sits: a fence that names `file.path`, `file.folder` or `file.name` is an error at the reference, and a note moved between folders changes no query's answer.
100
+
99
101
  An inline `{{sql "SELECT COUNT(*) FROM notes"}}` expression inserts one scalar query result. Use `{{words (sql "SELECT COUNT(*) FROM notes")}}` for a count in running prose, or `{{digits (sql "SELECT COUNT(*) FROM entries")}}` for grouped numerals. The query must return exactly one row and one column with a nonempty value; a mismatch is a located build error. Prefix `\{{` to show the syntax literally. The [expression reference](reference/format-details.md#dates-and-calendars) describes the helpers and the available SQL relations.
100
102
 
101
103
  Add `gm` to `tags` for a GM-only note. Public site and book builds omit it;
@@ -203,7 +205,44 @@ The [first-note guide](authoring/first-note.md) shows the build commands and how
203
205
 
204
206
  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).
205
207
 
206
- 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).
208
+ A region declares its table in `data.settlements: { rows, tail }`. The rows
209
+ carry the settlement's `population` and numeric `market`, and either `place`
210
+ (the existing note's Address, default type `place`) or `name` (plain text where
211
+ no note exists), never both. Where the region's culture holds land, a row's
212
+ optional `heldBy` names what holds it by Address (an affiliation or a being,
213
+ default type `affiliation`), and what that note is says how the land is held: a
214
+ temple, a lineage, the city, a kin group. A people who hold no land state no
215
+ `heldBy`, and their table has no Held by column. Optional `for` states why the
216
+ settlement exists. `tail` is the sentence stating what the table leaves out.
217
+
218
+ The guard reads indexed rows. A linked row's population and market agree with
219
+ its note, and every settlement note covered through `parents` has one row in
220
+ the nearest declaring region. `government` states who governs; it never supplies
221
+ who holds the land. HTML, Foundry and Typst render the declared table once.
222
+ A body table under `## Settlements` is an error at its header, naming
223
+ `data.settlements`; no facts are read from it.
224
+
225
+ ```yaml
226
+ data:
227
+ settlements:
228
+ rows:
229
+ - place: ashford
230
+ market: 4
231
+ population: 3400
232
+ heldBy: affiliation-crown
233
+ for: the ford on the drove road
234
+ - name: Weyshott
235
+ market: 2
236
+ population: 420
237
+ heldBy: being-aldwyn
238
+ for: the manor's mill
239
+ - name: Cold Hatch
240
+ market: 1
241
+ population: 60
242
+ tail: Some four hundred further villages lie in the vale, none of them named here.
243
+ ```
244
+
245
+ See [a region's settlements table](reference/format-details.md#a-regions-settlements-table).
207
246
 
208
247
  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. `complexion` is a list, required once a being states an appearance. `data.appearance.extra_features` lists the being's visible physical marks, each a short plain description of one, and an entry stating a colour, a skin condition or anything that is not a mark is an error. See [a being's look](reference/format-details.md#a-beings-look).
209
248
 
@@ -303,8 +303,8 @@ The optional [book page break](authoring/links-and-markup.md#book-page-breaks)
303
303
  uses only `--- {.page-break}` between blocks at the note's top level.
304
304
  Other classes, ids or attributes on an attributed `---` line receive findings
305
305
  at their opening brace. A `.page-break` on `***` or `___` receives a finding
306
- at the marker. A marker inside a list, block quote, alert, secret block or
307
- fenced div receives `a page break stands between blocks at the top level of a
306
+ at the marker. A marker inside a list, block quote, alert, footnote definition,
307
+ secret block or fenced div receives `a page break stands between blocks at the top level of a
308
308
  note`, at that marker's line and column. Missing surrounding blank lines
309
309
  receive the same placement finding. Code blocks and complete verse divs are
310
310
  literal and receive no page-break findings.
@@ -53,15 +53,16 @@ second behaviour.
53
53
  | `case` | `{single-letter: preserve, consonant-group: lowercase-or-initial-uppercase, other-group-cases: forbidden}`; single letters retain authored case, and groups use lowercase or an initial capital. |
54
54
  | `stress` | `{modifier, position: after-stressed-vowel, printing: false, effect: stress-only, changes-sound: false, changes-length: false, vowel-group-position: first-vowel}`; `modifier` names a font code and acute mappings append it to the unchanged vowel. |
55
55
  | `vowel-groups` | `{reading: established, allow-splitting: false, unrecognized-adjacent-vowels: separate, groups: [...]}`; the explicit group list determines where an acute belongs on its first vowel. |
56
- | `diaeresis` | `{allowed: boolean}` permits or forbids explicitly mapped diaeresis characters. |
56
+ | `diaeresis` | `{allowed: boolean}`; false forbids diaeresis characters; true derives plain-vowel mappings and restores splits of distinctly mapped declared groups. |
57
57
  | `grave-accent` | `{allowed: boolean}` permits or forbids explicitly mapped grave-accent characters. |
58
58
  | `contextual-mappings` | A list of `{match, emit, before?, at?}`; following-letter or source-position conditions precede unconditional mappings. |
59
59
  | `y` | `{kind: consonant, sound: "y in yes", vowel-reading: false}`; y cannot also appear in `data.vowels`. |
60
60
 
61
61
  The accent blocks accept an explicit Boolean `allowed`. False forbids that
62
- accent; true permits it only where the character is accepted and mapped.
63
- Omitting the block also permits such explicit mappings. Neither choice invents
64
- an accent pronunciation or adds a character to the inventory.
62
+ accent. True on the grave accent permits it only where the character is
63
+ accepted and mapped; true on the diaeresis turns on the
64
+ [diaeresis rule](#the-diaeresis-by-rule). Omitting either block permits
65
+ explicit mappings alone, and no choice invents an accent pronunciation.
65
66
 
66
67
  `rules.roundTrip` is optional and defaults to `exact`. Exact mappings must be
67
68
  uniquely decodable across adjacent mappings and spaces; ambiguous outputs and
@@ -116,7 +117,8 @@ canonical round trips. Decoding keeps the shared space as a space.
116
117
  A contextual match cannot contain whitespace, literal markers or forbidden
117
118
  accents. Both modes enforce the same accepted characters, normalization,
118
119
  case, stress, accent and literal-control rules. The compiler does not infer
119
- linguistic vowel groups or silently add missing letter mappings.
120
+ linguistic vowel groups or silently add missing plain-letter mappings. The
121
+ [diaeresis rule](#the-diaeresis-by-rule) uses only explicitly declared groups.
120
122
 
121
123
  Lint checks every authored word, variant and registered name for a language
122
124
  with an encoding document, including unused entries. It also checks actual
@@ -147,6 +149,42 @@ text to the surrounding sentence. Names, literal codage and independent
147
149
  language children count as text. An explicit mapping can give a standard
148
150
  source its own font glyph. Other punctuation needs its own mapping.
149
151
 
152
+ ### The diaeresis by rule
153
+
154
+ A diaeresis splits a listed vowel group: _kau_ uses the group _au_, while
155
+ _kaü_ sounds its vowels apart. Adjacent vowels outside the declared groups
156
+ already sound separately and need no mark: _kae_ remains _kae_.
157
+
158
+ `diaeresis: {allowed: true}` accepts _ä_, _ë_, _ï_, _ö_ and _ü_, and their
159
+ capitals, using the corresponding plain vowel's mappings and contextual
160
+ rules. No extra mapping rows are needed. This applies only when the plain
161
+ vowel has a mapping of its own and no source explicitly names that diaeresis
162
+ vowel. Explicit mappings retain their authored spelling and glyphs. A derived
163
+ diaeresis blocks a row from joining the preceding vowel, while permitting
164
+ matching from the marked vowel onward. Source pieces and errors retain their
165
+ authored UTF-16 offsets, including decomposed input normalized to NFC.
166
+
167
+ Decoding retains the selected native mapping boundaries. A group written by
168
+ its own mapping stays together; separate mappings receive a diaeresis where
169
+ their letters would otherwise be joined by a declared group mapping. For
170
+ example, with `au: AU`, `a: A` and `u: U`, the AU glyph reads _au_ and the A
171
+ then U glyphs read _aü_. Groups use `rules.vowel-groups.groups`, longest first;
172
+ there is no default group list. Existing marks and the interiors of explicit
173
+ mappings retain their spelling. Literal `⟦…⟧` spans receive no derived marks.
174
+
175
+ The native writing must distinguish the group from its split. A group with
176
+ no mapping of its own, or one emitting the same sequence as its separate
177
+ vowels, cannot restore that distinction. The compiler still accepts such an
178
+ encoding and its bare vowel spelling. An exact encoding refuses a newly
179
+ derived split input when its native path cannot recover the mark. This also
180
+ applies when a particular context or an acute prevents the distinct group
181
+ mapping from matching. Canonical encodings retain their existing longest
182
+ complete native match and authored-alias preferences, so an ambiguous split
183
+ may return the conventional bare spelling. Standalone or non-group diaeresis
184
+ aliases remain accepted, but have no rule-derived mark to restore; this is
185
+ not a pronunciation validator. An encoding that omits the permission block
186
+ or forbids the diaeresis derives no aliases or marks.
187
+
150
188
  ## Conversion API
151
189
 
152
190
  The pure engine works in browsers and Node and opens no files. The caller
@@ -134,7 +134,10 @@ its language's code. It also has ASCII-folded `nameAscii` and `formsAscii`, the
134
134
  listed forms by language, for searching names typed without diacritics, and
135
135
  `lang`, the language the note is in, derived as
136
136
  [a note's language](../reference/format-details.md#a-notes-language) describes.
137
- A language note that `languages:` names carries its code as `languageCode`. Asset files under `assets/icons`, `assets/images`, and
137
+ A language note that `languages:` names carries its code as `languageCode`.
138
+ An affiliation carries `officeHolders`, each of its offices that a living
139
+ being holds with those beings' Addresses, derived as
140
+ [office holders](../reference/format-details.md#type-affiliation) describes. Asset files under `assets/icons`, `assets/images`, and
138
141
  `assets/audio` have records in the same index, identified by an `asset` block.
139
142
  The index is disposable build output; regenerate it from the authored tree
140
143
  when the content changes.
@@ -244,7 +244,7 @@ notes' sources. A page is reachable when an authored link leads to it:
244
244
  The links the build generates do not make a page reachable. These are the
245
245
  Related card, the previous and next links, the infobox links, and the sections
246
246
  generated after a note's text (Within, Governed by, Governed places,
247
- Chronology, Events, Accounts, What followed, In song and story). Each is drawn
247
+ Office holders, Chronology, Events, Accounts, What followed, In song and story). Each is drawn
248
248
  without a link to a page outside the reachable set, and a section left with
249
249
  nothing under it is not drawn. An infobox value that names an unreachable page
250
250
  is set as plain text. So is a link in a dictionary or glossary entry, whether