@heroiclands/package-build 0.6.1 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG-content-build.md +965 -0
  2. package/CHANGELOG.md +68 -0
  3. package/CONTENT.md +824 -0
  4. package/MIGRATING.md +127 -0
  5. package/README.md +29 -17
  6. package/bin/content-build.mjs +1082 -0
  7. package/bin/package-build.mjs +2 -2
  8. package/bin/report.mjs +2 -2
  9. package/config.mjs +21 -18
  10. package/content-config.mjs +1424 -0
  11. package/coverage.mjs +1 -1
  12. package/e2e.mjs +1 -1
  13. package/engine/abbreviations.mjs +0 -0
  14. package/engine/base-compiler.mjs +608 -0
  15. package/engine/code-fences.mjs +311 -0
  16. package/engine/compendiums.mjs +353 -0
  17. package/engine/content-address.mjs +141 -0
  18. package/engine/content-links.mjs +438 -0
  19. package/engine/content-lint.mjs +204 -0
  20. package/engine/content-package.mjs +67 -0
  21. package/engine/content-slug.mjs +169 -0
  22. package/engine/content-tables.mjs +1216 -0
  23. package/engine/content-tree.mjs +56 -0
  24. package/engine/diagnostics.mjs +270 -0
  25. package/engine/field-reference.mjs +242 -0
  26. package/engine/field-spec.mjs +218 -0
  27. package/engine/foreign-catalog.mjs +450 -0
  28. package/engine/foreign-manifests.mjs +129 -0
  29. package/engine/frontmatter-lint.mjs +343 -0
  30. package/engine/frontmatter.mjs +258 -0
  31. package/engine/generate.mjs +305 -0
  32. package/engine/helpers.mjs +809 -0
  33. package/engine/ids.mjs +192 -0
  34. package/engine/index.mjs +142 -0
  35. package/engine/item-docs.mjs +153 -0
  36. package/engine/item-registry.mjs +129 -0
  37. package/engine/journals.mjs +374 -0
  38. package/engine/kb-manifest.mjs +473 -0
  39. package/engine/macros.mjs +333 -0
  40. package/engine/manifest-emit.mjs +339 -0
  41. package/engine/map-notes.mjs +1302 -0
  42. package/engine/pack-config.mjs +479 -0
  43. package/engine/pack-router.mjs +254 -0
  44. package/engine/prose-config.mjs +238 -0
  45. package/engine/prose-lint.mjs +294 -0
  46. package/engine/region-events.mjs +79 -0
  47. package/engine/scene-levels.mjs +420 -0
  48. package/engine/scenes.mjs +528 -0
  49. package/engine/site-build.mjs +778 -0
  50. package/engine/site-index.mjs +332 -0
  51. package/engine/web-wikilinks.mjs +330 -0
  52. package/engine/wikilink-syntax.mjs +105 -0
  53. package/engine/wikilinks.mjs +645 -0
  54. package/index.mjs +42 -25
  55. package/lang.mjs +1 -1
  56. package/manifest.mjs +79 -5
  57. package/markdownlint-config.mjs +45 -0
  58. package/package.json +78 -35
  59. package/prettier-config.mjs +34 -0
  60. package/sohl/actors.mjs +581 -0
  61. package/sohl/affiliation-standings.mjs +44 -0
  62. package/sohl/being-info.mjs +173 -0
  63. package/sohl/default-item-art.mjs +82 -0
  64. package/sohl/index.mjs +55 -0
  65. package/sohl/item-builders.mjs +114 -0
  66. package/sohl/item-fields.mjs +867 -0
  67. package/sohl/items.mjs +197 -0
  68. package/sohl/kb-passes.mjs +207 -0
  69. package/sohl/note-schemas.mjs +322 -0
  70. package/templates.mjs +1 -1
  71. package/types/config.d.mts +1 -1
  72. package/types/content-config.d.mts +571 -0
  73. package/types/engine/abbreviations.d.mts +44 -0
  74. package/types/engine/base-compiler.d.mts +320 -0
  75. package/types/engine/code-fences.d.mts +102 -0
  76. package/types/engine/compendiums.d.mts +77 -0
  77. package/types/engine/content-address.d.mts +61 -0
  78. package/types/engine/content-links.d.mts +87 -0
  79. package/types/engine/content-lint.d.mts +47 -0
  80. package/types/engine/content-package.d.mts +36 -0
  81. package/types/engine/content-slug.d.mts +59 -0
  82. package/types/engine/content-tables.d.mts +131 -0
  83. package/types/engine/content-tree.d.mts +12 -0
  84. package/types/engine/diagnostics.d.mts +152 -0
  85. package/types/engine/field-reference.d.mts +20 -0
  86. package/types/engine/field-spec.d.mts +181 -0
  87. package/types/engine/foreign-catalog.d.mts +103 -0
  88. package/types/engine/foreign-manifests.d.mts +43 -0
  89. package/types/engine/frontmatter-lint.d.mts +74 -0
  90. package/types/engine/frontmatter.d.mts +99 -0
  91. package/types/engine/generate.d.mts +56 -0
  92. package/types/engine/helpers.d.mts +348 -0
  93. package/types/engine/ids.d.mts +126 -0
  94. package/types/engine/index.d.mts +33 -0
  95. package/types/engine/item-docs.d.mts +73 -0
  96. package/types/engine/item-registry.d.mts +57 -0
  97. package/types/engine/journals.d.mts +121 -0
  98. package/types/engine/kb-manifest.d.mts +214 -0
  99. package/types/engine/macros.d.mts +201 -0
  100. package/types/engine/manifest-emit.d.mts +137 -0
  101. package/types/engine/map-notes.d.mts +283 -0
  102. package/types/engine/pack-config.d.mts +77 -0
  103. package/types/engine/pack-router.d.mts +53 -0
  104. package/types/engine/prose-config.d.mts +121 -0
  105. package/types/engine/prose-lint.d.mts +59 -0
  106. package/types/engine/region-events.d.mts +43 -0
  107. package/types/engine/scene-levels.d.mts +36 -0
  108. package/types/engine/scenes.d.mts +48 -0
  109. package/types/engine/site-build.d.mts +214 -0
  110. package/types/engine/site-index.d.mts +141 -0
  111. package/types/engine/web-wikilinks.d.mts +67 -0
  112. package/types/engine/wikilink-syntax.d.mts +101 -0
  113. package/types/engine/wikilinks.d.mts +172 -0
  114. package/types/index.d.mts +3 -0
  115. package/types/manifest.d.mts +52 -4
  116. package/types/markdownlint-config.d.mts +8 -0
  117. package/types/prettier-config.d.mts +1 -0
  118. package/types/sohl/actors.d.mts +65 -0
  119. package/types/sohl/affiliation-standings.d.mts +22 -0
  120. package/types/sohl/being-info.d.mts +78 -0
  121. package/types/sohl/default-item-art.d.mts +28 -0
  122. package/types/sohl/index.d.mts +7 -0
  123. package/types/sohl/item-builders.d.mts +11 -0
  124. package/types/sohl/item-fields.d.mts +20 -0
  125. package/types/sohl/items.d.mts +19 -0
  126. package/types/sohl/kb-passes.d.mts +71 -0
  127. package/types/sohl/note-schemas.d.mts +7 -0
