@heroiclands/package-build 17.2.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +1711 -0
  2. package/CONTENT.md +288 -72
  3. package/README.md +1 -1
  4. package/bin/content-build.mjs +218 -137
  5. package/bin/package-build.mjs +43 -0
  6. package/content-config.mjs +62 -67
  7. package/docs/content-format.md +768 -58
  8. package/engine/actor-compiler.mjs +586 -0
  9. package/engine/address-charset.mjs +11 -8
  10. package/engine/address-diff.mjs +266 -22
  11. package/engine/anchored-sections.mjs +83 -0
  12. package/engine/anchors.mjs +83 -0
  13. package/engine/base-compiler.mjs +148 -18
  14. package/engine/bundle-notes.mjs +276 -0
  15. package/engine/bundles.mjs +307 -0
  16. package/engine/code-fences.mjs +103 -0
  17. package/engine/compile-corpus.mjs +89 -0
  18. package/engine/content-address.mjs +267 -5
  19. package/engine/content-format-check.mjs +54 -11
  20. package/engine/content-format.mjs +37 -5
  21. package/engine/content-index.mjs +225 -127
  22. package/engine/content-links.mjs +185 -77
  23. package/engine/content-lint.mjs +232 -14
  24. package/engine/content-package.mjs +2 -1
  25. package/engine/content-tables.mjs +99 -3
  26. package/engine/document-subtypes.mjs +49 -6
  27. package/engine/field-reference.mjs +1 -1
  28. package/engine/field-spec.mjs +56 -6
  29. package/engine/folder-notes.mjs +470 -0
  30. package/engine/foreign-catalog.mjs +195 -4
  31. package/engine/{manifest-emit.mjs → foundry-entries.mjs} +55 -83
  32. package/engine/frontmatter-lint.mjs +245 -19
  33. package/engine/frontmatter.mjs +88 -8
  34. package/engine/generate.mjs +304 -13
  35. package/engine/helpers.mjs +293 -64
  36. package/engine/ids.mjs +137 -2
  37. package/engine/index-records.mjs +126 -0
  38. package/engine/index.mjs +23 -5
  39. package/engine/item-compiler.mjs +349 -0
  40. package/engine/item-docs.mjs +7 -1
  41. package/engine/item-registry.mjs +6 -0
  42. package/engine/journals.mjs +93 -27
  43. package/engine/macros.mjs +4 -2
  44. package/engine/metadata-index.mjs +495 -0
  45. package/engine/note-claims.mjs +146 -25
  46. package/engine/note-ids.mjs +112 -0
  47. package/engine/note-renames.mjs +134 -0
  48. package/engine/note-schemas.mjs +25 -0
  49. package/engine/note-vocabulary.mjs +110 -27
  50. package/engine/pack-config.mjs +4 -0
  51. package/engine/pack-router.mjs +23 -0
  52. package/engine/retired-fields.mjs +137 -3
  53. package/engine/scenes.mjs +66 -34
  54. package/engine/schema-check.mjs +58 -17
  55. package/engine/site-build.mjs +71 -12
  56. package/engine/site-index.mjs +20 -7
  57. package/engine/sql-tables.mjs +485 -0
  58. package/engine/subtype-registry.mjs +102 -0
  59. package/engine/system-block.mjs +96 -12
  60. package/engine/systems.mjs +201 -0
  61. package/engine/web-wikilinks.mjs +50 -20
  62. package/engine/wikilink-syntax.mjs +17 -8
  63. package/engine/wikilinks.mjs +124 -51
  64. package/engine/yaml-lint.mjs +282 -0
  65. package/hm3/actors.mjs +352 -0
  66. package/hm3/default-item-art.mjs +75 -0
  67. package/hm3/document-subtypes.mjs +134 -0
  68. package/hm3/index.mjs +56 -0
  69. package/hm3/item-builders.mjs +84 -0
  70. package/hm3/item-fields.mjs +180 -0
  71. package/hm3/items.mjs +96 -0
  72. package/hm3/template-priority.mjs +85 -0
  73. package/manifest.mjs +43 -2
  74. package/package.json +18 -4
  75. package/release.mjs +62 -7
  76. package/sohl/actors.mjs +33 -487
  77. package/sohl/being-info.mjs +16 -7
  78. package/sohl/default-item-art.mjs +14 -3
  79. package/sohl/document-subtypes.mjs +16 -10
  80. package/sohl/item-builders.mjs +14 -5
  81. package/sohl/item-fields.mjs +68 -7
  82. package/sohl/items.mjs +44 -258
  83. package/sohl/note-schemas.mjs +2 -2
  84. package/types/content-config.d.mts +21 -42
  85. package/types/engine/actor-compiler.d.mts +204 -0
  86. package/types/engine/address-charset.d.mts +11 -8
  87. package/types/engine/address-diff.d.mts +53 -5
  88. package/types/engine/anchored-sections.d.mts +21 -0
  89. package/types/engine/anchors.d.mts +20 -0
  90. package/types/engine/base-compiler.d.mts +17 -17
  91. package/types/engine/bundle-notes.d.mts +173 -0
  92. package/types/engine/bundles.d.mts +60 -0
  93. package/types/engine/code-fences.d.mts +43 -0
  94. package/types/engine/compile-corpus.d.mts +32 -0
  95. package/types/engine/content-address.d.mts +205 -5
  96. package/types/engine/content-format-check.d.mts +6 -2
  97. package/types/engine/content-format.d.mts +57 -1
  98. package/types/engine/content-index.d.mts +54 -49
  99. package/types/engine/content-links.d.mts +52 -7
  100. package/types/engine/content-lint.d.mts +10 -1
  101. package/types/engine/content-package.d.mts +2 -1
  102. package/types/engine/content-tables.d.mts +21 -39
  103. package/types/engine/document-subtypes.d.mts +37 -3
  104. package/types/engine/field-spec.d.mts +76 -5
  105. package/types/engine/folder-notes.d.mts +159 -0
  106. package/types/engine/foreign-catalog.d.mts +53 -0
  107. package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +2 -43
  108. package/types/engine/frontmatter-lint.d.mts +10 -2
  109. package/types/engine/frontmatter.d.mts +64 -0
  110. package/types/engine/generate.d.mts +38 -0
  111. package/types/engine/helpers.d.mts +94 -30
  112. package/types/engine/ids.d.mts +96 -0
  113. package/types/engine/index-records.d.mts +68 -0
  114. package/types/engine/index.d.mts +9 -3
  115. package/types/engine/item-compiler.d.mts +131 -0
  116. package/types/engine/journals.d.mts +47 -9
  117. package/types/engine/metadata-index.d.mts +226 -0
  118. package/types/engine/note-claims.d.mts +57 -11
  119. package/types/engine/note-ids.d.mts +38 -0
  120. package/types/engine/note-renames.d.mts +102 -0
  121. package/types/engine/note-vocabulary.d.mts +44 -8
  122. package/types/engine/retired-fields.d.mts +75 -0
  123. package/types/engine/scenes.d.mts +3 -2
  124. package/types/engine/schema-check.d.mts +25 -4
  125. package/types/engine/site-build.d.mts +4 -4
  126. package/types/engine/site-index.d.mts +1 -1
  127. package/types/engine/sql-tables.d.mts +185 -0
  128. package/types/engine/subtype-registry.d.mts +49 -0
  129. package/types/engine/system-block.d.mts +40 -1
  130. package/types/engine/systems.d.mts +106 -0
  131. package/types/engine/web-wikilinks.d.mts +4 -2
  132. package/types/engine/wikilink-syntax.d.mts +10 -3
  133. package/types/engine/wikilinks.d.mts +41 -13
  134. package/types/engine/yaml-lint.d.mts +107 -0
  135. package/types/hm3/actors.d.mts +48 -0
  136. package/types/hm3/default-item-art.d.mts +42 -0
  137. package/types/hm3/document-subtypes.d.mts +24 -0
  138. package/types/hm3/index.d.mts +7 -0
  139. package/types/hm3/item-builders.d.mts +11 -0
  140. package/types/hm3/item-fields.d.mts +12 -0
  141. package/types/hm3/items.d.mts +23 -0
  142. package/types/hm3/template-priority.d.mts +21 -0
  143. package/types/manifest.d.mts +22 -1
  144. package/types/release.d.mts +9 -4
  145. package/types/sohl/actors.d.mts +7 -74
  146. package/types/sohl/being-info.d.mts +8 -27
  147. package/types/sohl/default-item-art.d.mts +5 -3
  148. package/types/sohl/items.d.mts +17 -32
  149. package/engine/foreign-manifests.mjs +0 -126
  150. package/engine/kb-manifest.mjs +0 -490
  151. package/types/engine/foreign-manifests.d.mts +0 -43
  152. package/types/engine/kb-manifest.d.mts +0 -241
