@heroiclands/package-build 23.1.0 → 23.2.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 (248) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/bin/content-commands.mjs +68 -149
  3. package/bin/package-build.mjs +56 -10
  4. package/ci/check-banners.mjs +42 -6
  5. package/ci/confirm-npm-version.mjs +135 -0
  6. package/ci/content-format-reference.mjs +14 -5
  7. package/config.mjs +1 -0
  8. package/container.mjs +185 -4
  9. package/content-config.mjs +333 -210
  10. package/deploy.mjs +116 -10
  11. package/docs/api.md +238 -246
  12. package/docs/authoring/assets.md +11 -5
  13. package/docs/authoring/frontmatter.md +1 -1
  14. package/docs/authoring/links-and-markup.md +87 -0
  15. package/docs/commands.md +138 -99
  16. package/docs/configuration.md +277 -125
  17. package/docs/content-format.md +2 -0
  18. package/docs/diagnostics.md +48 -13
  19. package/docs/guides/book.md +341 -21
  20. package/docs/guides/e2e.md +13 -2
  21. package/docs/guides/icons.md +14 -4
  22. package/docs/guides/module-project.md +18 -8
  23. package/docs/guides/release-and-deploy.md +54 -6
  24. package/docs/guides/site.md +5 -2
  25. package/docs/project-setup.md +30 -10
  26. package/docs/reference/format-details.md +373 -170
  27. package/docs/reference/note-types.md +128 -109
  28. package/docs/reference/theme-pages.md +31 -6
  29. package/e2e.mjs +140 -28
  30. package/engine/actor-compiler.mjs +22 -31
  31. package/engine/address-charset.mjs +4 -17
  32. package/engine/address-fields.mjs +1 -1
  33. package/engine/address-render.mjs +12 -1
  34. package/engine/address-values.mjs +6 -0
  35. package/engine/address.mjs +46 -33
  36. package/engine/art-fields.mjs +30 -5
  37. package/engine/art-slots.mjs +10 -1
  38. package/engine/asset-bindings.mjs +12 -1
  39. package/engine/base-compiler.mjs +34 -65
  40. package/engine/being-age.mjs +2 -7
  41. package/engine/being-measurements.mjs +12 -1
  42. package/engine/calendar-artifacts.mjs +6 -0
  43. package/engine/calendar-choice.mjs +12 -1
  44. package/engine/calendar-format.mjs +12 -1
  45. package/engine/calendar-human.mjs +12 -1
  46. package/engine/compile-corpus.mjs +3 -1
  47. package/engine/content-address.mjs +9 -12
  48. package/engine/content-alerts.mjs +13 -1
  49. package/engine/content-blocks.mjs +14 -3
  50. package/engine/content-embeds.mjs +11 -3
  51. package/engine/content-figures.mjs +9 -0
  52. package/engine/content-footnotes.mjs +12 -1
  53. package/engine/content-format-check.mjs +11 -262
  54. package/engine/content-format.yaml +5 -2
  55. package/engine/content-images.mjs +3 -0
  56. package/engine/content-index.mjs +443 -20
  57. package/engine/content-links.mjs +29 -35
  58. package/engine/content-lint.mjs +2 -3
  59. package/engine/content-poetry.mjs +12 -1
  60. package/engine/content-slug.mjs +2 -14
  61. package/engine/content-spans.mjs +13 -1
  62. package/engine/content-tables.mjs +12 -1
  63. package/engine/culture-choice.mjs +210 -13
  64. package/engine/data-keys.mjs +19 -16
  65. package/engine/date-conversion.mjs +12 -1
  66. package/engine/declaration-check.mjs +9 -0
  67. package/engine/derived-fields.mjs +6 -7
  68. package/engine/diagnostics.mjs +34 -4
  69. package/engine/docs-checks.mjs +9 -0
  70. package/engine/document-subtypes.mjs +47 -34
  71. package/engine/event-views.mjs +139 -14
  72. package/engine/exported-scene.mjs +92 -28
  73. package/engine/extension-attributes.mjs +12 -1
  74. package/engine/field-spec.mjs +11 -102
  75. package/engine/folder-notes.mjs +0 -64
  76. package/engine/foreign-catalog.mjs +2 -1
  77. package/engine/format-generated.mjs +9 -0
  78. package/engine/foundry-entries.mjs +10 -28
  79. package/engine/frontmatter-lint.mjs +83 -392
  80. package/engine/frontmatter.mjs +20 -70
  81. package/engine/generate.mjs +10 -15
  82. package/engine/heading-attributes.mjs +12 -1
  83. package/engine/helpers.mjs +41 -102
  84. package/engine/holdings.mjs +1 -1
  85. package/engine/homepage.mjs +3 -10
  86. package/engine/ids.mjs +1 -167
  87. package/engine/index-records.mjs +6 -4
  88. package/engine/index.mjs +0 -3
  89. package/engine/infobox-render.mjs +15 -3
  90. package/engine/infobox.mjs +13 -14
  91. package/engine/item-compiler.mjs +4 -10
  92. package/engine/item-docs.mjs +3 -5
  93. package/engine/item-registry.mjs +2 -10
  94. package/engine/journals.mjs +1 -5
  95. package/engine/literature-notes.mjs +4 -12
  96. package/engine/macros.mjs +1 -2
  97. package/engine/map-graphviz.mjs +12 -1
  98. package/engine/markdown-expressions.mjs +12 -1
  99. package/engine/markdownlint-run.mjs +567 -0
  100. package/engine/metadata-index.mjs +42 -12
  101. package/engine/note-addresses.mjs +143 -35
  102. package/engine/note-claims.mjs +14 -28
  103. package/engine/note-dates.mjs +3 -3
  104. package/engine/note-frontmatter.mjs +6 -0
  105. package/engine/note-ids.mjs +8 -14
  106. package/engine/note-package.mjs +8 -25
  107. package/engine/note-state.mjs +1 -3
  108. package/engine/note-vocabulary.mjs +148 -84
  109. package/engine/number-words.mjs +12 -1
  110. package/engine/pack-config.mjs +15 -25
  111. package/engine/pack-router.mjs +10 -16
  112. package/engine/pathnames.mjs +8 -22
  113. package/engine/pdf-build.mjs +773 -121
  114. package/engine/pdf-coverage.mjs +188 -0
  115. package/engine/pdf-index.mjs +54 -7
  116. package/engine/pdf-pages.mjs +247 -0
  117. package/engine/pdf-render.mjs +114 -4
  118. package/engine/pdf-toc.mjs +29 -32
  119. package/engine/project-init.mjs +46 -6
  120. package/engine/prose-config.mjs +1 -1
  121. package/engine/prose-lint.mjs +52 -88
  122. package/engine/readability-lint.mjs +9 -0
  123. package/engine/reckoning-markers.mjs +12 -1
  124. package/engine/reference-targets.mjs +7 -0
  125. package/engine/runtime-only-fields.mjs +5 -8
  126. package/engine/scene-levels.mjs +2 -2
  127. package/engine/scenes.mjs +1 -1
  128. package/engine/shortcode-references.mjs +7 -0
  129. package/engine/site-build.mjs +30 -16
  130. package/engine/site-index.mjs +10 -16
  131. package/engine/site-preview.mjs +6 -0
  132. package/engine/site-root.mjs +8 -1
  133. package/engine/social-tie-terms.mjs +12 -1
  134. package/engine/social-ties.mjs +3 -0
  135. package/engine/sql-tables.mjs +56 -20
  136. package/engine/standing-terms.mjs +11 -1
  137. package/engine/standings.mjs +11 -27
  138. package/engine/system-block.mjs +39 -141
  139. package/engine/system-vocabulary.mjs +6 -0
  140. package/engine/walk-files.mjs +69 -0
  141. package/engine/web-wikilinks.mjs +23 -3
  142. package/engine/wikilink-syntax.mjs +32 -21
  143. package/engine/wikilinks.mjs +27 -16
  144. package/engine/yaml-lint.mjs +7 -4
  145. package/githooks/attribution.mjs +8 -0
  146. package/githooks/commit-message.mjs +9 -0
  147. package/hm3/actors.mjs +9 -48
  148. package/hm3/default-item-art.mjs +9 -9
  149. package/hm3/item-fields.mjs +2 -9
  150. package/hm3/template-priority.mjs +6 -7
  151. package/hugo-theme/data/banner-kinds.yaml +78 -0
  152. package/hugo-theme/data/banners.yaml +2 -3
  153. package/hugo-theme/layouts/404.html +3 -3
  154. package/hugo-theme/layouts/partials/banner-url.html +31 -7
  155. package/hugo-theme/layouts/partials/hero-banner.html +13 -14
  156. package/hugo-theme/layouts/partials/related.html +1 -1
  157. package/hugo-theme/layouts/partials/warn-missing-banner.html +1 -1
  158. package/hugo-theme/static/css/style.css +1 -1
  159. package/manifest.mjs +11 -4
  160. package/markdownlint-config.mjs +1 -1
  161. package/package.json +6 -3
  162. package/release.mjs +83 -18
  163. package/sohl/actors.mjs +16 -27
  164. package/sohl/default-item-art.mjs +3 -3
  165. package/sohl/document-subtypes.mjs +1 -16
  166. package/sohl/infobox.mjs +2 -3
  167. package/sohl/item-builders.mjs +5 -8
  168. package/sohl/item-fields.mjs +5 -35
  169. package/sohl/items.mjs +0 -4
  170. package/sohl/kb-passes.mjs +3 -3
  171. package/types/container.d.mts +116 -5
  172. package/types/content-config.d.mts +83 -35
  173. package/types/deploy.d.mts +42 -6
  174. package/types/e2e.d.mts +35 -3
  175. package/types/engine/address-charset.d.mts +4 -17
  176. package/types/engine/address.d.mts +17 -8
  177. package/types/engine/art-fields.d.mts +7 -4
  178. package/types/engine/art-slots.d.mts +6 -0
  179. package/types/engine/base-compiler.d.mts +15 -17
  180. package/types/engine/content-address.d.mts +9 -12
  181. package/types/engine/content-embeds.d.mts +4 -2
  182. package/types/engine/content-format-check.d.mts +0 -64
  183. package/types/engine/content-index.d.mts +82 -3
  184. package/types/engine/content-links.d.mts +2 -4
  185. package/types/engine/culture-choice.d.mts +51 -8
  186. package/types/engine/data-keys.d.mts +2 -2
  187. package/types/engine/derived-fields.d.mts +1 -2
  188. package/types/engine/diagnostics.d.mts +27 -4
  189. package/types/engine/document-subtypes.d.mts +12 -31
  190. package/types/engine/event-views.d.mts +14 -2
  191. package/types/engine/exported-scene.d.mts +3 -2
  192. package/types/engine/field-spec.d.mts +13 -122
  193. package/types/engine/folder-notes.d.mts +0 -39
  194. package/types/engine/foreign-catalog.d.mts +2 -1
  195. package/types/engine/foundry-entries.d.mts +4 -5
  196. package/types/engine/frontmatter-lint.d.mts +2 -2
  197. package/types/engine/frontmatter.d.mts +19 -45
  198. package/types/engine/generate.d.mts +1 -1
  199. package/types/engine/helpers.d.mts +25 -42
  200. package/types/engine/homepage.d.mts +3 -10
  201. package/types/engine/ids.d.mts +0 -114
  202. package/types/engine/index-records.d.mts +6 -4
  203. package/types/engine/index.d.mts +0 -1
  204. package/types/engine/infobox-render.d.mts +7 -1
  205. package/types/engine/infobox.d.mts +3 -2
  206. package/types/engine/item-docs.d.mts +1 -1
  207. package/types/engine/item-registry.d.mts +2 -4
  208. package/types/engine/literature-notes.d.mts +1 -1
  209. package/types/engine/markdownlint-run.d.mts +40 -0
  210. package/types/engine/metadata-index.d.mts +9 -1
  211. package/types/engine/note-addresses.d.mts +27 -2
  212. package/types/engine/note-claims.d.mts +3 -5
  213. package/types/engine/note-ids.d.mts +2 -4
  214. package/types/engine/note-package.d.mts +1 -1
  215. package/types/engine/note-state.d.mts +1 -3
  216. package/types/engine/note-vocabulary.d.mts +28 -21
  217. package/types/engine/pack-config.d.mts +2 -6
  218. package/types/engine/pack-router.d.mts +4 -4
  219. package/types/engine/pathnames.d.mts +3 -3
  220. package/types/engine/pdf-build.d.mts +98 -12
  221. package/types/engine/pdf-coverage.d.mts +60 -0
  222. package/types/engine/pdf-index.d.mts +34 -5
  223. package/types/engine/pdf-pages.d.mts +143 -0
  224. package/types/engine/pdf-render.d.mts +45 -1
  225. package/types/engine/pdf-toc.d.mts +12 -14
  226. package/types/engine/prose-config.d.mts +1 -1
  227. package/types/engine/prose-lint.d.mts +11 -10
  228. package/types/engine/runtime-only-fields.d.mts +2 -2
  229. package/types/engine/site-build.d.mts +9 -5
  230. package/types/engine/site-index.d.mts +4 -5
  231. package/types/engine/site-root.d.mts +1 -1
  232. package/types/engine/sql-tables.d.mts +31 -2
  233. package/types/engine/standing-terms.d.mts +2 -1
  234. package/types/engine/standings.d.mts +7 -9
  235. package/types/engine/system-block.d.mts +10 -44
  236. package/types/engine/walk-files.d.mts +15 -0
  237. package/types/engine/wikilink-syntax.d.mts +18 -19
  238. package/types/engine/yaml-lint.d.mts +5 -4
  239. package/types/hm3/actors.d.mts +1 -2
  240. package/types/hm3/item-fields.d.mts +2 -4
  241. package/types/hm3/template-priority.d.mts +6 -7
  242. package/types/manifest.d.mts +3 -2
  243. package/types/release.d.mts +12 -7
  244. package/types/sohl/actors.d.mts +2 -1
  245. package/types/sohl/document-subtypes.d.mts +1 -2
  246. package/types/sohl/item-fields.d.mts +3 -7
  247. package/engine/retired-fields.mjs +0 -631
  248. package/types/engine/retired-fields.d.mts +0 -354
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  /**
15
- * The shared markdownlint rules, as a markdownlint-cli2 options module.
15
+ * The shared markdownlint rules, as a `.markdownlint-cli2.mjs` options module.
16
16
  *
17
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "23.1.0",
3
+ "version": "23.2.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",
@@ -145,13 +145,16 @@
145
145
  "flesch": "^2.0.1",
146
146
  "glob": "^13.0.6",
147
147
  "gray-matter": "^4.0.3",
148
- "handlebars": "^4.7.9",
148
+ "handlebars": "^4.7.10",
149
+ "ignore": "^7.0.12",
150
+ "jsonc-parser": "^3.3.1",
149
151
  "loglevel": "^1.9.2",
150
152
  "loglevel-plugin-prefix": "^0.8.4",
151
153
  "markdown-it": "^15.0.0",
152
154
  "markdown-it-deflist": "^4.0.0",
153
155
  "markdown-it-footnote": "^4.0.0",
154
- "markdownlint-cli2": "^0.23.3",
156
+ "markdownlint": "^0.41.1",
157
+ "minimatch": "^10.2.6",
155
158
  "pagefind": "^1.5.2",
156
159
  "prettier": "^3.9.6",
157
160
  "remark-frontmatter": "^5.0.0",
package/release.mjs CHANGED
@@ -45,6 +45,60 @@ import { ZipArchive } from "archiver";
45
45
 
46
46
  import { SCHEMA_ARTIFACT_FILE } from "./engine/foreign-catalog.mjs";
47
47
 
48
+ /** The earliest instant a zip entry's date field can hold. */
49
+ const ZIP_EPOCH_MS = Date.UTC(1980, 0, 1);
50
+
51
+ /**
52
+ * The timestamp stamped on every archive entry: `SOURCE_DATE_EPOCH` (seconds)
53
+ * when set to a number, otherwise 1980-01-01T00:00:00Z. A value earlier than
54
+ * 1980 is raised to it, which is as early as a zip can record.
55
+ *
56
+ * @returns {Date}
57
+ */
58
+ function archiveDate() {
59
+ const seconds = Number(process.env.SOURCE_DATE_EPOCH);
60
+ const ms =
61
+ process.env.SOURCE_DATE_EPOCH && Number.isFinite(seconds) ? seconds * 1000 : ZIP_EPOCH_MS;
62
+ return new Date(Math.max(ms, ZIP_EPOCH_MS));
63
+ }
64
+
65
+ /**
66
+ * Append a directory tree to an archive in path order, with a fixed date and
67
+ * fixed permissions (0755 directories, 0644 files) so the bytes depend only on
68
+ * the tree's names and contents.
69
+ *
70
+ * Entries are appended one at a time from buffers: archiver's own directory
71
+ * walk stats files concurrently, so its entry order and recorded mtimes follow
72
+ * the filesystem.
73
+ *
74
+ * @param {import("archiver").ZipArchive} archive
75
+ * @param {string} root - The directory whose contents become the archive.
76
+ * @param {Date} date - The date recorded on every entry.
77
+ * @returns {Promise<void>}
78
+ */
79
+ async function appendTree(archive, root, date) {
80
+ /** @param {string} rel - Path relative to `root`, `/`-separated, or "". */
81
+ async function walk(rel) {
82
+ const entries = await fsp.readdir(path.join(root, rel), { withFileTypes: true });
83
+ entries.sort((a, b) =>
84
+ a.name < b.name ? -1
85
+ : a.name > b.name ? 1
86
+ : 0,
87
+ );
88
+ for (const entry of entries) {
89
+ const name = rel ? `${rel}/${entry.name}` : entry.name;
90
+ if (entry.isDirectory()) {
91
+ archive.append(Buffer.alloc(0), { name: `${name}/`, date, mode: 0o755 });
92
+ await walk(name);
93
+ } else {
94
+ const body = await fsp.readFile(path.join(root, name));
95
+ archive.append(body, { name, date, mode: 0o644 });
96
+ }
97
+ }
98
+ }
99
+ await walk("");
100
+ }
101
+
48
102
  /**
49
103
  * Zip the staged tree and place the manifest beside the archive.
50
104
  *
@@ -55,6 +109,10 @@ import { SCHEMA_ARTIFACT_FILE } from "./engine/foreign-catalog.mjs";
55
109
  * failure is timing-dependent, so it survives every run that happens to be
56
110
  * fast enough, which is what makes it worth being explicit about.
57
111
  *
112
+ * **The archive is reproducible.** Entries are written in path order with one
113
+ * date (`SOURCE_DATE_EPOCH` when set, otherwise 1980-01-01) and fixed
114
+ * permissions, so a rebuild of one staged tree is byte-identical.
115
+ *
58
116
  * @param {object} [opts]
59
117
  * @param {string} [opts.stageDir] - The staged package tree.
60
118
  * @param {string} [opts.outDir] - Where the release assets are written.
@@ -62,17 +120,18 @@ import { SCHEMA_ARTIFACT_FILE } from "./engine/foreign-catalog.mjs";
62
120
  * Determines both asset names.
63
121
  * @param {string} [opts.metadataDir] - Where the build writes its content
64
122
  * index, consulted when the advertised file was not staged.
65
- * @param {boolean} [opts.pdf] - Whether to build the book that ships beside the
123
+ * @param {boolean} [opts.pdf] - Whether to build the books that ship beside the
66
124
  * archive. `true` by default; `false` skips the build and reports the skip.
67
125
  * @returns {Promise<{zip: string, manifest: string, metadata?: string,
68
- * schema?: string, pdf?: string, pdfFindings: object[], pdfSkipped: string|null,
126
+ * schema?: string, pdfs: string[], pdfFindings: object[], pdfSkipped: string|null,
69
127
  * bytes: number, version: string}>} The paths written, what the book build
70
128
  * found, the archive's size, and the version the manifest declares.
71
129
  * `metadata` is absent when the manifest advertises no content index,
72
- * `schema` is absent when the stage carries no `schema.json`, and `pdf` is
73
- * absent when no book was written. `pdfSkipped` is `null` when a book was
74
- * built, and otherwise the reason none was — itself `null` when the book
75
- * builder could not be loaded, which is reported through `pdfFindings`.
130
+ * `schema` is absent when the stage carries no `schema.json`, and `pdfs` is
131
+ * every book's PDF that was written, in declared order. `pdfSkipped` is
132
+ * `null` when a book was built, and otherwise the reason none was — itself
133
+ * `null` when the book builder could not be loaded or a book failed, which
134
+ * is reported through `pdfFindings`.
76
135
  * @throws {Error} When the stage has no manifest — there is nothing to release,
77
136
  * and an archive without one installs as nothing.
78
137
  */
