@heroiclands/package-build 22.15.0 → 22.16.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 (223) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/README.md +39 -678
  3. package/bin/{content-build.mjs → content-commands.mjs} +197 -118
  4. package/bin/package-build.mjs +303 -42
  5. package/bin/report.mjs +1 -1
  6. package/ci/ci-docker.mjs +6 -3
  7. package/ci/ci-steps.mjs +6 -4
  8. package/ci/content-format-reference.mjs +106 -0
  9. package/config.mjs +22 -2
  10. package/content-config.mjs +74 -134
  11. package/docs/api.md +294 -234
  12. package/docs/authoring/assets.md +31 -0
  13. package/docs/authoring/dates-and-calendars.md +33 -0
  14. package/docs/authoring/first-note.md +48 -0
  15. package/docs/authoring/frontmatter.md +40 -0
  16. package/docs/authoring/links-and-markup.md +137 -0
  17. package/docs/commands.md +631 -162
  18. package/docs/configuration.md +176 -140
  19. package/docs/content-format.md +50 -4229
  20. package/docs/design.md +32 -0
  21. package/docs/diagnostics.md +29 -22
  22. package/docs/getting-started.md +21 -877
  23. package/docs/guides/book.md +153 -0
  24. package/docs/guides/e2e.md +143 -0
  25. package/docs/guides/icons.md +126 -0
  26. package/docs/guides/localization.md +120 -0
  27. package/docs/guides/module-project.md +915 -0
  28. package/docs/guides/packs.md +167 -0
  29. package/docs/guides/reference-pages.md +148 -0
  30. package/docs/guides/release-and-deploy.md +114 -0
  31. package/docs/guides/site.md +152 -0
  32. package/docs/guides/system-project.md +45 -0
  33. package/docs/index.md +16 -0
  34. package/docs/project-setup.md +54 -35
  35. package/docs/reference/format-details.md +4420 -0
  36. package/docs/reference/note-types.md +531 -0
  37. package/engine/address-render.mjs +26 -0
  38. package/engine/address.mjs +3 -20
  39. package/engine/anchors.mjs +15 -6
  40. package/engine/asset-bindings.mjs +6 -2
  41. package/engine/base-compiler.mjs +47 -11
  42. package/engine/being-measurements.mjs +70 -0
  43. package/engine/calendar-artifacts.mjs +1 -1
  44. package/engine/calendar-choice.mjs +32 -0
  45. package/engine/calendar-human.mjs +71 -0
  46. package/engine/calendar-notes.mjs +41 -3
  47. package/engine/calendars.mjs +8 -1
  48. package/engine/changelog-lint.mjs +2 -2
  49. package/engine/code-fences.mjs +23 -97
  50. package/engine/compile-corpus.mjs +6 -7
  51. package/engine/content-admonitions.mjs +114 -0
  52. package/engine/content-captions.mjs +181 -0
  53. package/engine/content-embeds.mjs +2 -2
  54. package/engine/content-footnotes.mjs +34 -0
  55. package/engine/content-format-check.mjs +2 -2
  56. package/engine/content-format.mjs +127 -73
  57. package/engine/content-format.yaml +745 -0
  58. package/engine/content-html.mjs +1 -1
  59. package/engine/content-icons.mjs +45 -92
  60. package/engine/content-images.mjs +22 -74
  61. package/engine/content-index.mjs +12 -3
  62. package/engine/content-links.mjs +21 -26
  63. package/engine/content-lint.mjs +1 -5
  64. package/engine/content-secrets.mjs +3 -1
  65. package/engine/content-tables.mjs +47 -1257
  66. package/engine/culture-choice.mjs +32 -0
  67. package/engine/date-conversion.mjs +47 -0
  68. package/engine/declaration-check.mjs +87 -0
  69. package/engine/docs-checks.mjs +140 -0
  70. package/engine/documentation-links.mjs +31 -0
  71. package/engine/exported-scene.mjs +1 -1
  72. package/engine/extension-attributes.mjs +74 -0
  73. package/engine/field-reference.mjs +1 -1
  74. package/engine/field-spec.mjs +4 -0
  75. package/engine/foreign-catalog.mjs +5 -5
  76. package/engine/format-generated.mjs +15 -0
  77. package/engine/foundry-entries.mjs +3 -1
  78. package/engine/frontmatter-lint.mjs +5 -1
  79. package/engine/frontmatter.mjs +10 -0
  80. package/engine/generate.mjs +2 -2
  81. package/engine/helpers.mjs +71 -117
  82. package/engine/homepage.mjs +2 -3
  83. package/engine/index-records.mjs +1 -0
  84. package/engine/index.mjs +18 -3
  85. package/engine/infobox-registry.mjs +3 -1
  86. package/engine/infobox.mjs +38 -20
  87. package/engine/item-compiler.mjs +10 -8
  88. package/engine/item-docs.mjs +7 -6
  89. package/engine/itinerary-scenes.mjs +4 -2
  90. package/engine/journals.mjs +51 -10
  91. package/engine/macros.mjs +1 -1
  92. package/engine/map-build.mjs +1 -1
  93. package/engine/map-dot.mjs +1 -1
  94. package/engine/map-raster.mjs +4 -2
  95. package/engine/markdown-expressions.mjs +117 -0
  96. package/engine/metadata-index.mjs +7 -3
  97. package/engine/note-addresses.mjs +1 -0
  98. package/engine/note-dates.mjs +283 -61
  99. package/engine/note-package.mjs +5 -25
  100. package/engine/note-state.mjs +6 -2
  101. package/engine/note-vocabulary.mjs +74 -19
  102. package/engine/number-words.mjs +75 -0
  103. package/engine/office-holders.mjs +188 -0
  104. package/engine/pack-config.mjs +2 -15
  105. package/engine/pack-router.mjs +24 -5
  106. package/engine/packages.mjs +3 -2
  107. package/engine/pathnames.mjs +21 -24
  108. package/engine/pdf-build.mjs +217 -55
  109. package/engine/pdf-fonts.mjs +2 -2
  110. package/engine/pdf-images.mjs +82 -0
  111. package/engine/pdf-render.mjs +174 -22
  112. package/engine/populations.mjs +3 -2
  113. package/engine/project-init.mjs +499 -0
  114. package/engine/prose-config.mjs +1 -1
  115. package/engine/prose-lint.mjs +7 -6
  116. package/engine/readability-lint.mjs +86 -0
  117. package/engine/reckoning-markers.mjs +205 -93
  118. package/engine/scenes.mjs +5 -2
  119. package/engine/schema-check.mjs +3 -3
  120. package/engine/site-build.mjs +87 -37
  121. package/engine/site-config.mjs +8 -8
  122. package/engine/site-index.mjs +4 -0
  123. package/engine/site-maps.mjs +1 -1
  124. package/engine/site-preview.mjs +19 -8
  125. package/engine/social-ties.mjs +5 -1
  126. package/engine/sql-tables.mjs +92 -22
  127. package/engine/web-wikilinks.mjs +15 -31
  128. package/engine/wikilink-syntax.mjs +4 -1
  129. package/engine/wikilinks.mjs +12 -14
  130. package/hm3/item-fields.mjs +1 -1
  131. package/hm3/items.mjs +12 -45
  132. package/index.mjs +2 -14
  133. package/manifest.mjs +31 -4
  134. package/markdownlint-config.mjs +1 -1
  135. package/package.json +32 -11
  136. package/prettier-config.mjs +1 -1
  137. package/sohl/being-info.mjs +3 -145
  138. package/sohl/index.mjs +1 -1
  139. package/sohl/infobox.mjs +2 -1
  140. package/sohl/item-builders.mjs +3 -27
  141. package/sohl/item-fields.mjs +61 -22
  142. package/sohl/kb-passes.mjs +2 -2
  143. package/sohl/note-schemas.mjs +0 -8
  144. package/stage.mjs +19 -0
  145. package/types/config.d.mts +12 -3
  146. package/types/content-config.d.mts +15 -81
  147. package/types/engine/address-render.d.mts +13 -0
  148. package/types/engine/address.d.mts +2 -11
  149. package/types/engine/anchors.d.mts +3 -4
  150. package/types/engine/asset-bindings.d.mts +9 -2
  151. package/types/engine/base-compiler.d.mts +1 -2
  152. package/types/engine/being-measurements.d.mts +16 -0
  153. package/types/engine/calendar-choice.d.mts +14 -0
  154. package/types/engine/calendar-human.d.mts +4 -0
  155. package/types/engine/calendar-notes.d.mts +2 -0
  156. package/types/engine/calendars.d.mts +5 -3
  157. package/types/engine/code-fences.d.mts +6 -72
  158. package/types/engine/compile-corpus.d.mts +1 -3
  159. package/types/engine/content-admonitions.d.mts +24 -0
  160. package/types/engine/content-captions.d.mts +45 -0
  161. package/types/engine/content-footnotes.d.mts +10 -0
  162. package/types/engine/content-format-check.d.mts +17 -4
  163. package/types/engine/content-format.d.mts +24 -11
  164. package/types/engine/content-icons.d.mts +14 -23
  165. package/types/engine/content-images.d.mts +2 -2
  166. package/types/engine/content-index.d.mts +9 -2
  167. package/types/engine/content-links.d.mts +3 -5
  168. package/types/engine/content-tables.d.mts +11 -140
  169. package/types/engine/culture-choice.d.mts +14 -0
  170. package/types/engine/date-conversion.d.mts +4 -0
  171. package/types/engine/declaration-check.d.mts +24 -0
  172. package/types/engine/docs-checks.d.mts +28 -0
  173. package/types/engine/documentation-links.d.mts +7 -0
  174. package/types/engine/exported-scene.d.mts +6 -2
  175. package/types/engine/extension-attributes.d.mts +14 -0
  176. package/types/engine/field-spec.d.mts +9 -0
  177. package/types/engine/format-generated.d.mts +7 -0
  178. package/types/engine/frontmatter.d.mts +14 -4
  179. package/types/engine/generate.d.mts +1 -1
  180. package/types/engine/helpers.d.mts +37 -92
  181. package/types/engine/homepage.d.mts +2 -3
  182. package/types/engine/index.d.mts +5 -0
  183. package/types/engine/infobox-registry.d.mts +3 -1
  184. package/types/engine/infobox.d.mts +17 -6
  185. package/types/engine/item-compiler.d.mts +4 -4
  186. package/types/engine/item-docs.d.mts +4 -3
  187. package/types/engine/journals.d.mts +10 -7
  188. package/types/engine/map-graphviz.d.mts +1 -1
  189. package/types/engine/markdown-expressions.d.mts +17 -0
  190. package/types/engine/note-dates.d.mts +47 -1
  191. package/types/engine/note-package.d.mts +3 -19
  192. package/types/engine/note-state.d.mts +2 -1
  193. package/types/engine/note-vocabulary.d.mts +9 -0
  194. package/types/engine/number-words.d.mts +4 -0
  195. package/types/engine/office-holders.d.mts +14 -0
  196. package/types/engine/pack-config.d.mts +1 -1
  197. package/types/engine/packages.d.mts +3 -2
  198. package/types/engine/pdf-images.d.mts +38 -0
  199. package/types/engine/populations.d.mts +2 -2
  200. package/types/engine/project-init.d.mts +17 -0
  201. package/types/engine/prose-config.d.mts +1 -1
  202. package/types/engine/prose-lint.d.mts +3 -2
  203. package/types/engine/readability-lint.d.mts +22 -0
  204. package/types/engine/reckoning-markers.d.mts +4 -2
  205. package/types/engine/schema-check.d.mts +3 -3
  206. package/types/engine/site-build.d.mts +38 -28
  207. package/types/engine/site-config.d.mts +2 -2
  208. package/types/engine/site-index.d.mts +4 -0
  209. package/types/engine/site-preview.d.mts +1 -1
  210. package/types/engine/social-ties.d.mts +8 -2
  211. package/types/engine/sql-tables.d.mts +13 -2
  212. package/types/engine/web-wikilinks.d.mts +2 -1
  213. package/types/engine/wikilinks.d.mts +7 -1
  214. package/types/hm3/items.d.mts +5 -6
  215. package/types/labels.d.mts +40 -0
  216. package/types/manifest.d.mts +3 -3
  217. package/types/sohl/actors.d.mts +2 -2
  218. package/types/sohl/being-info.d.mts +3 -43
  219. package/types/sohl/index.d.mts +1 -1
  220. package/types/sohl/infobox.d.mts +5 -2
  221. package/types/sohl/item-fields.d.mts +4 -13
  222. package/types/stage.d.mts +9 -0
  223. package/CONTENT.md +0 -3239
