@heroiclands/package-build 9.0.0 → 10.0.1

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 (78) hide show
  1. package/CHANGELOG.md +721 -0
  2. package/CONTENT.md +273 -13
  3. package/bin/content-build.mjs +437 -7
  4. package/content-config.mjs +259 -28
  5. package/docs/content-format.md +1418 -0
  6. package/engine/address-charset.mjs +62 -0
  7. package/engine/alias-index.mjs +153 -0
  8. package/engine/base-compiler.mjs +194 -4
  9. package/engine/content-address.mjs +4 -4
  10. package/engine/content-format-check.mjs +570 -0
  11. package/engine/content-format.mjs +253 -0
  12. package/engine/content-links.mjs +132 -56
  13. package/engine/content-lint.mjs +8 -1
  14. package/engine/diagnostics.mjs +33 -0
  15. package/engine/document-subtypes.mjs +440 -0
  16. package/engine/field-spec.mjs +49 -43
  17. package/engine/frontmatter-lint.mjs +351 -28
  18. package/engine/generate.mjs +32 -4
  19. package/engine/helpers.mjs +41 -29
  20. package/engine/ids.mjs +19 -1
  21. package/engine/index.mjs +15 -0
  22. package/engine/item-registry.mjs +72 -5
  23. package/engine/kb-manifest.mjs +36 -7
  24. package/engine/map-notes.mjs +34 -18
  25. package/engine/note-claims.mjs +383 -0
  26. package/engine/note-vocabulary.mjs +678 -0
  27. package/engine/pack-config.mjs +39 -22
  28. package/engine/pack-router.mjs +17 -6
  29. package/engine/prose-lint.mjs +55 -3
  30. package/engine/retired-fields.mjs +117 -3
  31. package/engine/scenes.mjs +19 -1
  32. package/engine/schema-check.mjs +347 -3
  33. package/engine/site-build.mjs +1 -1
  34. package/engine/site-index.mjs +38 -21
  35. package/engine/system-block.mjs +513 -0
  36. package/engine/web-wikilinks.mjs +112 -80
  37. package/engine/wikilink-syntax.mjs +30 -0
  38. package/engine/wikilinks.mjs +67 -51
  39. package/package.json +6 -2
  40. package/sohl/actors.mjs +249 -36
  41. package/sohl/document-subtypes.mjs +82 -0
  42. package/sohl/index.mjs +3 -0
  43. package/sohl/items.mjs +110 -14
  44. package/sohl/note-schemas.mjs +11 -7
  45. package/types/content-config.d.mts +48 -4
  46. package/types/engine/address-charset.d.mts +45 -0
  47. package/types/engine/alias-index.d.mts +122 -0
  48. package/types/engine/base-compiler.d.mts +132 -4
  49. package/types/engine/content-address.d.mts +2 -2
  50. package/types/engine/content-format-check.d.mts +163 -0
  51. package/types/engine/content-format.d.mts +101 -0
  52. package/types/engine/content-links.d.mts +16 -1
  53. package/types/engine/content-lint.d.mts +6 -0
  54. package/types/engine/diagnostics.d.mts +29 -0
  55. package/types/engine/document-subtypes.d.mts +233 -0
  56. package/types/engine/field-spec.d.mts +76 -23
  57. package/types/engine/frontmatter-lint.d.mts +47 -2
  58. package/types/engine/generate.d.mts +14 -1
  59. package/types/engine/helpers.d.mts +21 -13
  60. package/types/engine/ids.d.mts +10 -0
  61. package/types/engine/index.d.mts +5 -0
  62. package/types/engine/item-registry.d.mts +21 -2
  63. package/types/engine/kb-manifest.d.mts +35 -8
  64. package/types/engine/map-notes.d.mts +21 -11
  65. package/types/engine/note-claims.d.mts +113 -0
  66. package/types/engine/note-vocabulary.d.mts +251 -0
  67. package/types/engine/pack-config.d.mts +4 -3
  68. package/types/engine/pack-router.d.mts +4 -4
  69. package/types/engine/prose-lint.d.mts +6 -2
  70. package/types/engine/retired-fields.d.mts +73 -2
  71. package/types/engine/schema-check.d.mts +182 -0
  72. package/types/engine/system-block.d.mts +281 -0
  73. package/types/engine/web-wikilinks.d.mts +23 -12
  74. package/types/engine/wikilink-syntax.d.mts +29 -0
  75. package/types/sohl/actors.d.mts +62 -6
  76. package/types/sohl/document-subtypes.d.mts +14 -0
  77. package/types/sohl/index.d.mts +1 -0
  78. package/types/sohl/items.d.mts +21 -0
