@heroiclands/package-build 23.0.0 → 23.1.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.
Files changed (153) hide show
  1. package/CHANGELOG.md +58 -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 +17 -1
  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/hugo-theme/layouts/partials/infobox/entry.html +4 -1
  89. package/hugo-theme/layouts/partials/page-body.html +0 -3
  90. package/hugo-theme/static/css/style.css +25 -55
  91. package/package.json +1 -2
  92. package/packagebuild-metadata.jsonl +13 -13
  93. package/sohl/note-schemas.mjs +8 -73
  94. package/types/engine/address-values.d.mts +50 -3
  95. package/types/engine/address.d.mts +21 -0
  96. package/types/engine/anchors.d.mts +49 -2
  97. package/types/engine/art-slots.d.mts +1 -1
  98. package/types/engine/base-compiler.d.mts +13 -4
  99. package/types/engine/calendar-notes.d.mts +7 -0
  100. package/types/engine/content-format.d.mts +52 -0
  101. package/types/engine/content-images.d.mts +5 -1
  102. package/types/engine/content-tables.d.mts +9 -1
  103. package/types/engine/data-keys.d.mts +145 -0
  104. package/types/engine/derived-sections.d.mts +73 -0
  105. package/types/engine/diagnostics.d.mts +15 -0
  106. package/types/engine/event-fields.d.mts +99 -0
  107. package/types/engine/event-rows.d.mts +12 -0
  108. package/types/engine/event-views.d.mts +67 -0
  109. package/types/engine/exported-scene.d.mts +5 -2
  110. package/types/engine/frontmatter-lint.d.mts +3 -3
  111. package/types/engine/generated-sections.d.mts +143 -0
  112. package/types/engine/holdings.d.mts +6 -20
  113. package/types/engine/ids.d.mts +15 -12
  114. package/types/engine/index.d.mts +1 -1
  115. package/types/engine/infobox.d.mts +13 -3
  116. package/types/engine/journals.d.mts +24 -11
  117. package/types/engine/literature-works.d.mts +1 -2
  118. package/types/engine/map-graphviz.d.mts +2 -10
  119. package/types/engine/markdown-expressions.d.mts +12 -3
  120. package/types/engine/near-miss.d.mts +42 -0
  121. package/types/engine/note-addresses.d.mts +7 -1
  122. package/types/engine/note-event-terms.d.mts +68 -0
  123. package/types/engine/note-events.d.mts +71 -0
  124. package/types/engine/note-vocabulary.d.mts +37 -8
  125. package/types/engine/office-holders.d.mts +3 -0
  126. package/types/engine/pdf-index.d.mts +35 -0
  127. package/types/engine/pdf-render.d.mts +8 -2
  128. package/types/engine/place-relation-terms.d.mts +83 -0
  129. package/types/engine/place-relations.d.mts +11 -66
  130. package/types/engine/rank-ladder.d.mts +2 -0
  131. package/types/engine/region-events.d.mts +6 -12
  132. package/types/engine/scenes.d.mts +6 -32
  133. package/types/engine/site-build.d.mts +8 -22
  134. package/types/engine/site-maps.d.mts +15 -7
  135. package/types/engine/sql-tables.d.mts +77 -2
  136. package/types/engine/standing-terms.d.mts +37 -4
  137. package/types/engine/standings.d.mts +14 -9
  138. package/types/engine/wikilink-syntax.d.mts +20 -0
  139. package/types/engine/wikilinks.d.mts +3 -2
  140. package/docs/guides/government-migration.md +0 -52
  141. package/engine/itinerary-scenes.mjs +0 -166
  142. package/engine/lore-events.mjs +0 -197
  143. package/engine/map-notes.mjs +0 -1309
  144. package/engine/map-raster.mjs +0 -51
  145. package/hugo-theme/layouts/partials/holdings/entry.html +0 -21
  146. package/hugo-theme/layouts/partials/holdings/kind-groups.html +0 -71
  147. package/hugo-theme/layouts/partials/holdings.html +0 -61
  148. package/hugo-theme/layouts/partials/literature-works.html +0 -29
  149. package/hugo-theme/layouts/partials/place-map.html +0 -9
  150. package/types/engine/itinerary-scenes.d.mts +0 -29
  151. package/types/engine/lore-events.d.mts +0 -14
  152. package/types/engine/map-notes.d.mts +0 -297
  153. package/types/engine/map-raster.d.mts +0 -8