@@ -14,7 +14,7 @@
14
14
  /**
15
15
  * The shared markdownlint rules, as a markdownlint-cli2 options module.
16
16
  *
17
- * `content-build markdown` applies these without a consumer declaring
17
+ * `package-build markdown` applies these without a consumer declaring
18
18
  * anything; this is for an editor's markdownlint extension, and for a consumer
19
19
  * that wants to *extend* the set rather than replace it:
20
20
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "22.15.0",
3
+ "version": "22.16.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",
@@ -95,13 +95,11 @@
95
95
  "./package.json": "./package.json"
96
96
  },
97
97
  "bin": {
98
- "package-build": "./bin/package-build.mjs",
99
- "content-build": "./bin/content-build.mjs"
98
+ "package-build": "./bin/package-build.mjs"
100
99
  },
101
100
  "files": [
102
101
  "CHANGELOG-content-build.md",
103
102
  "CHANGELOG.md",
104
- "CONTENT.md",
105
103
  "README.md",
106
104
  "assets",
107
105
  "packagebuild-metadata.jsonl",
@@ -149,18 +147,30 @@
149
147
  "loglevel": "^1.9.2",
150
148
  "loglevel-plugin-prefix": "^0.8.4",
151
149
  "markdown-it": "^15.0.0",
150
+ "markdown-it-deflist": "^4.0.0",
151
+ "markdown-it-footnote": "^4.0.0",
152
152
  "markdownlint-cli2": "^0.23.3",
153
153
  "pagefind": "^1.5.2",
154
154
  "prettier": "^3.9.6",
155
+ "remark-frontmatter": "^5.0.0",
156
+ "remark-parse": "^11.0.0",
157
+ "remark-retext": "^6.0.1",
158
+ "retext-english": "^5.0.0",
159
+ "retext-readability": "^8.0.0",
160
+ "retext-simplify": "^8.0.0",
161
+ "retext-stringify": "^4.0.0",
162
+ "sharp": "^0.35.4",
155
163
  "smol-toml": "^1.8.0",
156
164
  "ssh2-sftp-client": "^12.1.1",
157
165
  "typescript": "^6.0.3",
158
166
  "unidecode": "^1.1.0",
167
+ "unified": "^11.0.5",
159
168
  "yaml": "^2.8.1",
160
169
  "yargs": "^18.1.0"
161
170
  },
162
171
  "devDependencies": {
163
172
  "@changesets/cli": "^3.0.0",
173
+ "@heroiclands/hugo-theme": "^0.9.0",
164
174
  "@types/node": "^26.2.0",
165
175
  "npm-run-all": "^4.1.5",
166
176
  "vitest": "^5.0.0"
@@ -174,16 +184,26 @@
174
184
  "format": "prettier --write .",
175
185
  "format:check": "prettier --check .",
176
186
  "lint": "run-p --aggregate-output -c --max-parallel 4 format:check lint:markdown lint:yaml lint:labels lint:content-format",
177
- "lint:markdown": "node bin/content-build.mjs markdown",
187
+ "lint:markdown": "node bin/package-build.mjs markdown",
178
188
  "lint:yaml": "node bin/package-build.mjs yaml",
179
189
  "lint:labels": "node bin/package-build.mjs labels check",
180
- "lint:markdown:fix": "node bin/content-build.mjs markdown --fix",
181
- "lint:content-format": "npm run lint:content-format:schema && npm run lint:content-format:fields",
182
- "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",
183
- "lint:content-format:fields": "node bin/content-build.mjs content-format fields --fields sohl && node bin/content-build.mjs content-format fields --fields hm3",
190
+ "lint:markdown:fix": "node bin/package-build.mjs markdown --fix",
191
+ "lint:content-format": "npm run lint:content-format:schema && npm run lint:content-format:fields && node ci/content-format-reference.mjs --check",
192
+ "lint:content-format:schema": "node bin/package-build.mjs content-format schema --schema sohl=tests/fixtures/content-format/schema-sohl.json --schema hm3=tests/fixtures/content-format/schema-hm3.json",
193
+ "lint:content-format:fields": "node bin/package-build.mjs content-format fields --fields sohl && node bin/package-build.mjs content-format fields --fields hm3",
184
194
  "changeset": "changeset",
185
195
  "changeset:version": "changeset version && node bin/package-build.mjs changelog group && npm install --package-lock-only",
186
- "prepare": "git config core.hooksPath .githooks || true"
196
+ "prepare": "git config core.hooksPath .githooks || true",
197
+ "build:deps": "node bin/package-build.mjs deps fetch",
198
+ "build:content-index": "node bin/package-build.mjs content-index",
199
+ "build:site-content": "node bin/package-build.mjs site",
200
+ "build:site-html": "hugo --source build/hugo --minify --gc --cleanDestinationDir",
201
+ "build:site-root": "node bin/package-build.mjs site-root",
202
+ "build:site": "run-s build:deps build:content-index build:site-content build:site-html build:site-root",
203
+ "build:book": "run-s build:deps build:content-index build:book-content",
204
+ "build:book-content": "node bin/package-build.mjs pdf",
205
+ "serve:site": "run-s build:deps build:content-index build:site-content serve:site-html",
206
+ "serve:site-html": "hugo server --source build/hugo"
187
207
  },
188
208
  "engines": {
189
209
  "node": ">=24.0.0"
@@ -204,5 +224,6 @@
204
224
  "heroiclands",
205
225
  "sohl",
206
226
  "song-of-heroic-lands"
207
- ]
227
+ ],
228
+ "homepage": "https://github.com/HeroicLands/package-build#documentation"
208
229
  }
@@ -14,7 +14,7 @@
14
14
  /**
15
15
  * The shared Prettier configuration, as a Prettier config module.
16
16
  *
17
- * `content-build format` already applies these rules without a consumer
17
+ * `package-build format` already applies these rules without a consumer
18
18
  * declaring anything, so this exists for the **editor**: a format-on-save that
19
19
  * disagrees with the lint chain is a papercut every contributor hits, and
20
20
  * Prettier's editor integrations read a config file rather than asking this
@@ -11,42 +11,10 @@
11
11
  * SPDX-License-Identifier: GPL-3.0-or-later
12
12
  */