@@ -114,10 +173,10 @@ export async function packRelease({
114
173
  });
115
174
 
116
175
  archive.pipe(output);
117
- // `false` — no top-level directory inside the zip. Foundry unpacks the
118
- // archive *into* the package directory, so an extra level would nest the
119
- // manifest one deeper than it looks for it.
120
- archive.directory(stage, false);
176
+ // No top-level directory inside the zip: Foundry unpacks the archive
177
+ // *into* the package directory, so an extra level would nest the manifest
178
+ // one deeper than it looks for it.
179
+ await appendTree(archive, stage, archiveDate());
121
180
  await archive.finalize();
122
181
  await closed;
123
182
 
@@ -132,23 +191,24 @@ export async function packRelease({
132
191
  const book =
133
192
  pdf ?
134
193
  await packReleasePdf({ out, version: manifest.version })
135
- : { pdf: null, findings: [], reason: "the release was asked not to build one" };
194
+ : { pdfs: [], findings: [], reason: "the release was asked not to build one" };
136
195
 
137
196
  return {
138
197
  zip: zipPath,
139
198
  manifest: path.join(out, manifestName),
140
199
  ...(metadata ? { metadata } : {}),
141
200
  ...(schema ? { schema } : {}),
142
- ...(book.pdf ? { pdf: book.pdf } : {}),
201
+ pdfs: book.pdfs,
143
202
  pdfFindings: book.findings,
144
- pdfSkipped: book.pdf ? null : book.reason,
203
+ pdfSkipped: book.pdfs.length ? null : book.reason,
145
204
  bytes: archive.pointer(),
146
205
  version: manifest.version,
147
206
  };
148
207
  }
149
208
 
150
209
  /**
151
- * Build the book that ships beside the archive.
210
+ * Build the books that ship beside the archive — every one `pdf.books`
211
+ * declares.
152
212
  *
153
213
  * **Imported when it is used, not when this module is.** The book build pulls
154
214
  * in a markdown parser, DuckDB and the whole content engine; `release.mjs`
@@ -165,8 +225,9 @@ export async function packRelease({
165
225
  * @param {object} opts - Options.
166
226
  * @param {string} opts.out - The release directory.
167
227
  * @param {string} opts.version - The version the manifest declares.
168
- * @returns {Promise<{pdf: string|null, findings: object[], reason: string|null}>}
169
- * What was built, and what was found on the way.
228
+ * @returns {Promise<{pdfs: string[], findings: object[], reason: string|null}>}
229
+ * Each book's PDF that was built, in declared order, and what was found on
230
+ * the way.
170
231
  */
171
232
  async function packReleasePdf({ out, version }) {
172
233
  let buildPdf;
@@ -174,7 +235,7 @@ async function packReleasePdf({ out, version }) {
174
235
  ({ buildPdf } = await import("./engine/pdf-build.mjs"));
175
236
  } catch (err) {
176
237
  return {
177
- pdf: null,
238
+ pdfs: [],
178
239
  findings: [
179
240
  {
180
241
  severity: "warning",
@@ -185,7 +246,11 @@ async function packReleasePdf({ out, version }) {
185
246
  };
186
247
  }
187
248
  const result = await buildPdf({ out, version });
188
- return { pdf: result.pdf, findings: result.findings, reason: result.reason };
249
+ return {
250
+ pdfs: result.books.map((book) => book.pdf).filter((pdf) => pdf),
251
+ findings: result.findings,
252
+ reason: result.reason,
253
+ };
189
254
  }
190
255
 
191
256
  /**
package/sohl/actors.mjs CHANGED
@@ -42,10 +42,11 @@
42
42
  * **A membership is derived, never authored here.** `data.affiliations` names
43
43
  * the bodies a being belongs to and the standing it holds in each, and each
44
44
  * entry becomes one embedded affiliation item: the entry's `rank` is that
45
- * item's `system.level` and its `office` is `system.office`. A standing is a
46
- * fact about the setting rather than about one system — it does not become true
47
- * because SoHL is installed — so the system block states none of it, and a
48
- * sheet and a page cannot come to disagree.
45
+ * item's `system.level`, its `office` is `system.office`, and the first of its
46
+ * `societies` is `system.society`, the one society the SoHL item carries. A
47
+ * standing is a fact about the setting rather than about one system — it does
48
+ * not become true because SoHL is installed — so the system block states none
49
+ * of it, and a sheet and a page cannot come to disagree.
49
50
  *
50
51
  * Not a standalone script — exports the `Actors` compiler class, imported and
51
52
  * driven by `engine/generate.mjs`. Must run after the items passes, since it
@@ -155,18 +156,7 @@ export class Actors extends SystemActorCompiler {
155
156
  this.errorCount++;
156
157
  return;
157
158
  }
158
- const { model, shortcode, type, ...rest } = entry;
159
- if (shortcode !== undefined) {
160
- this.noteError(
161
- `${ctx}: sohl.items[${index}] carries a top-level ` +
162
- `\`shortcode\` — that key is retired. Name the item ` +
163
- `this entry copies with \`model:\` (an address, e.g. ` +
164
- `\`${type ?? "skill"}-${shortcode}\`), and use ` +
165
- `\`system.shortcode\` for this item's own identity.`,
166
- );
167
- this.errorCount++;
168
- return;
169
- }
159
+ const { model, type, ...rest } = entry;
170
160
  const read = model === undefined ? null : this.readModel(model, index, ctx);
171
161
  if (model !== undefined && !read) return;
172
162
  if (read && type !== undefined) {
@@ -216,7 +206,8 @@ export class Actors extends SystemActorCompiler {
216
206
  * The body's own compiled item is the model, so a membership carries
217
207
  * whatever that body declares — its subtype, its common skills, its
218
208
  * standings toward others — and the being adds only what is true of *this*
219
- * membership: the rung it stands on and the post it holds.
209
+ * membership: the rung it stands on, the post it holds, and the society it
210
+ * is held in.
220
211
  *
221
212
  * A being naming a body that compiles to no item is a finding rather than a
222
213
  * silently dropped membership: the note says the being belongs somewhere,
@@ -236,9 +227,12 @@ export class Actors extends SystemActorCompiler {
236
227
  if (!shortcode) continue;
237
228
  const level = Number(standing?.rank);
238
229
  const office = typeof standing?.office === "string" ? standing.office.trim() : "";
230
+ const [first] = Array.isArray(standing?.societies) ? standing.societies : [];
231
+ const society = typeof first === "string" ? first.trim() : "";
239
232
  const system = {
240
233
  ...(Number.isFinite(level) ? { level } : {}),
241
234
  ...(office ? { office } : {}),
235
+ ...(society ? { society } : {}),
242
236
  };
243
237
  const embedded = this.resolveEmbedded(
244
238
  itemsMap,
@@ -335,16 +329,12 @@ export class Actors extends SystemActorCompiler {
335
329
  // identity (the dedup/override key of the Create-dialog picker).
336
330
  shortcode: fm.shortcode || "",
337
331
  // Required nullable number: a priority, or `null` for a being that
338
- // is not a template (the archetype contract). `system.archetype`
339
- // is the legacy spelling; the emitted key moves
340
- // with the schema, since an undeclared `system` key is discarded at
341
- // construction without a warning.
332
+ // is not a template (the archetype contract).
342
333
  templatePriority: systemTemplatePriority(fm, ctx),
343
334
  // `system.portrait` is **not** written here. A picture of the
344
335
  // subject is a picture, so a being's portrait is the lead image of
345
- // its `{#appearance}` section — which is the markup below — and the
346
- // key that used to declare one is not a key. With no authored
347
- // source left, the field keeps the schema's own initial.
336
+ // its `{#appearance}` section — which is the markup below. With no
337
+ // authored source, the field keeps the schema's own initial.
348
338
  appearance: renderSection(body || "", "appearance"),
349
339
  // One document says it once. `dossier` is the field that carries
350
340
  // an unsettled entry's notice, and `appearance` carries none —
@@ -399,9 +389,8 @@ export class Actors extends SystemActorCompiler {
399
389
  folder,
400
390
  sort: 0,
401
391
  ownership: { default: 0 },
402
- // Whatever the note authors, and nothing else. The priority used to
403
- // be spliced in here as `flags.sohl.docArchetype`; it is a schema
404
- // field now and sits in `system`.
392
+ // Whatever the note authors, and nothing else. The template
393
+ // priority is a schema field and sits in `system`.
405
394
  flags: blockProperty(fm, SYSTEM, "flags", {}),
406
395
  _stats: this.stats,
407
396
  _key: `!actors!${id}`,
@@ -46,9 +46,9 @@
46
46
  * **The keys are SoHL *document* subtypes, not markdown note types.** The
47
47
  * runtime reads this with `itemData.type`, which is a Foundry Item subtype, and
48
48
  * that is the vocabulary a compendium document carries. The two vocabularies
49
- * would be the same string but for three renamed note types (`armorgear` →
50
- * `armor`, and its two siblings), and this map stayed on the document side
51
- * because that is the side the runtime cannot translate: `sohl/item-builders.mjs`
49
+ * are the same string but for three note types (`armor` and its two siblings,
50
+ * whose document subtypes are `armorgear` and its siblings), and this map stays
51
+ * on the document side because that is the side the runtime cannot translate: `sohl/item-builders.mjs`
52
52
  * asks SoHL's own note-type → subtype map before it looks art up here, so the
53
53
  * build translates once and no second copy of those three rows exists.
54
54
  */
@@ -28,20 +28,6 @@
28
28
  * silently. Sixteen lines of data are the price of the two vocabularies being
29
29
  * separately stated.
30
30
  *
31
- * **Three rows are no longer the identity, and that is what the map was for.**
32
- * `armor`, `concoction` and `projectile` were renamed from the `…gear`
33
- * spellings that named SoHL's *document* subtype rather than the thing the note
34
- * is about; the subtypes themselves did not move, so those three rows now
35
- * say two different words where they used to say one twice. Landing the rename
36
- * cost exactly what having the map first promised it would: a data change on
37
- * three lines here, and no mechanism change anywhere.
38
- *
39
- * The old spellings are still read — {@link RENAMED_TYPES} normalises them
40
- * before this map is consulted — so a tree that has not swept its 30,741
41
- * embedded `(type, shortcode)` references compiles byte-identically. `weapon`
42
- * is deliberately **not** among them: both systems call that document a
43
- * `weapongear`, so the name says nothing SoHL-specific and is left alone.
44
- *
45
31
  * **What this map is not.** It says which document a note becomes, never what
46
32
  * that document contains: the `system` block comes from the item registry's
47
33
  * builders (`item-builders.mjs`), and an item's own `system.subType` field —
@@ -58,8 +44,7 @@ import { defineDocumentSubtypes } from "../engine/document-subtypes.mjs";
58
44
  * becomes.
59
45
  *
60
46
  * The Item rows are the thirteen types the item registry declares; the one
61
- * Actor row is `being`, which was two types (`character` and `creature`)
62
- * compiling to the same actor before they were retired. Types this
47
+ * Actor row is `being`. Types this
63
48
  * map does not name — `doc`, `macro`, the three map types — compile into
64
49
  * documents that carry no system subtype at all, so they have no row and never
65
50
  * needed one.
package/sohl/infobox.mjs CHANGED
@@ -62,7 +62,6 @@ import {
62
62
  humanizeValue,
63
63
  systemRowsSection,
64
64
  } from "../engine/infobox.mjs";
65
- import { currentType } from "../engine/ids.mjs";
66
65
  import { subTypes } from "../engine/note-vocabulary.mjs";
67
66
  import { systemBlock, systemData } from "../engine/system-block.mjs";
68
67
  import { readCanonicalKey } from "../engine/content-address.mjs";
@@ -205,7 +204,7 @@ function modelAddress(model) {
205
204
  const target = parseAddress(
206
205
  model,
207
206
  { package: contentPackage(), system: "sohl", types: MODEL_TYPES },
208
- { declared: true, legacyShortcodeCase: true },
207
+ { declared: true },
209
208
  );
210
209
  return target.reason ? undefined : target;
211
210
  }
@@ -240,7 +239,7 @@ export function decodeItem(entry) {
240
239
  if (!type) return undefined;
241
240
  return {
242
241
  ...(address ? { address } : {}),
243
- type: currentType(type),
242
+ type: type,
244
243
  shortcode,
245
244
  name: typeof entry.name === "string" ? entry.name : "",
246
245
  system,
@@ -18,13 +18,10 @@
18
18
  * There is one list, not two. This repository hands {@link ITEM_BUILDERS} to
19
19
  * the build as `itemBuilders` in `package-build.config.yaml`, and `itemTypes()`
20
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. Split, the
22
- * whitelist and the builder table were maintained by hand and had already
23
- * drifted: `trait` — a **retired** item type, 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.
21
+ * whitelisted for compilation without a builder to compile it. Kept apart, a
22
+ * whitelist naming a type absent from `documentTypes.Item` would let a note of
23
+ * that type pass the gate and then die on `BUILDERS[type] is not a function`,
24
+ * swallowed as a per-file error.
28
25
  *
29
26
  * **The builders are generated, not written.** Each type's `system` builder
30
27
  * comes from its field declaration in `item-fields.mjs` by way of
@@ -58,7 +55,7 @@ import { ITEM_FIELDS } from "./item-fields.mjs";
58
55
  *
59
56
  * Reading {@link defaultItemArt} here is what makes the two lists one: this
60
57
  * module cannot name a type the art map does not cover, because that function
61
- * throws and this module evaluates at import. A drift that a test used to catch
58
+ * throws and this module evaluates at import. A drift between the lists
62
59
  * is unrepresentable.
63
60
  *
64
61
  * **The art map is keyed by the document subtype**, which the runtime reads
@@ -246,7 +246,7 @@ const STRIKE_MODES = Object.freeze({
246
246
  * @returns {number} The die size, `0` when the projectile declares none.
247
247
  */
248
248
  function impactDie(fm) {
249
- return Number(sohlSystemField(fm, "impactBase.die", 0, { legacyKey: "impact.die" })) || 0;
249
+ return Number(sohlSystemField(fm, "impactBase.die", 0, { name: "impact.die" })) || 0;
250
250
  }
251
251
 
252
252
  /** A strike mode discriminated by `type`, mandatory on a combat technique. */
@@ -281,7 +281,6 @@ const GEAR_COMMON = Object.freeze([
281
281
  },
282
282
  {
283
283
  name: "data.weight",
284
- legacyKey: "weight",
285
284
  to: "weightBase",
286
285
  ...AS_AUTHORED,
287
286
  kind: "number",
@@ -290,7 +289,6 @@ const GEAR_COMMON = Object.freeze([
290
289
  },
291
290
  {
292
291
  name: "data.value",
293
- legacyKey: "value",
294
292
  to: "valueBase",
295
293
  ...AS_AUTHORED,
296
294
  kind: "number",
@@ -299,7 +297,6 @@ const GEAR_COMMON = Object.freeze([
299
297
  },
300
298
  {
301
299
  name: "data.quality",
302
- legacyKey: "quality",
303
300
  to: "qualityBase",
304
301
  ...AS_AUTHORED,
305
302
  kind: "number",
@@ -308,7 +305,6 @@ const GEAR_COMMON = Object.freeze([
308
305
  },
309
306
  {
310
307
  name: "data.durability",
311
- legacyKey: "durability",
312
308
  to: "durabilityBase",
313
309
  ...AS_AUTHORED,
314
310
  kind: "number",
@@ -352,13 +348,9 @@ export const COMBAT_TECHNIQUE_STRIKE_MODE = Object.freeze({
352
348
  * Every item type's frontmatter vocabulary, in the order the `system` block
353
349
  * emits it.
354
350
  *
355
- * **A field whose shared source is a path into `data:` names both positions.**
356
- * `name` is the source the content format's mapping tables state — `data.seat`,
357
- * `data.weight` — and `legacyKey` is the key the system block still carries, which
358
- * wins while a tree writes it. Naming only the source would move the in-block
359
- * read to a key spelled `data.seat`, which nothing authors; naming only the key
360
- * leaves the container the specification points at unread, and the field then
361
- * compiles its default over an authored value with nothing said.
351
+ * **A field whose shared source is a path into `data:` is authored there and
352
+ * nowhere else.** `name` is the source the content format's mapping tables
353
+ * state — `data.seat`, `data.weight`.
362
354
  *
363
355
  * @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
364
356
  */
@@ -418,7 +410,6 @@ export const ITEM_FIELDS = Object.freeze({
418
410
  // Plural because the field holds a map of many standings, one per
419
411
  // affiliation — the singular reads as a misnomer.
420
412
  name: "data.relations",
421
- legacyKey: "relations",
422
413
  to: "relations",
423
414
  ...RELATION,
424
415
  address: AFFILIATION_KEYS,
@@ -430,7 +421,6 @@ export const ITEM_FIELDS = Object.freeze({
430
421
  // A list, because an affiliation may sit under more than one at
431
422
  // once — an arcane tradition within an order, say.
432
423
  name: "data.parents",
433
- legacyKey: "parents",
434
424
  to: "parents",
435
425
  ...ADDRESS_LIST,
436
426
  address: AFFILIATION_ITEMS,
@@ -442,7 +432,6 @@ export const ITEM_FIELDS = Object.freeze({
442
432
  // subTypes, while a seat covers a polity, a guild, an order and a
443
433
  // faith alike.
444
434
  name: "data.seat",
445
- legacyKey: "seat",
446
435
  to: "seat",
447
436
  ...BLANK_IS_NULL,
448
437
  address: PLACE_VALUE,
@@ -487,7 +476,6 @@ export const ITEM_FIELDS = Object.freeze({
487
476
  },
488
477
  {
489
478
  name: "data.contagionIndex",
490
- legacyKey: "contagionIndex",
491
479
  to: "contagionIndexBase",
492
480
  ...NUMBER,
493
481
  default: 0,
@@ -495,7 +483,6 @@ export const ITEM_FIELDS = Object.freeze({
495
483
  },
496
484
  {
497
485
  name: "data.transmission",
498
- legacyKey: "transmission",
499
486
  to: "transmission",
500
487
  ...AS_AUTHORED,
501
488
  default: "none",
@@ -518,7 +505,6 @@ export const ITEM_FIELDS = Object.freeze({
518
505
  },
519
506
  {
520
507
  name: "data.outcomeTraumas",
521
- legacyKey: "outcomeTraumas",
522
508
  to: "outcomeTraumas",
523
509
  ...BLANK_IS_NULL,
524
510
  default: null,
@@ -526,7 +512,6 @@ export const ITEM_FIELDS = Object.freeze({
526
512
  },
527
513
  {
528
514
  name: "data.outcome",
529
- legacyKey: "outcome",
530
515
  to: "outcome",
531
516
  ...BLANK_IS_DEFAULT,
532
517
  default: "cured",
@@ -551,7 +536,6 @@ export const ITEM_FIELDS = Object.freeze({
551
536
  // the whole content of the declaration.
552
537
  {
553
538
  name: "data.onsetDurationFormula",
554
- legacyKey: "onsetDurationFormula",
555
539
  to: "onsetDurationFormula",
556
540
  ...DURATION_FORMULA,
557
541
  omitWhenAbsent: true,
@@ -560,7 +544,6 @@ export const ITEM_FIELDS = Object.freeze({
560
544
  },
561
545
  {
562
546
  name: "data.onsetDurationBase",
563
- legacyKey: "onsetDurationBase",
564
547
  to: "onsetDurationBase",
565
548
  ...DURATION_BASE,
566
549
  omitWhenAbsent: true,
@@ -569,7 +552,6 @@ export const ITEM_FIELDS = Object.freeze({
569
552
  },
570
553
  {
571
554
  name: "data.healingCheckDurationFormula",
572
- legacyKey: "healingCheckDurationFormula",
573
555
  to: "healingCheckDurationFormula",
574
556
  ...DURATION_FORMULA,
575
557
  omitWhenAbsent: true,
@@ -578,7 +560,6 @@ export const ITEM_FIELDS = Object.freeze({
578
560
  },
579
561
  {
580
562
  name: "data.healingCheckDurationBase",
581
- legacyKey: "healingCheckDurationBase",
582
563
  to: "healingCheckDurationBase",
583
564
  ...DURATION_BASE,
584
565
  omitWhenAbsent: true,
@@ -587,7 +568,6 @@ export const ITEM_FIELDS = Object.freeze({
587
568
  },
588
569
  {
589
570
  name: "data.resolutionDurationFormula",
590
- legacyKey: "resolutionDurationFormula",
591
571
  to: "resolutionDurationFormula",
592
572
  ...DURATION_FORMULA,
593
573
  omitWhenAbsent: true,
@@ -596,7 +576,6 @@ export const ITEM_FIELDS = Object.freeze({
596
576
  },
597
577
  {
598
578
  name: "data.resolutionDurationBase",
599
- legacyKey: "resolutionDurationBase",
600
579
  to: "resolutionDurationBase",
601
580
  ...DURATION_BASE,
602
581
  omitWhenAbsent: true,
@@ -831,7 +810,6 @@ export const ITEM_FIELDS = Object.freeze({
831
810
  },
832
811
  {
833
812
  name: "data.skillAptitudes",
834
- legacyKey: "skillAptitudes",
835
813
  to: "skillAptitudes",
836
814
  ...SKILL_APTITUDES,
837
815
  default: {},
@@ -839,7 +817,6 @@ export const ITEM_FIELDS = Object.freeze({
839
817
  },
840
818
  {
841
819
  name: "data.charges",
842
- legacyKey: "charges",
843
820
  to: "charges",
844
821
  ...CHARGES,
845
822
  default: { value: null, max: null },
@@ -896,7 +873,6 @@ export const ITEM_FIELDS = Object.freeze({
896
873
  },
897
874
  {
898
875
  name: "data.charges",
899
- legacyKey: "charges",
900
876
  to: "charges",
901
877
  ...CHARGES,
902
878
  default: { value: null, max: null },
@@ -1089,7 +1065,6 @@ export const ITEM_FIELDS = Object.freeze({
1089
1065
  // that sets it bleeds, and one that leaves it unset does not.
1090
1066
  {
1091
1067
  name: "data.healingCheckDurationFormula",
1092
- legacyKey: "healingCheckDurationFormula",
1093
1068
  to: "healingCheckDurationFormula",
1094
1069
  ...DURATION_FORMULA,
1095
1070
  omitWhenAbsent: true,
@@ -1098,7 +1073,6 @@ export const ITEM_FIELDS = Object.freeze({
1098
1073
  },
1099
1074
  {
1100
1075
  name: "data.healingCheckDurationBase",
1101
- legacyKey: "healingCheckDurationBase",
1102
1076
  to: "healingCheckDurationBase",
1103
1077
  ...DURATION_BASE,
1104
1078
  omitWhenAbsent: true,
@@ -1107,7 +1081,6 @@ export const ITEM_FIELDS = Object.freeze({
1107
1081
  },
1108
1082
  {
1109
1083
  name: "data.bloodLossAdvanceDurationFormula",
1110
- legacyKey: "bloodLossAdvanceDurationFormula",
1111
1084
  to: "bloodLossAdvanceDurationFormula",
1112
1085
  ...DURATION_FORMULA,
1113
1086
  omitWhenAbsent: true,
@@ -1116,7 +1089,6 @@ export const ITEM_FIELDS = Object.freeze({
1116
1089
  },
1117
1090
  {
1118
1091
  name: "data.bloodLossAdvanceDurationBase",
1119
- legacyKey: "bloodLossAdvanceDurationBase",
1120
1092
  to: "bloodLossAdvanceDurationBase",
1121
1093
  ...DURATION_BASE,
1122
1094
  omitWhenAbsent: true,
@@ -1125,7 +1097,6 @@ export const ITEM_FIELDS = Object.freeze({
1125
1097
  },
1126
1098
  {
1127
1099
  name: "data.courseDurationFormula",
1128
- legacyKey: "courseDurationFormula",
1129
1100
  to: "courseDurationFormula",
1130
1101
  ...DURATION_FORMULA,
1131
1102
  omitWhenAbsent: true,
@@ -1134,7 +1105,6 @@ export const ITEM_FIELDS = Object.freeze({
1134
1105
  },
1135
1106
  {
1136
1107
  name: "data.courseDurationBase",
1137
- legacyKey: "courseDurationBase",
1138
1108
  to: "courseDurationBase",
1139
1109
  ...DURATION_BASE,
1140
1110
  omitWhenAbsent: true,
@@ -1177,7 +1147,7 @@ export const ITEM_FIELDS = Object.freeze({
1177
1147
  to: "strikeModes",
1178
1148
  ...STRIKE_MODES,
1179
1149
  default: [],
1180
- describe: "The ways the weapon can be used to strike.",
1150
+ describe: "The ways the weapon can strike.",
1181
1151
  },
1182
1152
  ]),
1183
1153
  });
package/sohl/items.mjs CHANGED
@@ -88,10 +88,6 @@ export class Items extends SystemItemCompiler {
88
88
  shortcode: fm.shortcode,
89
89
  // Required nullable number: a priority, or `null` for a document
90
90
  // that is not a template (the archetype contract).
91
- // `system.archetype` is the legacy spelling; the receiving
92
- // schema declares only the new name, so the emitted key moves with
93
- // it — an undeclared `system` key is discarded at construction
94
- // without a warning.
95
91
  templatePriority: systemTemplatePriority(fm, label),
96
92
  actionDefs: [],
97
93
  notes: "",