@heroiclands/package-build 19.0.0 → 20.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 (85) hide show
  1. package/CHANGELOG.md +1100 -0
  2. package/CONTENT.md +264 -33
  3. package/README.md +43 -4
  4. package/bin/content-build.mjs +94 -3
  5. package/config.mjs +9 -1
  6. package/content-config.mjs +99 -19
  7. package/docs/content-format.md +394 -72
  8. package/e2e.mjs +297 -3
  9. package/engine/actor-compiler.mjs +197 -7
  10. package/engine/address-charset.mjs +23 -5
  11. package/engine/base-compiler.mjs +63 -2
  12. package/engine/bundles.mjs +9 -0
  13. package/engine/content-address.mjs +92 -1
  14. package/engine/content-charset.mjs +434 -0
  15. package/engine/content-format.mjs +102 -0
  16. package/engine/content-icons.mjs +388 -0
  17. package/engine/content-index.mjs +11 -8
  18. package/engine/content-links.mjs +37 -21
  19. package/engine/field-reference.mjs +57 -5
  20. package/engine/field-spec.mjs +214 -7
  21. package/engine/folder-notes.mjs +24 -1
  22. package/engine/foreign-catalog.mjs +112 -7
  23. package/engine/foundry-entries.mjs +14 -0
  24. package/engine/frontmatter-lint.mjs +377 -56
  25. package/engine/frontmatter.mjs +11 -11
  26. package/engine/generate.mjs +72 -12
  27. package/engine/helpers.mjs +96 -10
  28. package/engine/index.mjs +9 -0
  29. package/engine/item-compiler.mjs +37 -0
  30. package/engine/journals.mjs +21 -4
  31. package/engine/macros.mjs +8 -0
  32. package/engine/map-notes.mjs +7 -7
  33. package/engine/note-claims.mjs +208 -5
  34. package/engine/note-ids.mjs +25 -1
  35. package/engine/note-vocabulary.mjs +76 -9
  36. package/engine/pack-config.mjs +102 -12
  37. package/engine/pack-router.mjs +0 -0
  38. package/engine/prose-config.mjs +42 -0
  39. package/engine/prose-lint.mjs +126 -0
  40. package/engine/retired-fields.mjs +57 -16
  41. package/engine/runtime-only-fields.mjs +204 -0
  42. package/engine/scenes.mjs +12 -19
  43. package/engine/schema-check.mjs +23 -1
  44. package/engine/schema-extract.mjs +13 -0
  45. package/engine/site-index.mjs +17 -0
  46. package/engine/subtype-registry.mjs +30 -0
  47. package/engine/system-block.mjs +81 -3
  48. package/engine/web-wikilinks.mjs +33 -27
  49. package/engine/wikilink-syntax.mjs +7 -0
  50. package/engine/wikilinks.mjs +74 -16
  51. package/hm3/actors.mjs +63 -13
  52. package/package.json +2 -2
  53. package/sohl/actors.mjs +106 -7
  54. package/sohl/item-fields.mjs +203 -0
  55. package/sohl/note-schemas.mjs +6 -3
  56. package/types/config.d.mts +7 -0
  57. package/types/e2e.d.mts +130 -3
  58. package/types/engine/actor-compiler.d.mts +83 -3
  59. package/types/engine/address-charset.d.mts +22 -4
  60. package/types/engine/base-compiler.d.mts +54 -3
  61. package/types/engine/content-address.d.mts +64 -0
  62. package/types/engine/content-charset.d.mts +127 -0
  63. package/types/engine/content-format.d.mts +9 -0
  64. package/types/engine/content-icons.d.mts +151 -0
  65. package/types/engine/field-spec.d.mts +271 -3
  66. package/types/engine/folder-notes.d.mts +20 -0
  67. package/types/engine/foreign-catalog.d.mts +38 -2
  68. package/types/engine/foundry-entries.d.mts +6 -0
  69. package/types/engine/frontmatter-lint.d.mts +164 -30
  70. package/types/engine/frontmatter.d.mts +11 -11
  71. package/types/engine/generate.d.mts +27 -0
  72. package/types/engine/helpers.d.mts +45 -9
  73. package/types/engine/index.d.mts +3 -0
  74. package/types/engine/map-notes.d.mts +2 -2
  75. package/types/engine/note-claims.d.mts +67 -0
  76. package/types/engine/note-ids.d.mts +14 -0
  77. package/types/engine/pack-config.d.mts +35 -0
  78. package/types/engine/prose-config.d.mts +41 -0
  79. package/types/engine/prose-lint.d.mts +36 -0
  80. package/types/engine/retired-fields.d.mts +29 -13
  81. package/types/engine/runtime-only-fields.d.mts +102 -0
  82. package/types/engine/schema-check.d.mts +10 -1
  83. package/types/engine/subtype-registry.d.mts +21 -0
  84. package/types/engine/system-block.d.mts +28 -2
  85. package/types/sohl/actors.d.mts +3 -3
