@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
@@ -0,0 +1,84 @@
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
+ * **HM3's item-type registry** — every content type that compiles into an HM3
16
+ * Foundry Item, keyed to the builder that produces its `system` block.
17
+ *
18
+ * The same arrangement as `sohl/item-builders.mjs`, and a repository feeding
19
+ * both systems names both: `itemBuilders: [sohl, hm3]`. The engine keeps them
20
+ * apart from there on — `itemBuilder(type, system)` takes the system that is
21
+ * asking, and a type both registries declare **throws** rather than resolving
22
+ * when nobody says which (#58). That guard existed before there was a second
23
+ * registry to trip it; this is the registry it was written for.
24
+ *
25
+ * **The builders are generated, not written**, from the field declarations in
26
+ * `item-fields.mjs`, compiled against the `hm3` block. `buildFromFields` takes
27
+ * the block as an argument precisely so one declaration mechanism serves any
28
+ * number of systems: `data.weight` reaches `system.weightBase` under `sohl:`
29
+ * and `system.weight` under `hm3:`, from two declarations and one engine.
30
+ *
31
+ * **No finalizers.** SoHL has one — a combat technique's strike mode, whose
32
+ * *presence* depends on another field's value — and HM3 has no such
33
+ * conditional. The absence is worth stating: a finalizer is opaque to every
34
+ * reader a declaration is legible to, so having none is the state to keep.
35
+ *
36
+ * **A leaf.** It imports the declaration primitives, the field table and the
37
+ * art map, and never the resolved configuration — the configuration file
38
+ * imports *this*, so a read back out would close a cycle around the
39
+ * configuration's own evaluation.
40
+ *
41
+ * @module
42
+ */
43
+
44
+ import { buildFromFields } from "../engine/field-spec.mjs";
45
+ import { hm3DefaultItemArt } from "./default-item-art.mjs";
46
+ import { HM3_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
47
+ import { HM3_ITEM_FIELDS } from "./item-fields.mjs";
48
+
49
+ /**
50
+ * The frontmatter block these builders read.
51
+ *
52
+ * Taken from the map rather than spelled here, so the block name, the subtype
53
+ * map and the registry are one statement.
54
+ *
55
+ * @type {string}
56
+ */
57
+ const BLOCK = HM3_DOCUMENT_SUBTYPES.block;
58
+
59
+ /**
60
+ * Build one registry entry: the declared fields, the builder they generate, and
61
+ * the default art for the type.
62
+ *
63
+ * @param {string} type - The note type, and the art map's key.
64
+ * @returns {Readonly<{system: (fm: object) => object, img: string, fields: readonly object[]}>}
65
+ * The registry entry.
66
+ */
67
+ function entryFor(type) {
68
+ const fields = HM3_ITEM_FIELDS[type];
69
+ return Object.freeze({
70
+ system: buildFromFields(fields, { block: BLOCK }),
71
+ img: hm3DefaultItemArt(type),
72
+ fields,
73
+ });
74
+ }
75
+
76
+ /**
77
+ * Every HM3 item type, paired with the builder for its `system` block, the
78
+ * default art for the type, and the frontmatter fields it declares.
79
+ *
80
+ * @type {Readonly<Record<string, Readonly<{system: (fm: object) => object, img: string, fields: readonly object[]}>>>}
81
+ */
82
+ export const HM3_ITEM_BUILDERS = Object.freeze(
83
+ Object.fromEntries(Object.keys(HM3_ITEM_FIELDS).map((type) => [type, entryFor(type)])),
84
+ );
@@ -0,0 +1,180 @@
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 `hm3:` frontmatter vocabulary of every HM3 item type.
16
+ *
17
+ * The same arrangement `sohl/item-fields.mjs` describes: the declaration **is**
18
+ * the builder — {@link buildFromFields} turns each list into the function that
19
+ * runs — so a field that is not here is not emitted, and there is no second
20
+ * statement of the mapping to drift.
21
+ *
22
+ * **Deliberately shorter than SoHL's, and that is the honest answer.** The
23
+ * mapping tables in `docs/content-format.md` are the specification for what a
24
+ * shared `data:` fact becomes in each system, and HM3's column is `NA` far more
25
+ * often than SoHL's: a weapon's weight and value cross over, its quality and
26
+ * durability do not. Every row below is one the specification states. Where it
27
+ * says nothing — HM3's `armorlocation` has no `data:` row at all, and `trauma`
28
+ * and `mysticalability` have none either — this file declares nothing rather
29
+ * than inventing a plausible path. A guessed `to` compiles clean and is
30
+ * discarded by Foundry at load without a word, which is exactly the failure the
31
+ * schema check exists to catch and exactly the failure a guess would create.
32
+ *
33
+ * **One shared source, two destinations.** `name` is the shared property a
34
+ * field draws from and `to` is where it lands, so a single authored `weight`
35
+ * feeds `sohl.system.weightBase` *and* `hm3.system.weight` — one authored fact,
36
+ * two documents (#58). That is the whole reason the two are declared
37
+ * separately.
38
+ *
39
+ * The specification writes those sources as `data.weight`, and the sources here
40
+ * are written bare, exactly as SoHL's are: the `data:` region is #128's
41
+ * migration and neither half has moved to it. Spelling HM3's differently would
42
+ * make a note feeding both systems author the same fact twice for no gain, and
43
+ * would take these rows out of reach of `content-build content-format fields`,
44
+ * which pairs a `data.<key>` claim with a bare declared `<key>`. Both halves
45
+ * move together when #128 lands.
46
+ *
47
+ * **What a note authors under `hm3.system` is not declared here.** HM3 fields
48
+ * that no shared `data:` property feeds — a skill's `type`, an armour's
49
+ * `protection`, a character's `abilities` — are written at their own paths in
50
+ * the note's `hm3.system` block and reach the document through the verbatim
51
+ * passthrough. They are checked against HM3's published `schema.json` like
52
+ * everything else; they simply have no shared source to declare.
53
+ *
54
+ * **A leaf**, like its SoHL counterpart: declaration primitives only, never the
55
+ * resolved configuration.
56
+ *
57
+ * @module
58
+ */
59
+
60
+ import { AS_AUTHORED, NUMBER } from "../engine/field-spec.mjs";
61
+
62
+ /* --------------------------------------------------------------------- */
63
+ /* Rows shared by the four gear types */
64
+ /* --------------------------------------------------------------------- */
65
+
66
+ /**
67
+ * What HM3's gear template takes from the shared `data:` region.
68
+ *
69
+ * `weight` and `value` are the two rows every gear table in the content format
70
+ * gives an HM3 destination. `quality` and `durability` are SoHL's alone — HM3
71
+ * carries `weaponQuality` and `armorQuality` on the subtypes that have them,
72
+ * which is a different fact about a different thing, so the shared row is `NA`
73
+ * and nothing is emitted.
74
+ *
75
+ * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
76
+ */
77
+ const GEAR_COMMON = Object.freeze([
78
+ {
79
+ name: "weight",
80
+ to: "weight",
81
+ ...AS_AUTHORED,
82
+ kind: "number",
83
+ default: 0,
84
+ describe: "Weight of one, in pounds.",
85
+ },
86
+ {
87
+ name: "value",
88
+ to: "value",
89
+ ...AS_AUTHORED,
90
+ kind: "number",
91
+ default: 0,
92
+ describe: "Worth of one, in pence.",
93
+ },
94
+ ]);
95
+
96
+ /**
97
+ * The stack count, for the gear types whose content-format table gives it an
98
+ * HM3 destination.
99
+ *
100
+ * `armor`, `containergear` and `weapongear` are excluded on purpose: their
101
+ * tables state that quantity is always 1 and may not be authored, so the note
102
+ * has nothing to say and HM3's own `initial` is the right value to ship.
103
+ *
104
+ * @type {import("../engine/field-spec.mjs").FieldSpec}
105
+ */
106
+ const QUANTITY = Object.freeze({
107
+ name: "quantity",
108
+ to: "quantity",
109
+ ...NUMBER,
110
+ default: 1,
111
+ describe: "How many the stack holds.",
112
+ });
113
+
114
+ /* --------------------------------------------------------------------- */
115
+ /* Per-type declarations */
116
+ /* --------------------------------------------------------------------- */
117
+
118
+ /**
119
+ * Every HM3 item type's frontmatter vocabulary, in the order the `system` block
120
+ * emits it.
121
+ *
122
+ * The keys are **note** types, not HM3 document subtypes — `projectile`
123
+ * rather than `missilegear`, `mysticalability` rather than `psionic` — because
124
+ * a registry is addressed by what a note calls itself. What the document is
125
+ * called is the map's answer, and only the map's.
126
+ *
127
+ * @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
128
+ */
129
+ export const HM3_ITEM_FIELDS = Object.freeze({
130
+ armor: Object.freeze([...GEAR_COMMON]),
131
+
132
+ // The one subtype that extends the Foundry base directly, with none of the
133
+ // shared templates. Its content-format table has a single row, and that row
134
+ // is `NA` on both sides — so an armour location is authored entirely under
135
+ // `hm3.system` (`probWeight`, `impactType`, the per-aspect protections) and
136
+ // there is nothing for a shared source to feed.
137
+ armorlocation: Object.freeze([]),
138
+
139
+ containergear: Object.freeze([
140
+ ...GEAR_COMMON,
141
+ {
142
+ name: "capacity",
143
+ to: "capacity.max",
144
+ ...NUMBER,
145
+ default: 0,
146
+ describe: "How much the container holds, in pounds.",
147
+ },
148
+ ]),
149
+
150
+ miscgear: Object.freeze([...GEAR_COMMON, QUANTITY]),
151
+
152
+ // A note's `projectile` is HM3's `missilegear`; the fields are the gear
153
+ // template's, and the aspect, impact and range that make it a missile are
154
+ // authored under `hm3.system`.
155
+ projectile: Object.freeze([...GEAR_COMMON, QUANTITY]),
156
+
157
+ // `hm3.system.type` — "Craft", "Physical", "Communication", "Combat",
158
+ // "Magic", "Ritual" — has no shared source: the content format states
159
+ // outright that it does not map onto the note's `subType` and must be
160
+ // written for each HM3 skill. So it is authored at its own path and passes
161
+ // through, and only the mastery level is declared here.
162
+ skill: Object.freeze([
163
+ {
164
+ name: "masteryLevel",
165
+ to: "masteryLevel",
166
+ ...NUMBER,
167
+ default: 0,
168
+ describe: "Mastery level, as a percentage.",
169
+ },
170
+ ]),
171
+
172
+ // Both one-to-many item rows: every shared row in their content-format
173
+ // tables is `NA`, so what an HM3 trauma or mystical ability carries — a
174
+ // severity and heal rate, a convocation and level, a circle and deity — is
175
+ // authored under `hm3.system` against whichever subtype `hm3.type` names.
176
+ trauma: Object.freeze([]),
177
+ mysticalability: Object.freeze([]),
178
+
179
+ weapongear: Object.freeze([...GEAR_COMMON]),
180
+ });
package/hm3/items.mjs ADDED
@@ -0,0 +1,96 @@
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
+ * **HM3's Item pass** — the two things about compiling a note into an HM3 Item
16
+ * that are facts about HM3 rather than about the note format (#139).
17
+ *
18
+ * Everything else is {@link module:engine/item-compiler}'s, and is the same
19
+ * code the SoHL pass runs: which notes are claimed, which subtype each becomes,
20
+ * which registry builds it, the authored `hm3.system` passthrough, the schema
21
+ * check, and the compendium envelope. A second system is a map and a handful of
22
+ * emitted keys, which is the arrangement #79 and #58 were building towards.
23
+ *
24
+ * **What HM3's compiler writes on every item: one key, and only when there is
25
+ * something to write.** The content format gives an item's `{#appearance}`
26
+ * section a home in HM3 — `description` — and SoHL none, since no SoHL Item
27
+ * subtype declares such a field. So the section is rendered here and nowhere
28
+ * else. Where a note has no such section nothing is emitted at all, which
29
+ * matters for `armorlocation`: it is the one HM3 subtype that extends the
30
+ * Foundry base directly, declaring neither `description` nor `notes`, so an
31
+ * unconditional key would be a finding on every armour-location document in the
32
+ * pack.
33
+ *
34
+ * **And one flag, for the fact HM3's `system` has no field for.** The template
35
+ * priority is a shared statement — a note declaring `data.templatePriority` says
36
+ * the same thing to both systems — but HM3 declares no `system` field for it, so
37
+ * it lands under this system's own flag scope as `flags.hm3.templatePriority`,
38
+ * exactly as the Actor pass writes it. This pass wrote no such flag until #283:
39
+ * an item note declaring the priority compiled into a SoHL item that knew it was
40
+ * a template and an HM3 item that did not.
41
+ *
42
+ * **There is no HM3 equivalent of `docHtml`.** SoHL points an item at the
43
+ * JournalEntry its prose compiled into, and HM3's data model has nowhere to put
44
+ * such a pointer; inventing one would emit a key Foundry discards at load
45
+ * without a word. The prose still compiles into its JournalEntry — the journals
46
+ * pass claims every doc-carrying type regardless of system — it simply is not
47
+ * addressed from the item.
48
+ *
49
+ * @module
50
+ */
51
+
52
+ import { renderSection } from "../engine/anchored-sections.mjs";
53
+ import { SystemItemCompiler } from "../engine/item-compiler.mjs";
54
+ import { HM3_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
55
+ import { templateFlags } from "./template-priority.mjs";
56
+
57
+ export class Hm3Items extends SystemItemCompiler {
58
+ /**
59
+ * HM3's note-type → document-subtype map — the one declaration that says
60
+ * which block this pass reads, which notes it claims, and what each becomes
61
+ * (#58/#79).
62
+ *
63
+ * @type {import("../engine/document-subtypes.mjs").DocumentSubtypeMap}
64
+ */
65
+ static documentSubtypes = HM3_DOCUMENT_SUBTYPES;
66
+
67
+ /**
68
+ * The `system.*` field HM3 writes on an item from the note's prose.
69
+ *
70
+ * @param {object} fm - The note's frontmatter.
71
+ * @param {object} at - What the pass already knows about this note.
72
+ * @param {string} at.markdown - The note body, tables expanded and
73
+ * wikilinks resolved.
74
+ * @returns {object} The shared `system` fields — `description`, or nothing.
75
+ */
76
+ commonSystem(fm, { markdown }) {
77
+ const description = renderSection(markdown, "appearance");
78
+ return description ? { description } : {};
79
+ }
80
+
81
+ /**
82
+ * The `flags` HM3 writes on an item: whatever the note authors, plus the
83
+ * template priority.
84
+ *
85
+ * The same statement the Actor pass records, through the same
86
+ * {@link module:hm3/template-priority.templateFlags} — see that module for
87
+ * why the priority lives in flags at all, and for what its absence here
88
+ * cost (#283).
89
+ *
90
+ * @param {object} fm - The note's frontmatter.
91
+ * @returns {object} The flags to emit.
92
+ */
93
+ commonFlags(fm) {
94
+ return templateFlags(fm, this.system);
95
+ }
96
+ }
@@ -0,0 +1,85 @@
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
+ * **Where HM3 records the template priority** — one statement, read by both of
16
+ * this system's passes (#283).
17
+ *
18
+ * `data.templatePriority` is the shared fact that a note is a *starting
19
+ * template*, and the specification states it as a row every type maps:
20
+ * `system.templatePriority` in SoHL, `flags.hm3.templatePriority` here. It lands
21
+ * in flags because HM3's data model declares no field for it, and an undeclared
22
+ * `system` key is discarded at load without a word.
23
+ *
24
+ * It was written by the Actor pass alone. The Item pass emitted only whatever
25
+ * `flags` the note itself authored, so an item note declaring the priority
26
+ * compiled into a SoHL item that knew it was a template and an HM3 item that did
27
+ * not — silently on both sides of the build, since an omitted flag is exactly how
28
+ * this system says *not a template*, making a lost priority and a deliberate one
29
+ * the same output. Nothing could report it either: the emitted-`system` check
30
+ * compares against a declared schema, and a flag is declared by nothing.
31
+ *
32
+ * So the rule lives here rather than in either pass, and both call it. Two
33
+ * copies of it would be two chances to diverge again, which is the failure this
34
+ * module exists to close.
35
+ *
36
+ * @module
37
+ */
38
+
39
+ import log from "loglevel";
40
+
41
+ import { resolveName, statedTemplatePriority } from "../engine/helpers.mjs";
42
+ import { blockProperty } from "../engine/system-block.mjs";
43
+
44
+ /**
45
+ * A document's `flags`: whatever the note authors, plus this system's template
46
+ * priority.
47
+ *
48
+ * A note that is not a template — or states nothing — writes nothing, rather
49
+ * than a `null` nothing reads.
50
+ *
51
+ * **Read through the shared resolver, not a field declaration** (#266). A
52
+ * `FieldSpec`'s shared source is a single position, and this value has five:
53
+ * `data:`, this block, the top level, and the retiring `archetype` spelling in
54
+ * the latter two. The resolver is the single implementation of what a note said,
55
+ * so the two systems cannot disagree about it. It is read against **this**
56
+ * block: the `sohl:` block is not a source for an HM3 document, so a tree that
57
+ * still states the priority there (`harn-ensemble`, on 2,502 notes) writes no
58
+ * HM3 flag until it sweeps to `data:`.
59
+ *
60
+ * @param {object} fm - The note's frontmatter.
61
+ * @param {string} block - This pass's system block.
62
+ * @returns {object} The flags to emit.
63
+ */
64
+ export function templateFlags(fm, block) {
65
+ const authored = blockProperty(fm, block, "flags", {});
66
+ const stated = statedTemplatePriority(fm, resolveName(fm), { block });
67
+ if (stated == null) return authored;
68
+ // Coerced, as the field declaration this replaced coerced it: a note may
69
+ // state the priority as a YAML string, and `"3"` is a priority.
70
+ const value = Number(stated);
71
+ if (!Number.isFinite(value)) {
72
+ log.warn(
73
+ `${resolveName(fm)}: template priority ${JSON.stringify(stated)} is ` +
74
+ `not a number; no template flag written.`,
75
+ );
76
+ return authored;
77
+ }
78
+ return {
79
+ ...authored,
80
+ [block]: {
81
+ .../** @type {Record<string, unknown>} */ (authored)[block],
82
+ templatePriority: value,
83
+ },
84
+ };
85
+ }
package/manifest.mjs CHANGED
@@ -53,6 +53,7 @@ import fsSync from "node:fs";
53
53
  import path from "node:path";
54
54
 
55
55
  import { emitDiagnostic, positionOfYamlPath } from "./engine/diagnostics.mjs";
56
+ import { metadataFileName } from "./engine/metadata-index.mjs";
56
57
 
57
58
  /**
58
59
  * The two package kinds Foundry defines, as the artifact name each one's
@@ -97,7 +98,7 @@ export function normalizeRepoUrl(repository) {
97
98
  }
98
99
 
99
100
  /**
100
- * The four addresses a Foundry manifest advertises.
101
+ * The addresses a Foundry manifest advertises.
101
102
  *
102
103
  * `manifest` deliberately points at **`releases/latest`** rather than at this
103
104
  * version: it is the URL an *installed* package re-fetches to discover that a
@@ -120,6 +121,26 @@ export function releaseUrls({ repoUrl, version, artifact }) {
120
121
  };
121
122
  }
122
123
 
124
+ /**
125
+ * Where this release publishes its content index (#239).
126
+ *
127
+ * **Pinned to this version, like `download` and unlike `manifest`.** A
128
+ * consumer reaches this URL by reading the dependency's manifest, so the
129
+ * manifest it read and the index it then fetches describe the same release —
130
+ * which is the whole point of publishing them together. A `releases/latest`
131
+ * index would silently pair a pinned manifest with a moving index, and the
132
+ * mismatch would surface as a cross-package link that resolved yesterday.
133
+ *
134
+ * @param {object} opts
135
+ * @param {string} opts.repoUrl - Normalised repository URL.
136
+ * @param {string} opts.version - The version being built.
137
+ * @param {string} opts.contentPackage - The content package name.
138
+ * @returns {string} The version-pinned asset URL.
139
+ */
140
+ export function metadataUrl({ repoUrl, version, contentPackage }) {
141
+ return `${repoUrl}/releases/download/v${version}/${metadataFileName(contentPackage)}`;
142
+ }
143
+
123
144
  /**
124
145
  * The order the manifest's keys are written in.
125
146
  *
@@ -455,8 +476,28 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
455
476
 
456
477
  const merged = { ...declared, ...derived };
457
478
 
479
+ // The index every consumer resolves this package's addresses through
480
+ // (#239). Written unconditionally, because a package that publishes no
481
+ // index is one nothing can link into — and the failure of an absent one is
482
+ // a dead link in somebody else's build, which is exactly the kind of
483
+ // silence this replaced the vendored manifest to end.
484
+ //
485
+ // A flag rather than a top-level key because Foundry's manifest schema is
486
+ // closed and `flags` is its declared extension point; an unknown key at the
487
+ // top level is dropped by some readers and rejected by others.
488
+ if (config.contentPackage) {
489
+ merged.flags = {
490
+ ...(declared.flags ?? {}),
491
+ metadataUrl: metadataUrl({
492
+ repoUrl,
493
+ version: packageJson.version,
494
+ contentPackage: config.contentPackage,
495
+ }),
496
+ };
497
+ }
498
+
458
499
  if (flags && Object.keys(flags).length) {
459
- merged.flags = { ...(declared.flags ?? {}) };
500
+ merged.flags = { ...(merged.flags ?? declared.flags ?? {}) };
460
501
  for (const [namespace, values] of Object.entries(flags)) {
461
502
  merged.flags[namespace] = {
462
503
  ...(declared.flags?.[namespace] ?? {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "17.2.0",
3
+ "version": "18.0.0",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -27,6 +27,14 @@
27
27
  "types": "./types/sohl/*.d.mts",
28
28
  "import": "./sohl/*.mjs"
29
29
  },
30
+ "./hm3": {
31
+ "types": "./types/hm3/index.d.mts",
32
+ "import": "./hm3/index.mjs"
33
+ },
34
+ "./hm3/*": {
35
+ "types": "./types/hm3/*.d.mts",
36
+ "import": "./hm3/*.mjs"
37
+ },
30
38
  "./content-config": {
31
39
  "types": "./types/content-config.d.mts",
32
40
  "import": "./content-config.mjs"
@@ -105,6 +113,7 @@
105
113
  "deploy.mjs",
106
114
  "e2e.mjs",
107
115
  "engine",
116
+ "hm3",
108
117
  "index.mjs",
109
118
  "lang.mjs",
110
119
  "labels.mjs",
@@ -118,11 +127,14 @@
118
127
  "types"
119
128
  ],
120
129
  "dependencies": {
130
+ "@duckdb/node-api": "^1.5.5-r.4",
121
131
  "@foundryvtt/foundryvtt-cli": "^3.0.4",
122
132
  "acorn": "^8.18.0",
123
133
  "archiver": "^8.0.0",
124
134
  "classic-level": "^3.0.0",
125
135
  "dotenv": "^17.2.3",
136
+ "eslint": "^10.10.0",
137
+ "eslint-plugin-yml": "^3.8.1",
126
138
  "fflate": "^0.8.3",
127
139
  "glob": "^13.0.6",
128
140
  "gray-matter": "^4.0.3",
@@ -150,12 +162,14 @@
150
162
  "build:types": "tsc -p tsconfig.dts.json",
151
163
  "format": "prettier --write .",
152
164
  "format:check": "prettier --check .",
153
- "lint": "npm run format:check && npm run lint:markdown && npm run lint:content-format",
165
+ "lint": "npm run format:check && npm run lint:markdown && npm run lint:yaml && npm run lint:labels && npm run lint:content-format",
154
166
  "lint:markdown": "node bin/content-build.mjs markdown",
167
+ "lint:yaml": "node bin/package-build.mjs yaml",
168
+ "lint:labels": "node bin/package-build.mjs labels check",
155
169
  "lint:markdown:fix": "node bin/content-build.mjs markdown --fix",
156
170
  "lint:content-format": "npm run lint:content-format:schema && npm run lint:content-format:fields",
157
- "lint:content-format:schema": "node bin/content-build.mjs content-format schema --schema sohl=tests/fixtures/content-format/schema-sohl.json",
158
- "lint:content-format:fields": "node bin/content-build.mjs content-format fields --fields sohl",
171
+ "lint:content-format:schema": "node bin/content-build.mjs content-format schema --schema sohl=tests/fixtures/content-format/schema-sohl.json --schema hm3=tests/fixtures/content-format/schema-hm3.json",
172
+ "lint:content-format:fields": "node bin/content-build.mjs content-format fields --fields sohl && node bin/content-build.mjs content-format fields --fields hm3",
159
173
  "changeset": "changeset",
160
174
  "changeset:check": "changeset status --since=origin/main",
161
175
  "changeset:version": "changeset version && npm install --package-lock-only",