@heroiclands/package-build 20.3.0 → 20.4.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 (178) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/CONTENT.md +111 -92
  3. package/README.md +2 -9
  4. package/bin/content-build.mjs +70 -61
  5. package/bin/package-build.mjs +2 -2
  6. package/config.mjs +6 -8
  7. package/content-config.mjs +187 -64
  8. package/docs/content-format.md +57 -68
  9. package/e2e.mjs +4 -4
  10. package/engine/actor-compiler.mjs +36 -36
  11. package/engine/address-charset.mjs +6 -6
  12. package/engine/address-diff.mjs +12 -11
  13. package/engine/anchored-sections.mjs +1 -1
  14. package/engine/anchors.mjs +1 -1
  15. package/engine/base-compiler.mjs +36 -38
  16. package/engine/bundle-notes.mjs +4 -4
  17. package/engine/bundles.mjs +8 -8
  18. package/engine/code-fences.mjs +3 -3
  19. package/engine/compendiums.mjs +8 -8
  20. package/engine/compile-corpus.mjs +4 -5
  21. package/engine/content-address.mjs +15 -15
  22. package/engine/content-charset.mjs +1 -1
  23. package/engine/content-format-check.mjs +24 -25
  24. package/engine/content-format.mjs +8 -8
  25. package/engine/content-html.mjs +202 -0
  26. package/engine/content-icons.mjs +225 -98
  27. package/engine/content-index.mjs +17 -17
  28. package/engine/content-links.mjs +30 -30
  29. package/engine/content-lint.mjs +20 -24
  30. package/engine/content-package.mjs +7 -9
  31. package/engine/content-slug.mjs +2 -2
  32. package/engine/content-tables.mjs +7 -8
  33. package/engine/content-tree.mjs +1 -1
  34. package/engine/derived-fields.mjs +174 -0
  35. package/engine/diagnostics.mjs +3 -4
  36. package/engine/document-subtypes.mjs +6 -7
  37. package/engine/field-reference.mjs +8 -8
  38. package/engine/field-spec.mjs +29 -30
  39. package/engine/folder-notes.mjs +14 -14
  40. package/engine/foreign-catalog.mjs +8 -9
  41. package/engine/foundry-entries.mjs +19 -20
  42. package/engine/frontmatter-lint.mjs +64 -69
  43. package/engine/frontmatter.mjs +11 -11
  44. package/engine/generate.mjs +39 -39
  45. package/engine/helpers.mjs +63 -54
  46. package/engine/homepage.mjs +20 -22
  47. package/engine/ids.mjs +15 -15
  48. package/engine/index-records.mjs +4 -4
  49. package/engine/index.mjs +14 -8
  50. package/engine/item-compiler.mjs +54 -29
  51. package/engine/item-docs.mjs +11 -13
  52. package/engine/item-registry.mjs +13 -13
  53. package/engine/journals.mjs +15 -15
  54. package/engine/macros.mjs +3 -3
  55. package/engine/map-notes.mjs +6 -6
  56. package/engine/metadata-index.mjs +5 -5
  57. package/engine/note-claims.mjs +36 -34
  58. package/engine/note-ids.mjs +5 -5
  59. package/engine/note-package.mjs +4 -4
  60. package/engine/note-renames.mjs +3 -3
  61. package/engine/note-schemas.mjs +3 -3
  62. package/engine/note-vocabulary.mjs +27 -26
  63. package/engine/pack-config.mjs +13 -13
  64. package/engine/pack-router.mjs +0 -0
  65. package/engine/prose-config.mjs +9 -9
  66. package/engine/prose-lint.mjs +12 -13
  67. package/engine/region-events.mjs +2 -2
  68. package/engine/retired-fields.mjs +24 -22
  69. package/engine/runtime-only-fields.mjs +1 -1
  70. package/engine/scene-levels.mjs +1 -1
  71. package/engine/scenes.mjs +19 -20
  72. package/engine/schema-check.mjs +20 -20
  73. package/engine/schema-extract.mjs +1 -1
  74. package/engine/site-build.mjs +45 -47
  75. package/engine/site-index.mjs +17 -18
  76. package/engine/sql-tables.mjs +21 -13
  77. package/engine/subtype-registry.mjs +5 -4
  78. package/engine/system-block.mjs +18 -19
  79. package/engine/systems.mjs +2 -2
  80. package/engine/web-wikilinks.mjs +35 -35
  81. package/engine/wikilink-syntax.mjs +16 -16
  82. package/engine/wikilinks.mjs +39 -39
  83. package/hm3/actors.mjs +22 -22
  84. package/hm3/document-subtypes.mjs +5 -5
  85. package/hm3/index.mjs +2 -2
  86. package/hm3/item-builders.mjs +1 -1
  87. package/hm3/item-fields.mjs +3 -3
  88. package/hm3/items.mjs +20 -8
  89. package/hm3/template-priority.mjs +2 -2
  90. package/lang.mjs +3 -3
  91. package/manifest.mjs +17 -20
  92. package/package.json +1 -2
  93. package/release.mjs +3 -3
  94. package/sohl/actors.mjs +28 -28
  95. package/sohl/affiliation-standings.mjs +2 -2
  96. package/sohl/being-info.mjs +5 -5
  97. package/sohl/default-item-art.mjs +5 -5
  98. package/sohl/document-subtypes.mjs +5 -5
  99. package/sohl/index.mjs +3 -3
  100. package/sohl/item-builders.mjs +7 -7
  101. package/sohl/item-fields.mjs +8 -9
  102. package/sohl/items.mjs +20 -6
  103. package/sohl/kb-passes.mjs +5 -5
  104. package/sohl/note-schemas.mjs +7 -7
  105. package/sohl/skill-base.mjs +1 -1
  106. package/types/content-config.d.mts +25 -24
  107. package/types/e2e.d.mts +3 -3
  108. package/types/engine/actor-compiler.d.mts +14 -13
  109. package/types/engine/address-charset.d.mts +6 -6
  110. package/types/engine/address-diff.d.mts +3 -3
  111. package/types/engine/base-compiler.d.mts +17 -19
  112. package/types/engine/bundle-notes.d.mts +3 -3
  113. package/types/engine/bundles.d.mts +1 -1
  114. package/types/engine/code-fences.d.mts +3 -3
  115. package/types/engine/compendiums.d.mts +3 -3
  116. package/types/engine/compile-corpus.d.mts +1 -1
  117. package/types/engine/content-address.d.mts +11 -11
  118. package/types/engine/content-format-check.d.mts +6 -6
  119. package/types/engine/content-format.d.mts +2 -2
  120. package/types/engine/content-html.d.mts +78 -0
  121. package/types/engine/content-icons.d.mts +150 -62
  122. package/types/engine/content-index.d.mts +2 -2
  123. package/types/engine/content-links.d.mts +8 -8
  124. package/types/engine/content-lint.d.mts +2 -2
  125. package/types/engine/content-package.d.mts +6 -8
  126. package/types/engine/derived-fields.d.mts +101 -0
  127. package/types/engine/diagnostics.d.mts +2 -2
  128. package/types/engine/document-subtypes.d.mts +3 -3
  129. package/types/engine/field-spec.d.mts +37 -39
  130. package/types/engine/folder-notes.d.mts +4 -5
  131. package/types/engine/foreign-catalog.d.mts +3 -3
  132. package/types/engine/foundry-entries.d.mts +6 -7
  133. package/types/engine/frontmatter-lint.d.mts +7 -7
  134. package/types/engine/frontmatter.d.mts +7 -7
  135. package/types/engine/generate.d.mts +7 -7
  136. package/types/engine/helpers.d.mts +25 -27
  137. package/types/engine/homepage.d.mts +16 -18
  138. package/types/engine/ids.d.mts +13 -13
  139. package/types/engine/index-records.d.mts +3 -3
  140. package/types/engine/index.d.mts +2 -0
  141. package/types/engine/item-compiler.d.mts +21 -5
  142. package/types/engine/item-docs.d.mts +2 -2
  143. package/types/engine/item-registry.d.mts +6 -6
  144. package/types/engine/journals.d.mts +3 -3
  145. package/types/engine/map-notes.d.mts +2 -2
  146. package/types/engine/metadata-index.d.mts +4 -4
  147. package/types/engine/note-claims.d.mts +16 -15
  148. package/types/engine/note-ids.d.mts +4 -4
  149. package/types/engine/note-package.d.mts +1 -1
  150. package/types/engine/note-renames.d.mts +3 -3
  151. package/types/engine/note-vocabulary.d.mts +9 -8
  152. package/types/engine/pack-config.d.mts +3 -3
  153. package/types/engine/pack-router.d.mts +1 -1
  154. package/types/engine/prose-config.d.mts +9 -9
  155. package/types/engine/prose-lint.d.mts +3 -4
  156. package/types/engine/region-events.d.mts +2 -2
  157. package/types/engine/retired-fields.d.mts +10 -9
  158. package/types/engine/schema-check.d.mts +11 -11
  159. package/types/engine/site-build.d.mts +18 -19
  160. package/types/engine/site-index.d.mts +3 -4
  161. package/types/engine/sql-tables.d.mts +11 -5
  162. package/types/engine/subtype-registry.d.mts +3 -3
  163. package/types/engine/system-block.d.mts +3 -3
  164. package/types/engine/web-wikilinks.d.mts +7 -7
  165. package/types/engine/wikilink-syntax.d.mts +17 -17
  166. package/types/engine/wikilinks.d.mts +13 -12
  167. package/types/hm3/actors.d.mts +1 -1
  168. package/types/hm3/document-subtypes.d.mts +1 -1
  169. package/types/hm3/items.d.mts +1 -2
  170. package/types/hm3/template-priority.d.mts +1 -1
  171. package/types/manifest.d.mts +8 -8
  172. package/types/sohl/actors.d.mts +2 -2
  173. package/types/sohl/affiliation-standings.d.mts +2 -2
  174. package/types/sohl/being-info.d.mts +2 -2
  175. package/types/sohl/document-subtypes.d.mts +1 -1
  176. package/types/sohl/items.d.mts +1 -2
  177. package/types/sohl/note-schemas.d.mts +1 -1
  178. package/MIGRATING.md +0 -608