13
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 legacy spellings of `being`, so it had matched
28
- * nothing since the merge and all 95 of its being pages published with empty
29
- * sidebar sections. 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
- // The retirement window for a renamed note type: an embedded reference
40
- // still spelling `armorgear` names the same gear group as `armor`.
41
- import { currentType } from "../engine/ids.mjs";
14
+ /** Shared being type and gear groups used by the SoHL infobox. @module */
42
15
 
43
16
  /**
44
- * The note `type` whose pages carry a being info block.
45
- *
46
- * One name: `character` and `creature` are the `being` they
47
- * had always compiled into. The retired names are deliberately **not** accepted
48
- * as aliases: they throw elsewhere in the system, and tolerating them here
49
- * would hide the next drift of this kind rather than surface it.
17
+ * The note type for a being.
50
18
  */
51
19
  export const BEING_TYPE = "being";
52
20
 
@@ -61,16 +29,7 @@ export function isBeing(fm) {
61
29
  }
62
30
 
63
31
  /**
64
- * The sidebar group each gear item type is displayed under.
65
- *
66
- * Presentation naming, not data-model naming: the note type says `weapongear`,
67
- * the sidebar heading says "weapons". Kept as one table so a new gear type is
68
- * added in a single place rather than in each consumer's site build.
69
- *
70
- * Keyed by **note** type, which is what a being's embedded `(type, shortcode)`
71
- * references spell — and three of those are not the document
72
- * subtype they compile into. A reference still on a renamed spelling is
73
- * normalised at the lookup below rather than given a second row here.
32
+ * The infobox group displayed for each gear note type.
74
33
  *
75
34
  * @type {Readonly<Record<string, string>>}
76
35
  */
