@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
@@ -0,0 +1,126 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * The shape of a content-index record, and the accessors every reader of one
16
+ * needs (#243).
17
+ *
18
+ * **Why these live apart from the index that builds them.** Deriving records
19
+ * reaches the pack router and the manifest emitter, and those reach the
20
+ * compilers — so a module the compilers load cannot import
21
+ * `engine/content-index.mjs` without closing a cycle, and
22
+ * `engine/helpers.mjs` is exactly such a module. But nothing about *reading* a
23
+ * record needs any of that machinery: these are pure functions over a plain
24
+ * object, and `node:path` is the whole of their dependency.
25
+ *
26
+ * So the accessors sit here, where every reader can reach them, and
27
+ * `engine/content-index.mjs` re-exports them — it is where callers have always
28
+ * addressed them, and the split is an implementation detail of the import
29
+ * graph rather than a second place to look.
30
+ *
31
+ * @module
32
+ */
33
+
34
+ import path from "node:path";
35
+
36
+ /**
37
+ * The keys the content index adds to a record, which a note therefore may not
38
+ * carry itself.
39
+ *
40
+ * `package` is the note's distribution unit — the configured `contentPackage`,
41
+ * since a note declaring its own is a hard error (#56) — and it matches what
42
+ * the content-table expander puts on the same field, so a query reads the same
43
+ * value from either. `file` namespaces the note's place in the tree, again
44
+ * matching the expander's `file.*`.
45
+ *
46
+ * Both are checked rather than assumed: `folder` is real frontmatter on most
47
+ * notes, so the neighbouring names are close enough to a real key that a silent
48
+ * overwrite is a plausible future rather than a hypothetical one.
49
+ *
50
+ * @type {ReadonlyArray<string>}
51
+ */
52
+ export const DERIVED_KEYS = Object.freeze([
53
+ "package",
54
+ "file",
55
+ "address",
56
+ "anchors",
57
+ "nameAscii",
58
+ "aliasesAscii",
59
+ "foundry",
60
+ "documentation",
61
+ "documents",
62
+ ]);
63
+
64
+ /**
65
+ * The file a record was read from, as an absolute path.
66
+ *
67
+ * **The one composition, because there were four.** `file.path` is recorded
68
+ * *relative* to the content root deliberately — an absolute one is a fact about
69
+ * the machine that built the index, would differ between two checkouts of the
70
+ * same tree, and would put someone's home directory in a published artifact. So
71
+ * every pass that reads the index and then needs to open a note has to compose
72
+ * the absolute form, and each of the readers converted for #243 had written its
73
+ * own `path.join(base, ...record.file.path.split("/"))`. Four copies of one
74
+ * rule is what #243 exists to remove, so here it is once.
75
+ *
76
+ * The split is on `"/"` rather than `path.sep` because the recorded path is
77
+ * always POSIX — that is what makes the index identical on every platform.
78
+ *
79
+ * @param {string} contentBase - Root of the content tree the index was built from.
80
+ * @param {object} record - An index record.
81
+ * @returns {string} The note's absolute path.
82
+ */
83
+ export function noteFile(contentBase, record) {
84
+ return path.join(contentBase, ...String(record?.file?.path ?? "").split("/"));
85
+ }
86
+
87
+ /**
88
+ * The note's own frontmatter, as authored, from an index record.
89
+ *
90
+ * The inverse of the record's spread, and exact rather than best-effort: a
91
+ * record is the note's frontmatter plus {@link DERIVED_KEYS}, and a note that
92
+ * authors one of those keys fails the walk — so removing them cannot remove
93
+ * anything the note wrote. That enforced pairing is what lets a pass read the
94
+ * corpus from the index and still lint, route or compile what the *author*
95
+ * typed, rather than reasoning about `address:` and `anchors:` as though
96
+ * someone had written them.
97
+ *
98
+ * Lives beside the list it is the inverse of, so the two cannot drift.
99
+ *
100
+ * @param {Record<string, any>} record - An index record.
101
+ * @returns {Record<string, any>} The frontmatter, without the derived keys.
102
+ */
103
+ export function authoredFrontmatter(record) {
104
+ const fm = {};
105
+ for (const [key, value] of Object.entries(record ?? {})) {
106
+ if (!DERIVED_KEYS.includes(key)) fm[key] = value;
107
+ }
108
+ return fm;
109
+ }
110
+
111
+ /**
112
+ * Whether a record is a note's, rather than a documentation journal's.
113
+ *
114
+ * An item note yields two records — itself and the JournalEntry its prose
115
+ * compiles into — and the second is a document, not a note: it has no file of
116
+ * its own to read, no frontmatter an author wrote, and its `type` is the
117
+ * virtual `doc<type>` that `readQualifier` resolves rather than a type any tree
118
+ * declares. A reader enumerating the corpus wants the notes; one resolving an
119
+ * address wants both.
120
+ *
121
+ * @param {Record<string, any>} record - An index record.
122
+ * @returns {boolean} True for a note's own record.
123
+ */
124
+ export function isNoteRecord(record) {
125
+ return !record?.documents;
126
+ }
package/engine/index.mjs CHANGED
@@ -57,6 +57,9 @@ export * as packRouter from "./pack-router.mjs";
57
57
  /** Which note types a configuration compiles, and the notes nothing claims (#146). */
58
58
  export * as noteClaims from "./note-claims.mjs";
59
59
 
60
+ /** Folders as notes: the address index, the parent chain, and the emitted `Folder` (#256). */
61
+ export * as folderNotes from "./folder-notes.mjs";
62
+
60
63
  /** The content package a build compiles, and the Foundry package it ships in. */
61
64
  export * as contentPackage from "./content-package.mjs";
62
65
 
@@ -75,6 +78,9 @@ export * as noteSchemas from "./note-schemas.mjs";
75
78
  /** The closed frontmatter regions: the `data:` container and each type's `subType`. */
76
79
  export * as noteVocabulary from "./note-vocabulary.mjs";
77
80
 
81
+ /** The closed registry of system ids, and the `none` that stands for no system. */
82
+ export * as systems from "./systems.mjs";
83
+
78
84
  /** The shipped Foundry manifest: locating it, reading it, guarding its id. */
79
85
 
80
86
  /** The one normalisation this build makes: prose to a URL-safe token. */
@@ -83,14 +89,20 @@ export * as contentSlug from "./content-slug.mjs";
83
89
  /** Where a content note publishes: its section, and its `type-shortcode` URL. */
84
90
  export * as contentAddress from "./content-address.mjs";
85
91
 
86
- /** Whether a vendored manifest can still be addressed, not merely read. */
87
- export * as foreignManifests from "./foreign-manifests.mjs";
92
+ /** Which note-type document-subtype maps this toolchain ships. */
93
+ export * as subtypeRegistry from "./subtype-registry.mjs";
94
+
95
+ /** The id a note's document is filed under: its pin, or its address (#270). */
96
+ export * as noteIds from "./note-ids.mjs";
88
97
 
89
- /** The cross-package link manifest: reader, writer, and canonical keys. */
90
- export * as kbManifest from "./kb-manifest.mjs";
98
+ /** The shortcodes a note declares it used to be published under (#278). */
99
+ export * as noteRenames from "./note-renames.mjs";
100
+
101
+ /** The published content index packages exchange addresses through (#239). */
102
+ export * as metadataIndex from "./metadata-index.mjs";
91
103
 
92
104
  /** Deriving this package's own link manifest from its content tree. */
93
- export * as manifestEmit from "./manifest-emit.mjs";
105
+ export * as foundryEntries from "./foundry-entries.mjs";
94
106
 
95
107
  /** Publishing the note tree as a queryable JSON Lines index. */
96
108
  export * as contentIndex from "./content-index.mjs";
@@ -146,6 +158,12 @@ export * as mapNotes from "./map-notes.mjs";
146
158
  /** The Scene and Adventure compiler. */
147
159
  export * as scenes from "./scenes.mjs";
148
160
 
161
+ /** What a bundle note says, and how compiled documents become an Adventure. */
162
+ export * as bundleNotes from "./bundle-notes.mjs";
163
+
164
+ /** The Adventure compiler — the pass a `type: bundle` note compiles through. */
165
+ export * as bundles from "./bundles.mjs";
166
+
149
167
  /** The compiled-pack Scene/Level integrity guard. */
150
168
  export * as sceneLevels from "./scene-levels.mjs";
151
169
 
@@ -0,0 +1,349 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * **The Item pass, for any system** — everything about compiling a note into a
16
+ * Foundry Item that is a fact about the *note format* rather than about a
17
+ * particular game system (#139).
18
+ *
19
+ * `sohl/items.mjs` was the whole of it, and every system-specific thing in it
20
+ * was reached through one module-level constant read off SoHL's map. That is
21
+ * exactly the shape a second system needs, so the class moved here and the two
22
+ * halves declare what differs:
23
+ *
24
+ * | stated by the subclass | what it decides |
25
+ * | --- | --- |
26
+ * | `static documentSubtypes` | the block its notes write, the types it claims, the subtype each becomes |
27
+ * | `commonSystem()` | the `system` keys this system's compiler writes on every item |
28
+ * | `commonFlags()` | the `flags` it writes on every item, beside the authored ones |
29
+ *
30
+ * Everything else — claiming a note, looking the subtype up, resolving the art,
31
+ * merging the authored `<system>.system` block, checking what was emitted
32
+ * against the receiving schema, writing the envelope — is one implementation
33
+ * serving both. That is not tidiness: it is the guarantee that a second
34
+ * system's items are compiled by the code the first system's are, so a fix to
35
+ * either is a fix to both.
36
+ *
37
+ * **The `system` block has two authors, and the split is deliberate.** The
38
+ * declared fields come from the consuming repository's `itemBuilders` registry,
39
+ * addressed by *this pass's system*, so a type both systems declare is built by
40
+ * the right one. On top of that sits {@link SystemItemCompiler#commonSystem} —
41
+ * the keys the compiler writes on its own initiative for every item of the
42
+ * system, which no field declaration states and which nothing else could
43
+ * therefore check. Both are checked against the receiving subtype's published
44
+ * schema before the document is written.
45
+ *
46
+ * @module
47
+ */
48
+
49
+ import log from "loglevel";
50
+
51
+ import { resolveName, resolveImg } from "./helpers.mjs";
52
+ import { BasePackCompiler } from "./base-compiler.mjs";
53
+ import { journalPageId, splitPages } from "./journals.mjs";
54
+ import { foundryPackageId } from "./content-package.mjs";
55
+ import { itemDocEntryId, itemDocPointer } from "./item-docs.mjs";
56
+ // The whitelist and the per-type `system` builders both come from the resolved
57
+ // configuration, so the types this pass claims and the builders it compiles
58
+ // them with are one table — the consuming repository's, not this package's
59
+ // (#1504/#1563).
60
+ import { itemTypes, itemBuilder, itemArt, itemFields } from "./item-registry.mjs";
61
+ import { currentType } from "./ids.mjs";
62
+ // Which Foundry Item subtype a note's `type` compiles into. Looked up in the
63
+ // system's declared map, never inferred from the type itself (#79).
64
+ import { documentSubtype, subtypeRow } from "./document-subtypes.mjs";
65
+ // The note-level `<system>:` block: `<system>.system` onto the document's
66
+ // `system` verbatim, and `<system>.img` / `.effects` / `.flags` overriding
67
+ // their shared top-level forms for this system alone (#58).
68
+ import { blockField, blockProperty, claimedPaths, mergeSystemData } from "./system-block.mjs";
69
+
70
+ /**
71
+ * The description an item carries: a pointer to its **item doc**, the
72
+ * JournalEntry the journals pass compiles this same body into (#1348).
73
+ *
74
+ * The prose is not rendered into the item at all. Carrying it would duplicate
75
+ * it onto every actor holding the item — 7.59 MB of copies across the actors
76
+ * pack, of which 133 KB was distinct — where a link is 60 bytes and always
77
+ * current. The two passes derive the target from the note's own id, so neither
78
+ * has to see the other's output; both split the *converted* markdown, so an H1
79
+ * carrying a wikilink names the same page on both sides.
80
+ *
81
+ * An item with no prose points at nothing, exactly as the journals pass writes
82
+ * no entry for it.
83
+ *
84
+ * @param {string} markdown - The note body, tables expanded and wikilinks
85
+ * resolved.
86
+ * @param {object} fm - The note's frontmatter.
87
+ * @param {string} name - The item's name.
88
+ * @returns {string} The pointer, or "" for a note with no body.
89
+ */
90
+ export function itemDescription(markdown, fm, name) {
91
+ if (!String(markdown).trim()) return "";
92
+ const [leadPage] = splitPages(markdown, name);
93
+ const pageId = journalPageId(itemDocEntryId(fm.id), leadPage);
94
+ return itemDocPointer(foundryPackageId(), fm.id, name, pageId);
95
+ }
96
+
97
+ /**
98
+ * The Item compile pass of one game system.
99
+ *
100
+ * A subclass declares its {@link SystemItemCompiler.documentSubtypes} and, if
101
+ * its system writes any, the {@link SystemItemCompiler#commonSystem} keys. It
102
+ * declares nothing else: the class is abstract only in the sense that a map is
103
+ * required, and instantiating it without one is a programming error rather than
104
+ * a configuration one.
105
+ */
106
+ export class SystemItemCompiler extends BasePackCompiler {
107
+ static id = "items";
108
+ static label = "item";
109
+
110
+ /**
111
+ * An Item **is** a system's data, so this pack takes only notes carrying
112
+ * this system's block (#58).
113
+ */
114
+ static requiresSystemBlock = true;
115
+
116
+ /**
117
+ * The note-type → document-subtype map this pass compiles against.
118
+ *
119
+ * Stated by the class rather than reached for through a module import, so
120
+ * every decision the pass makes — which notes it claims, which subtype each
121
+ * becomes, which registry builds it, which block it reads — reads one
122
+ * declaration. A second system replaces that declaration and nothing else.
123
+ *
124
+ * @type {import("./document-subtypes.mjs").DocumentSubtypeMap|undefined}
125
+ */
126
+ static documentSubtypes = undefined;
127
+
128
+ /**
129
+ * The frontmatter block this pass reads, and the registry it addresses.
130
+ *
131
+ * @returns {string} The system's block name.
132
+ */
133
+ get system() {
134
+ const map = /** @type {typeof SystemItemCompiler} */ (this.constructor).documentSubtypes;
135
+ if (!map) {
136
+ throw new Error(
137
+ `${this.constructor.name} declares no \`documentSubtypes\` — an ` +
138
+ `Item pass reads its system's map to know which notes it ` +
139
+ `claims and what each becomes.`,
140
+ );
141
+ }
142
+ return map.block;
143
+ }
144
+
145
+ /**
146
+ * How many of each item type this pass wrote, for the summary. Every type
147
+ * is present from the start so the tally reads as a census of the
148
+ * whitelist rather than of what happened to compile.
149
+ *
150
+ * @type {Record<string, number>}
151
+ */
152
+ counts = Object.fromEntries([...itemTypes()].map((t) => [t, 0]));
153
+
154
+ /**
155
+ * Every content type that compiles into an item **for this system**.
156
+ *
157
+ * The whitelist is the consuming repository's `itemBuilders` keys (#1504),
158
+ * and the system's own map is a second filter on top of it: a type this
159
+ * system maps onto some *other* document class is not an item however a
160
+ * registry spells it, which is the "no wrongly-typed document" half of #79.
161
+ * A type the map does not name at all is left to the registry — see
162
+ * {@link SystemItemCompiler#itemSubtype}.
163
+ *
164
+ * @param {object} fm - The note's frontmatter.
165
+ * @returns {boolean} True for a whitelisted item type.
166
+ */
167
+ selects(fm) {
168
+ // Through {@link currentType}: the registry is keyed by the current
169
+ // spelling, and a note still on a renamed one compiles unchanged
170
+ // during the retirement window (#78).
171
+ if (!fm.type || !itemTypes().has(currentType(fm.type))) return false;
172
+ const map = /** @type {typeof SystemItemCompiler} */ (this.constructor).documentSubtypes;
173
+ const row = subtypeRow(/** @type {never} */ (map), fm.type);
174
+ return !row || row.document === "Item";
175
+ }
176
+
177
+ /**
178
+ * The Foundry Item subtype a note compiles into.
179
+ *
180
+ * **Looked up, not inferred.** For every type this system declares, the
181
+ * emitted subtype is the map's, so the note vocabulary and the document
182
+ * vocabulary are two separately-stated things rather than one string
183
+ * written twice (#79).
184
+ *
185
+ * **A type the map does not name belongs to the consumer**, and its
186
+ * registry entry is the declaration: a repository shipping an item type of
187
+ * its own writes it once, in the `itemBuilders` table of its
188
+ * `package-build.config.yaml`, and that key is what the document is a
189
+ * subtype of. That is an authored statement in the consumer's own
190
+ * configuration, not a coincidence inside this package's source — and
191
+ * refusing it here would silently drop every document of a type this system
192
+ * has no opinion about (#7/#1563).
193
+ *
194
+ * @param {object} fm - The note's frontmatter.
195
+ * @returns {string} The document's `type`.
196
+ */
197
+ itemSubtype(fm) {
198
+ const map = /** @type {typeof SystemItemCompiler} */ (this.constructor).documentSubtypes;
199
+ const declared = documentSubtype(/** @type {never} */ (map), fm.type, fm, {
200
+ absPath: this.currentNote?.absPath,
201
+ });
202
+ return declared ?? fm.type;
203
+ }
204
+
205
+ /** An item is named by its own type in the log, not by "item". */
206
+ noteLabel(fm) {
207
+ return fm.type;
208
+ }
209
+
210
+ /**
211
+ * The `system` keys this system's compiler writes on **every** item, beside
212
+ * whatever the type's declared fields emit.
213
+ *
214
+ * Nothing by default, which is the honest position for a system that has
215
+ * not said otherwise: a key written here lands on every document of every
216
+ * type, so inventing one that the receiving DataModel does not declare
217
+ * would be a finding on the whole pack (#155).
218
+ *
219
+ * @param {object} fm - The note's frontmatter.
220
+ * @param {object} at - What the pass already knows about this note.
221
+ * @param {string} at.description - The pointer to the note's item doc.
222
+ * @param {string} at.markdown - The note body, tables expanded and
223
+ * wikilinks resolved, for a system that reads an anchored section out of
224
+ * it.
225
+ * @param {string} at.label - Human-readable context for error messages.
226
+ * @returns {object} The shared `system` fields.
227
+ */
228
+ // eslint-disable-next-line no-unused-vars
229
+ commonSystem(fm, { description, markdown, label }) {
230
+ return {};
231
+ }
232
+
233
+ /**
234
+ * The document's `flags` — whatever the note authors, and whatever this
235
+ * system writes there of its own accord.
236
+ *
237
+ * The authored flags alone by default, which is the honest position for a
238
+ * system that has not said otherwise. A system whose data model has nowhere
239
+ * to record a shared fact keeps it here instead: HM3 writes the template
240
+ * priority as `flags.hm3.templatePriority`, because it declares no `system`
241
+ * field for it and an undeclared `system` key is discarded at load without
242
+ * a word.
243
+ *
244
+ * **This is the one emitted key nothing else can check** (#283). A `system`
245
+ * key this pass invents is caught by the emitted-`system` check against the
246
+ * receiving schema, but a flag is declared by no schema — so an omission
247
+ * here is silent, and was: the Actor pass wrote the priority and this one
248
+ * did not, for as long as there had been two passes.
249
+ *
250
+ * @param {object} fm - The note's frontmatter.
251
+ * @returns {object} The flags to emit.
252
+ */
253
+ commonFlags(fm) {
254
+ return blockProperty(fm, this.system, "flags", {});
255
+ }
256
+
257
+ /**
258
+ * Construct the full compendium envelope for one item.
259
+ *
260
+ * @param {object} fm - The note's frontmatter.
261
+ * @param {string} markdown - The body, tables expanded and wikilinks
262
+ * resolved.
263
+ * @returns {object} The item document, keyed for the pack.
264
+ */
265
+ buildEntry(fm, markdown) {
266
+ const system = this.system;
267
+ const type = fm.type;
268
+ const name = resolveName(fm);
269
+ const description = itemDescription(markdown, fm, name);
270
+ const id = fm.id;
271
+ const subType = this.itemSubtype(fm);
272
+ const built = {
273
+ ...this.commonSystem(fm, { description, markdown, label: `item "${name}"` }),
274
+ ...itemBuilder(type, system)(fm),
275
+ };
276
+ // Whatever the note authors under `<system>.system`, at the DataModel's
277
+ // own paths. A path a declared field already writes is left to that
278
+ // field: its value came from the same authored place and went through
279
+ // the field's own coercion (#58).
280
+ mergeSystemData(built, fm, {
281
+ block: system,
282
+ claimed: claimedPaths(itemFields(type, system)),
283
+ });
284
+ this.reportUndeclaredSystemData(fm, system, "Item", subType);
285
+ // And what *this* pass wrote on its own initiative, which no field
286
+ // declaration states and so no other check can see (#155). Read off the
287
+ // assembled block, so a key added to `commonSystem` is checked without
288
+ // anyone remembering to list it.
289
+ this.reportEmittedSystemData(built, {
290
+ fm,
291
+ block: system,
292
+ documentType: "Item",
293
+ subType,
294
+ type,
295
+ fields: itemFields(type, system),
296
+ });
297
+
298
+ const effects = blockProperty(fm, system, "effects");
299
+ // Read through the system block like every other item field, so both
300
+ // spellings work wherever a note already writes one. `packFolder` is a
301
+ // folder note's address and `folder` an id; which it is comes from the
302
+ // field, never from the string (#251, #255).
303
+ const packFolderAddress = blockField(fm, system, "packFolder", null);
304
+ const folder =
305
+ packFolderAddress ?
306
+ this.folderResolver(packFolderAddress, { isAddress: true })
307
+ : this.folderResolver(blockField(fm, system, "folder", null));
308
+
309
+ return {
310
+ name,
311
+ // The note's `type` addresses the builder and the default art —
312
+ // both registries are keyed by content type — while the document's
313
+ // own subtype comes from the system's map (#79).
314
+ type: subType,
315
+ // Nullish, not `||` (#218): `resolveImg` returns `null` for a
316
+ // note that names no art and `""` for one that wants none, and only
317
+ // the first may be replaced by the type's default.
318
+ img: resolveImg(blockProperty(fm, system, "img")) ?? itemArt(type, system),
319
+ _id: id,
320
+ system: built,
321
+ effects: Array.isArray(effects) ? [...effects] : [],
322
+ // Whatever the note authors, plus whatever this system records in
323
+ // flags because its data model has nowhere else for it (#283).
324
+ flags: this.commonFlags(fm),
325
+ _stats: this.stats,
326
+ ownership: { default: 0 },
327
+ folder,
328
+ _key: `!items!${id}`,
329
+ };
330
+ }
331
+
332
+ /** @inheritdoc */
333
+ onCompiled(fm) {
334
+ this.counts[currentType(fm.type)]++;
335
+ }
336
+
337
+ /** @inheritdoc */
338
+ reportCompiled(stats) {
339
+ log.info(`Compiled ${stats.compiled} items:`);
340
+ for (const [t, n] of Object.entries(this.counts)) {
341
+ if (n > 0) log.info(` ${t}: ${n}`);
342
+ }
343
+ }
344
+
345
+ /** @inheritdoc */
346
+ reportDetail(stats) {
347
+ log.debug(`Skipped ${stats.skippedOther} non-item file(s) (no recognized type)`);
348
+ }
349
+ }
@@ -44,6 +44,7 @@
44
44
 
45
45
  import { compendiumUuid, makeId, pageUuid } from "./ids.mjs";
46
46
  import { loadPackConfig } from "./pack-config.mjs";
47
+ import { currentType } from "./ids.mjs";
47
48
  import { itemTypes } from "./item-registry.mjs";
48
49
  import { packRouter } from "./pack-router.mjs";
49
50
 
@@ -97,7 +98,12 @@ export function docEntryTypes() {
97
98
  * false for `doc` and for actors.
98
99
  */
99
100
  export function hasDocEntry(type) {
100
- return docEntryTypes().has(String(type));
101
+ // Through {@link currentType}, because the set is derived from the item
102
+ // registry's keys and those are the *current* spelling of a note type. A
103
+ // note still on a renamed one carries its documentation journal exactly as
104
+ // before — this is the one lookup between an item compiling and its prose
105
+ // silently compiling into nothing (#78).
106
+ return docEntryTypes().has(String(currentType(type)));
101
107
  }
102
108
 
103
109
  /**
@@ -40,6 +40,7 @@
40
40
  * @module
41
41
  */
42
42
 
43
+ import { currentType } from "./ids.mjs";
43
44
  import { loadPackConfig } from "./pack-config.mjs";
44
45
  import { resolveImg } from "./helpers.mjs";
45
46
 
@@ -83,6 +84,11 @@ export function itemTypes() {
83
84
  */
84
85
  function lookup(what, table, type, system) {
85
86
  const config = loadPackConfig();
87
+ // The registry is keyed by the current spelling of a note type, and a note
88
+ // still on a renamed one resolves through it unchanged. One place, because
89
+ // every type-keyed table this module reaches — builders, art, fields — is
90
+ // indexed here (#78).
91
+ type = currentType(type);
86
92
  if (system !== undefined) {
87
93
  const perSystem = /** @type {Record<string, Record<string, unknown>>} */ (
88
94
  config[`${table}BySystem`]