@@ -40,6 +40,15 @@
40
40
  *
41
41
  * Paths are fully resolved (`systems/sohl/...`) — the served path both layers
42
42
  * need. If a new item type is added, add its default here.
43
+ *
44
+ * **The keys are SoHL *document* subtypes, not markdown note types.** The
45
+ * runtime reads this with `itemData.type`, which is a Foundry Item subtype, and
46
+ * that is the vocabulary a compendium document carries. The two vocabularies
47
+ * were the same string until #78 renamed three note types (`armorgear` →
48
+ * `armor`, and its two siblings), and this map stayed on the document side
49
+ * because that is the side the runtime cannot translate: `sohl/item-builders.mjs`
50
+ * asks SoHL's own note-type → subtype map before it looks art up here, so the
51
+ * build translates once and no second copy of those three rows exists.
43
52
  */
44
53
  export const DEFAULT_ITEM_ART = {
45
54
  affiliation: "systems/sohl/assets/icons/noun/shield.svg",
@@ -58,7 +67,8 @@ export const DEFAULT_ITEM_ART = {
58
67
  };
59
68
 
60
69
  /**
61
- * The default art path for an item `type`, or throw when the type is unknown —
70
+ * The default art path for an item **document subtype**, or throw when it is
71
+ * unknown —
62
72
  * the build's fail-fast contract, so an unrecognized type is never silently
63
73
  * defaulted (aborting the pack build rather than shipping a mismatched icon).
64
74
  *
@@ -66,8 +76,9 @@ export const DEFAULT_ITEM_ART = {
66
76
  * {@link DEFAULT_ITEM_ART} directly and fall back to Foundry's default instead
67
77
  * of calling this — see `SohlItem.getDefaultArtwork`.
68
78
  *
69
- * @param {string} type - the item type.
70
- * @returns {string} the default image path for that type.
79
+ * @param {string} type - The SoHL Item document subtype — `armorgear`, not the
80
+ * `armor` note type that compiles into one.
81
+ * @returns {string} the default image path for that subtype.
71
82
  */
72
83
  export function defaultItemArt(type) {
73
84
  if (!(type in DEFAULT_ITEM_ART)) {
@@ -28,13 +28,19 @@
28
28
  * silently. Sixteen lines of data are the price of the two vocabularies being
29
29
  * separately stated.
30
30
  *
31
- * **Today every row is the identity, and that is a fact about SoHL rather than
32
- * a rule.** The renames the format calls for (`armorgear` → `armor`, and its
33
- * three siblings) are #78, deliberately deferred: they cost 30,741 embedded
34
- * `(type, shortcode)` references across four content repositories, and until
35
- * they land the compiled packs must not move by a byte. When one does land it
36
- * changes one row here and the notes that address it a data change, not a
37
- * mechanism change, which is the whole point of having the map first.
31
+ * **Three rows are no longer the identity, and that is what the map was for.**
32
+ * `armor`, `concoction` and `projectile` were renamed from the `…gear`
33
+ * spellings that named SoHL's *document* subtype rather than the thing the note
34
+ * is about (#78); the subtypes themselves did not move, so those three rows now
35
+ * say two different words where they used to say one twice. Landing the rename
36
+ * cost exactly what having the map first promised it would: a data change on
37
+ * three lines here, and no mechanism change anywhere.
38
+ *
39
+ * The old spellings are still read — {@link RENAMED_TYPES} normalises them
40
+ * before this map is consulted — so a tree that has not swept its 30,741
41
+ * embedded `(type, shortcode)` references compiles byte-identically. `weapon`
42
+ * is deliberately **not** among them: both systems call that document a
43
+ * `weapongear`, so the name says nothing SoHL-specific and #78 left it alone.
38
44
  *
39
45
  * **What this map is not.** It says which document a note becomes, never what
40
46
  * that document contains: the `system` block comes from the item registry's
@@ -65,14 +71,14 @@ export const SOHL_DOCUMENT_SUBTYPES = defineDocumentSubtypes({
65
71
  types: {
66
72
  affiliation: { document: "Item", subType: "affiliation" },
67
73
  affliction: { document: "Item", subType: "affliction" },
68
- armorgear: { document: "Item", subType: "armorgear" },
74
+ armor: { document: "Item", subType: "armorgear" },
69
75
  attribute: { document: "Item", subType: "attribute" },
70
- concoctiongear: { document: "Item", subType: "concoctiongear" },
76
+ concoction: { document: "Item", subType: "concoctiongear" },
71
77
  containergear: { document: "Item", subType: "containergear" },
72
78
  miscgear: { document: "Item", subType: "miscgear" },
73
79
  mystery: { document: "Item", subType: "mystery" },
74
80
  mysticalability: { document: "Item", subType: "mysticalability" },
75
- projectilegear: { document: "Item", subType: "projectilegear" },
81
+ projectile: { document: "Item", subType: "projectilegear" },
76
82
  skill: { document: "Item", subType: "skill" },
77
83
  trauma: { document: "Item", subType: "trauma" },
78
84
  weapongear: { document: "Item", subType: "weapongear" },
@@ -47,6 +47,8 @@
47
47
  */
48
48
 
49
49
  import { defaultItemArt } from "./default-item-art.mjs";
50
+ import { documentSubtype } from "../engine/document-subtypes.mjs";
51
+ import { SOHL_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
50
52
  import { buildFromFields, readField } from "../engine/field-spec.mjs";
51
53
  import { COMBAT_TECHNIQUE_STRIKE_MODE, ITEM_FIELDS } from "./item-fields.mjs";
52
54
 
@@ -82,11 +84,18 @@ const FINALIZERS = Object.freeze({
82
84
  * throws and this module evaluates at import. A drift that a test used to catch
83
85
  * is unrepresentable.
84
86
  *
85
- * Importing the art map keeps this module a leaf it is plain data, not the
86
- * resolved configuration, and the cycle the module note above warns about is
87
- * only ever closed by reading configuration back out.
87
+ * **The art map is keyed by the document subtype**, which the runtime reads
88
+ * with a Foundry `Item`'s own `type`, so this asks SoHL's map what an `armor`
89
+ * note becomes before looking art up (#78). Translating on the build side is
90
+ * the only place it can happen: the runtime has no note in hand. Every SoHL
91
+ * Item row is one-to-one, so `documentSubtype` needs no frontmatter and cannot
92
+ * throw for a discriminator.
88
93
  *
89
- * @param {string} type - The item type, and the art map's key.
94
+ * Importing the art map and SoHL's own subtype map keeps this module a leaf —
95
+ * both are plain data, not the resolved configuration, and the cycle the module
96
+ * note above warns about is only ever closed by reading configuration back out.
97
+ *
98
+ * @param {string} type - The note type, and {@link ITEM_FIELDS}'s key.
90
99
  * @returns {Readonly<{system: (fm: object) => object, img: string, fields: readonly object[]}>}
91
100
  * The registry entry.
92
101
  */
@@ -96,7 +105,7 @@ function entryFor(type) {
96
105
  const finalize = FINALIZERS[type];
97
106
  return Object.freeze({
98
107
  system: finalize ? (fm) => finalize(fm, build(fm)) : build,
99
- img: defaultItemArt(type),
108
+ img: defaultItemArt(documentSubtype(SOHL_DOCUMENT_SUBTYPES, type, {}) ?? type),
100
109
  fields,
101
110
  });
102
111
  }
@@ -51,7 +51,7 @@ import {
51
51
  resolveCharges,
52
52
  resolveRelation,
53
53
  resolveSkillAptitudes,
54
- sohlField,
54
+ sohlSystemField,
55
55
  } from "../engine/frontmatter.mjs";
56
56
 
57
57
  /* --------------------------------------------------------------------- */
@@ -115,6 +115,27 @@ const RELATION = Object.freeze({
115
115
  read: (_raw, { fm }) => resolveRelation(fm, noteContext(fm, "affiliation")),
116
116
  });
117
117
 
118
+ /**
119
+ * A list of references, each a bare shortcode.
120
+ *
121
+ * Blank entries are dropped rather than emitted: a list that has been edited
122
+ * down in a property editor keeps its empty rows, and an empty string is not a
123
+ * reference to anything. Absent reads as `[]`, because "refers to nothing" is a
124
+ * value here — a sovereign polity is subordinate to nobody — rather than an
125
+ * unset one.
126
+ */
127
+ const SHORTCODE_LIST = Object.freeze({
128
+ shape: "list of shortcodes",
129
+ kind: "list",
130
+ read: (raw) =>
131
+ (Array.isArray(raw) ? raw
132
+ : raw == null || raw === "" ? []
133
+ : [raw]
134
+ )
135
+ .map((entry) => String(entry ?? "").trim())
136
+ .filter((entry) => entry !== ""),
137
+ });
138
+
118
139
  /**
119
140
  * Every element must carry a non-blank `shortcode`, unique on the weapon — the
120
141
  * shortcode is the mode's identity. The list is otherwise emitted verbatim.
@@ -141,11 +162,14 @@ const STRIKE_MODES = Object.freeze({
141
162
  * A projectile's impact die, read wherever the default of another impact field
142
163
  * depends on it.
143
164
  *
165
+ * Authored `impact.die`, stored `impactBase.die` — the one re-reading field
166
+ * whose two positions are spelled differently, so both are named (#126).
167
+ *
144
168
  * @param {object} fm - The note's frontmatter.
145
169
  * @returns {number} The die size, `0` when the projectile declares none.
146
170
  */
147
171
  function impactDie(fm) {
148
- return Number(sohlField(fm, "impact.die", 0)) || 0;
172
+ return Number(sohlSystemField(fm, "impactBase.die", 0, { legacyKey: "impact.die" })) || 0;
149
173
  }
150
174
 
151
175
  /** A strike mode discriminated by `type`, mandatory on a combat technique. */
@@ -281,12 +305,49 @@ export const ITEM_FIELDS = Object.freeze({
281
305
  describe: "Standing within the society.",
282
306
  },
283
307
  {
284
- name: "relation",
285
- to: "relation",
308
+ // Plural because the field holds a map of many standings, one per
309
+ // affiliation — the singular was a misnomer every author read past
310
+ // (SoHL#1781).
311
+ name: "relations",
312
+ to: "relations",
286
313
  ...RELATION,
287
314
  default: {},
288
315
  describe: "How this society regards others: aligned, unaligned, rival or nemesis.",
289
316
  },
317
+ {
318
+ // The organisational relation: which bodies this one answers to.
319
+ // A list, because an affiliation may sit under more than one at
320
+ // once — an arcane tradition within an order, say.
321
+ name: "parents",
322
+ to: "parents",
323
+ ...SHORTCODE_LIST,
324
+ default: [],
325
+ describe: "Affiliations this one is subordinate to, by shortcode.",
326
+ },
327
+ {
328
+ // Not `capital` or `headquarters`: each fits about half the eleven
329
+ // subTypes, while a seat covers a polity, a guild, an order and a
330
+ // faith alike.
331
+ name: "seat",
332
+ to: "seat",
333
+ ...BLANK_IS_NULL,
334
+ default: null,
335
+ describe: "Where the affiliation's authority sits, by place shortcode.",
336
+ },
337
+ {
338
+ // The geographic relation, kept apart from the organisational one
339
+ // above: `parents` is *subordinate to*, this is *holds sway over*.
340
+ //
341
+ // Authored plural and emitted singular, as the content format's
342
+ // mapping row states (`data.domains` → `system.domain`). The two
343
+ // spellings are deliberate rather than a slip, so the declaration
344
+ // carries both rather than either side guessing.
345
+ name: "domains",
346
+ to: "domain",
347
+ ...SHORTCODE_LIST,
348
+ default: [],
349
+ describe: "Places this affiliation holds sway over, by shortcode.",
350
+ },
290
351
  ]),
291
352
 
292
353
  affliction: Object.freeze([
@@ -355,7 +416,7 @@ export const ITEM_FIELDS = Object.freeze({
355
416
  },
356
417
  ]),
357
418
 
358
- armorgear: Object.freeze([
419
+ armor: Object.freeze([
359
420
  ...GEAR_COMMON,
360
421
  {
361
422
  name: "material",
@@ -470,7 +531,7 @@ export const ITEM_FIELDS = Object.freeze({
470
531
  },
471
532
  ]),
472
533
 
473
- concoctiongear: Object.freeze([
534
+ concoction: Object.freeze([
474
535
  ...GEAR_COMMON,
475
536
  {
476
537
  name: "subType",
@@ -613,7 +674,7 @@ export const ITEM_FIELDS = Object.freeze({
613
674
  },
614
675
  ]),
615
676
 
616
- projectilegear: Object.freeze([
677
+ projectile: Object.freeze([
617
678
  ...GEAR_COMMON,
618
679
  {
619
680
  name: "subType",
package/sohl/items.mjs CHANGED
@@ -12,282 +12,68 @@
12
12
  */
13
13
 
14
14
  /**
15
- * Items pack compilerproduces JSON pack files for the single "items"
16
- * Foundry compendium from markdown notes in the `assets/content/` tree.
15
+ * **SoHL's Item pass**the two things about compiling a note into a SoHL Item
16
+ * that are facts about SoHL rather than about the note format.
17
17
  *
18
- * The content root (`contentBase`) is walked recursively; any `.md` file whose
19
- * frontmatter declares a recognized `type:` is compiled into one JSON entry.
20
- * Files outside the whitelist (blog posts, rules text, templates) are
21
- * silently skipped.
18
+ * Everything else lives in {@link module:engine/item-compiler}: claiming a
19
+ * note, looking its subtype up in the system's map, dispatching to the
20
+ * consumer's registry, merging the authored `sohl.system` block, checking what
21
+ * was emitted against the receiving schema, and writing the envelope. That was
22
+ * all here until a second system needed it (#139), and it reached its
23
+ * system-specific facts through one constant read off SoHL's own map — which is
24
+ * why lifting it cost a subclass rather than a rewrite.
22
25
  *
23
- * Type-specific `system.*` fields come from the nested `sohl:` block in
24
- * vault frontmatter, read via `sohlField()`. The body is **not** rendered
25
- * here: it compiles into the item's doc in the journals pack, and
26
- * `system.docHtml` becomes a pointer to it (see `item-docs.mjs`). Folder
27
- * assignment is deferred every item currently emits `folder: null`.
26
+ * What stays:
27
+ *
28
+ * - **The map**, declared in `document-subtypes.mjs` and named here, which
29
+ * decides which notes this pass claims and what each one becomes (#79).
30
+ * - **`commonSystem`**`shortcode`, `templatePriority`, `actionDefs`, `notes` and
31
+ * `docHtml`, which SoHL's compiler writes on every item of every type and no
32
+ * field declaration states.
28
33
  *
29
34
  * Not a standalone script — exports the `Items` compiler class, imported and
30
- * driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`).
35
+ * driven by `engine/generate.mjs`.
31
36
  *
32
- * The walk itself — filtering by type, expanding tables, converting
33
- * wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
34
- * states only what makes this pass its own (#1509).
37
+ * @module
35
38
  */
36
39
 
37
- import log from "loglevel";
38
-
39
- import {
40
- sohlField,
41
- resolveName,
42
- resolveImg,
43
- defaultStats,
44
- systemArchetype,
45
- } from "../engine/helpers.mjs";
46
- import { BasePackCompiler } from "../engine/base-compiler.mjs";
47
- // Per-type default art lives in one framework-free module shared with the
48
- // runtime (`SohlItem.getDefaultArtwork`), so the two can't drift — see #932.
49
- // It lives in the build package, not `src/`: a relative path out of the package
50
- // resolves to garbage once this pipeline runs from `node_modules` (#1510).
51
- import { journalPageId, splitPages } from "../engine/journals.mjs";
52
- import { foundryPackageId } from "../engine/content-package.mjs";
53
- import { itemDocEntryId, itemDocPointer } from "../engine/item-docs.mjs";
54
- // The whitelist and the per-type `system` builders both come from the resolved
55
- // configuration, so the types this pass claims and the builders it compiles
56
- // them with are one table — the consuming repository's, not this package's
57
- // (#1504/#1563).
58
- import { itemTypes, itemBuilder, itemArt } from "../engine/item-registry.mjs";
59
- // Which Foundry Item subtype a note's `type` compiles into. Looked up in the
60
- // system's declared map, never inferred from the type itself (#79).
61
- import { documentSubtype, subtypeRow } from "../engine/document-subtypes.mjs";
40
+ import { systemTemplatePriority } from "../engine/helpers.mjs";
41
+ import { SystemItemCompiler } from "../engine/item-compiler.mjs";
62
42
  import { SOHL_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
63
- // The note-level `sohl:` block: `sohl.system` onto the document's `system`
64
- // verbatim, and `sohl.img` / `sohl.effects` / `sohl.flags` overriding their
65
- // shared top-level forms for this system alone (#58).
66
- import { blockProperty, claimedPaths, mergeSystemData } from "../engine/system-block.mjs";
67
- import { itemFields } from "../engine/item-registry.mjs";
68
-
69
- /**
70
- * The description an item carries: a pointer to its **item doc**, the
71
- * JournalEntry the journals pass compiles this same body into (#1348).
72
- *
73
- * The prose is not rendered here at all. Carrying it would duplicate it onto
74
- * every actor holding the item — 7.59 MB of copies across the actors pack, of
75
- * which 133 KB was distinct — where a link is 60 bytes and always current. The
76
- * two passes derive the target from the note's own id, so neither has to see
77
- * the other's output; both split the *converted* markdown, so an H1 carrying a
78
- * wikilink names the same page on both sides.
79
- *
80
- * An item with no prose points at nothing, exactly as the journals pass writes
81
- * no entry for it.
82
- *
83
- * @param {string} markdown - The note body, tables expanded and wikilinks
84
- * resolved.
85
- * @param {object} fm - The note's frontmatter.
86
- * @param {string} name - The item's name.
87
- * @returns {string} The pointer, or "" for a note with no body.
88
- */
89
- function itemDescription(markdown, fm, name) {
90
- if (!String(markdown).trim()) return "";
91
- const [leadPage] = splitPages(markdown, name);
92
- const pageId = journalPageId(itemDocEntryId(fm.id), leadPage, 0);
93
- return itemDocPointer(foundryPackageId(), fm.id, name, pageId);
94
- }
95
-
96
- /**
97
- * Build the `system.*` fields shared by every item type:
98
- * shortcode, archetype, actionDefs, notes, docHtml.
99
- *
100
- * @param {object} fm - The note's frontmatter.
101
- * @param {string} description - The item's documentation pointer.
102
- * @param {string} label - Human-readable context for error messages.
103
- * @returns {object} The shared `system` fields.
104
- */
105
- function commonSystem(fm, description, label) {
106
- return {
107
- shortcode: fm.shortcode,
108
- // Required nullable number: a priority, or `null` for a document that
109
- // is not an archetype (#126 / archetype contract #604).
110
- archetype: systemArchetype(fm, label),
111
- actionDefs: Array.isArray(fm.actionDefs) ? fm.actionDefs : [],
112
- notes: "",
113
- docHtml: description || "",
114
- };
115
- }
116
-
117
- /* -------------------------------------------------------------------- */
118
- /* Synthesized Active Effects */
119
- /* -------------------------------------------------------------------- */
120
-
121
- /* -------------------------------------------------------------------- */
122
- /* Compiler */
123
- /* -------------------------------------------------------------------- */
124
-
125
- /**
126
- * The system this pass compiles for — the block its notes write, and the
127
- * registry its builders come from.
128
- *
129
- * Read from the map rather than spelled here, so the block name, the subtype
130
- * map and the registry key are one statement (#58/#79).
131
- *
132
- * @type {string}
133
- */
134
- const SYSTEM = SOHL_DOCUMENT_SUBTYPES.block;
135
-
136
- export class Items extends BasePackCompiler {
137
- static id = "items";
138
- static label = "item";
139
-
140
- /**
141
- * An Item **is** a system's data, so this pack takes only notes carrying
142
- * this system's block (#58).
143
- */
144
- static requiresSystemBlock = true;
145
43
 
44
+ export class Items extends SystemItemCompiler {
146
45
  /**
147
- * How many of each item type this pass wrote, for the summary. Every type
148
- * is present from the start so the tally reads as a census of the
149
- * whitelist rather than of what happened to compile.
46
+ * SoHL's note-type document-subtype map the one declaration that says
47
+ * which block this pass reads, which notes it claims, and what each becomes
48
+ * (#58/#79).
150
49
  *
151
- * @type {Record<string, number>}
50
+ * @type {import("../engine/document-subtypes.mjs").DocumentSubtypeMap}
152
51
  */
153
- counts = Object.fromEntries([...itemTypes()].map((t) => [t, 0]));
52
+ static documentSubtypes = SOHL_DOCUMENT_SUBTYPES;
154
53
 
155
54
  /**
156
- * Every content type that compiles into an item.
157
- *
158
- * The whitelist is the consuming repository's `itemBuilders` keys (#1504),
159
- * and the system's own map is a second filter on top of it: a type SoHL
160
- * maps onto some *other* document class is not an item however a registry
161
- * spells it, which is the "no wrongly-typed document" half of #79. A type
162
- * the map does not name at all is left to the registry — see
163
- * {@link Items#itemSubtype}.
55
+ * The `system.*` fields SoHL writes on every item, whatever its type:
56
+ * shortcode, templatePriority, actionDefs, notes, docHtml.
164
57
  *
165
58
  * @param {object} fm - The note's frontmatter.
166
- * @returns {boolean} True for a whitelisted item type.
59
+ * @param {object} at - What the pass already knows about this note.
60
+ * @param {string} at.description - The pointer to the note's item doc.
61
+ * @param {string} at.label - Human-readable context for error messages.
62
+ * @returns {object} The shared `system` fields.
167
63
  */
168
- selects(fm) {
169
- if (!fm.type || !itemTypes().has(fm.type)) return false;
170
- const row = subtypeRow(SOHL_DOCUMENT_SUBTYPES, fm.type);
171
- return !row || row.document === "Item";
172
- }
173
-
174
- /**
175
- * The Foundry Item subtype a note compiles into.
176
- *
177
- * **Looked up, not inferred.** For every type this system declares, the
178
- * emitted subtype is the map's, so the note vocabulary and the document
179
- * vocabulary are two separately-stated things rather than one string
180
- * written twice (#79).
181
- *
182
- * **A type the map does not name belongs to the consumer**, and its
183
- * registry entry is the declaration: a repository shipping an item type of
184
- * its own writes it once, in the `itemBuilders` table of its
185
- * `package-build.config.yaml`, and that key is what the document is a
186
- * subtype of. That is an authored statement in the consumer's own
187
- * configuration, not a coincidence inside this package's source — and
188
- * refusing it here would silently drop every document of a type SoHL has
189
- * no opinion about (#7/#1563).
190
- *
191
- * @param {object} fm - The note's frontmatter.
192
- * @returns {string} The document's `type`.
193
- */
194
- itemSubtype(fm) {
195
- const declared = documentSubtype(SOHL_DOCUMENT_SUBTYPES, fm.type, fm, {
196
- absPath: this.currentNote?.absPath,
197
- });
198
- return declared ?? fm.type;
199
- }
200
-
201
- /** An item is named by its own type in the log, not by "item". */
202
- noteLabel(fm) {
203
- return fm.type;
204
- }
205
-
206
- /**
207
- * Construct the full compendium envelope for one item, including
208
- * synthesized Active Effects where applicable.
209
- *
210
- * @param {object} fm - The note's frontmatter.
211
- * @param {string} markdown - The body, tables expanded and wikilinks
212
- * resolved.
213
- * @returns {object} The item document, keyed for the pack.
214
- */
215
- buildEntry(fm, markdown) {
216
- const type = fm.type;
217
- const name = resolveName(fm);
218
- const description = itemDescription(markdown, fm, name);
219
- const id = fm.id;
220
- const subType = this.itemSubtype(fm);
221
- const system = {
222
- ...commonSystem(fm, description, `item "${name}"`),
223
- ...itemBuilder(type, SYSTEM)(fm),
224
- };
225
- // Whatever the note authors under `sohl.system`, at the DataModel's own
226
- // paths. A path a declared field already writes is left to that field:
227
- // its value came from the same authored place and went through the
228
- // field's own coercion (#58).
229
- mergeSystemData(system, fm, {
230
- block: SYSTEM,
231
- claimed: claimedPaths(itemFields(type, SYSTEM)),
232
- });
233
- this.reportUndeclaredSystemData(fm, SYSTEM, "Item", subType);
234
- // And what *this* pass wrote on its own initiative — `shortcode`,
235
- // `archetype`, `actionDefs`, `notes`, `docHtml` — which no field
236
- // declaration states and so no other check can see (#155). Read off the
237
- // assembled block, so a key added to `commonSystem` is checked without
238
- // anyone remembering to list it.
239
- this.reportEmittedSystemData(system, {
240
- fm,
241
- block: SYSTEM,
242
- documentType: "Item",
243
- subType,
244
- type,
245
- fields: itemFields(type, SYSTEM),
246
- });
247
-
248
- const effects = blockProperty(fm, SYSTEM, "effects");
249
- const folderId = sohlField(fm, "folder", null);
250
- const folder = this.folderResolver(folderId);
251
-
64
+ commonSystem(fm, { description, label }) {
252
65
  return {
253
- name,
254
- // The note's `type` addresses the builder and the default art —
255
- // both registries are keyed by content type while the document's
256
- // own subtype comes from the system's map (#79).
257
- type: subType,
258
- // Nullish, not `||` (#218): `resolveImg` returns `null` for a note
259
- // that names no art and `""` for one that wants none, and only the
260
- // first may be replaced by the type's default.
261
- img: resolveImg(blockProperty(fm, SYSTEM, "img")) ?? itemArt(type, SYSTEM),
262
- _id: id,
263
- system,
264
- effects: Array.isArray(effects) ? [...effects] : [],
265
- // Whatever the note authors, and nothing else. `archetype` used to
266
- // be spliced in here as `flags.sohl.docArchetype`; it is a schema
267
- // field now and sits in `system` (#126).
268
- flags: blockProperty(fm, SYSTEM, "flags", {}),
269
- _stats: this.stats,
270
- ownership: { default: 0 },
271
- folder,
272
- _key: `!items!${id}`,
66
+ shortcode: fm.shortcode,
67
+ // Required nullable number: a priority, or `null` for a document
68
+ // that is not a template (#126 / archetype contract #604). The
69
+ // field was `system.archetype` until #266 / sohl#1836; the receiving
70
+ // schema declares only the new name, so the emitted key moves with
71
+ // it an undeclared `system` key is discarded at construction
72
+ // without a warning.
73
+ templatePriority: systemTemplatePriority(fm, label),
74
+ actionDefs: Array.isArray(fm.actionDefs) ? fm.actionDefs : [],
75
+ notes: "",
76
+ docHtml: description || "",
273
77
  };
274
78
  }
275
-
276
- /** @inheritdoc */
277
- onCompiled(fm) {
278
- this.counts[fm.type]++;
279
- }
280
-
281
- /** @inheritdoc */
282
- reportCompiled(stats) {
283
- log.info(`Compiled ${stats.compiled} items:`);
284
- for (const [t, n] of Object.entries(this.counts)) {
285
- if (n > 0) log.info(` ${t}: ${n}`);
286
- }
287
- }
288
-
289
- /** @inheritdoc */
290
- reportDetail(stats) {
291
- log.debug(`Skipped ${stats.skippedOther} non-item file(s) (no recognized type)`);
292
- }
293
79
  }
@@ -244,7 +244,7 @@ const MAP_FIELDS = Object.freeze([
244
244
  * @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
245
245
  */
246
246
  const PRESENTATION_FIELDS = Object.freeze({
247
- armorgear: Object.freeze([
247
+ armor: Object.freeze([
248
248
  {
249
249
  name: "craft",
250
250
  ...MAP,
@@ -291,7 +291,7 @@ const PRESENTATION_FIELDS = Object.freeze({
291
291
  "The craft that makes it — `{skill, secondary}` — for knowledgebase grouping.",
292
292
  },
293
293
  ]),
294
- projectilegear: Object.freeze([
294
+ projectile: Object.freeze([
295
295
  {
296
296
  name: "craft",
297
297
  ...MAP,