@@ -0,0 +1,90 @@
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 near miss a closed vocabulary names when it refuses a key: the declared
16
+ * key an unknown one was most likely meant to be, and the declared list as an
17
+ * English sentence.
18
+ *
19
+ * Shared by every closed region — the top level, `data:`, a system block, and
20
+ * the inner keys of a `data:` field — so a misspelling is answered the same
21
+ * way at every depth.
22
+ *
23
+ * @module
24
+ */
25
+
26
+ /**
27
+ * Edit distance, capped — enough to answer "did you mean".
28
+ *
29
+ * A misspelled property is the failure class this check exists for, and a
30
+ * finding that names the key the author *meant* turns a hunt through the
31
+ * reference into a one-character fix.
32
+ *
33
+ * @param {string} a - One string.
34
+ * @param {string} b - The other.
35
+ * @returns {number} The Levenshtein distance.
36
+ */
37
+ export function distance(a, b) {
38
+ const rows = a.length + 1;
39
+ const cols = b.length + 1;
40
+ let prev = Array.from({ length: cols }, (_, j) => j);
41
+ for (let i = 1; i < rows; i += 1) {
42
+ const row = [i];
43
+ for (let j = 1; j < cols; j += 1) {
44
+ row[j] = Math.min(
45
+ prev[j] + 1,
46
+ row[j - 1] + 1,
47
+ prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
48
+ );
49
+ }
50
+ prev = row;
51
+ }
52
+ return prev[cols - 1];
53
+ }
54
+
55
+ /**
56
+ * A closed vocabulary as an English list, for the message that names it.
57
+ *
58
+ * Written from the declaration rather than spelled into the message, so a key
59
+ * the vocabulary gains cannot go unmentioned by the finding that refuses its
60
+ * neighbours.
61
+ *
62
+ * @param {readonly string[]} keys - The declared keys, in declared order.
63
+ * @returns {string} `"a, b, or c"`.
64
+ */
65
+ export function listed(keys) {
66
+ if (keys.length < 2) return keys.join("");
67
+ return `${keys.slice(0, -1).join(", ")}, or ${keys[keys.length - 1]}`;
68
+ }
69
+
70
+ /**
71
+ * The declared key an unknown one was most likely meant to be.
72
+ *
73
+ * @param {string} key - The unknown key.
74
+ * @param {Iterable<string>} candidates - The declared keys.
75
+ * @returns {string|undefined} The nearest, when it is near enough to suggest.
76
+ */
77
+ export function nearest(key, candidates) {
78
+ let best;
79
+ let bestAt = Infinity;
80
+ for (const candidate of candidates) {
81
+ const d = distance(key.toLowerCase(), candidate.toLowerCase());
82
+ if (d < bestAt) {
83
+ bestAt = d;
84
+ best = candidate;
85
+ }
86
+ }
87
+ // A third of the key's length, so a suggestion is a plausible typo rather
88
+ // than the least-bad of a list of unrelated words.
89
+ return bestAt <= Math.max(1, Math.floor(key.length / 3)) ? best : undefined;
90
+ }
@@ -12,15 +12,25 @@ import {
12
12
  parseAddress,
13
13
  renderAddress,
14
14
  acceptsType,
15
+ hasAnchor,
15
16
  readCanonicalKey,
17
+ splitAnchor,
16
18
  } from "./address.mjs";
17
19
  import { NOTE_VOCABULARY, dataFields } from "./note-vocabulary.mjs";
18
20
  import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
19
21
  import { ART_SLOTS } from "./art-slots.mjs";
20
22
  import { SHIPPED_SYSTEMS } from "./subtype-registry.mjs";
23
+ import { EVENT_ADDRESS_POSITIONS } from "./note-events.mjs";
21
24
 
22
- import { AddressEntries, AddressLink } from "./address-values.mjs";
23
- export { AddressEntries, encodeAddresses, cloneAddressState } from "./address-values.mjs";
25
+ import { AddressEntries, AddressLink, readPublishedAddresses } from "./address-values.mjs";
26
+ export {
27
+ AddressEntries,
28
+ encodeAddresses,
29
+ cloneAddressState,
30
+ publishAddresses,
31
+ readPublishedAddresses,
32
+ flattenPublishedAddresses,
33
+ } from "./address-values.mjs";
24
34
 
