@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
@@ -91,6 +91,7 @@ import { currentType } from "./ids.mjs";
91
91
  // What a place is next to and reachable from — the two relation lists and
92
92
  // the checks that hold them to their closed sets and to each other.
93
93
  import { checkBorders, checkRoutes } from "./place-relations.mjs";
94
+ import { BORDER_FIELDS, ROUTE_FIELDS } from "./place-relation-terms.mjs";
94
95
  import {
95
96
  CALENDAR_FIELDS,
96
97
  INVARIANT_FIELDS,
@@ -103,11 +104,20 @@ import {
103
104
  import { checkBeingAge } from "./being-age.mjs";
104
105
  import { checkSocialTies } from "./social-ties.mjs";
105
106
  import { checkStandings } from "./standings.mjs";
106
- import { STANDING_BODY_TYPES } from "./standing-terms.mjs";
107
+ import {
108
+ OFFICE_FIELDS,
109
+ RUNG_FIELDS,
110
+ STANDING_BODY_TYPES,
111
+ STANDING_FIELDS,
112
+ } from "./standing-terms.mjs";
107
113
  import { checkDatedOffices } from "./office-holders.mjs";
108
114
  import { checkAffiliationRankFloor, checkRankLadder } from "./rank-ladder.mjs";
109
115
  import { checkCalendarChoice } from "./calendar-choice.mjs";
110
- import { checkLoreEvents } from "./lore-events.mjs";
116
+ import { checkNoteEvents } from "./note-events.mjs";
117
+ // The closed set of standings one body holds toward another, read from the one
118
+ // list the compiled Item is held to, so the lint and the compile cannot differ.
119
+ import { AFFILIATION_STANDINGS } from "../sohl/affiliation-standings.mjs";
120
+ import { EVENT_ENTRY } from "./note-event-terms.mjs";
111
121
  import { checkCultureChoice } from "./culture-choice.mjs";
112
122
  import { LITERATURE_FIELDS, checkLiteratureNote } from "./literature-notes.mjs";
113
123
  import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
@@ -154,6 +164,10 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
154
164
  * so each takes a row labelled and anchored by the post's own name.
155
165
  * @property {"subType"} [keySelector] - An alternative `subType:<skill-subtype>` key.
156
166
  * @property {readonly string[]} [accepts] - Allowed Address types, separate from `ref`.
167
+ * @property {readonly ("prose"|"event")[]} [anchors] - The anchor kinds an
168
+ * Address here may name with `#<anchor>`. Absent, an anchor is refused.
169
+ * @property {boolean} [single] - The Address names exactly one event: an
170
+ * anchor of kind `event`, or a note holding one event and no more.
157
171
  * @property {string} [nullText] - Infobox text for an explicitly authored null with a declared meaning.
158
172
  * @property {string} [shape] - Human-readable shape, for a finding and for
159
173
  * documentation.
@@ -162,8 +176,7 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
162
176
  * quoting the whole map back, so the string an author has to correct is the
163
177
  * one the message holds.
164
178
  * @property {string} [ref] - The Address's default type, an art slot declares
165
- * one — `icon` for `icon` and `tokenIcon`, `image` for `bgImage` and
166
- * `banner` — so `icon: anvil` names `icon-anvil` while a value carrying the
179
+ * one — `icon` for `icon` and `tokenIcon`, `image` for `banner` — so `icon: anvil` names `icon-anvil` while a value carrying the
167
180
  * separator states its own address. Distinct from a `FieldSpec`'s `code`
168
181
  * ({@link module:engine/field-spec}), which names a **Shortcode** instead.
169
182
  * @property {"pack"} [keys] - For a `scalar-or-map` field, what its keys name.
@@ -176,6 +189,13 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
176
189
  * it, and the linter calls whatever it is handed. A place's `borders` and
177
190
  * `routes` carry one, because what they state is checked against the note
178
191
  * at the other end.
192
+ * @property {readonly import("./data-keys.mjs").InnerKeySpec[]} [fields] - The
193
+ * keys of a value that is a map, closed to them — see
194
+ * {@link module:engine/data-keys}.
195
+ * @property {object} [entries] - What one entry of a list value is, where the
196
+ * entries are not Addresses (`entryKind` says that).
197
+ * @property {object} [values] - What the value under each key of a keyed map
198
+ * is, where the keys are data rather than declared names.
179
199
  * @property {string} describe - One line, for the author-facing reference.
180
200
  */
181
201
 
@@ -226,6 +246,13 @@ const TEXT = Object.freeze({ shape: "string", kind: "string" });
226
246
  /** A list. */
227
247
  const LIST = Object.freeze({ shape: "list", kind: "list" });
228
248
 
249
+ /** A list of strings. */
250
+ const TEXT_LIST = Object.freeze({
251
+ shape: "list of strings",
252
+ kind: "list",
253
+ entries: Object.freeze({ kind: "string", shape: "a string" }),
254
+ });
255
+
229
256
  /**
230
257
  * One value, or several.
231
258
  *
@@ -234,7 +261,11 @@ const LIST = Object.freeze({ shape: "list", kind: "list" });
234
261
  * That is what separates this from `list-or-map`, whose halves say different
235
262
  * things.
236
263
  */
237
- const TEXT_OR_LIST = Object.freeze({ shape: "string or list", kind: "string-or-list" });
264
+ const TEXT_OR_LIST = Object.freeze({
265
+ shape: "string or list",
266
+ kind: "string-or-list",
267
+ entries: Object.freeze({ kind: "string", shape: "a string" }),
268
+ });
238
269
 
239
270
  /** A single Address. */
240
271
  const LINK = Object.freeze({ shape: "an Address", kind: "address" });
@@ -259,6 +290,11 @@ const STANDINGS = Object.freeze({
259
290
  kind: "list-or-map",
260
291
  entryKind: "address",
261
292
  keyKind: "address",
293
+ values: Object.freeze({
294
+ kind: "map",
295
+ shape: "a map — `{ rank, office? }`",
296
+ fields: STANDING_FIELDS,
297
+ }),
262
298
  standings: true,
263
299
  ref: "affiliation",
264
300
  accepts: STANDING_BODY_TYPES,
@@ -321,6 +357,29 @@ const TOKEN_ICON = Object.freeze({
321
357
  describe: "What a token on the canvas wears — an `icon` address; unset, it follows `icon`.",
322
358
  });
323
359
 
360
+ /**
361
+ * What happened to or at a subject — the `events` key, declared once and spread
362
+ * into every type that carries events, so the schema cannot differ between
363
+ * them. The entry's own schema and its check live in
364
+ * {@link module:engine/note-events}.
365
+ *
366
+ * Declared here rather than in that module because its imports reach back to
367
+ * this one: a function declaration is bound before either module runs, and an
368
+ * object is not.
369
+ *
370
+ * @type {DataFieldSpec}
371
+ */
372
+ export const EVENTS_FIELD = Object.freeze({
373
+ name: "events",
374
+ kind: "list",
375
+ shape: "list of event entries",
376
+ entries: EVENT_ENTRY,
377
+ check: checkNoteEvents,
378
+ describe:
379
+ "What happened to or at this subject — each entry one dated, attributed event: " +
380
+ "a founding, a war, a fall, or an occasion that recurs.",
381
+ });
382
+
324
383
  /**
325
384
  * One top-level frontmatter key.
326
385
  *
@@ -455,9 +514,24 @@ export const SHARED_DATA_FIELDS = Object.freeze([
455
514
  }),
456
515
  Object.freeze({
457
516
  name: "harnworld",
458
- shape: "map",
517
+ shape: "`{ realm?, ritual? }`",
459
518
  kind: "map",
460
- describe: "HârnWorld source details shared by every system.",
519
+ fields: Object.freeze([
520
+ Object.freeze({
521
+ name: "realm",
522
+ kind: "string",
523
+ shape: "a string",
524
+ describe: "The HârnWorld realm the subject belongs to.",
525
+ }),
526
+ Object.freeze({
527
+ name: "ritual",
528
+ kind: "list",
529
+ shape: "list of strings",
530
+ entries: Object.freeze({ kind: "string", shape: "a string" }),
531
+ describe: "The HârnWorld religions the subject observes.",
532
+ }),
533
+ ]),
534
+ describe: "HârnWorld source details shared by every system — `realm` and `ritual`.",
461
535
  }),
462
536
  Object.freeze({
463
537
  name: "icon",
@@ -478,7 +552,20 @@ export const SHARED_DATA_FIELDS = Object.freeze([
478
552
  ]);
479
553
 
480
554
  /**
481
- * The four gear values every carried thing declares.
555
+ * An Address naming one event: `place-ironfells#sack`, or a note holding
556
+ * exactly one event, with a bare shortcode read as a `lore` note.
557
+ */
558
+ const EVENT_ADDRESS = Object.freeze({
559
+ kind: "address",
560
+ ref: "lore",
561
+ anchors: Object.freeze(["event"]),
562
+ single: true,
563
+ shape: "an Address of one event",
564
+ });
565
+
566
+ /**
567
+ * The values every carried thing declares: the four a game reads, and the two
568
+ * events of its history.
482
569
  *
483
570
  * @type {readonly DataFieldSpec[]}
484
571
  */
@@ -487,6 +574,20 @@ const GEAR = Object.freeze([
487
574
  { name: "value", ...NUM, describe: "What the thing is worth." },
488
575
  { name: "quality", ...NUM, describe: "How well it is made." },
489
576
  { name: "durability", ...NUM, describe: "How much wear it takes before it fails." },
577
+ {
578
+ name: "made",
579
+ ...EVENT_ADDRESS,
580
+ describe:
581
+ "The event in which the thing was made — `lore-forging`, or one event of a " +
582
+ "note as `place-ironfells#raising`.",
583
+ },
584
+ {
585
+ name: "lost",
586
+ ...EVENT_ADDRESS,
587
+ describe:
588
+ "The event in which the thing was lost — `lore-flood`, or one event of a note " +
589
+ "as `place-ironfells#sack`.",
590
+ },
490
591
  ]);
491
592
 
492
593
  /** Gear that is counted rather than carried singly. */
@@ -855,10 +956,9 @@ export const NOTE_VOCABULARY = Object.freeze({
855
956
  // package's declared present compute — see `engine/being-age.mjs`.
856
957
  check: checkBeingAge,
857
958
  data: Object.freeze([
858
- { name: "social", shape: "map", kind: "map", describe: "The being's social profile." },
859
959
  TOKEN_ICON,
860
960
  TEMPLATE_PRIORITY,
861
- { name: "archetypes", ...LIST, describe: "Archetypal behaviours the being fits." },
961
+ { name: "archetypes", ...TEXT_LIST, describe: "Archetypal behaviours the being fits." },
862
962
  { name: "occupation", ...TEXT, describe: "What the being does for a living." },
863
963
  {
864
964
  name: "stations",
@@ -906,6 +1006,10 @@ export const NOTE_VOCABULARY = Object.freeze({
906
1006
  accepts: SOCIAL_TIE_TARGET_TYPES,
907
1007
  terms: SOCIAL_TIES,
908
1008
  shape: "a map keyed by Address",
1009
+ values: Object.freeze({
1010
+ kind: "string",
1011
+ shape: `a tie — ${SOCIAL_TIES.map(({ term }) => term).join(", ")}`,
1012
+ }),
909
1013
  check: checkSocialTies,
910
1014
  describe: `Defining ties directed from this being to others: ${SOCIAL_TIES.map(({ term, meaning }) => `\`${term}\` (${meaning})`).join("; ")}`,
911
1015
  },
@@ -999,7 +1103,7 @@ export const NOTE_VOCABULARY = Object.freeze({
999
1103
  },
1000
1104
  {
1001
1105
  name: "appearance.extra_features",
1002
- ...LIST,
1106
+ ...TEXT_LIST,
1003
1107
  describe: "Anything else a stranger would notice.",
1004
1108
  },
1005
1109
  ]),
@@ -1031,6 +1135,7 @@ export const NOTE_VOCABULARY = Object.freeze({
1031
1135
  ]),
1032
1136
  data: Object.freeze([
1033
1137
  TEMPLATE_PRIORITY,
1138
+ EVENTS_FIELD,
1034
1139
  {
1035
1140
  name: "demonym",
1036
1141
  ...TEXT,
@@ -1061,6 +1166,12 @@ export const NOTE_VOCABULARY = Object.freeze({
1061
1166
  {
1062
1167
  name: "governance.ranks",
1063
1168
  ...LIST,
1169
+ shape: "list of `{ level, title, description, lore? }`",
1170
+ entries: Object.freeze({
1171
+ kind: "map",
1172
+ shape: "a map — `{ level, title, description, lore? }`",
1173
+ fields: RUNG_FIELDS,
1174
+ }),
1064
1175
  // A rung is `{level, title, description}` with an optional
1065
1176
  // `lore`. The shape check sees a list and stops there, so the
1066
1177
  // rungs inside it are checked here — a ladder is what a being's
@@ -1075,7 +1186,13 @@ export const NOTE_VOCABULARY = Object.freeze({
1075
1186
  },
1076
1187
  {
1077
1188
  name: "governance.offices",
1078
- ...ANY,
1189
+ kind: "map",
1190
+ shape: "a map of named offices",
1191
+ values: Object.freeze({
1192
+ kind: "scalar-or-map",
1193
+ shape: "a description, or `{ description, holders }`",
1194
+ fields: OFFICE_FIELDS,
1195
+ }),
1079
1196
  // A map of named posts: each takes a row of its own, labelled
1080
1197
  // by the post and anchored by it, so the description a body
1081
1198
  // declares is a destination a being's `office` can link to.
@@ -1120,6 +1237,11 @@ export const NOTE_VOCABULARY = Object.freeze({
1120
1237
  ref: "affiliation",
1121
1238
  accepts: ["affiliation"],
1122
1239
  shape: "a map keyed by Address",
1240
+ values: Object.freeze({
1241
+ kind: "string",
1242
+ allowed: AFFILIATION_STANDINGS,
1243
+ shape: `a standing — ${AFFILIATION_STANDINGS.join(", ")}`,
1244
+ }),
1123
1245
  describe: "Standing with other affiliations — aligned, unaligned, rival, nemesis.",
1124
1246
  },
1125
1247
  ]),
@@ -1249,6 +1371,7 @@ export const NOTE_VOCABULARY = Object.freeze({
1249
1371
  keyKind: "shortcode",
1250
1372
  keySelector: "subType",
1251
1373
  shape: "a map keyed by Shortcode or subType selector",
1374
+ values: Object.freeze({ kind: "number", shape: "a number" }),
1252
1375
  describe:
1253
1376
  "Bonuses and penalties, each naming a skill or a `subType:<skill-subtype>`.",
1254
1377
  },
@@ -1492,19 +1615,7 @@ export const NOTE_VOCABULARY = Object.freeze({
1492
1615
  ...checkCalendarNote(note, context),
1493
1616
  ...checkLiteratureNote(note),
1494
1617
  ],
1495
- data: Object.freeze([
1496
- ...CALENDAR_FIELDS,
1497
- ...LITERATURE_FIELDS,
1498
- {
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.",
1506
- },
1507
- ]),
1618
+ data: Object.freeze([...CALENDAR_FIELDS, ...LITERATURE_FIELDS, EVENTS_FIELD]),
1508
1619
  }),
1509
1620
 
1510
1621
  place: Object.freeze({
@@ -1520,6 +1631,7 @@ export const NOTE_VOCABULARY = Object.freeze({
1520
1631
  ]),
1521
1632
  check: checkPlace,
1522
1633
  data: Object.freeze([
1634
+ EVENTS_FIELD,
1523
1635
  {
1524
1636
  name: "calendar",
1525
1637
  ...LINK,
@@ -1586,6 +1698,11 @@ export const NOTE_VOCABULARY = Object.freeze({
1586
1698
  name: "borders",
1587
1699
  ...LIST,
1588
1700
  shape: "list of `{ to, bearing }` entries",
1701
+ entries: Object.freeze({
1702
+ kind: "map",
1703
+ shape: "a map — `{ to, bearing }`",
1704
+ fields: BORDER_FIELDS,
1705
+ }),
1589
1706
  check: checkBorders,
1590
1707
  describe:
1591
1708
  "Places sharing a frontier with this one — each the other's shortcode " +
@@ -1595,6 +1712,11 @@ export const NOTE_VOCABULARY = Object.freeze({
1595
1712
  name: "routes",
1596
1713
  ...LIST,
1597
1714
  shape: "list of `{ to, bearing, mode, days, terrain?, leagues? }` entries",
1715
+ entries: Object.freeze({
1716
+ kind: "map",
1717
+ shape: "a map — `{ to, bearing, mode, days, terrain?, leagues? }`",
1718
+ fields: ROUTE_FIELDS,
1719
+ }),
1598
1720
  check: checkRoutes,
1599
1721
  describe:
1600
1722
  "Journeys from this place's centre — where the destination lies, how " +
@@ -1658,7 +1780,7 @@ export const NOTE_VOCABULARY = Object.freeze({
1658
1780
  },
1659
1781
  {
1660
1782
  name: "party.archetypes",
1661
- ...LIST,
1783
+ ...TEXT_LIST,
1662
1784
  describe: "Archetypes the scenario is written for.",
1663
1785
  },
1664
1786
  ]),
@@ -1668,66 +1790,52 @@ export const NOTE_VOCABULARY = Object.freeze({
1668
1790
 
1669
1791
  map: Object.freeze({
1670
1792
  stubbable: true,
1671
- // The subType classifies the map; `data.scene` carries its canvas.
1793
+ // The subType classifies the map; `data.scene` is its Foundry Scene.
1672
1794
  subTypes: Object.freeze(["battlemap", "localmap", "regionalmap", "totm"]),
1673
1795
  data: Object.freeze([
1674
1796
  {
1675
1797
  name: "scene",
1676
- ...ANY,
1677
- describe: "A Foundry Scene export with all its authored fields.",
1798
+ kind: "map",
1799
+ shape: "an object — a Foundry Scene document as exported",
1800
+ describe:
1801
+ "The Foundry Scene as exported, passed through unchanged apart " +
1802
+ "from `data.fixup` and its pins marked `#anchor`. Nothing inside it is checked; " +
1803
+ "the book prints its levels' background images.",
1678
1804
  },
1679
1805
  {
1680
1806
  name: "fixup",
1681
1807
  ...LIST,
1808
+ shape: "list of `{ path, type, value }`",
1809
+ entries: Object.freeze({
1810
+ kind: "map",
1811
+ shape: "a map — `{ path, type, value }`",
1812
+ fields: Object.freeze([
1813
+ Object.freeze({
1814
+ name: "path",
1815
+ kind: "string",
1816
+ required: true,
1817
+ shape: "a property path into `data.scene`",
1818
+ describe: "The exported field to replace, from `data.scene`.",
1819
+ }),
1820
+ Object.freeze({
1821
+ name: "type",
1822
+ kind: "string",
1823
+ required: true,
1824
+ allowed: Object.freeze(["address"]),
1825
+ shape: "`address`",
1826
+ describe: "What `value` is: `address`.",
1827
+ }),
1828
+ Object.freeze({
1829
+ name: "value",
1830
+ kind: "address",
1831
+ required: true,
1832
+ shape: "an asset Address",
1833
+ describe: "The asset Address whose path replaces the exported one.",
1834
+ }),
1835
+ ]),
1836
+ }),
1682
1837
  describe: "Asset address replacements in an exported Scene.",
1683
1838
  },
1684
- // A Scene has no `img`, so the shared art key reaches nothing here:
1685
- // a map's background is its own slot, and an `image` address.
1686
- {
1687
- name: "bgImage",
1688
- ...LINK,
1689
- ref: "image",
1690
- accepts: ART_SLOTS.find((slot) => slot.key === "bgImage").accepts,
1691
- describe: "The map's background art — an `image` address.",
1692
- },
1693
- {
1694
- name: "scale",
1695
- ...ANY,
1696
- describe: "Regional map distance per grid unit: {distance, unit}.",
1697
- },
1698
- {
1699
- name: "dimensions",
1700
- ...LIST,
1701
- describe: "`[width, height]` in whole pixels — the art's own size.",
1702
- },
1703
- {
1704
- name: "pxPerGrid",
1705
- ...NUM,
1706
- describe: "Whole pixels per grid square; must match the art.",
1707
- },
1708
- { name: "navName", ...TEXT, describe: "Short name for the navigation bar." },
1709
- { name: "levelName", ...TEXT, describe: "Name of the embedded level." },
1710
- {
1711
- name: "backgroundColor",
1712
- ...TEXT,
1713
- describe: "Colour shown where the art does not reach.",
1714
- },
1715
- { name: "overlay", ...TEXT, describe: "Path to the foreground art." },
1716
- // Geometry carries no `kind`. The specification lists each
1717
- // as a sequence while the notes authoring them today write
1718
- // a map keyed by name, and a lint has no business picking
1719
- // the winner of a disagreement the format has not settled.
1720
- { name: "walls", ...ANY, describe: "Wall segments." },
1721
- { name: "doors", ...ANY, describe: "Doors." },
1722
- { name: "lights", ...ANY, describe: "Light sources." },
1723
- { name: "tiles", ...ANY, describe: "Tiles." },
1724
- { name: "sounds", ...ANY, describe: "Ambient sounds." },
1725
- { name: "regions", ...ANY, describe: "Regions and their behaviours." },
1726
- {
1727
- name: "notes",
1728
- ...ANY,
1729
- describe: "Map pins, each a grid location and an anchor in this note's own body.",
1730
- },
1731
1839
  {
1732
1840
  name: "place",
1733
1841
  ...LINK,
@@ -1867,15 +1975,3 @@ export function subTypes(type, vocabulary = NOTE_VOCABULARY) {
1867
1975
  if (!entry || !Object.hasOwn(entry, "subTypes")) return undefined;
1868
1976
  return entry.subTypes;
1869
1977
  }
1870
-
1871
- /**
1872
- * Accepted migration input with no canonical field or emitted value.
1873
- *
1874
- * Legacy affiliation domains are ignored. They never supply a government.
1875
- * @param {string} type - The note type.
1876
- * @param {string} key - An authored data key.
1877
- * @returns {boolean} Whether the obsolete key is accepted during migration.
1878
- */
1879
- export function isLegacyDataField(type, key) {
1880
- return currentType(type) === "affiliation" && key === "domains";
1881
- }
@@ -1,4 +1,13 @@
1
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
+ *
2
11
  * SPDX-License-Identifier: GPL-3.0-or-later
3
12
  */
4
13
 
@@ -7,8 +16,10 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
7
16
  import { parseNoteDate } from "./note-dates.mjs";
8
17
  import { reckoningContext } from "./reckoning-markers.mjs";
9
18
 
10
- const OFFICE_KEYS = new Set(["description", "holders"]);
11
- const HOLDER_KEYS = new Set(["being", "start", "end", "contested"]);
19
+ import { HOLDER_FIELDS, OFFICE_FIELDS } from "./standing-terms.mjs";
20
+
21
+ export { HOLDER_FIELDS, OFFICE_FIELDS };
22
+
12
23
  const mapping = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
13
24
 
14
25
  /**
@@ -26,65 +37,20 @@ export function checkDatedOffices(note, { index } = {}) {
26
37
  severity: "error",
27
38
  message,
28
39
  });
29
- if (!mapping(offices))
30
- return [
31
- at(
32
- ["data", "governance", "offices"],
33
- "data.governance.offices must be a map of named offices",
34
- ),
35
- ];
40
+ // The roster's shape — a map of offices, each a description or a map of
41
+ // `OFFICE_FIELDS`, each holder a map of `HOLDER_FIELDS` — is the inner-key
42
+ // check's. What is checked here is what the holders and their dates mean.
43
+ if (!mapping(offices)) return [];
36
44
 
37
45
  const dates = reckoningContext(index);
38
46
  for (const [office, value] of Object.entries(offices)) {
39
47
  const base = ["data", "governance", "offices", office];
40
- if (typeof value === "string") continue;
41
- if (!mapping(value)) {
42
- findings.push(
43
- at(base, `office ${office} must be a description or a {description, holders} map`),
44
- );
45
- continue;
46
- }
47
- for (const key of Object.keys(value))
48
- if (!OFFICE_KEYS.has(key))
49
- findings.push(
50
- at(
51
- [...base, key],
52
- `office ${office} has unknown key ${key}; use description or holders`,
53
- ),
54
- );
55
- if (typeof value.description !== "string")
56
- findings.push(
57
- at([...base, "description"], `office ${office} needs a string description`),
58
- );
59
- if (!Array.isArray(value.holders)) {
60
- findings.push(at([...base, "holders"], `office ${office} needs a holders list`));
61
- continue;
62
- }
48
+ if (!mapping(value) || !Array.isArray(value.holders)) continue;
63
49
  const terms = [];
64
50
  value.holders.forEach((row, position) => {
65
51
  const rowPath = [...base, "holders", position];
66
- if (!mapping(row)) {
67
- findings.push(at(rowPath, `office ${office} holder must be a map`));
68
- return;
69
- }
70
- for (const key of Object.keys(row))
71
- if (!HOLDER_KEYS.has(key))
72
- findings.push(
73
- at([...rowPath, key], `office ${office} holder has unknown key ${key}`),
74
- );
75
- if (row.contested !== undefined && typeof row.contested !== "boolean")
76
- findings.push(
77
- at(
78
- [...rowPath, "contested"],
79
- `office ${office} contested must be true or false`,
80
- ),
81
- );
82
-
83
- if (typeof row.being !== "string" || !row.being.trim()) {
84
- findings.push(
85
- at([...rowPath, "being"], `office ${office} holder needs a being Address`),
86
- );
87
- } else {
52
+ if (!mapping(row)) return;
53
+ if (typeof row.being === "string" && row.being.trim()) {
88
54
  const tuple = parseAddress(row.being, {
89
55
  package: index?.contentPackage,
90
56
  system: "note",
@@ -50,6 +50,7 @@ import { fileURLToPath } from "node:url";
50
50
  import { collectAssetRecords } from "./asset-index.mjs";
51
51
  import { recordPath, sortKeysDeep } from "./index-records.mjs";
52
52
  import { PACKAGEBUILD_PACKAGE, metadataFileName } from "./packages.mjs";
53
+ import { decodeIndexAddresses, publishAddresses } from "./note-addresses.mjs";
53
54
 
54
55
  /**
55
56
  * This package's own root, wherever it is installed.
@@ -117,7 +118,21 @@ export function packageBuildRecords(assetsBase = PACKAGEBUILD_ASSETS) {
117
118
  */
118
119
  export function emitPackageBuildIndex({ assetsBase, file = PACKAGEBUILD_INDEX_FILE } = {}) {
119
120
  const records = packageBuildRecords(assetsBase);
120
- const text = records.length ? `${records.map((r) => JSON.stringify(r)).join("\n")}\n` : "";
121
+ // Written the way every published index is: each Address an object.
122
+ const text =
123
+ records.length ?
124
+ `${records
125
+ .map((record) =>
126
+ JSON.stringify(
127
+ sortKeysDeep(
128
+ publishAddresses(
129
+ decodeIndexAddresses(record, { package: PACKAGEBUILD_PACKAGE }),
130
+ ),
131
+ ),
132
+ ),
133
+ )
134
+ .join("\n")}\n`
135
+ : "";
121
136
  fs.mkdirSync(path.dirname(file), { recursive: true });
122
137
  fs.writeFileSync(file, text);
123
138
  return { file, assets: records.length, bytes: Buffer.byteLength(text) };