@heroiclands/package-build 22.9.2 → 22.10.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 (96) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/CONTENT.md +33 -0
  3. package/bin/content-build.mjs +3 -1
  4. package/docs/api.md +10 -6
  5. package/docs/content-format.md +384 -200
  6. package/engine/actor-compiler.mjs +20 -7
  7. package/engine/address-fields.mjs +404 -0
  8. package/engine/address-values.mjs +80 -0
  9. package/engine/address.mjs +769 -0
  10. package/engine/art-fields.mjs +112 -97
  11. package/engine/art-slots.mjs +81 -0
  12. package/engine/base-compiler.mjs +59 -7
  13. package/engine/bundle-notes.mjs +5 -2
  14. package/engine/bundles.mjs +17 -10
  15. package/engine/content-address.mjs +23 -186
  16. package/engine/content-embeds.mjs +18 -4
  17. package/engine/content-format-check.mjs +50 -22
  18. package/engine/content-index.mjs +85 -43
  19. package/engine/content-links.mjs +105 -104
  20. package/engine/content-lint.mjs +11 -1
  21. package/engine/content-tables.mjs +9 -1
  22. package/engine/field-spec.mjs +20 -4
  23. package/engine/folder-notes.mjs +44 -15
  24. package/engine/foundry-entries.mjs +3 -0
  25. package/engine/frontmatter-lint.mjs +144 -43
  26. package/engine/frontmatter.mjs +55 -10
  27. package/engine/generate.mjs +7 -1
  28. package/engine/helpers.mjs +37 -9
  29. package/engine/holdings.mjs +41 -12
  30. package/engine/index-records.mjs +2 -0
  31. package/engine/infobox-render.mjs +2 -1
  32. package/engine/infobox.mjs +26 -6
  33. package/engine/item-compiler.mjs +5 -0
  34. package/engine/item-docs.mjs +7 -44
  35. package/engine/map-dot.mjs +2 -0
  36. package/engine/map-notes.mjs +7 -2
  37. package/engine/map-places.mjs +42 -10
  38. package/engine/metadata-index.mjs +60 -5
  39. package/engine/note-addresses.mjs +310 -0
  40. package/engine/note-vocabulary.mjs +145 -26
  41. package/engine/pdf-build.mjs +1 -1
  42. package/engine/pdf-toc.mjs +17 -2
  43. package/engine/place-relations.mjs +160 -50
  44. package/engine/populations.mjs +17 -9
  45. package/engine/reference-targets.mjs +54 -0
  46. package/engine/scenes.mjs +5 -2
  47. package/engine/shortcode-references.mjs +68 -0
  48. package/engine/site-build.mjs +39 -15
  49. package/engine/site-index.mjs +57 -75
  50. package/engine/sql-tables.mjs +43 -9
  51. package/engine/system-block.mjs +21 -0
  52. package/engine/system-vocabulary.mjs +46 -0
  53. package/engine/web-wikilinks.mjs +11 -9
  54. package/engine/wikilink-syntax.mjs +21 -0
  55. package/engine/wikilinks.mjs +92 -306
  56. package/hm3/actors.mjs +8 -24
  57. package/hm3/item-fields.mjs +15 -5
  58. package/package.json +1 -1
  59. package/sohl/infobox.mjs +80 -17
  60. package/sohl/item-fields.mjs +114 -41
  61. package/types/engine/address-fields.d.mts +90 -0
  62. package/types/engine/address-values.d.mts +34 -0
  63. package/types/engine/address.d.mts +452 -0
  64. package/types/engine/art-fields.d.mts +59 -53
  65. package/types/engine/art-slots.d.mts +40 -0
  66. package/types/engine/base-compiler.d.mts +24 -2
  67. package/types/engine/bundle-notes.d.mts +2 -2
  68. package/types/engine/content-address.d.mts +3 -139
  69. package/types/engine/content-embeds.d.mts +1 -1
  70. package/types/engine/content-format-check.d.mts +25 -0
  71. package/types/engine/content-index.d.mts +2 -2
  72. package/types/engine/content-links.d.mts +6 -5
  73. package/types/engine/field-spec.d.mts +44 -9
  74. package/types/engine/folder-notes.d.mts +11 -10
  75. package/types/engine/frontmatter-lint.d.mts +7 -97
  76. package/types/engine/frontmatter.d.mts +16 -1
  77. package/types/engine/helpers.d.mts +2 -2
  78. package/types/engine/infobox.d.mts +6 -0
  79. package/types/engine/item-docs.d.mts +1 -35
  80. package/types/engine/map-notes.d.mts +2 -2
  81. package/types/engine/note-addresses.d.mts +41 -0
  82. package/types/engine/note-vocabulary.d.mts +20 -3
  83. package/types/engine/place-relations.d.mts +12 -1
  84. package/types/engine/populations.d.mts +17 -2
  85. package/types/engine/reference-targets.d.mts +9 -0
  86. package/types/engine/shortcode-references.d.mts +14 -0
  87. package/types/engine/site-build.d.mts +3 -3
  88. package/types/engine/site-index.d.mts +9 -30
  89. package/types/engine/sql-tables.d.mts +2 -2
  90. package/types/engine/system-block.d.mts +17 -0
  91. package/types/engine/system-vocabulary.d.mts +16 -0
  92. package/types/engine/wikilink-syntax.d.mts +7 -0
  93. package/types/engine/wikilinks.d.mts +6 -109
  94. package/types/hm3/item-fields.d.mts +5 -0
  95. package/types/sohl/infobox.d.mts +4 -5
  96. package/types/sohl/item-fields.d.mts +8 -0