25
35
  /** Structured positions whose Address is nested inside a declared container. */
26
36
  export const STRUCTURED_ADDRESSES = Object.freeze([
@@ -77,12 +87,24 @@ export function addressPositions(fm, context = {}) {
77
87
  shape,
78
88
  type: field.ref,
79
89
  accepts: field.accepts,
90
+ ...(field.anchors ? { anchors: field.anchors } : {}),
80
91
  ...(ASSET_TYPE_NAMES.has(field.ref) || field.ref === "folder" ?
81
92
  { system: "none" }
82
93
  : {}),
83
94
  });
84
95
  }
85
96
  out.push(...STRUCTURED_ADDRESSES.filter((p) => !p.noteType || p.noteType === fm.type));
97
+ // An event's Addresses, on every type that carries events. Read leniently:
98
+ // a value that is no accepted Address stays as written, for the event
99
+ // check to refuse at its own key with the message its key earns, rather
100
+ // than taking the whole note out of the index.
101
+ if ((dataFields(fm.type) ?? []).some((field) => field.name === "events"))
102
+ for (const position of EVENT_ADDRESS_POSITIONS)
103
+ out.push({
104
+ ...position,
105
+ path: ["data", "events", "*", ...position.path],
106
+ lenient: true,
107
+ });
86
108
  for (const system of new Set([
87
109
  ...SHIPPED_SYSTEMS,
88
110
  ...Object.keys(context.systemBlocks ?? {}),
@@ -206,7 +228,27 @@ export function decodeNoteAddresses(fm, context) {
206
228
  };
207
229
  const read = (value, path) => {
208
230
  if (value == null || value === "") return value;
209
- const tuple = parseAddress(value, defaults, {
231
+ if (value instanceof AddressLink) {
232
+ if (!position.anchors)
233
+ fail(
234
+ value,
235
+ path,
236
+ "takes no anchor — it names a whole note, so write the Address without `#…`",
237
+ );
238
+ return value;
239
+ }
240
+ // An anchor is admitted only where the position declares the kinds
241
+ // it takes; the anchor's existence and kind are a resolution
242
+ // question, asked where the index is.
243
+ const anchored = hasAnchor(value);
244
+ if (anchored && !position.anchors)
245
+ fail(
246
+ value,
247
+ path,
248
+ "takes no anchor — it names a whole note, so write the Address without `#…`",
249
+ );
250
+ const { address: written, anchor } = anchored ? splitAnchor(value) : { address: value };
251
+ const tuple = parseAddress(written, defaults, {
210
252
  declared: true,
211
253
  legacyShortcodeCase: position.legacyShortcodeCase,
212
254
  });
@@ -217,7 +259,13 @@ export function decodeNoteAddresses(fm, context) {
217
259
  path,
218
260
  `must name a ${position.accepts.join(" or ")} Address, but names ${tuple.type}`,
219
261
  );
220
- return tuple;
262
+ return anchored ?
263
+ new AddressLink(
264
+ tuple,
265
+ anchor,
266
+ position.anchors.length === 1 ? position.anchors[0] : null,
267
+ )
268
+ : tuple;
221
269
  };
222
270
  const convert = (value, path) => {
223
271
  if (value == null || value === "") return value;
@@ -258,12 +306,24 @@ export function decodeNoteAddresses(fm, context) {
258
306
  }
259
307
  return read(value, path);
260
308
  };
309
+ // A lenient position keeps a value it cannot read, as written.
310
+ const settle = (value, path) => {
311
+ if (!position.lenient) return convert(value, path);
312
+ try {
313
+ return convert(value, path);
314
+ } catch {
315
+ return value;
316
+ }
317
+ };
261
318
  // Copy each traversed owner so YAML aliases retain their own read context.
262
319
  const visit = (owner, path, offset = 0, actual = []) => {
263
320
  if (owner == null || owner === "") return owner;
264
321
  const key = path[offset];
265
322
  if (key === "*") {
266
- if (!Array.isArray(owner)) fail(owner, actual, "must be a list of entries");
323
+ if (!Array.isArray(owner)) {
324
+ if (position.lenient) return owner;
325
+ fail(owner, actual, "must be a list of entries");
326
+ }
267
327
  return owner.map((value, i) => visit(value, path, offset + 1, [...actual, i]));
268
328
  }
269
329
  if (typeof owner !== "object") return owner;
@@ -271,7 +331,7 @@ export function decodeNoteAddresses(fm, context) {
271
331
  const copy = Array.isArray(owner) ? [...owner] : { ...owner };
272
332
  copy[key] =
273
333
  offset === path.length - 1 ?
274
- convert(owner[key], [...actual, key])
334
+ settle(owner[key], [...actual, key])
275
335
  : visit(owner[key], path, offset + 1, [...actual, key]);
276
336
  return copy;
277
337
  };
@@ -312,11 +372,18 @@ export function noteAddressContext(config) {
312
372
 
313
373
  /**
314
374
  * Decode Address properties contributed by an index record.
375
+ *
376
+ * A record read from a published index writes every Address as
377
+ * `{ address, anchor, anchorKind }`; each becomes a tuple, or an
378
+ * {@link AddressLink} where it carries an anchor, before the declared
379
+ * positions are read.
380
+ *
315
381
  * @param {object} record - The record.
316
382
  * @param {object} context - Builder defaults.
317
383
  * @returns {object} The typed record.
318
384
  */
319
385
  export function decodeIndexAddresses(record, context) {
386
+ Object.assign(record, readPublishedAddresses(record));
320
387
  decodeNoteAddresses(record, context);
321
388
  const canonical = (value, keyPath) => {
322
389
  const target = readCanonicalKey(value);
@@ -335,19 +402,8 @@ export function decodeIndexAddresses(record, context) {
335
402
  if (record.address?.canonical != null)
336
403
  record.address.canonical = canonical(record.address.canonical, ["address", "canonical"]);
337
404
  for (const anchor of record.anchors ?? []) {
338
- if (!anchor.link || anchor.link instanceof AddressLink) continue;
339
- const [written, section] = anchor.link.split("#");
340
- const target = parseAddress(
341
- written,
342
- {
343
- ...context,
344
- system: "note",
345
- types: new Set([...Object.keys(NOTE_VOCABULARY), ...ASSET_TYPE_NAMES]),
346
- },
347
- { declared: true },
348
- );
349
- if (target.reason) throw new Error(`Index anchor link is not an Address: ${anchor.link}`);
350
- anchor.link = new AddressLink(target, section);
405
+ if (anchor.link != null && !(anchor.link instanceof AddressLink))
406
+ throw new Error(`Index anchor link is not an Address: ${JSON.stringify(anchor.link)}`);
351
407
  }
352
408
  return record;
353
409
  }
@@ -955,11 +955,13 @@ function resolveEventEntry(entry, context) {
955
955
 
956
956
  /** Normalize a note's declared dates for indexes and generated pages. */
957
957
  export function resolvedDateFields(fm, context) {
958
- if (fm?.type === "lore") {
959
- const events = Array.isArray(fm.data?.events) ? fm.data.events : [];
960
- const resolved = events.map((entry) => resolveEventEntry(entry, context));
958
+ // A note carrying `data.events` — a lore, place or affiliation note — is
959
+ // dated by them.
960
+ if (Array.isArray(fm?.data?.events)) {
961
+ const resolved = fm.data.events.map((entry) => resolveEventEntry(entry, context));
961
962
  return resolved.length ? { events: resolved } : {};
962
963
  }
964
+ if (fm?.type === "lore") return {};
963
965
  const fields =
964
966
  fm?.type === "being" ?
965
967
  [
@@ -0,0 +1,364 @@
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 **terms of an event**: the closed value lists an event entry draws on,
16
+ * and the declaration of every key an entry and each map inside it take.
17
+ *
18
+ * A leaf module, importing nothing. The note vocabulary reads
19
+ * {@link EVENT_ENTRY} as it loads, so the frontmatter lint's inner-key check
20
+ * closes every key of an entry the way it closes every other `data:` field's;
21
+ * {@link module:engine/note-events} walks the same declaration for what the
22
+ * values mean — closed lists, Addresses, identity and order — and reaches the
23
+ * vocabulary through the Address reader.
24
+ *
25
+ * An entry is declared in the inner-key vocabulary of
26
+ * {@link module:engine/data-keys} — `fields`, `entries`, `kind`, `shape`,
27
+ * `required`, `describe` — with four properties of its own that the event
28
+ * check reads:
29
+ *
30
+ * - `oneOf` — `{name, values}`: the value is one of `values`, a closed list the
31
+ * format reference states as the vocabulary table `name`;
32
+ * - `ref`, `accepts`, `anchors`, `single`, `eventOrLore`, `calendar` on an
33
+ * Address: its default type, its accepted types, the anchor kinds it may
34
+ * name, and whether it must name exactly one event, an event or a `lore`
35
+ * note, or a calendar note;
36
+ * - `own` — the key's value is judged by the event check's own code (`id`,
37
+ * `when`, `until`, `recurs`) rather than by the walker.
38
+ *
39
+ * @module
40
+ */
41
+
42
+ /** What an event is, grouped for reading and closed for validation. */
43
+ export const EVENT_KINDS = Object.freeze([
44
+ // peoples
45
+ "arrival",
46
+ "departure",
47
+ "migration",
48
+ "contact",
49
+ "displacement",
50
+ // polities
51
+ "founding",
52
+ "charter",
53
+ "accession",
54
+ "secession",
55
+ "conquest",
56
+ "treaty",
57
+ "dissolution",
58
+ // conflict
59
+ "battle",
60
+ "war",
61
+ "siege",
62
+ "revolt",
63
+ // ruin
64
+ "fall",
65
+ "catastrophe",
66
+ "plague",
67
+ "famine",
68
+ // works
69
+ "raising",
70
+ "ruin",
71
+ "making",
72
+ "loss",
73
+ "discovery",
74
+ // institutions and belief
75
+ "schism",
76
+ "law",
77
+ "council",
78
+ // persons
79
+ "birth",
80
+ "death",
81
+ ]);
82
+
83
+ /** How far an event's history reaches. Set by hand. */
84
+ export const EVENT_DEPTHS = Object.freeze(["world", "region", "local"]);
85
+
86
+ /** What the world's evidence supports about an event. */
87
+ export const EVENT_STANDINGS = Object.freeze([
88
+ "attested",
89
+ "single-source",
90
+ "reconstructed",
91
+ "disputed",
92
+ "legendary",
93
+ ]);
94
+
95
+ /** Whether a place that felt an event knows what caused it. */
96
+ export const REACH_KNOWLEDGE = Object.freeze(["named", "misattributed", "unlinked"]);
97
+
98
+ /** What a participant was to an event. */
99
+ export const PARTICIPANT_ROLES = Object.freeze([
100
+ "actor",
101
+ "victim",
102
+ "instrument",
103
+ "witness",
104
+ "founder",
105
+ "ruler",
106
+ "author",
107
+ "signatory",
108
+ ]);
109
+
110
+ /** How a later event follows from an earlier one. */
111
+ export const FOLLOWS_HOW = Object.freeze(["caused", "enabled", "ended", "answered"]);
112
+
113
+ /** How far an account agrees with the event's summary. */
114
+ export const ACCOUNT_AGREES = Object.freeze(["full", "partly", "disputes", "denies", "silent"]);
115
+
116
+ /**
117
+ * Every closed list, by the name of the vocabulary table the format reference
118
+ * and `engine/content-format.yaml` state it under. `eventKind` is not `kind`
119
+ * because the infobox's row kinds already hold that name.
120
+ */
121
+ export const EVENT_VOCABULARIES = Object.freeze({
122
+ eventKind: EVENT_KINDS,
123
+ depth: EVENT_DEPTHS,
124
+ standing: EVENT_STANDINGS,
125
+ knowledge: REACH_KNOWLEDGE,
126
+ role: PARTICIPANT_ROLES,
127
+ followsHow: FOLLOWS_HOW,
128
+ agrees: ACCOUNT_AGREES,
129
+ });
130
+
131
+ /** A key holding text, stated with something in it. */
132
+ const text = (name, describe, required = false) =>
133
+ Object.freeze({ name, kind: "string", shape: "a string", required, describe });
134
+
135
+ /** A key holding one value of a closed list. */
136
+ const oneOf = (name, vocabulary, describe, required = false) =>
137
+ Object.freeze({
138
+ name,
139
+ kind: "string",
140
+ shape: `one \`${vocabulary}\` value`,
141
+ oneOf: Object.freeze({ name: vocabulary, values: EVENT_VOCABULARIES[vocabulary] }),
142
+ required,
143
+ describe,
144
+ });
145
+
146
+ /** A key holding one Address, with what it may name. */
147
+ const address = (name, shape, describe, extra = {}, required = false) =>
148
+ Object.freeze({ name, kind: "address", shape, required, describe, ...extra });
149
+
150
+ /** A key holding a list of maps, each closed to `fields`. */
151
+ const listOf = (name, shape, describe, fields) =>
152
+ Object.freeze({
153
+ name,
154
+ kind: "list",
155
+ shape,
156
+ entries: Object.freeze({
157
+ kind: "map",
158
+ shape: `a map — ${shape.replace(/\[\]$/, "")}`,
159
+ fields,
160
+ }),
161
+ describe,
162
+ });
163
+
164
+ /** An Address naming a place; a bare shortcode is read as one. */
165
+ const PLACE = Object.freeze({ ref: "place", accepts: Object.freeze(["place"]) });
166
+
167
+ /**
168
+ * One event entry, as an inner-key declaration: every key it takes, and every
169
+ * key of each map nested in it.
170
+ *
171
+ * @type {Readonly<{kind: "map", shape: string, fields: readonly object[]}>}
172
+ */
173
+ export const EVENT_ENTRY = Object.freeze({
174
+ kind: "map",
175
+ shape: "an event — a map of the keys below",
176
+ fields: Object.freeze([
177
+ Object.freeze({
178
+ name: "id",
179
+ kind: "shortcode",
180
+ shape: "an address segment",
181
+ own: true,
182
+ describe: "The event's name within its note — lowercase letters and digits.",
183
+ }),
184
+ oneOf("kind", "eventKind", "What sort of thing happened."),
185
+ oneOf("depth", "depth", "How far the event's history reaches."),
186
+ Object.freeze({
187
+ name: "when",
188
+ kind: "date",
189
+ shape: 'a date, or `"0.<day>"`',
190
+ required: true,
191
+ own: true,
192
+ describe: "When it happened, or the anchor and first instance of an event that recurs.",
193
+ }),
194
+ Object.freeze({
195
+ name: "until",
196
+ kind: "date",
197
+ shape: "a date",
198
+ own: true,
199
+ describe: "When a continuous event ended, or where a recurring series stopped.",
200
+ }),
201
+ Object.freeze({
202
+ name: "recurs",
203
+ kind: "map",
204
+ shape: "`{ every }` or `{ on }`",
205
+ own: true,
206
+ fields: Object.freeze([
207
+ Object.freeze({
208
+ name: "every",
209
+ kind: "integer",
210
+ shape: "a whole number of years, 1 or more",
211
+ describe: "A period counted on the canonical axis from `when`.",
212
+ }),
213
+ Object.freeze({
214
+ name: "on",
215
+ kind: "list",
216
+ shape: "a list of dates, strictly increasing, each later than `when`",
217
+ entries: Object.freeze({ kind: "date", shape: "a date" }),
218
+ describe: "Recorded occurrences beyond the first, in place of a period.",
219
+ }),
220
+ ]),
221
+ describe: "How further occurrences are found; absent for an event that happened once.",
222
+ }),
223
+ text("summary", "What happened, stated plainly.", true),
224
+ oneOf("standing", "standing", "What the world's evidence supports."),
225
+ listOf(
226
+ "names",
227
+ "`{ name, by, gloss? }[]`",
228
+ "The event's names in the world, each with who uses it.",
229
+ [
230
+ text("name", "One name the event goes by in the world.", true),
231
+ address(
232
+ "by",
233
+ "an Address naming an `affiliation`, `lore`, `place`, `being` or `skill` note",
234
+ "Who uses that name — a people, a polity, a faith, a place, a tongue.",
235
+ { accepts: Object.freeze(["affiliation", "lore", "place", "being", "skill"]) },
236
+ true,
237
+ ),
238
+ text("gloss", "What the name means, or how it is used."),
239
+ ],
240
+ ),
241
+ Object.freeze({
242
+ name: "where",
243
+ kind: "map",
244
+ shape: "`{ locus?, reach? }`",
245
+ fields: Object.freeze([
246
+ Object.freeze({
247
+ name: "locus",
248
+ kind: "list",
249
+ shape: "a list of Addresses, each defaulting to `place`",
250
+ entries: Object.freeze({ kind: "address", shape: "a place Address", ...PLACE }),
251
+ describe: "Where the event physically happened; each resolves to a place.",
252
+ }),
253
+ listOf(
254
+ "reach",
255
+ "`{ place, how, knowledge, attributedTo? }[]`",
256
+ "Where the event was felt, and whether each place knows why.",
257
+ [
258
+ address(
259
+ "place",
260
+ "an Address, defaulting to `place`",
261
+ "A place where the event was felt.",
262
+ PLACE,
263
+ true,
264
+ ),
265
+ text(
266
+ "how",
267
+ "One clause: a consequence someone in that place could notice.",
268
+ true,
269
+ ),
270
+ oneOf(
271
+ "knowledge",
272
+ "knowledge",
273
+ "Whether that place connects what it felt to this event.",
274
+ true,
275
+ ),
276
+ address(
277
+ "attributedTo",
278
+ "an Address, defaulting to `lore`, naming an event or a `lore` note",
279
+ "Only beside `knowledge: misattributed` — the cause that place names instead.",
280
+ { ref: "lore", anchors: Object.freeze(["event"]), eventOrLore: true },
281
+ ),
282
+ ],
283
+ ),
284
+ ]),
285
+ describe: "Where it happened, and where it was felt.",
286
+ }),
287
+ listOf("who", "`{ ref, role }[]`", "Who took part, and as what.", [
288
+ address(
289
+ "ref",
290
+ "an Address naming a `being`, `affiliation` or `lore` note",
291
+ "A participant — a being, a people, an affiliation.",
292
+ { accepts: Object.freeze(["being", "affiliation", "lore"]) },
293
+ true,
294
+ ),
295
+ oneOf("role", "role", "What the participant was to the event.", true),
296
+ ]),
297
+ listOf(
298
+ "follows",
299
+ "`{ event, how, note? }[]`",
300
+ "The earlier events this one follows from.",
301
+ [
302
+ address(
303
+ "event",
304
+ "an Address, defaulting to `lore`, anchored to an `event` where its note holds several",
305
+ "The earlier event; it resolves to exactly one event.",
306
+ { ref: "lore", anchors: Object.freeze(["event"]), single: true },
307
+ true,
308
+ ),
309
+ oneOf("how", "followsHow", "How this event follows from it.", true),
310
+ text("note", "One clause saying what connects the two."),
311
+ ],
312
+ ),
313
+ listOf(
314
+ "accounts",
315
+ "`{ by, says, agrees, withholds? }[]`",
316
+ "What each people, polity, faith or place says about it.",
317
+ [
318
+ address(
319
+ "by",
320
+ "an Address naming an `affiliation`, `lore`, `place` or `being` note",
321
+ "Who holds the account.",
322
+ { accepts: Object.freeze(["affiliation", "lore", "place", "being"]) },
323
+ true,
324
+ ),
325
+ text("says", "What they say happened, in their terms.", true),
326
+ oneOf("agrees", "agrees", "How far their account agrees with `summary`.", true),
327
+ text("withholds", "What they decline to say."),
328
+ ],
329
+ ),
330
+ Object.freeze({
331
+ name: "unresolved",
332
+ kind: "list",
333
+ shape: "a list of strings",
334
+ entries: Object.freeze({ kind: "string", shape: "a string" }),
335
+ describe: "What the world itself has not settled.",
336
+ }),
337
+ Object.freeze({
338
+ name: "sources",
339
+ kind: "list",
340
+ shape: "a list of Addresses",
341
+ entries: Object.freeze({ kind: "address", shape: "an Address, written with its type" }),
342
+ describe: "The notes that state or support the event; each resolves.",
343
+ }),
344
+ Object.freeze({
345
+ name: "stated",
346
+ kind: "map",
347
+ shape: "`{ calendar, text }`",
348
+ fields: Object.freeze([
349
+ address(
350
+ "calendar",
351
+ "an Address, or a lore shortcode",
352
+ "The calendar that tradition reckons in — a `lore` note with `subType: calendar`.",
353
+ { ref: "lore", accepts: Object.freeze(["lore"]), calendar: true },
354
+ true,
355
+ ),
356
+ text("text", "The date as that tradition writes it.", true),
357
+ ]),
358
+ describe: "The date as one tradition writes it in its own reckoning.",
359
+ }),
360
+ ]),
361
+ });
362
+
363
+ /** Every key an event entry admits, in the order the reference documents them. */
364
+ export const EVENT_ENTRY_KEYS = Object.freeze(EVENT_ENTRY.fields.map((field) => field.name));