@heroiclands/package-build 8.1.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 (122) hide show
  1. package/CHANGELOG.md +748 -0
  2. package/CONTENT.md +273 -13
  3. package/bin/content-build.mjs +479 -123
  4. package/bin/package-build.mjs +27 -69
  5. package/bin/report.mjs +1 -2
  6. package/bundle.mjs +2 -10
  7. package/config.mjs +31 -106
  8. package/container.mjs +13 -57
  9. package/content-config.mjs +300 -188
  10. package/coverage.mjs +14 -55
  11. package/deploy.mjs +4 -13
  12. package/docs/content-format.md +1418 -0
  13. package/e2e.mjs +16 -55
  14. package/engine/address-charset.mjs +62 -0
  15. package/engine/address-diff.mjs +1 -4
  16. package/engine/alias-index.mjs +153 -0
  17. package/engine/base-compiler.mjs +203 -31
  18. package/engine/code-fences.mjs +4 -13
  19. package/engine/compendiums.mjs +13 -37
  20. package/engine/content-address.mjs +6 -10
  21. package/engine/content-format-check.mjs +570 -0
  22. package/engine/content-format.mjs +253 -0
  23. package/engine/content-links.mjs +144 -99
  24. package/engine/content-lint.mjs +12 -16
  25. package/engine/content-slug.mjs +2 -6
  26. package/engine/content-tables.mjs +26 -79
  27. package/engine/diagnostics.mjs +37 -15
  28. package/engine/document-subtypes.mjs +440 -0
  29. package/engine/field-reference.mjs +6 -20
  30. package/engine/field-spec.mjs +49 -45
  31. package/engine/foreign-catalog.mjs +7 -22
  32. package/engine/foreign-manifests.mjs +1 -4
  33. package/engine/frontmatter-lint.mjs +347 -43
  34. package/engine/frontmatter.mjs +3 -8
  35. package/engine/generate.mjs +36 -20
  36. package/engine/helpers.mjs +54 -81
  37. package/engine/homepage.mjs +4 -15
  38. package/engine/ids.mjs +21 -12
  39. package/engine/index.mjs +15 -0
  40. package/engine/item-registry.mjs +72 -9
  41. package/engine/journals.mjs +4 -14
  42. package/engine/kb-manifest.mjs +41 -24
  43. package/engine/macros.mjs +2 -10
  44. package/engine/manifest-emit.mjs +6 -17
  45. package/engine/map-notes.mjs +53 -87
  46. package/engine/note-claims.mjs +383 -0
  47. package/engine/note-package.mjs +1 -4
  48. package/engine/note-vocabulary.mjs +678 -0
  49. package/engine/pack-config.mjs +56 -60
  50. package/engine/pack-router.mjs +18 -8
  51. package/engine/prose-config.mjs +20 -4
  52. package/engine/prose-lint.mjs +61 -17
  53. package/engine/region-events.mjs +1 -3
  54. package/engine/retired-fields.mjs +117 -3
  55. package/engine/scene-levels.mjs +8 -22
  56. package/engine/scenes.mjs +31 -47
  57. package/engine/schema-check.mjs +348 -7
  58. package/engine/schema-extract.mjs +11 -39
  59. package/engine/site-build.mjs +13 -38
  60. package/engine/site-index.mjs +40 -35
  61. package/engine/system-block.mjs +513 -0
  62. package/engine/web-wikilinks.mjs +115 -92
  63. package/engine/wikilink-syntax.mjs +30 -0
  64. package/engine/wikilinks.mjs +147 -183
  65. package/index.mjs +1 -5
  66. package/lang.mjs +1 -3
  67. package/manifest.mjs +10 -37
  68. package/markdownlint-config.mjs +1 -5
  69. package/package.json +6 -2
  70. package/sohl/actors.mjs +251 -68
  71. package/sohl/being-info.mjs +3 -6
  72. package/sohl/document-subtypes.mjs +82 -0
  73. package/sohl/index.mjs +4 -6
  74. package/sohl/item-builders.mjs +1 -3
  75. package/sohl/item-fields.mjs +16 -34
  76. package/sohl/items.mjs +111 -17
  77. package/sohl/kb-passes.mjs +29 -39
  78. package/sohl/note-schemas.mjs +11 -7
  79. package/sohl/skill-base.mjs +7 -23
  80. package/stage.mjs +3 -13
  81. package/templates.mjs +4 -15
  82. package/types/bundle.d.mts +1 -1
  83. package/types/container.d.mts +2 -2
  84. package/types/content-config.d.mts +48 -4
  85. package/types/coverage.d.mts +1 -1
  86. package/types/e2e.d.mts +4 -4
  87. package/types/engine/address-charset.d.mts +45 -0
  88. package/types/engine/alias-index.d.mts +122 -0
  89. package/types/engine/base-compiler.d.mts +132 -4
  90. package/types/engine/content-address.d.mts +2 -2
  91. package/types/engine/content-format-check.d.mts +163 -0
  92. package/types/engine/content-format.d.mts +101 -0
  93. package/types/engine/content-links.d.mts +16 -1
  94. package/types/engine/content-lint.d.mts +6 -0
  95. package/types/engine/diagnostics.d.mts +29 -0
  96. package/types/engine/document-subtypes.d.mts +233 -0
  97. package/types/engine/field-spec.d.mts +76 -23
  98. package/types/engine/frontmatter-lint.d.mts +47 -2
  99. package/types/engine/generate.d.mts +15 -2
  100. package/types/engine/helpers.d.mts +22 -14
  101. package/types/engine/ids.d.mts +10 -0
  102. package/types/engine/index.d.mts +5 -0
  103. package/types/engine/item-registry.d.mts +21 -2
  104. package/types/engine/kb-manifest.d.mts +35 -8
  105. package/types/engine/map-notes.d.mts +21 -11
  106. package/types/engine/note-claims.d.mts +113 -0
  107. package/types/engine/note-vocabulary.d.mts +251 -0
  108. package/types/engine/pack-config.d.mts +4 -3
  109. package/types/engine/pack-router.d.mts +4 -4
  110. package/types/engine/prose-lint.d.mts +6 -2
  111. package/types/engine/retired-fields.d.mts +73 -2
  112. package/types/engine/schema-check.d.mts +182 -0
  113. package/types/engine/schema-extract.d.mts +1 -1
  114. package/types/engine/site-index.d.mts +1 -1
  115. package/types/engine/system-block.d.mts +281 -0
  116. package/types/engine/web-wikilinks.d.mts +23 -12
  117. package/types/engine/wikilink-syntax.d.mts +29 -0
  118. package/types/manifest.d.mts +1 -1
  119. package/types/sohl/actors.d.mts +62 -6
  120. package/types/sohl/document-subtypes.d.mts +14 -0
  121. package/types/sohl/index.d.mts +1 -0
  122. package/types/sohl/items.d.mts +21 -0
@@ -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
+ };
@@ -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
+ }>>;