@heroiclands/package-build 17.1.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 (153) hide show
  1. package/CHANGELOG.md +1773 -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 +394 -111
  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} +78 -81
  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/labels.mjs +191 -0
  74. package/manifest.mjs +43 -2
  75. package/package.json +19 -4
  76. package/release.mjs +62 -7
  77. package/sohl/actors.mjs +33 -487
  78. package/sohl/being-info.mjs +16 -7
  79. package/sohl/default-item-art.mjs +14 -3
  80. package/sohl/document-subtypes.mjs +16 -10
  81. package/sohl/item-builders.mjs +14 -5
  82. package/sohl/item-fields.mjs +68 -7
  83. package/sohl/items.mjs +44 -258
  84. package/sohl/note-schemas.mjs +2 -2
  85. package/types/content-config.d.mts +21 -42
  86. package/types/engine/actor-compiler.d.mts +204 -0
  87. package/types/engine/address-charset.d.mts +11 -8
  88. package/types/engine/address-diff.d.mts +53 -5
  89. package/types/engine/anchored-sections.d.mts +21 -0
  90. package/types/engine/anchors.d.mts +20 -0
  91. package/types/engine/base-compiler.d.mts +17 -17
  92. package/types/engine/bundle-notes.d.mts +173 -0
  93. package/types/engine/bundles.d.mts +60 -0
  94. package/types/engine/code-fences.d.mts +43 -0
  95. package/types/engine/compile-corpus.d.mts +32 -0
  96. package/types/engine/content-address.d.mts +205 -5
  97. package/types/engine/content-format-check.d.mts +6 -2
  98. package/types/engine/content-format.d.mts +57 -1
  99. package/types/engine/content-index.d.mts +64 -80
  100. package/types/engine/content-links.d.mts +52 -7
  101. package/types/engine/content-lint.d.mts +10 -1
  102. package/types/engine/content-package.d.mts +2 -1
  103. package/types/engine/content-tables.d.mts +21 -39
  104. package/types/engine/document-subtypes.d.mts +37 -3
  105. package/types/engine/field-spec.d.mts +76 -5
  106. package/types/engine/folder-notes.d.mts +159 -0
  107. package/types/engine/foreign-catalog.d.mts +53 -0
  108. package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +16 -38
  109. package/types/engine/frontmatter-lint.d.mts +10 -2
  110. package/types/engine/frontmatter.d.mts +64 -0
  111. package/types/engine/generate.d.mts +38 -0
  112. package/types/engine/helpers.d.mts +94 -30
  113. package/types/engine/ids.d.mts +96 -0
  114. package/types/engine/index-records.d.mts +68 -0
  115. package/types/engine/index.d.mts +9 -3
  116. package/types/engine/item-compiler.d.mts +131 -0
  117. package/types/engine/journals.d.mts +47 -9
  118. package/types/engine/metadata-index.d.mts +226 -0
  119. package/types/engine/note-claims.d.mts +57 -11
  120. package/types/engine/note-ids.d.mts +38 -0
  121. package/types/engine/note-renames.d.mts +102 -0
  122. package/types/engine/note-vocabulary.d.mts +44 -8
  123. package/types/engine/retired-fields.d.mts +75 -0
  124. package/types/engine/scenes.d.mts +3 -2
  125. package/types/engine/schema-check.d.mts +25 -4
  126. package/types/engine/site-build.d.mts +4 -4
  127. package/types/engine/site-index.d.mts +1 -1
  128. package/types/engine/sql-tables.d.mts +185 -0
  129. package/types/engine/subtype-registry.d.mts +49 -0
  130. package/types/engine/system-block.d.mts +40 -1
  131. package/types/engine/systems.d.mts +106 -0
  132. package/types/engine/web-wikilinks.d.mts +4 -2
  133. package/types/engine/wikilink-syntax.d.mts +10 -3
  134. package/types/engine/wikilinks.d.mts +41 -13
  135. package/types/engine/yaml-lint.d.mts +107 -0
  136. package/types/hm3/actors.d.mts +48 -0
  137. package/types/hm3/default-item-art.d.mts +42 -0
  138. package/types/hm3/document-subtypes.d.mts +24 -0
  139. package/types/hm3/index.d.mts +7 -0
  140. package/types/hm3/item-builders.d.mts +11 -0
  141. package/types/hm3/item-fields.d.mts +12 -0
  142. package/types/hm3/items.d.mts +23 -0
  143. package/types/hm3/template-priority.d.mts +21 -0
  144. package/types/manifest.d.mts +22 -1
  145. package/types/release.d.mts +9 -4
  146. package/types/sohl/actors.d.mts +7 -74
  147. package/types/sohl/being-info.d.mts +8 -27
  148. package/types/sohl/default-item-art.d.mts +5 -3
  149. package/types/sohl/items.d.mts +17 -32
  150. package/engine/foreign-manifests.mjs +0 -126
  151. package/engine/kb-manifest.mjs +0 -490
  152. package/types/engine/foreign-manifests.d.mts +0 -43
  153. package/types/engine/kb-manifest.d.mts +0 -241
