@heroiclands/package-build 23.1.1 → 23.2.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 (246) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/bin/content-commands.mjs +68 -149
  3. package/bin/package-build.mjs +56 -10
  4. package/ci/check-banners.mjs +42 -6
  5. package/ci/confirm-npm-version.mjs +135 -0
  6. package/ci/content-format-reference.mjs +14 -5
  7. package/config.mjs +1 -0
  8. package/container.mjs +185 -4
  9. package/content-config.mjs +333 -210
  10. package/deploy.mjs +116 -10
  11. package/docs/api.md +238 -246
  12. package/docs/authoring/assets.md +11 -5
  13. package/docs/authoring/frontmatter.md +1 -1
  14. package/docs/authoring/links-and-markup.md +87 -0
  15. package/docs/commands.md +138 -99
  16. package/docs/configuration.md +277 -125
  17. package/docs/content-format.md +2 -0
  18. package/docs/diagnostics.md +48 -13
  19. package/docs/guides/book.md +341 -21
  20. package/docs/guides/e2e.md +13 -2
  21. package/docs/guides/icons.md +14 -4
  22. package/docs/guides/module-project.md +18 -8
  23. package/docs/guides/release-and-deploy.md +32 -0
  24. package/docs/guides/site.md +5 -2
  25. package/docs/project-setup.md +30 -10
  26. package/docs/reference/format-details.md +373 -170
  27. package/docs/reference/note-types.md +128 -109
  28. package/docs/reference/theme-pages.md +31 -6
  29. package/e2e.mjs +140 -28
  30. package/engine/actor-compiler.mjs +22 -31
  31. package/engine/address-charset.mjs +4 -17
  32. package/engine/address-fields.mjs +1 -1
  33. package/engine/address-render.mjs +12 -1
  34. package/engine/address-values.mjs +6 -0
  35. package/engine/address.mjs +46 -33
  36. package/engine/art-fields.mjs +30 -5
  37. package/engine/art-slots.mjs +10 -1
  38. package/engine/asset-bindings.mjs +12 -1
  39. package/engine/base-compiler.mjs +34 -65
  40. package/engine/being-age.mjs +2 -7
  41. package/engine/being-measurements.mjs +12 -1
  42. package/engine/calendar-artifacts.mjs +6 -0
  43. package/engine/calendar-choice.mjs +12 -1
  44. package/engine/calendar-format.mjs +12 -1
  45. package/engine/calendar-human.mjs +12 -1
  46. package/engine/compile-corpus.mjs +3 -1
  47. package/engine/content-address.mjs +9 -12
  48. package/engine/content-alerts.mjs +13 -1
  49. package/engine/content-blocks.mjs +14 -3
  50. package/engine/content-embeds.mjs +11 -3
  51. package/engine/content-figures.mjs +9 -0
  52. package/engine/content-footnotes.mjs +12 -1
  53. package/engine/content-format-check.mjs +11 -262
  54. package/engine/content-format.yaml +5 -2
  55. package/engine/content-images.mjs +3 -0
  56. package/engine/content-index.mjs +443 -20
  57. package/engine/content-links.mjs +29 -35
  58. package/engine/content-lint.mjs +2 -3
  59. package/engine/content-poetry.mjs +12 -1
  60. package/engine/content-slug.mjs +2 -14
  61. package/engine/content-spans.mjs +13 -1
  62. package/engine/content-tables.mjs +12 -1
  63. package/engine/culture-choice.mjs +210 -13
  64. package/engine/data-keys.mjs +19 -16
  65. package/engine/date-conversion.mjs +12 -1
  66. package/engine/declaration-check.mjs +9 -0
  67. package/engine/derived-fields.mjs +6 -7
  68. package/engine/diagnostics.mjs +34 -4
  69. package/engine/docs-checks.mjs +9 -0
  70. package/engine/document-subtypes.mjs +47 -34
  71. package/engine/event-views.mjs +139 -14
  72. package/engine/exported-scene.mjs +92 -28
  73. package/engine/extension-attributes.mjs +12 -1
  74. package/engine/field-spec.mjs +11 -102
  75. package/engine/folder-notes.mjs +0 -64
  76. package/engine/foreign-catalog.mjs +2 -1
  77. package/engine/format-generated.mjs +9 -0
  78. package/engine/foundry-entries.mjs +10 -28
  79. package/engine/frontmatter-lint.mjs +83 -392
  80. package/engine/frontmatter.mjs +20 -70
  81. package/engine/generate.mjs +10 -15
  82. package/engine/heading-attributes.mjs +12 -1
  83. package/engine/helpers.mjs +41 -102
  84. package/engine/holdings.mjs +1 -1
  85. package/engine/homepage.mjs +3 -10
  86. package/engine/ids.mjs +1 -167
  87. package/engine/index-records.mjs +6 -4
  88. package/engine/index.mjs +0 -3
  89. package/engine/infobox-render.mjs +15 -3
  90. package/engine/infobox.mjs +13 -14
  91. package/engine/item-compiler.mjs +4 -10
  92. package/engine/item-docs.mjs +3 -5
  93. package/engine/item-registry.mjs +2 -10
  94. package/engine/journals.mjs +1 -5
  95. package/engine/literature-notes.mjs +4 -12
  96. package/engine/macros.mjs +1 -2
  97. package/engine/map-graphviz.mjs +12 -1
  98. package/engine/markdown-expressions.mjs +12 -1
  99. package/engine/markdownlint-run.mjs +567 -0
  100. package/engine/metadata-index.mjs +42 -12
  101. package/engine/note-addresses.mjs +143 -35
  102. package/engine/note-claims.mjs +14 -28
  103. package/engine/note-dates.mjs +3 -3
  104. package/engine/note-frontmatter.mjs +6 -0
  105. package/engine/note-ids.mjs +8 -14
  106. package/engine/note-package.mjs +8 -25
  107. package/engine/note-state.mjs +1 -3
  108. package/engine/note-vocabulary.mjs +148 -84
  109. package/engine/number-words.mjs +12 -1
  110. package/engine/pack-config.mjs +15 -25
  111. package/engine/pack-router.mjs +10 -16
  112. package/engine/pathnames.mjs +8 -22
  113. package/engine/pdf-build.mjs +773 -121
  114. package/engine/pdf-coverage.mjs +188 -0
  115. package/engine/pdf-index.mjs +54 -7
  116. package/engine/pdf-pages.mjs +247 -0
  117. package/engine/pdf-render.mjs +114 -4
  118. package/engine/pdf-toc.mjs +29 -32
  119. package/engine/project-init.mjs +46 -6
  120. package/engine/prose-config.mjs +1 -1
  121. package/engine/prose-lint.mjs +52 -88
  122. package/engine/readability-lint.mjs +9 -0
  123. package/engine/reckoning-markers.mjs +12 -1
  124. package/engine/reference-targets.mjs +7 -0
  125. package/engine/runtime-only-fields.mjs +5 -8
  126. package/engine/scene-levels.mjs +2 -2
  127. package/engine/scenes.mjs +1 -1
  128. package/engine/shortcode-references.mjs +7 -0
  129. package/engine/site-build.mjs +30 -16
  130. package/engine/site-index.mjs +10 -16
  131. package/engine/site-preview.mjs +6 -0
  132. package/engine/site-root.mjs +8 -1
  133. package/engine/social-tie-terms.mjs +12 -1
  134. package/engine/social-ties.mjs +3 -0
  135. package/engine/sql-tables.mjs +56 -20
  136. package/engine/standing-terms.mjs +11 -1
  137. package/engine/standings.mjs +11 -27
  138. package/engine/system-block.mjs +39 -141
  139. package/engine/system-vocabulary.mjs +6 -0
  140. package/engine/walk-files.mjs +69 -0
  141. package/engine/web-wikilinks.mjs +23 -3
  142. package/engine/wikilink-syntax.mjs +32 -21
  143. package/engine/wikilinks.mjs +27 -16
  144. package/githooks/attribution.mjs +8 -0
  145. package/githooks/commit-message.mjs +9 -0
  146. package/hm3/actors.mjs +9 -48
  147. package/hm3/default-item-art.mjs +9 -9
  148. package/hm3/item-fields.mjs +2 -9
  149. package/hm3/template-priority.mjs +6 -7
  150. package/hugo-theme/data/banner-kinds.yaml +78 -0
  151. package/hugo-theme/data/banners.yaml +2 -3
  152. package/hugo-theme/layouts/404.html +3 -3
  153. package/hugo-theme/layouts/partials/banner-url.html +31 -7
  154. package/hugo-theme/layouts/partials/hero-banner.html +13 -14
  155. package/hugo-theme/layouts/partials/related.html +1 -1
  156. package/hugo-theme/layouts/partials/warn-missing-banner.html +1 -1
  157. package/hugo-theme/static/css/style.css +1 -1
  158. package/manifest.mjs +11 -4
  159. package/markdownlint-config.mjs +1 -1
  160. package/package.json +6 -3
  161. package/release.mjs +21 -14
  162. package/sohl/actors.mjs +16 -27
  163. package/sohl/default-item-art.mjs +3 -3
  164. package/sohl/document-subtypes.mjs +1 -16
  165. package/sohl/infobox.mjs +2 -3
  166. package/sohl/item-builders.mjs +5 -8
  167. package/sohl/item-fields.mjs +5 -35
  168. package/sohl/items.mjs +0 -4
  169. package/sohl/kb-passes.mjs +3 -3
  170. package/types/container.d.mts +116 -5
  171. package/types/content-config.d.mts +83 -35
  172. package/types/deploy.d.mts +42 -6
  173. package/types/e2e.d.mts +35 -3
  174. package/types/engine/address-charset.d.mts +4 -17
  175. package/types/engine/address.d.mts +17 -8
  176. package/types/engine/art-fields.d.mts +7 -4
  177. package/types/engine/art-slots.d.mts +6 -0
  178. package/types/engine/base-compiler.d.mts +15 -17
  179. package/types/engine/content-address.d.mts +9 -12
  180. package/types/engine/content-embeds.d.mts +4 -2
  181. package/types/engine/content-format-check.d.mts +0 -64
  182. package/types/engine/content-index.d.mts +82 -3
  183. package/types/engine/content-links.d.mts +2 -4
  184. package/types/engine/culture-choice.d.mts +51 -8
  185. package/types/engine/data-keys.d.mts +2 -2
  186. package/types/engine/derived-fields.d.mts +1 -2
  187. package/types/engine/diagnostics.d.mts +27 -4
  188. package/types/engine/document-subtypes.d.mts +12 -31
  189. package/types/engine/event-views.d.mts +14 -2
  190. package/types/engine/exported-scene.d.mts +3 -2
  191. package/types/engine/field-spec.d.mts +13 -122
  192. package/types/engine/folder-notes.d.mts +0 -39
  193. package/types/engine/foreign-catalog.d.mts +2 -1
  194. package/types/engine/foundry-entries.d.mts +4 -5
  195. package/types/engine/frontmatter-lint.d.mts +2 -2
  196. package/types/engine/frontmatter.d.mts +19 -45
  197. package/types/engine/generate.d.mts +1 -1
  198. package/types/engine/helpers.d.mts +25 -42
  199. package/types/engine/homepage.d.mts +3 -10
  200. package/types/engine/ids.d.mts +0 -114
  201. package/types/engine/index-records.d.mts +6 -4
  202. package/types/engine/index.d.mts +0 -1
  203. package/types/engine/infobox-render.d.mts +7 -1
  204. package/types/engine/infobox.d.mts +3 -2
  205. package/types/engine/item-docs.d.mts +1 -1
  206. package/types/engine/item-registry.d.mts +2 -4
  207. package/types/engine/literature-notes.d.mts +1 -1
  208. package/types/engine/markdownlint-run.d.mts +40 -0
  209. package/types/engine/metadata-index.d.mts +9 -1
  210. package/types/engine/note-addresses.d.mts +27 -2
  211. package/types/engine/note-claims.d.mts +3 -5
  212. package/types/engine/note-ids.d.mts +2 -4
  213. package/types/engine/note-package.d.mts +1 -1
  214. package/types/engine/note-state.d.mts +1 -3
  215. package/types/engine/note-vocabulary.d.mts +28 -21
  216. package/types/engine/pack-config.d.mts +2 -6
  217. package/types/engine/pack-router.d.mts +4 -4
  218. package/types/engine/pathnames.d.mts +3 -3
  219. package/types/engine/pdf-build.d.mts +98 -12
  220. package/types/engine/pdf-coverage.d.mts +60 -0
  221. package/types/engine/pdf-index.d.mts +34 -5
  222. package/types/engine/pdf-pages.d.mts +143 -0
  223. package/types/engine/pdf-render.d.mts +45 -1
  224. package/types/engine/pdf-toc.d.mts +12 -14
  225. package/types/engine/prose-config.d.mts +1 -1
  226. package/types/engine/prose-lint.d.mts +11 -10
  227. package/types/engine/runtime-only-fields.d.mts +2 -2
  228. package/types/engine/site-build.d.mts +9 -5
  229. package/types/engine/site-index.d.mts +4 -5
  230. package/types/engine/site-root.d.mts +1 -1
  231. package/types/engine/sql-tables.d.mts +31 -2
  232. package/types/engine/standing-terms.d.mts +2 -1
  233. package/types/engine/standings.d.mts +7 -9
  234. package/types/engine/system-block.d.mts +10 -44
  235. package/types/engine/walk-files.d.mts +15 -0
  236. package/types/engine/wikilink-syntax.d.mts +18 -19
  237. package/types/hm3/actors.d.mts +1 -2
  238. package/types/hm3/item-fields.d.mts +2 -4
  239. package/types/hm3/template-priority.d.mts +6 -7
  240. package/types/manifest.d.mts +3 -2
  241. package/types/release.d.mts +8 -7
  242. package/types/sohl/actors.d.mts +2 -1
  243. package/types/sohl/document-subtypes.d.mts +1 -2
  244. package/types/sohl/item-fields.d.mts +3 -7
  245. package/engine/retired-fields.mjs +0 -631
  246. package/types/engine/retired-fields.d.mts +0 -354
