@heroiclands/package-build 22.25.0 → 22.26.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,11 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.26.0
4
+
5
+ ### Minor Changes
6
+
7
+ Place notes can name their governing affiliation in `data.government`, or state `null` for complete anarchy. A place with positive population and no government now receives an advisory. Places with zero, null or unspecified population no longer receive the old unheld-land warning; holdings remain separate tenure facts. Governments appear in note infoboxes and persist in published metadata.
8
+
3
9
  ## 22.25.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -46,4 +46,6 @@ The documentation lives in the Markdown files in this repository. Start with [th
46
46
  | Error locations and severities | [Diagnostics](docs/diagnostics.md) |
47
47
  | Architectural rationale | [Design](docs/design.md) |
48
48
 
49
+ 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](docs/authoring/frontmatter.md#place-governments).
50
+
49
51
  The package is GPL-3.0-or-later for code and CC-BY-SA-4.0 for original content. See [LICENSE.md](LICENSE.md).
package/docs/api.md CHANGED
@@ -348,6 +348,8 @@ The note types the engine itself declares — vocabulary that is a fact about th
348
348
 
349
349
  ### `engine.noteVocabulary`
350
350
 
351
+ `DataFieldSpec.nullText` declares the infobox text for an explicitly authored null. The place `government` field uses “Complete anarchy”; omitted fields remain absent. Its Address targets must be affiliations.
352
+
351
353
  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`.
352
354
 
353
355
  | Export | Signature | Returns | Use it when |
@@ -495,17 +497,18 @@ What a place is next to (`data.borders`) and reachable from (`data.routes`): the
495
497
 
496
498
  ### `engine.holdings`
497
499
 
498
- What lies within a place, who holds it, and what an affiliation holds — read off `data.parents` and `data.domains` across this package and every fetched index, inverted into the `contains`, `held_by` and `holdings` lists the site build writes — and the tenure check `package-build lint` runs on a place.
499
-
500
- | Export | Signature | Returns | Use it when |
501
- | ---------------------- | ------------------------------------ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
502
- | `HOLDINGS_KEYS` | `const HOLDINGS_KEYS` | `readonly string[]` | the three derived keys — `contains`, `held_by`, `holdings` — which a note cannot author |
503
- | `HELD_SUBTYPES` | `const HELD_SUBTYPES` | `readonly string[]` | the place subTypes tenure is checked on — `settlement`, `site`, `structure` |
504
- | `holdingsNode` | `holdingsNode(fm, { title, url })` | `HoldingsNode\|null` | reading a local note as a node of the derivation, or `null` for one that is neither a place nor an affiliation |
505
- | `foreignHoldingsNodes` | `foreignHoldingsNodes(foreignIndex)` | `HoldingsNode[]` | reading a fetched index's places and affiliations as nodes |
506
- | `holdingsPages` | `holdingsPages(nodes)` | `Map<string, Holdings>` | inverting `parents` and `domains` into each page's sorted lists, keyed by URL, for every page with at least one entry |
507
- | `shortcodesOf` | `shortcodesOf(value)` | `string[]` | the shortcodes a `parents` or `domains` value names — bare, addressed or wikilinked — empties dropped and repeats collapsed |
508
- | `checkHeld` | `checkHeld(note, { index })` | `object[]` | linting a place's tenure — a settlement, site or structure no affiliation's `domains` names is a warning at its `type:` line |
500
+ What lies within a place, who holds it, and what an affiliation holds — read off `data.parents` and `data.domains` across this package and every fetched index, inverted into the `contains`, `held_by` and `holdings` lists the site build writes — and the population-based government advisory `package-build lint` runs on a place.
501
+
502
+ | Export | Signature | Returns | Use it when |
503
+ | ---------------------- | ------------------------------------ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------- |
504
+ | `HOLDINGS_KEYS` | `const HOLDINGS_KEYS` | `readonly string[]` | the three derived keys — `contains`, `held_by`, `holdings` — which a note cannot author |
505
+ | `HELD_SUBTYPES` | `const HELD_SUBTYPES` | `readonly string[]` | deprecated historical tenure subtypes; government checks apply to every place subtype |
506
+ | `holdingsNode` | `holdingsNode(fm, { title, url })` | `HoldingsNode\|null` | reading a local note as a node of the derivation, or `null` for one that is neither a place nor an affiliation |
507
+ | `foreignHoldingsNodes` | `foreignHoldingsNodes(foreignIndex)` | `HoldingsNode[]` | reading a fetched index's places and affiliations as nodes |
508
+ | `holdingsPages` | `holdingsPages(nodes)` | `Map<string, Holdings>` | inverting `parents` and `domains` into each page's sorted lists, keyed by URL, for every page with at least one entry |
509
+ | `shortcodesOf` | `shortcodesOf(value)` | `string[]` | the shortcodes a `parents` or `domains` value names — bare, addressed or wikilinked — empties dropped and repeats collapsed |
510
+ | `checkGovernment` | `checkGovernment(note)` | `object[]` | warning at `data.population` for positive population with no own `data.government` key; explicit null means anarchy |
511
+ | `checkHeld` | `checkHeld(note, { index })` | `object[]` | deprecated compatibility wrapper for `checkGovernment`; holdings do not determine government |
509
512
 
510
513
  ### `engine.populations`
511
514
 
@@ -39,6 +39,27 @@ A `sohl:`, `hm3:` or `dnd5e:` block holds values for that Foundry system. Its `s
39
39
 
40
40
  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.
41
41
 
42
+ ## Place governments
43
+
44
+ A place may state its governing affiliation in `data.government`. Use an affiliation Address, either a local shortcode or a fully qualified Address from this package or a fetched dependency:
45
+
46
+ ```yaml
47
+ data:
48
+ population: 1200
49
+ government: crown
50
+ ```
51
+
52
+ Write `government: null` for **complete anarchy**. Omitting the key leaves government unspecified. Null is retained in published metadata and displayed as “Complete anarchy” in website, Foundry and book infoboxes; an omitted key has no Government row. A reference displays a link to the governing affiliation.
53
+
54
+ | Population | Government | Governance advisory |
55
+ | --------------------- | ------------------- | -------------------------------- |
56
+ | Positive | Omitted | Warning at `data.population` |
57
+ | Missing, zero or null | Omitted | None |
58
+ | Any | Explicit `null` | None; complete anarchy |
59
+ | Any | Affiliation Address | None; the reference must resolve |
60
+
61
+ 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. `domains`, `parents`, containment and capitals do not supply a government. Holdings remain tenure facts; the former unconditional “unheld land” warning is retired.
62
+
42
63
  ## Publication and summaries
43
64
 
44
65
  A note infobox draws from shared `data`. A being profile shows its top-level `subType` as `Type: Character`, `Type: NPC`, or `Type: Creature`. A system infobox draws from the system's fields, and a mapped system with no document says “Not available.” The same authored body appears in Foundry, on the website, and in the book; the book places boxes after the authored body, Foundry appends one page per box, and the website places boxes in its responsive side rail. [Infobox details](../reference/format-details.md#the-infobox) describe field order and empty values.
@@ -85,3 +85,5 @@ A sheltered place where ships load and unload.
85
85
  The note's readable Address is `<package>-note-lore-harbor`. In prose, link to it with `[[lore-harbor|Harbor]]`. The package segment comes from `package-build.config.yaml`; a regular wikilink defaults to the readable `note` system. An image embed such as `![[icon-harbor|Harbor]]` defaults to `none` instead.
86
86
 
87
87
  The [first-note guide](authoring/first-note.md) shows the build commands and how to add a system document. Use the [command reference](commands.md) for exact flags and the [diagnostics guide](diagnostics.md) for located errors.
88
+
89
+ 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).
@@ -4255,32 +4255,33 @@ map's prose.
4255
4255
  - feature: A place significant by its terrain — forests, rivers, falls, passes, fords.
