@heroiclands/package-build 23.0.0 → 23.1.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 (157) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +1 -1
  3. package/bin/content-commands.mjs +1 -1
  4. package/ci/content-format-reference.mjs +46 -0
  5. package/docs/api.md +124 -141
  6. package/docs/authoring/assets.md +1 -1
  7. package/docs/authoring/dates-and-calendars.md +12 -6
  8. package/docs/authoring/frontmatter.md +2 -2
  9. package/docs/authoring/links-and-markup.md +145 -29
  10. package/docs/commands.md +8 -10
  11. package/docs/configuration.md +13 -11
  12. package/docs/content-format.md +2 -2
  13. package/docs/guides/book.md +59 -4
  14. package/docs/guides/icons.md +4 -5
  15. package/docs/guides/packs.md +6 -0
  16. package/docs/guides/reference-pages.md +21 -1
  17. package/docs/guides/release-and-deploy.md +39 -7
  18. package/docs/guides/site.md +17 -18
  19. package/docs/reference/format-details.md +877 -276
  20. package/docs/reference/note-types.md +278 -91
  21. package/docs/reference/theme-pages.md +58 -97
  22. package/engine/address-values.mjs +124 -4
  23. package/engine/address.mjs +44 -3
  24. package/engine/anchors.mjs +103 -3
  25. package/engine/art-slots.mjs +2 -9
  26. package/engine/base-compiler.mjs +25 -6
  27. package/engine/calendar-artifacts.mjs +4 -1
  28. package/engine/calendar-notes.mjs +166 -102
  29. package/engine/content-format-check.mjs +2 -3
  30. package/engine/content-format.mjs +44 -1
  31. package/engine/content-format.yaml +79 -16
  32. package/engine/content-images.mjs +10 -1
  33. package/engine/content-index.mjs +37 -9
  34. package/engine/content-links.mjs +24 -5
  35. package/engine/content-tables.mjs +24 -3
  36. package/engine/data-keys.mjs +347 -0
  37. package/engine/derived-sections.mjs +322 -0
  38. package/engine/diagnostics.mjs +28 -0
  39. package/engine/event-fields.mjs +187 -0
  40. package/engine/event-rows.mjs +126 -0
  41. package/engine/event-views.mjs +305 -0
  42. package/engine/exported-scene.mjs +29 -53
  43. package/engine/frontmatter-lint.mjs +103 -77
  44. package/engine/frontmatter.mjs +7 -2
  45. package/engine/generate.mjs +53 -0
  46. package/engine/generated-sections.mjs +223 -0
  47. package/engine/heading-attributes.mjs +4 -2
  48. package/engine/helpers.mjs +12 -1
  49. package/engine/holdings.mjs +12 -33
  50. package/engine/ids.mjs +17 -11
  51. package/engine/index.mjs +4 -4
  52. package/engine/infobox-render.mjs +21 -6
  53. package/engine/infobox.mjs +217 -99
  54. package/engine/journals.mjs +104 -47
  55. package/engine/literature-notes.mjs +4 -1
  56. package/engine/literature-works.mjs +21 -13
  57. package/engine/map-graphviz.mjs +4 -18
  58. package/engine/markdown-expressions.mjs +116 -11
  59. package/engine/metadata-index.mjs +26 -0
  60. package/engine/near-miss.mjs +90 -0
  61. package/engine/note-addresses.mjs +75 -19
  62. package/engine/note-dates.mjs +5 -3
  63. package/engine/note-event-terms.mjs +364 -0
  64. package/engine/note-events.mjs +823 -0
  65. package/engine/note-vocabulary.mjs +184 -88
  66. package/engine/office-holders.mjs +20 -54
  67. package/engine/packagebuild-index.mjs +16 -1
  68. package/engine/pdf-build.mjs +97 -60
  69. package/engine/pdf-index.mjs +263 -0
  70. package/engine/pdf-render.mjs +47 -5
  71. package/engine/place-relation-terms.mjs +174 -0
  72. package/engine/place-relations.mjs +72 -160
  73. package/engine/rank-ladder.mjs +14 -41
  74. package/engine/reckoning-markers.mjs +2 -0
  75. package/engine/region-events.mjs +6 -12
  76. package/engine/scenes.mjs +73 -468
  77. package/engine/site-build.mjs +75 -101
  78. package/engine/site-index.mjs +3 -1
  79. package/engine/site-maps.mjs +43 -29
  80. package/engine/site-preview.mjs +40 -31
  81. package/engine/social-ties.mjs +2 -1
  82. package/engine/sql-tables.mjs +212 -9
  83. package/engine/standing-terms.mjs +145 -6
  84. package/engine/standings.mjs +37 -49
  85. package/engine/web-wikilinks.mjs +21 -1
  86. package/engine/wikilink-syntax.mjs +34 -4
  87. package/engine/wikilinks.mjs +27 -2
  88. package/engine/yaml-lint.mjs +7 -4
  89. package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
  90. package/hugo-theme/layouts/partials/page-body.html +0 -3
  91. package/hugo-theme/static/css/style.css +25 -55
  92. package/package.json +1 -2
  93. package/packagebuild-metadata.jsonl +13 -13
  94. package/release.mjs +62 -4
  95. package/sohl/note-schemas.mjs +8 -73
  96. package/types/engine/address-values.d.mts +50 -3
  97. package/types/engine/address.d.mts +21 -0
  98. package/types/engine/anchors.d.mts +49 -2
  99. package/types/engine/art-slots.d.mts +1 -1
  100. package/types/engine/base-compiler.d.mts +13 -4
  101. package/types/engine/calendar-notes.d.mts +7 -0
  102. package/types/engine/content-format.d.mts +52 -0
  103. package/types/engine/content-images.d.mts +5 -1
  104. package/types/engine/content-tables.d.mts +9 -1
  105. package/types/engine/data-keys.d.mts +145 -0
  106. package/types/engine/derived-sections.d.mts +73 -0
  107. package/types/engine/diagnostics.d.mts +15 -0
  108. package/types/engine/event-fields.d.mts +99 -0
  109. package/types/engine/event-rows.d.mts +12 -0
  110. package/types/engine/event-views.d.mts +67 -0
  111. package/types/engine/exported-scene.d.mts +5 -2
  112. package/types/engine/frontmatter-lint.d.mts +3 -3
  113. package/types/engine/generated-sections.d.mts +143 -0
  114. package/types/engine/holdings.d.mts +6 -20
  115. package/types/engine/ids.d.mts +15 -12
  116. package/types/engine/index.d.mts +1 -1
  117. package/types/engine/infobox.d.mts +13 -3
  118. package/types/engine/journals.d.mts +24 -11
  119. package/types/engine/literature-works.d.mts +1 -2
  120. package/types/engine/map-graphviz.d.mts +2 -10
  121. package/types/engine/markdown-expressions.d.mts +12 -3
  122. package/types/engine/near-miss.d.mts +42 -0
  123. package/types/engine/note-addresses.d.mts +7 -1
  124. package/types/engine/note-event-terms.d.mts +68 -0
  125. package/types/engine/note-events.d.mts +71 -0
  126. package/types/engine/note-vocabulary.d.mts +37 -8
  127. package/types/engine/office-holders.d.mts +3 -0
  128. package/types/engine/pdf-index.d.mts +35 -0
  129. package/types/engine/pdf-render.d.mts +8 -2
  130. package/types/engine/place-relation-terms.d.mts +83 -0
  131. package/types/engine/place-relations.d.mts +11 -66
  132. package/types/engine/rank-ladder.d.mts +2 -0
  133. package/types/engine/region-events.d.mts +6 -12
  134. package/types/engine/scenes.d.mts +6 -32
  135. package/types/engine/site-build.d.mts +8 -22
  136. package/types/engine/site-maps.d.mts +15 -7
  137. package/types/engine/sql-tables.d.mts +77 -2
  138. package/types/engine/standing-terms.d.mts +37 -4
  139. package/types/engine/standings.d.mts +14 -9
  140. package/types/engine/wikilink-syntax.d.mts +20 -0
  141. package/types/engine/wikilinks.d.mts +3 -2
  142. package/types/engine/yaml-lint.d.mts +5 -4
  143. package/types/release.d.mts +4 -0
  144. package/docs/guides/government-migration.md +0 -52
  145. package/engine/itinerary-scenes.mjs +0 -166
  146. package/engine/lore-events.mjs +0 -197
  147. package/engine/map-notes.mjs +0 -1309
  148. package/engine/map-raster.mjs +0 -51
  149. package/hugo-theme/layouts/partials/holdings/entry.html +0 -21
  150. package/hugo-theme/layouts/partials/holdings/kind-groups.html +0 -71
  151. package/hugo-theme/layouts/partials/holdings.html +0 -61
  152. package/hugo-theme/layouts/partials/literature-works.html +0 -29
  153. package/hugo-theme/layouts/partials/place-map.html +0 -9
  154. package/types/engine/itinerary-scenes.d.mts +0 -29
  155. package/types/engine/lore-events.d.mts +0 -14
  156. package/types/engine/map-notes.d.mts +0 -297
  157. package/types/engine/map-raster.d.mts +0 -8
