@heroiclands/package-build 22.9.1 → 22.10.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 (100) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/CONTENT.md +33 -0
  3. package/bin/content-build.mjs +3 -1
  4. package/docs/api.md +10 -6
  5. package/docs/content-format.md +384 -200
  6. package/docs/project-setup.md +23 -0
  7. package/engine/actor-compiler.mjs +20 -7
  8. package/engine/address-fields.mjs +404 -0
  9. package/engine/address-values.mjs +80 -0
  10. package/engine/address.mjs +769 -0
  11. package/engine/art-fields.mjs +112 -97
  12. package/engine/art-slots.mjs +81 -0
  13. package/engine/base-compiler.mjs +59 -7
  14. package/engine/bundle-notes.mjs +5 -2
  15. package/engine/bundles.mjs +17 -10
  16. package/engine/content-address.mjs +23 -186
  17. package/engine/content-embeds.mjs +18 -4
  18. package/engine/content-format-check.mjs +50 -22
  19. package/engine/content-index.mjs +85 -43
  20. package/engine/content-links.mjs +105 -104
  21. package/engine/content-lint.mjs +11 -1
  22. package/engine/content-tables.mjs +9 -1
  23. package/engine/field-spec.mjs +20 -4
  24. package/engine/folder-notes.mjs +44 -15
  25. package/engine/foundry-entries.mjs +3 -0
  26. package/engine/frontmatter-lint.mjs +144 -43
  27. package/engine/frontmatter.mjs +55 -10
  28. package/engine/generate.mjs +7 -1
  29. package/engine/helpers.mjs +37 -9
  30. package/engine/holdings.mjs +41 -12
  31. package/engine/index-records.mjs +2 -0
  32. package/engine/infobox-render.mjs +2 -1
  33. package/engine/infobox.mjs +26 -6
  34. package/engine/item-compiler.mjs +5 -0
  35. package/engine/item-docs.mjs +7 -44
  36. package/engine/map-dot.mjs +2 -0
  37. package/engine/map-notes.mjs +7 -2
  38. package/engine/map-places.mjs +42 -10
  39. package/engine/metadata-index.mjs +60 -5
  40. package/engine/note-addresses.mjs +310 -0
  41. package/engine/note-vocabulary.mjs +145 -26
  42. package/engine/pdf-build.mjs +1 -1
  43. package/engine/pdf-toc.mjs +17 -2
  44. package/engine/place-relations.mjs +160 -50
  45. package/engine/populations.mjs +17 -9
  46. package/engine/reference-targets.mjs +54 -0
  47. package/engine/scenes.mjs +5 -2
  48. package/engine/shortcode-references.mjs +68 -0
  49. package/engine/site-build.mjs +39 -15
  50. package/engine/site-index.mjs +57 -75
  51. package/engine/sql-tables.mjs +43 -9
  52. package/engine/system-block.mjs +21 -0
  53. package/engine/system-vocabulary.mjs +46 -0
  54. package/engine/web-wikilinks.mjs +11 -9
  55. package/engine/wikilink-syntax.mjs +21 -0
  56. package/engine/wikilinks.mjs +92 -306
  57. package/githooks/attribution.mjs +82 -0
  58. package/githooks/commit-message.mjs +26 -0
  59. package/githooks/commit-msg +3 -35
  60. package/hm3/actors.mjs +8 -24
  61. package/hm3/item-fields.mjs +15 -5
  62. package/package.json +1 -1
  63. package/sohl/infobox.mjs +80 -17
  64. package/sohl/item-fields.mjs +114 -41
  65. package/types/engine/address-fields.d.mts +90 -0
  66. package/types/engine/address-values.d.mts +34 -0
  67. package/types/engine/address.d.mts +452 -0
  68. package/types/engine/art-fields.d.mts +59 -53
  69. package/types/engine/art-slots.d.mts +40 -0
  70. package/types/engine/base-compiler.d.mts +24 -2
  71. package/types/engine/bundle-notes.d.mts +2 -2
  72. package/types/engine/content-address.d.mts +3 -139
  73. package/types/engine/content-embeds.d.mts +1 -1
  74. package/types/engine/content-format-check.d.mts +25 -0
  75. package/types/engine/content-index.d.mts +2 -2
  76. package/types/engine/content-links.d.mts +6 -5
  77. package/types/engine/field-spec.d.mts +44 -9
  78. package/types/engine/folder-notes.d.mts +11 -10
  79. package/types/engine/frontmatter-lint.d.mts +7 -97
  80. package/types/engine/frontmatter.d.mts +16 -1
  81. package/types/engine/helpers.d.mts +2 -2
  82. package/types/engine/infobox.d.mts +6 -0
  83. package/types/engine/item-docs.d.mts +1 -35
  84. package/types/engine/map-notes.d.mts +2 -2
  85. package/types/engine/note-addresses.d.mts +41 -0
  86. package/types/engine/note-vocabulary.d.mts +20 -3
  87. package/types/engine/place-relations.d.mts +12 -1
  88. package/types/engine/populations.d.mts +17 -2
  89. package/types/engine/reference-targets.d.mts +9 -0
  90. package/types/engine/shortcode-references.d.mts +14 -0
  91. package/types/engine/site-build.d.mts +3 -3
  92. package/types/engine/site-index.d.mts +9 -30
  93. package/types/engine/sql-tables.d.mts +2 -2
  94. package/types/engine/system-block.d.mts +17 -0
  95. package/types/engine/system-vocabulary.d.mts +16 -0
  96. package/types/engine/wikilink-syntax.d.mts +7 -0
  97. package/types/engine/wikilinks.d.mts +6 -109
  98. package/types/hm3/item-fields.d.mts +5 -0
  99. package/types/sohl/infobox.d.mts +4 -5
  100. package/types/sohl/item-fields.d.mts +8 -0