@@ -1,31 +1,37 @@
1
- /**
2
- * Write `value` at a dotted path, creating the intermediate objects.
3
- *
4
- * Insertion order is the emitted JSON's key order, so a declaration's order is
5
- * the compiled document's order — which is what lets a field list replace a
6
- * hand-written object literal without changing a single byte of output.
7
- *
8
- * @param {object} target - The object to write into (mutated).
9
- * @param {string} dotted - Path, e.g. `"locations.flexible"`.
10
- * @param {any} value - The value to set.
11
- * @returns {object} `target`, for chaining.
12
- */
13
- export function setPath(target: object, dotted: string, value: any): object;
14
1
  /**
15
2
  * Read one declared field out of a note's frontmatter.
16
3
  *
4
+ * The *position* is resolved by {@link resolveFieldValue} — `<system>.system`
5
+ * first, then the legacy in-block key, then the declared shared source, then
6
+ * the default (#58). The **coercion** is applied here, once, wherever the value
7
+ * came from: a field's `read` is a statement about the field, not about where
8
+ * an author happened to write it, so `weight: "7"` reads as `7` at every one of
9
+ * those positions.
10
+ *
17
11
  * @param {FieldSpec} field - The declaration.
18
12
  * @param {object} fm - The note's frontmatter.
13
+ * @param {object} [options] - Options.
14
+ * @param {string} [options.block="sohl"] - Which system's block to resolve
15
+ * against. The default is the one block every existing tree authors; a
16
+ * second system passes its own.
19
17
  * @returns {any} The value to emit.
20
18
  */
21
- export function readField(field: FieldSpec, fm: object): any;
19
+ export function readField(field: FieldSpec, fm: object, { block }?: {
20
+ block?: string | undefined;
21
+ }): any;
22
22
  /**
23
23
  * Turn a field declaration into the builder it declares.
24
24
  *
25
25
  * @param {readonly FieldSpec[]} fields - The declaration, in emission order.
26
+ * @param {object} [options] - Options.
27
+ * @param {string} [options.block="sohl"] - Which system's block the builder
28
+ * reads. One declaration compiles against any block, which is what lets two
29
+ * systems declare the same shared source and different destinations.
26
30
  * @returns {(fm: object) => object} A `system`-block builder.
27
31
  */
28
- export function buildFromFields(fields: readonly FieldSpec[]): (fm: object) => object;
32
+ export function buildFromFields(fields: readonly FieldSpec[], { block }?: {
33
+ block?: string | undefined;
34
+ }): (fm: object) => object;
29
35
  /**
30
36
  * The fields of a declaration an author actually writes.
31
37
  *
@@ -38,12 +44,25 @@ export function buildFromFields(fields: readonly FieldSpec[]): (fm: object) => o
38
44
  * @returns {FieldSpec[]} Only the fields with a frontmatter `name`.
39
45
  */
40
46
  export function authoredFields(fields: readonly FieldSpec[]): FieldSpec[];
47
+ export { setPath };
41
48
  /**
42
49
  * @typedef {object} FieldSpec
43
- * @property {string} to - Dotted path in the emitted `system` block.
44
- * @property {string} [name] - Frontmatter key under `sohl:`, dotted for a
45
- * nested one (`impact.die`). Absent means the value is not authored — see
46
- * `value`.
50
+ * @property {string} to - Dotted path in the emitted `system` block — and,
51
+ * since #58, the path a note authors the field at directly:
52
+ * `<system>.system.<to>`.
53
+ * @property {string} [name] - The **shared, top-level property this field draws
54
+ * from** when the note authors no value at `<system>.system.<to>`. Dotted for
55
+ * a path into a shared container (`data.portrait`), which is now the ordinary
56
+ * case: `data:` (#128) puts every type-specific fact under one.
57
+ *
58
+ * It used to mean "frontmatter key under `sohl:`", and that reading is the
59
+ * degenerate case where the shared source and the system destination happen
60
+ * to share a name. They constantly do not — one shared `data.portrait` feeds
61
+ * `sohl.system.portrait` *and* `hm3.system.bioImage` — so the source is
62
+ * declared rather than matched by spelling (#58). The in-block position is
63
+ * still read, second, until #126 moves the corpus off it.
64
+ *
65
+ * Absent means the value is not authored at all — see `value`.
47
66
  * @property {string} [shape] - Human-readable shape, for documentation. Comes
48
67
  * paired with `read` from one of the coercion constants below.
49
68
  * @property {(raw: any, ctx: {fm: object, field: FieldSpec}) => any} [read] -
@@ -51,6 +70,15 @@ export function authoredFields(fields: readonly FieldSpec[]): FieldSpec[];
51
70
  * @property {any} [default] - Emitted when the note does not carry the field.
52
71
  * @property {boolean} [required] - Whether a note must carry it. A required
53
72
  * field's `read` is expected to throw when it is missing.
73
+ * @property {boolean} [shared] - Whether the field is authored at the note's
74
+ * **top level** rather than inside a system block, because what it states is
75
+ * not system-specific — a map's background art is the same art whichever
76
+ * system reads the note. It changes no reader: the top level is already the
77
+ * third step of {@link module:engine/system-block.resolveFieldValue}'s order,
78
+ * so the value resolves whichever region carries it. What it tells the
79
+ * author-facing surfaces is which of the two is the field's *home*, so a
80
+ * message about it names `img` rather than sending an author to `sohl.img`
81
+ * (#142).
54
82
  * @property {"string"|"number"|"boolean"|"list"|"map"} [kind] - The value's
55
83
  * shape, for the frontmatter linter (#19). Distinct from `shape`, which is
56
84
  * prose for a reader, and from `read`, which is what the compiler does: a
@@ -121,13 +149,25 @@ export const BLANK_IS_DEFAULT: Readonly<{
121
149
  }>;
122
150
  export type FieldSpec = {
123
151
  /**
124
- * - Dotted path in the emitted `system` block.
152
+ * - Dotted path in the emitted `system` block — and,
153
+ * since #58, the path a note authors the field at directly:
154
+ * `<system>.system.<to>`.
125
155
  */