@@ -0,0 +1,174 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * The closed sets a place's `borders` and `routes` are held to, and the keys
16
+ * one entry of each carries.
17
+ *
18
+ * A leaf module, importing nothing: the note vocabulary reads the entry
19
+ * declarations as it loads, and {@link module:engine/place-relations}, which
20
+ * checks what the values mean, reaches the vocabulary through the Address
21
+ * reader.
22
+ *
23
+ * @module
24
+ */
25
+
26
+ /* --------------------------------------------------------------------- */
27
+ /* The closed sets */
28
+ /* --------------------------------------------------------------------- */
29
+
30
+ /**
31
+ * The type a border's or a route's `to` names.
32
+ *
33
+ * It is both the default its `<type>` segment takes when an author omits it and
34
+ * the whole of the set the position accepts, which is what makes `vylar` and
35
+ * `place-vylar` the same Address and a `to` naming a `lore` note an error rather
36
+ * than a lookup that quietly finds nothing.
37
+ *
38
+ * @type {string}
39
+ */
40
+ export const RELATION_TYPE = "place";
41
+
42
+ /**
43
+ * The eight compass bearings, clockwise from north.
44
+ *
45
+ * Where a neighbour or a destination lies from the place stating it. The
46
+ * order is load-bearing: {@link oppositeBearing} reads four steps around it.
47
+ *
48
+ * @type {readonly string[]}
49
+ */
50
+ export const BEARINGS = Object.freeze(["N", "NE", "E", "SE", "S", "SW", "W", "NW"]);
51
+
52
+ /**
53
+ * The modes a route is travelled by: `land`, `boat` on a river or a lake,
54
+ * `ship` on open sea.
55
+ *
56
+ * @type {readonly string[]}
57
+ */
58
+ export const ROUTE_MODES = Object.freeze(["land", "boat", "ship"]);
59
+
60
+ /**
61
+ * The days markers a route may state.
62
+ *
63
+ * Each is "about this, under normal conditions": `10`, `20` and `30` are one,
64
+ * two and three ten-day weeks, `180` is many months, `360` is "who knows".
65
+ *
66
+ * @type {readonly number[]}
67
+ */
68
+ export const TRAVEL_DAYS = Object.freeze([1, 2, 3, 5, 10, 20, 30, 45, 60, 90, 180, 360]);
69
+
70
+ /**
71
+ * The terrain registry, each terrain naming the modes that cross it.
72
+ *
73
+ * A land terrain is crossed by land alone; open sea by ship; a river or a lake
74
+ * by boat; a coast by all three, because a coast road and a coasting voyage
75
+ * are both journeys along it.
76
+ *
77
+ * @type {Readonly<Record<string, readonly string[]>>}
78
+ */
79
+ export const TERRAIN_MODES = Object.freeze({
80
+ road: Object.freeze(["land"]),
81
+ track: Object.freeze(["land"]),
82
+ plain: Object.freeze(["land"]),
83
+ steppe: Object.freeze(["land"]),
84
+ hills: Object.freeze(["land"]),
85
+ mountains: Object.freeze(["land"]),
86
+ forest: Object.freeze(["land"]),
87
+ jungle: Object.freeze(["land"]),
88
+ marsh: Object.freeze(["land"]),
89
+ dunes: Object.freeze(["land"]),
90
+ desert: Object.freeze(["land"]),
91
+ ice: Object.freeze(["land"]),
92
+ coast: Object.freeze(["land", "boat", "ship"]),
93
+ "open-sea": Object.freeze(["ship"]),
94
+ river: Object.freeze(["boat"]),
95
+ lake: Object.freeze(["boat"]),
96
+ });
97
+
98
+ /**
99
+ * The terrain names, in registry order — the closed set a `terrain` list
100
+ * draws from.
101
+ *
102
+ * @type {readonly string[]}
103
+ */
104
+ export const TERRAINS = Object.freeze(Object.keys(TERRAIN_MODES));
105
+
106
+ /** A list entry's `to` and `bearing`, which both relations carry. */
107
+ const RELATION_ENDS = Object.freeze([
108
+ Object.freeze({
109
+ name: "to",
110
+ kind: "address",
111
+ required: true,
112
+ shape: "the other place — its shortcode, or its Address",
113
+ describe: "The other place, by Address; the type defaults to `place`.",
114
+ }),
115
+ Object.freeze({
116
+ name: "bearing",
117
+ kind: "string",
118
+ required: true,
119
+ shape: `one of ${BEARINGS.join(", ")}`,
120
+ describe: "Where the other place lies from here.",
121
+ }),
122
+ ]);
123
+
124
+ /**
125
+ * The keys a `borders` entry carries, as inner-key declarations. Both are
126
+ * required.
127
+ *
128
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
129
+ */
130
+ export const BORDER_FIELDS = RELATION_ENDS;
131
+
132
+ /**
133
+ * The keys a `routes` entry carries, as inner-key declarations. `to`,
134
+ * `bearing`, `mode` and `days` are required; `terrain` and `leagues` are
135
+ * optional.
136
+ *
137
+ * @type {readonly import("./data-keys.mjs").InnerKeySpec[]}
138
+ */
139
+ export const ROUTE_FIELDS = Object.freeze([
140
+ ...RELATION_ENDS,
141
+ Object.freeze({
142
+ name: "mode",
143
+ kind: "string",
144
+ required: true,
145
+ shape: `one of ${ROUTE_MODES.join(", ")}`,
146
+ describe: "How the journey is travelled.",
147
+ }),
148
+ Object.freeze({
149
+ name: "days",
150
+ kind: "number",
151
+ required: true,
152
+ shape: `one of ${TRAVEL_DAYS.join(", ")}`,
153
+ describe: "About how many days the journey takes, under normal conditions.",
154
+ }),
155
+ Object.freeze({
156
+ name: "terrain",
157
+ kind: "list",
158
+ shape: "a list of terrains in travel order",
159
+ entries: Object.freeze({ kind: "string", shape: "a terrain" }),
160
+ describe: "The terrains crossed, in travel order, each crossable by the mode.",
161
+ }),
162
+ Object.freeze({
163
+ name: "leagues",
164
+ kind: "number",
165
+ shape: "a distance in leagues",
166
+ describe: "The distance, in leagues.",
167
+ }),
168
+ ]);
169
+
170
+ /** The keys a `borders` entry carries. @type {readonly string[]} */
171
+ export const BORDER_KEYS = Object.freeze(BORDER_FIELDS.map((field) => field.name));
172
+
173
+ /** The keys a `routes` entry carries. @type {readonly string[]} */
174
+ export const ROUTE_KEYS = Object.freeze(ROUTE_FIELDS.map((field) => field.name));
@@ -34,8 +34,8 @@
34
34
  * shortcode in different packages never answer for each other.