@@ -82,104 +41,3 @@ export const GEAR_TYPE_TO_KEY = Object.freeze({
82
41
  containergear: "containers",
83
42
  concoctiongear: "concoctions",
84
43
  });
85
-
86
- /** Whether a value is a plain mapping. */
87
- const isMap = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
88
-
89
- /** Whether a value is a non-empty array. */
90
- const nonEmpty = (v) => Array.isArray(v) && v.length > 0;
91
-
92
- /**
93
- * Derive a being's info-block fields from its raw `sohl.items[]`.
94
- *
95
- * Each item's `shortcode` is resolved against `index` — keyed
96
- * `"<type>:<shortcode>"` — for a display name and a link to the item's own
97
- * page. `attributes` already match the sidebar shape and pass through
98
- * untouched.
99
- *
100
- * **Authored values win.** Only fields the author did not supply are derived,
101
- * so a note that hand-writes `sohl.skills` keeps exactly what it wrote. An
102
- * item's inline `name` beats the index, and an unresolved shortcode falls back
103
- * to *itself* rather than being dropped — a page that names an item the index
104
- * has not heard of is better than a page silently missing a row.
105
- *
106
- * Returns a new object; the input is not mutated.
107
- *
108
- * @param {object|null|undefined} sohl - The note's `sohl` frontmatter block.
109
- * @param {Map<string, {name?: string, url?: string}>} index - Content index,
110
- * `"<type>:<shortcode>"` → the item's page.
111
- * @returns {object|null} The block with its info-block fields filled in, or the
112
- * input unchanged when there is nothing to derive from — with an absent block
113
- * reported as `null`, the value an empty one already carries.
114
- */
115
- export function deriveBeingInfo(sohl, index) {
116
- // A note declaring no `sohl:` key at all arrives as `undefined`, and the
117
- // site emitter assigns this result straight into a page's front matter.
118
- // js-yaml refuses to dump a property whose value is `undefined`, and the
119
- // throw aborts the whole build rather than the one page — so "no block"
120
- // is answered with the same `null` an empty block gets.
121
- if (!isMap(sohl)) return sohl ?? null;
122
- const out = { ...sohl };
123
- const items = Array.isArray(out.items) ? out.items : [];
124
- if (items.length === 0) return out;
125
-
126
- const lookup = (type, shortcode) => (shortcode ? index.get(`${type}:${shortcode}`) : undefined);
127
-
128
- /** An item's display name: its own, then the index's, then its shortcode. */
129
- const displayName = (it, ref, shortcode) =>
130
- (typeof it.name === "string" && it.name) || ref?.name || shortcode;
131
-
132
- // Skills: { shortcode: masteryLevelBase }.
133
- if (!(isMap(out.skills) && Object.keys(out.skills).length > 0)) {
134
- const skills = {};
135
- for (const it of items) {
136
- if (!isMap(it) || it.type !== "skill") continue;
137
- const level = it.system?.masteryLevelBase;
138
- if (typeof it.shortcode === "string" && typeof level === "number") {
139
- skills[it.shortcode] = level;
140
- }
141
- }
142
- if (Object.keys(skills).length > 0) out.skills = skills;
143
- }
144
-
145
- // Gear: { weapons: [{ name, shortcode?, url? }], armor: [...], … }.
146
- if (!isMap(out.gear)) {
147
- const gear = {};
148
- for (const it of items) {
149
- if (!isMap(it)) continue;
150
- const key = GEAR_TYPE_TO_KEY[currentType(it.type)];
151
- if (!key) continue;
152
- const shortcode = typeof it.shortcode === "string" ? it.shortcode : undefined;
153
- const ref = lookup(it.type, shortcode);
154
- const name = displayName(it, ref, shortcode);
155
- if (!name) continue;
156
- const entry = { name };
157
- if (shortcode) entry.shortcode = shortcode;
158
- if (ref?.url) entry.url = ref.url;
159
- (gear[key] ??= []).push(entry);
160
- }
161
- if (Object.keys(gear).length > 0) out.gear = gear;
162
- }
163
-
164
- // Mystical abilities, split by subType into spells / talents.
165
- const spells = [];
166
- const talents = [];
167
- for (const it of items) {
168
- if (!isMap(it) || it.type !== "mysticalability") continue;
169
- const shortcode = typeof it.shortcode === "string" ? it.shortcode : undefined;
170
- const ref = lookup("mysticalability", shortcode);
171
- // No shortcode fallback here: an ability with neither an inline name
172
- // nor an index entry has nothing to show, and a row reading like a
173
- // shortcode is worse than no row.
174
- const name = (typeof it.name === "string" && it.name) || ref?.name;
175
- if (!name) continue;
176
- const entry = { name };
177
- if (ref?.url) entry.url = ref.url;
178
- if (it.subType === "arcaneincantation") spells.push(entry);
179
- else if (it.subType === "arcanetalent") talents.push(entry);
180
- }
181
- if (spells.length > 0 && !nonEmpty(out.spells)) out.spells = spells;
182
- if (talents.length > 0 && !nonEmpty(out.talents)) out.talents = talents;
183
-
184
- return out;
185
- }
package/sohl/index.mjs CHANGED
@@ -53,4 +53,4 @@ export * as infobox from "./infobox.mjs";
53
53
  // compilers arrived.