package/sohl/actors.mjs CHANGED
@@ -12,8 +12,16 @@
12
12
  */
13
13
 
14
14
  /**
15
- * Actors pack compilerproduces JSON pack files for the "actors" Foundry
16
- * compendium from markdown `being` notes in the `assets/content/` tree.
15
+ * **SoHL's Actor pass**what a SoHL `being` document holds, and nothing else.
16
+ *
17
+ * The machinery an actor of any system needs — the predefined-item catalogue,
18
+ * reference translation through the system's map, embedding and its stable ids,
19
+ * the anchored prose sections — lives in
20
+ * {@link module:engine/actor-compiler}, where a second system reaches it
21
+ * (#139). What is left here is SoHL's data model: the body structure and its
22
+ * movement profiles, the attributes-and-items frontmatter that becomes embedded
23
+ * documents, the opening mastery level a skill is baked with, and the `system`
24
+ * block itself.
17
25
  *
18
26
  * One content type today, and the actor subtype it produces is **declared**
19
27
  * rather than assumed to be the same word: `sohl/document-subtypes.mjs` maps
@@ -26,62 +34,31 @@
26
34
  * SoHL#1580 and are now reported by `assertTypeNotRetired` in
27
35
  * `engine/ids.mjs`.
28
36
  *
29
- * Each actor's embedded items are resolved by looking `(type, shortcode)` up
30
- * against the generated JSON tree of every Item pack (built in prior items
31
- * passes and named by the caller as `itemsSourceDirs`). All of them, because a
32
- * repository may group its items into several Item packs (#1566) and a being
33
- * may hold items from any of them.
34
37
  * Attributes (`sohl.attributes` map) become embedded attribute items with
35
38
  * `scoreBase` set from the map value. Each entry in `sohl.items` is similarly
36
39
  * resolved by `(type, shortcode)` and deep-merged with the entry's other
37
40
  * properties. `sohl.skills` is ignored.
38
41
  *
39
- * **Those references are in the note vocabulary and the addresses are in the
40
- * document's**, and the difference is stated rather than assumed away (#140):
41
- * {@link itemAddress} keys a predefined item by the subtype its compiled
42
- * document carries, and {@link Actors#embeddedSubtype} translates each
43
- * authored reference forward through the system's map before the lookup. A
44
- * reference that then resolves to nothing is a finding naming the note and the
45
- * reference — never an item quietly missing from the compiled actor.
46
- *
47
42
  * Not a standalone script — exports the `Actors` compiler class, imported and
48
- * driven by `packages/content-build/engine/generate.mjs` (via `npm run build:compiledb`). Must run
49
- * after the items pass, since it reads the items pack's generated JSON tree.
43
+ * driven by `engine/generate.mjs`. Must run after the items passes, since it
44
+ * reads their generated JSON trees.
50
45
  *
51
- * The walk itself — filtering by type, expanding tables, converting
52
- * wikilinks, writing the JSON and counting errors — belongs to {@link sohl.utils.packs.BasePackCompiler}; this module
53
- * states only what makes this pass its own (#1509).
46
+ * @module
54
47
  */
55
48
 
56
- import fs from "fs";
57
- import path from "path";
58
- import log from "loglevel";
59
-
60
49
  import {
61
50
  sohlField,
62
- getFrontmatter,
63
- makeId,
64
51
  resolveName,
65
52
  resolveImg,
66
- defaultStats,
67
- systemArchetype,
68
- md,
53
+ systemTemplatePriority,
54
+ folderField,
69
55
  } from "../engine/helpers.mjs";
70
- import { emitDiagnostic } from "../engine/diagnostics.mjs";
71
56
  import { openingMasteryLevel } from "./skill-base.mjs";