35
35
  *
36
36
  * **The value sets are closed and stated once.** The eight bearings, the three
37
- * travel modes, the days markers and the terrain registry are the constants
38
- * below, read by the lint, compared against the specification's tables by the
37
+ * travel modes, the days markers and the terrain registry are the constants of
38
+ * {@link module:engine/place-relation-terms}, re-exported here, read by the lint, compared against the specification's tables by the
39
39
  * test suite, and exported for a consumer drawing a map. A second copy of any
40
40
  * of them is one more list free to disagree.
41
41
  *
@@ -69,100 +69,31 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
69
69
  import { acceptsType, parseAddress, renderAddress } from "./address.mjs";
70
70
  import { NOTE_SYSTEM } from "./systems.mjs";
71
71
 
72
- /* --------------------------------------------------------------------- */
73
- /* The closed sets */
74
- /* --------------------------------------------------------------------- */
75
-
76
- /**
77
- * The type a border's or a route's `to` names.
78
- *
79
- * It is both the default its `<type>` segment takes when an author omits it and
80
- * the whole of the set the position accepts, which is what makes `vylar` and
81
- * `place-vylar` the same Address and a `to` naming a `lore` note an error rather
82
- * than a lookup that quietly finds nothing.
83
- *
84
- * @type {string}
85
- */
86
- export const RELATION_TYPE = "place";
87
-
88
- /**
89
- * The eight compass bearings, clockwise from north.
90
- *
91
- * Where a neighbour or a destination lies from the place stating it. The
92
- * order is load-bearing: {@link oppositeBearing} reads four steps around it.
93
- *
94
- * @type {readonly string[]}
95
- */
96
- export const BEARINGS = Object.freeze(["N", "NE", "E", "SE", "S", "SW", "W", "NW"]);
97
-
98
- /**
99
- * The modes a route is travelled by: `land`, `boat` on a river or a lake,
100
- * `ship` on open sea.
101
- *
102
- * @type {readonly string[]}
103
- */
104
- export const ROUTE_MODES = Object.freeze(["land", "boat", "ship"]);
105
-
106
- /**
107
- * The days markers a route may state.
108
- *
109
- * Each is "about this, under normal conditions": `10`, `20` and `30` are one,
110
- * two and three ten-day weeks, `180` is many months, `360` is "who knows".
111
- *
112
- * @type {readonly number[]}
113
- */
114
- export const TRAVEL_DAYS = Object.freeze([1, 2, 3, 5, 10, 20, 30, 45, 60, 90, 180, 360]);
115
-
116
- /**
117
- * The terrain registry, each terrain naming the modes that cross it.
118
- *
119
- * A land terrain is crossed by land alone; open sea by ship; a river or a lake
120
- * by boat; a coast by all three, because a coast road and a coasting voyage
121
- * are both journeys along it.
122
- *
123
- * @type {Readonly<Record<string, readonly string[]>>}
124
- */
125
- export const TERRAIN_MODES = Object.freeze({
126
- road: Object.freeze(["land"]),
127
- track: Object.freeze(["land"]),
128
- plain: Object.freeze(["land"]),
129
- steppe: Object.freeze(["land"]),
130
- hills: Object.freeze(["land"]),
131
- mountains: Object.freeze(["land"]),
132
- forest: Object.freeze(["land"]),
133
- jungle: Object.freeze(["land"]),
134
- marsh: Object.freeze(["land"]),
135
- dunes: Object.freeze(["land"]),
136
- desert: Object.freeze(["land"]),
137
- ice: Object.freeze(["land"]),
138
- coast: Object.freeze(["land", "boat", "ship"]),
139
- "open-sea": Object.freeze(["ship"]),
140
- river: Object.freeze(["boat"]),
141
- lake: Object.freeze(["boat"]),
142
- });
143
-
144
- /**
145
- * The terrain names, in registry order — the closed set a `terrain` list
146
- * draws from.
147
- *
148
- * @type {readonly string[]}
149
- */
150
- export const TERRAINS = Object.freeze(Object.keys(TERRAIN_MODES));
151
-
152
- /**
153
- * The keys a `borders` entry may carry. Both are required.
154
- *
155
- * @type {readonly string[]}
156
- */
157
- export const BORDER_KEYS = Object.freeze(["to", "bearing"]);
158
-
159
- /**
160
- * The keys a `routes` entry may carry. `to`, `bearing`, `mode` and `days` are
161
- * required; `terrain` and `leagues` are optional.
162
- *
163
- * @type {readonly string[]}
164
- */
165
- export const ROUTE_KEYS = Object.freeze(["to", "bearing", "mode", "days", "terrain", "leagues"]);
72
+ import {
73
+ RELATION_TYPE,
74
+ BEARINGS,
75
+ ROUTE_MODES,
76
+ TRAVEL_DAYS,
77
+ TERRAIN_MODES,
78
+ TERRAINS,
79
+ BORDER_FIELDS,
80
+ ROUTE_FIELDS,
81
+ BORDER_KEYS,
82
+ ROUTE_KEYS,
83
+ } from "./place-relation-terms.mjs";
84
+
85
+ export {
86
+ RELATION_TYPE,
87
+ BEARINGS,
88
+ ROUTE_MODES,
89
+ TRAVEL_DAYS,
90
+ TERRAIN_MODES,
91
+ TERRAINS,
92
+ BORDER_FIELDS,
93
+ ROUTE_FIELDS,
94
+ BORDER_KEYS,
95
+ ROUTE_KEYS,
96
+ };
166
97
 