54
54
  export { DEFAULT_ITEM_ART, defaultItemArt } from "./default-item-art.mjs";
55
55
  export { AFFILIATION_STANDINGS } from "./affiliation-standings.mjs";
56
- export { BEING_TYPE, GEAR_TYPE_TO_KEY, deriveBeingInfo, isBeing } from "./being-info.mjs";
56
+ export { BEING_TYPE, GEAR_TYPE_TO_KEY, isBeing } from "./being-info.mjs";
package/sohl/infobox.mjs CHANGED
@@ -148,6 +148,7 @@ export const SOHL_FIELD_PRESENTATION = Object.freeze({
148
148
  improveFlag: Object.freeze({
149
149
  withheld: "character-sheet machinery — whether the item is flagged for improvement",
150
150
  }),
151
+ "trauma.treatmentModifierBase": Object.freeze({ label: "Treatment modifier" }),
151
152
  facing: Object.freeze({
152
153
  withheld: "a body-location layout, which has no summary shape",
153
154
  }),
@@ -312,7 +313,7 @@ function itemValue(item, resolve, block) {
312
313
  * sparse creature's box short rather than mostly empty.
313
314
  *
314
315
  * @param {object} fm - The note's frontmatter.
315
- * @param {object} ctx - `{ block, resolve }`.
316
+ * @param {{block: string, resolve?: Function}} ctx - Rendering context.
316
317
  * @returns {object[]} The sections.
317
318
  */
318
319
  export function beingSections(fm, { block, resolve }) {
@@ -49,31 +49,8 @@
49
49
  import { defaultItemArt } from "./default-item-art.mjs";
50
50
  import { documentSubtype } from "../engine/document-subtypes.mjs";
51
51
  import { SOHL_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
52
- import { buildFromFields, readField } from "../engine/field-spec.mjs";
53
- import { COMBAT_TECHNIQUE_STRIKE_MODE, ITEM_FIELDS } from "./item-fields.mjs";
54
-
55
- /**
56
- * The conditionals a flat field list cannot state, keyed by item type.
57
- *
58
- * A finalizer runs after the declaration has produced the `system` block, and
59
- * exists only for a field whose *presence* depends on another field's value.
60
- * There is exactly one: a combat technique is authored as a `skill` of subtype
61
- * `combattechnique`, and carries a strike mode that no other skill has.
62
- *
63
- * Keeping this list short is the point. Anything expressible as a field
64
- * declaration is said there, where a generator and a linter can read it; a
65
- * finalizer is opaque again, so it earns its place one conditional at a time.
66
- *
67
- * @type {Readonly<Record<string, (fm: object, out: object) => object>>}
68
- */
69
- const FINALIZERS = Object.freeze({
70
- skill(fm, out) {
71
- if (out.subType === "combattechnique") {
72
- out.strikeMode = readField(COMBAT_TECHNIQUE_STRIKE_MODE, fm);
73
- }
74
- return out;
75
- },
76
- });
52
+ import { buildFromFields } from "../engine/field-spec.mjs";
53
+ import { ITEM_FIELDS } from "./item-fields.mjs";
77
54
 
78
55
  /**
79
56
  * Build one registry entry: the declared fields, the builder they generate, and
@@ -102,9 +79,8 @@ const FINALIZERS = Object.freeze({
102
79
  function entryFor(type) {
103
80
  const fields = ITEM_FIELDS[type];
104
81
  const build = buildFromFields(fields);
105
- const finalize = FINALIZERS[type];
106
82
  return Object.freeze({
107
- system: finalize ? (fm) => finalize(fm, build(fm)) : build,
83
+ system: build,
108
84
  img: defaultItemArt(documentSubtype(SOHL_DOCUMENT_SUBTYPES, type, {}) ?? type),
109
85
  fields,
110
86
  });
@@ -336,6 +336,18 @@ const GEAR_COMMON = Object.freeze([
336
336
  /* Per-type declarations */
337
337
  /* --------------------------------------------------------------------- */
338
338
 
339
+ /** The strike mode required for combat techniques and omitted for other skills.
340
+ * @type {import("../engine/field-spec.mjs").FieldSpec}
341
+ */
342
+ export const COMBAT_TECHNIQUE_STRIKE_MODE = Object.freeze({
343
+ name: "strikeMode",
344
+ to: "strikeMode",
345
+ ...STRIKE_MODE,
346
+ required: true,
347
+ when: (_fm, out) => out.subType === "combattechnique",
348
+ describe: "The strike mode the combat technique trains. Required for combat techniques only.",
349
+ });
350
+
339
351
  /**
340
352
  * Every item type's frontmatter vocabulary, in the order the `system` block
341
353
  * emits it.
@@ -513,6 +525,21 @@ export const ITEM_FIELDS = Object.freeze({
513
525
  describe:
514
526
  "Days from contracting to onset, rolled by the receiving actor. Unset means no incubation.",
515
527
  },
528
+ {
529
+ name: "onsetMacroUuid",
530
+ to: "onsetMacroUuid",
531
+ ...BLANK_IS_NULL,
532
+ default: null,
533
+ describe: "UUID of the macro run when symptoms begin, if any.",
534
+ },
535
+ {
536
+ name: "data.outcomeTraumas",
537
+ legacyKey: "outcomeTraumas",
538
+ to: "outcomeTraumas",
539
+ ...BLANK_IS_NULL,
540
+ default: null,
541
+ describe: "SafeExpression selecting traumas caused by the affliction's outcome.",
542
+ },
516
543
  {
517
544
  name: "data.outcome",
518
545
  legacyKey: "outcome",
@@ -630,6 +657,11 @@ export const ITEM_FIELDS = Object.freeze({
630
657
 
631
658
  armorgear: Object.freeze([
632
659
  ...GEAR_COMMON,
660
+ {
661
+ to: "isWorn",
662
+ runtimeOnly: "whether the armor is currently worn by an actor",
663
+ describe: "Worn state set during play, never authored.",
664
+ },
633
665
  {
634
666
  name: "material",
635
667
  to: "material",
@@ -973,6 +1005,13 @@ export const ITEM_FIELDS = Object.freeze({
973
1005
  default: "",
974
1006
  describe: "Shortcode of the skill this one specialises, for a specialisation.",
975
1007
  },
1008
+ {
1009
+ name: "adoptParentMasteryLevel",
1010
+ to: "adoptParentMasteryLevel",
1011
+ ...BOOLEAN,
1012
+ default: false,
1013
+ describe: "Whether a specialization uses its parent's mastery level.",
1014
+ },
976
1015
  {
977
1016
  name: "initSkillMult",
978
1017
  to: "initSkillMult",
@@ -987,6 +1026,7 @@ export const ITEM_FIELDS = Object.freeze({
987
1026
  default: [],
988
1027
  describe: "Body-part roles whose impairment penalises tests against this skill.",
989
1028
  },
1029
+ COMBAT_TECHNIQUE_STRIKE_MODE,
990
1030
  ]),
991
1031
 
992
1032
  trauma: Object.freeze([
@@ -1018,6 +1058,27 @@ export const ITEM_FIELDS = Object.freeze({
1018
1058
  default: 0,
1019
1059
  describe: "How readily it heals.",
1020
1060
  },
1061
+ {
1062
+ name: "treatmentModifierBase",
1063
+ to: "treatmentModifierBase",
1064
+ ...NULLABLE_NUMBER,
1065
+ default: null,
1066
+ describe: "Modifier to treatment tests; unset means no modifier.",
1067
+ },
1068
+ {
1069
+ name: "permanentImpairmentEligible",
1070
+ to: "permanentImpairmentEligible",
1071
+ ...BOOLEAN,
1072
+ default: false,
1073
+ describe: "Whether healing may leave a permanent impairment.",
1074
+ },
1075
+ {
1076
+ name: "infectable",
1077
+ to: "infectable",
1078
+ ...BOOLEAN,
1079
+ default: false,
1080
+ describe: "Whether a failed healing test can cause infection.",
1081
+ },
1021
1082
  {
1022
1083
  name: "aspect",
1023
1084
  to: "aspect",
@@ -1136,25 +1197,3 @@ export const ITEM_FIELDS = Object.freeze({
1136
1197
  },
1137
1198
  ]),
1138
1199
  });
1139
-
1140
- /**
1141
- * The one conditional field in the vocabulary: a combat technique's strike
1142
- * mode.
1143
- *
1144
- * A combat technique is authored as a `skill` of subtype `combattechnique` —
1145
- * the standalone item type was merged into Skill — and carries an embedded,
1146
- * discriminated strike mode. It is mandatory for that subtype and absent from
1147
- * every other skill, which is a conditional a flat field list cannot state, so
1148
- * it is applied after the declaration runs.
1149
- *
1150
- * @type {import("../engine/field-spec.mjs").FieldSpec}
1151
- */
1152
- export const COMBAT_TECHNIQUE_STRIKE_MODE = Object.freeze({
1153
- name: "strikeMode",
1154
- to: "strikeMode",
1155
- ...STRIKE_MODE,
1156
- required: true,
1157
- default: null,
1158
- describe:
1159
- "The strike mode the technique trains. Required on a `combattechnique` skill, and set on no other.",
1160
- });
@@ -14,7 +14,7 @@
14
14
  /**
15
15
  * The `sohl` knowledgebase's own body pass.
16
16
  *
17
- * `content-build site` publishes a content tree as a website, and almost all of
17
+ * `package-build site` publishes a content tree as a website, and almost all of
18
18
  * that job is the same for every package. This rewrite is not: it is driven by
19
19
  * a TypeDoc symbol map only this package has, and it is ruled explicitly
20
20
  * per-consumer.
@@ -60,7 +60,7 @@ import log from "loglevel";
60
60
  *
61
61
  * The path is resolved against the **repository root**, never the process cwd.
62
62
  * `site.passOptions.symbolMap` is authored repo-relative, so a cwd-relative
63
- * read misses the moment `content-build site` is driven from anywhere but the
63
+ * read misses the moment `package-build site` is driven from anywhere but the
64
64
  * repository root — which is exactly how the end-to-end verification, running
65
65
  * through `PACKAGE_BUILD_CONFIG` from outside the tree, found this.
66
66
  *
@@ -295,14 +295,6 @@ const PRESENTATION_FIELDS = Object.freeze({
295
295
  describe: "Descriptive traits, read by the publishing sites.",
296
296
  },
297
297
  ]),
298
- skill: Object.freeze([
299
- {
300
- name: "strikeMode",
301
- ...AS_AUTHORED,
302
- describe:
303
- "The strike mode a combat technique declares. Compiled, but applied by the builder rather than listed in the type's fields.",
304
- },
305
- ]),
306
298
  being: Object.freeze([
307
299
  {
308
300
  name: "attrRollFormula",
package/stage.mjs CHANGED
@@ -166,3 +166,22 @@ export function cleanBuildArtifacts(root, { extra = [], includeNodeModules = fal
166
166
  }
167
167
  return removed;
168
168
  }
169
+
170
+ /**
171
+ * Remove the configured Foundry stage before a complete build assembles it.
172
+ * Other build outputs, including the content index, remain available.
173
+ *
174
+ * @param {string} root - Repository root.
175
+ * @param {string} stageDir - Configured stage directory, relative to root.
176
+ * @returns {boolean} Whether the stage existed.
177
+ */
178
+ export function resetStage(root, stageDir) {
179
+ const projectRoot = path.resolve(root);
180
+ const target = path.resolve(projectRoot, stageDir);
181
+ if (!target.startsWith(`${projectRoot}${path.sep}`)) {
182
+ throw new Error("The stage directory must be inside the project root.");
183
+ }
184
+ if (!fs.existsSync(target)) return false;
185
+ fs.rmSync(target, { recursive: true, force: true });
186
+ return true;
187
+ }
@@ -68,7 +68,7 @@ export function resolvePackageBuildConfig(shared: object): Readonly<PackageBuild
68
68
  /**
69
69
  * The repository's resolved package-build configuration.
70
70
  *
71
- * Read on call rather than at import, exactly as content-build resolves its
71
+ * Read on call rather than at import, exactly as package-build resolves its
72
72
  * own: importing a module of this package must not require a configuration to
73
73
  * exist anywhere above it.
74
74
  *
@@ -93,7 +93,8 @@ export const DERIVED_MANIFEST_KEYS: Readonly<Record<string, string>>;
93
93
  * The resolved `packageBuild` section, every optional half filled in.
94
94
  *
95
95
  * @typedef {object} PackageBuildConfig
96
- * @property {string} rootDir The repository root, from content-build.
96
+ * @property {string} rootDir The repository root, from package-build.
97
+ * @property {{age: number, threshold: number, minWords: number}} proseLint Optional prose analysis settings.
97
98
  * @property {string} packageKind `systems` or `modules`.
98
99
  * @property {string} packageId The Foundry package id.
99
100
  * @property {string} artifact Derived: `system` or `module`.
@@ -193,9 +194,17 @@ export type AssetSpec = {
193
194
  */
194
195
  export type PackageBuildConfig = {
195
196
  /**
196
- * The repository root, from content-build.
197
+ * The repository root, from package-build.
197
198
  */
198
199
  rootDir: string;
200
+ /**
201
+ * Optional prose analysis settings.
202
+ */
203
+ proseLint: {
204
+ age: number;
205
+ threshold: number;
206
+ minWords: number;
207
+ };
199
208
  /**
200
209
  * `systems` or `modules`.
201
210
  */