@@ -0,0 +1,174 @@
1
+ /*
2
+ * This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
3
+ * Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
4
+ *
5
+ * This work is licensed under the GNU General Public License v3.0 (GPLv3).
6
+ * You may copy, modify, and distribute it under the terms of that license.
7
+ *
8
+ * For full terms, see the LICENSE.md file in the project root or visit:
9
+ * https://www.gnu.org/licenses/gpl-3.0.html
10
+ *
11
+ * SPDX-License-Identifier: GPL-3.0-or-later
12
+ */
13
+
14
+ /**
15
+ * `system` keys a note may **never** author, because the *compiler* writes them
16
+ * from the note itself.
17
+ *
18
+ * The third of three refusals that read alike and state different facts:
19
+ *
20
+ * | module | refuses a field | because |
21
+ * | --- | --- | --- |
22
+ * | `retired-fields.mjs` | a note may no longer declare | it has been withdrawn |
23
+ * | `runtime-only-fields.mjs` | a note never could | the *document* writes it in play |
24
+ * | this one | a note never could | the *compiler* writes it, from the note |
25
+ *
26
+ * The distinction between the last two is where the value comes from, and it
27
+ * decides what an author is told. A runtime-only field has no compile-time
28
+ * value at all, so the fix is to delete the key. A derived one **does** have a
29
+ * compile-time value — it is already being written, from somewhere else in the
30
+ * same note — so the fix is to move the content to wherever the compiler reads
31
+ * it from, and the message has to say where that is.
32
+ *
33
+ * ## Why an authored value cannot be the source of anything
34
+ *
35
+ * The compiler writes the key unconditionally. An authored one is therefore at
36
+ * best redundant, and at worst the wrong type in a shipped document: SoHL's
37
+ * `docHtml` holds a `@UUID` pointing at the JournalEntry a note's prose
38
+ * compiled into, so a note writing prose there ships a string where every
39
+ * reader expects a pointer, with nothing to report it.
40
+ *
41
+ * Either way the text is not reaching a reader the way its author intended. It
42
+ * was written as a description, and the field it was written into is not where
43
+ * descriptions live — the note's own body is.
44
+ *
45
+ * ## Declared by the compiler, not named here
46
+ *
47
+ * A derived key is a fact about a *pass*, not about a schema: it exists because
48
+ * that compiler chose to write it. So the list is a static on the compiler and
49
+ * this module knows no key names, which is what lets a second system declare
50
+ * its own — HM3 derives `description` from an anchored prose section exactly as
51
+ * SoHL derives `docHtml` from the body.
52
+ *
53
+ * @module
54
+ */
55
+
56
+ import { getFrontmatter } from "./frontmatter.mjs";
57
+ import { locateFrontmatterKey } from "./retired-fields.mjs";
58
+ import { SYSTEM_DATA_KEY, systemData } from "./system-block.mjs";
59
+
60
+ /**
61
+ * What a note authoring a derived key is told, in one place.
62
+ *
63
+ * Shared by every caller that can meet one, so an author meets the same
64
+ * sentence whichever runs first. It names the correction rather than only the
65
+ * fault, because "delete this" is not the fix here: the text is wanted, and
66
+ * there is somewhere it belongs.
67
+ *
68
+ * @param {string} key - The **whole key the note wrote**, from the region it
69
+ * sits in down to the field: `sohl.system.docHtml`. Composed by the caller,
70
+ * because only it knows where it found the value.
71
+ * @param {string} from - Where the compiler reads the value from, completing
72
+ * "the compiler writes it from …". The declaration carries it.
73
+ * @param {string} [file] - The note's path, named in the message. Omit it where
74
+ * the caller emits through a diagnostic, whose locator already starts the
75
+ * line — repeating it prints the path twice.
76
+ * @returns {string} The message, unpunctuated at the end as a finding is.
77
+ */
78
+ export function derivedMessage(key, from, file) {
79
+ return (
80
+ `\`${key}:\` is written by the compiler, not authored` +
81
+ (file ? ` — ${file}` : "") +
82
+ `. It is derived from ${from}, and the compiler writes it ` +
83
+ `unconditionally, so an authored value is overwritten or ships the ` +
84
+ `wrong type. Move the text to ${from} and delete the key`
85
+ );
86
+ }
87
+
88
+ /**
89
+ * The derived keys a note actually writes, in declaration order.
90
+ *
91
+ * **Presence is the whole test**, as it is for a retired or a runtime-only
92
+ * field: an authored empty string is as much a claim on the key as prose is,
93
+ * and it is the same belief the message exists to correct. So the question is
94
+ * whether the path resolves to anything at all, never whether the value is a
95
+ * usable one.
96
+ *
97
+ * @param {object|null|undefined} fm - Parsed frontmatter.
98
+ * @param {readonly {key: string, from: string}[]} [keys] - What the compiler
99
+ * derives. A pass that derives nothing passes.
100
+ * @param {object} options - Options.
101
+ * @param {string} options.block - The system block to look in.
102
+ * @returns {{key: string, from: string}[]} The offending declarations.
103
+ */
104
+ export function authoredDerivedKeys(fm, keys, { block } = {}) {
105
+ if (!fm || typeof fm !== "object" || !block) return [];
106
+ return derivedIn(systemData(fm, block), keys);
107
+ }
108
+
109
+ /**
110
+ * The same question asked of a `system` block directly.
111
+ *
112
+ * A note's own block is reached through {@link authoredDerivedKeys}, but it is
113
+ * not the only place an author writes one: an actor note's `items:` entries
114
+ * carry a `system:` overlay that is deep-merged onto the template verbatim,
115
+ * with no field declaration in the path at all. That overlay is a `system`
116
+ * block by every meaning except where it sits, and a `docHtml` written there
117
+ * ships exactly as one written on the item's own note.
118
+ *
119
+ * @param {Record<string, unknown>|null|undefined} data - The authored `system`
120
+ * data.
121
+ * @param {readonly {key: string, from: string}[]} [keys] - What the compiler
122
+ * derives.
123
+ * @returns {{key: string, from: string}[]} The offending declarations.
124
+ */
125
+ export function derivedIn(data, keys) {
126
+ if (!data || typeof data !== "object") return [];
127
+ return (keys ?? []).filter(
128
+ (entry) =>
129
+ typeof entry?.key === "string" &&
130
+ entry.key !== "" &&
131
+ getFrontmatter(data, entry.key, undefined) !== undefined,
132
+ );
133
+ }
134
+
135
+ /**
136
+ * Refuse a note that authors any key its compiler derives.
137
+ *
138
+ * Refused rather than reported, for the reason its two siblings are: the note
139
+ * is not compiled, so nothing it would have emitted reaches a pack, and each
140
+ * caller counts the refused note and emits a located diagnostic — a refusal is
141
+ * never a silent skip.
142
+ *
143
+ * The **first** offending key is thrown on. A note authoring two of them is one
144
+ * mistake with one fix, and the build stops on this note either way.
145
+ *
146
+ * @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
147
+ * could not be parsed.
148
+ * @param {readonly {key: string, from: string}[]} [keys] - What the compiler
149
+ * derives.
150
+ * @param {object} options - Options.
151
+ * @param {string} options.block - The system block to look in.
152
+ * @param {string} [options.file] - The note's path, named in the message. Omit
153
+ * it where the caller emits through a diagnostic, which puts the locator at
154
+ * the start of the line already.
155
+ * @param {string} [options.absPath] - The note's file on disk, read only on the
156
+ * failing path to locate the offending line and column. The position rides on
157
+ * the thrown error as `position`, for a caller that emits a diagnostic.
158
+ * @returns {void}
159
+ * @throws {Error} When the note authors one.
160
+ */
161
+ export function assertNoDerivedFields(fm, keys, { block, file, absPath } = {}) {
162
+ const [entry] = authoredDerivedKeys(fm, keys, { block });
163
+ if (!entry) return;
164
+
165
+ const wrote = `${block}.${SYSTEM_DATA_KEY}.${entry.key}`;
166
+ const err = new Error(`${derivedMessage(wrote, entry.from, file)}.`);
167
+ // The **leaf** of the key, which is how the note writes it: a locator
168
+ // handed a dotted path would find nothing. Deliberately not anchored at
169
+ // column 1 — the key lives two levels in, under `<block>.system`.
170
+ const leaf = entry.key.split(".").pop();
171
+ const position = locateFrontmatterKey(absPath, /** @type {string} */ (leaf));
172
+ if (position) err.position = position;
173
+ throw err;
174
+ }
@@ -18,8 +18,7 @@
18
18
  * in "The Capital Nome"` — has told the author what is wrong and not where it
19
19
  * is. A name is not an address: finding it means searching the tree for a note
20
20
  * whose `name.full` matches and then searching that file for the link, and
21
- * four identical warnings on one note are indistinguishable from one another
22
- * (#17).
21
+ * four identical warnings on one note are indistinguishable from one another.
23
22
  *
24
23
  * So every diagnostic about a note is emitted in the form every C-family
25
24
  * compiler, `tsc` and ESLint already use:
@@ -185,7 +184,7 @@ export function positionInBody(body, offset, { bodyLine = 1, bodyColumn = 1, lin
185
184
  * same name answers when no top-level one is present — which is usually what a
186
185
  * reader wants, the key being unique in nearly every note. Pass `topLevel` where
187
186
  * it is not: `aliases` is both a retired top-level field and a **permitted**
188
- * `name.aliases` (#180), and a finding about the first must never open on the
187
+ * `name.aliases`, and a finding about the first must never open on the
189
188
  * second, which would tell an author to delete a field they are allowed to
190
189
  * write.
191
190
  *
@@ -387,7 +386,7 @@ export function yamlKeyPath(field) {
387
386
  * the same name is legal in two regions — `data.weight` and a top-level
388
387
  * `weight` are different keys, and a search finds whichever comes first.
389
388
  *
390
- * So this is the located form for the closed regions (#128). It reuses
389
+ * So this is the located form for the closed regions. It reuses
391
390
  * {@link positionOfYamlPath} rather than repeating its parse, which is what
392
391
  * keeps a finding about `data.weight` pointing at `data.weight`.
393
392
  *
@@ -13,8 +13,7 @@
13
13
 
14
14
  /**
15
15
  * **The note-type → document-subtype map** — the mechanism that stops a build
16
- * inferring a Foundry document's subtype from the markdown note's `type`
17
- * (#79).
16
+ * inferring a Foundry document's subtype from the markdown note's `type`.
18
17
  *
19
18
  * A note's `type` and the subtype of the document it compiles into are two
20
19
  * vocabularies, and until now they were the same identifier for one reason
@@ -26,7 +25,7 @@
26
25
  * risk with **one** system, not merely with two.
27
26
  *
28
27
  * **The mechanism is here; the declaration is the system's.** That is the
29
- * `engine/` ÷ `sohl/` line everywhere else in this package (#36): note-format
28
+ * `engine/` ÷ `sohl/` line everywhere else in this package: note-format
30
29
  * knowledge here, game-system knowledge there. `sohl/document-subtypes.mjs`
31
30
  * declares SoHL's own map, *including its identity rows* — the coincidence of
32
31
  * names may never stand in for a mapping, so `skill` → `skill` is written out
@@ -59,7 +58,7 @@
59
58
  * *compiled documents*, which carry only the subtype. {@link referencedSubtype}
60
59
  * is the translation for that side, and the reason it is separate from
61
60
  * {@link documentSubtype} is that a reference has no frontmatter of its own to
62
- * read a discriminator from (#140).
61
+ * read a discriminator from.
63
62
  *
64
63
  * @module
65
64
  */
@@ -207,7 +206,7 @@ function frozenRow(system, noteType, row) {
207
206
  * {@link mapsNoteType}, {@link noteTypesFor}'s consumers and
208
207
  * {@link systemOf} all arrive here. A note still spelling `armorgear` finds the
209
208
  * `armor` row, compiles into the `armorgear` document it always did, and keeps
210
- * the address it publishes at; only the *report* tells it to move (#78).
209
+ * the address it publishes at; only the *report* tells it to move.
211
210
  *
212
211
  * @param {DocumentSubtypeMap} map - The system's map.
213
212
  * @param {string|undefined} noteType - The note's declared `type`.
@@ -317,7 +316,7 @@ export function documentSubtype(map, noteType, fm, { file, absPath } = {}) {
317
316
  */
318
317
 
319
318
  /**
320
- * The document subtype a `(type, shortcode)` **reference** addresses (#140).
319
+ * The document subtype a `(type, shortcode)` **reference** addresses.
321
320
  *
322
321
  * A being's frontmatter names each embedded item by the *note's* type — the
323
322
  * vocabulary an author writes — while the predefined items it resolves against
@@ -354,7 +353,7 @@ export function documentSubtype(map, noteType, fm, { file, absPath } = {}) {
354
353
  * retirement exists to stop.
355
354
  *
356
355
  * A **renamed** spelling is the opposite case and resolves normally, through
357
- * the same {@link currentType} every other lookup goes through (#78). It has to
356
+ * the same {@link currentType} every other lookup goes through. It has to
358
357
  * be this side as much as the note's own `type:`: the overwhelming majority of
359
358
  * the 31,000 occurrences of the old names are `(type, shortcode)` references
360
359
  * inside a being's `items:` list, so a window that resolved notes but not
@@ -20,7 +20,7 @@
20
20
  * types, each with a shape, a default and a requiredness, is exactly the kind of
21
21
  * prose that drifts silently from the code it describes. The declarations
22
22
  * (`fields` on each `itemBuilders` entry) already *are* that table, so the page
23
- * is rendered from them rather than transcribed (#22).
23
+ * is rendered from them rather than transcribed.
24
24
  *
25
25
  * **It ships here, not in the consumer.** Any repository can define an item
26
26
  * type of its own, so "what frontmatter does this type accept?" is a question
@@ -104,7 +104,7 @@ function fieldTable(fields) {
104
104
  `\`${field.name}\``,