167
98
  /**
168
99
  * The bearing the other end of a relation states, or `undefined` for a value
@@ -362,7 +293,6 @@ function checkRelation(note, { field, index }) {
362
293
  })
363
294
  : undefined;
364
295
  const isRoute = field === "routes";
365
- const keys = isRoute ? ROUTE_KEYS : BORDER_KEYS;
366
296
  const at = (i, key) =>
367
297
  positionOfFrontmatterPath(raw, ["data", field, i, ...(key ? [key] : [])]);
368
298
  const label = (i, key) => `\`data.${field}[${i}]${key ? `.${key}` : ""}\``;
@@ -373,31 +303,20 @@ function checkRelation(note, { field, index }) {
373
303
  message,
374
304
  });
375
305
  const error = (i, key, message) => findings.push(finding(i, key, "error", message));
306
+ // Absent, or not one value: the inner-key check's finding, made from the
307
+ // declaration, so nothing here repeats it.
308
+ const unread = (value) =>
309
+ value === undefined || value === null || value === "" || typeof value === "object";
376
310
 
377
311
  /** `to` → the modes it has been listed with, for the once-per-pair rule. */
378
312
  const seen = new Map();
379
313
 
380
314
  list.forEach((entry, i) => {
381
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
382
- error(
383
- i,
384
- undefined,
385
- `${label(i)} must be a map — \`{ to: <shortcode>, bearing: <bearing>` +
386
- `${isRoute ? ", mode: <mode>, days: <days>" : ""} }\` — but reads ` +
387
- JSON.stringify(entry),
388
- );
389
- return;
390
- }
391
-
392
- for (const key of Object.keys(entry)) {
393
- if (keys.includes(key)) continue;
394
- error(
395
- i,
396
- key,
397
- `"${key}" is not a key of a ${field} entry; an entry carries ` +
398
- `${oneOf(keys.map((k) => `\`${k}\``))} and nothing else`,
399
- );
400
- }
315
+ // An entry that is not a map, an undeclared key, a required key left
316
+ // out and a value of the wrong kind are the inner-key check's
317
+ // findings, made from `BORDER_FIELDS` and `ROUTE_FIELDS`. What is
318
+ // checked here is what the values mean.
319
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return;
401
320
 
402
321
  // `to`: an Address naming a place, and one a place declares.
403
322
  const to = entry.to;
@@ -407,7 +326,7 @@ function checkRelation(note, { field, index }) {
407
326
  let toAddress;
408
327
  const read = parseAddress(to, here);
409
328
  if (to === undefined || to === null || to === "") {
410
- error(i, undefined, `${label(i)} must name the other place in \`to\``);
329
+ toIsValid = false;
411
330
  } else if (
412
331
  read.reason === "no-content-index" ||
413
332
  (!read.reason && here.noIndexPackages?.has(read.package))
@@ -468,8 +387,8 @@ function checkRelation(note, { field, index }) {
468
387
  // `bearing`.
469
388
  const bearing = entry.bearing;
470
389
  let bearingIsValid = false;
471
- if (bearing === undefined || bearing === null || bearing === "") {
472
- error(i, undefined, `${label(i)} must state a \`bearing\` — ${oneOf(BEARINGS)}`);
390
+ if (unread(bearing)) {
391
+ bearingIsValid = false;
473
392
  } else if (!BEARINGS.includes(String(bearing))) {
474
393
  error(
475
394
  i,
@@ -485,8 +404,8 @@ function checkRelation(note, { field, index }) {
485
404
  let mode;
486
405
  let days;
487
406
  if (isRoute) {
488
- if (entry.mode === undefined || entry.mode === null || entry.mode === "") {
489
- error(i, undefined, `${label(i)} must state a \`mode\` — ${oneOf(ROUTE_MODES)}`);
407
+ if (unread(entry.mode)) {
408
+ mode = undefined;
490
409
  } else if (!ROUTE_MODES.includes(String(entry.mode))) {
491
410
  error(
492
411
  i,
@@ -498,9 +417,7 @@ function checkRelation(note, { field, index }) {
498
417
  mode = String(entry.mode);
499
418
  }
500
419
 
501
- if (entry.days === undefined || entry.days === null || entry.days === "") {
502
- error(i, undefined, `${label(i)} must state \`days\` — ${oneOf(TRAVEL_DAYS)}`);
503
- } else {
420
+ if (!unread(entry.days)) {
504
421
  const n = daysOf(entry.days);
505
422
  if (n === undefined || !TRAVEL_DAYS.includes(n)) {
506
423
  error(
@@ -515,44 +432,39 @@ function checkRelation(note, { field, index }) {
515
432
  }
516
433
  }
517
434
 
518
- if (entry.terrain !== undefined && entry.terrain !== null) {
519
- if (!Array.isArray(entry.terrain)) {
520
- error(
521
- i,
522
- "terrain",
523
- `${label(i, "terrain")} must be a list of terrains in travel order, ` +
524
- `but reads ${JSON.stringify(entry.terrain)}`,
525
- );
526
- } else {
527
- entry.terrain.forEach((terrain, j) => {
528
- const name = String(terrain);
529
- const modes = TERRAIN_MODES[name];
530
- if (!modes) {
531
- findings.push({
532
- file: note.file,
533
- ...positionOfFrontmatterPath(raw, ["data", field, i, "terrain", j]),
534
- severity: "error",
535
- message:
536
- `${label(i, "terrain")} names "${name}", which is not a ` +
537
- `terrain; the registry is ${oneOf(TERRAINS)}`,
538
- });
539
- } else if (mode && !modes.includes(mode)) {
540
- findings.push({
541
- file: note.file,
542
- ...positionOfFrontmatterPath(raw, ["data", field, i, "terrain", j]),
543
- severity: "error",
544
- message:
545
- `${label(i, "terrain")} names "${name}", which is crossed ` +
546
- `by ${oneOf(modes)}, but the route is by ${mode}`,
547
- });
548
- }
549
- });
550
- }
435
+ // A `terrain` that is not a list is the inner-key check's finding.
436
+ if (Array.isArray(entry.terrain)) {
437
+ entry.terrain.forEach((terrain, j) => {
438
+ if (unread(terrain)) return;
439
+ const name = String(terrain);
440
+ const modes = TERRAIN_MODES[name];
441
+ if (!modes) {
442
+ findings.push({
443
+ file: note.file,
444
+ ...positionOfFrontmatterPath(raw, ["data", field, i, "terrain", j]),
445
+ severity: "error",
446
+ message:
447
+ `${label(i, "terrain")} names "${name}", which is not a ` +
448
+ `terrain; the registry is ${oneOf(TERRAINS)}`,
449
+ });
450
+ } else if (mode && !modes.includes(mode)) {
451
+ findings.push({
452
+ file: note.file,
453
+ ...positionOfFrontmatterPath(raw, ["data", field, i, "terrain", j]),
454
+ severity: "error",
455
+ message:
456
+ `${label(i, "terrain")} names "${name}", which is crossed ` +
457
+ `by ${oneOf(modes)}, but the route is by ${mode}`,
458
+ });
459
+ }
460
+ });
551
461
  }
552
462
 
553
- if (entry.leagues !== undefined && entry.leagues !== null) {
463
+ // Not a number at all is the inner-key check's finding; a number
464
+ // that is no distance is this one's.
465
+ if (!unread(entry.leagues)) {
554
466
  const n = daysOf(entry.leagues);
555
- if (n === undefined || n <= 0) {
467
+ if (n !== undefined && n <= 0) {
556
468
  error(
557
469
  i,
558
470
  "leagues",
@@ -30,25 +30,22 @@
30
30
  * nothing, and one missing its `description` ships a rung that says nothing on
31
31
  * a sheet or a page.
32
32
  *
33
- * Two things are deliberately **not** checked here, because something else
34
- * already does and a finding reported twice is a finding read once and fixed
35
- * neither time:
36
- *
37
- * - **that `ranks` is a list at all** — the field declares `kind: "list"`, and
38
- * the shape check names it;
39
- * - **that `lore` resolves** — the reference checker reads every Address in the
40
- * note, this one included.
33
+ * What a rung *holds* is declared in {@link RUNG_FIELDS} and checked by the
34
+ * inner-key check: that `ranks` is a list of maps, that a rung carries no key
35
+ * beyond the four, that `level`, `title` and `description` are stated, and that
36
+ * each is the kind it is declared — a `level` a whole number. What is checked
37
+ * here is what only a ladder can say — that a level is held by one rung — and nothing is
38
+ * reported twice, because a finding reported twice is a finding read once and
39
+ * fixed neither time. That `lore` resolves is the reference checker's.
41
40
  *
42
41
  * @module
43
42
  */
44
43
 
45
44
  import { positionOfFrontmatterPath } from "./diagnostics.mjs";
46
45
 
47
- /** The keys a rung may carry. */
48
- const RUNG_KEYS = Object.freeze(["level", "title", "description", "lore"]);
46
+ import { RUNG_FIELDS } from "./standing-terms.mjs";
49
47
 
50
- /** The keys a rung must carry a value for. */
51
- const REQUIRED_TEXT = Object.freeze(["title", "description"]);
48
+ export { RUNG_FIELDS };
52
49
 
53
50
  /** Every affiliation has an ordinary standing that a being can name as rank 1. */
54
51
  export function checkAffiliationRankFloor(note) {
@@ -176,27 +173,12 @@ export function checkRankLadder(note) {
176
173
  const which =
177
174
  integer(rung?.level) ? `rank ${rung.level}` : `rank ${index + 1} of the ladder`;
178
175
 
179
- if (!mapping(rung)) {
180
- at([rungPath, base], `${which} must be a map of level, title and description`);
181
- return;
182
- }
183
-
184
- for (const key of Object.keys(rung)) {
185
- if (RUNG_KEYS.includes(key)) continue;
186
- at(
187
- [[...rungPath, key], rungPath],
188
- `${which} has unknown key ${key}; a rung takes ${RUNG_KEYS.join(", ")}`,
189
- );
190
- }
176
+ // Not a map, an undeclared key, and an absent or mistyped `level`,
177
+ // `title` or `description` are the inner-key check's findings.
178
+ if (!mapping(rung)) return;
191
179
 
192
- if (!integer(rung.level)) {
193
- at(
194
- [[...rungPath, "level"], rungPath],
195
- rung.level === undefined || rung.level === null ?
196
- `${which} needs a level — the rung's position on this body's own ladder`
197
- : `${which} has a level that is not a whole number`,
198
- );
199
- } else {
180
+ // A level that is not a whole number is the inner-key check's finding.
181
+ if (integer(rung.level)) {
200
182
  // A level belongs to one rung. Two rungs claiming it leave a
201
183
  // member's `rank` answering to the one written first, so the
202
184
  // ladder's own order decides what the standing is called and
@@ -215,15 +197,6 @@ export function checkRankLadder(note) {
215
197
  `each rung states its own`,
216
198
  );
217
199
  }
218
-
219
- for (const key of REQUIRED_TEXT) {
220
- if (stated(rung[key])) continue;
221
- at(
222
- [[...rungPath, key], rungPath],
223
- `${which} needs a ${key}: ` +
224
- (key === "title" ? "what the standing is called" : "what the standing is"),
225
- );
226
- }
227
200
  });
228
201
 
229
202
  return findings;
@@ -131,6 +131,8 @@ export function resolveReckoningMarkers(index, daysPerYear) {
131
131
  );
132
132
  const byStart = new Map();
133
133
  for (const row of declared) {
134
+ // A fractional start is the inner-key check's one finding.
135
+ if (typeof row.start === "number" && !Number.isInteger(row.start)) continue;
134
136
  if (row.start !== null && (!Number.isSafeInteger(row.start) || row.start < 1)) {
135
137
  error(
136
138
  note,
@@ -15,19 +15,13 @@
15
15
  * The curated Foundry region-event vocabulary, as plain data.
16
16
  *
17
17
  * Deliberately **plain ESM** — no TypeScript, no `@src` aliases, no Foundry —
18
- * for the same reason `../sohl/default-item-art.mjs` is: the map-note pack
19
- * compiler runs under bare `node`, outside the bundler that resolves `@src` and
20
- * strips types, and it must reject an authored region event that the runtime
21
- * would silently drop. One list here is what keeps the build-time lint and the
22
- * runtime bridge from drifting apart.
18
+ * so the Foundry runtime and any build-time consumer read one list, and the
19
+ * events a region trigger forwards cannot drift between them.
23
20
  *
24
- * It sits in this package rather than in the system's `src/` tree because the
25
- * map-note compiler that reads it is installed as a dependency, and a
26
- * relative path out of the package would resolve to garbage from
27
- * `node_modules`. The runtime reaches it back through the package's
28
- * `./engine/region-events` entry point. It is engine-side, not
29
- * SoHL-side, because any content module that authors a scene region — an
30
- * adventure module included — needs this vocabulary.
21
+ * The runtime reaches it through the package's `./engine/region-events` entry
22
+ * point. It is engine-side, not SoHL-side, because a Scene region exported
23
+ * from any content module — an adventure module included — names its events
24
+ * from this vocabulary.
31
25
  *
32
26
  * The prose explaining *why* each event is curated or excluded lives with the
33
27
  * typed re-exports in `src/entity/event/region-triggers.ts`, which is the