@heroiclands/package-build 22.21.0 → 22.23.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 (152) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/assets/styles/content-base.css +162 -0
  3. package/bin/content-commands.mjs +77 -17
  4. package/bin/package-build.mjs +15 -2
  5. package/ci/content-format-reference.mjs +68 -1
  6. package/config.mjs +26 -0
  7. package/content-config.mjs +153 -6
  8. package/docs/api.md +120 -89
  9. package/docs/authoring/assets.md +14 -0
  10. package/docs/authoring/dates-and-calendars.md +41 -3
  11. package/docs/authoring/frontmatter.md +2 -2
  12. package/docs/authoring/links-and-markup.md +325 -35
  13. package/docs/commands.md +12 -10
  14. package/docs/configuration.md +117 -14
  15. package/docs/content-format.md +4 -2
  16. package/docs/guides/book.md +5 -4
  17. package/docs/guides/reference-pages.md +15 -0
  18. package/docs/reference/format-details.md +545 -104
  19. package/docs/reference/note-types.md +30 -13
  20. package/engine/actor-compiler.mjs +23 -0
  21. package/engine/anchored-sections.mjs +35 -4
  22. package/engine/anchors.mjs +26 -30
  23. package/engine/art-fields.mjs +118 -0
  24. package/engine/art-slots.mjs +7 -1
  25. package/engine/asset-index.mjs +160 -10
  26. package/engine/asset-replacement.mjs +93 -0
  27. package/engine/asset-types.mjs +240 -0
  28. package/engine/base-compiler.mjs +139 -22
  29. package/engine/base-styles.mjs +149 -0
  30. package/engine/bundles.mjs +13 -2
  31. package/engine/compendiums.mjs +8 -1
  32. package/engine/content-blocks.mjs +410 -0
  33. package/engine/content-embeds.mjs +19 -0
  34. package/engine/content-figures.mjs +468 -0
  35. package/engine/content-footnotes.mjs +197 -0
  36. package/engine/content-format-check.mjs +15 -12
  37. package/engine/content-format.yaml +1 -1
  38. package/engine/content-images.mjs +126 -25
  39. package/engine/content-index.mjs +38 -12
  40. package/engine/content-links.mjs +14 -6
  41. package/engine/content-tables.mjs +45 -4
  42. package/engine/content-tasklists.mjs +189 -0
  43. package/engine/docs-checks.mjs +4 -4
  44. package/engine/extension-attributes.mjs +48 -2
  45. package/engine/field-reference.mjs +31 -10
  46. package/engine/field-spec.mjs +10 -8
  47. package/engine/foreign-catalog.mjs +143 -18
  48. package/engine/frontmatter-lint.mjs +120 -65
  49. package/engine/frontmatter.mjs +3 -3
  50. package/engine/heading-attributes.mjs +321 -0
  51. package/engine/helpers.mjs +109 -41
  52. package/engine/homepage.mjs +8 -8
  53. package/engine/index.mjs +3 -0
  54. package/engine/infobox-registry.mjs +4 -1
  55. package/engine/infobox-render.mjs +15 -3
  56. package/engine/infobox.mjs +129 -15
  57. package/engine/itinerary-scenes.mjs +4 -10
  58. package/engine/journals.mjs +294 -50
  59. package/engine/lore-events.mjs +197 -0
  60. package/engine/macros.mjs +1 -1
  61. package/engine/markdown-expressions.mjs +434 -59
  62. package/engine/note-dates.mjs +354 -12
  63. package/engine/note-frontmatter.mjs +7 -16
  64. package/engine/note-vocabulary.mjs +183 -42
  65. package/engine/page-lists.mjs +358 -0
  66. package/engine/pathnames.mjs +36 -0
  67. package/engine/pdf-build.mjs +431 -74
  68. package/engine/pdf-images.mjs +64 -0
  69. package/engine/pdf-render.mjs +563 -66
  70. package/engine/rank-ladder.mjs +198 -0
  71. package/engine/reckoning-markers.mjs +1 -1
  72. package/engine/retired-fields.mjs +8 -13
  73. package/engine/scenes.mjs +26 -7
  74. package/engine/site-build.mjs +166 -23
  75. package/engine/site-config.mjs +10 -1
  76. package/engine/site-index.mjs +90 -3
  77. package/engine/site-preview.mjs +20 -7
  78. package/engine/social-ties.mjs +52 -14
  79. package/engine/sql-tables.mjs +66 -9
  80. package/engine/standings.mjs +89 -6
  81. package/engine/system-block.mjs +56 -30
  82. package/engine/web-wikilinks.mjs +48 -1
  83. package/hm3/actors.mjs +8 -5
  84. package/manifest.mjs +43 -24
  85. package/package.json +1 -1
  86. package/packagebuild-metadata.jsonl +13 -13
  87. package/sohl/actors.mjs +22 -99
  88. package/sohl/item-fields.mjs +5 -5
  89. package/sohl/items.mjs +1 -1
  90. package/sohl/note-schemas.mjs +11 -21
  91. package/types/config.d.mts +9 -0
  92. package/types/content-config.d.mts +48 -1
  93. package/types/engine/actor-compiler.d.mts +11 -0
  94. package/types/engine/anchored-sections.d.mts +24 -0
  95. package/types/engine/anchors.d.mts +9 -4
  96. package/types/engine/art-fields.d.mts +64 -0
  97. package/types/engine/asset-index.d.mts +43 -0
  98. package/types/engine/asset-replacement.d.mts +22 -0
  99. package/types/engine/asset-types.d.mts +18 -0
  100. package/types/engine/base-compiler.d.mts +19 -17
  101. package/types/engine/base-styles.d.mts +93 -0
  102. package/types/engine/content-blocks.d.mts +87 -0
  103. package/types/engine/content-embeds.d.mts +16 -0
  104. package/types/engine/content-figures.d.mts +101 -0
  105. package/types/engine/content-footnotes.d.mts +80 -0
  106. package/types/engine/content-format-check.d.mts +8 -6
  107. package/types/engine/content-images.d.mts +78 -12
  108. package/types/engine/content-index.d.mts +8 -2
  109. package/types/engine/content-tables.d.mts +12 -4
  110. package/types/engine/content-tasklists.d.mts +72 -0
  111. package/types/engine/extension-attributes.d.mts +22 -1
  112. package/types/engine/field-reference.d.mts +21 -0
  113. package/types/engine/field-spec.d.mts +20 -16
  114. package/types/engine/foreign-catalog.d.mts +39 -0
  115. package/types/engine/frontmatter.d.mts +1 -1
  116. package/types/engine/heading-attributes.d.mts +179 -0
  117. package/types/engine/helpers.d.mts +29 -10
  118. package/types/engine/homepage.d.mts +7 -7
  119. package/types/engine/index.d.mts +1 -0
  120. package/types/engine/infobox-registry.d.mts +4 -1
  121. package/types/engine/infobox-render.d.mts +6 -1
  122. package/types/engine/infobox.d.mts +6 -0
  123. package/types/engine/journals.d.mts +41 -12
  124. package/types/engine/lore-events.d.mts +14 -0
  125. package/types/engine/markdown-expressions.d.mts +46 -2
  126. package/types/engine/note-dates.d.mts +64 -1
  127. package/types/engine/note-frontmatter.d.mts +1 -4
  128. package/types/engine/note-vocabulary.d.mts +82 -6
  129. package/types/engine/page-lists.d.mts +126 -0
  130. package/types/engine/pathnames.d.mts +26 -0
  131. package/types/engine/pdf-build.d.mts +28 -6
  132. package/types/engine/pdf-images.d.mts +52 -0
  133. package/types/engine/pdf-render.d.mts +84 -2
  134. package/types/engine/rank-ladder.d.mts +32 -0
  135. package/types/engine/retired-fields.d.mts +4 -6
  136. package/types/engine/site-build.d.mts +27 -4
  137. package/types/engine/site-config.d.mts +9 -0
  138. package/types/engine/site-index.d.mts +67 -1
  139. package/types/engine/site-preview.d.mts +1 -1
  140. package/types/engine/sql-tables.d.mts +31 -7
  141. package/types/engine/standings.d.mts +21 -3
  142. package/types/engine/system-block.d.mts +37 -14
  143. package/types/engine/web-wikilinks.d.mts +3 -1
  144. package/types/hm3/actors.d.mts +21 -3
  145. package/types/manifest.d.mts +19 -23
  146. package/types/sohl/actors.d.mts +6 -7
  147. package/engine/content-admonitions.mjs +0 -114
  148. package/engine/content-captions.mjs +0 -181
  149. package/engine/content-secrets.mjs +0 -112
  150. package/types/engine/content-admonitions.d.mts +0 -24
  151. package/types/engine/content-captions.d.mts +0 -45
  152. package/types/engine/content-secrets.d.mts +0 -17