@@ -403,15 +403,37 @@ export class BasePackCompiler {
403
403
  *
404
404
  * An address nothing answers is reported against the note and treated as
405
405
  * unnamed, so the document takes its default rather than shipping a path
406
- * that installs nowhere.
406
+ * that installs nowhere. **A type this slot does not accept is different**:
407
+ * the address is real, so no default repairs it, and it is an error rather
408
+ * than a fallback.
407
409
  *
408
410
  * @param {unknown} value - The value as authored.
409
411
  * @param {string} key - The key it was authored at, for the message.
410
412
  * @param {string} type - The asset type a bare value takes.
413
+ * @param {Iterable<string>} [accepts] - The types this slot accepts.
411
414
  * @returns {string|null} The Foundry-relative path, `""` for a deliberate
412
415
  * blank, or `null` where the note names none.
413
416
  */
414
- artPathOf(value: unknown, key: string, type: string): string | null;
417
+ artPathOf(value: unknown, key: string, type: string, accepts?: Iterable<string>): string | null;
418
+ /**
419
+ * Reduce every Address an assembled `system` block holds to the Shortcode
420
+ * the document reads.
421
+ *
422
+ * The positions come from the type's own field declarations, so a system
423
+ * adding a field that names a note gets the reduction by declaring it; see
424
+ * {@link module:engine/address-fields}.
425
+ *
426
+ * Called **after** the builder and the authored merge have both written, so
427
+ * one step reduces a value the field read and a value authored straight at
428
+ * the destination alike. Each finding is located at the value inside the
429
+ * note, which for a list or a map is the entry that is wrong rather than the
430
+ * key that introduces it.
431
+ *
432
+ * @param {object} emitted - The `system` block, assembled.
433
+ * @param {readonly object[]} [fields] - The type's field declaration.
434
+ * @returns {number} How many findings were reported.
435
+ */
436
+ reduceEmittedAddresses(emitted: object, fields?: readonly object[]): number;
415
437
  /**
416
438
  * The Foundry path one art slot of a note names, or `null`.
417
439
  *
@@ -18,8 +18,8 @@ export function stripAdventureKeys(value: any): any;
18
18
  /**
19
19
  * An authored address with any wikilink brackets and label stripped.
20
20
  *
21
- * The specification types `contents` as a `WikiLink[]`, and a frontmatter link
22
- * is written as a bare address — but `[[address]]` is what an author reaches
21
+ * The specification types `contents` as an `Address[]`, written bare — but
22
+ * `[[address]]` is what an author reaches
23
23
  * for, and it is what a folder note's `parent` already accepts. Accepting both
24
24
  * costs one regex and removes a failure whose message would have to explain the
25
25
  * difference.
@@ -2,7 +2,7 @@
2
2
  * The single path segment a note is addressed by: `type-shortcode`.
3
3
  *
4
4
  * Lowercased and hyphen-joined by the same rule as the note's canonical key
5
- * ({@link canonicalKey}, below, lowercases too), so it is that
5
+ * ({@link canonicalKey} lowercases too), so it is that
6
6
  * key's **last two segments** — which is what makes a manifest entry's `path`
7
7
  * derivable from the key it is filed under rather than transported beside it.
8
8
  *
@@ -64,55 +64,6 @@ export function addressSlug(fm: object): string;
64
64
  * dead entry in the manifest.
65
65
  */
66
66
  export function packageAddress(fm: object): string;
67
- /**
68
- * The **canonical** address of a note: fully qualified, one spelling per
69
- * document, and globally unique.
70
- *
71
- * The written form of a link is a **partial** address: it may omit leading
72
- * segments, and each one it omits is filled in by rule rather than left
73
- * unconstrained. An omitted package (`[[skill-lang]]`) defaults to the citing
74
- * note's own, so an unqualified link resolves locally and only locally, and a
75
- * link into another package must name it. An omitted **system** is a
76
- * *wildcard*, not a default — most links target items, which belong to a
77
- * system — and the resolver requires exactly one match: none is a dead link,
78
- * more than one is an ambiguity reported with every candidate named.
79
- * Everything internal — index keys, cache keys, every lookup — uses this fully
80
- * qualified form instead, so no consumer has to know what a short form
81
- * defaulted to or matched.
82
- *
83
- * Global uniqueness is what lets a dependency's index merge straight into a
84
- * local one: the keys cannot collide by accident, so a key already present on
85
- * merge is a real conflict rather than an artefact of two packages sharing a
86
- * namespace. `(type, shortcode)` alone is unique only *within* a package, and
87
- * two independently authored packages reaching for the same short string is a
88
- * matter of time.
89
- *
90
- * **The system segment.** A package may ship content for more than one
91
- * system, and one note then compiles into a document per system — an actor in
92
- * `actors-sohl` *and* an actor in `actors-hm3`. Without a system segment both
93
- * land on one key, so the address cannot name either of them. `harn-ensemble`
94
- * carries 2,497 such notes.
95
- *
96
- * The value is a system id, or the literal **`none`** for a document no game
97
- * system defines: a journal, a macro, a scene, and an item's documentation
98
- * journal — which is `none` however many systems the item itself declares,
99
- * because it is one journal.
100
- *
101
- * `none` rather than `any`: every segment of an address is an exact literal,
102
- * and `any` reads as a wildcard — "matches under any system" — which is not
103
- * what it does. A resolver written to that misreading would fail silently,
104
- * since a lookup miss already returns nothing rather than erroring. And not
105
- * `null` or `~`, both of which are YAML nulls that parse to an absent value and
106
- * drop the segment entirely.
107
- *
108
- * @param {string} pkg - The owning **content** package (`sohl`, `thalorna`) —
109
- * not the Foundry package, which varies per compilation target.
110
- * @param {string} system - The system whose document this addresses, or `none`.
111
- * @param {string} type - The note's `type`.
112
- * @param {string} shortcode - The note's `shortcode`.
113
- * @returns {string} `package-system-type-shortcode`, lowercased.
114
- */
115
- export function canonicalKey(pkg: string, system: string, type: string, shortcode: string): string;
116
67
  /**
117
68
  * Which system a frontmatter key path is written under.
118
69
  *
@@ -122,7 +73,7 @@ export function canonicalKey(pkg: string, system: string, type: string, shortcod
122
73
  * top-level frontmatter, the shared `data:` container, and body prose, which has
123
74
  * no key path at all and passes `undefined`.
124
75
  *
125
- * It is the block rather than the field, so a `WikiLink` field needs no opinion
76
+ * It is the block rather than the field, so an `Address` field needs no opinion
126
77
  * about systems and no per-field table has to be kept in step with the schema.
127
78
  *
128
79
  * The first segment must **be** a declared system, not merely look like one:
@@ -134,83 +85,6 @@ export function canonicalKey(pkg: string, system: string, type: string, shortcod
134
85
  * @returns {string} The system id, or `none`.
135
86
  */
136
87
  export function blockSystem(keyPath?: string): string;
137
- /**
138
- * Expand a written address to the one canonical address it names.
139
- *
140
- * **An omitted segment defaults from where the link is written** — it is
141
- * not a wildcard, and resolution is not a search. Package omitted means the
142
- * citing note's own; system omitted means {@link blockSystem} of the key path it
143
- * was written under. So every short form has exactly one expansion, computed
144
- * before anything is looked up, and there is no candidate set to disambiguate.
145
- *
146
- * **Under `none`, a system-bearing type addresses its documentation journal.**
147
- * A note's `none` address *is* its `doc<type>` entry — the Item is the one with
148
- * a system — so a prose `[[affiliation-sirvadar|…]]` names the page, which is
149
- * almost always what prose means. A link that means the Item states the system
150
- * and gets it. This is the defaulting rule applied, not an exception carved out
151
- * of it.
152
- *
153
- * **Only a type whose own document carries a system is redirected.** A `macro`
154
- * and the map types have documentation journals too, but their own documents
155
- * are core ones and already live at `none` — so `<pkg>-none-macro-x` names the
156
- * Macro and `<pkg>-none-docmacro-x` its journal, two live addresses that the
157
- * redirect would collapse into one. The test is the note type's own system,
158
- * not merely whether it has a doc entry.
159
- *
160
- * A `doc<type>` written explicitly is `none` **wherever** it appears, even
161
- * inside a system block: no game system defines a JournalEntry, so there is no
162
- * other system for one to belong to.
163
- *
164
- * @param {{type: string, shortcode: string, package?: string, system?: string,
165
- * itemDoc?: boolean}} read - A qualifier, as `readQualifier` returns one.
166
- * @param {{package: string, system?: string}} where - The citing context: the
167
- * tree's own content package, and the system of the block the link sits in.
168
- * @returns {string} The canonical `package-system-type-shortcode`.
169
- */
170
- export function expandAddress(read: {
171
- type: string;
172
- shortcode: string;
173
- package?: string;
174
- system?: string;
175
- itemDoc?: boolean;
176
- }, where: {
177
- package: string;
178
- system?: string;
179
- }): string;
180
- /**
181
- * Reads a canonical key back into its parts.
182
- *
183
- * Parsing is plain positional counting: split on the separator, require
184
- * {@link CANONICAL_KEY_SEGMENTS} of them, and assign each position its field.
185
- * **The charset rule is what makes that sound** — every segment matches
186
- * `ADDRESS_SEGMENT_PATTERN` (`engine/address-charset.mjs`), so the hyphen is
187
- * purely a separator and the count alone determines every field. That is
188
- * enforced at each of the three sources rather than assumed of
189
- * the data: shortcodes by `content-lint.mjs`, `contentPackage` by
190
- * `defineConfig`, and types are bare words. Were any of them free to
191
- * carry a hyphen, no amount of counting would recover the fields and the reader
192
- * would need a vocabulary to match against instead.
193
- *
194
- * **Nothing to read and nothing readable are different answers.** A key that
195
- * cannot be canonical — `harn-adventures-sohl-skill-melee`, five segments,
196
- * because the package name carries the separator — yields `null`, while an
197
- * absent or blank input yields `undefined`. Both are falsy, so
198
- * every call site (all of which test the result for truthiness) is unaffected;
199
- * the distinction is there so a caller reporting "this key is unreadable" can
200
- * tell that it has a key to report about.
201
- *
202
- * @param {unknown} key - A canonical key, or nothing.
203
- * @returns {{package: string, system: string, type: string, shortcode: string}
204
- * |null|undefined}
205
- * The parts; `null` when there is a string that is not in canonical form;
206
- * `undefined` when there is no key at all.
207
- */
208
- export function readCanonicalKey(key: unknown): {
209
- package: string;
210
- system: string;
211
- type: string;
212
- shortcode: string;
213
- } | null | undefined;
214
88
  /**
215
89
  * Asserts a base is usable as a prefix and returns it.
216
90
  *
@@ -280,17 +154,6 @@ export function resolvePackageUrl(rel: string, base: string): string;
280
154
  export function documentId(pkg: string, system: string, type: string, shortcode: string): string;
281
155
  /** The knowledgebase's mount within this package's site. */
282
156
  export const KB_PREFIX: "kb/";
283
- /**
284
- * How many segments a canonical key has, and therefore how many the reader
285
- * below counts.
286
- *
287
- * Named rather than written as a literal because it is the *grammar*, not an
288
- * implementation detail of one function: it is the number a change to the
289
- * address form would move, and the thing a reader of that change has to find.
290
- *
291
- * @type {number}
292
- */
293
- export const CANONICAL_KEY_SEGMENTS: number;
294
157
  /**
295
158
  * Where this build serves each package, keyed by package name.
296
159
  *
@@ -324,3 +187,4 @@ export const DOCUMENT_ID_NAMESPACE: string;
324
187
  import { DEFAULT_ADDRESS_SCHEME } from "../content-config.mjs";
325
188
  import { NO_SYSTEM } from "./systems.mjs";
326
189
  export { DEFAULT_ADDRESS_SCHEME, NO_SYSTEM };
190
+ export { canonicalKey, expandAddress, readCanonicalKey, CANONICAL_KEY_SEGMENTS } from "./address.mjs";
@@ -24,7 +24,7 @@
24
24
  * @param {string} body - The note's markdown, without its frontmatter.
25
25
  * @returns {ParsedEmbed[]} One entry per embed.
26
26
  */
27
- export function embedsIn(body: string): ParsedEmbed[];
27
+ export function embedsIn(body: string, context: any): ParsedEmbed[];
28
28
  /**
29
29
  * What one embed resolves to, or why it does not.
30
30
  *
@@ -105,6 +105,31 @@ export function measureCorpus(notes: Iterable<object>, format: import("./content
105
105
  * @returns {string} The message.
106
106
  */
107
107
  export function fieldDriftMessage({ noteType, source, target, name, to }: object): string;
108
+ /**
109
+ * The declaration that compiles one mapping row, and what is left over.
110
+ *
111
+ * The longest declared name the row's source sits under: a field declared
112
+ * `impact.die` claims `data.impact.die` ahead of any field declared `impact`,
113
+ * and a field declared `charges` claims `data.charges.max` with `.max` left
114
+ * over. Compared on the normalized spelling, so a field naming its source under
115
+ * `data:` matches at the same length a bare one does.
116
+ *
117
+ * Exported because two readers ask it and must agree: {@link
118
+ * checkDeclaredFields}, which holds the row's target to the declaration's, and
119
+ * the guard that holds the declaration's *source* to the row's — a second copy
120
+ * of the match would let the two disagree about which declaration a row is even
121
+ * about.
122
+ *
123
+ * @param {string} source - The row's shared source, as either side spells it.
124
+ * @param {readonly object[]} fields - The type's authored declarations.
125
+ * @returns {{field: object, rest: string}|undefined} The declaration and the
126
+ * remainder of the source beneath its name, or nothing where no declaration
127
+ * names it.
128
+ */
129
+ export function declarationFor(source: string, fields: readonly object[]): {
130
+ field: object;
131
+ rest: string;
132
+ } | undefined;
108
133
  /**
109
134
  * Check the specification's per-type tables against the field declarations that
110
135
  * compile them.
@@ -112,7 +112,7 @@ export function asciiAliases(aliases: unknown): Array<string>;
112
112
  * otherwise be overwritten without a word. `file` and, where the file was
113
113
  * read, `position` ride on the error.
114
114
  */
115
- export function buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest, }: {
115
+ export function buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest, addressContext, }: {
116
116
  frontmatter: Record<string, any>;
117
117
  relPath: string;
118
118
  absPath?: string | undefined;
@@ -152,7 +152,7 @@ export function buildIndexRecord({ frontmatter, relPath, absPath, contentPackage
152
152
  * @returns {Array<Record<string, any>>} The records, in a total order that does
153
153
  * not depend on directory-read order.
154
154
  */
155
- export function collectContentIndex(contentBase: string, { contentPackage, skipDirectories, assetsBase, manifest, problems }: {
155
+ export function collectContentIndex(contentBase: string, { contentPackage, skipDirectories, assetsBase, manifest, problems, addressContext }: {
156
156
  contentPackage: string;
157
157
  skipDirectories: readonly string[];
158
158
  assetsBase?: string | undefined;
@@ -131,11 +131,12 @@ export function auditHomepageLinks(index: ReturnType<typeof buildLinkIndex>): Ar
131
131
  * unlabelledLinks: object[], frontmatterLinks: object[],
132
132
  * homepageLinks: object[], usedManifest: Set<string>}} The findings, and
133
133
  * which addresses a foreign manifest answered. Each `deadAddresses` entry
134
- * carries a `reason` from {@link LINK_FINDING_REASONS} —
135
- * `"not-an-address"`, `"unknown-type"`, `"ambiguous"` (with the claiming
136
- * `packages`), `"no-content-index"`, `"stub"` (with the stub's path) or
137
- * `"unresolved"` — and every one of them is an **error**: the three
138
- * resolvers agree on severity for every class.
134
+ * carries a `reason` from {@link LINK_FINDING_REASONS} — `"not-an-address"`,
135
+ * `"unknown-type"`, `"no-content-index"`, `"stub"` (with the stub's path) or
136
+ * `"unresolved"`, never `"ambiguous"`: every target here expands to one
137
+ * canonical address before anything is looked up, so there is no candidate
138
+ * set for two packages to claim — and every reason is an **error**: the
139
+ * three resolvers agree on severity for every class.
139
140
  */
140
141
  export function auditLinks(index: ReturnType<typeof buildLinkIndex>): {
141
142
  deadAnchors: object[];
@@ -267,10 +267,26 @@ export function runtimeOnlyFields(fields: readonly FieldSpec[]): FieldSpec[];
267
267
  * several do — `weight` is coerced leniently but is still a number, and
268
268
  * `weight: heavy` is an authoring mistake worth reporting where it was made.
269
269
  * Absent means the lint makes no claim about the value.
270
- * @property {string} [ref] - The content type a value addresses by shortcode,
271
- * for the linter's dead-reference check. Only for references to a **note**:
272
- * `bodyLocationCode` names a part inside a being's own body structure, not a
273
- * note, so it declares none.
270
+ * @property {string} [code] - The content type a value names by **Shortcode**,
271
+ * for the linter's dead-reference check. The authored value is one segment —
272
+ * held to {@link module:engine/address-charset.ADDRESS_SEGMENT_PATTERN} —
273
+ * persisted verbatim into the emitted field and resolved at runtime among
274
+ * the items embedded on one actor, where packages do not exist; the linter
275
+ * still forms the `type-shortcode` pair to check it against the index. Only
276
+ * for references to a **note**: `bodyLocationCode` names a part inside a
277
+ * being's own body structure, not a note, so it declares none. Distinct
278
+ * from a `DataFieldSpec`'s `ref` ({@link module:engine/note-vocabulary}),
279
+ * which names an **Address** instead.
280
+ * @property {import("./address-fields.mjs").AddressPosition} [address] - **The
281
+ * authored value is an Address and the emitted one is its Shortcode.** The
282
+ * statement carries the default type an omitted `<type>` segment takes, the
283
+ * set of types the position accepts — two separate things, which coincide at
284
+ * most positions — and where the Addresses sit: the value itself, every
285
+ * element of a list, or every key of a map.
286
+ * {@link module:engine/address-fields.reduceAddressFields} performs the
287
+ * reduction once the block is assembled, so a value authored at the
288
+ * destination is reduced along with one this field read. Distinct from `code`,
289
+ * which is a Shortcode at both ends and resolves nothing.
274
290
  * @property {any|((fm: object) => any)} [value] - For a field with no `name`:
275
291
  * the constant, or a function deriving it from the frontmatter.
276
292
  * @property {boolean} [omitWhenAbsent] - **The key is left out entirely when
@@ -524,12 +540,31 @@ export type FieldSpec = {
524
540
  */
525
541
  kind?: "string" | "number" | "boolean" | "map" | "list" | undefined;
526
542
  /**
527
- * - The content type a value addresses by shortcode,
528
- * for the linter's dead-reference check. Only for references to a **note**:
529
- * `bodyLocationCode` names a part inside a being's own body structure, not a
530
- * note, so it declares none.
543
+ * - The content type a value names by **Shortcode**,
544
+ * for the linter's dead-reference check. The authored value is one segment —
545
+ * held to {@link module :engine/address-charset.ADDRESS_SEGMENT_PATTERN} —
546
+ * persisted verbatim into the emitted field and resolved at runtime among
547
+ * the items embedded on one actor, where packages do not exist; the linter
548
+ * still forms the `type-shortcode` pair to check it against the index. Only
549
+ * for references to a **note**: `bodyLocationCode` names a part inside a
550
+ * being's own body structure, not a note, so it declares none. Distinct
551
+ * from a `DataFieldSpec`'s `ref` ({@link module :engine/note-vocabulary}),
552
+ * which names an **Address** instead.
531
553
  */
532
- ref?: string | undefined;
554
+ code?: string | undefined;
555
+ /**
556
+ * - **The
557
+ * authored value is an Address and the emitted one is its Shortcode.** The
558
+ * statement carries the default type an omitted `<type>` segment takes, the
559
+ * set of types the position accepts — two separate things, which coincide at
560
+ * most positions — and where the Addresses sit: the value itself, every
561
+ * element of a list, or every key of a map.
562
+ * {@link module :engine/address-fields.reduceAddressFields} performs the
563
+ * reduction once the block is assembled, so a value authored at the
564
+ * destination is reduced along with one this field read. Distinct from `code`,
565
+ * which is a Shortcode at both ends and resolves nothing.
566
+ */
567
+ address?: import("./address-fields.mjs").AddressPosition | undefined;
533
568
  /**
534
569
  * - For a field with no `name`:
535
570
  * the constant, or a function deriving it from the frontmatter.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * An authored `parent` with any wikilink brackets and label stripped.
3
3
  *
4
- * The specification types `parent` as a `WikiLink`, and a frontmatter link is
5
- * written as a bare address — but `[[address]]` is what an author reaches for,
4
+ * The specification types `parent` as an `Address`, written bare — but
5
+ * `[[address]]` is what an author reaches for,
6
6
  * and Obsidian wrote that form for years. Accepting both costs one regex and
7
7
  * removes a failure whose message would have to explain the difference.
8
8
  *
@@ -56,14 +56,15 @@ export function collectFolderNotes(notes: Iterable<{
56
56
  absPath: string;
57
57
  }>, pkg: string): FolderNote[];
58
58
  /**
59
- * Index folder notes by every form an author may address one by, and check the
60
- * invariants that make the index sound.
61
- *
62
- * Three keys per folder, and no more: the canonical address, the
63
- * `folder-<shortcode>` short form, and the bare shortcode. They are the
64
- * suffixes of the canonical address the grammar admits — a `packFolder`
65
- * or `parent` field supplies the type itself, so a bare shortcode is a complete
66
- * address there.
59
+ * Index folder notes by their canonical address, and check the invariants
60
+ * that make the index sound.
61
+ *
62
+ * A folder reference is resolved by {@link parseAddress}, taking `folder` as
63
+ * the type an omitted segment fills in — the same suffix ladder every other
64
+ * position climbs, so `cooking`, `folder-cooking` and the full address all
65
+ * expand to the one key this index holds. A `packFolder` or `parent` field
66
+ * supplies no type of its own; the position's default is what makes a bare
67
+ * shortcode a complete address there.
67
68
  *
68
69
  * @param {FolderNote[]} folders - From {@link collectFolderNotes}.
69
70
  * @returns {{byKey: Map<string, FolderNote>, folders: FolderNote[],
@@ -1,97 +1,3 @@
1
- /**
2
- * What one system block accepts beyond the keys every block carries.
3
- *
4
- * @typedef {object} SystemBlockSpec
5
- * @property {readonly string[]} [known] - Keys stated outright.
6
- * @property {boolean} [fieldVocabulary] - Whether the note type's declared field
7
- * names, as the caller's `schemas` state them, are keys of this block.
8
- * @property {Readonly<Record<string, readonly object[]>>} [fields] - Type → this
9
- * system's own declared fields. A type it does not name is a type this system
10
- * says nothing about, and its block is left unchecked on such a note rather
11
- * than reported wholesale.
12
- */
13
- /**
14
- * Every system a configuration says its tree carries.
15
- *
16
- * **Which systems a package ships for is already declared**, in three places
17
- * that answer different questions, so this reads all three rather than asking a
18
- * new one:
19
- *
20
- * - `systems:` declares them without requiring one, which is how a
21
- * package ships for several;
22
- * - a **pack's** `system:` is the same statement made per pack, and it is the
23
- * one some trees make: `harn-ensemble` declares an `actors-sohl` and an
24
- * `actors-hm3` and nothing else about either system. It is already
25
- * authoritative elsewhere — `eligibleFor` fails a note for want of the block
26
- * a pack's `system:` names — so a lint that did not read it would refuse a
27
- * note at compile for a block it never checked;
28
- * - `stats.systemId` is the package-wide answer where there is one, and it has
29
- * already absorbed every way of spelling that: a system package is its own
30
- * system, and a module takes `requiresSystem`, its lone `systems:` entry, or
31
- * its lone system relationship.
32
- *
33
- * A package naming a system in none of them is system-agnostic on purpose — its
34
- * packs are core document types carrying no system data — so it carries no
35
- * system block and naming one would invent it.
36
- *
37
- * @param {object} [config] - A resolved configuration from `defineConfig`.
38
- * @returns {string[]} The system ids, deduplicated, in declared order.
39
- */
40
- export function declaredSystems(config?: object): string[];
41
- /**
42
- * The system blocks a configuration says its tree carries, and what each
43
- * accepts.
44
- *
45
- * The lint checks the blocks its caller names, and for as long as there was one
46
- * system the only caller named none — so every tree took the `sohl:` of
47
- * {@link DEFAULT_SYSTEM_BLOCKS}, a constant, in a module whose whole discipline
48
- * is that it states no vocabulary of its own. That is wrong in both directions
49
- * the moment a second system exists, and the second direction is the worse:
50
- *
51
- * - a package shipping for `hm3` had its `hm3:` block **never looked at**, so
52
- * every key in it was discarded at compile without a word — the silent-drop
53
- * family this check exists to close;
54
- * - and the block that *was* checked was named after a system that package does
55
- * not ship for, so the one finding it could make was about nothing.
56
- *
57
- * **A block's vocabulary has two sources, and a system may have both.**
58
- *
59
- * - The **note schemas** the caller hands in as `schemas`. Those belong to one
60
- * system — the CLI imports `sohl/note-schemas.mjs` — and `schemaSystem` is the
61
- * caller naming which, because only the caller knows. It is the only source
62
- * that reaches a type no item registry declares, which is to say `being`: the
63
- * 2,512 notes `harn-ensemble` is made of, and the reason this is not an
64
- * optional refinement.
65
- * - The system's **own registry**, `itemFieldsBySystem`, keyed by system and
66
- * until now read by nothing. This is what a *second* system's block is held
67
- * to, since the note schemas describe its neighbour.
68
- *
69
- * A system with neither is left out: nothing can state what its block may
70
- * carry, and holding it to an empty vocabulary would report every key in a
71
- * correct tree. **That is a check that does not run**, which is
72
- * indistinguishable from one that passed, so the caller says it out loud —
73
- * {@link declaredSystems} is the other half of that comparison. `harn-ensemble`
74
- * is the tree it names: two systems, and an `itemBuilders` registry for
75
- * neither, so its `hm3:` block is unchecked until it declares one.
76
- *
77
- * An earlier draft of this took the note schemas for a system's vocabulary only
78
- * where the package declared **one** system, on the reasoning that with several
79
- * there is nothing to say which one they describe. There is: the caller, which
80
- * chose them. The guess cost `harn-ensemble` its whole `sohl:` check — two
81
- * systems declared, so the fallback never fired — which is the coverage this
82
- * change exists to widen rather than narrow.
83
- *
84
- * @param {object} [config] - A resolved configuration from `defineConfig`.
85
- * @param {object} [options] - Options.
86
- * @param {string} [options.schemaSystem] - The system whose vocabulary the
87
- * caller's `schemas` state. There are two systems, not an open set, so this is
88
- * one word from the caller rather than a mechanism.
89
- * @returns {Readonly<Record<string, SystemBlockSpec>>} The blocks to check, in
90
- * declared order. A system nothing states the vocabulary of is absent.
91
- */
92
- export function systemBlocksFor(config?: object, { schemaSystem }?: {
93
- schemaSystem?: string | undefined;
94
- }): Readonly<Record<string, SystemBlockSpec>>;
95
1
  /**
96
2
  * Whether a value satisfies a declared {@link FieldSpec.kind}.
97
3
  *
@@ -103,9 +9,10 @@ export function systemBlocksFor(config?: object, { schemaSystem }?: {
103
9
  *
104
10
  * @param {unknown} value - The authored value.
105
11
  * @param {string} kind - The declared kind.
12
+ * @param {object} [context] - Complete Address defaults and known type vocabulary.
106
13
  * @returns {boolean} Whether it is acceptable.
107
14
  */
108
- export function matchesKind(value: unknown, kind: string): boolean;
15
+ export function matchesKind(value: unknown, kind: string, context?: object): boolean;
109
16
  /**
110
17
  * Check one note against its type's schema.
111
18
  *
@@ -113,7 +20,7 @@ export function matchesKind(value: unknown, kind: string): boolean;
113
20
  * @param {object} opts
114
21
  * @param {Record<string, readonly object[]>} opts.schemas - Type → declaration.
115
22
  * @param {object} [opts.index] - The link index, for the reference check, which
116
- * runs through its `referenceHit`. Its absence skips that check rather than
23
+ * runs through its `shortcodeHit`. Its absence skips that check rather than
117
24
  * reporting every reference as dead.
118
25
  * @param {Record<string, object>} [opts.vocabulary] - Type → the closed regions
119
26
  * it declares, as `engine/note-vocabulary.mjs` states them. Supplied
@@ -132,13 +39,14 @@ export function matchesKind(value: unknown, kind: string): boolean;
132
39
  * for the same reason `schemas` is — a build derives them from its
133
40
  * configuration through {@link systemBlocksFor}, and this module states no
134
41
  * system name of its own. See {@link DEFAULT_SYSTEM_BLOCKS} for the fallback.
42
+ * @param {object} [opts.addressContext] - Builder package and Address type vocabulary.
135
43
  * @param {readonly string[]} [opts.packs] - The pack names this package
136
44
  * declares, for a `data:` field whose map is keyed by pack. Supplied by the
137
45
  * caller like `schemas` and `vocabulary`, and absent it no claim is made
138
46
  * about those keys.
139
47
  * @returns {object[]} Findings, each with a locator where one is obtainable.
140
48
  */
141
- export function lintNote(note: object, { schemas, index, vocabulary, packs, emittedArt, systems }: {
49
+ export function lintNote(note: object, { schemas, index, vocabulary, packs, emittedArt, addressContext, systems, }: {
142
50
  schemas: Record<string, readonly object[]>;
143
51
  index?: object | undefined;
144
52
  vocabulary?: Record<string, object> | undefined;
@@ -147,6 +55,7 @@ export function lintNote(note: object, { schemas, index, vocabulary, packs, emit
147
55
  art: readonly string[];
148
56
  } | null) | undefined;
149
57
  systems?: Readonly<Record<string, SystemBlockSpec>> | undefined;
58
+ addressContext?: object | undefined;
150
59
  packs?: readonly string[] | undefined;
151
60
  }): object[];
152
61
  /**
@@ -280,3 +189,4 @@ export type SystemBlockSpec = {
280
189
  */
281
190
  fields?: Readonly<Record<string, readonly object[]>> | undefined;
282
191
  };
192
+ export { declaredSystems, systemBlocksFor } from "./system-vocabulary.mjs";
@@ -32,16 +32,31 @@ export function sohlField(fm: any, key: any, defaultValue?: undefined): any;
32
32
  * has to be told both. It is the same split `FieldSpec.name`/`legacyKey` makes,
33
33
  * for the same reason — one name cannot key two positions.
34
34
  *
35
+ * `shared` is the field's declared shared source, and it completes the order the
36
+ * three other positions are two thirds of. A field whose source is a path into
37
+ * `data:` is authored there by the specification's own mapping tables, and a
38
+ * reader that knew the destination, the block and the top level alone skipped
39
+ * straight past the container: every authored value read as unset, and the
40
+ * document shipped the field's default with nothing said. It sits **between**
41
+ * the block and the top level, exactly where
42
+ * {@link module:engine/system-block.resolveFieldValue} puts it — the block is the
43
+ * position a tree still writes and wins while it does, and the bare top-level
44
+ * key is the spelling `data:` gathered the fact off.
45
+ *
35
46
  * @param {object} fm - The note's frontmatter.
36
47
  * @param {string} to - The key at the destination, dotted for a nested one.
37
48
  * @param {any} [defaultValue] - What an unauthored field reads as.
38
49
  * @param {object} [options] - Options.
39
50
  * @param {string} [options.legacyKey] - The key the block still carries, when
40
51
  * it is not spelled `to`. Defaults to `to`.
52
+ * @param {string} [options.shared] - The field's declared shared source, when it
53
+ * is a path into a container (`data.relations`). Omitted by a field whose
54
+ * shared position is the note's top level, which `sohlField` already reads.
41
55
  * @returns {any} The value.
42
56
  */
43
- export function sohlSystemField(fm: object, to: string, defaultValue?: any, { legacyKey }?: {
57
+ export function sohlSystemField(fm: object, to: string, defaultValue?: any, { legacyKey, shared }?: {
44
58
  legacyKey?: string | undefined;
59
+ shared?: string | undefined;
45
60
  }): any;
46
61
  /**
47
62
  * Resolve the `charges` block shared by Mystery and Mystical Ability items.
@@ -10,7 +10,7 @@
10
10
  * `{ frontmatter: null, body: "", description: "" }` with a warn log, and no
11
11
  * position: there is no body to have one.
12
12
  */
13
- export function parseMarkdownFile(filePath: any): {
13
+ export function parseMarkdownFile(filePath: any, { addressContext }?: {}): {
14
14
  frontmatter: null;
15
15
  body: string;
16
16
  description: string;
@@ -85,7 +85,7 @@ export function assertSuppliedCorpus(records: readonly object[] | undefined, who
85
85
  * @throws {Error} When `skipDirectories` is not stated — see
86
86
  * {@link assertStatedScope}.
87
87
  */
88
- export function walkMarkdownTree(rootDir: string, { skipDirectories }?: {
88
+ export function walkMarkdownTree(rootDir: string, { skipDirectories, addressContext }?: {
89
89
  skipDirectories: readonly string[];
90
90
  }): Generator<{
91
91
  file: string & NonSharedBuffer;
@@ -5,6 +5,12 @@
5
5
  * `<type>.<field>` wins over the bare name, so a spelling two types use for two
6
6
  * quantities can be said differently on each without splitting the overlay.
7
7
  *
8
+ * **Keyed by the key an author writes, not by the path a declaration names.**
9
+ * One fact has two declarations — the note vocabulary's `weight` and an item
10
+ * field's `data.weight` — and the overlay says what a *reader* is shown, which
11
+ * is one word either way. So the container is dropped before the lookup, and
12
+ * `GEAR_UNITS` answers for both sides with one entry.
13
+ *
8
14
  * @param {Readonly<Record<string, object>>} presentation - The overlay.
9
15
  * @param {string|undefined} type - The note's type.
10
16
  * @param {string} name - The field's declared name.