@heroiclands/package-build 22.28.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 (156) hide show
  1. package/CHANGELOG.md +72 -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 +125 -142
  6. package/docs/authoring/assets.md +1 -1
  7. package/docs/authoring/dates-and-calendars.md +12 -6
  8. package/docs/authoring/first-note.md +1 -1
  9. package/docs/authoring/frontmatter.md +2 -2
  10. package/docs/authoring/links-and-markup.md +145 -29
  11. package/docs/commands.md +8 -10
  12. package/docs/configuration.md +13 -11
  13. package/docs/content-format.md +3 -3
  14. package/docs/guides/book.md +59 -4
  15. package/docs/guides/icons.md +4 -5
  16. package/docs/guides/packs.md +6 -0
  17. package/docs/guides/reference-pages.md +21 -1
  18. package/docs/guides/release-and-deploy.md +17 -1
  19. package/docs/guides/site.md +17 -18
  20. package/docs/reference/format-details.md +904 -275
  21. package/docs/reference/note-types.md +282 -91
  22. package/docs/reference/theme-pages.md +58 -66
  23. package/engine/address-values.mjs +124 -4
  24. package/engine/address.mjs +44 -3
  25. package/engine/anchors.mjs +103 -3
  26. package/engine/art-slots.mjs +2 -9
  27. package/engine/base-compiler.mjs +25 -6
  28. package/engine/calendar-artifacts.mjs +4 -1
  29. package/engine/calendar-notes.mjs +166 -102
  30. package/engine/content-format-check.mjs +2 -3
  31. package/engine/content-format.mjs +44 -1
  32. package/engine/content-format.yaml +84 -16
  33. package/engine/content-images.mjs +10 -1
  34. package/engine/content-index.mjs +37 -9
  35. package/engine/content-links.mjs +24 -5
  36. package/engine/content-tables.mjs +24 -3
  37. package/engine/data-keys.mjs +347 -0
  38. package/engine/derived-sections.mjs +322 -0
  39. package/engine/diagnostics.mjs +28 -0
  40. package/engine/event-fields.mjs +187 -0
  41. package/engine/event-rows.mjs +126 -0
  42. package/engine/event-views.mjs +305 -0
  43. package/engine/exported-scene.mjs +29 -53
  44. package/engine/frontmatter-lint.mjs +103 -77
  45. package/engine/frontmatter.mjs +7 -2
  46. package/engine/generate.mjs +53 -0
  47. package/engine/generated-sections.mjs +223 -0
  48. package/engine/heading-attributes.mjs +4 -2
  49. package/engine/helpers.mjs +12 -1
  50. package/engine/holdings.mjs +12 -33
  51. package/engine/ids.mjs +17 -11
  52. package/engine/index.mjs +4 -4
  53. package/engine/infobox-render.mjs +21 -6
  54. package/engine/infobox.mjs +217 -99
  55. package/engine/journals.mjs +104 -47
  56. package/engine/literature-notes.mjs +136 -0
  57. package/engine/literature-works.mjs +161 -0
  58. package/engine/map-graphviz.mjs +4 -18
  59. package/engine/markdown-expressions.mjs +116 -11
  60. package/engine/metadata-index.mjs +34 -0
  61. package/engine/near-miss.mjs +90 -0
  62. package/engine/note-addresses.mjs +75 -19
  63. package/engine/note-dates.mjs +5 -3
  64. package/engine/note-event-terms.mjs +364 -0
  65. package/engine/note-events.mjs +823 -0
  66. package/engine/note-vocabulary.mjs +195 -92
  67. package/engine/office-holders.mjs +20 -54
  68. package/engine/packagebuild-index.mjs +16 -1
  69. package/engine/pdf-build.mjs +97 -60
  70. package/engine/pdf-index.mjs +263 -0
  71. package/engine/pdf-render.mjs +47 -5
  72. package/engine/place-relation-terms.mjs +174 -0
  73. package/engine/place-relations.mjs +72 -160
  74. package/engine/rank-ladder.mjs +14 -41
  75. package/engine/reckoning-markers.mjs +2 -0
  76. package/engine/region-events.mjs +6 -12
  77. package/engine/scenes.mjs +73 -468
  78. package/engine/site-build.mjs +74 -85
  79. package/engine/site-index.mjs +3 -1
  80. package/engine/site-maps.mjs +43 -29
  81. package/engine/site-preview.mjs +40 -31
  82. package/engine/social-ties.mjs +2 -1
  83. package/engine/sql-tables.mjs +212 -9
  84. package/engine/standing-terms.mjs +145 -6
  85. package/engine/standings.mjs +37 -49
  86. package/engine/stub-lint.mjs +6 -0
  87. package/engine/web-wikilinks.mjs +21 -1
  88. package/engine/wikilink-syntax.mjs +34 -4
  89. package/engine/wikilinks.mjs +27 -2
  90. package/hugo-theme/layouts/partials/infobox/entry.html +4 -1
  91. package/hugo-theme/layouts/partials/page-body.html +0 -2
  92. package/hugo-theme/static/css/style.css +25 -55
  93. package/package.json +1 -2
  94. package/packagebuild-metadata.jsonl +13 -13
  95. package/sohl/default-item-art.mjs +6 -6
  96. package/sohl/note-schemas.mjs +8 -73
  97. package/types/engine/address-values.d.mts +50 -3
  98. package/types/engine/address.d.mts +21 -0
  99. package/types/engine/anchors.d.mts +49 -2
  100. package/types/engine/art-slots.d.mts +1 -1
  101. package/types/engine/base-compiler.d.mts +13 -4
  102. package/types/engine/calendar-notes.d.mts +7 -0
  103. package/types/engine/content-format.d.mts +52 -0
  104. package/types/engine/content-images.d.mts +5 -1
  105. package/types/engine/content-tables.d.mts +9 -1
  106. package/types/engine/data-keys.d.mts +145 -0
  107. package/types/engine/derived-sections.d.mts +73 -0
  108. package/types/engine/diagnostics.d.mts +15 -0
  109. package/types/engine/event-fields.d.mts +99 -0
  110. package/types/engine/event-rows.d.mts +12 -0
  111. package/types/engine/event-views.d.mts +67 -0
  112. package/types/engine/exported-scene.d.mts +5 -2
  113. package/types/engine/frontmatter-lint.d.mts +3 -3
  114. package/types/engine/generated-sections.d.mts +143 -0
  115. package/types/engine/holdings.d.mts +6 -20
  116. package/types/engine/ids.d.mts +15 -12
  117. package/types/engine/index.d.mts +1 -1
  118. package/types/engine/infobox.d.mts +13 -3
  119. package/types/engine/journals.d.mts +24 -11
  120. package/types/engine/literature-notes.d.mts +29 -0
  121. package/types/engine/literature-works.d.mts +67 -0
  122. package/types/engine/map-graphviz.d.mts +2 -10
  123. package/types/engine/markdown-expressions.d.mts +12 -3
  124. package/types/engine/near-miss.d.mts +42 -0
  125. package/types/engine/note-addresses.d.mts +7 -1
  126. package/types/engine/note-event-terms.d.mts +68 -0
  127. package/types/engine/note-events.d.mts +71 -0
  128. package/types/engine/note-vocabulary.d.mts +37 -8
  129. package/types/engine/office-holders.d.mts +3 -0
  130. package/types/engine/pdf-index.d.mts +35 -0
  131. package/types/engine/pdf-render.d.mts +8 -2
  132. package/types/engine/place-relation-terms.d.mts +83 -0
  133. package/types/engine/place-relations.d.mts +11 -66
  134. package/types/engine/rank-ladder.d.mts +2 -0
  135. package/types/engine/region-events.d.mts +6 -12
  136. package/types/engine/scenes.d.mts +6 -32
  137. package/types/engine/site-build.d.mts +8 -22
  138. package/types/engine/site-maps.d.mts +15 -7
  139. package/types/engine/sql-tables.d.mts +77 -2
  140. package/types/engine/standing-terms.d.mts +37 -4
  141. package/types/engine/standings.d.mts +14 -9
  142. package/types/engine/wikilink-syntax.d.mts +20 -0
  143. package/types/engine/wikilinks.d.mts +3 -2
  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/place-map.html +0 -9
  153. package/types/engine/itinerary-scenes.d.mts +0 -29
  154. package/types/engine/lore-events.d.mts +0 -14
  155. package/types/engine/map-notes.d.mts +0 -297
  156. 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,12 +104,22 @@ 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";