105
105
  cell(field.shape ?? "as authored"),
106
106
  field.required ? "**yes**" : "no",
107
- // Three different answers, and the third is not a value (#329). A
107
+ // Three different answers, and the third is not a value. A
108
108
  // required field has no default because omitting it fails the
109
109
  // build; an `omitWhenAbsent` field has none because omitting it
110
110
  // omits the *key*, leaving the data model to answer. Rendering
@@ -128,7 +128,7 @@ function fieldTable(fields) {
128
128
  * the note level the fallback is off, and an author has no way to tell from the
129
129
  * table — the field is there, the value is written, and the document ships the
130
130
  * default. So the reason each such field declares is rendered beside its table
131
- * rather than left in the source (#218).
131
+ * rather than left in the source.
132
132
  *
133
133
  * Below the table, not inside it: the reason is a sentence or two, and
134
134
  * {@link padTable} pads every column to its widest cell, so a cell holding it
@@ -148,7 +148,7 @@ function sharedExemptions(fields) {
148
148
  }
149
149
 
150
150
  /**
151
- * The fields of one type that a note may **never** write (#330).
151
+ * The fields of one type that a note may **never** write.
152
152
  *
153
153
  * A type's table lists what an author writes, and says nothing about the rest
154
154
  * of its schema — which is right for a constant or a derived value, since
@@ -208,15 +208,15 @@ function workedExample(type, fields) {
208
208
  `type: ${type}`,
209
209
  "shortcode: xmpl",
210
210
  // No `package:`. A note's package is the repository's configured
211
- // `contentPackage`, and declaring the field is a build error (#56)
211
+ // `contentPackage`, and declaring the field is a build error —
212
212
  // this example is the smallest note that compiles.
213
213
  //
214
214
  // No `id:` either, for the same reason it is not shown optional-first:
215
- // a note's document `_id` derives from its canonical address (#270,
216
- // #277), and the authored field is the escape hatch for keeping a
215
+ // a note's document `_id` derives from its canonical address, and the
216
+ // authored field is the escape hatch for keeping a
217
217
  // document's identity across a shortcode rename, not part of the
218
218
  // envelope. This block is the one an author copies as a template, so
219
- // showing the field taught every note in the tree to write it (#314).
219
+ // showing the field taught every note in the tree to write it.
220
220
  "sohl:",
221
221
  " templatePriority: null",
222
222
  ];
@@ -17,8 +17,8 @@
17
17
  * **The declaration is the builder, not a description of one.** A builder used
18
18
  * to be an opaque function: the mapping from a note's `sohl:` frontmatter to
19
19
  * the emitted `system` block existed only inside its body, so nothing could
20
- * read it — not a documentation generator, not a validator, not a person
21
- * (#22). The obvious repair, a table written *beside* the function, buys very
20
+ * read it — not a documentation generator, not a validator, not a person.
21
+ * The obvious repair, a table written *beside* the function, buys very
22
22
  * little: two statements of one rule drift, and nothing notices.
23
23
  *
24
24
  * So the table is the only statement. {@link buildFromFields} turns a field
@@ -50,19 +50,19 @@ export { legacyKeyOf, retiredTopLevelKey, setPath };
50
50
  /**
51
51
  * @typedef {object} FieldSpec
52
52
  * @property {string} to - Dotted path in the emitted `system` block — and,
53
- * since #58, the path a note authors the field at directly:
53
+ * the path a note authors the field at directly:
54
54
  * `<system>.system.<to>`.
55
55
  * @property {string} [name] - The **shared, top-level property this field draws
56
56
  * from** when the note authors no value at `<system>.system.<to>`. Dotted for
57
57
  * a path into a shared container (`data.portrait`), which is now the ordinary
58
- * case: `data:` (#128) puts every type-specific fact under one.
58
+ * case: `data:` puts every type-specific fact under one.
59
59
  *
60
- * It used to mean "frontmatter key under `sohl:`", and that reading is the
60
+ * Read as "frontmatter key under `sohl:`", that meaning is the
61
61
  * degenerate case where the shared source and the system destination happen
62
62
  * to share a name. They constantly do not — one shared `data.portrait` feeds
63
63
  * `sohl.system.portrait` *and* `hm3.system.bioImage` — so the source is
64
- * declared rather than matched by spelling (#58). The in-block position is
65
- * still read, second, until #126 moves the corpus off it — keyed on
64
+ * declared rather than matched by spelling. The in-block position is
65
+ * still read, second, until the corpus moves off it — keyed on
66
66
  * `legacyKey` where the two spellings differ.
67
67
  *
68
68
  * Absent means the value is not authored at all — see `value`.
@@ -70,8 +70,8 @@ export { legacyKeyOf, retiredTopLevelKey, setPath };
70
70
  * the system block** — the second position of the resolution order — when
71
71
  * that is not `name`. Absent, the position is keyed on `name`.
72
72
  *
73
- * The two were one property until #305, which held only while a field's
74
- * shared source and its in-block key were the same word. `data:` (#128) ended
73
+ * As one property the two would hold only while a field's
74
+ * shared source and its in-block key were the same word. `data:` ended
75
75
  * that: a shared source is a path into a container, so `data.species` and
76
76
  * `species` name two different places and no single value reached both.
77
77
  * `name: "species"` could not see `data.species`; `name: "data.species"`
@@ -87,7 +87,7 @@ export { legacyKeyOf, retiredTopLevelKey, setPath };
87
87
  * @property {string} [topLevelMeans] - **What the note's top-level key of this
88
88
  * name means instead** — declared only where it means something else, and
89
89
  * stating it removes the shared top-level position from this field's
90
- * resolution order (#218).
90
+ * resolution order.
91
91
  *
92
92
  * A field's `name` doubles as its identity and as the shared property it
93
93
  * draws from, which is right wherever the two levels state the same quantity
@@ -114,7 +114,7 @@ export { legacyKeyOf, retiredTopLevelKey, setPath };
114
114
  * if the two positions hold unrelated quantities then the *in-block* position
115
115
  * is not the note-level field either, so a check about the note-level field
116
116
  * reads past it. `engine/frontmatter-lint.mjs` resolves that through
117
- * `collidingBlockKeys`. Read for the emitted field alone until #312, which is
117
+ * `collidingBlockKeys`. Reading it for the emitted field alone is
118
118
  * how an affiliation's office style came to answer for its page heading.
119
119
  * @property {string} [shape] - Human-readable shape, for documentation. Comes
120
120
  * paired with `read` from one of the coercion constants below.
@@ -130,10 +130,9 @@ export { legacyKeyOf, retiredTopLevelKey, setPath };
130
130
  * third step of {@link module:engine/system-block.resolveFieldValue}'s order,
131
131
  * so the value resolves whichever region carries it. What it tells the
132
132
  * author-facing surfaces is which of the two is the field's *home*, so a
133
- * message about it names `img` rather than sending an author to `sohl.img`
134
- * (#142).
133
+ * message about it names `img` rather than sending an author to `sohl.img`.
135
134
  * @property {"string"|"number"|"boolean"|"list"|"map"} [kind] - The value's
136
- * shape, for the frontmatter linter (#19). Distinct from `shape`, which is
135
+ * shape, for the frontmatter linter. Distinct from `shape`, which is
137
136
  * prose for a reader, and from `read`, which is what the compiler does: a
138
137
  * field may declare `kind` without changing a byte of what it emits, and
139
138
  * several do — `weight` is coerced leniently but is still a number, and
@@ -146,7 +145,7 @@ export { legacyKeyOf, retiredTopLevelKey, setPath };
146
145
  * @property {any|((fm: object) => any)} [value] - For a field with no `name`:
147
146
  * the constant, or a function deriving it from the frontmatter.
148
147
  * @property {boolean} [omitWhenAbsent] - **The key is left out entirely when
149
- * the note does not carry the field** (#329), rather than written from a
148
+ * the note does not carry the field**, rather than written from a
150
149
  * declared default.
151
150
  *
152
151
  * Every other field answers absence with a value: an unauthored `weight` is
@@ -181,7 +180,7 @@ export { legacyKeyOf, retiredTopLevelKey, setPath };
181
180
  * which the generated reference states in the field's own row.
182
181
  * @property {string} [runtimeOnly] - **What the field holds once play has
183
182
  * started** — declared on a field the *document* writes for itself, which no
184
- * note may author (#330).
183
+ * note may author.
185
184
  *
186
185
  * A schema declares plenty of fields a compiled document has no business
187
186
  * carrying: an affliction's `onsetDate` is the world time its onset fired
@@ -295,7 +294,7 @@ export const BLANK_IS_DEFAULT = Object.freeze({
295
294
  *
296
295
  * **Only for a field that declares a `legacyKey`.** Every other field's
297
296
  * in-block position is simply where it lives; reporting those would put a
298
- * finding on every field of every note in every tree, which is #126's corpus
297
+ * finding on every field of every note in every tree, which is the corpus
299
298
  * migration rather than a signal anyone could act on.
300
299
  *
301
300
  * @param {FieldSpec} field - The declaration.
@@ -309,7 +308,7 @@ export function readsLegacyKey(field, from) {
309
308
 
310
309
  /**
311
310
  * Whether a resolution read a field from the top-level key `data:` gathered it
312
- * off — the shared level's retiring position (#332).
311
+ * off — the shared level's retiring position.
313
312
  *
314
313
  * {@link readsLegacyKey}'s sibling, and the same signal: a finding here counts
315
314
  * one note still on the pre-`data:` spelling, so the sweep has something to
@@ -335,7 +334,7 @@ export function readsRetiredTopLevel(field, from) {
335
334
  *
336
335
  * The *position* is resolved by {@link resolveFieldValue} — `<system>.system`
337
336
  * first, then the legacy in-block key, then the declared shared source, then
338
- * the default (#58). The **coercion** is applied here, once, wherever the value
337
+ * the default. The **coercion** is applied here, once, wherever the value
339
338
  * came from: a field's `read` is a statement about the field, not about where
340
339
  * an author happened to write it, so `weight: "7"` reads as `7` at every one of
341
340
  * those positions.
@@ -347,12 +346,12 @@ export function readsRetiredTopLevel(field, from) {
347
346
  * against. The default is the one block every existing tree authors; a
348
347
  * second system passes its own.
349
348
  * @param {(field: FieldSpec) => void} [options.onLegacyKey] - Called with each
350
- * field read from the position it is being swept off (#305). A callback
349
+ * field read from the position it is being swept off. A callback
351
350
  * rather than a returned list because the caller is a compiler, which already
352
351
  * knows the note and how to locate a key in it; this module knows neither and
353
352
  * would have to invent a finding shape to say so.
354
353
  * @param {(field: FieldSpec) => void} [options.onRetiredTopLevel] - Called with
355
- * each field read from the top-level key `data:` gathered it off (#332). The
354
+ * each field read from the top-level key `data:` gathered it off. The
356
355
  * shared level's counterpart to `onLegacyKey`, and a separate callback
357
356
  * because it is a separate position: a note may have moved one of the two and
358
357
  * not the other, and a caller that conflated them would tell its author to
@@ -370,7 +369,7 @@ export function readField(field, fm, options = {}) {
370
369
  * every caller wants. A builder has one further question — *should the key be
371
370
  * written at all* — and it cannot be answered from the value: `null` from a
372
371
  * note and `null` from a declared default are the same value and opposite
373
- * facts (#329).
372
+ * facts.
374
373
  *
375
374
  * So the position rides back beside the value, resolved **once**. The
376
375
  * alternative is a builder that calls {@link resolveFieldValue} for the source
@@ -403,7 +402,7 @@ export function readFieldEntry(field, fm, { block = "sohl", onLegacyKey, onRetir
403
402
  * The question {@link FieldSpec.omitWhenAbsent} turns on, asked of the
404
403
  * *position* rather than of the value — which cannot answer it, since a
405
404
  * declared `default: null` and an authored `null` are indistinguishable once
406
- * the value is in hand (#329).
405
+ * the value is in hand.
407
406
  *
408
407
  * `undefined` counts as absent whatever position reported it, because writing
409
408
  * the key then emits a value `JSON.stringify` drops — the key present in the
@@ -434,17 +433,17 @@ export function isAuthored(from, value) {
434
433
  * systems declare the same shared source and different destinations.
435
434
  * @param {(field: FieldSpec) => void} [options.onLegacyKey] - Passed through to
436
435
  * {@link readField}: called with each field the note authored at the position
437
- * it is being swept off (#305).
436
+ * it is being swept off.
438
437
  * @param {(field: FieldSpec) => void} [options.onRetiredTopLevel] - Passed
439
438
  * through to {@link readField}: called with each field the note authored at
440
- * the top-level key `data:` gathered it off (#332).
439
+ * the top-level key `data:` gathered it off.
441
440
  * @returns {(fm: object) => object} A `system`-block builder.
442
441
  */
443
442
  export function buildFromFields(fields, { block = "sohl", onLegacyKey, onRetiredTopLevel } = {}) {
444
443
  return function buildDeclaredSystem(fm) {
445
444
  const out = {};
446
445
  for (const field of fields) {
447
- // A runtime-only field is not this builder's to write (#330). It is
446
+ // A runtime-only field is not this builder's to write. It is
448
447
  // declared so that the path is *claimed* — so the verbatim
449
448
  // passthrough leaves it alone and the refusal has a name — not so
450
449
  // that a compile-time answer is invented for a question only play
@@ -457,8 +456,8 @@ export function buildFromFields(fields, { block = "sohl", onLegacyKey, onRetired
457
456
  onLegacyKey,
458
457
  onRetiredTopLevel,
459
458
  });
460
- // The other conditional row: a field whose *absence* is meaningful
461
- // (#329). Writing a declared default would answer a question the
459
+ // The other conditional row: a field whose *absence* is meaningful.
460
+ // Writing a declared default would answer a question the
462
461
  // note did not ask — "this affliction's onset takes `null` days" —
463
462
  // and would make the field's unset state indistinguishable from an
464
463
  // authored one for every reader downstream. Omitting the key leaves
@@ -490,8 +489,8 @@ export function authoredFields(fields) {
490
489
  * The fields of a declaration a note may **never** write.
491
490
  *
492
491
  * The complement of {@link authoredFields} in the direction that matters: those
493
- * are the fields an author may write, these are the ones authoring is an error
494
- * (#330). Everything else in a declaration — a constant, a derived value — is
492
+ * are the fields an author may write, these are the ones authoring is an error.
493
+ * Everything else in a declaration — a constant, a derived value — is
495
494
  * simply not authored, which is a statement about the *builder* rather than
496
495
  * about the author, and says nothing about what happens if a note writes the
497
496
  * path anyway.
@@ -16,9 +16,9 @@
16
16
  *
17
17
  * A `Folder` is a real Foundry document, and it was the last kind this package
18
18
  * compiled from bespoke configuration — `*-folders.yaml`, five files per tree —
19
- * rather than from a note. That was the one hole in the rule #243 establishes,
19
+ * rather than from a note. That is the one hole in the rule,
20
20
  * *the compiler follows the index*: a pass cannot follow the index for things
21
- * the index does not contain (#256).
21
+ * the index does not contain.
22
22
  *
23
23
  * Three things follow from a folder being a note, and each is a defect that
24
24
  * becomes unrepresentable rather than a tidiness win:
@@ -26,14 +26,14 @@
26
26
  * 1. **`parent` is an address**, resolved and checked like every other
27
27
  * reference. A dangling parent stops being a special-cased
28
28
  * `Unknown folder id` and becomes an ordinary dead-address finding.
29
- * 2. **Where a folder materialises is derived from what references it** (#257).
29
+ * 2. **Where a folder materialises is derived from what references it**.
30
30
  * A documentation journal is filed beside the item it describes, which used
31
31
  * to mean passing the *items* pack's folder id into the *journals* pack —
32
32
  * verbatim, validated nowhere, and correct only where the two folder files
33
33
  * happened to mirror each other. They did in one tree of three. With one
34
34
  * folder note and one address there is no second file to disagree with the
35
35
  * first, so a pack cannot fail to declare a folder something in it points at.
36
- * 3. **The Foundry id is derived from the address** (#258), the way a
36
+ * 3. **The Foundry id is derived from the address**, the way a
37
37
  * `JournalEntryPage` id is already hashed from its anchor — with an authored
38
38
  * `id` still winning, so a world already holding these folders keeps
39
39
  * resolving them.
@@ -44,7 +44,7 @@
44
44
  *
45
45
  * **A folder note carries no prose.** It is structure, not content, so it wants
46
46
  * no documentation journal and takes no part in `docEntryTypes` — the one
47
- * decision #256 left open, settled the way it recommended.
47
+ * open decision, settled the way it recommended.
48
48
  *
49
49
  * @module
50
50
  */
@@ -109,7 +109,8 @@ export const DEFAULT_PARENT = "default";
109
109
  * `parent` and `color` are `data:` properties, which is where the
110
110
  * specification's `### type: folder` table puts them — the closed container,
111
111
  * so a misspelled `colour` is a finding rather than a theme parameter. They are
112
- * accepted at the top level too, because that is where #256's own example wrote
112
+ * accepted at the top level too, because that is where the canonical example
113
+ * writes
113
114
  * them and an author following the issue rather than the specification should
114
115
  * get a folder, not a silent default.
115
116
  *
@@ -194,7 +195,7 @@ export function folderAddress(pkg, shortcode) {
194
195
  * never run this build. Deriving it twice is how the index came to publish a
195
196
  * plausible-looking id that resolved to nothing — the general id rule hashes a
196
197
  * note's address under the `document` namespace, and a folder's is hashed under
197
- * {@link FOLDER_ID_NAMESPACE} (#310).
198
+ * {@link FOLDER_ID_NAMESPACE}.
198
199
  *
199
200
  * An **authored `id` still wins**, and is applied by the caller: this is the
200
201
  * derivation, not the resolution, so the pin rule stays stated once, where
@@ -212,8 +213,7 @@ export function folderDocId(pkg, shortcode) {
212
213
  * Collect every folder note in a content tree.
213
214
  *
214
215
  * The walk is the caller's to supply, so this stays testable without a tree on
215
- * disk and so one build cannot disagree with another about what the corpus is
216
- * (#243).
216
+ * disk and so one build cannot disagree with another about what the corpus is.
217
217
  *
218
218
  * @param {Iterable<{frontmatter: object|null, absPath: string}>} notes - As
219
219
  * yielded by `walkMarkdownTree`.
@@ -237,7 +237,7 @@ export function collectFolderNotes(notes, pkg) {
237
237
  // The charset rule is load-bearing here rather than tidy: the address
238
238
  // is parsed by counting separators, so a hyphenated shortcode would be
239
239
  // read back as two segments and resolve to nothing, reporting nothing
240
- // about why (#1397, #273).
240
+ // about why.
241
241
  if (!isAddressSegment(shortcode)) {
242
242
  throw Object.assign(
243
243
  new Error(
@@ -266,7 +266,7 @@ export function collectFolderNotes(notes, pkg) {
266
266
  Object.entries(parent).map(([pack, value]) => [pack, bareAddress(value)]),
267
267
  ),
268
268
  // An authored id is kept, and a folder without one derives a
269
- // stable one from its address (#258). Keeping the authored id is
269
+ // stable one from its address. Keeping the authored id is
270
270
  // what makes this a build change rather than a world migration: a
271
271
  // world already holding these folders goes on resolving them.
272
272
  id: authoredId || folderDocId(pkg, shortcode),
@@ -283,7 +283,7 @@ export function collectFolderNotes(notes, pkg) {
283
283
  *
284
284
  * Three keys per folder, and no more: the canonical address, the
285
285
  * `folder-<shortcode>` short form, and the bare shortcode. They are the
286
- * suffixes of the canonical address the grammar admits (#273) — a `packFolder`
286
+ * suffixes of the canonical address the grammar admits — a `packFolder`
287
287
  * or `parent` field supplies the type itself, so a bare shortcode is a complete
288
288
  * address there.
289
289
  *
@@ -402,7 +402,7 @@ export function buildFolderNoteIndex(folders) {
402
402
  *
403
403
  * A folder cannot materialise without them: a `Folder` whose parent is
404
404
  * absent from the pack is an orphan Foundry renders at the root, so the
405
- * tree would be broken at the top rather than merely incomplete (#257).
405
+ * tree would be broken at the top rather than merely incomplete.
406
406
  *
407
407
  * @param {FolderNote} folder - The folder.
408
408
  * @param {string} [pack] - The pack being compiled.
@@ -497,7 +497,7 @@ export function folderDocument(folder, parent, documentType, stats) {
497
497
  * Refuse a note that declares the retired `folder:` spelling.
498
498
  *
499
499
  * `folder:` named a compendium folder by the raw Foundry id declared in a
500
- * per-pack `*-folders.yaml`. Both halves are retired together (#260): the id
500
+ * per-pack `*-folders.yaml`. Both halves are retired together: the id
501
501
  * spelling has nothing left to resolve against once the YAML is gone, and the
502
502
  * YAML has no reader once the spelling is refused.
503
503
  *