package/sohl/infobox.mjs CHANGED
@@ -51,6 +51,8 @@
51
51
  * @module
52
52
  */
53
53
 
54
+ import { parseAddress, isAddressTuple, completeAddress } from "../engine/address.mjs";
55
+ import { contentPackage } from "../engine/content-package.mjs";
54
56
  import {
55
57
  DURATION_LABELS,
56
58
  GEAR_UNITS,
@@ -63,6 +65,8 @@ import {
63
65
  import { currentType } from "../engine/ids.mjs";
64
66
  import { subTypes } from "../engine/note-vocabulary.mjs";
65
67
  import { systemBlock, systemData } from "../engine/system-block.mjs";
68
+ import { readCanonicalKey } from "../engine/content-address.mjs";
69
+ import { readQualifier } from "../engine/address.mjs";
66
70
  import { NOTE_SCHEMAS } from "./note-schemas.mjs";
67
71
  import { GEAR_TYPE_TO_KEY } from "./being-info.mjs";
68
72
 
@@ -174,15 +178,45 @@ function isMapping(value) {
174
178
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
175
179
  }
176
180
 
181
+ /**
182
+ * Every content type `model:` may name — the note vocabulary this system
183
+ * declares, engine types included.
184
+ *
185
+ * @type {ReadonlySet<string>}
186
+ */
187
+ const MODEL_TYPES = new Set(Object.keys(NOTE_SCHEMAS));
188
+
189
+ /**
190
+ * The `(type, shortcode)` an entry's `model:` names, or `undefined`.
191
+ *
192
+ * `model` is an Address, read by the same grammar every wikilink is —
193
+ * `type-shortcode` within this package, `package-system-type-shortcode` to
194
+ * name a dependency's catalogue. This decode answers neither of the two
195
+ * questions {@link module:engine/address} asks about the package: a runtime
196
+ * lookup among one actor's embedded items resolves by `(type, shortcode)`
197
+ * alone, where packages do not exist, so a fully qualified `model` is read for
198
+ * its last two segments exactly as a bare one is.
199
+ *
200
+ * @param {unknown} model - The authored value.
201
+ * @returns {{type: string, shortcode: string}|undefined} What it names.
202
+ */
203
+ function modelAddress(model) {
204
+ const target = parseAddress(
205
+ model,
206
+ { package: contentPackage(), system: "sohl", types: MODEL_TYPES },
207
+ { declared: true, legacyShortcodeCase: true },
208
+ );
209
+ return target.reason ? undefined : target;
210
+ }
211
+
177
212
  /**
178
213
  * What one entry of `sohl.items` names.
179
214
  *
180
215
  * An entry addresses its item three ways and every tree uses all three: a
181
- * `model` address whose last two hyphen-separated segments are always
182
- * `<type>-<shortcode>`, explicit `type` / `shortcode` keys, or a locally
183
- * authored item stating its type with the shortcode inside its own `system`
184
- * block. One decode covers all of them, so nothing downstream has to know
185
- * which form a note happened to use.
216
+ * `model` address, explicit `type` / `shortcode` keys, or a locally authored
217
+ * item stating its type with the shortcode inside its own `system` block. One
218
+ * decode covers all of them, so nothing downstream has to know which form a
219
+ * note happened to use.
186
220
  *
187
221
  * @param {object} entry - One `sohl.items` entry.
188
222
  * @returns {{type: string, shortcode: string, name: string, system: object}|undefined}
@@ -190,19 +224,21 @@ function isMapping(value) {
190
224
  */
191
225
  export function decodeItem(entry) {
192
226
  if (!isMapping(entry)) return undefined;
227
+ const address = entry.model ? modelAddress(entry.model) : undefined;
193
228
  let type = typeof entry.type === "string" ? entry.type : "";
194
229
  let shortcode = typeof entry.shortcode === "string" ? entry.shortcode : "";
195
- if (!type && typeof entry.model === "string") {
196
- const segments = entry.model.split("-");
197
- if (segments.length >= 2) {
198
- type = segments[segments.length - 2];
199
- if (!shortcode) shortcode = segments[segments.length - 1];
230
+ if (!type) {
231
+ const named = address;
232
+ if (named) {
233
+ type = named.type;
234
+ if (!shortcode) shortcode = named.shortcode;
200
235
  }
201
236
  }
202
237
  const system = isMapping(entry.system) ? entry.system : {};
203
238
  if (!shortcode && typeof system.shortcode === "string") shortcode = system.shortcode;
204
239
  if (!type) return undefined;
205
240
  return {
241
+ ...(address ? { address } : {}),
206
242
  type: currentType(type),
207
243
  shortcode,
208
244
  name: typeof entry.name === "string" ? entry.name : "",
@@ -222,6 +258,32 @@ function itemsOf(fm, block) {
222
258
  return (Array.isArray(declared) ? declared : []).map(decodeItem).filter(Boolean);
223
259
  }
224
260
 
261
+ /**
262
+ * A model's documentation presentation, with its native name as plain text
263
+ * when no documentation is published. The model keeps its native identity.
264
+ * @param {object} item - Decoded native item.
265
+ * @param {Function} resolve - The medium's reference resolver.
266
+ * @param {string} block - The native system.
267
+ * @returns {object|undefined} Documentation links and display metadata.
268
+ */
269
+ function itemPresentation(item, resolve, block) {
270
+ if (!item.shortcode) return undefined;
271
+ if (!item.address)
272
+ return resolve?.(item.shortcode, { kind: "shortcode", type: item.type, system: block });
273
+ const documentation = completeAddress({ ...item.address, system: "none" });
274
+ const found = resolve?.(documentation, { type: documentation.type });
275
+ const native =
276
+ !found?.name || !found?.subType ? resolve?.(item.address, { type: item.type }) : undefined;
277
+ if (!found && !native) return undefined;
278
+ return {
279
+ name: found?.name ?? native?.name,
280
+ subType: found?.subType ?? native?.subType,
281
+ address: documentation,
282
+ ...(found?.url ? { url: found.url } : {}),
283
+ ...(found?.uuid ? { uuid: found.uuid } : {}),
284
+ };
285
+ }
286
+
225
287
  /**
226
288
  * One item as a value a renderer can draw: its name, and a link where the
227
289
  * index reached it.
@@ -229,10 +291,11 @@ function itemsOf(fm, block) {
229
291
  * @param {object} item - A decoded item.
230
292
  * @param {(ref: unknown, hint?: object) => object|undefined} resolve - The
231
293
  * medium's resolver.
232
- * @returns {{text: string, url?: string, uuid?: string, address?: string}} The value.
294
+ * @param {string} block - The native system.
295
+ * @returns {{text: string, url?: string, uuid?: string, address?: import("../engine/address.mjs").AddressTuple}} The value.
233
296
  */
234
- function itemValue(item, resolve) {
235
- const found = item.shortcode ? resolve?.(item.shortcode, { type: item.type }) : undefined;
297
+ function itemValue(item, resolve, block) {
298
+ const found = itemPresentation(item, resolve, block);
236
299
  const value = { text: item.name || found?.name || humanizeValue(item.shortcode) };
237
300
  if (found?.url) value.url = found.url;
238
301
  if (found?.uuid) value.uuid = found.uuid;
@@ -277,9 +340,9 @@ export function beingSections(fm, { block, resolve }) {
277
340
  if (item.type !== "skill") continue;
278
341
  const mastery = item.system.masteryLevelBase;
279
342
  if (!hasValue(mastery)) continue;
280
- const found = item.shortcode ? resolve?.(item.shortcode, { type: "skill" }) : undefined;
343
+ const found = itemPresentation(item, resolve, block);
281
344
  const family = item.system.subType ?? found?.subType;
282
- const value = itemValue(item, resolve);
345
+ const value = itemValue(item, resolve, block);
283
346
  const entry = { ...value, text: `${value.text} ${mastery}` };
284
347
  if (family && families.has(family)) families.get(family).push(entry);
285
348
  else unplaced.push(entry);
@@ -298,7 +361,7 @@ export function beingSections(fm, { block, resolve }) {
298
361
  const mystical = [];
299
362
  for (const item of items) {
300
363
  if (item.type !== "mysticalability") continue;
301
- mystical.push(itemValue(item, resolve));
364
+ mystical.push(itemValue(item, resolve, block));
302
365
  }
303
366
  if (mystical.length) {
304
367
  sections.push({
@@ -313,7 +376,7 @@ export function beingSections(fm, { block, resolve }) {
313
376
  for (const item of items) {
314
377
  const key = GEAR_TYPE_TO_KEY[item.type];
315
378
  if (!key) continue;
316
- gear.get(key).push(itemValue(item, resolve));
379
+ gear.get(key).push(itemValue(item, resolve, block));
317
380
  }
318
381
  const gearGroups = [];
319
382
  for (const [key, entries] of gear) {
@@ -35,6 +35,7 @@
35
35
  * @module
36
36
  */
37
37
 
38
+ import { isAddressTuple } from "../engine/address.mjs";
38
39
  import {
39
40
  AS_AUTHORED,
40
41
  BLANK_IS_DEFAULT,
@@ -143,14 +144,48 @@ const SKILL_APTITUDES = Object.freeze({
143
144
  read: (_raw, { fm }) => resolveSkillAptitudes(fm, noteContext(fm, "mystery")),
144
145
  });
145
146
 
147
+ /* --------------------------------------------------------------------- */
148
+ /* The Address positions */
149
+ /* --------------------------------------------------------------------- */
150
+
151
+ /*
152
+ * An affiliation names other notes in four of its fields, and each of those
153
+ * values is an **Address** — written at whatever length says what it means —
154
+ * while the compiled Item holds the **Shortcode** the runtime resolves among
155
+ * one actor's items. `engine/address-fields.mjs` is the step between them, and
156
+ * these are the positions it reads.
157
+ *
158
+ * Each states two things, and they are not the same statement: `type` is the
159
+ * default an omitted `<type>` segment takes, and `accepts` is the set of types
160
+ * the field takes at all. They coincide at all four, which is the common case
161
+ * rather than the rule — an art slot defaults to `icon` and accepts `icon` or
162
+ * `image` — so each is written out rather than derived from the other.
163
+ */
164
+
165
+ /** One place: an affiliation's seat. */
166
+ const PLACE_VALUE = Object.freeze({ type: "place", accepts: Object.freeze(["place"]) });
167
+
168
+ /** A list of places: the ones an affiliation holds sway over. */
169
+ const PLACE_ITEMS = Object.freeze({ ...PLACE_VALUE, holds: "items" });
170
+
171
+ /** A list of affiliations: the ones this affiliation answers to. */
172
+ const AFFILIATION_ITEMS = Object.freeze({
173
+ type: "affiliation",
174
+ accepts: Object.freeze(["affiliation"]),
175
+ holds: "items",
176
+ });
177
+
178
+ /** A map keyed by affiliation: this one's standing toward each. */
179
+ const AFFILIATION_KEYS = Object.freeze({ ...AFFILIATION_ITEMS, holds: "keys" });
180
+
146
181
  /** Standings toward other affiliations, validated against the closed list. */
147
182
  const RELATION = Object.freeze({
148
- shape: "map of affiliation shortcode → standing",
183
+ shape: "map of affiliation address → standing",
149
184
  read: (_raw, { fm }) => resolveRelation(fm, noteContext(fm, "affiliation")),
150
185
  });
151
186
 
152
187
  /**
153
- * A list of references, each a bare shortcode.
188
+ * A list of references, each an address.
154
189
  *
155
190
  * Blank entries are dropped rather than emitted: a list that has been edited
156
191
  * down in a property editor keeps its empty rows, and an empty string is not a
@@ -158,15 +193,15 @@ const RELATION = Object.freeze({
158
193
  * value here — a sovereign polity is subordinate to nobody — rather than an
159
194
  * unset one.
160
195
  */
161
- const SHORTCODE_LIST = Object.freeze({
162
- shape: "list of shortcodes",
196
+ const ADDRESS_LIST = Object.freeze({
197
+ shape: "list of addresses",
163
198
  kind: "list",
164
199
  read: (raw) =>
165
200
  (Array.isArray(raw) ? raw
166
201
  : raw == null || raw === "" ? []
167
202
  : [raw]
168
203
  )
169
- .map((entry) => String(entry ?? "").trim())
204
+ .map((entry) => (isAddressTuple(entry) ? entry : String(entry ?? "").trim()))
170
205
  .filter((entry) => entry !== ""),
171
206
  });
172
207
 
@@ -237,7 +272,8 @@ const GEAR_COMMON = Object.freeze([
237
272
  describe: "How many the stack holds. A compendium article ships one.",
238
273
  },
239
274
  {
240
- name: "weight",
275
+ name: "data.weight",
276
+ legacyKey: "weight",
241
277
  to: "weightBase",
242
278
  ...AS_AUTHORED,
243
279
  kind: "number",
@@ -245,7 +281,8 @@ const GEAR_COMMON = Object.freeze([
245
281
  describe: "Weight of one, in pounds.",
246
282
  },
247
283
  {
248
- name: "value",
284
+ name: "data.value",
285
+ legacyKey: "value",
249
286
  to: "valueBase",
250
287
  ...AS_AUTHORED,
251
288
  kind: "number",
@@ -253,7 +290,8 @@ const GEAR_COMMON = Object.freeze([
253
290
  describe: "Worth of one, in pence.",
254
291
  },
255
292
  {
256
- name: "quality",
293
+ name: "data.quality",
294
+ legacyKey: "quality",
257
295
  to: "qualityBase",
258
296
  ...AS_AUTHORED,
259
297
  kind: "number",
@@ -261,7 +299,8 @@ const GEAR_COMMON = Object.freeze([
261
299
  describe: "Craftsmanship, as a modifier to what the article does.",
262
300
  },
263
301
  {
264
- name: "durability",
302
+ name: "data.durability",
303
+ legacyKey: "durability",
265
304
  to: "durabilityBase",
266
305
  ...AS_AUTHORED,
267
306
  kind: "number",
@@ -293,6 +332,14 @@ const GEAR_COMMON = Object.freeze([
293
332
  * Every item type's frontmatter vocabulary, in the order the `system` block
294
333
  * emits it.
295
334
  *
335
+ * **A field whose shared source is a path into `data:` names both positions.**
336
+ * `name` is the source the content format's mapping tables state — `data.seat`,
337
+ * `data.weight` — and `legacyKey` is the key the system block still carries, which
338
+ * wins while a tree writes it. Naming only the source would move the in-block
339
+ * read to a key spelled `data.seat`, which nothing authors; naming only the key
340
+ * leaves the container the specification points at unread, and the field then
341
+ * compiles its default over an authored value with nothing said.
342
+ *
296
343
  * @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
297
344
  */
298
345
  export const ITEM_FIELDS = Object.freeze({
@@ -341,9 +388,11 @@ export const ITEM_FIELDS = Object.freeze({
341
388
  {
342
389
  // Plural because the field holds a map of many standings, one per
343
390
  // affiliation — the singular reads as a misnomer.
344
- name: "relations",
391
+ name: "data.relations",
392
+ legacyKey: "relations",
345
393
  to: "relations",
346
394
  ...RELATION,
395
+ address: AFFILIATION_KEYS,
347
396
  default: {},
348
397
  describe: "How this society regards others: aligned, unaligned, rival or nemesis.",
349
398
  },
@@ -351,21 +400,25 @@ export const ITEM_FIELDS = Object.freeze({
351
400
  // The organisational relation: which bodies this one answers to.
352
401
  // A list, because an affiliation may sit under more than one at
353
402
  // once — an arcane tradition within an order, say.
354
- name: "parents",
403
+ name: "data.parents",
404
+ legacyKey: "parents",
355
405
  to: "parents",
356
- ...SHORTCODE_LIST,
406
+ ...ADDRESS_LIST,
407
+ address: AFFILIATION_ITEMS,
357
408
  default: [],
358
- describe: "Affiliations this one is subordinate to, by shortcode.",
409
+ describe: "Affiliations this one is subordinate to, by address.",
359
410
  },
360
411
  {
361
412
  // Not `capital` or `headquarters`: each fits about half the eleven
362
413
  // subTypes, while a seat covers a polity, a guild, an order and a
363
414
  // faith alike.
364
- name: "seat",
415
+ name: "data.seat",
416
+ legacyKey: "seat",
365
417
  to: "seat",
366
418
  ...BLANK_IS_NULL,
419
+ address: PLACE_VALUE,
367
420
  default: null,
368
- describe: "Where the affiliation's authority sits, by place shortcode.",
421
+ describe: "Where the affiliation's authority sits, by place address.",
369
422
  },
370
423
  {
371
424
  // The geographic relation, kept apart from the organisational one
@@ -375,11 +428,13 @@ export const ITEM_FIELDS = Object.freeze({
375
428
  // mapping row states (`data.domains` → `system.domain`). The two
376
429
  // spellings are deliberate rather than a slip, so the declaration
377
430
  // carries both rather than either side guessing.
378
- name: "domains",
431
+ name: "data.domains",
432
+ legacyKey: "domains",
379
433
  to: "domain",
380
- ...SHORTCODE_LIST,
434
+ ...ADDRESS_LIST,
435
+ address: PLACE_ITEMS,
381
436
  default: [],
382
- describe: "Places this affiliation holds sway over, by shortcode.",
437
+ describe: "Places this affiliation holds sway over, by address.",
383
438
  },
384
439
  ]),
385
440
 
@@ -418,14 +473,16 @@ export const ITEM_FIELDS = Object.freeze({
418
473
  describe: "How readily the host throws it off.",
419
474
  },
420
475
  {
421
- name: "contagionIndex",
476
+ name: "data.contagionIndex",
477
+ legacyKey: "contagionIndex",
422
478
  to: "contagionIndexBase",
423
479
  ...NUMBER,
424
480
  default: 0,
425
481
  describe: "How readily it passes to someone else.",
426
482
  },
427
483
  {
428
- name: "transmission",
484
+ name: "data.transmission",
485
+ legacyKey: "transmission",
429
486
  to: "transmission",
430
487
  ...AS_AUTHORED,
431
488
  default: "none",
@@ -440,7 +497,8 @@ export const ITEM_FIELDS = Object.freeze({
440
497
  "Days from contracting to onset, rolled by the receiving actor. Unset means no incubation.",
441
498
  },
442
499
  {
443
- name: "outcome",
500
+ name: "data.outcome",
501
+ legacyKey: "outcome",
444
502
  to: "outcome",
445
503
  ...BLANK_IS_DEFAULT,
446
504
  default: "cured",
@@ -464,7 +522,8 @@ export const ITEM_FIELDS = Object.freeze({
464
522
  // cannot be grepped, and each phase's prose differs anyway — which is
465
523
  // the whole content of the declaration.
466
524
  {
467
- name: "onsetDurationFormula",
525
+ name: "data.onsetDurationFormula",
526
+ legacyKey: "onsetDurationFormula",
468
527
  to: "onsetDurationFormula",
469
528
  ...DURATION_FORMULA,
470
529
  omitWhenAbsent: true,
@@ -472,7 +531,8 @@ export const ITEM_FIELDS = Object.freeze({
472
531
  "Interval from contracting the affliction to the start of onset. Omitted when unset, leaving no incubation.",
473
532
  },
474
533
  {
475
- name: "onsetDurationBase",
534
+ name: "data.onsetDurationBase",
535
+ legacyKey: "onsetDurationBase",
476
536
  to: "onsetDurationBase",
477
537
  ...DURATION_BASE,
478
538
  omitWhenAbsent: true,
@@ -480,7 +540,8 @@ export const ITEM_FIELDS = Object.freeze({
480
540
  "The onset interval in seconds, standing in for a roll of the formula. Omitted when unset.",
481
541
  },
482
542
  {
483
- name: "healingCheckDurationFormula",
543
+ name: "data.healingCheckDurationFormula",
544
+ legacyKey: "healingCheckDurationFormula",
484
545
  to: "healingCheckDurationFormula",
485
546
  ...DURATION_FORMULA,
486
547
  omitWhenAbsent: true,
@@ -488,7 +549,8 @@ export const ITEM_FIELDS = Object.freeze({
488
549
  "Interval between healing checks, once the affliction is symptomatic. Omitted when unset.",
489
550
  },
490
551
  {
491
- name: "healingCheckDurationBase",
552
+ name: "data.healingCheckDurationBase",
553
+ legacyKey: "healingCheckDurationBase",
492
554
  to: "healingCheckDurationBase",
493
555
  ...DURATION_BASE,
494
556
  omitWhenAbsent: true,
@@ -496,7 +558,8 @@ export const ITEM_FIELDS = Object.freeze({
496
558
  "The healing-check interval in seconds, standing in for a roll of the formula. Omitted when unset.",
497
559
  },
498
560
  {
499
- name: "resolutionDurationFormula",
561
+ name: "data.resolutionDurationFormula",
562
+ legacyKey: "resolutionDurationFormula",
500
563
  to: "resolutionDurationFormula",
501
564
  ...DURATION_FORMULA,
502
565
  omitWhenAbsent: true,
@@ -504,7 +567,8 @@ export const ITEM_FIELDS = Object.freeze({
504
567
  "Interval from onset to the affliction running its course. Omitted when unset.",
505
568
  },
506
569
  {
507
- name: "resolutionDurationBase",
570
+ name: "data.resolutionDurationBase",
571
+ legacyKey: "resolutionDurationBase",
508
572
  to: "resolutionDurationBase",
509
573
  ...DURATION_BASE,
510
574
  omitWhenAbsent: true,
@@ -711,7 +775,7 @@ export const ITEM_FIELDS = Object.freeze({
711
775
  {
712
776
  name: "assocSkillCode",
713
777
  to: "assocSkillCode",
714
- ref: "skill",
778
+ code: "skill",
715
779
  ...BLANK_IS_NULL,
716
780
  default: null,
717
781
  describe: "Shortcode of the skill the mystery is tested against.",
@@ -719,7 +783,7 @@ export const ITEM_FIELDS = Object.freeze({
719
783
  {
720
784
  name: "assocAffiliationCode",
721
785
  to: "assocAffiliationCode",
722
- ref: "affiliation",
786
+ code: "affiliation",
723
787
  ...BLANK_IS_NULL,
724
788
  default: null,
725
789
  describe:
@@ -733,14 +797,16 @@ export const ITEM_FIELDS = Object.freeze({
733
797
  describe: "The mystery's level before any modifier.",
734
798
  },
735
799
  {
736
- name: "skillAptitudes",
800
+ name: "data.skillAptitudes",
801
+ legacyKey: "skillAptitudes",
737
802
  to: "skillAptitudes",
738
803
  ...SKILL_APTITUDES,
739
804
  default: {},
740
805
  describe: "Aptitude the mystery grants, per skill selector.",
741
806
  },
742
807
  {
743
- name: "charges",
808
+ name: "data.charges",
809
+ legacyKey: "charges",
744
810
  to: "charges",
745
811
  ...CHARGES,
746
812
  default: { value: null, max: null },
@@ -760,7 +826,7 @@ export const ITEM_FIELDS = Object.freeze({
760
826
  {
761
827
  name: "assocSkillCode",
762
828
  to: "assocSkillCode",
763
- ref: "skill",
829
+ code: "skill",
764
830
  ...AS_AUTHORED,
765
831
  default: "",
766
832
  describe: "Shortcode of the skill the ability is tested against.",
@@ -768,7 +834,7 @@ export const ITEM_FIELDS = Object.freeze({
768
834
  {
769
835
  name: "assocAffiliationCode",
770
836
  to: "assocAffiliationCode",
771
- ref: "affiliation",
837
+ code: "affiliation",
772
838
  ...BLANK_IS_NULL,
773
839
  default: null,
774
840
  describe:
@@ -796,7 +862,8 @@ export const ITEM_FIELDS = Object.freeze({
796
862
  describe: "The ability's level before any modifier.",
797
863
  },
798
864
  {
799
- name: "charges",
865
+ name: "data.charges",
866
+ legacyKey: "charges",
800
867
  to: "charges",
801
868
  ...CHARGES,
802
869
  default: { value: null, max: null },
@@ -884,7 +951,7 @@ export const ITEM_FIELDS = Object.freeze({
884
951
  {
885
952
  name: "parentSkillCode",
886
953
  to: "parentSkillCode",
887
- ref: "skill",
954
+ code: "skill",
888
955
  ...AS_AUTHORED,
889
956
  default: "",
890
957
  describe: "Shortcode of the skill this one specialises, for a specialisation.",
@@ -959,7 +1026,8 @@ export const ITEM_FIELDS = Object.freeze({
959
1026
  // `bloodLossAdvance*` is the one an author reaches for most: a trauma
960
1027
  // that sets it bleeds, and one that leaves it unset does not.
961
1028
  {
962
- name: "healingCheckDurationFormula",
1029
+ name: "data.healingCheckDurationFormula",
1030
+ legacyKey: "healingCheckDurationFormula",
963
1031
  to: "healingCheckDurationFormula",
964
1032
  ...DURATION_FORMULA,
965
1033
  omitWhenAbsent: true,
@@ -967,7 +1035,8 @@ export const ITEM_FIELDS = Object.freeze({
967
1035
  "Interval between healing checks. Omitted when unset, leaving the world's configured interval to apply.",
968
1036
  },
969
1037
  {
970
- name: "healingCheckDurationBase",
1038
+ name: "data.healingCheckDurationBase",
1039
+ legacyKey: "healingCheckDurationBase",
971
1040
  to: "healingCheckDurationBase",
972
1041
  ...DURATION_BASE,
973
1042
  omitWhenAbsent: true,
@@ -975,7 +1044,8 @@ export const ITEM_FIELDS = Object.freeze({
975
1044
  "The healing-check interval in seconds, standing in for a roll of the formula. Omitted when unset.",
976
1045
  },
977
1046
  {
978
- name: "bloodLossAdvanceDurationFormula",
1047
+ name: "data.bloodLossAdvanceDurationFormula",
1048
+ legacyKey: "bloodLossAdvanceDurationFormula",
979
1049
  to: "bloodLossAdvanceDurationFormula",
980
1050
  ...DURATION_FORMULA,
981
1051
  omitWhenAbsent: true,
@@ -983,7 +1053,8 @@ export const ITEM_FIELDS = Object.freeze({
983
1053
  "Interval between blood-loss advances. Omitted when unset, leaving the world's configured interval to apply.",
984
1054
  },
985
1055
  {
986
- name: "bloodLossAdvanceDurationBase",
1056
+ name: "data.bloodLossAdvanceDurationBase",
1057
+ legacyKey: "bloodLossAdvanceDurationBase",
987
1058
  to: "bloodLossAdvanceDurationBase",
988
1059
  ...DURATION_BASE,
989
1060
  omitWhenAbsent: true,
@@ -991,7 +1062,8 @@ export const ITEM_FIELDS = Object.freeze({
991
1062
  "The blood-loss interval in seconds. Setting it is what makes the wound bleed; omitted when unset, and the wound does not.",
992
1063
  },
993
1064
  {
994
- name: "courseDurationFormula",
1065
+ name: "data.courseDurationFormula",
1066
+ legacyKey: "courseDurationFormula",
995
1067
  to: "courseDurationFormula",
996
1068
  ...DURATION_FORMULA,
997
1069
  omitWhenAbsent: true,
@@ -999,7 +1071,8 @@ export const ITEM_FIELDS = Object.freeze({
999
1071
  "Interval between course tests, for a condition that runs one — shock, coma, infection. Omitted when unset.",
1000
1072
  },
1001
1073
  {
1002
- name: "courseDurationBase",
1074
+ name: "data.courseDurationBase",
1075
+ legacyKey: "courseDurationBase",
1003
1076
  to: "courseDurationBase",
1004
1077
  ...DURATION_BASE,
1005
1078
  omitWhenAbsent: true,
@@ -0,0 +1,90 @@
1
+ /**
2
+ * An Address position, as a field declaration states it.
3
+ *
4
+ * @typedef {object} AddressPosition
5
+ * @property {string} type - The type an omitted `<type>` segment takes.
6
+ * @property {readonly string[]} accepts - Every type the position accepts.
7
+ * @property {"value"|"items"|"keys"} [holds] - Where the Addresses are.
8
+ * Defaults to `value`.
9
+ */
10
+ /**
11
+ * What a reduction could not do, for a caller to report.
12
+ *
13
+ * @typedef {object} AddressFinding
14
+ * @property {string} key - The field, named as an author writes it.
15
+ * @property {string} written - The authored value, so the caller can locate the
16
+ * finding at the line carrying it rather than at the key.
17
+ * @property {string} message - One sentence, naming the fix.
18
+ */
19
+ /**
20
+ * The declarations that state an Address position.
21
+ *
22
+ * @param {readonly object[]} fields - A field declaration.
23
+ * @returns {object[]} Only the fields carrying an `address` statement.
24
+ */
25
+ export function addressFields(fields: readonly object[]): object[];
26
+ /**
27
+ * Reduce every Address a compiled block holds to the Shortcode it names.
28
+ *
29
+ * The block is edited in place, as {@link
30
+ * module:engine/system-block.mergeSystemData} edits it: what the caller holds is
31
+ * the document being assembled, and a field the reduction cannot read is left
32
+ * exactly as authored — the findings stop the build, so nothing ships either
33
+ * way, and an untouched value is what a reader of the failing note sees.
34
+ *
35
+ * @param {object} emitted - The `system` block, already built.
36
+ * @param {readonly object[]} fields - The type's field declaration.
37
+ * @param {import("./address.mjs").AddressVocabulary & {package?: string}}
38
+ * vocabulary - The tree's vocabularies and the package the values are written
39
+ * in.
40
+ * @returns {AddressFinding[]} Every value that could not be reduced.
41
+ */
42
+ export function reduceAddressFields(emitted: object, fields: readonly object[], vocabulary?: import("./address.mjs").AddressVocabulary & {
43
+ package?: string;
44
+ }): AddressFinding[];
45
+ /**
46
+ * How many values a position holds.
47
+ *
48
+ * - `value` — the field's value is one Address.
49
+ * - `items` — every element of a list is one.
50
+ * - `keys` — every **key** of a map is one, and the values are untouched.
51
+ *
52
+ * @type {readonly string[]}
53
+ */
54
+ export const ADDRESS_FIELD_SHAPES: readonly string[];
55
+ /**
56
+ * An Address position, as a field declaration states it.
57
+ */
58
+ export type AddressPosition = {
59
+ /**
60
+ * - The type an omitted `<type>` segment takes.
61
+ */
62
+ type: string;
63
+ /**
64
+ * - Every type the position accepts.
65
+ */
66
+ accepts: readonly string[];
67
+ /**
68
+ * - Where the Addresses are.
69
+ * Defaults to `value`.
70
+ */
71
+ holds?: "keys" | "value" | "items" | undefined;
72
+ };
73
+ /**
74
+ * What a reduction could not do, for a caller to report.
75
+ */
76
+ export type AddressFinding = {
77
+ /**
78
+ * - The field, named as an author writes it.
79
+ */
80
+ key: string;
81
+ /**
82
+ * - The authored value, so the caller can locate the
83
+ * finding at the line carrying it rather than at the key.
84
+ */
85
+ written: string;
86
+ /**
87
+ * - One sentence, naming the fix.
88
+ */
89
+ message: string;
90
+ };