4256
4256
  - celestial: A body observed from a world rather than located on one — a sun, a planet, a comet.
4257
4257
 
4258
- | `data` property | Values | Description |
4259
- | --------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------- |
4260
- | `calendar` | `Address` | Calendar note used to print this place's dates |
4261
- | `demonym` | `string` | What a person from this place is called — a Vylarian |
4262
- | `purpose` | `placeCharacter` tag | The reason a settlement, site, or structure exists |
4263
- | `lore` | `Address[]` | Lore concerning this place — its peoples, its law, its calendar, its history |
4264
- | `parents` | `Address[]` | Enclosing places within which this place is located |
4265
- | `population` | `number` | Approximate population (precision 2 significant digits) |
4266
- | `market` | `number` | Market class, 1 to 6 — what trade a settlement supports |
4267
- | `borders` | `{ to, bearing }[]` | Places sharing a frontier with this one, and where each lies from here |
4268
- | `routes` | `{ to, bearing, mode, days, terrain?, leagues? }[]` | Journeys from this place's centre to another place |
4269
- | `world.equatorialCircumferenceKm` | `number` | The distance round the body at its equator, in kilometres |
4270
- | `world.surfaceGravityG` | `number` | Surface gravity, as a multiple of Earth's |
4271
- | `world.axialTiltDegrees` | `number` | The tilt of the axis, in degrees |
4272
- | `year.days` | `number` | How many days the body's year holds |
4273
- | `year.hoursPerDay` | `number` | How many hours the day divides into |
4274
- | `year.minutesPerHour` | `number` | How many minutes the hour divides into |
4275
- | `year.secondsPerMinute` | `number` | How many seconds the minute divides into |
4276
- | `present` | `date` | The day the setting stops and play begins |
4277
- | `body.diameterKm` | `number` | The body's diameter, in kilometres |
4278
- | `body.orbitalRadiusKm` | `number` | How far it orbits from the body it circles, in kilometres |
4279
- | `body.orbit` | `string` | The orbit's shape — `circular` means the cycle never varies |
4280
- | `body.inclined` | `boolean` | Whether the orbit is inclined to the plane the world orbits in |
4281
- | `moon.cycle` | `number` | How many days it takes to return to the same phase |
4282
- | `moon.newOn` | day-precision `date` | A day it was new, written in the reference calendar |
4283
- | `moon.eclipses` | `string` | `never`, `rare`, `occasional` or `frequent` |
4258
+ | `data` property | Values | Description |
4259
+ | --------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
4260
+ | `calendar` | `Address` | Calendar note used to print this place's dates |
4261
+ | `demonym` | `string` | What a person from this place is called — a Vylarian |
4262
+ | `purpose` | `placeCharacter` tag | The reason a settlement, site, or structure exists |
4263
+ | `lore` | `Address[]` | Lore concerning this place — its peoples, its law, its calendar, its history |
4264
+ | `parents` | `Address[]` | Enclosing places within which this place is located |
4265
+ | `population` | `number` | Approximate population (precision 2 significant digits) |
4266
+ | `government` | affiliation Address or `null` | Governing affiliation; explicit null means complete anarchy. Omission warns only for positive population. |
4267
+ | `market` | `number` | Market class, 1 to 6 — what trade a settlement supports |
4268
+ | `borders` | `{ to, bearing }[]` | Places sharing a frontier with this one, and where each lies from here |
4269
+ | `routes` | `{ to, bearing, mode, days, terrain?, leagues? }[]` | Journeys from this place's centre to another place |
4270
+ | `world.equatorialCircumferenceKm` | `number` | The distance round the body at its equator, in kilometres |
4271
+ | `world.surfaceGravityG` | `number` | Surface gravity, as a multiple of Earth's |
4272
+ | `world.axialTiltDegrees` | `number` | The tilt of the axis, in degrees |
4273
+ | `year.days` | `number` | How many days the body's year holds |
4274
+ | `year.hoursPerDay` | `number` | How many hours the day divides into |
4275
+ | `year.minutesPerHour` | `number` | How many minutes the hour divides into |
4276
+ | `year.secondsPerMinute` | `number` | How many seconds the minute divides into |
4277
+ | `present` | `date` | The day the setting stops and play begins |
4278
+ | `body.diameterKm` | `number` | The body's diameter, in kilometres |
4279
+ | `body.orbitalRadiusKm` | `number` | How far it orbits from the body it circles, in kilometres |
4280
+ | `body.orbit` | `string` | The orbit's shape — `circular` means the cycle never varies |
4281
+ | `body.inclined` | `boolean` | Whether the orbit is inclined to the plane the world orbits in |
4282
+ | `moon.cycle` | `number` | How many days it takes to return to the same phase |
4283
+ | `moon.newOn` | day-precision `date` | A day it was new, written in the reference calendar |
4284
+ | `moon.eclipses` | `string` | `never`, `rare`, `occasional` or `frequent` |
4284
4285
 