@@ -39,12 +39,10 @@
39
39
  * vocabulary (an adventure module ships skills, beings and magic swords), so
40
40
  * every consumer loads all of it.
41
41
  *
42
- * The two rules that are not schema-driven sit on the *note format* side of
43
- * that line, which is why they are here and not in `sohl/`: the retired
44
- * top-level fields, and the address-bearing fields a `type: homepage` note
45
- * refuses. Each supplies its own message from the module that owns the
46
- * knowledge — `retired-fields.mjs` and `homepage.mjs` — and this module only
47
- * locates it in the file.
42
+ * The rule that is not schema-driven sits on the *note format* side of that
43
+ * line, which is why it is here and not in `sohl/`: the address-bearing fields
44
+ * a `type: homepage` note refuses. The message comes from `homepage.mjs`, which
45
+ * owns the knowledge, and this module only locates it in the file.
48
46
  *
49
47
  * **It takes a built link index rather than walking itself.** The dead-
50
48
  * reference check has to resolve exactly as a wikilink does, cross-package
@@ -67,12 +65,10 @@ import {
67
65
  import { AddressEntries, AddressLink } from "./address-values.mjs";
68
66
  import { resolveEventReference } from "./note-events.mjs";
69
67
  import { addressPositions } from "./note-addresses.mjs";
70
- import { authoredFields, readsLegacyKey, readsRetiredTopLevel } from "./field-spec.mjs";
68
+ import { authoredFields } from "./field-spec.mjs";
71
69
  import {
72
- legacyKeyOf,
73
- resolveDataProperty,
70
+ inBlockKey,
74
71
  resolveFieldValue,
75
- retiredTopLevelKey,
76
72
  systemBlock,
77
73
  SYSTEM_BLOCK_KEYS,
78
74
  unknownBlockKeys,
@@ -84,7 +80,6 @@ import {
84
80
  } from "./diagnostics.mjs";
85
81
  import { pathnameProblem } from "./pathnames.mjs";
86
82
  import { checkHomepageAddressFields } from "./homepage.mjs";
87
- import { RETIRED_TYPES, RENAMED_TYPES, currentType, renamedTypeMessage } from "./ids.mjs";
88
83
  import { parseAddress, acceptsType, renderAddress, hasAnchor } from "./address.mjs";
89
84
  import { ASSET_TYPE_NAMES } from "./asset-types.mjs";
90
85
  import { isAddressSegment } from "./address-charset.mjs";
@@ -101,19 +96,6 @@ import {
101
96
  } from "./note-vocabulary.mjs";
102
97
  // The art slots, declared once. The linter states no art key of its own.
103
98
  import { ART_SLOTS } from "./art-fields.mjs";
104
- import {
105
- RETIRED_FIELD_ALIASES,
106
- declaresRetiredAlias,
107
- aliasesRetiredMessage,
108
- declaresRetiredAliasesField,
109
- draftRetiredMessage,
110
- legacyKeyMessage,
111
- readAliasedField,
112
- retiredAliasMessage,
113
- retiredTopLevelMessage,
114
- sectionRetiredMessage,
115
- traitsRetiredMessage,
116
- } from "./retired-fields.mjs";
117
99
 
118
100
  /**
119
101
  * `sohl:` keys every type accepts, whatever its schema says.
@@ -123,10 +105,10 @@ import {
123
105
  * declaration and both would otherwise be reported as unknown on every note
124
106
  * that uses them.
125
107
  *
126
- * - `folder` — the compendium folder the document is filed in.
108
+ * - `packFolder` — the folder note the document is filed in.
127
109
  * - `pack` — which compendium of the note's document type receives it, for a
128
110
  * repository shipping several.
129
- * - `archetype` — the archetype flag, read for any note by the walk itself.
111
+ * - `templatePriority` — which of several templates the Create dialog offers.
130
112
  * - `kbcat` — the knowledgebase category a note is grouped under.
131
113
  *
132
114
  * `kbcat` is the one that matters for what this check *is*. **The pack build is
@@ -144,22 +126,9 @@ import {
144
126
  * @type {ReadonlySet<string>}
145
127
  */
146
128
  export const UNIVERSAL_KEYS = Object.freeze(
147
- new Set(["packFolder", "pack", "archetype", "templatePriority", "kbcat"]),
129
+ new Set(["packFolder", "pack", "templatePriority", "kbcat"]),
148
130
  );
149
131
 
150
- /**
151
- * The shared `data:` keys declared for every note type that still have a
152
- * retiring top-level spelling — `pack` and `packFolder`, the two
153
- * {@link module:engine/note-vocabulary.SHARED_DATA_FIELDS} entries
154
- * {@link module:engine/system-block.resolveDataProperty} reads off a tree's
155
- * bare top level. Unlike a type's own declarations, which the retiring set
156
- * below derives from `schemas`, these apply to every type at once, so they
157
- * are named once here rather than attached to one type's vocabulary.
158
- *
159
- * @type {readonly string[]}
160
- */
161
- const UNIVERSAL_RETIRING_DATA_KEYS = Object.freeze(["pack", "packFolder"]);
162
-
163
132
  /**
164
133
  * The system blocks a build checks, and what each accepts beyond the shared
165
134
  * vocabulary.
@@ -331,17 +300,10 @@ export function matchesKind(value, kind, context = {}) {
331
300
  // field admits. A list has neither.
332
301
  return matchesKind(value, "string") || matchesKind(value, "map");
333
302
  case "string-or-list":
334
- // One value or several of the same kind. Unlike `list-or-map` the
303
+ // One value or several of the same kind. The
335
304
  // two halves say the same thing at different lengths, so either is
336
305
  // accepted here and the entries are read by the field's own check.
337
306
  return matchesKind(value, "string") || Array.isArray(value);
338
- case "list-or-map":
339
- // A list, or a map. The two carry different facts rather than
340
- // being two spellings of one — a map keyed by Address says what is
341
- // true *of* each key, which a list has nowhere to put — so the
342
- // field's own check is what reads the entries, and all this
343
- // answers is whether the value has one of the two shapes.
344
- return Array.isArray(value) || matchesKind(value, "map");
345
307
  default:
346
308
  return true;
347
309
  }
@@ -432,34 +394,8 @@ function checkDataContainer(note, { type, fields, packs, addressContext, index }
432
394
  /** First segment of each declared name — `charges.value` is authored as `charges`. */
433
395
  const declared = new Set(fields.map((f) => f.name.split(".")[0]));
434
396
 
435
- // The retired spelling of a key this type declares → what to write now.
436
- // Built from the type's own declaration, so a renamed `data:` key is
437
- // retired exactly where its replacement exists and stays an unrecognised
438
- // key on every other type.
439
- const renamed = new Map();
440
- for (const name of declared) {
441
- const retired = RETIRED_FIELD_ALIASES[name];
442
- if (retired) renamed.set(retired, name);
443
- }
444
-
445
397
  for (const key of Object.keys(entries)) {
446
398
  if (declared.has(key)) continue;
447
- const current = renamed.get(key);
448
- if (current) {
449
- findings.push({
450
- file: note.file,
451
- ...positionOfFrontmatterPath(raw, ["data", key], { key: true }),
452
- // A warning, as every retired spelling is: the note compiles to
453
- // the correct document, so failing a build over it would red a
454
- // tree that has done nothing wrong yet. The refusal comes once
455
- // no tree writes the key, and needs no code — with the alias
456
- // gone the spelling is an unrecognised key again, which the
457
- // loop below already refuses.
458
- severity: "warning",
459
- message: retiredAliasMessage(key, current),
460
- });
461
- continue;
462
- }
463
399
  const guess = nearest(key, declared);
464
400
  findings.push({
465
401
  file: note.file,
@@ -468,9 +404,6 @@ function checkDataContainer(note, { type, fields, packs, addressContext, index }
468
404
  message:
469
405
  type === "affiliation" && key === "commonSkills" ?
470
406
  "`data.commonSkills` is not an affiliation field; write skill Addresses at `sohl.system.commonSkills`"
471
- : type === "lore" && key === "event" ?
472
- "`data.event` is retired — write `data.events`, a list of occurrences, " +
473
- "instead, each `{ when, until?, recurs? }`; a note may state several"
474
407
  : `"${key}" is not a \`data:\` property declared by ${type}; ` +
475
408
  `the container is closed, so the key reaches no document ` +
476
409
  `and no page` +
@@ -604,13 +537,7 @@ function eventReferenceProblem(value, field, defaults, index) {
604
537
  function checkDataReferences(note, field, value, segments, context, index) {
605
538
  const checks = [];
606
539
  if (field.kind === "address") checks.push({ value, path: [], kind: "address" });
607
- // A `list-or-map` field declares both halves, and each describes one shape:
608
- // `entryKind` the list's entries, `keyKind` the map's keys. Reading both of
609
- // one value would check a map's standings as if they were Addresses and a
610
- // list's indices as if they were its keys.
611
- const dual = field.kind === "list-or-map";
612
- const written = Array.isArray(value) ? "list" : "map";
613
- if (field.entryKind && !(dual && written === "map")) {
540
+ if (field.entryKind) {
614
541
  if (Array.isArray(value)) {
615
542
  value.forEach((entry, i) =>
616
543
  checks.push({ value: entry, path: [i], kind: field.entryKind }),
@@ -622,7 +549,7 @@ function checkDataReferences(note, field, value, segments, context, index) {
622
549
  });
623
550
  } else checks.push({ value, path: [], kind: field.entryKind });
624
551
  }
625
- if (field.keyKind && !(dual && written === "list")) {
552
+ if (field.keyKind && !Array.isArray(value)) {
626
553
  if (value instanceof AddressEntries) {
627
554
  for (const entry of value.entries)
628
555
  checks.push({
@@ -639,7 +566,9 @@ function checkDataReferences(note, field, value, segments, context, index) {
639
566
  const findings = [];
640
567
  const defaults = { ...context, type: field.ref };
641
568
  const accepted = field.accepts;
569
+ const slot = ART_SLOTS.find((candidate) => candidate.key === field.name);
642
570
  for (const check of checks) {
571
+ if (field.declines !== undefined && check.value === field.declines) continue;
643
572
  if (
644
573
  check.key &&
645
574
  field.keySelector === "subType" &&
@@ -661,10 +590,14 @@ function checkDataReferences(note, field, value, segments, context, index) {
661
590
  reason = `accepts ${accepted.join(" or ")}, not ${tuple.type}`;
662
591
  else if (
663
592
  index?.addressHit &&
664
- !ART_SLOTS.some((slot) => slot.key === field.name) &&
593
+ (!slot || slot.declines !== undefined) &&
665
594
  !index.addressHit(renderAddress(tuple))
666
595
  )
667
- reason = `names ${renderAddress(tuple)}, which does not resolve in this package or its declared dependencies`;
596
+ reason =
597
+ `names ${renderAddress(tuple)}, which does not resolve in this package or its declared dependencies` +
598
+ (slot?.declines === undefined ?
599
+ ""
600
+ : `; write \`${slot.declines}\` to decline the image on purpose`);
668
601
  }
669
602
  if (!reason) continue;
670
603
  const path = ["data", ...segments, ...check.path];
@@ -767,10 +700,7 @@ export function systemAddressFindings(note, { index, schemas, systems } = {}) {
767
700
  const address =
768
701
  isAddressTuple(item.value) ? item.value
769
702
  : typeof item.value === "string" ?
770
- parseAddress(item.value, context, {
771
- declared: true,
772
- legacyShortcodeCase: position.legacyShortcodeCase,
773
- })
703
+ parseAddress(item.value, context, { declared: true })
774
704
  : null;
775
705
  if (!address || address.reason) continue;
776
706
  if (position.accepts && !acceptsType(address, position.accepts)) continue;
@@ -883,11 +813,8 @@ function checkKeyedMap(note, { field, segments, entries, raw, packs }) {
883
813
  * whatever the type declares, and only once it is a well-formed term is the
884
814
  * type's own list the reason to refuse it.
885
815
  *
886
- * There is deliberately no third, retired-spelling check ahead of them
887
- * accepting `user-guide` as a warning naming `userguide`. Every consumer tree
888
- * has swept, so it would guard nothing: the old spelling falls through to the
889
- * charset check, which refuses it for the reason that always applied — it
890
- * contains a hyphen.
816
+ * A hyphenated term such as `user-guide` falls through to the charset check,
817
+ * which refuses it because it contains a hyphen.
891
818
  *
892
819
  * @param {object} note - The note.
893
820
  * @param {object} opts
@@ -1027,10 +954,8 @@ export const ART_FIELDS = Object.freeze(
1027
954
  * The keys a field declaration is authored at **inside a system block**.
1028
955
  *
1029
956
  * The first segment of each field's in-block key: `impact.die` is authored as
1030
- * `impact`, and a field whose shared source moved under `data:` is authored at
1031
- * the `legacyKey` it declares rather than at its dotted name. Keying on
1032
- * the name instead would report `sohl.species` as a property no `being` has,
1033
- * against exactly the notes the sweep has not reached yet.
957
+ * `impact`. A field whose `name` is a path into `data:` has none: it is
958
+ * authored in the container.
1034
959
  *
1035
960
  * Written once and read twice: the note type's own declaration answers for the
1036
961
  * system whose vocabulary the caller's `schemas` are, and a second system's
@@ -1041,7 +966,11 @@ export const ART_FIELDS = Object.freeze(
1041
966
  * @returns {Set<string>} The in-block keys.
1042
967
  */
1043
968
  function inBlockKeys(schema) {
1044
- return new Set(authoredFields(schema ?? []).map((f) => legacyKeyOf(f).split(".")[0]));
969
+ return new Set(
970
+ authoredFields(schema ?? [])
971
+ .map((f) => inBlockKey(f)?.split(".")[0])
972
+ .filter((key) => key !== undefined),
973
+ );
1045
974
  }
1046
975
 
1047
976
  /**
@@ -1060,8 +989,8 @@ function inBlockKeys(schema) {
1060
989
  * nothing outside that item supplies them and a note-level check must not go
1061
990
  * looking in the block for one.
1062
991
  *
1063
- * Keyed on the **in-block** key — `legacyKey` where a field declares one, and
1064
- * its first segment where that is dotted — because that is the position a note
992
+ * Keyed on the **in-block** key — its first segment where that is dotted —
993
+ * because that is the position a note
1065
994
  * authors, and so the position a note-level check would otherwise read.
1066
995
  *
1067
996
  * @param {readonly object[]|null|undefined} schema - The type's declarations.
@@ -1073,7 +1002,8 @@ function collidingBlockKeys(schema) {
1073
1002
  if (!Array.isArray(schema)) return keys;
1074
1003
  for (const field of authoredFields(schema)) {
1075
1004
  if (field.topLevelMeans === undefined) continue;
1076
- keys.add(String(legacyKeyOf(field)).split(".")[0]);
1005
+ const key = inBlockKey(field);
1006
+ if (key !== undefined) keys.add(key.split(".")[0]);
1077
1007
  }
1078
1008
  return keys;
1079
1009
  }
@@ -1256,21 +1186,6 @@ export function lintNote(
1256
1186
  const raw = () => note.raw ?? "";
1257
1187
  const at = (key, literal) => positionInFrontmatter(raw(), key, literal ?? undefined);
1258
1188
 
1259
- // The keys the closed top-level region refuses in general, but a
1260
- // declaration still reads at the retiring position — this type's own
1261
- // fields, which `resolveFieldValue`'s step 3b resolves, plus `pack` and
1262
- // `packFolder`, declared for every type and resolved the same way by
1263
- // {@link resolveDataProperty}. Derived ahead of the schema lookup below,
1264
- // which runs after the closed-region check, so that check can skip
1265
- // exactly these and nothing else: a key no declaration names stays
1266
- // refused.
1267
- const retiringTopLevelKeys = new Set([
1268
- ...UNIVERSAL_RETIRING_DATA_KEYS,
1269
- ...authoredFields(schemas?.[currentType(type)] ?? [])
1270
- .filter((field) => field.topLevelMeans === undefined)
1271
- .map((field) => retiredTopLevelKey(field))
1272
- .filter((key) => key !== undefined),
1273
- ]);
1274
1189
  if (type === "being") {
1275
1190
  const archetypes = fm.data?.archetypes;
1276
1191
  if (
@@ -1374,76 +1289,26 @@ export function lintNote(
1374
1289
  * each check would otherwise ask the same question of the same
1375
1290
  * declarations.
1376
1291
  */
1377
- const blockCollisions = collidingBlockKeys(schemas?.[currentType(type)]);
1292
+ const blockCollisions = collidingBlockKeys(schemas?.[type]);
1378
1293
 
1379
- // The retired top-level fields, checked before the type: a note may carry
1380
- // one whatever its type is, and each finding stands on its own. Reported
1381
- // here as well as refused at compile because this is where an author meets
1382
- // every finding in the tree at once, rather than one note at a time.
1294
+ // `package:` is never a note property: the package is the repository's
1295
+ // configured `contentPackage`. Reported here as well as refused at compile
1296
+ // because this is where an author meets every finding in the tree at once.
1383
1297
  if (Object.hasOwn(fm, "package")) {
1384
1298
  findings.push({
1385
1299
  file: note.file,
1386
1300
  ...at("package"),
1387
1301
  severity: "error",
1388
1302
  message:
1389
- "`package:` is a retired frontmatter field — delete it. A " +
1390
- "note's package is this repository's configured " +
1391
- "`contentPackage`, in package-build.config.yaml, and every " +
1392
- "note in the tree belongs to it",
1303
+ "`package:` is not a frontmatter field — delete it. A note's package " +
1304
+ "is this repository's configured `contentPackage`, in " +
1305
+ "package-build.config.yaml, and every note in the tree belongs to it",
1393
1306
  });
1394
1307
  }
1395
- // `folder:` named a compendium folder by the raw Foundry id declared in a
1396
- // per-pack `*-folders.yaml`. Both halves are retired together: the
1397
- // id spelling has nothing left to resolve against once the YAML is gone.
1398
- //
1399
- // Checked here as well as refused at compile because this is where an
1400
- // author meets every one of them in the tree at once — which is what a
1401
- // tree still to sweep needs, the whole corpus rather than the first note
1402
- // the compile happens to reach.
1403
- //
1404
- // **Both positions**, because notes wrote it both ways: top-level, and
1405
- // inside the `sohl:` block. The block spelling is no longer a universal
1406
- // key, so it would otherwise be reported as merely unrecognized, which
1407
- // says nothing about what to write instead.
1408
- const sohlBlock = fm.sohl;
1409
- const folderInBlock =
1410
- !!sohlBlock && typeof sohlBlock === "object" && Object.hasOwn(sohlBlock, "folder");
1411
- if (Object.hasOwn(fm, "folder") || folderInBlock) {
1412
- findings.push({
1413
- file: note.file,
1414
- ...at("folder"),
1415
- severity: "error",
1416
- message:
1417
- "`folder:` is a retired frontmatter field — write `packFolder` " +
1418
- "instead. A folder is a note (`type: folder`) now, and " +
1419
- "`packFolder` names it by its address, not by the Foundry id a " +
1420
- "retired `*-folders.yaml` used to declare",
1421
- });
1422
- }
1423
- // `img: ""` was how a note said "I name no art" while `resolveImg`
1424
- // conflated the two empties and every caller defaulted with `||`. It now
1425
- // says the opposite — "ship no art, and do not default me" — so a
1426
- // note carrying the old spelling has quietly changed meaning. Forty-five
1427
- // `sohl-thalorna` notes were written under the old reading and would have
1428
- // lost their default art with no error and no warning; this is the guard
1429
- // that would have caught them.
1430
- //
1431
- // **Both art fields, because both go through `resolveImg`.** `portrait` is
1432
- // not a variant spelling of `img` — a being carries the two independently —
1433
- // and checking only the more common one is how the sweep that prompted this
1434
- // guard missed eleven `sohl-kethira-basic` beings that write
1435
- // `portrait: ""`. Whatever the rule is, it belongs to the function, not to
1436
- // one of the keys that reaches it.
1437
- //
1438
- // A **warning**, on the pattern the `package:` and retired-alias sweeps
1439
- // set: the note still compiles, to a document that is merely iconless, so
1440
- // reddening a tree over it would refuse before the sweep rather than after
1441
- // it. It is transitional in the same sense — `""` is a legal thing to mean,
1442
- // and the message says so, but nothing in any tree means it yet.
1443
- //
1444
- // **The art slots only.** The rule reads as a general one about optional
1445
- // strings, and it is not — it belongs to `resolveImg`, and nothing else
1446
- // goes through it.
1308
+ // An empty art value is an error. A slot names an image or is left out; a
1309
+ // blank is how an unfinished note looks, and declining art has to be said
1310
+ // by omitting the key. Every art slot is checked, wherever the note writes
1311
+ // it, because the rule belongs to the slots and not to one key.
1447
1312
  //
1448
1313
  // **A block key a type claims for something else is read past.**
1449
1314
  // `topLevelMeans` settles which position the *emitted field* reads; it says
@@ -1452,29 +1317,6 @@ export function lintNote(
1452
1317
  // of address would answer for a note's art, in twenty-eight
1453
1318
  // `sohl-kethira-basic` affiliations. Hence `blockCollisions`.
1454
1319
 
1455
- // The template priority is a *shared source* — the specification states it
1456
- // once for every type, as it does `pack` — so its retirement is reported
1457
- // here rather than by the per-type loop below, which only reaches a field
1458
- // some type's vocabulary declares.
1459
- if (declaresRetiredAlias(fm, "templatePriority")) {
1460
- findings.push({
1461
- file: note.file,
1462
- ...at(RETIRED_FIELD_ALIASES.templatePriority),
1463
- // An error, unlike the other retired alias. `archetype` is not
1464
- // a field of its own — it is `templatePriority` under its prior
1465
- // name, and both sit one letter from `archetypes`, which means
1466
- // something else entirely. A tree still on it is one where a
1467
- // priority and a taxonomy are told apart by a plural `s`, which
1468
- // is worth stopping rather than mentioning. This reds every
1469
- // tree until each is swept; that is the point.
1470
- severity: "error",
1471
- message: retiredAliasMessage(
1472
- RETIRED_FIELD_ALIASES.templatePriority,
1473
- "templatePriority",
1474
- ),
1475
- });
1476
- }
1477
-
1478
1320
  // An art field a note's own type never emits. `img` is a *shared
1479
1321
  // top-level* field — `BLOCK_DOCUMENT_PROPERTIES` maps it onto
1480
1322
  // `document.img`, so it is legal on every note whatever the type — and a
@@ -1500,12 +1342,11 @@ export function lintNote(
1500
1342
  // state, and telling each of them to delete a key that already means
1501
1343
  // nothing would bury the fifty-seven that name a path they believe ships.
1502
1344
  //
1503
- // A **warning**, as the `package:` and retired-alias sweeps are: the note
1504
- // compiles correctly and the value is merely inert. Nor is it certainly
1345
+ // A **warning**: the note compiles correctly and the value is merely inert. Nor is it certainly
1505
1346
  // unwanted — a note's top level is the generated page's front matter as
1506
1347
  // well, so a template may read there what no document carries, which is a
1507
1348
  // judgement only the tree's author can make.
1508
- const emitted = emittedArt ? emittedArt(currentType(type)) : null;
1349
+ const emitted = emittedArt ? emittedArt(type) : null;
1509
1350
  /** The art fields this note's type reaches nothing through. */
1510
1351
  const inertArt = new Set(
1511
1352
  emitted ?
@@ -1521,6 +1362,8 @@ export function lintNote(
1521
1362
  blockCollides: blockCollisions.has(key),
1522
1363
  });
1523
1364
  if (typeof authored !== "string" && !isAddressTuple(authored)) continue;
1365
+ // An empty value has its own finding below; one finding per key.
1366
+ if (authored === "") continue;
1524
1367
  findings.push({
1525
1368
  file: note.file,
1526
1369
  ...at(key),
@@ -1549,6 +1392,8 @@ export function lintNote(
1549
1392
  blockCollides: blockCollisions.has(key),
1550
1393
  });
1551
1394
  if (typeof authored !== "string" && !isAddressTuple(authored)) continue;
1395
+ // An empty value has its own finding below.
1396
+ if (authored === "") continue;
1552
1397
  const problem = pathnameProblem(authored);
1553
1398
  if (!problem) continue;
1554
1399
  findings.push({
@@ -1560,70 +1405,20 @@ export function lintNote(
1560
1405
  }
1561
1406
 
1562
1407
  for (const { key, inData } of ART_FIELDS) {
1563
- // Reported above, and the distinction this draws does not exist there:
1564
- // where nothing is emitted, `""` and `null` are equally inert and the
1565
- // note has no default art to lose.
1566
- if (inertArt.has(key)) continue;
1567
1408
  if (authoredValue(fm, key, { inData, blockCollides: blockCollisions.has(key) }) !== "")
1568
1409
  continue;
1569
1410
  findings.push({
1570
1411
  file: note.file,
1571
1412
  ...at(key),
1572
- severity: "warning",
1573
- message:
1574
- `\`${key}: ""\` means "ship no art at all" — it no longer falls ` +
1575
- `back to this type's default. Write \`${key}: null\` for a note ` +
1576
- 'that simply names none; keep `""` only where the document is ' +
1577
- "meant to have no image",
1578
- });
1579
- }
1580
- if (Object.hasOwn(fm, "draft")) {
1581
- findings.push({
1582
- file: note.file,
1583
- ...at("draft"),
1584
- severity: "error",
1585
- message: draftRetiredMessage(),
1586
- });
1587
- }
1588
- // Anchored at column 1 for the same reason `aliases` is: a nested
1589
- // `section` under some other block is not this field.
1590
- if (Object.hasOwn(fm, "section")) {
1591
- findings.push({
1592
- file: note.file,
1593
- ...positionInFrontmatter(raw(), "section", undefined, { topLevel: true }),
1594
- severity: "error",
1595
- message: sectionRetiredMessage(),
1596
- });
1597
- }
1598
- // Anchored at column 1 for the same reason `section` is, and with more at
1599
- // stake: `sohl.traits` is a *different field that shares the name* —
1600
- // `projectilegear` declares one and the theme's gear sidebar reads it — so
1601
- // a finding about the retired top-level block must never open on it.
1602
- if (Object.hasOwn(fm, "traits")) {
1603
- findings.push({
1604
- file: note.file,
1605
- ...positionInFrontmatter(raw(), "traits", undefined, { topLevel: true }),
1606
- severity: "error",
1607
- message: traitsRetiredMessage(),
1608
- });
1609
- }
1610
- // Only the top-level `aliases` is retired. `name.aliases` writes the same
1611
- // key indented under `name:` and is **permitted** — reserved and unread —
1612
- // so both the test and the locator are anchored at column 1.
1613
- if (declaresRetiredAliasesField(fm)) {
1614
- findings.push({
1615
- file: note.file,
1616
- ...positionInFrontmatter(raw(), "aliases", undefined, { topLevel: true }),
1617
1413
  severity: "error",
1618
- message: aliasesRetiredMessage(),
1414
+ message:
1415
+ `\`${key}: ""\` is not a valid art value — an empty value names no ` +
1416
+ `image. Remove the key to name none, or name an image`,
1619
1417
  });
1620
1418
  }
1621
-
1622
1419
  // What the address rule says about a homepage's top-level fields: the
1623
- // `shortcode` it owes, and the `id` it may not write. Reported
1624
- // beside the retired fields above because it is the same kind of statement
1625
- // about the same note, and, like them, it must survive the two early
1626
- // returns below: the finding stands whatever else the type is.
1420
+ // `shortcode` it owes, and the `id` it may not write. It must survive the
1421
+ // two early returns below: the finding stands whatever else the type is.
1627
1422
  // Tags are checked here for the same reason: a classifying tag is not a
1628
1423
  // type's property — `draft` belongs to any note and `village` to a place —
1629
1424
  // so the finding must survive the early returns below.
@@ -1651,9 +1446,6 @@ export function lintNote(
1651
1446
 
1652
1447
  for (const key of authoredNoteKeys(raw())) {
1653
1448
  if (NOTE_TOP_LEVEL_KEY_SET.has(key)) continue;
1654
- // Reported below, as the retiring position it is rather than a key
1655
- // nobody recognises.
1656
- if (retiringTopLevelKeys.has(key)) continue;
1657
1449
  const position = positionInFrontmatter(raw(), key, undefined, { topLevel: true });
1658
1450
  if (
1659
1451
  findings.some(
@@ -1675,28 +1467,6 @@ export function lintNote(
1675
1467
  });
1676
1468
  }
1677
1469
 
1678
- // `pack` and `packFolder` are declared for every type, so each is
1679
- // checked once here rather than in the per-type `fields` loop below,
1680
- // which only iterates a type's own declarations. A **warning**, for the
1681
- // reason the per-type retiring position above is one: the note compiles
1682
- // to the correct document either way, so failing a build over it would
1683
- // red a tree that has done nothing wrong yet. Checked against every
1684
- // configured system block, because `resolveDataProperty` reads a
1685
- // system's own override before the top level, and a note may configure
1686
- // more than one.
1687
- for (const key of UNIVERSAL_RETIRING_DATA_KEYS) {
1688
- const retiring = Object.keys(systems ?? {}).some(
1689
- (blockName) => resolveDataProperty(fm, blockName, key).from === "topLevel",
1690
- );
1691
- if (!retiring) continue;
1692
- findings.push({
1693
- file: note.file,
1694
- ...at(key),
1695
- severity: "warning",
1696
- message: retiredTopLevelMessage({ name: `data.${key}` }),
1697
- });
1698
- }
1699
-
1700
1470
  // The type's charset, before anything that looks the type up. A
1701
1471
  // hyphenated type is unaddressable, and every lookup below would report it
1702
1472
  // as a type nobody declared — true, but not the reason, and it would send
@@ -1716,41 +1486,7 @@ export function lintNote(
1716
1486
  return findings;
1717
1487
  }
1718
1488
 
1719
- const replacement = RETIRED_TYPES[type];
1720
- if (replacement) {
1721
- findings.push({
1722
- file: note.file,
1723
- ...at("type", type),
1724
- severity: "error",
1725
- message:
1726
- `content type "${type}" was retired in favour of ` +
1727
- `"${replacement}"; both compiled to the same document, so the ` +
1728
- `fix is mechanical: write "${replacement}"`,
1729
- });
1730
- return findings;
1731
- }
1732
-
1733
- // A **renamed** type is the opposite case, and the opposite answer: the
1734
- // note compiles into exactly the document it always did, so refusing it
1735
- // would fail a build over a note that is not wrong. It is reported, and
1736
- // every lookup below reads the current spelling.
1737
- const renamedTo = RENAMED_TYPES[type];
1738
- if (renamedTo) {
1739
- findings.push({
1740
- file: note.file,
1741
- ...at("type", type),
1742
- // A warning, for the reason the retired *field* alias below is one:
1743
- // the sweep is the content trees' work and the refusal comes after
1744
- // it, as `package:`'s did.
1745
- severity: "warning",
1746
- message: renamedTypeMessage(type, renamedTo),
1747
- });
1748
- }
1749
- // What every type-keyed table is keyed by. The authored spelling is still
1750
- // what a message quotes — it is what the reader has in front of them.
1751
- const current = currentType(type);
1752
-
1753
- const schema = schemas[current];
1489
+ const schema = schemas[type];
1754
1490
  if (!schema) {
1755
1491
  findings.push({
1756
1492
  file: note.file,
@@ -1768,9 +1504,9 @@ export function lintNote(
1768
1504
  // one this type may write. Skipped entirely when the caller declares no
1769
1505
  // vocabulary — reporting every key as unknown because nothing was loaded
1770
1506
  // to recognise it would be worse than not checking.
1771
- const entry = vocabulary?.[current];
1507
+ const entry = vocabulary?.[type];
1772
1508
  if (entry) {
1773
- const fields = dataFields(current, vocabulary) ?? [];
1509
+ const fields = dataFields(type, vocabulary) ?? [];
1774
1510
  findings.push(
1775
1511
  ...checkDataContainer(note, {
1776
1512
  type,
@@ -1801,33 +1537,6 @@ export function lintNote(
1801
1537
  /** The keys this type's own declaration is authored at inside a block. */
1802
1538
  const declared = inBlockKeys(schema);
1803
1539
 
1804
- // The retired spelling of a field this type declares → what to write now.
1805
- // Built from the type's own vocabulary, so a renamed field is retired
1806
- // exactly where its replacement exists and the old name stays an unknown
1807
- // key everywhere else.
1808
- const renamed = new Map();
1809
- for (const name of declared) {
1810
- const retired = RETIRED_FIELD_ALIASES[name];
1811
- if (retired) renamed.set(retired, name);
1812
- }
1813
- for (const [retired, current] of renamed) {
1814
- // `declaresRetiredAlias` searches both regions, because both are read:
1815
- // a note that moved the key to the top level without renaming it has
1816
- // done half the migration. It is the same predicate the compile-time
1817
- // report asks, so the two cannot disagree about what a note declares.
1818
- if (!declaresRetiredAlias(fm, current)) continue;
1819
- findings.push({
1820
- file: note.file,
1821
- ...at(retired),
1822
- // A warning, not an error: the note compiles to the correct
1823
- // document, so failing a build over it would red a tree that has
1824
- // done nothing wrong yet. The refusal comes after the sweep, as
1825
- // `package:`'s did.
1826
- severity: "warning",
1827
- message: retiredAliasMessage(retired, current),
1828
- });
1829
- }
1830
-
1831
1540
  // Every declared system's block, each against its own vocabulary. A
1832
1541
  // block carries the shared keys any system's does — `system`, `type`,
1833
1542
  // `img`, `effects`, `flags`, `pack` — plus whatever that system declares:
@@ -1859,12 +1568,29 @@ export function lintNote(
1859
1568
  ...(spec?.fieldVocabulary ? declared : []),
1860
1569
  ...(own ? inBlockKeys(own[type]) : []),
1861
1570
  ]);
1571
+ // A field whose home is `data:`, written in the block instead. The
1572
+ // block does not read it, and it may share a name with a field the
1573
+ // block does read, so it is named here rather than left to the
1574
+ // unknown-key finding below.
1575
+ const misplaced = new Set(
1576
+ [...(spec?.fieldVocabulary ? fields : []), ...authoredFields(own?.[type] ?? [])]
1577
+ .filter((field) => field.name.startsWith("data."))
1578
+ .map((field) => field.name.slice("data.".length).split(".")[0]),
1579
+ );
1580
+ const block = systemBlock(fm, blockName);
1581
+ for (const key of Object.keys(block ?? {})) {
1582
+ if (!misplaced.has(key)) continue;
1583
+ findings.push({
1584
+ file: note.file,
1585
+ ...positionOfFrontmatterPath(raw(), [blockName, key], { key: true }),
1586
+ severity: "error",
1587
+ message:
1588
+ `\`${blockName}.${key}\` is a \`data:\` property of a ${type}; ` +
1589
+ `write it at \`data.${key}\` — the block does not read it`,
1590
+ });
1591
+ }
1862
1592
  for (const key of unknownBlockKeys(fm, blockName, { known: accepted })) {
1863
- // Reported above, with what to write instead — a retired spelling
1864
- // is a rename to schedule, not a key nobody recognises. Only where
1865
- // the block's vocabulary is this type's: the alias renames *this*
1866
- // system's field, and another system's like-spelled key is not it.
1867
- if (spec?.fieldVocabulary && renamed.has(key)) continue;
1593
+ if (misplaced.has(key)) continue;
1868
1594
  const guess = nearest(key, [...accepted, ...SYSTEM_BLOCK_KEYS]);
1869
1595
  findings.push({
1870
1596
  file: note.file,
@@ -1890,42 +1616,7 @@ export function lintNote(
1890
1616
  // the moment it moved to another of them. A **shared** field needs
1891
1617
  // nothing extra here — the note's top level *is* the third step — so
1892
1618
  // `shared` says only where the field's home is, for the message below.
1893
- let { value, from } = resolveFieldValue(field, fm, { block: "sohl" });
1894
- // A **renamed** field may still be written under its retired spelling,
1895
- // which that order knows nothing about. It resolves through the reader
1896
- // the compiler uses, so the lint cannot disagree with the build about
1897
- // which value a note carries.
1898
- if ((from === "default" || value == null) && RETIRED_FIELD_ALIASES[field.name]) {
1899
- const aliased = readAliasedField(fm, field.name);
1900
- if (aliased !== undefined) {
1901
- value = aliased;
1902
- from = "block";
1903
- }
1904
- }
1905
- // The sweep's progress signal. A **warning**, for the reason a
1906
- // retired spelling is one: the note compiles to the correct document,
1907
- // so failing a build over it would red a tree that has done nothing
1908
- // wrong yet. The refusal comes once no tree writes the position.
1909
- if (readsLegacyKey(field, from)) {
1910
- findings.push({
1911
- file: note.file,
1912
- ...at(legacyKeyOf(field)),
1913
- severity: "warning",
1914
- message: legacyKeyMessage("sohl", field),
1915
- });
1916
- }
1917
- // `readsLegacyKey`'s sibling for the other retiring position — the
1918
- // note's own top level, which `data:` gathered the fact off. Also a
1919
- // **warning**: the closed-region check above already let this exact
1920
- // key through for this exact reason, and the two must agree.
1921
- if (readsRetiredTopLevel(field, from)) {
1922
- findings.push({
1923
- file: note.file,
1924
- ...at(retiredTopLevelKey(field)),
1925
- severity: "warning",
1926
- message: retiredTopLevelMessage(field),
1927
- });
1928
- }
1619
+ const { value, from } = resolveFieldValue(field, fm, { block: "sohl" });
1929
1620
  const absent = from === "default" || value === undefined || value === null;
1930
1621
  // Where the field belongs, as a message names it: a shared field is not
1931
1622
  // under `sohl:`, so telling an author to write `sohl.img` would send
@@ -1933,7 +1624,7 @@ export function lintNote(
1933
1624
  // path into `data:` — `sohl.data.species` is a region that does not
1934
1625
  // exist, and the home of that field is the container it names.
1935
1626
  const label =
1936
- field.shared || (field.name.includes(".") && field.legacyKey !== undefined) ?
1627
+ field.shared || field.name.startsWith("data.") ?
1937
1628
  `\`${field.name}\``
1938
1629
  : `\`sohl.${field.name}\``;
1939
1630