@@ -12,30 +12,32 @@
12
12
  */
13
13
 
14
14
  /**
15
- * The **closed** half of a note's frontmatter: the `data:` container, and the
16
- * `subType` each note type declares.
17
- *
18
- * A note's frontmatter has three regions, and only one of them is open. The
19
- * **top level** describes the note as a published artefact, every key of it is
20
- * copied into the generated web page, and an unrecognised key there is a Hugo
21
- * or theme parameter this build has no standing to refuse. The **system
22
- * blocks** describe the subject as one system's documents. Between them sits
23
- * `data:` — the type-specific facts about the subject itself, system-agnostic,
24
- * and closed.
25
- *
26
- * Closed is the whole point. Those facts previously sat at the top level, where
27
- * the pass-through rule applied to them too, so a misspelled `wieght` became a
28
- * theme parameter rather than a finding — indistinguishable, from the outside,
29
- * from a weapon that simply weighs nothing. Under `data:` the same misspelling
30
- * is an error naming the note and the key it was probably meant to be.
31
- *
32
- * **`subType` rides along, and stays at the top level.** It is not a `data:`
33
- * key: a note's `(type, subType)` is what each system's map reads to derive a
34
- * document type, so it describes the note rather than the subject. But it is
35
- * the other per-type vocabulary the format closes, it is enumerated in the same
36
- * `### type:` section of the specification that enumerates the `data:` keys,
37
- * and keeping the two together means one entry per type rather than two
38
- * registries free to disagree about which types exist.
15
+ * The **closed vocabularies** of a note's frontmatter: the top-level keys, the
16
+ * `data:` container, and the `subType` each note type declares.
17
+ *
18
+ * A note's frontmatter has three regions and every one of them is closed. The
19
+ * **top level** describes the note as a published artefact and is a fixed list
20
+ * — {@link NOTE_TOP_LEVEL_FIELDS} — so a key it does not name is a finding at
21
+ * its own line rather than a value copied into the generated page. The
22
+ * **system blocks** describe the subject as one system's documents, and are
23
+ * checked against the published schema. Between them sits `data:` — the
24
+ * type-specific facts about the subject itself, system-agnostic.
25
+ *
26
+ * Closed is the whole point, and the failure it answers is a misspelling. A
27
+ * region that passes an unrecognised key through cannot tell `wieght` from a
28
+ * weapon that weighs nothing: the note says one thing, the build does another,
29
+ * and nothing says so. Under a closed vocabulary the same misspelling is an
30
+ * error naming the note and the key it was probably meant to be.
31
+ *
32
+ * **`subType` is a top-level key with a per-type vocabulary**, which is why it
33
+ * is declared twice over: once in {@link NOTE_TOP_LEVEL_FIELDS} as a key the
34
+ * region accepts, and once per type below as the closed set of values that type
35
+ * takes. It is not a `data:` key — a note's `(type, subType)` is what each
36
+ * system's map reads to derive a document type, so it describes the note rather
37
+ * than the subject — but its values are enumerated in the same `### type:`
38
+ * section of the specification that enumerates the `data:` keys, and keeping
39
+ * the two together means one entry per type rather than two registries free to
40
+ * disagree about which types exist.
39
41
  *
40
42
  * **This is note-format knowledge, so it lives in `engine/`.** `data:` holds
41
43
  * what is true of the *thing* — a weapon's weight, an affliction's
@@ -79,6 +81,9 @@
79
81
  // is how a disagreement between the three arises.
80
82
  import { ART_SLOTS } from "./art-slots.mjs";
81
83
  import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./address-charset.mjs";
84
+ // The system registry, so the top-level block keys below are the systems this
85
+ // toolchain recognises rather than a second list of them.
86
+ import { SYSTEM_IDS } from "./systems.mjs";
82
87
  // The retirement window for a renamed type, read rather than restated: a
83
88
  // vocabulary that answered only to the current spelling would report every key
84
89
  // of an unswept note as unknown.
@@ -100,7 +105,9 @@ import { checkSocialTies } from "./social-ties.mjs";
100
105
  import { checkStandings } from "./standings.mjs";
101
106
  import { STANDING_BODY_TYPES } from "./standing-terms.mjs";
102
107
  import { checkDatedOffices } from "./office-holders.mjs";
108
+ import { checkRankLadder } from "./rank-ladder.mjs";
103
109
  import { checkCalendarChoice } from "./calendar-choice.mjs";
110
+ import { checkLoreEvents } from "./lore-events.mjs";
104
111
  import { checkCultureChoice } from "./culture-choice.mjs";
105
112
  import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
106
113
  import { parseNoteDate } from "./note-dates.mjs";
@@ -218,6 +225,16 @@ const TEXT = Object.freeze({ shape: "string", kind: "string" });
218
225
  /** A list. */