4285
4286
  #### Why a place exists
4286
4287
 
@@ -4501,12 +4502,26 @@ never expanded**: a region held by a polity lists the polity in `held_by`, and
4501
4502
  a settlement within that region lists only the house or order whose `domains`
4502
4503
  names it — its lord's lord is reached through `affiliation.parents`.
4503
4504
 
4504
- **A settlement, a site or a structure is held by someone.** One of those that
4505
- no affiliation's `domains` names — in this package or in a fetched index — is
4506
- a warning from `package-build lint`, `unheld land`, located at the note's
4507
- `type:` line, so a gap in tenure shows while a note is written. A `world`, a
4508
- `region` and a `feature` are exempt: a region is held through its polity's
4509
- `domains`, and a river by nobody.
4505
+ #### Who governs a place
4506
+
4507
+ A place may state its governing affiliation in `data.government`. Use an affiliation Address, either a local shortcode or a fully qualified Address from this package or a fetched dependency:
4508
+
4509
+ ```yaml
4510
+ data:
4511
+ population: 1200
4512
+ government: crown
4513
+ ```
4514
+
4515
+ Write `government: null` for **complete anarchy**. Omitting the key leaves government unspecified. Null is retained in published metadata and displayed as “Complete anarchy” in website, Foundry and book infoboxes; an omitted key has no Government row. A reference displays a link to the governing affiliation.
4516
+
4517
+ | Population | Government | Governance advisory |
4518
+ | --------------------- | ------------------- | -------------------------------- |
4519
+ | Positive | Omitted | Warning at `data.population` |
4520
+ | Missing, zero or null | Omitted | None |
4521
+ | Any | Explicit `null` | None; complete anarchy |
4522
+ | Any | Affiliation Address | None; the reference must resolve |
4523
+
4524
+ 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. `domains`, `parents`, containment and capitals do not supply a government. Holdings remain tenure facts; the former unconditional “unheld land” warning is retired.
4510
4525
 
