@heroiclands/package-build 9.0.0 → 10.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 (78) hide show
  1. package/CHANGELOG.md +694 -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 +343 -27
  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
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Every field path any subtype of a published schema declares.
3
+ *
4
+ * `own` and `inherited` are collapsed here, unlike
5
+ * {@link module:engine/schema-check}, and for the same reason that module keeps
6
+ * them apart: it asks two questions of one artifact and only one of them wants
7
+ * the inherited set. This asks the single question "is this field defined
8
+ * anywhere in the system", for which an inherited field is defined.
9
+ *
10
+ * @param {object} artifact - A `version: 1` schema artifact.
11
+ * @returns {Set<string>} The paths, `system.` prefix stripped as the artifact
12
+ * stores them.
13
+ */
14
+ export function declaredPaths(artifact: object): Set<string>;
15
+ /**
16
+ * What an author is told about a target no schema declares.
17
+ *
18
+ * Names the version, because a field may be perfectly well defined on the
19
+ * system's `main` and simply unreleased — the same distinction
20
+ * {@link undeclaredMessage} draws, and the difference between "the
21
+ * specification is wrong" and "the schema has not caught up".
22
+ *
23
+ * @param {object} finding - `{system, systemVersion, noteType, source, target}`.
24
+ * @returns {string} The message.
25
+ */
26
+ export function undeclaredTargetMessage({ system, systemVersion, noteType, source, target }: object): string;
27
+ /**
28
+ * Check every `system.*` target the specification names.
29
+ *
30
+ * @param {object} opts
31
+ * @param {import("./content-format.mjs").ContentFormat} opts.format - The
32
+ * parsed specification.
33
+ * @param {Record<string, object>} opts.schemas - System id → its published
34
+ * `version: 1` schema artifact. A system absent from this map is counted
35
+ * `unchecked` rather than passed.
36
+ * @returns {{findings: object[], checked: number, unchecked: Record<string, number>}}
37
+ * Findings ready for `emitDiagnostic`, how many claims were resolved, and how
38
+ * many were left unresolved per system.
39
+ */
40
+ export function checkSchemaTargets({ format, schemas }: {
41
+ format: import("./content-format.mjs").ContentFormat;
42
+ schemas: Record<string, object>;
43
+ }): {
44
+ findings: object[];
45
+ checked: number;
46
+ unchecked: Record<string, number>;
47
+ };
48
+ /**
49
+ * The systems the specification names.
50
+ *
51
+ * Taken from the columns of its mapping tables rather than from a list written
52
+ * here, so a system added to the document is measured with no code change.
53
+ * Cached per parsed document, since the answer is the same for every note.
54
+ *
55
+ * @param {import("./content-format.mjs").ContentFormat} format - The parsed
56
+ * specification.
57
+ * @returns {ReadonlySet<string>} The system ids.
58
+ */
59
+ export function systemsNamed(format: import("./content-format.mjs").ContentFormat): ReadonlySet<string>;
60
+ /**
61
+ * Measure one note against the vocabulary the format declares for its type.
62
+ *
63
+ * @param {object} note - `{file, raw, fm}` — as the link index hands one over.
64
+ * @param {import("./content-format.mjs").ContentFormat} format - The parsed
65
+ * specification.
66
+ * @param {object} [opts]
67
+ * @param {"warning"|"error"} [opts.severity="warning"] - What the findings are.
68
+ * A report by default; `--strict` raises it.
69
+ * @returns {object[]} Findings, each carrying the `class` it belongs to
70
+ * alongside the fields `emitDiagnostic` reads.
71
+ */
72
+ export function measureNote(note: object, format: import("./content-format.mjs").ContentFormat, { severity }?: {
73
+ severity?: "error" | "warning" | undefined;
74
+ }): object[];
75
+ /**
76
+ * Measure a corpus, and count what it finds by class.
77
+ *
78
+ * The counts are the point as much as the findings: #127 promotes a class to
79
+ * fatal when its count reaches zero, so a run that prints them is the epic's
80
+ * progress bar.
81
+ *
82
+ * @param {Iterable<object>} notes - `{file, raw, fm}` for each authored note.
83
+ * @param {import("./content-format.mjs").ContentFormat} format - The parsed
84
+ * specification.
85
+ * @param {object} [opts]
86
+ * @param {boolean} [opts.strict=false] - Report the findings as errors rather
87
+ * than warnings. #127 turns this on one slice at a time.
88
+ * @returns {{findings: object[], notes: number, byClass: Record<string, number>}}
89
+ */
90
+ export function measureCorpus(notes: Iterable<object>, format: import("./content-format.mjs").ContentFormat, { strict }?: {
91
+ strict?: boolean | undefined;
92
+ }): {
93
+ findings: object[];
94
+ notes: number;
95
+ byClass: Record<string, number>;
96
+ };
97
+ /**
98
+ * What an author is told when the specification and the declaration disagree.
99
+ *
100
+ * @param {object} finding - `{noteType, source, target, name, to}`.
101
+ * @returns {string} The message.
102
+ */
103
+ export function fieldDriftMessage({ noteType, source, target, name, to }: object): string;
104
+ /**
105
+ * Check the specification's per-type tables against the field declarations that
106
+ * compile them (#136).
107
+ *
108
+ * @param {object} opts
109
+ * @param {import("./content-format.mjs").ContentFormat} opts.format - The
110
+ * parsed specification.
111
+ * @param {Record<string, readonly object[]>} opts.itemFields - Item type → its
112
+ * `fields` declaration, as `itemBuilders` carries it.
113
+ * @param {string} opts.system - Which system column of the mapping tables these
114
+ * declarations compile. Supplied by the caller rather than assumed here: the
115
+ * declarations belong to one system, and the document maps onto several.
116
+ * @param {"warning"|"error"} [opts.severity="error"] - What a contradiction is.
117
+ * @returns {{findings: object[], coverage: object[], checked: string[],
118
+ * skipped: {spec: string[], registry: string[]}, fields: number}}
119
+ * Contradictions ready for `emitDiagnostic`, the per-type coverage, the types
120
+ * compared, the types out of reach on each side, and how many field pairs
121
+ * were compared.
122
+ */
123
+ export function checkDeclaredFields({ format, itemFields, system, severity }: {
124
+ format: import("./content-format.mjs").ContentFormat;
125
+ itemFields: Record<string, readonly object[]>;
126
+ system: string;
127
+ severity?: "error" | "warning" | undefined;
128
+ }): {
129
+ findings: object[];
130
+ coverage: object[];
131
+ checked: string[];
132
+ skipped: {
133
+ spec: string[];
134
+ registry: string[];
135
+ };
136
+ fields: number;
137
+ };
138
+ /**
139
+ * Top-level keys the format names as the note's own, whatever its type.
140
+ *
141
+ * Top level is otherwise **open** — an unrecognised key there is a Hugo or
142
+ * theme parameter this build has no standing to refuse — so this set exists
143
+ * only to keep a note's own identity fields from being mistaken for a
144
+ * misplaced `data` property where the two share a name. `type` and `subType`
145
+ * are the pair that matters: several mapping tables name `subType` as a shared
146
+ * source, and it is authored at top level by design.
147
+ *
148
+ * @type {ReadonlySet<string>}
149
+ */
150
+ export const NOTE_LEVEL_KEYS: ReadonlySet<string>;
151
+ /**
152
+ * Where one declared field sends one authored value.
153
+ */
154
+ export type FieldPair = {
155
+ /**
156
+ * - The authored key, as the declaration spells it.
157
+ */
158
+ name: string;
159
+ /**
160
+ * - The emitted path, `system.` prefix stripped.
161
+ */
162
+ to: string;
163
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Parse the specification's tables.
3
+ *
4
+ * Pure: text in, model out, so a test states a miniature document rather than
5
+ * asserting against the real one and its 1,100 lines of prose.
6
+ *
7
+ * @param {string} text - The document's contents.
8
+ * @param {object} [opts]
9
+ * @param {string} [opts.file] - Path recorded on the result, for diagnostics.
10
+ * @returns {ContentFormat} What the document declares.
11
+ */
12
+ export function parseContentFormat(text: string, { file }?: {
13
+ file?: string | undefined;
14
+ }): ContentFormat;
15
+ /**
16
+ * Read and parse the specification from disk.
17
+ *
18
+ * @param {string} [file] - The document. Defaults to {@link CONTENT_FORMAT_PATH}.
19
+ * @returns {ContentFormat} What it declares.
20
+ */
21
+ export function loadContentFormat(file?: string): ContentFormat;
22
+ /**
23
+ * The specification this package ships.
24
+ *
25
+ * Resolved from this module rather than from the working directory: a consumer
26
+ * runs `content-build content-format` inside its own repository, and the
27
+ * document it should be checked against is the one that came with the toolchain
28
+ * version it resolved — the same rule `--version` follows.
29
+ *
30
+ * @type {string}
31
+ */
32
+ export const CONTENT_FORMAT_PATH: string;
33
+ /**
34
+ * What one note type's section declares.
35
+ */
36
+ export type TypeSpec = {
37
+ /**
38
+ * - The note type, as the `### type:` heading spells it.
39
+ */
40
+ name: string;
41
+ /**
42
+ * - 1-based line of that heading.
43
+ */
44
+ line: number;
45
+ /**
46
+ * - The head segment of each declared `data`
47
+ * property — what a note actually writes. `appearance.eye_color` is authored
48
+ * as `appearance`, so that is the key recorded.
49
+ */
50
+ dataKeys: Set<string>;
51
+ /**
52
+ * - The declared paths, whole.
53
+ */
54
+ dataPaths: Set<string>;
55
+ };
56
+ /**
57
+ * One `system.*` target the specification names for one note type.
58
+ */
59
+ export type MappingClaim = {
60
+ /**
61
+ * - The type whose section makes the claim.
62
+ */
63
+ noteType: string;
64
+ /**
65
+ * - The system column it sits under, from the header.
66
+ */
67
+ system: string;
68
+ /**
69
+ * - The shared source cell, stripped of its backticks.
70
+ */
71
+ source: string;
72
+ /**
73
+ * - The dotted path, `system.` prefix included.
74
+ */
75
+ target: string;
76
+ /**
77
+ * - 1-based line of the row.
78
+ */
79
+ line: number;
80
+ /**
81
+ * - 1-based column of the cell's first character.
82
+ */
83
+ column: number;
84
+ };
85
+ /**
86
+ * The specification, as data.
87
+ */
88
+ export type ContentFormat = {
89
+ /**
90
+ * - Where it was read from, for diagnostics.
91
+ */
92
+ file: string;
93
+ /**
94
+ * - Note type → what its section declares.
95
+ */
96
+ types: Map<string, TypeSpec>;
97
+ /**
98
+ * - Every `system.*` target, in document order.
99
+ */
100
+ claims: MappingClaim[];
101
+ };
@@ -86,15 +86,30 @@ export function auditHomepageLinks(index: ReturnType<typeof buildLinkIndex>): Ar
86
86
  /**
87
87
  * Every link in a tree that lands nowhere.
88
88
  *
89
+ * **The two failure modes are separate findings, because they are separate
90
+ * problems** (#131). A piped target the author declared to be an address, and
91
+ * which resolves nowhere, is a typo: every package it could name is either
92
+ * built here or vendored, so there is no third possibility. An unpiped target
93
+ * naming no note of the source's type may be exactly that typo — or a
94
+ * worldbuilding placeholder, which is a long-standing convention in the
95
+ * setting trees. So the first is an error and the second a warning, and the
96
+ * caller can tell them apart without parsing a message.
97
+ *
89
98
  * @param {ReturnType<typeof buildLinkIndex>} index - The built index.
90
99
  * @returns {{deadAnchors: object[], deadAddresses: object[],
100
+ * deadAliases: object[], aliasCollisions: object[],
91
101
  * frontmatterLinks: object[], homepageLinks: object[],
92
102
  * usedManifest: Set<string>}} The findings, and which addresses a foreign
93
- * manifest answered.
103
+ * manifest answered. Each `deadAddresses` entry carries a `reason`:
104
+ * `"not-an-address"` when the target does not parse as one at all,
105
+ * `"unknown-type"` when it is qualified but names no known type, and
106
+ * `"unresolved"` when it parses and nothing answers it.
94
107
  */
95
108
  export function auditLinks(index: ReturnType<typeof buildLinkIndex>): {
96
109
  deadAnchors: object[];
97
110
  deadAddresses: object[];
111
+ deadAliases: object[];
112
+ aliasCollisions: object[];
98
113
  frontmatterLinks: object[];
99
114
  homepageLinks: object[];
100
115
  usedManifest: Set<string>;
@@ -39,6 +39,12 @@ export function lintContentTree(contentBase: string, { skipDirectories, contentP
39
39
  /**
40
40
  * The shape every `shortcode` must match: ASCII letters and digits only.
41
41
  *
42
+ * This is {@link ADDRESS_SEGMENT_PATTERN}, not a second copy of it. A shortcode
43
+ * is the last segment of a canonical address, and the rule it is held to is the
44
+ * rule *every* segment is held to — so the two are one constant rather than two
45
+ * free to drift apart (#59). The name survives because this is where the rule
46
+ * is applied to a note.
47
+ *
42
48
  * Case is deliberately **not** constrained: hundreds of authored shortcodes are
43
49
  * mixed-case and collide with nothing, so tightening that is a separate
44
50
  * decision from this one.
@@ -210,3 +210,32 @@ export function positionOfYamlPath(text: string, keyPath: ReadonlyArray<string |
210
210
  * @returns {Array<string|number>} Its segments, sequence indices as numbers.
211
211
  */
212
212
  export function yamlKeyPath(field: string): Array<string | number>;
213
+ /**
214
+ * Where a key sits **inside a note's frontmatter fence**, addressed by path.
215
+ *
216
+ * {@link positionInFrontmatter} searches the fence for a key by name, which is
217
+ * the right tool while a note's vocabulary is flat: a key appears once and the
218
+ * first line matching it is the one. It stops being the right tool the moment
219
+ * the same name is legal in two regions — `data.weight` and a top-level
220
+ * `weight` are different keys, and a search finds whichever comes first.
221
+ *
222
+ * So this is the located form for the closed regions (#128). It reuses
223
+ * {@link positionOfYamlPath} rather than repeating its parse, which is what
224
+ * keeps a finding about `data.weight` pointing at `data.weight`.
225
+ *
226
+ * @param {string} raw - The note's full contents, frontmatter included.
227
+ * @param {ReadonlyArray<string|number>} keyPath - Path to the node, from the
228
+ * top of the frontmatter.
229
+ * @param {object} [opts]
230
+ * @param {boolean} [opts.key=false] - Report where the last segment is
231
+ * *declared* rather than where its value sits.
232
+ * @returns {{line?: number, column?: number}} Spreadable position fields, empty
233
+ * when there is no fence or the path resolves to nothing — dropped rather
234
+ * than guessed.
235
+ */
236
+ export function positionOfFrontmatterPath(raw: string, keyPath: ReadonlyArray<string | number>, { key }?: {
237
+ key?: boolean | undefined;
238
+ }): {
239
+ line?: number;
240
+ column?: number;
241
+ };
@@ -0,0 +1,233 @@
1
+ /**
2
+ * One row of a system's map: what a note of this type becomes.
3
+ *
4
+ * @typedef {object} DocumentSubtypeRow
5
+ * @property {string} document - The Foundry document class — `"Item"`,
6
+ * `"Actor"`, and so on. It must agree with what the engine's own
7
+ * {@link packForType} routes the type to; they are two statements about one
8
+ * fact, and a test holds them together.
9
+ * @property {string} [subType] - The document subtype, for a one-to-one row.
10
+ * Mutually exclusive with `discriminator`.
11
+ * @property {string} [discriminator] - For a one-to-many row: the key the note
12
+ * supplies in this system's own frontmatter block to say which subtype it
13
+ * is.
14
+ * @property {readonly string[]} [subTypes] - The subtypes `discriminator` may
15
+ * name. Required with it, and never empty — a row permitting nothing is a
16
+ * row that can never resolve.
17
+ */
18
+ /**
19
+ * A system's whole declaration: which system it belongs to, which frontmatter
20
+ * block its notes write, and every note type it maps.
21
+ *
22
+ * @typedef {object} DocumentSubtypeMap
23
+ * @property {string} system - The system this map belongs to (`"sohl"`).
24
+ * @property {string} block - The note frontmatter block this system's notes
25
+ * write, and the only place a discriminator is read from. Defaults to
26
+ * `system`.
27
+ * @property {Readonly<Record<string, DocumentSubtypeRow>>} types - Note type →
28
+ * row.
29
+ */
30
+ /**
31
+ * Declare one system's note-type → document-subtype map.
32
+ *
33
+ * Every malformed row is refused **here**, at module evaluation, rather than
34
+ * when some note happens to reach it: a map is a small piece of authored data
35
+ * loaded once per build, so a mistake in it should stop the build immediately
36
+ * and name the row, not surface as a missing document a thousand notes later.
37
+ *
38
+ * @param {object} declaration - The system's declaration.
39
+ * @param {string} declaration.system - The system this map belongs to.
40
+ * @param {string} [declaration.block] - The frontmatter block its notes write.
41
+ * Defaults to `system`.
42
+ * @param {Readonly<Record<string, DocumentSubtypeRow>>} declaration.types -
43
+ * Note type → row.
44
+ * @returns {DocumentSubtypeMap} The frozen map, rows and all.
45
+ * @throws {Error} When the declaration names no system, or any row is neither
46
+ * of the two permitted shapes.
47
+ */
48
+ export function defineDocumentSubtypes({ system, block, types }?: {
49
+ system: string;
50
+ block?: string | undefined;
51
+ types: Readonly<Record<string, DocumentSubtypeRow>>;
52
+ }): DocumentSubtypeMap;
53
+ /**
54
+ * The row a system declares for a note type, or nothing.
55
+ *
56
+ * @param {DocumentSubtypeMap} map - The system's map.
57
+ * @param {string|undefined} noteType - The note's declared `type`.
58
+ * @returns {Readonly<DocumentSubtypeRow>|undefined} The row, or `undefined`
59
+ * where this system maps the type at all.
60
+ */
61
+ export function subtypeRow(map: DocumentSubtypeMap, noteType: string | undefined): Readonly<DocumentSubtypeRow> | undefined;
62
+ /**
63
+ * Whether a system maps a note type — optionally, onto one document class.
64
+ *
65
+ * This is what a pass asks to decide whether it claims a note, which is the
66
+ * whole of the "no mapping, no document" property: a type the map does not
67
+ * carry is skipped exactly as quietly as the thousands of notes that belong to
68
+ * another pass.
69
+ *
70
+ * @param {DocumentSubtypeMap} map - The system's map.
71
+ * @param {string|undefined} noteType - The note's declared `type`.
72
+ * @param {string} [document] - Restrict the question to one document class.
73
+ * @returns {boolean} True when the map carries a matching row.
74
+ */
75
+ export function mapsNoteType(map: DocumentSubtypeMap, noteType: string | undefined, document?: string): boolean;
76
+ /**
77
+ * Every note type a system maps onto one document class, sorted.
78
+ *
79
+ * A pass reports what it skipped by naming what it would have claimed, and
80
+ * that list is the map's — never a constant restating it.
81
+ *
82
+ * @param {DocumentSubtypeMap} map - The system's map.
83
+ * @param {string} document - The Foundry document class.
84
+ * @returns {string[]} The note types, in sorted order.
85
+ */
86
+ export function noteTypesFor(map: DocumentSubtypeMap, document: string): string[];
87
+ /**
88
+ * The document subtype a note compiles into for one system.
89
+ *
90
+ * @param {DocumentSubtypeMap} map - The system's map.
91
+ * @param {string|undefined} noteType - The note's declared `type`.
92
+ * @param {object} fm - The note's frontmatter, read only for a one-to-many
93
+ * row's discriminator.
94
+ * @param {object} [options] - Options.
95
+ * @param {string} [options.file] - The note's path, appended to the message.
96
+ * Omit it where the caller emits through a diagnostic, which already puts
97
+ * the locator at the start of the line.
98
+ * @param {string} [options.absPath] - The note's file on disk, read only on
99
+ * the failing path to locate the offending line. The position rides on the
100
+ * thrown error as `position`, for a caller that emits a diagnostic.
101
+ * @returns {string|undefined} The subtype, or `undefined` where this system
102
+ * maps nothing for the type — which means no document, not an error.
103
+ * @throws {Error} When a one-to-many row's discriminator is absent, blank, or
104
+ * names a value the row does not permit.
105
+ */
106
+ export function documentSubtype(map: DocumentSubtypeMap, noteType: string | undefined, fm: object, { file, absPath }?: {
107
+ file?: string | undefined;
108
+ absPath?: string | undefined;
109
+ }): string | undefined;
110
+ /**
111
+ * The answer {@link referencedSubtype} gives: a subtype, or why there is none.
112
+ *
113
+ * Exactly one of the two fields is set. A `problem` is a sentence a caller
114
+ * prefixes with its own context and emits as a finding — it never throws,
115
+ * because the caller resolving a reference is walking a list and has to report
116
+ * this one and carry on.
117
+ *
118
+ * @typedef {object} ReferencedSubtype
119
+ * @property {string} [subType] - The document subtype the reference addresses.
120
+ * @property {string} [problem] - Why the reference names no document subtype.
121
+ */
122
+ /**
123
+ * The document subtype a `(type, shortcode)` **reference** addresses (#140).
124
+ *
125
+ * A being's frontmatter names each embedded item by the *note's* type — the
126
+ * vocabulary an author writes — while the predefined items it resolves against
127
+ * are compiled documents, which carry only the *subtype*. One side has to
128
+ * translate, and it is this one: the map is a function from note type to
129
+ * subtype by construction, whereas the reverse is not — two note types may
130
+ * compile into one subtype, and a compiled document records nothing about the
131
+ * note that produced it. So the addresses stay keyed on the **document
132
+ * subtype**, which is the only vocabulary both a local pack and an extracted
133
+ * dependency catalogue actually carry, and a reference is translated forward
134
+ * here before it is looked up.
135
+ *
136
+ * Four answers, and only the first resolves:
137
+ *
138
+ * - _A one-to-one row_ → the subtype it declares. `armor` addresses an
139
+ * `armorgear`.
140
+ * - _No row at all_ → the note type itself. A consumer declares its own item
141
+ * types in its `itemBuilders` table rather than in this system's map, and the
142
+ * Item pass stamps such a document with the note type; the reference has to
143
+ * agree, or a consumer's own items would stop resolving the moment a map
144
+ * existed.
145
+ * - _A row for another document class_ → a problem. A being is not an item,
146
+ * however the address is spelled.
147
+ * - _A one-to-many row_ → a problem naming the candidates. The note that owns
148
+ * such a row resolves it from its own frontmatter block; a reference has no
149
+ * block, so nothing here can choose, and choosing anyway would be right about
150
+ * half the time. No system declares a one-to-many **Item** row today, so this
151
+ * is a guard rather than a behaviour — but it is a loud one, which is the
152
+ * whole point of the issue.
153
+ *
154
+ * A **retired** spelling is refused by name before any of that. Without it a
155
+ * reference left behind by a rename would take the unmapped fallback and
156
+ * address a document of the old name — resolving silently, which is precisely
157
+ * what a retirement exists to stop (#78).
158
+ *
159
+ * @param {DocumentSubtypeMap} map - The system's map.
160
+ * @param {string|undefined} noteType - The type the reference names.
161
+ * @param {string} document - The Foundry document class the reference must
162
+ * address — `"Item"` for a being's embedded items.
163
+ * @returns {ReferencedSubtype} The subtype, or why there is none.
164
+ */
165
+ export function referencedSubtype(map: DocumentSubtypeMap, noteType: string | undefined, document: string): ReferencedSubtype;
166
+ /**
167
+ * One row of a system's map: what a note of this type becomes.
168
+ */
169
+ export type DocumentSubtypeRow = {
170
+ /**
171
+ * - The Foundry document class — `"Item"`,
172
+ * `"Actor"`, and so on. It must agree with what the engine's own
173
+ * {@link packForType} routes the type to; they are two statements about one
174
+ * fact, and a test holds them together.
175
+ */
176
+ document: string;
177
+ /**
178
+ * - The document subtype, for a one-to-one row.
179
+ * Mutually exclusive with `discriminator`.
180
+ */
181
+ subType?: string | undefined;
182
+ /**
183
+ * - For a one-to-many row: the key the note
184
+ * supplies in this system's own frontmatter block to say which subtype it
185
+ * is.
186
+ */
187
+ discriminator?: string | undefined;
188
+ /**
189
+ * - The subtypes `discriminator` may
190
+ * name. Required with it, and never empty — a row permitting nothing is a
191
+ * row that can never resolve.
192
+ */
193
+ subTypes?: readonly string[] | undefined;
194
+ };
195
+ /**
196
+ * A system's whole declaration: which system it belongs to, which frontmatter
197
+ * block its notes write, and every note type it maps.
198
+ */
199
+ export type DocumentSubtypeMap = {
200
+ /**
201
+ * - The system this map belongs to (`"sohl"`).
202
+ */
203
+ system: string;
204
+ /**
205
+ * - The note frontmatter block this system's notes
206
+ * write, and the only place a discriminator is read from. Defaults to
207
+ * `system`.
208
+ */
209
+ block: string;
210
+ /**
211
+ * - Note type →
212
+ * row.
213
+ */
214
+ types: Readonly<Record<string, DocumentSubtypeRow>>;
215
+ };
216
+ /**
217
+ * The answer {@link referencedSubtype} gives: a subtype, or why there is none.
218
+ *
219
+ * Exactly one of the two fields is set. A `problem` is a sentence a caller
220
+ * prefixes with its own context and emits as a finding — it never throws,
221
+ * because the caller resolving a reference is walking a list and has to report
222
+ * this one and carry on.
223
+ */
224
+ export type ReferencedSubtype = {
225
+ /**
226
+ * - The document subtype the reference addresses.
227
+ */
228
+ subType?: string | undefined;
229
+ /**
230
+ * - Why the reference names no document subtype.
231
+ */
232
+ problem?: string | undefined;
233
+ };