122
+ import { LITERATURE_FIELDS, checkLiteratureNote } from "./literature-notes.mjs";
112
123
  import { SOCIAL_TIES, SOCIAL_TIE_TARGET_TYPES } from "./social-tie-terms.mjs";
113
124
  import { parseNoteDate } from "./note-dates.mjs";
114
125
  import { reckoningContext } from "./reckoning-markers.mjs";
@@ -153,6 +164,10 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
153
164
  * so each takes a row labelled and anchored by the post's own name.
154
165
  * @property {"subType"} [keySelector] - An alternative `subType:<skill-subtype>` key.
155
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.
156
171
  * @property {string} [nullText] - Infobox text for an explicitly authored null with a declared meaning.
157
172
  * @property {string} [shape] - Human-readable shape, for a finding and for
158
173
  * documentation.
@@ -161,8 +176,7 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
161
176
  * quoting the whole map back, so the string an author has to correct is the
162
177
  * one the message holds.
163
178
  * @property {string} [ref] - The Address's default type, an art slot declares
164
- * one — `icon` for `icon` and `tokenIcon`, `image` for `bgImage` and
165
- * `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
166
180
  * separator states its own address. Distinct from a `FieldSpec`'s `code`
167
181
  * ({@link module:engine/field-spec}), which names a **Shortcode** instead.
168
182
  * @property {"pack"} [keys] - For a `scalar-or-map` field, what its keys name.
@@ -175,6 +189,13 @@ import { positionOfFrontmatterPath } from "./diagnostics.mjs";
175
189
  * it, and the linter calls whatever it is handed. A place's `borders` and
176
190
  * `routes` carry one, because what they state is checked against the note
177
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.
178
199
  * @property {string} describe - One line, for the author-facing reference.
179
200
  */
180
201
 
@@ -225,6 +246,13 @@ const TEXT = Object.freeze({ shape: "string", kind: "string" });
225
246
  /** A list. */
226
247
  const LIST = Object.freeze({ shape: "list", kind: "list" });
227
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
+
228
256
  /**
229
257
  * One value, or several.
230
258
  *
@@ -233,7 +261,11 @@ const LIST = Object.freeze({ shape: "list", kind: "list" });
233
261
  * That is what separates this from `list-or-map`, whose halves say different
234
262
  * things.
235
263
  */
236
- 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
+ });
237
269
 
238
270
  /** A single Address. */
239
271
  const LINK = Object.freeze({ shape: "an Address", kind: "address" });
@@ -258,6 +290,11 @@ const STANDINGS = Object.freeze({
258
290
  kind: "list-or-map",
259
291
  entryKind: "address",
260
292
  keyKind: "address",
293
+ values: Object.freeze({
294
+ kind: "map",
295
+ shape: "a map — `{ rank, office? }`",
296
+ fields: STANDING_FIELDS,
297
+ }),
261
298
  standings: true,
262
299
  ref: "affiliation",
263
300
  accepts: STANDING_BODY_TYPES,
@@ -320,6 +357,29 @@ const TOKEN_ICON = Object.freeze({
320
357
  describe: "What a token on the canvas wears — an `icon` address; unset, it follows `icon`.",
321
358
  });
322
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
+
323
383
  /**
324
384
  * One top-level frontmatter key.
325
385
  *
@@ -454,9 +514,24 @@ export const SHARED_DATA_FIELDS = Object.freeze([
454
514
  }),
455
515
  Object.freeze({
456
516
  name: "harnworld",
457
- shape: "map",
517
+ shape: "`{ realm?, ritual? }`",
458
518
  kind: "map",
459
- 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`.",
460
535
  }),