219
226
  const LIST = Object.freeze({ shape: "list", kind: "list" });
220
227
 
228
+ /**
229
+ * One value, or several.
230
+ *
231
+ * The two are the same fact at different lengths rather than two different
232
+ * facts, so a single value means a list of one and a consumer reads one shape.
233
+ * That is what separates this from `list-or-map`, whose halves say different
234
+ * things.
235
+ */
236
+ const TEXT_OR_LIST = Object.freeze({ shape: "string or list", kind: "string-or-list" });
237
+
221
238
  /** A single Address. */
222
239
  const LINK = Object.freeze({ shape: "an Address", kind: "address" });
223
240
 
@@ -303,6 +320,101 @@ const TOKEN_ICON = Object.freeze({
303
320
  describe: "What a token on the canvas wears — an `icon` address; unset, it follows `icon`.",
304
321
  });
305
322
 
323
+ /**
324
+ * One top-level frontmatter key.
325
+ *
326
+ * Narrower than a {@link DataFieldSpec}: the top level is a fixed list rather
327
+ * than a per-type vocabulary, nothing there is keyed by pack or resolved as an
328
+ * Address, and the value's shape is checked by whatever owns the region the key
329
+ * opens — `name`, `tags` and each system block each have their own check.
330
+ *
331
+ * @typedef {object} TopLevelFieldSpec
332
+ * @property {string} name - The key, as a note writes it.
333
+ * @property {boolean} [system] - The key opens a game system's block, so it is
334
+ * one of {@link module:engine/systems.SYSTEM_IDS} rather than a fixed key.
335
+ * @property {string} describe - One line, for the author-facing reference.
336
+ */
337
+
338
+ /**
339
+ * **The top-level vocabulary**, in the order a formatted note writes it.
340
+ *
341
+ * The region describes the note as a published artefact: what it is called,
342
+ * what kind of thing it is about, how it is classified, and which of the other
343
+ * two regions it opens. Everything else a note states is a fact about the
344
+ * subject, which belongs under `data:`, or a fact about one system's document,
345
+ * which belongs inside that system's block.
346
+ *
347
+ * **Closed, like the other two regions.** A key this list does not name is a
348
+ * finding at its own line, because a region that passed one through would turn
349
+ * a misspelling into a value nothing reads — `title` beside `name`, or `pack`
350
+ * where `data.pack` was meant.
351
+ *
352
+ * **The system blocks are derived from the system registry**, so recognising a
353
+ * further system is a change to {@link module:engine/systems.SYSTEM_IDS} and to
354
+ * nothing else. They sort by name after the fixed keys: a block is the largest
355
+ * thing a note writes, every one of them belongs at the end, and a rule a
356
+ * reader can restate in one sentence is worth more than a hand-kept order.
357
+ *
358
+ * Five readers derive from this one list — the frontmatter lint's accepted set,
359
+ * the message it prints, the formatter's key order, the content-format check,
360
+ * and the author-facing reference — so none of them can fall behind the others.
361
+ *
362
+ * @type {readonly TopLevelFieldSpec[]}
363
+ */
364
+ export const NOTE_TOP_LEVEL_FIELDS = Object.freeze([
365
+ Object.freeze({
366
+ name: "shortcode",
367
+ describe: "The note's own address segment, unique within its type.",
368
+ }),
369
+ Object.freeze({
370
+ name: "name",
371
+ describe: "The display names — a required `full`, and any `aliases`.",
372
+ }),
373
+ Object.freeze({
374
+ name: "type",
375
+ describe: "What the note is about, which decides its vocabulary and its document.",
376
+ }),
377
+ Object.freeze({
378
+ name: "subType",
379
+ describe: "The type's own genre, where it declares one.",
380
+ }),
381
+ Object.freeze({
382
+ name: "description",
383
+ describe: "The short page summary.",
384
+ }),
385
+ Object.freeze({
386
+ name: "tags",
387
+ describe: "Draft state, GM routing, and the descriptive labels a page list reads.",
388
+ }),
389
+ Object.freeze({
390
+ name: "data",
391
+ describe: "The facts about the subject itself, shared by every system.",
392
+ }),
393
+ ...[...SYSTEM_IDS].sort().map((system) =>
394
+ Object.freeze({
395
+ name: system,
396
+ system: true,
397
+ describe:
398
+ `What the \`${system}\` system makes of the subject — its ` +
399
+ `document's mechanics, routing and art.`,
400
+ }),
401
+ ),
402
+ ]);
403
+
404
+ /**
405
+ * The top-level keys, in formatted order.
406
+ *
407
+ * @type {readonly string[]}
408
+ */
409
+ export const NOTE_TOP_LEVEL_KEYS = Object.freeze(NOTE_TOP_LEVEL_FIELDS.map((field) => field.name));
410
+
411
+ /**
412
+ * Membership test for the top-level vocabulary.
413
+ *
414
+ * @type {ReadonlySet<string>}
415
+ */
416
+ export const NOTE_TOP_LEVEL_KEY_SET = Object.freeze(new Set(NOTE_TOP_LEVEL_KEYS));
417
+
306
418
  /**
307
419
  * The `data:` keys **every** note type accepts, whatever it is.
308
420
  *
@@ -358,7 +470,9 @@ export const SHARED_DATA_FIELDS = Object.freeze([
358
470
  ...LINK,
359
471
  ref: "image",
360
472
  accepts: ART_SLOTS.find((slot) => slot.key === "banner").accepts,
361
- describe: "The page's hero image — an `image` address. Reaches no compiled document.",
473
+ describe:
474
+ "The page's hero image — an `image` address, cut to 1792×768. " +
475
+ "Reaches no compiled document.",
362
476
  }),
363
477
  ]);
364
478
 
@@ -448,7 +562,7 @@ function checkPlacePurpose(note) {
448
562
  },
449
563
  ];
450
564
  if (hasTag(note.fm, value)) return [];
451
- const tags = note.fm?.tags ?? note.fm?.tag;
565
+ const tags = note.fm?.tags;
452
566
  return [
453
567
  {
454
568
  ...at(),
@@ -482,15 +596,17 @@ export const GM_TAG = "gm";
482
596
  /**
483
597
  * The tags that **classify** a note, grouped by what they classify.
484
598
  *
485
- * `tags:` lives at the open top level and most tags belong there: a theme, a
486
- * region, a working state is the author's own and this build has no opinion
487
- * about it. A classifying tag is different, because something queries it — a
599
+ * `tags:` holds an open vocabulary of *values*, and most tags belong there: a
600
+ * theme, a region, a working state is the author's own and this build has no
601
+ * opinion about it. A classifying tag is different, because something queries
602
+ * it — a
488
603
  * settlement tagged `village` appears in the list of villages and an untagged
489
604
  * one does not, so `vilage` does not merely look wrong, it removes the note from
490
605
  * an index while the index still renders a table that looks complete.
491
606
  *
492
607
  * **This list is not a closed set.** An unrecognised tag is legal, because the
493
- * region is open; what is reported is a **near miss** — a tag close enough to a
608
+ * values are the author's; what is reported is a **near miss** — a tag close
609
+ * enough to a
494
610
  * declared one to be a typo of it.
495
611
  *
496
612
  * **Each group names the types it applies to**, and that scope is what makes the
@@ -662,14 +778,12 @@ export function exclusiveTagGroups(type, groups = DECLARED_TAGS) {
662
778
  * The spelling of the tag *itself* still is — a near miss is a near miss, and
663
779
  * the frontmatter lint is what reports it; nothing here guesses.
664
780
  *
665
- * Reads `tags` and the singular `tag` spelling.
666
- *
667
781
  * @param {object|null|undefined} fm - Parsed frontmatter.
668
782
  * @param {string} tag - The tag to look for, in its declared spelling.
669
783
  * @returns {boolean} Whether the note carries it.
670
784
  */