126
156
  to: string;
127
157
  /**
128
- * - Frontmatter key under `sohl:`, dotted for a
129
- * nested one (`impact.die`). Absent means the value is not authored — see
130
- * `value`.
158
+ * - The **shared, top-level property this field draws
159
+ * from** when the note authors no value at `<system>.system.<to>`. Dotted for
160
+ * a path into a shared container (`data.portrait`), which is now the ordinary
161
+ * case: `data:` (#128) puts every type-specific fact under one.
162
+ *
163
+ * It used to mean "frontmatter key under `sohl:`", and that reading is the
164
+ * degenerate case where the shared source and the system destination happen
165
+ * to share a name. They constantly do not — one shared `data.portrait` feeds
166
+ * `sohl.system.portrait` *and* `hm3.system.bioImage` — so the source is
167
+ * declared rather than matched by spelling (#58). The in-block position is
168
+ * still read, second, until #126 moves the corpus off it.
169
+ *
170
+ * Absent means the value is not authored at all — see `value`.
131
171
  */
132
172
  name?: string | undefined;
133
173
  /**
@@ -152,6 +192,18 @@ export type FieldSpec = {
152
192
  * field's `read` is expected to throw when it is missing.
153
193
  */
154
194
  required?: boolean | undefined;
195
+ /**
196
+ * - Whether the field is authored at the note's
197
+ * **top level** rather than inside a system block, because what it states is
198
+ * not system-specific — a map's background art is the same art whichever
199
+ * system reads the note. It changes no reader: the top level is already the
200
+ * third step of {@link module :engine/system-block.resolveFieldValue}'s order,
201
+ * so the value resolves whichever region carries it. What it tells the
202
+ * author-facing surfaces is which of the two is the field's *home*, so a
203
+ * message about it names `img` rather than sending an author to `sohl.img`
204
+ * (#142).
205
+ */
206
+ shared?: boolean | undefined;
155
207
  /**
156
208
  * - The value's
157
209
  * shape, for the frontmatter linter (#19). Distinct from `shape`, which is
@@ -179,3 +231,4 @@ export type FieldSpec = {
179
231
  */
180
232
  describe: string;
181
233
  };
234
+ import { setPath } from "./system-block.mjs";
@@ -20,11 +20,25 @@ export function matchesKind(value: unknown, kind: string): boolean;
20
20
  * @param {Record<string, readonly object[]>} opts.schemas - Type → declaration.
21
21
  * @param {object} [opts.index] - The link index, for the reference check. Its
22
22
  * absence skips that check rather than reporting every reference as dead.
23
+ * @param {Record<string, object>} [opts.vocabulary] - Type → the closed regions
24
+ * it declares, as `engine/note-vocabulary.mjs` states them (#128). Supplied
25
+ * by the caller for the same reason `schemas` is: this module validates a
26
+ * note against whatever its type declares and knows no type names of its
27
+ * own. Its absence skips the `data:` and `subType` checks rather than
28
+ * 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}.
23
32
  * @returns {object[]} Findings, each with a locator where one is obtainable.
24
33
  */