@@ -0,0 +1,173 @@
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
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * **A being's info-block fields**, derived from the items it embeds.
16
+ *
17
+ * A `being` note carries its embedded documents as `sohl.items` — a flat list
18
+ * of `{ shortcode, type, system? }` — but the shared theme's sidebar reads
19
+ * *resolved* shapes: a `skills` map, `gear` grouped by kind, and `spells` /
20
+ * `talents` split out of the mystical abilities. This is the translation
21
+ * between the two, and it is SoHL data-model knowledge: which item type is a
22
+ * skill, where a mastery level lives, what distinguishes a spell from a talent.
23
+ *
24
+ * **It lives here because it was living in two places.** Both
25
+ * `Song-of-Heroic-Lands-FoundryVTT` and `sohl-thalorna` carried a copy, and the
26
+ * copies drifted: SoHL's caller still gated the derivation on `character` and
27
+ * `creature`, the two types #1580 merged into `being`, so it had matched
28
+ * nothing since the merge and all 95 of its being pages published with empty
29
+ * sidebar sections (SoHL#1696). thalorna's copy checked `being` and was right.
30
+ * Nothing failed in either repository; the pages built and shipped.
31
+ *
32
+ * {@link isBeing} exists for that reason. The bug was not in the derivation —
33
+ * it was in each caller's idea of what a being *is*, written out per repository
34
+ * where it could rot independently. One definition, imported.
35
+ *
36
+ * @module
37
+ */
38
+
39
+ /**
40
+ * The note `type` whose pages carry a being info block.
41
+ *
42
+ * One name, since #1580 merged `character` and `creature` into the `being` they
43
+ * had always compiled into. The retired names are deliberately **not** accepted
44
+ * as aliases: they throw elsewhere in the system, and tolerating them here
45
+ * would hide the next drift of this kind rather than surface it.
46
+ */
47
+ export const BEING_TYPE = "being";
48
+
49
+ /**
50
+ * Whether a note's frontmatter describes a being.
51
+ *
52
+ * @param {{type?: unknown}|null|undefined} fm - A note's frontmatter.
53
+ * @returns {boolean} `true` when the note is a being.
54
+ */
55
+ export function isBeing(fm) {
56
+ return Boolean(fm) && fm.type === BEING_TYPE;
57
+ }
58
+
59
+ /**
60
+ * The sidebar group each gear item type is displayed under.
61
+ *
62
+ * Presentation naming, not data-model naming: the model says `weapongear`, the
63
+ * sidebar heading says "weapons". Kept as one table so a new gear type is added
64
+ * in a single place rather than in each consumer's site build.
65
+ *
66
+ * @type {Readonly<Record<string, string>>}
67
+ */
68
+ export const GEAR_TYPE_TO_KEY = Object.freeze({
69
+ weapongear: "weapons",
70
+ armorgear: "armor",
71
+ projectilegear: "projectiles",
72
+ miscgear: "misc",
73
+ containergear: "containers",
74
+ concoctiongear: "concoctions",
75
+ });
76
+
77
+ /** Whether a value is a plain mapping. */
78
+ const isMap = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
79
+
80
+ /** Whether a value is a non-empty array. */
81
+ const nonEmpty = (v) => Array.isArray(v) && v.length > 0;
82
+
83
+ /**
84
+ * Derive a being's info-block fields from its raw `sohl.items[]`.
85
+ *
86
+ * Each item's `shortcode` is resolved against `index` — keyed
87
+ * `"<type>:<shortcode>"` — for a display name and a link to the item's own
88
+ * page. `attributes` already match the sidebar shape and pass through
89
+ * untouched.
90
+ *
91
+ * **Authored values win.** Only fields the author did not supply are derived,
92
+ * so a note that hand-writes `sohl.skills` keeps exactly what it wrote. An
93
+ * item's inline `name` beats the index, and an unresolved shortcode falls back
94
+ * to *itself* rather than being dropped — a page that names an item the index
95
+ * has not heard of is better than a page silently missing a row.
96
+ *
97
+ * Returns a new object; the input is not mutated.
98
+ *
99
+ * @param {object|null|undefined} sohl - The note's `sohl` frontmatter block.
100
+ * @param {Map<string, {name?: string, url?: string}>} index - Content index,
101
+ * `"<type>:<shortcode>"` → the item's page.
102
+ * @returns {object|null|undefined} The block with its info-block fields filled
103
+ * in, or the input unchanged when there is nothing to derive from.
104
+ */
105
+ export function deriveBeingInfo(sohl, index) {
106
+ if (!isMap(sohl)) return sohl;
107
+ const out = { ...sohl };
108
+ const items = Array.isArray(out.items) ? out.items : [];
109
+ if (items.length === 0) return out;
110
+
111
+ const lookup = (type, shortcode) =>
112
+ shortcode ? index.get(`${type}:${shortcode}`) : undefined;
113
+
114
+ /** An item's display name: its own, then the index's, then its shortcode. */
115
+ const displayName = (it, ref, shortcode) =>
116
+ (typeof it.name === "string" && it.name) || ref?.name || shortcode;
117
+
118
+ // Skills: { shortcode: masteryLevelBase }.
119
+ if (!(isMap(out.skills) && Object.keys(out.skills).length > 0)) {
120
+ const skills = {};
121
+ for (const it of items) {
122
+ if (!isMap(it) || it.type !== "skill") continue;
123
+ const level = it.system?.masteryLevelBase;
124
+ if (typeof it.shortcode === "string" && typeof level === "number") {
125
+ skills[it.shortcode] = level;
126
+ }
127
+ }
128
+ if (Object.keys(skills).length > 0) out.skills = skills;
129
+ }
130
+
131
+ // Gear: { weapons: [{ name, shortcode?, url? }], armor: [...], … }.
132
+ if (!isMap(out.gear)) {
133
+ const gear = {};
134
+ for (const it of items) {
135
+ if (!isMap(it)) continue;
136
+ const key = GEAR_TYPE_TO_KEY[it.type];
137
+ if (!key) continue;
138
+ const shortcode =
139
+ typeof it.shortcode === "string" ? it.shortcode : undefined;
140
+ const ref = lookup(it.type, shortcode);
141
+ const name = displayName(it, ref, shortcode);
142
+ if (!name) continue;
143
+ const entry = { name };
144
+ if (shortcode) entry.shortcode = shortcode;
145
+ if (ref?.url) entry.url = ref.url;
146
+ (gear[key] ??= []).push(entry);
147
+ }
148
+ if (Object.keys(gear).length > 0) out.gear = gear;
149
+ }
150
+
151
+ // Mystical abilities, split by subType into spells / talents.
152
+ const spells = [];
153
+ const talents = [];
154
+ for (const it of items) {
155
+ if (!isMap(it) || it.type !== "mysticalability") continue;
156
+ const shortcode =
157
+ typeof it.shortcode === "string" ? it.shortcode : undefined;
158
+ const ref = lookup("mysticalability", shortcode);
159
+ // No shortcode fallback here: an ability with neither an inline name
160
+ // nor an index entry has nothing to show, and a row reading like a
161
+ // shortcode is worse than no row.
162
+ const name = (typeof it.name === "string" && it.name) || ref?.name;
163
+ if (!name) continue;
164
+ const entry = { name };
165
+ if (ref?.url) entry.url = ref.url;
166
+ if (it.subType === "arcaneincantation") spells.push(entry);
167
+ else if (it.subType === "arcanetalent") talents.push(entry);
168
+ }
169
+ if (spells.length > 0 && !nonEmpty(out.spells)) out.spells = spells;
170
+ if (talents.length > 0 && !nonEmpty(out.talents)) out.talents = talents;
171
+
172
+ return out;
173
+ }
@@ -0,0 +1,82 @@
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
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * Default item artwork, keyed by item `type` — the single source of truth for
16
+ * both the compendium builder (the items pack compiler, at build time) and the
17
+ * runtime `SohlItem.getDefaultArtwork` override (fresh, ad-hoc-created items).
18
+ *
19
+ * Foundry's `Item.DEFAULT_ICON` is the white `icons/svg/item-bag.svg`, which is
20
+ * invisible on the light Manuscript sheet and does not adapt to theme; every
21
+ * SoHL item type instead defaults to a themed `systems/sohl/assets/icons/**`
22
+ * SVG here (dark ink in light mode, cream in dark, via the build-time
23
+ * adaptive-fill injection in `utils/svg-theme.mjs`).
24
+ *
25
+ * This module is deliberately **plain ESM** — no TypeScript, no `@src` aliases,
26
+ * no Foundry — precisely so the plain-`node` pack scripts *and* the bundled TS
27
+ * runtime can both import it. A `.ts` module could not be required by the build
28
+ * scripts (they run under bare `node`, without the bundler that resolves the
29
+ * `@src` alias and strips types). Keeping one map here is what prevents the
30
+ * build-time and runtime defaults from drifting apart (issue #932 was exactly
31
+ * that drift: the builder had a default, runtime did not).
32
+ *
33
+ * It lives in this package rather than in the system's `src/` tree because the
34
+ * pack pipeline is installed as a dependency (#1501): a relative path out of
35
+ * the package would resolve to garbage once the pipeline runs from
36
+ * `node_modules`. The runtime imports it back through the package's
37
+ * `./sohl/default-item-art` entry point, so there is still exactly one map
38
+ * (#1510). Injecting the map through configuration instead would re-open #932,
39
+ * because the two sides would once more be free to disagree.
40
+ *
41
+ * Paths are fully resolved (`systems/sohl/...`) — the served path both layers
42
+ * need. If a new item type is added, add its default here.
43
+ */
44
+ export const DEFAULT_ITEM_ART = {
45
+ affiliation: "systems/sohl/assets/icons/noun/shield.svg",
46
+ affliction: "systems/sohl/assets/icons/other/sick.svg",
47
+ armorgear: "systems/sohl/assets/icons/game-icons/lorc/breastplate.svg",
48
+ attribute: "systems/sohl/assets/icons/other/charm.svg",
49
+ concoctiongear: "systems/sohl/assets/icons/game-icons/badges/flask.svg",
50
+ containergear: "systems/sohl/assets/icons/other/sack.svg",
51
+ miscgear: "systems/sohl/assets/icons/other/question-mark.svg",
52
+ mystery: "systems/sohl/assets/icons/other/sparkles.svg",
53
+ mysticalability: "systems/sohl/assets/icons/other/hand-sparkles.svg",
54
+ projectilegear: "systems/sohl/assets/icons/noun/arrow.svg",
55
+ skill: "systems/sohl/assets/icons/other/head-gear.svg",
56
+ trauma: "systems/sohl/assets/icons/other/injury.svg",
57
+ weapongear: "systems/sohl/assets/icons/other/sword.svg",
58
+ };
59
+
60
+ /**
61
+ * The default art path for an item `type`, or throw when the type is unknown —
62
+ * the build's fail-fast contract, so an unrecognized type is never silently
63
+ * defaulted (aborting the pack build rather than shipping a mismatched icon).
64
+ *
65
+ * Runtime callers that must tolerate unknown or `base`-typed items should read
66
+ * {@link DEFAULT_ITEM_ART} directly and fall back to Foundry's default instead
67
+ * of calling this — see `SohlItem.getDefaultArtwork`.
68
+ *
69
+ * @param {string} type - the item type.
70
+ * @returns {string} the default image path for that type.
71
+ */
72
+ export function defaultItemArt(type) {
73
+ if (!(type in DEFAULT_ITEM_ART)) {
74
+ throw new Error(
75
+ `No default art for item type "${type}" — add one to @heroiclands/package-build/sohl/default-item-art`,
76
+ );
77
+ }
78
+ // The map's inferred type has no index signature — deliberately, so callers
79
+ // reading it directly get the precise key set. The `in` guard above has
80
+ // already proved this key is present.
81
+ return /** @type {Record<string, string>} */ (DEFAULT_ITEM_ART)[type];
82
+ }
package/sohl/index.mjs ADDED
@@ -0,0 +1,55 @@
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
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * The SoHL-specific half of the toolchain: the knowledge of the Song of Heroic
16
+ * Lands data model that a generic content module must never receive.
17
+ *
18
+ * The item-type registry and its builders, the items and actors compilers, the
19
+ * default-art map, and the affiliation standings live here (#1512). Nothing in
20
+ * `@heroiclands/package-build/engine` exports any of it, so an adventure module
21
+ * that builds journals, macros, and scenes never receives `buildWeaponGear`.
22
+ *
23
+ * A consuming repository hands its own registry to the engine as configuration
24
+ * — `itemBuilders` in `package-build.config.yaml` — which is how the engine
25
+ * composes the one doc-carrying-type set without holding any package's data
26
+ * model.
27
+ *
28
+ * Namespaced rather than flattened, for the reason the engine barrel gives.
29
+ *
30
+ * @module
31
+ */
32
+
33
+ /** The item-type registry: every type that compiles into an Item, and its builder. */
34
+ export * as itemBuilders from "./item-builders.mjs";
35
+
36
+ /** The Item compiler. */
37
+ export * as items from "./items.mjs";
38
+
39
+ /** The Actor compiler. */
40
+ export * as actors from "./actors.mjs";
41
+
42
+ /** This package's own knowledgebase body passes, named from `site.pass`. */
43
+ export * as kbPasses from "./kb-passes.mjs";
44
+
45
+ // Flat as well as namespaced: the Foundry runtime imports these by name through
46
+ // their own entry points, and they were this barrel's surface before the
47
+ // compilers arrived (#1510).
48
+ export { DEFAULT_ITEM_ART, defaultItemArt } from "./default-item-art.mjs";
49
+ export { AFFILIATION_STANDINGS } from "./affiliation-standings.mjs";
50
+ export {
51
+ BEING_TYPE,
52
+ GEAR_TYPE_TO_KEY,
53
+ deriveBeingInfo,
54
+ isBeing,
55
+ } from "./being-info.mjs";
@@ -0,0 +1,114 @@
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
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * **The item-type registry** — every content type that compiles into a Foundry
16
+ * Item, keyed to the builder that produces its `system` block.
17
+ *
18
+ * There is one list, not two. This repository hands {@link ITEM_BUILDERS} to
19
+ * the build as `itemBuilders` in `package-build.config.yaml`, and `itemTypes()`
20
+ * — the whitelist — is derived from that table's own keys, so a type cannot be
21
+ * whitelisted for compilation without a builder to compile it. Previously the
22
+ * whitelist and the builder table were maintained by hand and had already
23
+ * drifted: `trait` — an item type **retired in #651**, absent from
24
+ * `documentTypes.Item` and reported by world migration as unrecognized — was
25
+ * still advertised as compilable, so a `type: trait` note passed the gate and
26
+ * then died on `BUILDERS[type] is not a function`, swallowed as a per-file
27
+ * error (#1504).
28
+ *
29
+ * **The builders are generated, not written.** Each type's `system` builder
30
+ * comes from its field declaration in `item-fields.mjs` by way of
31
+ * {@link buildFromFields}, so the vocabulary a note may write is readable data
32
+ * rather than statements buried in a function body — which is what lets the
33
+ * authoring reference be generated and a note be linted against its type
34
+ * (#22). One consequence worth naming: the registry's keys are now
35
+ * {@link ITEM_FIELDS}'s keys, so adding an item type is one edit *there* (plus
36
+ * its `documentTypes.Item` declaration and its default art), and removing one
37
+ * is likewise a single deletion.
38
+ *
39
+ * **A leaf module, and the reason the seam works.** It imports only the pure
40
+ * declaration primitives and the field table — never `helpers.mjs`, and never
41
+ * the resolved configuration. The config file imports *this* module, so a read
42
+ * back out of the configuration here would close a cycle around the config's
43
+ * own evaluation. The table travels into configuration; the engine's
44
+ * `item-registry.mjs` reads it back out and the Item compiler dispatches
45
+ * through that, which is how a consumer's own table is the one its notes
46
+ * compile with (#1563).
47
+ */
48
+
49
+ import { defaultItemArt } from "./default-item-art.mjs";
50
+ import { buildFromFields, readField } from "../engine/field-spec.mjs";
51
+ import { COMBAT_TECHNIQUE_STRIKE_MODE, ITEM_FIELDS } from "./item-fields.mjs";
52
+
53
+ /**
54
+ * The conditionals a flat field list cannot state, keyed by item type.
55
+ *
56
+ * A finalizer runs after the declaration has produced the `system` block, and
57
+ * exists only for a field whose *presence* depends on another field's value.
58
+ * There is exactly one: a combat technique is authored as a `skill` of subtype
59
+ * `combattechnique`, and carries a strike mode that no other skill has.
60
+ *
61
+ * Keeping this list short is the point. Anything expressible as a field
62
+ * declaration is said there, where a generator and a linter can read it; a
63
+ * finalizer is opaque again, so it earns its place one conditional at a time.
64
+ *
65
+ * @type {Readonly<Record<string, (fm: object, out: object) => object>>}
66
+ */
67
+ const FINALIZERS = Object.freeze({
68
+ skill(fm, out) {
69
+ if (out.subType === "combattechnique") {
70
+ out.strikeMode = readField(COMBAT_TECHNIQUE_STRIKE_MODE, fm);
71
+ }
72
+ return out;
73
+ },
74
+ });
75
+
76
+ /**
77
+ * Build one registry entry: the declared fields, the builder they generate, and
78
+ * the default art for the type.
79
+ *
80
+ * Reading {@link defaultItemArt} here is what makes the two lists one: this
81
+ * module cannot name a type the art map does not cover, because that function
82
+ * throws and this module evaluates at import. A drift that a test used to catch
83
+ * is unrepresentable.
84
+ *
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.
88
+ *
89
+ * @param {string} type - The item type, and the art map's key.
90
+ * @returns {Readonly<{system: (fm: object) => object, img: string, fields: readonly object[]}>}
91
+ * The registry entry.
92
+ */
93
+ function entryFor(type) {
94
+ const fields = ITEM_FIELDS[type];
95
+ const build = buildFromFields(fields);
96
+ const finalize = FINALIZERS[type];
97
+ return Object.freeze({
98
+ system: finalize ? (fm) => finalize(fm, build(fm)) : build,
99
+ img: defaultItemArt(type),
100
+ fields,
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Every item type, paired with the builder for its `system` block, the default
106
+ * art for the type, and the frontmatter fields it declares.
107
+ *
108
+ * @type {Readonly<Record<string, Readonly<{system: (fm: object) => object, img: string, fields: readonly object[]}>>>}
109
+ */
110
+ export const ITEM_BUILDERS = Object.freeze(
111
+ Object.fromEntries(
112
+ Object.keys(ITEM_FIELDS).map((type) => [type, entryFor(type)]),
113
+ ),
114
+ );