4511
4526
  #### Population figures
4512
4527
 
@@ -463,32 +463,33 @@ Lore records in-world knowledge. A `culture` describes a people; a `custom` desc
463
463
 
464
464
  **Subtypes:** `world`, `region`, `settlement`, `site`, `structure`, `feature`, `celestial`.
465
465
 
466
- | Field | Shape | Meaning |
467
- | -------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
468
- | `data.calendar` | an Address | Calendar used to display this place's dates. |
469
- | `data.demonym` | string | What a person from this place is called — a Vylarian. |
470
- | `data.purpose` | string | The reason this settlement, site, or structure exists, selected from its placeCharacter tags. |
471
- | `data.lore` | list of Addresses | Lore concerning this place — its peoples, its law, its calendar, its history. |
472
- | `data.parents` | list of Addresses | Enclosing places this one sits within. |
473
- | `data.population` | number | Approximate population, to two significant digits. |
474
- | `data.market` | number | What trade the settlement supports, on a scale of six: 1 hamlet, 2 village, 3 town, 4 market town, 5 city, 6 great city. |
475
- | `data.borders` | list of `{ to, bearing }` entries | Places sharing a frontier with this one — each the other's shortcode and where it lies from here. |
476
- | `data.routes` | list of `{ to, bearing, mode, days, terrain?, leagues? }` entries | Journeys from this place's centre — where the destination lies, how it is travelled, and about how many days it takes. |
477
- | `data.world.equatorialCircumferenceKm` | number | The distance round the body at its equator, in kilometres. |
478
- | `data.world.surfaceGravityG` | number | Surface gravity, as a multiple of Earth's. |
479
- | `data.world.axialTiltDegrees` | number | The tilt of the axis, in degrees — what makes the body have seasons. |
480
- | `data.year.days` | number | How many days the body's year holds. Every calendar's months sum to it. |
481
- | `data.year.hoursPerDay` | number | How many hours the day divides into. |
482
- | `data.year.minutesPerHour` | number | How many minutes the hour divides into. |
483
- | `data.year.secondsPerMinute` | number | How many seconds the minute divides into. |
484
- | `data.present` | a date | The day the setting stops and play begins — what an age is computed against. |
485
- | `data.body.diameterKm` | number | The body's diameter, in kilometres. |
486
- | `data.body.orbitalRadiusKm` | number | How far the body orbits from the one it circles, in kilometres. |
487
- | `data.body.orbit` | string | The orbit's shape — `circular` means the cycle never varies. |
488
- | `data.body.inclined` | boolean | Whether the orbit is inclined to the plane the world orbits in. |
489
- | `data.moon.cycle` | number | How many days the body takes to return to the same phase. |
490
- | `data.moon.newOn` | a day-precision date | A day the body was new, written in the reference calendar. |
491
- | `data.moon.eclipses` | string | How often the body eclipses or is eclipsed — `never`, `rare`, `occasional` or `frequent`. |
466
+ | Field | Shape | Meaning |
467
+ | -------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
468
+ | `data.calendar` | an Address | Calendar used to display this place's dates. |
469
+ | `data.demonym` | string | What a person from this place is called — a Vylarian. |
470
+ | `data.purpose` | string | The reason this settlement, site, or structure exists, selected from its placeCharacter tags. |
471
+ | `data.lore` | list of Addresses | Lore concerning this place — its peoples, its law, its calendar, its history. |
472
+ | `data.parents` | list of Addresses | Enclosing places this one sits within. |
473
+ | `data.population` | number | Approximate population, to two significant digits. |
474
+ | `data.government` | an affiliation Address or null | The governing affiliation; explicit null means complete anarchy. A positive population with no government key produces an advisory. |
475
+ | `data.market` | number | What trade the settlement supports, on a scale of six: 1 hamlet, 2 village, 3 town, 4 market town, 5 city, 6 great city. |
476
+ | `data.borders` | list of `{ to, bearing }` entries | Places sharing a frontier with this one — each the other's shortcode and where it lies from here. |
477
+ | `data.routes` | list of `{ to, bearing, mode, days, terrain?, leagues? }` entries | Journeys from this place's centre — where the destination lies, how it is travelled, and about how many days it takes. |
478
+ | `data.world.equatorialCircumferenceKm` | number | The distance round the body at its equator, in kilometres. |
479
+ | `data.world.surfaceGravityG` | number | Surface gravity, as a multiple of Earth's. |
480
+ | `data.world.axialTiltDegrees` | number | The tilt of the axis, in degrees — what makes the body have seasons. |
481
+ | `data.year.days` | number | How many days the body's year holds. Every calendar's months sum to it. |
482
+ | `data.year.hoursPerDay` | number | How many hours the day divides into. |
483
+ | `data.year.minutesPerHour` | number | How many minutes the hour divides into. |
484
+ | `data.year.secondsPerMinute` | number | How many seconds the minute divides into. |
485
+ | `data.present` | a date | The day the setting stops and play begins — what an age is computed against. |
486
+ | `data.body.diameterKm` | number | The body's diameter, in kilometres. |
487
+ | `data.body.orbitalRadiusKm` | number | How far the body orbits from the one it circles, in kilometres. |
488
+ | `data.body.orbit` | string | The orbit's shape — `circular` means the cycle never varies. |
489
+ | `data.body.inclined` | boolean | Whether the orbit is inclined to the plane the world orbits in. |
490
+ | `data.moon.cycle` | number | How many days the body takes to return to the same phase. |
491
+ | `data.moon.newOn` | a day-precision date | A day the body was new, written in the reference calendar. |
492
+ | `data.moon.eclipses` | string | How often the body eclipses or is eclipsed — `never`, `rare`, `occasional` or `frequent`. |
492
493
 