@@ -1,3 +1,97 @@
1
+ /**
2
+ * What one system block accepts beyond the keys every block carries.
3
+ *
4
+ * @typedef {object} SystemBlockSpec
5
+ * @property {readonly string[]} [known] - Keys stated outright.
6
+ * @property {boolean} [fieldVocabulary] - Whether the note type's declared field
7
+ * names, as the caller's `schemas` state them, are keys of this block.
8
+ * @property {Readonly<Record<string, readonly object[]>>} [fields] - Type → this
9
+ * system's own declared fields. A type it does not name is a type this system
10
+ * says nothing about, and its block is left unchecked on such a note rather
11
+ * than reported wholesale.
12
+ */
13
+ /**
14
+ * Every system a configuration says its tree carries (#58).
15
+ *
16
+ * **Which systems a package ships for is already declared**, in three places
17
+ * that answer different questions, so this reads all three rather than asking a
18
+ * new one:
19
+ *
20
+ * - `systems:` (#48) declares them without requiring one, which is how a
21
+ * package ships for several;
22
+ * - a **pack's** `system:` is the same statement made per pack, and it is the
23
+ * one some trees make: `harn-ensemble` declares an `actors-sohl` and an
24
+ * `actors-hm3` and nothing else about either system. It is already
25
+ * authoritative elsewhere — `eligibleFor` fails a note for want of the block
26
+ * a pack's `system:` names — so a lint that did not read it would refuse a
27
+ * note at compile for a block it never checked;
28
+ * - `stats.systemId` is the package-wide answer where there is one, and it has
29
+ * already absorbed every way of spelling that: a system package is its own
30
+ * system, and a module takes `requiresSystem`, its lone `systems:` entry, or
31
+ * its lone system relationship.
32
+ *
33
+ * A package naming a system in none of them is system-agnostic on purpose — its
34
+ * packs are core document types carrying no system data — so it carries no
35
+ * system block and naming one would invent it.
36
+ *
37
+ * @param {object} [config] - A resolved configuration from `defineConfig`.
38
+ * @returns {string[]} The system ids, deduplicated, in declared order.
39
+ */
40
+ export function declaredSystems(config?: object): string[];
41
+ /**
42
+ * The system blocks a configuration says its tree carries, and what each
43
+ * accepts (#58).
44
+ *
45
+ * The lint checks the blocks its caller names, and for as long as there was one
46
+ * system the only caller named none — so every tree took the `sohl:` of
47
+ * {@link DEFAULT_SYSTEM_BLOCKS}, a constant, in a module whose whole discipline
48
+ * is that it states no vocabulary of its own. That is wrong in both directions
49
+ * the moment a second system exists, and the second direction is the worse:
50
+ *
51
+ * - a package shipping for `hm3` had its `hm3:` block **never looked at**, so
52
+ * every key in it was discarded at compile without a word — the silent-drop
53
+ * family this check exists to close;
54
+ * - and the block that *was* checked was named after a system that package does
55
+ * not ship for, so the one finding it could make was about nothing.
56
+ *
57
+ * **A block's vocabulary has two sources, and a system may have both.**
58
+ *
59
+ * - The **note schemas** the caller hands in as `schemas`. Those belong to one
60
+ * system — the CLI imports `sohl/note-schemas.mjs` — and `schemaSystem` is the
61
+ * caller naming which, because only the caller knows. It is the only source
62
+ * that reaches a type no item registry declares, which is to say `being`: the
63
+ * 2,512 notes `harn-ensemble` is made of, and the reason this is not an
64
+ * optional refinement.
65
+ * - The system's **own registry**, `itemFieldsBySystem`, keyed by system and
66
+ * until now read by nothing. This is what a *second* system's block is held
67
+ * to, since the note schemas describe its neighbour.
68
+ *
69
+ * A system with neither is left out: nothing can state what its block may
70
+ * carry, and holding it to an empty vocabulary would report every key in a
71
+ * correct tree. **That is a check that does not run**, which is
72
+ * indistinguishable from one that passed, so the caller says it out loud —
73
+ * {@link declaredSystems} is the other half of that comparison. `harn-ensemble`
74
+ * is the tree it names: two systems, and an `itemBuilders` registry for
75
+ * neither, so its `hm3:` block is unchecked until it declares one.
76
+ *
77
+ * An earlier draft of this took the note schemas for a system's vocabulary only
78
+ * where the package declared **one** system, on the reasoning that with several
79
+ * there is nothing to say which one they describe. There is: the caller, which
80
+ * chose them. The guess cost `harn-ensemble` its whole `sohl:` check — two
81
+ * systems declared, so the fallback never fired — which is the coverage this
82
+ * change exists to widen rather than narrow.
83
+ *
84
+ * @param {object} [config] - A resolved configuration from `defineConfig`.
85
+ * @param {object} [options] - Options.
86
+ * @param {string} [options.schemaSystem] - The system whose vocabulary the
87
+ * caller's `schemas` state. There are two systems, not an open set, so this is
88
+ * one word from the caller rather than a mechanism.
89
+ * @returns {Readonly<Record<string, SystemBlockSpec>>} The blocks to check, in
90
+ * declared order. A system nothing states the vocabulary of is absent.
91
+ */
92
+ export function systemBlocksFor(config?: object, { schemaSystem }?: {
93
+ schemaSystem?: string | undefined;
94
+ }): Readonly<Record<string, SystemBlockSpec>>;
1
95
  /**
2
96
  * Whether a value satisfies a declared {@link FieldSpec.kind}.
3
97
  *
@@ -26,23 +120,32 @@ export function matchesKind(value: unknown, kind: string): boolean;
26
120
  * note against whatever its type declares and knows no type names of its
27
121
  * own. Its absence skips the `data:` and `subType` checks rather than
28
122
  * reporting every key as unknown.
29
- * @param {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>} [opts.systems]
30
- * The system blocks to check, and what each accepts. See
31
- * {@link DEFAULT_SYSTEM_BLOCKS}.
123
+ * @param {(type: string) => {document: string|null, art: readonly string[]}|null} [opts.emittedArt]
124
+ * What art a note of one type reaches its document through — the passes' own
125
+ * declaration, asked through `engine/generate.mjs`'s `emittedArtFor`.
126
+ * Supplied by the caller like `schemas`, so this module states no list of
127
+ * iconless types of its own; absent it, an inert `img:` goes unreported
128
+ * rather than every note's being (#349).
129
+ * @param {Readonly<Record<string, SystemBlockSpec>>} [opts.systems]
130
+ * The system blocks to check, and what each accepts. Supplied by the caller
131
+ * for the same reason `schemas` is — a build derives them from its
132
+ * configuration through {@link systemBlocksFor}, and this module states no
133
+ * system name of its own. See {@link DEFAULT_SYSTEM_BLOCKS} for the fallback.
32
134
  * @param {readonly string[]} [opts.packs] - The pack names this package
33
135
  * declares, for a `data:` field whose map is keyed by pack. Supplied by the
34
136
  * caller like `schemas` and `vocabulary`, and absent it no claim is made
35
137
  * about those keys.
36
138
  * @returns {object[]} Findings, each with a locator where one is obtainable.
37
139
  */