461
536
  Object.freeze({
462
537
  name: "icon",
@@ -477,7 +552,20 @@ export const SHARED_DATA_FIELDS = Object.freeze([
477
552
  ]);
478
553
 
479
554
  /**
480
- * 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.
481
569
  *
482
570
  * @type {readonly DataFieldSpec[]}
483
571
  */
@@ -486,6 +574,20 @@ const GEAR = Object.freeze([
486
574
  { name: "value", ...NUM, describe: "What the thing is worth." },
487
575
  { name: "quality", ...NUM, describe: "How well it is made." },
488
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
+ },
489
591
  ]);
490
592
 
491
593
  /** Gear that is counted rather than carried singly. */
@@ -854,10 +956,9 @@ export const NOTE_VOCABULARY = Object.freeze({
854
956
  // package's declared present compute — see `engine/being-age.mjs`.
855
957
  check: checkBeingAge,
856
958
  data: Object.freeze([
857
- { name: "social", shape: "map", kind: "map", describe: "The being's social profile." },
858
959
  TOKEN_ICON,
859
960
  TEMPLATE_PRIORITY,
860
- { name: "archetypes", ...LIST, describe: "Archetypal behaviours the being fits." },
961
+ { name: "archetypes", ...TEXT_LIST, describe: "Archetypal behaviours the being fits." },
861
962
  { name: "occupation", ...TEXT, describe: "What the being does for a living." },
862
963
  {
863
964
  name: "stations",
@@ -905,6 +1006,10 @@ export const NOTE_VOCABULARY = Object.freeze({
905
1006
  accepts: SOCIAL_TIE_TARGET_TYPES,
906
1007
  terms: SOCIAL_TIES,
907
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
+ }),
908
1013
  check: checkSocialTies,
909
1014
  describe: `Defining ties directed from this being to others: ${SOCIAL_TIES.map(({ term, meaning }) => `\`${term}\` (${meaning})`).join("; ")}`,
910
1015
  },
@@ -998,7 +1103,7 @@ export const NOTE_VOCABULARY = Object.freeze({
998
1103
  },
999
1104
  {
1000
1105
  name: "appearance.extra_features",
1001
- ...LIST,
1106
+ ...TEXT_LIST,
1002
1107
  describe: "Anything else a stranger would notice.",
1003
1108
  },
1004
1109
  ]),
@@ -1030,6 +1135,7 @@ export const NOTE_VOCABULARY = Object.freeze({
1030
1135
  ]),
1031
1136
  data: Object.freeze([
1032
1137
  TEMPLATE_PRIORITY,
1138
+ EVENTS_FIELD,
1033
1139
  {
1034
1140
  name: "demonym",
1035
1141
  ...TEXT,
@@ -1060,6 +1166,12 @@ export const NOTE_VOCABULARY = Object.freeze({
1060
1166
  {
1061
1167
  name: "governance.ranks",
1062
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
+ }),
1063
1175
  // A rung is `{level, title, description}` with an optional
1064
1176
  // `lore`. The shape check sees a list and stops there, so the
1065
1177
  // rungs inside it are checked here — a ladder is what a being's
@@ -1074,7 +1186,13 @@ export const NOTE_VOCABULARY = Object.freeze({
1074
1186
  },
1075
1187
  {
1076
1188
  name: "governance.offices",
1077
- ...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
+ }),
1078
1196
  // A map of named posts: each takes a row of its own, labelled
1079
1197
  // by the post and anchored by it, so the description a body
1080
1198
  // declares is a destination a being's `office` can link to.
@@ -1119,6 +1237,11 @@ export const NOTE_VOCABULARY = Object.freeze({
1119
1237
  ref: "affiliation",
1120
1238
  accepts: ["affiliation"],
1121
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
+ }),
1122
1245
  describe: "Standing with other affiliations — aligned, unaligned, rival, nemesis.",
1123
1246
  },
1124
1247
  ]),
@@ -1248,6 +1371,7 @@ export const NOTE_VOCABULARY = Object.freeze({
1248
1371
  keyKind: "shortcode",
1249
1372
  keySelector: "subType",
1250
1373
  shape: "a map keyed by Shortcode or subType selector",
1374
+ values: Object.freeze({ kind: "number", shape: "a number" }),
1251
1375
  describe:
1252
1376
  "Bonuses and penalties, each naming a skill or a `subType:<skill-subtype>`.",
1253
1377
  },
@@ -1479,25 +1603,19 @@ export const NOTE_VOCABULARY = Object.freeze({
1479
1603
  "custom", // How a people practices a rite, observance, or usage.
1480
1604
  "bestiary", // A kind of creature that is not a people.
1481
1605
  "gathering", // A recurring public assembly, such as a fair or tournament.
1606
+ "literature", // A work a people tells, sings, or writes, such as an epic or legend.
1482
1607
  ]),
1483
1608
  // A lore note is prose, and what it *is* about is its subType — with
1484
- // one exception. A calendar is a division of the year, and a division
1485
- // is data; the check scopes the family to the subType that means it,
1486
- // because `DataFieldSpec` declares the keys a type accepts and not the
1487
- // subType that may write them.
1488
- check: checkCalendarNote,
1489
- data: Object.freeze([
1490
- ...CALENDAR_FIELDS,
1491
- {
1492
- name: "events",
1493
- kind: "list",
1494
- shape: "list of `{ when, until?, recurs? }`",
1495
- check: checkLoreEvents,
1496
- describe:
1497
- "This note's dated occurrences — a founding once, an anniversary " +
1498
- "that recurs, or a list of recorded happenings.",
1499
- },
1500
- ]),
1609
+ // two exceptions. A calendar is a division of the year, and a division
1610
+ // is data; a work of literature states whose it is, in what tongue and
1611
+ // about what. The checks scope each family to the subType that means
1612
+ // it, because `DataFieldSpec` declares the keys a type accepts and not
1613
+ // the subType that may write them.
1614
+ check: (note, context) => [
1615
+ ...checkCalendarNote(note, context),
1616
+ ...checkLiteratureNote(note),
1617
+ ],
1618
+ data: Object.freeze([...CALENDAR_FIELDS, ...LITERATURE_FIELDS, EVENTS_FIELD]),
1501
1619
  }),
1502
1620
 
1503
1621
  place: Object.freeze({
@@ -1513,6 +1631,7 @@ export const NOTE_VOCABULARY = Object.freeze({
1513
1631
  ]),
1514
1632
  check: checkPlace,
1515
1633
  data: Object.freeze([
1634
+ EVENTS_FIELD,
1516
1635
  {
1517
1636
  name: "calendar",
1518
1637
  ...LINK,
@@ -1579,6 +1698,11 @@ export const NOTE_VOCABULARY = Object.freeze({
1579
1698
  name: "borders",
1580
1699
  ...LIST,
1581
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
+ }),
1582
1706
  check: checkBorders,
1583
1707
  describe:
1584
1708
  "Places sharing a frontier with this one — each the other's shortcode " +
@@ -1588,6 +1712,11 @@ export const NOTE_VOCABULARY = Object.freeze({
1588
1712
  name: "routes",
1589
1713
  ...LIST,
1590
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
+ }),
1591
1720
  check: checkRoutes,
1592
1721
  describe:
1593
1722
  "Journeys from this place's centre — where the destination lies, how " +
@@ -1651,7 +1780,7 @@ export const NOTE_VOCABULARY = Object.freeze({
1651
1780
  },
1652
1781
  {
1653
1782
  name: "party.archetypes",
1654
- ...LIST,
1783
+ ...TEXT_LIST,
1655
1784
  describe: "Archetypes the scenario is written for.",
1656
1785
  },
1657
1786
  ]),
@@ -1661,66 +1790,52 @@ export const NOTE_VOCABULARY = Object.freeze({
1661
1790
 
1662
1791
  map: Object.freeze({
1663
1792
  stubbable: true,
1664
- // The subType classifies the map; `data.scene` carries its canvas.
1793
+ // The subType classifies the map; `data.scene` is its Foundry Scene.
1665
1794
  subTypes: Object.freeze(["battlemap", "localmap", "regionalmap", "totm"]),
1666
1795
  data: Object.freeze([
1667
1796
  {
1668
1797
  name: "scene",
1669
- ...ANY,
1670
- 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.",
1671
1804
  },
1672
1805
  {
1673
1806
  name: "fixup",
1674
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
+ }),
1675
1837
  describe: "Asset address replacements in an exported Scene.",
1676
1838
  },
1677
- // A Scene has no `img`, so the shared art key reaches nothing here:
1678
- // a map's background is its own slot, and an `image` address.
1679
- {
1680
- name: "bgImage",
1681
- ...LINK,
1682
- ref: "image",
1683
- accepts: ART_SLOTS.find((slot) => slot.key === "bgImage").accepts,
1684
- describe: "The map's background art — an `image` address.",
1685
- },
1686
- {
1687
- name: "scale",
1688
- ...ANY,
1689
- describe: "Regional map distance per grid unit: {distance, unit}.",
1690
- },
1691
- {
1692
- name: "dimensions",
1693
- ...LIST,
1694
- describe: "`[width, height]` in whole pixels — the art's own size.",
1695
- },
1696
- {
1697
- name: "pxPerGrid",
1698
- ...NUM,
1699
- describe: "Whole pixels per grid square; must match the art.",
1700
- },
1701
- { name: "navName", ...TEXT, describe: "Short name for the navigation bar." },
1702
- { name: "levelName", ...TEXT, describe: "Name of the embedded level." },
1703
- {
1704
- name: "backgroundColor",
1705
- ...TEXT,
1706
- describe: "Colour shown where the art does not reach.",
1707
- },
1708
- { name: "overlay", ...TEXT, describe: "Path to the foreground art." },
1709
- // Geometry carries no `kind`. The specification lists each
1710
- // as a sequence while the notes authoring them today write
1711
- // a map keyed by name, and a lint has no business picking
1712
- // the winner of a disagreement the format has not settled.
1713
- { name: "walls", ...ANY, describe: "Wall segments." },
1714
- { name: "doors", ...ANY, describe: "Doors." },
1715
- { name: "lights", ...ANY, describe: "Light sources." },
1716
- { name: "tiles", ...ANY, describe: "Tiles." },
1717
- { name: "sounds", ...ANY, describe: "Ambient sounds." },
1718
- { name: "regions", ...ANY, describe: "Regions and their behaviours." },
1719
- {
1720
- name: "notes",
1721
- ...ANY,
1722
- describe: "Map pins, each a grid location and an anchor in this note's own body.",
1723
- },
1724
1839
  {
1725
1840
  name: "place",
1726
1841
  ...LINK,
@@ -1860,15 +1975,3 @@ export function subTypes(type, vocabulary = NOTE_VOCABULARY) {
1860
1975
  if (!entry || !Object.hasOwn(entry, "subTypes")) return undefined;
1861
1976
  return entry.subTypes;
1862
1977
  }
1863
-
1864
- /**
1865
- * Accepted migration input with no canonical field or emitted value.
1866
- *
1867
- * Legacy affiliation domains are ignored. They never supply a government.
1868
- * @param {string} type - The note type.
1869
- * @param {string} key - An authored data key.
1870
- * @returns {boolean} Whether the obsolete key is accepted during migration.
1871
- */
1872
- export function isLegacyDataField(type, key) {
1873
- return currentType(type) === "affiliation" && key === "domains";
1874
- }
@@ -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) };