493
494
  ### scenario
494
495
 
@@ -25,6 +25,8 @@ describes the keys the build writes from it.
25
25
 
26
26
  ## The infobox
27
27
 
28
+ A place’s Government row links to its authored governing affiliation. Explicit `data.government: null` produces a text row saying “Complete anarchy”; an omitted government produces no row. The same distinction is retained in the page’s `data` metadata.
29
+
28
30
  The toolchain settles what each box holds and in what order — see
29
31
  [the infobox](format-details.md#the-infobox) for the field semantics, the
30
32
  three states a system box can say, and the four section layouts a box is
@@ -511,7 +511,7 @@ function checkCalendarDayNames(note, daysPerYear) {
511
511
  /**
512
512
  * Check a `place` note's world facts.
513
513
  *
514
- * Declared on `place` beside its tenure check, so the lint runs both with the
514
+ * Declared on `place` beside its government advisory, so the lint runs both with the
515
515
  * same index. Four rules, and each is a fact about the setting rather than
516
516
  * about the file:
517
517
  *
@@ -292,6 +292,7 @@ types:
292
292
  - lore
293
293
  - parents
294
294
  - population
295
+ - government
295
296
  - market
296
297
  - borders
297
298
  - routes
@@ -35,16 +35,15 @@
35
35
  * package publishes is listed within a local region, and a house another
36
36
  * package publishes is named on the local manor it holds.
37
37
  *
38
- * **A place's tenure is checked.** A settlement, a site or a structure that no
39
- * affiliation's `domains` names is land nobody holds — a gap in tenure the
40
- * lint reports as a warning at the note's `type:` line. A region is held
41
- * through its polity's `domains` and a feature by nobody, so both are exempt.
38
+ * **Government is stated separately from tenure.** A place with positive
39
+ * `data.population` and no authored `data.government` receives an advisory.
40
+ * Explicit null means complete anarchy. Holdings never supply a government.
42
41
  *
43
42
  * @module
44
43
  */
45
44
 
46
45
  import { isAddressTuple } from "./address.mjs";
47
- import { positionInFrontmatter } from "./diagnostics.mjs";
46
+ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
48
47
  import { parseAddress } from "./address.mjs";
49
48
  import { readCanonicalKey } from "./content-address.mjs";
50
49
 
@@ -66,9 +65,9 @@ const PLACE_TYPES = Object.freeze(new Set(["place"]));
66
65
  export const HOLDINGS_KEYS = Object.freeze(["contains", "held_by", "holdings"]);
67
66
 
68
67
  /**
69
- * The place subTypes tenure is checked on. A region is held through its
70
- * polity's `domains`, a world by nobody, a feature by nobody — a river has no
71
- * lord — so the rest are the kinds of place a body holds directly.
68
+ * Legacy subtypes of the retired tenure advisory. Government advisories
69
+ * apply to every place subtype when its authored population is positive.
70
+ * @deprecated This list no longer gates any advisory.
72
71
  *
73
72
  * @type {readonly string[]}
74
73
  */
@@ -373,71 +372,44 @@ export function holdingsPages(nodes) {
373
372
  /* --------------------------------------------------------------------- */
374
373
 
375
374
  /**
376
- * Every shortcode some affiliation's `domains` names, computed once per link
377
- * index. The lint asks the question once per place note, and the answer is a
378
- * fact about the whole corpus — local notes and every fetched entry — so it
379
- * is read off the index the first time and kept beside it.
380
- *
381
- * @type {WeakMap<object, Set<string>>}
382
- */
383
- const HELD = new WeakMap();
384
-
385
- /**
386
- * The shortcodes every affiliation's `domains` names, across the link index.
387
- *
388
- * @param {object} index - The link index.
389
- * @returns {Set<string>} The held shortcodes, lower case.
375
+ * Advise when a place with an authored positive population omits government.
376
+ * Missing, null or zero population establishes no inhabited population.
377
+ * Property presence distinguishes omitted government from explicit null,
378
+ * which means complete anarchy. Non-null values are validated by the declared
379
+ * affiliation reference field. Neither geographic containment nor tenure
380
+ * supplies this fact, and the rule is independent of place subtype.
381
+ *
382
+ * @param {object} note - The note with parsed frontmatter and raw source.
383
+ * @returns {object[]} Located warning findings.
390
384
  */
391
- function heldShortcodes(index) {
392
- let held = HELD.get(index);
393
- if (held) return held;
394
- held = new Set();
395
- for (const note of index.notes ?? []) {
396
- if (String(note.type ?? note.fm?.type ?? "") !== "affiliation") continue;
397
- const data = note.fm?.data;
398
- const domains = data && typeof data === "object" ? data.domains : undefined;
399
- for (const shortcode of shortcodesOf(domains)) held.add(shortcode);
400
- }
401
- for (const entry of index.foreign?.index?.values?.() ?? []) {
402
- if (String(entry?.type ?? "") !== "affiliation") continue;
403
- for (const shortcode of shortcodesOf(entry.domains)) held.add(shortcode);
404
- }
405
- HELD.set(index, held);
406
- return held;
407
- }
408
-
409
- /**
410
- * Check a place note's tenure: a settlement, a site or a structure that no
411
- * affiliation's `domains` names is unheld land, reported as a warning at the
412
- * note's `type:` line.
413
- *
414
- * Declared on the `place` vocabulary as its type-level check, so the lint
415
- * runs it beside the field checks with the same index. Without an index the
416
- * question cannot be asked and nothing is reported.
417
- *
418
- * @param {object} note - The note, as the link index hands it over.
419
- * @param {object} [opts]
420
- * @param {object} [opts.index] - The link index, holding every affiliation.
421
- * @returns {object[]} Findings.
422
- */
423
- export function checkHeld(note, { index } = {}) {
424
- if (!index) return [];
385
+ export function checkGovernment(note) {
425
386
  const fm = note.fm ?? {};
426
- if (String(fm.type ?? "") !== "place") return [];
427
- const subType = String(fm.subType ?? "");
428
- if (!HELD_SUBTYPES.includes(subType)) return [];
429
- const shortcode = String(fm.shortcode ?? "").toLowerCase();
430
- if (!shortcode) return [];
431
- if (heldShortcodes(index).has(shortcode)) return [];
387
+ if (fm.type !== "place") return [];
388
+ const data = fm.data;
389
+ if (!data || typeof data !== "object" || Array.isArray(data)) return [];
390
+ const population = data.population;
391
+ if (typeof population !== "number" && typeof population !== "string") return [];
392
+ const count = Number(population);
393
+ if (!Number.isFinite(count) || count <= 0 || Object.hasOwn(data, "government")) return [];
432
394
  return [
433
395
  {
434
396
  file: note.file,
435
- ...positionInFrontmatter(note.raw ?? "", "type", undefined, { topLevel: true }),
397
+ ...positionOfFrontmatterPath(note.raw ?? "", ["data", "population"], { key: true }),
436
398
  severity: "warning",
437
399
  message:
438
- `unheld land: no affiliation's \`domains\` names "${shortcode}", so nothing ` +
439
- `says who holds this ${subType}; name it in the \`domains\` of the house, ` +
440
- `order or polity that does`,
400
+ `missing government: place "${fm.shortcode ?? ""}" has positive population ` +
401
+ "but no `data.government`; name an affiliation or write null for complete anarchy",
441
402
  },
442
403
  ];
443
404
  }
405
+
406
+ /**
407
+ * The historical API name for the government advisory. Tenure is no longer checked.
408
+ * @deprecated Use checkGovernment.
409
+ * @param {object} note - The place note.
410
+ * @param {object} [_opts] - Historical options, no longer needed.
411
+ * @returns {object[]} Government advisory findings.
412
+ */
413
+ export function checkHeld(note, _opts = {}) {
414
+ return checkGovernment(note);
415
+ }
package/engine/index.mjs CHANGED
@@ -99,7 +99,7 @@ export * as stubLint from "./stub-lint.mjs";
99
99
  /** What a place is next to and reachable from: the closed sets, and the checks. */
100
100
  export * as placeRelations from "./place-relations.mjs";
101
101
 
102
- /** What lies within a place, who holds it, and what an affiliation holds — the page lists, and the tenure check. */
102
+ /** What lies within a place, who holds it, and what an affiliation holds — the page lists, and the government advisory. */
103
103
  export * as holdings from "./holdings.mjs";
104
104
  export * as populations from "./populations.mjs";
105
105
 
@@ -427,6 +427,7 @@ export function isUnsetSentinel(value) {
427
427
  /**
428
428
  * Whether a value is worth a row.
429
429
  *
430
+ * A field declaring `nullText` handles explicit null before this general test.
430
431
  * Rule 4: an absent field is absent. `null`, `""` and `[]` are how the corpus
431
432
  * writes "nobody filled this in" — a note that declares every key of its type
432
433
  * and leaves most of them empty is the ordinary shape, not the exception — and
@@ -943,6 +944,15 @@ function noteBox(
943
944
  if (structured.length) shown.add(field.name);
944
945
  continue;
945
946
  }
947
+ if (raw === null && field.nullText) {
948
+ rows.push({
949
+ label: overlay.label ?? humanizeFieldName(field.name),
950
+ kind: "text",
951
+ value: field.nullText,
952
+ });
953
+ shown.add(field.name);
954
+ continue;
955
+ }
946
956
  if (!hasValue(raw)) continue;
947
957
 
948
958
  if (overlay.group) {
@@ -435,6 +435,9 @@ export function loadForeignIndexes(config, localPackages, bases = PACKAGE_BASE)
435
435
  parents: record.data?.parents ?? undefined,
436
436
  domains: record.data?.domains ?? undefined,
437
437
  population: record.data?.population ?? undefined,
438
+ ...(Object.hasOwn(record.data ?? {}, "government") ?
439
+ { government: record.data.government }
440
+ : {}),
438
441
  package: pkg,
439
442
  });
440
443
  const entry = index.get(key);
@@ -112,7 +112,7 @@ import { checkCultureChoice } from "./culture-choice.mjs";
112
112
  import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
113
113
  import { parseNoteDate } from "./note-dates.mjs";
114
114
  import { reckoningContext } from "./reckoning-markers.mjs";
115
- import { checkHeld } from "./holdings.mjs";
115
+ import { checkGovernment } from "./holdings.mjs";
116
116
  import {
117
117
  checkBeingMeasurement,
118
118
  parseBeingHeight,
@@ -153,6 +153,7 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
153
153
  * so each takes a row labelled and anchored by the post's own name.
154
154
  * @property {"subType"} [keySelector] - An alternative `subType:<skill-subtype>` key.
155
155
  * @property {readonly string[]} [accepts] - Allowed Address types, separate from `ref`.
156
+ * @property {string} [nullText] - Infobox text for an explicitly authored null with a declared meaning.
156
157
  * @property {string} [shape] - Human-readable shape, for a finding and for
157
158
  * documentation.
158
159
  * @property {string} [entryShape] - For a `scalar-or-map` field, what one
@@ -512,8 +513,8 @@ const CHARGES = Object.freeze([
512
513
  /**
513
514
  * Everything `place` asks of a whole note.
514
515
  *
515
- * Two questions about one subject, and they are independent: who holds this
516
- * land, and — where the note is a body rather than somewhere within one — what
516
+ * Two questions about one subject, and they are independent: who governs this
517
+ * inhabited place, and — where the note is a body rather than somewhere within one — what
517
518
  * the body states about itself. A type declares one whole-note check, so the
518
519
  * two are composed here rather than either being folded into the other.
519
520
  *
@@ -522,7 +523,7 @@ const CHARGES = Object.freeze([
522
523
  * @returns {object[]} Findings from both.
523
524
  */
524
525
  function checkPlace(note, opts) {
525
- return [...checkHeld(note, opts), ...checkWorldFacts(note, opts)];
526
+ return [...checkGovernment(note), ...checkWorldFacts(note, opts)];
526
527
  }
527
528
 
528
529
  /** Validate a being's authored date using the shared note-date grammar. */
@@ -1563,6 +1564,17 @@ export const NOTE_VOCABULARY = Object.freeze({
1563
1564
  check: checkPopulation,
1564
1565
  describe: "Approximate population, to two significant digits.",
1565
1566
  },
1567
+ {
1568
+ name: "government",
1569
+ ...LINK,
1570
+ shape: "an affiliation Address or null",
1571
+ ref: "affiliation",
1572
+ accepts: ["affiliation"],
1573
+ nullText: "Complete anarchy",
1574
+ describe:
1575
+ "The governing affiliation; explicit null means complete anarchy. " +
1576
+ "A positive population with no government key produces an advisory.",
1577
+ },
1566
1578
  {
1567
1579
  name: "market",
1568
1580
  ...NUM,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "22.25.0",
3
+ "version": "22.26.0",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -29,7 +29,7 @@ export function checkCalendarDateFormats(note: any): {
29
29
  /**
30
30
  * Check a `place` note's world facts.
31
31
  *
32
- * Declared on `place` beside its tenure check, so the lint runs both with the
32
+ * Declared on `place` beside its government advisory, so the lint runs both with the
33
33
  * same index. Four rules, and each is a fact about the setting rather than
34
34
  * about the file:
35
35
  *
@@ -53,22 +53,25 @@ export function foreignHoldingsNodes(foreignIndex: Map<string, object> | undefin
53
53
  */
54
54
  export function holdingsPages(nodes: Iterable<HoldingsNode | null>): Map<string, Holdings>;
55
55
  /**
56
- * Check a place note's tenure: a settlement, a site or a structure that no
57
- * affiliation's `domains` names is unheld land, reported as a warning at the
58
- * note's `type:` line.
56
+ * Advise when a place with an authored positive population omits government.
57
+ * Missing, null or zero population establishes no inhabited population.
58
+ * Property presence distinguishes omitted government from explicit null,
59
+ * which means complete anarchy. Non-null values are validated by the declared
60
+ * affiliation reference field. Neither geographic containment nor tenure
61
+ * supplies this fact, and the rule is independent of place subtype.
59
62
  *
60
- * Declared on the `place` vocabulary as its type-level check, so the lint
61
- * runs it beside the field checks with the same index. Without an index the
62
- * question cannot be asked and nothing is reported.
63
- *
64
- * @param {object} note - The note, as the link index hands it over.
65
- * @param {object} [opts]
66
- * @param {object} [opts.index] - The link index, holding every affiliation.
67
- * @returns {object[]} Findings.
63
+ * @param {object} note - The note with parsed frontmatter and raw source.
64
+ * @returns {object[]} Located warning findings.
65
+ */
66
+ export function checkGovernment(note: object): object[];
67
+ /**
68
+ * The historical API name for the government advisory. Tenure is no longer checked.
69
+ * @deprecated Use checkGovernment.
70
+ * @param {object} note - The place note.
71
+ * @param {object} [_opts] - Historical options, no longer needed.
72
+ * @returns {object[]} Government advisory findings.
68
73
  */
69
- export function checkHeld(note: object, { index }?: {
70
- index?: object | undefined;
71
- }): object[];
74
+ export function checkHeld(note: object, _opts?: object): object[];
72
75
  /**
73
76
  * The keys this module writes, and which a note cannot author: `contains`
74
77
  * and `held_by` on a place, `holdings` on an affiliation.
@@ -77,9 +80,9 @@ export function checkHeld(note: object, { index }?: {
77
80
  */
78
81
  export const HOLDINGS_KEYS: readonly string[];
79
82
  /**
80
- * The place subTypes tenure is checked on. A region is held through its
81
- * polity's `domains`, a world by nobody, a feature by nobody — a river has no
82
- * lord — so the rest are the kinds of place a body holds directly.
83
+ * Legacy subtypes of the retired tenure advisory. Government advisories
84
+ * apply to every place subtype when its authored population is positive.
85
+ * @deprecated This list no longer gates any advisory.
83
86
  *
84
87
  * @type {readonly string[]}
85
88
  */
@@ -27,6 +27,7 @@ export function isUnsetSentinel(value: unknown): boolean;
27
27
  /**
28
28
  * Whether a value is worth a row.
29
29
  *
30
+ * A field declaring `nullText` handles explicit null before this general test.
30
31
  * Rule 4: an absent field is absent. `null`, `""` and `[]` are how the corpus
31
32
  * writes "nobody filled this in" — a note that declares every key of its type
32
33
  * and leaves most of them empty is the ordinary shape, not the exception — and
@@ -380,6 +380,10 @@ export type DataFieldSpec = {
380
380
  * - Allowed Address types, separate from `ref`.
381
381
  */
382
382
  accepts?: readonly string[] | undefined;
383
+ /**
384
+ * - Infobox text for an explicitly authored null with a declared meaning.
385
+ */
386
+ nullText?: string | undefined;
383
387
  /**
384
388
  * - Human-readable shape, for a finding and for
385
389
  * documentation.