25
- export function lintNote(note: object, { schemas, index }: {
34
+ export function lintNote(note: object, { schemas, index, vocabulary, systems }: {
26
35
  schemas: Record<string, readonly object[]>;
27
36
  index?: object | undefined;
37
+ vocabulary?: Record<string, object> | undefined;
38
+ systems?: Readonly<Record<string, {
39
+ known?: readonly string[];
40
+ fieldVocabulary?: boolean;
41
+ }>> | undefined;
28
42
  }): object[];
29
43
  /**
30
44
  * Check every note in a built index against its type's schema.
@@ -32,13 +46,22 @@ export function lintNote(note: object, { schemas, index }: {
32
46
  * @param {object} index - From `buildLinkIndex`.
33
47
  * @param {object} opts
34
48
  * @param {Record<string, readonly object[]>} opts.schemas - Type → declaration.
49
+ * @param {Record<string, object>} [opts.vocabulary] - Type → the closed regions
50
+ * it declares (#128); see {@link lintNote}.
35
51
  * @param {boolean} [opts.references=true] - Whether to check references.
52
+ * @param {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>} [opts.systems]
53
+ * The system blocks to check. See {@link DEFAULT_SYSTEM_BLOCKS}.
36
54
  * @returns {{findings: object[], notes: number}} The findings, and how many
37
55
  * notes were inspected.
38
56
  */
39
- export function lintFrontmatter(index: object, { schemas, references }: {
57
+ export function lintFrontmatter(index: object, { schemas, vocabulary, references, systems }: {
40
58
  schemas: Record<string, readonly object[]>;
59
+ vocabulary?: Record<string, object> | undefined;
41
60
  references?: boolean | undefined;
61
+ systems?: Readonly<Record<string, {
62
+ known?: readonly string[];
63
+ fieldVocabulary?: boolean;
64
+ }>> | undefined;
42
65
  }): {
43
66
  findings: object[];
44
67
  notes: number;
@@ -72,3 +95,25 @@ export function lintFrontmatter(index: object, { schemas, references }: {
72
95
  * @type {ReadonlySet<string>}
73
96
  */
74
97
  export const UNIVERSAL_KEYS: ReadonlySet<string>;
98
+ /**
99
+ * The system blocks a build checks, and what each accepts beyond the shared
100
+ * vocabulary.
101
+ *
102
+ * One entry, because one system is what every existing tree declares — and the
103
+ * default is a *declaration*, not a hard-coded assumption: a build that ships
104
+ * content for two systems passes both, and each block is then checked against
105
+ * its own vocabulary rather than against the other's (#58). A block nothing
106
+ * declares is not checked, because nothing can say what it may carry, and
107
+ * inventing a rule for it would report a correct tree red.
108
+ *
109
+ * `fieldVocabulary` says the note type's own declared field names are keys of
110
+ * this block. True for `sohl` and untrue in general: those names come from the
111
+ * `itemBuilders` registry that this system declares, and a second system's
112
+ * notes write a second system's fields.
113
+ *
114
+ * @type {Readonly<Record<string, {known?: readonly string[], fieldVocabulary?: boolean}>>}
115
+ */
116
+ export const DEFAULT_SYSTEM_BLOCKS: Readonly<Record<string, {
117
+ known?: readonly string[];
118
+ fieldVocabulary?: boolean;
119
+ }>>;
@@ -10,14 +10,27 @@
10
10
  * order among packs of one type — and every one of them is written before the
11
11
  * actors pass that reads them.
12
12
  *
13
+ * **Scoped to one system when the caller has one (#58).** A being addresses an
14
+ * item by `(type, shortcode)`, and that address is unique within one system and
15
+ * not across two: `skill:sword` is an HM3 skill *and* a SoHL skill, with
16
+ * different data models behind them. The reference itself is unambiguous — it
17
+ * sits inside a system block, so position says which it means — but the
18
+ * resolver has to know which catalogue it is searching, or it resolves the pair
19
+ * by whichever pack was read first. So an Actor pass reads the Item packs of
20
+ * **its own** system plus the system-neutral ones, which belong to every
21
+ * system. Asking for no system reads them all, which is every single-system
22
+ * build and the behaviour this always had.
23
+ *
13
24
  * @param {object} [config] - The resolved build configuration. Defaults to this
14
25
  * repository's.
26
+ * @param {string|null} [system] - The system whose catalogue is wanted. Omitted
27
+ * or `null`, every Item pack is read.
15
28
  * @returns {string[]} Each Item pack's JSON directory. Empty when the
16
29
  * repository ships no items at all, which is a legitimate package: the actors
17
30
  * pass accepts an empty list and reports an item it cannot resolve per
18
31
  * `(type, shortcode)` instead, naming the being (#49).
19
32
  */
20
- export function itemPackJsonDirs(config?: object): string[];
33
+ export function itemPackJsonDirs(config?: object, system?: string | null): string[];
21
34
  /**
22
35
  * The passes to run, ordered so that each one follows the output it reads.
23
36
  *
@@ -62,8 +62,8 @@ export function walkMarkdownTree(rootDir: string, { skipDirectories }?: {
62
62
  }, void, unknown>;
63
63
  /**
64
64
  * Resolve the required `sohl.archetype` frontmatter for an Item/Actor entry
65
- * (see the archetype contract, #604 — `flags.sohl.docArchetype`). The property
66
- * is a nullable number that authors must state explicitly:
65
+ * (the archetype contract, #604). The property is a nullable number that
66
+ * authors must state explicitly:
67
67
  * - a number → the document is an archetype of that priority.
68
68
  * - `null` → the document is not an archetype.
69
69
  * - absent → an authoring error (throws), so "not an archetype" is never
@@ -79,19 +79,27 @@ export function walkMarkdownTree(rootDir: string, { skipDirectories }?: {
79
79
  */
80
80
  export function resolveArchetype(fm: object, label: string): number | undefined;
81
81
  /**
82
- * Merge the required `sohl.archetype` frontmatter into a document's `flags`,
83
- * returning a new object (the input is never mutated). A numeric archetype
84
- * seeds `flags.sohl.docArchetype`; `null` omits the flag (and clears any stale
85
- * `docArchetype` while preserving sibling `sohl` flags); an absent value
86
- * throws. See {@link resolveArchetype}.
87
- *
88
- * @param {object} fm Parsed frontmatter.
89
- * @param {object} [flags] The entry's existing flags (e.g. `fm.flags`).
90
- * @param {string} label Human-readable context for error messages.
91
- * @returns {object} The flags object with the archetype applied.
82
+ * The value a document's `system.archetype` carries, from the required
83
+ * `sohl.archetype` frontmatter (#126, sohl#1780).
84
+ *
85
+ * A **schema field**, so the tri-state is written out in full rather than
86
+ * expressed by a key's presence: a number is an archetype at that priority,
87
+ * and `null` is not an archetype. This is where {@link resolveArchetype}'s
88
+ * `undefined` becomes the field's `null` — an emitted `undefined` would be
89
+ * dropped by `JSON.stringify`, leaving the compiled document with no
90
+ * `archetype` at all and the tri-state readable as two.
91
+ *
92
+ * **`0` is an archetype.** It is the priority SoHL's own archetypes ship at,
93
+ * and it is falsy, so this returns it unchanged and every caller must ask
94
+ * `typeof v === "number"` rather than testing truthiness.
95
+ *
96
+ * @param {object} fm Parsed frontmatter.
97
+ * @param {string} label Human-readable context for error messages.
98
+ * @returns {number|null} The archetype priority, or `null` for a document
99
+ * that is not an archetype.
92
100
  * @throws {Error} When `sohl.archetype` is absent or invalid.
93
101
  */
94
- export function withArchetypeFlag(fm: object, flags?: object, label: string): object;
102
+ export function systemArchetype(fm: object, label: string): number | null;
95
103
  /**
96
104
  * Generates a compendium-source filename: `Name_id.json` with non-
97
105
  * alphanumeric runs replaced by underscores.
@@ -84,6 +84,16 @@ export function pageUuid(entryUuid: string, pageId: string): string;
84
84
  * @type {ReadonlySet<string>}
85
85
  */
86
86
  export const MAP_TYPES: ReadonlySet<string>;
87
+ /**
88
+ * The map subTypes, which differ only in the canvas defaults derived for them.
89
+ *
90
+ * They were three *types* until #174, which cost three entries in the pack
91
+ * router, three in the claims set and three in every consumer's section config
92
+ * — for one idea that the specification had always described as one type.
93
+ *
94
+ * @type {readonly string[]}
95
+ */
96
+ export const MAP_SUBTYPES: readonly string[];
87
97
  /**
88
98
  * Content type → the pack its documents compile into, and the document type
89
99
  * that pack holds.
@@ -1,14 +1,17 @@
1
1
  export * as ids from "./ids.mjs";
2
+ export * as systemBlock from "./system-block.mjs";
2
3
  export * as codeFences from "./code-fences.mjs";
3
4
  export * as frontmatter from "./frontmatter.mjs";
4
5
  export * as contentTree from "./content-tree.mjs";
5
6
  export * as packConfig from "./pack-config.mjs";
6
7
  export * as packRouter from "./pack-router.mjs";
8
+ export * as noteClaims from "./note-claims.mjs";
7
9
  export * as contentPackage from "./content-package.mjs";
8
10
  export * as notePackage from "./note-package.mjs";
9
11
  export * as retiredFields from "./retired-fields.mjs";
10
12
  export * as homepage from "./homepage.mjs";
11
13
  export * as noteSchemas from "./note-schemas.mjs";
14
+ export * as noteVocabulary from "./note-vocabulary.mjs";
12
15
  export * as contentSlug from "./content-slug.mjs";
13
16
  export * as contentAddress from "./content-address.mjs";
14
17
  export * as foreignManifests from "./foreign-manifests.mjs";
@@ -21,9 +24,11 @@ export * as webWikilinks from "./web-wikilinks.mjs";
21
24
  export * as contentTables from "./content-tables.mjs";
22
25
  export * as helpers from "./helpers.mjs";
23
26
  export * as itemRegistry from "./item-registry.mjs";
27
+ export * as documentSubtypes from "./document-subtypes.mjs";
24
28
  export * as itemDocs from "./item-docs.mjs";
25
29
  export * as wikilinks from "./wikilinks.mjs";
26
30
  export * as wikilinkSyntax from "./wikilink-syntax.mjs";
31
+ export * as aliasIndex from "./alias-index.mjs";
27
32
  export * as siteIndex from "./site-index.mjs";
28
33
  export * as baseCompiler from "./base-compiler.mjs";
29
34
  export * as journals from "./journals.mjs";
@@ -24,10 +24,27 @@ export function itemTypes(): ReadonlySet<string>;
24
24
  * failing as an anonymous `is not a function` (#1504).
25
25
  *
26
26
  * @param {string} type - The note's `type` frontmatter.
27
+ * @param {string} [system] - The system compiling it, where a build declares
28
+ * more than one registry. Omitted, a type only one registry declares still
29
+ * resolves; a contested one throws rather than picking a side.
27
30
  * @returns {(fm: object) => object} The builder for that type.
28
31
  * @throws {Error} When the configuration registers no builder for `type`.
29
32
  */
30
- export function itemBuilder(type: string): (fm: object) => object;
33
+ export function itemBuilder(type: string, system?: string): (fm: object) => object;
34
+ /**
35
+ * The frontmatter fields a type's registry entry declares, if any.
36
+ *
37
+ * Sparse by design: a type whose entry declares none compiles normally and is
38
+ * simply undocumented (#22). What reads it is the `system`-block passthrough,
39
+ * which has to know which paths a declared field already writes before it
40
+ * writes the rest (#58).
41
+ *
42
+ * @param {string} type - The item type.
43
+ * @param {string} [system] - The system compiling it, where a build declares
44
+ * more than one registry.
45
+ * @returns {readonly object[]|undefined} The declaration, or `undefined`.
46
+ */
47
+ export function itemFields(type: string, system?: string): readonly object[] | undefined;
31
48
  /**
32
49
  * The default art for an item type — the image a note of that type is given
33
50
  * when it carries no `img:` of its own.
@@ -51,7 +68,9 @@ export function itemBuilder(type: string): (fm: object) => object;
51
68
  * One spelling, one meaning, wherever it is written.
52
69
  *
53
70
  * @param {string} type - the item type.
71
+ * @param {string} [system] - The system compiling it, where a build declares
72
+ * more than one registry.
54
73
  * @returns {string} The default image path for that type.
55
74
  * @throws {Error} When the type's registry entry pairs no `img`.
56
75
  */
57
- export function itemArt(type: string): string;
76
+ export function itemArt(type: string, system?: string): string;
@@ -24,18 +24,34 @@ export function canonicalKey(pkg: string, type: string, shortcode: string): stri
24
24
  /**
25
25
  * Reads a canonical key back into its parts.
26
26
  *
27
- * Unambiguous because no package, type or shortcode contains a hyphen — types
28
- * are bare words and shortcodes are `^[A-Za-z0-9]+$` (#1397).
29
- *
30
- * @param {string} key - A canonical key.
31
- * @returns {{package: string, type: string, shortcode: string}|null} The parts,
32
- * or `null` when the key is not in canonical form.
27
+ * Parsing is plain positional counting: split on the separator, require
28
+ * {@link CANONICAL_KEY_SEGMENTS} of them, and assign each position its field.
29
+ * **The charset rule is what makes that sound** — every segment is
30
+ * `^[A-Za-z0-9]+$` (`ADDRESS_SEGMENT_PATTERN` in `engine/address-charset.mjs`),
31
+ * so the hyphen is purely a separator and the count alone determines every
32
+ * field. That is enforced at each of the three sources rather than assumed of
33
+ * the data: shortcodes by `content-lint.mjs` (#1397), `contentPackage` by
34
+ * `defineConfig` (#59), and types are bare words. Were any of them free to
35
+ * carry a hyphen, no amount of counting would recover the fields and the reader
36
+ * would need a vocabulary to match against instead.
37
+ *
38
+ * **Nothing to read and nothing readable are different answers.** A key that
39
+ * cannot be canonical — `harn-adventures-skill-melee`, four segments — yields
40
+ * `null`, while an absent or blank input yields `undefined`. Both are falsy, so
41
+ * every call site (all of which test the result for truthiness) is unaffected;
42
+ * the distinction is there so a caller reporting "this key is unreadable" can
43
+ * tell that it has a key to report about.
44
+ *
45
+ * @param {unknown} key - A canonical key, or nothing.
46
+ * @returns {{package: string, type: string, shortcode: string}|null|undefined}
47
+ * The parts; `null` when there is a string that is not in canonical form;
48
+ * `undefined` when there is no key at all.
33
49
  */
34
- export function readCanonicalKey(key: string): {
50
+ export function readCanonicalKey(key: unknown): {
35
51
  package: string;
36
52
  type: string;
37
53
  shortcode: string;
38
- } | null;
54
+ } | null | undefined;
39
55
  /**
40
56
  * The package-relative address a site-absolute URL records as.
41
57
  *
@@ -151,6 +167,17 @@ export function manifestsComplete(localPackages: Iterable<string>, manifestPacka
151
167
  * relaxes the build rather than breaking it.
152
168
  */
153
169
  export const LINK_PACKAGES: readonly string[];
170
+ /**
171
+ * How many segments a canonical key has, and therefore how many the reader
172
+ * below counts.
173
+ *
174
+ * Named rather than written as a literal because it is the *grammar*, not an
175
+ * implementation detail of one function: it is the number a change to the
176
+ * address form would move, and the thing a reader of that change has to find.
177
+ *
178
+ * @type {number}
179
+ */
180
+ export const CANONICAL_KEY_SEGMENTS: number;
154
181
  /**
155
182
  * Manifest format version.
156
183
  *
@@ -6,14 +6,19 @@
6
6
  */
7
7
  export function isMapType(type?: string): boolean;
8
8
  /**
9
- * The canvas profile for a map type.
9
+ * The canvas profile for a map subType.
10
10
  *
11
- * @param {string} type - The note's `type`.
12
- * @returns {object} The profile from {@link MAP_TYPE_PROFILES}.
13
- * @throws {Error} When the type is not a map type — the build's fail-fast
14
- * contract, so a typo never ships a scene with Foundry's own defaults.
11
+ * Keyed on the subType rather than the type since #174: every map note is
12
+ * `type: map`, and which canvas it derives is the one thing the three
13
+ * spellings ever decided.
14
+ *
15
+ * @param {string} subType - The note's `subType`.
16
+ * @returns {object} The profile from {@link MAP_SUBTYPE_PROFILES}.
17
+ * @throws {Error} When the subType is not a map subType — the build's
18
+ * fail-fast contract, so a typo never ships a scene with Foundry's own
19
+ * defaults.
15
20
  */
16
- export function mapProfile(type: string): object;
21
+ export function mapProfile(subType: string): object;
17
22
  /**
18
23
  * The id of one region within its scene.
19
24
  *
@@ -141,7 +146,7 @@ export function buildShape(spec: object, geom: MapGeometry): object;
141
146
  */
142
147
  export function buildScene(fm: object, ctx: object): object;
143
148
  /**
144
- * Synthesise the scene's single embedded Level from `image:` / `overlay:`.
149
+ * Synthesise the scene's single embedded Level from `img:` / `overlay:`.
145
150
  *
146
151
  * Authors never write `levels:`. A scene must ship at least one Level — the
147
152
  * client-side `_preCreate` net that would create one does not run for offline
@@ -151,9 +156,13 @@ export function buildScene(fm: object, ctx: object): object;
151
156
  *
152
157
  * @param {object} sohl - The note's `sohl:` block.
153
158
  * @param {string} sceneId - The owning scene's `_id`.
159
+ * @param {string} [img] - The background art, already resolved from the note.
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).
154
163
  * @returns {object} The Level document, keyed for the pack.
155
164
  */
156
- export function buildLevel(sohl: object, sceneId: string): object;
165
+ export function buildLevel(sohl: object, sceneId: string, img?: string): object;
157
166
  /**
158
167
  * Compile the `walls:` and `doors:` blocks into Wall documents.
159
168
  *
@@ -225,9 +234,8 @@ export function buildLocations(sohl: object, geom: MapGeometry, ctx: object): ob
225
234
  * @returns {object[]} The Region documents.
226
235
  */
227
236
  export function buildRegions(sohl: object, geom: MapGeometry, ctx: object): object[];
228
- export { MAP_TYPES };
229
237
  /**
230
- * Per-type canvas defaults, emitted **explicitly** on every scene.
238
+ * Per-subtype canvas defaults, emitted **explicitly** on every scene.
231
239
  *
232
240
  * This is not a convenience. `grid.type`, `grid.distance` and `grid.units` all
233
241
  * declare `initial: () => game.system.grid.*`, and there is no `game` at build
@@ -237,7 +245,7 @@ export { MAP_TYPES };
237
245
  *
238
246
  * @type {Readonly<Record<string, object>>}
239
247
  */
240
- export const MAP_TYPE_PROFILES: Readonly<Record<string, object>>;
248
+ export const MAP_SUBTYPE_PROFILES: Readonly<Record<string, object>>;
241
249
  /**
242
250
  * Foundry's own id for the level a scene is created with
243
251
  * (`Scene.metadata.defaultLevelId`). Adopting it makes every reference to the
@@ -280,4 +288,6 @@ export type MapGeometry = {
280
288
  */
281
289
  dimensions: number[];
282
290
  };
291
+ import { MAP_SUBTYPES } from "./ids.mjs";
283
292
  import { MAP_TYPES } from "./ids.mjs";
293
+ export { MAP_SUBTYPES, MAP_TYPES };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * The note types a pass of one document type claims — the claim table.
3
+ *
4
+ * Each row restates one pass's `selects`, in the only form that can be asked of
5
+ * a pack the configuration does not declare. A document type no compiler is
6
+ * registered for claims nothing, which is what keeps a prebuilt `Adventure`
7
+ * pack from appearing to answer for any note.
8
+ *
9
+ * @param {string} docType - The Foundry document type a pack holds.
10
+ * @param {ClaimSources} [sources] - What to answer from. Defaults to the
11
+ * configured registries and the systems this toolchain ships.
12
+ * @returns {ReadonlySet<string>} The note types such a pass would claim.
13
+ */
14
+ export function noteTypesClaimedBy(docType: string, sources?: ClaimSources): ReadonlySet<string>;
15
+ /**
16
+ * Every note type some pack in a configuration would compile.
17
+ *
18
+ * The union across the configured pack list, so a type claimed by any one pack
19
+ * is claimed — which is what keeps a type deliberately unmapped for one system,
20
+ * and claimed for another, silent (#79).
21
+ *
22
+ * @param {object} [config] - The resolved build configuration. Defaults to this
23
+ * repository's.
24
+ * @param {ClaimSources} [sources] - What to answer from.
25
+ * @returns {ReadonlySet<string>} The claimed note types.
26
+ */
27
+ export function claimedNoteTypes(config?: object, sources?: ClaimSources): ReadonlySet<string>;
28
+ /**
29
+ * Every note type this build knows, whatever any one repository configures.
30
+ *
31
+ * Wider than {@link claimedNoteTypes} on purpose: it is what distinguishes a
32
+ * repository that has not configured a pack for a real content type from an
33
+ * author who wrote a word nothing anywhere compiles. The engine's own types,
34
+ * the types every shipped system maps, and whatever the configured registries
35
+ * declare on top.
36
+ *
37
+ * @param {ClaimSources} [sources] - What to answer from.
38
+ * @returns {ReadonlySet<string>} The vocabulary.
39
+ */
40
+ export function noteTypeVocabulary(sources?: ClaimSources): ReadonlySet<string>;
41
+ /**
42
+ * Every note in the content tree that no configured pack would compile.
43
+ *
44
+ * Read-only: it walks the tree and reports, and writes nothing. Three kinds of
45
+ * note are passed over, each for a stated reason rather than by omission — a
46
+ * file with no frontmatter is not a note; a note with no `type:` is the
47
+ * frontmatter linter's finding, which can say what a type is *for*; and a
48
+ * retired type is answered by `assertTypeNotRetired` in `ids.mjs`, which names the
49
+ * replacement.
50
+ *
51
+ * @param {object} [config] - The resolved build configuration. Defaults to this
52
+ * repository's.
53
+ * @param {ClaimSources} [sources] - What to answer from.
54
+ * @returns {Array<{file: string, line?: number, column?: number,
55
+ * severity: "error", message: string, type: string}>} One finding per note.
56
+ */
57
+ export function unclaimedNoteFindings(config?: object, sources?: ClaimSources): Array<{
58
+ file: string;
59
+ line?: number;
60
+ column?: number;
61
+ severity: "error";
62
+ message: string;
63
+ type: string;
64
+ }>;
65
+ /**
66
+ * Note types that compile into **no compendium document, by design**.
67
+ *
68
+ * A homepage compiles into a *page*: it carries no compendium UUID, appears in
69
+ * no pack and in no link-manifest entry, and every package that publishes one
70
+ * would otherwise be told its front page is unclaimed. It is the one type whose
71
+ * absence from every pack is the intended state rather than a gap.
72
+ *
73
+ * @type {ReadonlySet<string>}
74
+ */
75
+ export const NEVER_PACKED_TYPES: ReadonlySet<string>;
76
+ /**
77
+ * The note-type → document-subtype maps this toolchain ships.
78
+ *
79
+ * One today. `hm3/` is #139, and when it lands its map joins this list rather
80
+ * than the claim table below growing a second copy of the same fact.
81
+ *
82
+ * `engine/` importing from `sohl/` is the arrangement `generate.mjs` already
83
+ * has — its `COMPILERS` table names the SoHL compilers by class — and for the
84
+ * same reason: the engine owns the *mechanism* that asks each system what it
85
+ * compiles, and the systems own the answers.
86
+ *
87
+ * @type {readonly import("./document-subtypes.mjs").DocumentSubtypeMap[]}
88
+ */
89
+ export const KNOWN_DOCUMENT_SUBTYPE_MAPS: readonly import("./document-subtypes.mjs").DocumentSubtypeMap[];
90
+ /**
91
+ * What a claim question is asked against.
92
+ *
93
+ * Each field defaults to what the compilers themselves read, so the answer here
94
+ * and the answer a pass gives are drawn from one source. They are parameters so
95
+ * that a test can pose a configuration this toolchain does not ship — two
96
+ * systems cutting the vocabulary differently, a registry declaring nothing —
97
+ * without a content tree or a config file on disk.
98
+ */
99
+ export type ClaimSources = {
100
+ /**
101
+ * -
102
+ * The systems' note-type → document-subtype maps.
103
+ */
104
+ maps?: readonly import("./document-subtypes.mjs").DocumentSubtypeMap[] | undefined;
105
+ /**
106
+ * - The declared item vocabulary.
107
+ */
108
+ itemTypes?: ReadonlySet<string> | undefined;
109
+ /**
110
+ * - The doc-carrying types.
111
+ */
112
+ docEntryTypes?: ReadonlySet<string> | undefined;
113
+ };