72
- import { BasePackCompiler } from "../engine/base-compiler.mjs";
73
- import { contentPackage } from "../engine/content-package.mjs";
74
- // Which Foundry Actor subtype a note's `type` compiles into, and which note
75
- // types are actors at all. Looked up in the system's declared map, never
76
- // inferred from the type itself (#79).
77
- import {
78
- documentSubtype,
79
- mapsNoteType,
80
- noteTypesFor,
81
- referencedSubtype,
82
- } from "../engine/document-subtypes.mjs";
57
+ import { SystemActorCompiler, renderSection } from "../engine/actor-compiler.mjs";
58
+ // Which Foundry Actor subtype a note's `type` compiles into. Looked up in the
59
+ // system's declared map, never inferred from the type itself (#79).
60
+ import { documentSubtype } from "../engine/document-subtypes.mjs";
83
61
  import { SOHL_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
84
- import { locateFrontmatterKey } from "../engine/retired-fields.mjs";
85
62
  // The note-level `sohl:` block: `sohl.system` onto the document's `system`
86
63
  // verbatim, and `sohl.img` / `sohl.effects` / `sohl.flags` overriding their
87
64
  // shared top-level forms for this system alone (#58).
@@ -97,19 +74,6 @@ import { blockProperty, mergeSystemData } from "../engine/system-block.mjs";
97
74
  */
98
75
  const SYSTEM = SOHL_DOCUMENT_SUBTYPES.block;
99
76
 
100
- /**
101
- * The note types this pass claims — every one the system's map sends to an
102
- * `Actor`.
103
- *
104
- * Read from the map rather than restated as a constant. It *was* a constant:
105
- * `ACTOR_VAULT_TYPE = "being"` sat here and `type: "being"` was emitted several
106
- * hundred lines below, with nothing relating them, so a change to either
107
- * followed the other only by coincidence (#79).
108
- *
109
- * @type {readonly string[]}
110
- */
111
- const ACTOR_NOTE_TYPES = Object.freeze(noteTypesFor(SOHL_DOCUMENT_SUBTYPES, "Actor"));
112
-
113
77
  // Default art per actor **subtype**, applied when frontmatter supplies no `img`
114
78
  // / `portrait`. Beings default to the generic person icon; another subtype adds
115
79
  // its own entry here as the map gains a row for it.
@@ -139,43 +103,6 @@ function defaultActorImg(subType) {
139
103
  return img;
140
104
  }
141
105
 
142
- /**
143
- * Strip compendium-only fields from a predefined item before embedding it
144
- * inside an actor's `items[]`. These fields belong on a top-level
145
- * compendium document, not on an embedded one.
146
- */
147
- function stripCompendiumFields(item) {
148
- // eslint-disable-next-line no-unused-vars
149
- const { _key, _stats, ownership, folder, ...rest } = item;
150
- return rest;
151
- }
152
-
153
- /**
154
- * Recursively merge `overlay` onto `base`. Plain objects merge key-by-key;
155
- * everything else (arrays, primitives, null) replaces. Inputs are not
156
- * mutated.
157
- */
158
- function deepMerge(base, overlay) {
159
- if (overlay === undefined) return base;
160
- if (!isPlainObject(base) || !isPlainObject(overlay)) {
161
- return overlay;
162
- }
163
- const out = { ...base };
164
- for (const [k, v] of Object.entries(overlay)) {
165
- out[k] = k in base ? deepMerge(base[k], v) : v;
166
- }
167
- return out;
168
- }
169
-
170
- function isPlainObject(v) {
171
- return (
172
- v !== null &&
173
- typeof v === "object" &&
174
- !Array.isArray(v) &&
175
- Object.getPrototypeOf(v) === Object.prototype
176
- );
177
- }
178
-
179
106
  /**
180
107
  * Normalize a being's persisted `system.body` from a `sohl.body` block. The
181
108
  * authoring frontmatter mirrors the schema field-for-field: `sohl.body` nests
@@ -223,275 +150,17 @@ function extractBodyAndMovement(fm) {
223
150
  };
224
151
  }
225
152
 
226
- /**
227
- * The key one predefined item is held under, and every place that spells it.
228
- *
229
- * **The vocabulary is the document's, not the note's** — `subType` is the
230
- * Foundry Item subtype the compiled document carries, which is the only thing a
231
- * compiled pack (or an extracted dependency catalogue) records about what an
232
- * item *is*. A being's frontmatter addresses the same item in the *note*
233
- * vocabulary, so a reference is translated forward through the system's map
234
- * before it reaches this function; see {@link Actors#embeddedSubtype} for why
235
- * the translation goes that way and not the other (#140).
236
- *
237
- * @param {string} subType - The Foundry Item subtype.
238
- * @param {string} shortcode - The item's `system.shortcode`.
239
- * @returns {string} The address, `subType:shortcode`.
240
- */
241
- function itemAddress(subType, shortcode) {
242
- return `${subType}:${shortcode}`;
243
- }
244
-
245
- /**
246
- * Load every JSON file under each of `itemsSourceDirs`, returning one Map keyed
247
- * by {@link itemAddress} — the compiled document's **subtype** and its
248
- * `system.shortcode`. Folder docs and entries without a shortcode are skipped.
249
- * The `_key` field is stripped from each entry — it is not part of the item
250
- * data model.
251
- *
252
- * The directories are read as one address space, because a being names an item
253
- * by `(type, shortcode)` and never by the pack it happens to ship in. Two local
254
- * Item packs claiming the same address is therefore ambiguous rather than a
255
- * last-one-wins ordering detail, and fails here.
256
- *
257
- * A **foreign** directory — the extracted item catalogue of a package this
258
- * repository depends on but does not contain — is a fallback rather than a
259
- * peer. A repository must be able to ship its own `skill:awar` that stands in
260
- * front of the system's, so a local address shadows a foreign one instead of
261
- * colliding with it. Local directories are therefore read first, and anything
262
- * already claimed is left alone.
263
- *
264
- * @param {readonly string[]} itemsSourceDirs - Every local Item pack's JSON tree.
265
- * @param {readonly string[]} [foreignSourceDirs] - Extracted dependency
266
- * catalogues, consulted only for addresses no local pack defines.
267
- * @returns {Map<string, object>} The predefined items, by address.
268
- */
269
- function loadItemsMap(itemsSourceDirs, foreignSourceDirs = []) {
270
- const map = new Map();
271
- const source = new Map();
272
- const shadowed = [];
273
- for (const itemsSourceDir of itemsSourceDirs) {
274
- if (!fs.existsSync(itemsSourceDir)) {
275
- // The generator orders the actors pass after every Item pass (#73),
276
- // so a whole-package build cannot reach this. What can is a run
277
- // restricted to this one pack, or a caller constructing the
278
- // compiler itself — neither of which reordering a pack list fixes,
279
- // so the message no longer suggests it.
280
- throw new Error(
281
- `Items source directory ${itemsSourceDir} does not exist — ` +
282
- `a being resolves its embedded items against the Item ` +
283
- `packs' compiled output, so those packs must be compiled ` +
284
- `before this one`,
285
- );
286
- }
287
- for (const name of fs.readdirSync(itemsSourceDir)) {
288
- if (!name.endsWith(".json")) continue;
289
- if (name.startsWith("folder_")) continue;
290
- const full = path.join(itemsSourceDir, name);
291
- let doc;
292
- try {
293
- doc = JSON.parse(fs.readFileSync(full, "utf8"));
294
- } catch (err) {
295
- emitDiagnostic({
296
- file: full,
297
- severity: "warning",
298
- message: `unparseable item JSON, skipping: ${err.message}`,
299
- });
300
- continue;
301
- }
302
- const shortcode = doc?.system?.shortcode;
303
- if (!doc?.type || !shortcode) continue;
304
- const address = itemAddress(doc.type, shortcode);
305
- const owner = source.get(address);
306
- if (owner && owner !== itemsSourceDir) {
307
- throw new Error(
308
- `Two Item packs both define "${address}" (${owner} and ` +
309
- `${itemsSourceDir}); a being addresses an item by ` +
310
- `(type, shortcode), so the address must be unique across ` +
311
- `every Item pack`,
312
- );
313
- }
314
- source.set(address, itemsSourceDir);
315
- // eslint-disable-next-line no-unused-vars
316
- const { _key, ...rest } = doc;
317
- map.set(address, rest);
318
- }
319
- }
320
- for (const foreignDir of foreignSourceDirs) {
321
- for (const name of fs.readdirSync(foreignDir)) {
322
- if (!name.endsWith(".json")) continue;
323
- if (name.startsWith("folder_")) continue;
324
- const full = path.join(foreignDir, name);
325
- let doc;
326
- try {
327
- doc = JSON.parse(fs.readFileSync(full, "utf8"));
328
- } catch (err) {
329
- emitDiagnostic({
330
- file: full,
331
- severity: "warning",
332
- message: `unparseable item JSON, skipping: ${err.message}`,
333
- });
334
- continue;
335
- }
336
- const shortcode = doc?.system?.shortcode;
337
- if (!doc?.type || !shortcode) continue;
338
- const address = itemAddress(doc.type, shortcode);
339
- if (map.has(address)) {
340
- // Deliberate: this repository defines it, so its version wins.
341
- if (source.has(address)) shadowed.push(address);
342
- continue;
343
- }
344
- // eslint-disable-next-line no-unused-vars
345
- const { _key, ...rest } = doc;
346
- map.set(address, rest);
347
- }
348
- }
349
- if (shadowed.length) {
350
- log.info(
351
- `${shadowed.length} dependency item(s) shadowed by this ` +
352
- `repository's own: ${shadowed.slice(0, 5).join(", ")}` +
353
- (shadowed.length > 5 ? ", …" : ""),
354
- );
355
- }
356
- return map;
357
- }
358
-
359
- /**
360
- * Extract the body of an H1 section whose heading carries the explicit
361
- * anchor decorator `{#<anchorId>}`. Captures every line after the H1 up
362
- * to (but not including) the next H1 — nested H2/H3 etc. and their bodies
363
- * are included. The H1 line itself is discarded. Returns "" if no such
364
- * heading exists. Fenced code blocks are respected so `# foo` inside
365
- * ``` blocks does not trigger a match.
366
- */
367
- function extractAnchorSection(body, anchorId) {
368
- const lines = body.split("\n");
369
- const captured = [];
370
- let inCodeBlock = false;
371
- let capturing = false;
372
- const wanted = String(anchorId).toLowerCase();
373
- for (const line of lines) {
374
- if (line.trim().startsWith("```")) {
375
- inCodeBlock = !inCodeBlock;
376
- if (capturing) captured.push(line);
377
- continue;
378
- }
379
- const h1Match = !inCodeBlock ? line.match(/^\s*#\s+(.+?)\s*#*\s*$/) : null;
380
- if (h1Match) {
381
- const anchor = h1Match[1].match(/\{#([^}]+)\}\s*$/);
382
- const id = anchor?.[1]?.trim().toLowerCase() || null;
383
- if (capturing) break;
384
- if (id === wanted) {
385
- capturing = true;
386
- continue;
387
- }
388
- }
389
- if (capturing) captured.push(line);
390
- }
391
- return captured.join("\n").trim();
392
- }
393
-
394
- /**
395
- * Render an extracted markdown section to HTML, or "" if empty.
396
- */
397
- function renderSection(body, anchorId) {
398
- const slice = extractAnchorSection(body, anchorId);
399
- return slice ? md.render(slice) : "";
400
- }
401
-
402
- export class Actors extends BasePackCompiler {
403
- static id = "actors";
404
- static label = "actor";
405
-
406
- // A being's embedded items are resolved against the *output* of the item
407
- // passes, so every Item pack compiles before this one. Declared rather than
408
- // left to the order `packs:` happens to list (#73).
409
- static readsPackOutputOf = Object.freeze(["Item"]);
410
-
411
- /** @type {readonly string[]} */
412
- itemsSourceDirs;
413
- foreignSourceDirs;
414
-
415
- constructor({ itemsSourceDirs = [], foreignSourceDirs = [], ...options }) {
416
- super(options);
417
- // Where the items passes wrote their JSON. Stated by the caller rather
418
- // than assumed to be this pack's sibling: the packs' locations are
419
- // configuration, and a consumer may put them anywhere (#1508). Every
420
- // Item pack, because a repository may ship more than one (#1566).
421
- //
422
- // **Optional, and empty is a legitimate package (#49).** This used to
423
- // throw unless at least one Item pack was declared, which asked a
424
- // package to declare the very thing it may exist not to have. An Item
425
- // pack is system-bound by construction — Foundry requires `system` on
426
- // Item packs — so a deliberately system-agnostic module could satisfy
427
- // the guard only by naming a system. `harn-ensemble` is the case:
428
- // 2,512 beings whose embedded items address the `sohl` and `hm3`
429
- // catalogues, and five affiliation notes of its own.
430
- //
431
- // The guard also did not test what it claimed. It counted *declared
432
- // directories*, not resolvable items, so an empty Item pack satisfied
433
- // it while a being naming a missing item still failed later. The
434
- // condition actually cared about is checked where it can be reported
435
- // precisely: {@link Actors#resolveEmbedded} already errors per
436
- // unresolved `(type, shortcode)`, naming the being. A package whose
437
- // beings embed nothing, or whose every address resolves against a
438
- // dependency catalogue through `foreignSourceDirs`, now compiles with
439
- // no Item pack at all — and one that is genuinely missing an item
440
- // still fails, saying which item and which actor rather than which
441
- // pack is absent.
442
- Object.defineProperty(this, "itemsSourceDirs", {
443
- value: Object.freeze([...itemsSourceDirs]),
444
- writable: false,
445
- });
446
- // The dependency catalogues, if any. Not required: a repository that
447
- // holds every item its beings name needs none, and one that declares
448
- // no `itemCatalog: true` relationship gets an empty list.
449
- Object.defineProperty(this, "foreignSourceDirs", {
450
- value: Object.freeze([...foreignSourceDirs]),
451
- writable: false,
452
- });
453
- }
454
-
153
+ export class Actors extends SystemActorCompiler {
455
154
  /**
456
- * The note-type → document-subtype map this pass compiles against.
457
- *
458
- * Stated by the class rather than reached for through the module import, so
459
- * every subtype decision the pass makes the actor's own, and each
460
- * embedded item reference's — reads one declaration that a subclass
461
- * compiling for another system can replace. That is also what lets the
462
- * non-identity behaviour be exercised without introducing a non-identity
463
- * row into SoHL's own map, which is #78's job and moves compiled bytes.
155
+ * SoHL's note-type → document-subtype map the one declaration that says
156
+ * which block this pass reads, which notes it claims, and what each
157
+ * becomes (#79). It is also what every embedded reference is translated
158
+ * through, which is why a subclass replaces one thing and not two.
464
159
  *
465
160
  * @type {import("../engine/document-subtypes.mjs").DocumentSubtypeMap}
466
161
  */
467
162
  static documentSubtypes = SOHL_DOCUMENT_SUBTYPES;
468
163
 
469
- /**
470
- * @param {object} fm - The note's frontmatter.
471
- * @returns {boolean} True for a note type the system maps onto an `Actor`.
472
- */
473
- selects(fm) {
474
- return mapsNoteType(this.constructor.documentSubtypes, fm.type, "Actor");
475
- }
476
-
477
- /**
478
- * An Actor **is** a system's data, so this pack takes only notes carrying
479
- * this system's block (#58).
480
- */
481
- static requiresSystemBlock = true;
482
-
483
- /**
484
- * The predefined items each being's embedded items resolve against, loaded
485
- * before the walk from the items passes' output.
486
- *
487
- * @returns {Promise<void>}
488
- */
489
- async prepare() {
490
- await super.prepare();
491
- this.itemsMap = loadItemsMap(this.itemsSourceDirs, this.foreignSourceDirs);
492
- log.info(`Loaded ${this.itemsMap.size} predefined items for actor resolution`);
493
- }
494
-
495
164
  /**
496
165
  * Compile one `being` note into its actor document.
497
166
  *
@@ -504,132 +173,6 @@ export class Actors extends BasePackCompiler {
504
173
  return this.buildBeing(this.itemsMap, fm, markdown);
505
174
  }
506
175
 
507
- /** @inheritdoc */
508
- reportDetail(stats) {
509
- log.debug(
510
- `Skipped ${stats.skippedOther} non-actor file(s) ` +
511
- `(not ${ACTOR_NOTE_TYPES.join("/")}, package:${contentPackage()})`,
512
- );
513
- }
514
-
515
- /**
516
- * The Foundry Item subtype an embedded reference's `type` addresses.
517
- *
518
- * **The reference is in the note vocabulary; the address is in the
519
- * document's** (#140). A being writes `(type, shortcode)` with the type an
520
- * author authors, while {@link itemAddress} keys the predefined items by
521
- * the subtype each compiled document carries — so exactly one of the two
522
- * sides has to translate, and it is this one. The system's map is a
523
- * function from note type to subtype by construction; the reverse is not,
524
- * and a compiled document records nothing about the note that produced it,
525
- * so there is no honest way to key the addresses the other way round.
526
- *
527
- * The two vocabularies are the same string in every SoHL row today, which
528
- * is why looking a reference up verbatim worked. The first non-identity row
529
- * (#78: `armor` → `armorgear`) ends that, and a reference resolving to
530
- * nothing must be a finding rather than an item quietly missing from the
531
- * compiled actor.
532
- *
533
- * @param {string} type - The type the reference names.
534
- * @returns {import("../engine/document-subtypes.mjs").ReferencedSubtype}
535
- * The subtype, or why the reference names none.
536
- */
537
- embeddedSubtype(type) {
538
- return referencedSubtype(this.constructor.documentSubtypes, type, "Item");
539
- }
540
-
541
- /**
542
- * Resolve one embedded item from a `(type, shortcode?, overlay)`
543
- * descriptor. If `shortcode` is given, the predefined item is fetched
544
- * from `itemsMap` and the overlay deep-merged on top. If absent, the
545
- * descriptor must carry enough fields to stand alone. The embedded
546
- * item's `_id` is regenerated deterministically from
547
- * `(actorId, subType, shortcode, indexKey)` so re-exports are stable —
548
- * from the **document subtype**, so that renaming a note type (#78) leaves
549
- * every embedded id exactly where it was.
550
- * Returns null if the descriptor cannot be resolved.
551
- *
552
- * @param {Map<string, object>} itemsMap - The predefined items, by address.
553
- * @param {string} actorId - The owning actor's id, seeding embedded ids.
554
- * @param {string} type - The **note** type the reference names.
555
- * @param {string|null} shortcode - The referenced item's shortcode, or
556
- * `null` for a stand-alone entry.
557
- * @param {object} [overlay] - The entry's remaining properties.
558
- * @param {string} indexKey - Distinguishes two references to one item.
559
- * @param {string} ctx - Diagnostic context (the actor's label).
560
- * @param {object} [at] - Where to locate a finding.
561
- * @param {string} [at.fmKey] - The frontmatter key the reference sits
562
- * under, so an unresolved one is reported at the reference rather than
563
- * at the note.
564
- * @returns {object|null} The embedded item, or null when it resolved to
565
- * nothing — always with a finding emitted.
566
- */
567
- resolveEmbedded(itemsMap, actorId, type, shortcode, overlay, indexKey, ctx, { fmKey } = {}) {
568
- // Where a finding about this reference points. The value locates the
569
- // exact entry in a list; the key is the fallback when it cannot be
570
- // found, which still beats naming the note alone.
571
- const where = () =>
572
- locateFrontmatterKey(this.currentNote?.absPath, fmKey ?? "items", shortcode || type);
573
-
574
- const { subType, problem } = this.embeddedSubtype(type);
575
- if (problem) {
576
- this.noteError(`${ctx}: ${indexKey}: ${problem}`, where());
577
- this.errorCount++;
578
- return null;
579
- }
580
- const address = itemAddress(/** @type {string} */ (subType), shortcode ?? "");
581
-
582
- let base = null;
583
- if (shortcode) {
584
- base = itemsMap.get(address);
585
- if (!base) {
586
- // Both vocabularies where they differ, so an author sees why an
587
- // address they wrote did not land where they expected.
588
- const translated =
589
- subType === type ? "" : (
590
- ` (looked up as "${address}", the ` +
591
- `${this.constructor.documentSubtypes.system} Item subtype a ` +
592
- `"${type}" note compiles into)`
593
- );
594
- this.noteError(
595
- `${ctx}: no predefined item for "${type}:${shortcode}"${translated}`,
596
- where(),
597
- );
598
- this.errorCount++;
599
- return null;
600
- }
601
- base = stripCompendiumFields(base);
602
- } else if (overlay && overlay.name && overlay.system) {
603
- base = { type: subType, name: overlay.name, system: {} };
604
- } else {
605
- this.noteError(
606
- `${ctx}: embedded item missing shortcode and not enough fields to stand alone`,
607
- where(),
608
- );
609
- this.errorCount++;
610
- return null;
611
- }
612
- const merged = overlay ? deepMerge(base, overlay) : base;
613
- merged.type = subType;
614
- merged._id = makeId(
615
- actorId,
616
- `${itemAddress(/** @type {string} */ (subType), shortcode || merged.name)}:${indexKey}`,
617
- );
618
- // Foundry's pack compiler flattens the document hierarchy into LevelDB,
619
- // storing each embedded document under its own `_key`. Embedded items
620
- // therefore need a hierarchical key, as do any effects they carry
621
- // (re-keyed under this actor's item rather than the items-pack key they
622
- // inherited). Mirrors the items pack convention in items.mjs.
623
- merged._key = `!actors.items!${actorId}.${merged._id}`;
624
- if (Array.isArray(merged.effects)) {
625
- for (const effect of merged.effects) {
626
- if (!effect?._id) continue;
627
- effect._key = `!actors.items.effects!${actorId}.${merged._id}.${effect._id}`;
628
- }
629
- }
630
- return merged;
631
- }
632
-
633
176
  /**
634
177
  * Build all embedded items for an actor: one per `sohl.attributes`
635
178
  * entry plus one per `sohl.items` entry. `sohl.skills` is ignored.
@@ -752,8 +295,8 @@ export class Actors extends BasePackCompiler {
752
295
 
753
296
  const items = this.buildEmbeddedItems(itemsMap, id, fm, ctx);
754
297
 
755
- const folderId = sohlField(fm, "folder", null);
756
- const folder = this.folderResolver(folderId);
298
+ const { value: authoredFolder, isAddress } = folderField(fm);
299
+ const folder = this.folderResolver(authoredFolder, { isAddress });
757
300
 
758
301
  const system = {
759
302
  // The frontmatter shortcode is the actor's stable `(type, shortcode)`
@@ -761,8 +304,11 @@ export class Actors extends BasePackCompiler {
761
304
  // identity (the dedup/override key of the Create-dialog picker, #604).
762
305
  shortcode: fm.shortcode || "",
763
306
  // Required nullable number: a priority, or `null` for a being that
764
- // is not an archetype (#126 / archetype contract #604).
765
- archetype: systemArchetype(fm, ctx),
307
+ // is not a template (#126 / archetype contract #604). The field was
308
+ // `system.archetype` until #266 / sohl#1836; the emitted key moves
309
+ // with the schema, since an undeclared `system` key is discarded at
310
+ // construction without a warning.
311
+ templatePriority: systemTemplatePriority(fm, ctx),
766
312
  // Nullish, not `||` (#218): a note that names no portrait gets the
767
313
  // subtype's default, one that writes `""` ships blank on purpose.
768
314
  portrait: resolveImg(blockProperty(fm, SYSTEM, "portrait")) ?? defaultImg,
@@ -803,7 +349,7 @@ export class Actors extends BasePackCompiler {
803
349
  this.reportUndeclaredSystemData(fm, SYSTEM, "Actor", subType);
804
350
  // And what this pass wrote itself. There is no field declaration for a
805
351
  // being at all, so *every* key here is a compiler emission — including
806
- // `archetype` (#126), which nothing compared until #155.
352
+ // `templatePriority` (#126), which nothing compared until #155.
807
353
  this.reportEmittedSystemData(system, {
808
354
  fm,
809
355
  block: SYSTEM,
@@ -836,7 +382,7 @@ export class Actors extends BasePackCompiler {
836
382
  folder,
837
383
  sort: 0,
838
384
  ownership: { default: 0 },
839
- // Whatever the note authors, and nothing else. `archetype` used to
385
+ // Whatever the note authors, and nothing else. The priority used to
840
386
  // be spliced in here as `flags.sohl.docArchetype`; it is a schema
841
387
  // field now and sits in `system` (#126).
842
388
  flags: blockProperty(fm, SYSTEM, "flags", {}),
@@ -36,6 +36,10 @@
36
36
  * @module
37
37
  */
38
38
 
39
+ // The retirement window for a renamed note type (#78): an embedded reference
40
+ // still spelling `armorgear` names the same gear group as `armor`.
41
+ import { currentType } from "../engine/ids.mjs";
42
+
39
43
  /**
40
44
  * The note `type` whose pages carry a being info block.
41
45
  *
@@ -59,19 +63,24 @@ export function isBeing(fm) {
59
63
  /**
60
64
  * The sidebar group each gear item type is displayed under.
61
65
  *
62
- * Presentation naming, not data-model naming: the model says `weapongear`, the
63
- * sidebar heading says "weapons". Kept as one table so a new gear type is added
64
- * in a single place rather than in each consumer's site build.
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 since #78 three of those are no longer 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.
65
74
  *
66
75
  * @type {Readonly<Record<string, string>>}
67
76
  */
68
77
  export const GEAR_TYPE_TO_KEY = Object.freeze({
69
78
  weapongear: "weapons",
70
- armorgear: "armor",
71
- projectilegear: "projectiles",
79
+ armor: "armor",
80
+ projectile: "projectiles",
72
81
  miscgear: "misc",
73
82
  containergear: "containers",
74
- concoctiongear: "concoctions",
83
+ concoction: "concoctions",
75
84
  });
76
85
 
77
86
  /** Whether a value is a plain mapping. */
@@ -132,7 +141,7 @@ export function deriveBeingInfo(sohl, index) {
132
141
  const gear = {};
133
142
  for (const it of items) {
134
143
  if (!isMap(it)) continue;
135
- const key = GEAR_TYPE_TO_KEY[it.type];
144
+ const key = GEAR_TYPE_TO_KEY[currentType(it.type)];
136
145
  if (!key) continue;
137
146
  const shortcode = typeof it.shortcode === "string" ? it.shortcode : undefined;
138
147
  const ref = lookup(it.type, shortcode);