671
785
  export function hasTag(fm, tag) {
672
- const raw = fm?.tags ?? fm?.tag;
786
+ const raw = fm?.tags;
673
787
  if (raw == null) return false;
674
788
  const wanted = String(tag).toLowerCase();
675
789
  for (const entry of Array.isArray(raw) ? raw : [raw]) {
@@ -781,8 +895,8 @@ export const NOTE_VOCABULARY = Object.freeze({
781
895
  ...STANDINGS,
782
896
  describe:
783
897
  "Bodies the being belongs to, keyed by Address, each entry holding the " +
784
- "standing it holds there — `rank`, a level on that body's own ladder, " +
785
- "and `office`, a post that body names.",
898
+ "standing it holds there — `rank`, required, a level on that body's own " +
899
+ "ladder, and `office`, optional, a post that body names.",
786
900
  },
787
901
  {
788
902
  name: "socialTies",
@@ -794,7 +908,14 @@ export const NOTE_VOCABULARY = Object.freeze({
794
908
  check: checkSocialTies,
795
909
  describe: `Defining ties directed from this being to others: ${SOCIAL_TIES.map(({ term, meaning }) => `\`${term}\` (${meaning})`).join("; ")}`,
796
910
  },
797
- { name: "gender", ...TEXT, describe: "`male`, `female` or `other`." },
911
+ {
912
+ name: "gender",
913
+ ...TEXT,
914
+ describe:
915
+ "One of `male`, `female`, `nonbinary`, `none` or `other`. " +
916
+ "`none` says the being has no gender; an absent field says its " +
917
+ "gender is unrecorded.",
918
+ },
798
919
  {
799
920
  name: "species",
800
921
  ...LINK,
@@ -862,12 +983,19 @@ export const NOTE_VOCABULARY = Object.freeze({
862
983
  {
863
984
  name: "frame",
864
985
  ...TEXT,
865
- describe: "Relative frame — `scant`, `light`, `medium`, `large` or `massive`.",
986
+ describe:
987
+ "Relative frame — one of `scant`, `light`, `medium`, `heavy` or `massive`.",
866
988
  },
867
989
  { name: "appearance.eye_color", ...TEXT, describe: "Eye colour." },
868
990
  { name: "appearance.hair_color", ...TEXT, describe: "Hair colour." },
869
991
  { name: "appearance.skin_color", ...TEXT, describe: "Skin colour." },
870
- { name: "appearance.complexion", ...TEXT, describe: "Complexion." },
992
+ {
993
+ name: "appearance.complexion",
994
+ ...TEXT_OR_LIST,
995
+ describe:
996
+ "The skin's condition — one value, or several, because a face " +
997
+ "carries more than one at once. A single value means a list of one.",
998
+ },
871
999
  {
872
1000
  name: "appearance.extra_features",
873
1001
  ...LIST,
@@ -931,6 +1059,16 @@ export const NOTE_VOCABULARY = Object.freeze({
931
1059
  {
932
1060
  name: "governance.ranks",
933
1061
  ...LIST,
1062
+ // A rung is `{level, title, description}` with an optional
1063
+ // `lore`. The shape check sees a list and stops there, so the
1064
+ // rungs inside it are checked here — a ladder is what a being's
1065
+ // `rank` indexes into, and an incomplete rung resolves to a
1066
+ // standing with no name.
1067
+ check: checkRankLadder,
1068
+ // Each rung takes a row of its own, labelled and anchored by
1069
+ // its title, ordered by level — the ladder's parallel to
1070
+ // `governance.offices`' `roster`.
1071
+ ranks: true,
934
1072
  describe: "The ladder of ranks the body confers — level, title, description.",
935
1073
  },
936
1074
  {
@@ -1358,10 +1496,13 @@ export const NOTE_VOCABULARY = Object.freeze({
1358
1496
  data: Object.freeze([
1359
1497
  ...CALENDAR_FIELDS,
1360
1498
  {
1361
- name: "event",
1362
- kind: "map",
1363
- shape: "event metadata map",
1364
- describe: "A dated occurrence and its relationships to other events and places.",
1499
+ name: "events",
1500
+ kind: "list",
1501
+ shape: "list of `{ when, until?, recurs? }`",
1502
+ check: checkLoreEvents,
1503
+ describe:
1504
+ "This note's dated occurrences — a founding once, an anniversary " +
1505
+ "that recurs, or a list of recorded happenings.",
1365
1506
  },
1366
1507
  ]),
1367
1508
  }),