38
- export function lintNote(note: object, { schemas, index, vocabulary, packs, systems }: {
140
+ export function lintNote(note: object, { schemas, index, vocabulary, packs, emittedArt, systems }: {
39
141
  schemas: Record<string, readonly object[]>;
40
142
  index?: object | undefined;
41
143
  vocabulary?: Record<string, object> | undefined;
42
- systems?: Readonly<Record<string, {
43
- known?: readonly string[];
44
- fieldVocabulary?: boolean;
45
- }>> | undefined;
144
+ emittedArt?: ((type: string) => {
145
+ document: string | null;
146
+ art: readonly string[];
147
+ } | null) | undefined;
148
+ systems?: Readonly<Record<string, SystemBlockSpec>> | undefined;
46
149
  packs?: readonly string[] | undefined;
47
150
  }): object[];
48
151
  /**
@@ -54,22 +157,26 @@ export function lintNote(note: object, { schemas, index, vocabulary, packs, syst
54
157
  * @param {Record<string, object>} [opts.vocabulary] - Type → the closed regions
55
158
  * it declares (#128); see {@link lintNote}.
56
159
  * @param {boolean} [opts.references=true] - Whether to check references.
57
- * @param {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>} [opts.systems]
58
- * The system blocks to check. See {@link DEFAULT_SYSTEM_BLOCKS}.
160
+ * @param {Readonly<Record<string, SystemBlockSpec>>} [opts.systems]
161
+ * The system blocks to check; see {@link lintNote} and
162
+ * {@link systemBlocksFor}.
59
163
  * @param {readonly string[]} [opts.packs] - The declared pack names; see
60
164
  * {@link lintNote}.
165
+ * @param {(type: string) => {document: string|null, art: readonly string[]}|null} [opts.emittedArt]
166
+ * What art a type reaches its document through; see {@link lintNote}.
61
167
  * @returns {{findings: object[], notes: number}} The findings, and how many
62
168
  * notes were inspected.
63
169
  */
64
- export function lintFrontmatter(index: object, { schemas, vocabulary, packs, references, systems }: {
170
+ export function lintFrontmatter(index: object, { schemas, vocabulary, packs, emittedArt, references, systems }: {
65
171
  schemas: Record<string, readonly object[]>;
66
172
  vocabulary?: Record<string, object> | undefined;
67
173
  references?: boolean | undefined;
68
- systems?: Readonly<Record<string, {
69
- known?: readonly string[];
70
- fieldVocabulary?: boolean;
71
- }>> | undefined;
174
+ systems?: Readonly<Record<string, SystemBlockSpec>> | undefined;
72
175
  packs?: readonly string[] | undefined;
176
+ emittedArt?: ((type: string) => {
177
+ document: string | null;
178
+ art: readonly string[];
179
+ } | null) | undefined;
73
180
  }): {
74
181
  findings: object[];
75
182
  notes: number;
@@ -107,21 +214,48 @@ export const UNIVERSAL_KEYS: ReadonlySet<string>;
107
214
  * The system blocks a build checks, and what each accepts beyond the shared
108
215
  * vocabulary.
109
216
  *
110
- * One entry, because one system is what every existing tree declares — and the
111
- * default is a *declaration*, not a hard-coded assumption: a build that ships
112
- * content for two systems passes both, and each block is then checked against
113
- * its own vocabulary rather than against the other's (#58). A block nothing
114
- * declares is not checked, because nothing can say what it may carry, and
115
- * inventing a rule for it would report a correct tree red.
217
+ * One entry, because one system is what every existing tree declares — and it
218
+ * is a *fallback*, not the rule. {@link systemBlocksFor} derives the map from
219
+ * the configuration, which is what makes the block a package actually ships for
220
+ * the block that gets checked; this is what a caller holding no configuration
221
+ * gets, which in practice is a unit test.
116
222
  *
117
- * `fieldVocabulary` says the note type's own declared field names are keys of
118
- * this block. True for `sohl` and untrue in general: those names come from the
119
- * `itemBuilders` registry that this system declares, and a second system's
120
- * notes write a second system's fields.
223
+ * A block nothing declares is not checked, because nothing can say what it may
224
+ * carry, and inventing a rule for it would report a correct tree red.
121
225
  *
122
- * @type {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>}
226
+ * Three ways a block may state its vocabulary, and a spec declares at most one:
227
+ *
228
+ * - `known` — an explicit list of keys, for a caller stating them outright.
229
+ * - `fieldVocabulary` — the note type's own declared field names, as the
230
+ * caller's `schemas` state them, are keys of this block. That holds for the
231
+ * **one** system a single-registry tree ships for, where `schemas` *is* that
232
+ * system's vocabulary, and in general holds for no other.
233
+ * - `fields` — type → that system's own declared fields, from the registry the
234
+ * system declares. What a second system's block is checked against, because a
235
+ * second system's notes write a second system's fields and the note-type
236
+ * schemas describe somebody else's.
237
+ *
238
+ * @type {Readonly<Record<string, SystemBlockSpec>>}
123
239
  */
124
- export const DEFAULT_SYSTEM_BLOCKS: Readonly<Record<string, {
125
- known?: readonly string[];
126
- fieldVocabulary?: boolean;
127
- }>>;
240
+ export const DEFAULT_SYSTEM_BLOCKS: Readonly<Record<string, SystemBlockSpec>>;
241
+ /**
242
+ * What one system block accepts beyond the keys every block carries.
243
+ */
244
+ export type SystemBlockSpec = {
245
+ /**
246
+ * - Keys stated outright.
247
+ */
248
+ known?: readonly string[] | undefined;
249
+ /**
250
+ * - Whether the note type's declared field
251
+ * names, as the caller's `schemas` state them, are keys of this block.
252
+ */
253
+ fieldVocabulary?: boolean | undefined;
254
+ /**
255
+ * - Type → this
256
+ * system's own declared fields. A type it does not name is a type this system
257
+ * says nothing about, and its block is left unchecked on such a note rather
258
+ * than reported wholesale.
259
+ */
260
+ fields?: Readonly<Record<string, readonly object[]>> | undefined;
261
+ };
@@ -132,13 +132,15 @@ export function parseValueDesc(raw: any): {
132
132
  maxValue: number;
133
133
  }[];
134
134
  /**
135
- * The compendium folder a note names, and how it named it.
135
+ * The compendium folder a note names.
136
136
  *
137
- * Two spellings, deliberately not merged into one value: `packFolder:` is a
138
- * folder note's **address** (`folder-poisonsandtoxins`) and `folder:` is a
139
- * Foundry **id** (`ONXsqZAIZr2qzxTb`). Which one a value is cannot be told from
140
- * the string both are alphanumeric so the field it was written in is what
141
- * says, and that answer is carried rather than re-derived (#251).
137
+ * **There is one spelling.** `packFolder:` is a folder note's **address**
138
+ * (`folder-poisonsandtoxins`), resolved through the address index the whole
139
+ * build shares. The `folder:` Foundry-id spelling this function once read
140
+ * beside it, and the per-pack `*-folders.yaml` that id was resolved against,
141
+ * are retired together (#260) a note declaring `folder:` is refused by
142
+ * {@link module:engine/folder-notes.assertNoDeclaredFolder} rather than
143
+ * reaching here, so there is no second source for a value to come from.
142
144
  *
143
145
  * **`packFolder` was a path for one release** (`Possessions/Misc_Gear/Cooking`)
144
146
  * and is an address now (#255). A path encoded the hierarchy *in the value*, so
@@ -149,13 +151,11 @@ export function parseValueDesc(raw: any): {
149
151
  * authors to migrate, which is the whole reason the change was cheap enough to
150
152
  * make.
151
153
  *
152
- * `packFolder` wins where both are present. Nothing about `folder` changes: a
153
- * note that names one is read, resolved and emitted exactly as before, until
154
- * #260 retires it.
155
- *
156
154
  * @param {object|null|undefined} fm - Parsed frontmatter.
157
155
  * @returns {{value: string|null, isAddress: boolean}} The authored value, and
158
- * whether it is a folder note's address.
156
+ * whether it is a folder note's address. `isAddress` is always `true` and is
157
+ * kept so a caller reads the same shape it always did; it distinguished the
158
+ * two spellings, and there is only one left to be.
159
159
  */
160
160
  export function folderField(fm: object | null | undefined): {
161
161
  value: string | null;
@@ -8,6 +8,33 @@
8
8
  * rather than defaulting past.
9
9
  */
10
10
  export function compilerFor(docType: string, system?: string | null): Function | undefined;
11
+ /**
12
+ * The art fields a note of one content type reaches its document through, and
13
+ * the document it reaches (#349).
14
+ *
15
+ * **Derived, never listed.** A note's type routes to a document type
16
+ * ({@link packForType}), a document type routes to the pass that compiles it
17
+ * ({@link compilerFor}), and the pass declares which art it emits
18
+ * ({@link BasePackCompiler.emitsArt}). So the answer is assembled from the same
19
+ * three statements the compile itself follows, and a pass that starts or stops
20
+ * emitting art changes this by changing its own declaration. A second table of
21
+ * "types with no image" would be a table free to drift from what is emitted,
22
+ * which is the defect this exists to report rather than to reproduce.
23
+ *
24
+ * **The union across systems**, because a note is compiled by whichever pack
25
+ * claims it: a tree feeding both SoHL and HM3 has two Actor passes, and a field
26
+ * either of them emits is live for the note. Only a field *no* pass emits is
27
+ * inert, and that is the finding this supports.
28
+ *
29
+ * @param {string} type - The note's content type.
30
+ * @returns {{document: string|null, art: readonly string[]}|null} What the type
31
+ * compiles into and the art it carries there, or `null` where no claim can be
32
+ * made — a retired type, which is reported as retired instead.
33
+ */
34
+ export function emittedArtFor(type: string): {
35
+ document: string | null;
36
+ art: readonly string[];
37
+ } | null;
11
38
  /**
12
39
  * The generated JSON of **every** configured Item pack — what the actors pass
13
40
  * reads its predefined items from.
@@ -169,19 +169,47 @@ export function systemTemplatePriority(fm: object, label: string): number | null
169
169
  * alphanumeric runs replaced by underscores.
170
170
  */
171
171
  export function makeFilename(name: any, id: any): string;
172
- /**
173
- * Standardize a name into a slug: lowercase, apostrophes removed,
174
- * non-alphanumerics collapsed to single hyphens.
175
- */
176
172
  /**
177
173
  * Translate a content-relative image path into its Foundry-relative form.
178
174
  *
179
175
  * Content frontmatter (`img` / `portrait`) authors a single path that has to
180
- * work for Foundry, the knowledgebase, and the website. For Foundry the bundled
181
- * asset roots `icons/...` and `images/...` are served from the package
182
- * directory, so they are rewritten to `<assetRoot>/<path>` — `systems/sohl/assets`
183
- * for this repository, `modules/<id>/assets` for a module (#1508). Any other
184
- * path (already package-rooted, an absolute URL) is returned unchanged.
176
+ * work for Foundry, the knowledgebase, and the website. **Its first segment
177
+ * says which package owns the file** (#331), and there are exactly three
178
+ * answers:
179
+ *
180
+ * | Authored path starts with | Owner | Emitted |
181
+ * | ------------------------- | --------------------- | -------------------- |
182
+ * | `systems/` | a separate **system** | unchanged |
183
+ * | `modules/` | a separate **module** | unchanged |
184
+ * | anything else | **this package** | `<assetRoot>/<path>` |
185
+ *
186
+ * So `icons/relic.svg` compiles to `systems/sohl/assets/icons/relic.svg` here
187
+ * and to `modules/sohl-thalorna/assets/icons/relic.svg` in a module — the asset
188
+ * root is derived from the configuration, and is the one place `systems/sohl`
189
+ * is ever spelled (#1508). An authored
190
+ * `systems/sohl/assets/icons/noun/shield.svg` is left exactly as written,
191
+ * whichever package is compiling it.
192
+ *
193
+ * **This is a rule about ownership, not an allowlist of directories.** It used
194
+ * to prefix `icons/…` and `images/…` and pass everything else through — the
195
+ * same answer for every path any tree authors today, and the wrong one for the
196
+ * next directory a package ships. `sohl-kethira-basic` keeps art under
197
+ * `assets/artwork/`, so an authored `artwork/deity.webp` would have shipped
198
+ * unprefixed: a 404 in Foundry, reported by nothing. That a package owns its
199
+ * own tree is the fact; the directory names inside it are that package's
200
+ * business (#331).
201
+ *
202
+ * **Off-install addresses pass through too**, which is the same rule rather
203
+ * than a fourth: a URL, a `data:` URI, or a `/`-rooted path names something no
204
+ * package owns. See {@link addressesAnotherPackage}.
205
+ *
206
+ * **`banner:` does not follow this rule, deliberately (#331).** It is not an
207
+ * asset path inside a Foundry install at all: it reaches no compiled document,
208
+ * and its only consumer is the Hugo theme, which prefixes a relative value with
209
+ * `images/` and joins it onto `params.cdnBaseURL`. The two fields look alike
210
+ * and address different places — `img:` a file Foundry serves, `banner:` a file
211
+ * the CDN serves — so they are documented apart rather than reconciled into one
212
+ * rule that would be true of neither.
185
213
  *
186
214
  * **Two empties, and they mean opposite things (#218).** `null` — or an absent
187
215
  * key, which reaches here as `undefined` — means _unset_: the note names no art
@@ -434,6 +462,14 @@ export function expandNoteTables(body: string, { docs, name, fm, bodyLine, sqlTa
434
462
  * underscores.
435
463
  */
436
464
  export function folderFilename(name: any, id: any): string;
465
+ /**
466
+ * The markdown renderer every surface shares.
467
+ *
468
+ * `html: true` is long-standing and load-bearing — notes carry raw blocks — and
469
+ * it is also why {@link module:engine/content-icons} exists rather than an
470
+ * instruction to write `<i class="fa-solid …">` by hand: that would render on
471
+ * the two HTML surfaces and be silently dropped by the third (#378).
472
+ */
437
473
  export const md: import("markdown-it").MarkdownIt;
438
474
  export { slugify } from "./content-slug.mjs";
439
475
  export { makeId } from "./ids.mjs";
@@ -10,6 +10,7 @@ export * as folderNotes from "./folder-notes.mjs";
10
10
  export * as contentPackage from "./content-package.mjs";
11
11
  export * as notePackage from "./note-package.mjs";
12
12
  export * as retiredFields from "./retired-fields.mjs";
13
+ export * as runtimeOnlyFields from "./runtime-only-fields.mjs";
13
14
  export * as homepage from "./homepage.mjs";
14
15
  export * as noteSchemas from "./note-schemas.mjs";
15
16
  export * as noteVocabulary from "./note-vocabulary.mjs";
@@ -24,6 +25,8 @@ export * as foundryEntries from "./foundry-entries.mjs";
24
25
  export * as contentIndex from "./content-index.mjs";
25
26
  export * as siteBuild from "./site-build.mjs";
26
27
  export * as contentLint from "./content-lint.mjs";
28
+ export * as contentCharset from "./content-charset.mjs";
29
+ export * as contentIcons from "./content-icons.mjs";
27
30
  export * as contentLinks from "./content-links.mjs";
28
31
  export * as webWikilinks from "./web-wikilinks.mjs";
29
32
  export * as contentTables from "./content-tables.mjs";
@@ -158,8 +158,8 @@ export function buildScene(fm: object, ctx: object): object;
158
158
  * @param {string} sceneId - The owning scene's `_id`.
159
159
  * @param {string} [img] - The background art, already resolved from the note.
160
160
  * Passed by {@link buildScene}, which reads it from the note rather than from
161
- * the block; defaults to whichever spelling the block itself carries, so a
162
- * direct two-argument call still works (#142).
161
+ * the block; defaults to the block's own `img`, so a direct two-argument call
162
+ * still works.
163
163
  * @returns {object} The Level document, keyed for the pack.
164
164
  */
165
165
  export function buildLevel(sohl: object, sceneId: string, img?: string): object;
@@ -15,6 +15,59 @@
15
15
  * @returns {ReadonlySet<string>} The note types such a pass would claim.
16
16
  */
17
17
  export function noteTypesClaimedBy(docType: string, sources?: ClaimSources): ReadonlySet<string>;
18
+ /**
19
+ * Every document class a note of one type compiles into (#152).
20
+ *
21
+ * **A note produces more than one document, and that is the ordinary case.** An
22
+ * item note compiles an Item *and* the JournalEntry its prose becomes; a map
23
+ * note a Scene and a JournalEntry; since #337 an actor note an Actor and a
24
+ * JournalEntry too. {@link claimedNoteTypes} unions over the configured packs
25
+ * and so answers "is this note compiled *at all*", which is #146's question and
26
+ * cannot see a note that compiles one of its two documents and loses the other.
27
+ *
28
+ * Asked of the **claim table** rather than of a list of its own, so the set of
29
+ * documents a type produces and the set of passes that claim it are one
30
+ * statement. A pass that starts claiming a type starts producing its document
31
+ * here, with nothing to remember.
32
+ *
33
+ * **Union across systems, never per system.** A type one system maps and
34
+ * another does not appears once, because the `Item` and `Actor` rows already
35
+ * fold the maps together — so this cannot report a document class a system
36
+ * deliberately declines to produce, which is the silence #79 requires.
37
+ *
38
+ * ## The JournalEntry row is the one that is per *note*
39
+ *
40
+ * Every other row is a property of the type: a `macro` note produces a Macro, a
41
+ * map note a Scene, whatever either says. Documentation is not. `Journals`
42
+ * declines a doc-carrying note whose body is empty — *"an item with no prose
43
+ * gets no doc, and the items pass leaves its description empty rather than
44
+ * pointing at nothing"* — so whether an item note produces a JournalEntry is
45
+ * decided by the note, not by its type.
46
+ *
47
+ * That distinction is the whole difference between a useful finding and a
48
+ * useless one. `sohl-kethira-basic` declares no JournalEntry pack and ships 393
49
+ * notes whose descriptions are *deliberately* empty, under the Fan Material
50
+ * Guidelines its configuration explains at length. A type-level answer would
51
+ * report every one of them for losing a document none of them produces. Asking
52
+ * per note, it reports none, and still reports `harn-ensemble`'s 2,517 beings,
53
+ * whose `{#appearance}` and `{#dossier}` prose is real and is lost.
54
+ *
55
+ * `hasProse` is therefore how the caller answers that, and it is a **thunk** so
56
+ * that the file is read only where the answer could change the outcome. Omitted,
57
+ * the answer is the type's full potential — every document such a note *could*
58
+ * produce — which is what a caller asking about a type rather than a note wants.
59
+ *
60
+ * @param {string} type - The note's declared `type`, current spelling.
61
+ * @param {ClaimSources} [sources] - What to answer from.
62
+ * @param {object} [opts] - Options.
63
+ * @param {(() => boolean)|boolean} [opts.hasProse] - Whether *this note* carries
64
+ * a body. Omitted, the type's potential is reported.
65
+ * @returns {string[]} The document classes, in {@link CLAIMABLE_DOCUMENT_TYPES}
66
+ * order. Empty for a type nothing compiles.
67
+ */
68
+ export function documentClassesFor(type: string, sources?: ClaimSources, { hasProse }?: {
69
+ hasProse?: boolean | (() => boolean) | undefined;
70
+ }): string[];
18
71
  /**
19
72
  * Every note type some pack in a configuration would compile.
20
73
  *
@@ -133,6 +186,20 @@ export const UNIMPLEMENTED_TYPES: ReadonlySet<string>;
133
186
  */
134
187
  export const DERIVED_PACKED_TYPES: ReadonlySet<string>;
135
188
  export { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
189
+ /**
190
+ * Every Foundry document class {@link noteTypesClaimedBy} answers for.
191
+ *
192
+ * The switch above, read the other way round. It is written out rather than
193
+ * derived because a `switch` cannot be enumerated — and
194
+ * `tests/unclaimed-note-types.test.ts` holds the two together by checking that
195
+ * no class outside this list claims anything, so a row added there and not here
196
+ * fails rather than going quiet.
197
+ *
198
+ * Order is the order a reader meets them in a message, not a precedence.
199
+ *
200
+ * @type {readonly string[]}
201
+ */
202
+ export const CLAIMABLE_DOCUMENT_TYPES: readonly string[];
136
203
  /**
137
204
  * What a claim question is asked against.
138
205
  *
@@ -1,6 +1,20 @@
1
1
  /**
2
2
  * The document id a note compiles under: its pin, or its address.
3
3
  *
4
+ * **One type hashes its address differently, and that is not an exception to
5
+ * the rule but an application of it.** A `Folder` is a document of its own
6
+ * class, and its id is hashed under the `folder` namespace so that a folder and
7
+ * an item sharing a shortcode cannot derive one id — a collision Foundry would
8
+ * not report, since it keys folders and documents in separate collections
9
+ * (#258). So the answer for a folder comes from
10
+ * {@link module:engine/folder-notes.folderDocId}, the pass that emits those
11
+ * documents, rather than from a second derivation here.
12
+ *
13
+ * That this function ever answered differently was invisible from inside a
14
+ * build — no pass reads a folder's id from here — and surfaced only in the
15
+ * content index, which is read from outside and had no way to be checked
16
+ * against what shipped (#310).
17
+ *
4
18
  * Returns `undefined` for a file with **no address** — no `type`, or no
5
19
  * `shortcode`. Such a file is not an addressable note, so it has no document
6
20
  * and inventing an id for one would file it under nothing. Every caller already
@@ -14,6 +14,41 @@
14
14
  * {@link CONFIG_FILENAMES}.
15
15
  */
16
16
  export function findConfigFile(from: string): string | undefined;
17
+ /**
18
+ * Which configuration file a build launched here should read, and what each
19
+ * walk found.
20
+ *
21
+ * Kept separate from {@link loadPackConfig} because the *choice* is worth being
22
+ * able to ask about without loading anything: the two walks disagreeing is the
23
+ * observable form of #364, and a caller that wants to report it — or a test
24
+ * that wants to describe it — should not have to reproduce the resolution and
25
+ * risk disagreeing with the loader about it. It performs I/O, and is named for
26
+ * it, like the {@link findConfigFile} it calls twice.
27
+ *
28
+ * `PACKAGE_BUILD_CONFIG` is deliberately not consulted here. An explicit name
29
+ * is not a search result: {@link loadPackConfig} short-circuits on it before it
30
+ * ever asks, so there is no walk to report and nothing to disagree with.
31
+ *
32
+ * @param {object} [from] - Where to walk up from; both default to the real
33
+ * thing, and are parameters only so a caller can describe a tree it is not
34
+ * standing in.
35
+ * @param {string} [from.cwd] - The directory the build was launched in.
36
+ * @param {string} [from.moduleDir] - The directory this module sits in.
37
+ * @returns {{path: string|undefined, fromCwd: string|undefined, fromModule: string|undefined}}
38
+ * The file to read, and each walk's own answer — the same file in an ordinary
39
+ * build, different ones in a worktree resolving the toolchain out of its
40
+ * parent checkout.
41
+ * @throws {Error} As {@link findConfigFile}, when one directory holds more than
42
+ * one configuration.
43
+ */
44
+ export function resolveConfigFile({ cwd, moduleDir }?: {
45
+ cwd?: string | undefined;
46
+ moduleDir?: string | undefined;
47
+ }): {
48
+ path: string | undefined;
49
+ fromCwd: string | undefined;
50
+ fromModule: string | undefined;
51
+ };
17
52
  /**
18
53
  * Attach the position of the key a configuration error names.
19
54
  *
@@ -9,6 +9,47 @@
9
9
  * `overrides`: passing that inline is what silently did nothing (#76).
10
10
  */
11
11
  export function sharedPrettierOptionsFor(file: string): object;
12
+ /**
13
+ * Where a resolved Prettier configuration disagrees with the shared one.
14
+ *
15
+ * The runner resolves each file's options as *either* the consumer's own config
16
+ * or {@link sharedPrettierOptionsFor}, never a merge. That is what bare Prettier
17
+ * does and it is the contract — but it means the conventions this package exists
18
+ * to publish hold by convention alone, and they lapse in two opposite directions
19
+ * (#133). A consumer that declares any config of its own gets whatever that
20
+ * config says: spread {@link PRETTIER_BASE} without the markdown override and
21
+ * every note reindents at 4, the reindentation the override was added to prevent
22
+ * (#76); write a partial `.prettierrc` such as `{"tabWidth": 2}` and
23
+ * `printWidth`, `trailingComma`, `experimentalTernaries` and the rest fall back
24
+ * to Prettier's own defaults. A consumer that declares *nothing* formats one way
25
+ * under this command and another under a bare `npx prettier`.
26
+ *
27
+ * This is the comparison that makes either absence visible. It is a **report,
28
+ * not a merge**: what a consumer declared still wins, and a deliberate local
29
+ * choice stays possible — it stops being silent, and nothing here fails a build
30
+ * over it.
31
+ *
32
+ * Every shared value is a primitive, so `!==` is the whole comparison. An option
33
+ * holding an object would need a deeper one, and the shared set has none —
34
+ * `overrides` is not compared, because `resolveConfig` has already applied and
35
+ * removed it by the time a configuration reaches this.
36
+ *
37
+ * @param {object|null|undefined} resolved - What `prettier.resolveConfig`
38
+ * returned for `file`, with the consumer's own `overrides` already applied.
39
+ * `null` — no configuration at all — reports every shared key as absent.
40
+ * @param {string} file - Path the options were resolved for. Decides whether
41
+ * {@link PRETTIER_MARKDOWN} is part of what is expected.
42
+ * @returns {Array<{key: string, shared: unknown, local: unknown}>} One entry per
43
+ * shared key the resolved configuration does not carry the value of, in the
44
+ * order {@link PRETTIER_BASE} declares them. `local` is `undefined` where the
45
+ * key is absent entirely, which is not the same finding as a key set to
46
+ * something else and is reported differently.
47
+ */
48
+ export function sharedPrettierDivergence(resolved: object | null | undefined, file: string): Array<{
49
+ key: string;
50
+ shared: unknown;
51
+ local: unknown;
52
+ }>;
12
53
  /**
13
54
  * The prose conventions every content repository writes to — one Prettier
14
55
  * configuration and one markdownlint rule set, declared here so a note
@@ -35,6 +35,42 @@ export function checkFormatting(root: string, opts?: {
35
35
  checked: number;
36
36
  written: string[];
37
37
  }>;
38
+ /**
39
+ * Report where a repository's own Prettier configuration parts from the shared
40
+ * one — or that it has none at all (#133).
41
+ *
42
+ * **Warnings, every one of them.** A consumer's config wins by design and this
43
+ * does not change that; it only refuses to let the divergence be silent, which
44
+ * is the whole of what the issue asks for. Turning any of this into an error
45
+ * would make the shared conventions mandatory, and they are a default.
46
+ *
47
+ * The no-configuration case is the sharper one and is reported even though the
48
+ * command itself behaves correctly there: with no config file the shared
49
+ * conventions reach `content-build format` and reach *nothing else*, so an
50
+ * editor's format-on-save and a bare `npx prettier --check .` apply Prettier's
51
+ * own defaults to the same tree, and the two take turns rewriting the same
52
+ * lines. That is not hypothetical — it is what the config files in
53
+ * `sohl-thalorna` and `sohl-kethira-basic` were added to stop.
54
+ *
55
+ * @param {string} root - Repository to ask about.
56
+ * @param {object} [opts]
57
+ * @param {object} [opts.prettier] - The Prettier module, for tests.
58
+ * @returns {Promise<{findings: Array<{file?: string, severity: string,
59
+ * message: string}>, configFile: string|null}>} The findings and the config
60
+ * file they are about, which is `null` when the repository declares none. A
61
+ * finding about a missing file carries no `file`: #17's rule is to drop a
62
+ * field rather than invent one.
63
+ */
64
+ export function checkPrettierConventions(root: string, opts?: {
65
+ prettier?: object | undefined;
66
+ }): Promise<{
67
+ findings: Array<{
68
+ file?: string;
69
+ severity: string;
70
+ message: string;
71
+ }>;
72
+ configFile: string | null;
73
+ }>;
38
74
  /**
39
75
  * Lint a repository's markdown against the shared rule set.
40
76
  *