@heroiclands/package-build 18.0.0 → 18.1.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 18.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 029cf03: **A homepage note is no longer refused for an `id` it did not author** (#319).
8
+
9
+ `resolveNoteId` fills `fm.id` **in place**, so every downstream reader sees one
10
+ derived value — deliberate, and documented as such. The homepage refusal
11
+ iterated that same object, so a note that authors no `id` was reported with a
12
+ message telling the author to delete a field that is not in the file.
13
+
14
+ It was an **error**, so it failed `lint:addresses`, and `lint` heads the build
15
+ chain — which meant it failed every pull request opened against a repository
16
+ carrying a homepage note, whatever that pull request changed.
17
+
18
+ A refused field must now be one the note actually wrote. The caller already owns
19
+ the raw note text and already positions these findings with it, so it answers
20
+ which keys are declared at the note's own top level; `positionInFrontmatter`'s
21
+ `topLevel` option is the existing helper for exactly that question, so a nested
22
+ `id:` under some other key is not mistaken for the note's own. With no answer
23
+ supplied, every key in `fm` still counts — the previous behaviour, and the right
24
+ one for a caller holding authored frontmatter only.
25
+
26
+ Measured on `sohl`: `lint:addresses` goes from one error to **clean across all
27
+ 1,685 notes**.
28
+
29
+ ## 18.1.0
30
+
31
+ ### Minor Changes
32
+
33
+ - e9a4200: **The gear-suffixed note types are restored** — `armorgear`, `concoctiongear`
34
+ and `projectilegear` are the vocabulary again, reversing #78.
35
+
36
+ #78 renamed them to `armor`, `concoction` and `projectile`, on the argument that
37
+ the suffix named the _SoHL document subtype_ a note compiled into rather than
38
+ the thing the note is about. The argument had a cost the rename did not pay for.
39
+
40
+ **Nothing adopted the bare spellings.** Across all five content trees, every note
41
+ still authors the suffix — 331 `armorgear` and 18 `projectilegear` in `sohl`, 71
42
+ `concoctiongear` in `thalorna` — and **not one note anywhere** writes `armor`,
43
+ `concoction` or `projectile`. The rename produced **349 warnings and zero
44
+ adopters**, and it was those warnings that made `lint:addresses` noise on every
45
+ consumer that took 18.0.0.
46
+
47
+ **It also cost a property worth more than the argument.** `weapongear` and
48
+ `containergear` kept their suffix — SoHL and HM3 both call those documents that —
49
+ so the rename left three of the five gear types spelled one way and two the
50
+ other. With it reversed, every SoHL row of the note-type → document-subtype map
51
+ is the identity again, all fourteen of them, which is what lets two tests drop
52
+ their exception lists entirely.
53
+
54
+ `RENAMED_TYPES` is reversed rather than emptied, so the bare spellings are still
55
+ _read_ and _reported_ rather than refused — the same retirement window the
56
+ rename itself used, pointing the other way. `armorlocation`, the type #78 added,
57
+ is unaffected: it was new, not renamed.
58
+
3
59
  ## 18.0.0
4
60
 
5
61
  ### Major Changes
@@ -24,11 +24,11 @@ note.** A type that both systems support always shows both system boxes, and a
24
24
  box whose system this particular note produces no document for reads
25
25
  **"Not available"**.
26
26
 
27
- | note type | system boxes shown |
28
- | ----------------------------------------------------------------- | -------------------------------------------------- |
29
- | `weapon`, `skill`, `being`, `armor`, `containergear`, `miscgear`… | SoHL **and** HM3 — either may read _Not available_ |
30
- | `affiliation`, `affliction`, `attribute`, `concoction`, `mystery` | SoHL only |
31
- | `armorlocation` | HM3 only |
27
+ | note type | system boxes shown |
28
+ | --------------------------------------------------------------------- | -------------------------------------------------- |
29
+ | `weapon`, `skill`, `being`, `armorgear`, `containergear`, `miscgear`… | SoHL **and** HM3 — either may read _Not available_ |
30
+ | `affiliation`, `affliction`, `attribute`, `concoctiongear`, `mystery` | SoHL only |
31
+ | `armorlocation` | HM3 only |
32
32
 
33
33
  This is stated rather than inferred from an empty block, because an absence is a
34
34
  poor signal: noticing that something is missing requires already knowing it
@@ -178,7 +178,7 @@ at all in any particular system.
178
178
 
179
179
  Each system then declares a map from the note's `(type, subType)` onto its own
180
180
  document type — and, for SoHL, its own `system.subType`. The map is **declared**,
181
- never inferred from a coincidence of names: `skill`, `weapongear`, `armor`,
181
+ never inferred from a coincidence of names: `skill`, `weapongear`, `armorgear`,
182
182
  `containergear` and `miscgear` exist in both systems with _different_ data
183
183
  models, so name-matching there would not fail, it would succeed wrongly.
184
184
 
@@ -902,7 +902,7 @@ A fenced `dataview` block is replaced by the table its query selects:
902
902
  ````markdown
903
903
  ```dataview
904
904
  TABLE WITHOUT ID name.full AS "Name", shortcode AS "Code"
905
- WHERE type = "armor"
905
+ WHERE type = "armorgear"
906
906
  ```
907
907
  ````
908
908
 
@@ -1423,7 +1423,7 @@ If `sohl` is present, this becomes an `affliction` item.
1423
1423
  | `data.healingCheckDurationFormula` | `system.healingCheckDurationFormula` | NA |
1424
1424
  | `data.resolutionDurationFormula` | `system.resolutionDurationFormula` | NA |
1425
1425
 
1426
- ### type: armor
1426
+ ### type: armorgear
1427
1427
 
1428
1428
  Note: `data.quantity` may not be specified. Quantity is always 1.
1429
1429
 
@@ -1437,9 +1437,7 @@ Note: `data.quantity` may not be specified. Quantity is always 1.
1437
1437
 
1438
1438
  If a `sohl` property is present, a SoHL item of type "armorgear" will be created.
1439
1439
 
1440
- if a `hm3` property is present, an HM3 item of type "armorgear" will be created. The
1441
- note type is `armor` in both cases: the `gear` suffix named a document subtype rather
1442
- than the thing the note is about.
1440
+ The note type is `armorgear` in both cases. The `gear` suffix was briefly renamed away on the argument that it named a SoHL document subtype rather than the thing the note is about; that rename is reversed. Nothing had adopted the bare spelling — every note in every tree still writes the suffix — and dropping it from three of the five gear types while `weapongear` and `containergear` kept theirs cost more consistency than the argument bought.
1443
1441
 
1444
1442
  | shared source | → sohl | → hm3 |
1445
1443
  | ----------------- | ----------------------- | --------------- |
@@ -1471,7 +1469,7 @@ if a `sohl` property is present, a SoHL item of type "attribute" will be created
1471
1469
  | shared source | → sohl | → hm3 |
1472
1470
  | ------------- | ------ | ----- |
1473
1471
 
1474
- ### type: concoction
1472
+ ### type: concoctiongear
1475
1473
 
1476
1474
  **subType**:
1477
1475
 
@@ -1624,7 +1622,7 @@ If an `hm3` property is present, an HM3 item is created, and `hm3.type` states w
1624
1622
  | `data.charges.value` | `system.charges.value` | NA |
1625
1623
  | `data.charges.max` | `system.charges.max` | NA |
1626
1624
 
1627
- ### type: projectile
1625
+ ### type: projectilegear
1628
1626
 
1629
1627
  **subTypes**:
1630
1628
 
@@ -1649,7 +1647,7 @@ if a `sohl` property is present, a SoHL item of type "projectilegear" will be cr
1649
1647
  If an `hm3` property is present, then an HM3 item of type "missilegear" will be created.
1650
1648
 
1651
1649
  Note that `weapon` can also produce an HM3 `missilegear`. Since `(type, shortcode)` is a
1652
- flat namespace, a `projectile` and a `weapon` sharing a shortcode would collide on the
1650
+ flat namespace, a `projectilegear` and a `weapon` sharing a shortcode would collide on the
1653
1651
  HM3 side while remaining distinct on the SoHL side.
1654
1652
 
1655
1653
  | shared source | → sohl | → hm3 |
@@ -890,7 +890,18 @@ export function lintNote(
890
890
  // so the finding must survive the early returns below.
891
891
  findings.push(...checkTags(note, { type }));
892
892
 
893
- for (const { locator, message } of checkHomepageAddressFields(fm)) {
893
+ // A refused field must be one the note *wrote*: `resolveNoteId` fills
894
+ // `fm.id` in place, so the parsed frontmatter carries a derived id the
895
+ // author never typed (#319). The raw text is the only place that
896
+ // distinguishes them, and `positionInFrontmatter` already answers it —
897
+ // `topLevel` so a nested `id:` under some other key is not mistaken for the
898
+ // note's own.
899
+ const authoredAtTopLevel = (key) =>
900
+ positionInFrontmatter(note.raw ?? "", key, undefined, { topLevel: true }).line !==
901
+ undefined;
902
+ for (const { locator, message } of checkHomepageAddressFields(fm, {
903
+ isAuthored: authoredAtTopLevel,
904
+ })) {
894
905
  findings.push({
895
906
  file: note.file,
896
907
  ...at(locator.key, locator.literal),
@@ -192,12 +192,24 @@ export const HOMEPAGE_REFUSED_FIELDS = Object.freeze(
192
192
  * belong to the caller. This mirrors {@link module:engine/retired-fields},
193
193
  * whose retired-field messages are likewise positioned by whoever reports them.
194
194
  *
195
+ * **A refused field must be one the note *wrote*.** `resolveNoteId` fills
196
+ * `fm.id` **in place** so every downstream reader sees one derived value —
197
+ * deliberately, and documented as such — and this ran over the same object, so
198
+ * a homepage that authors no `id` was told to delete one that is not there
199
+ * (#319). Since the caller already owns the raw note text, it also answers
200
+ * which keys the note actually declared; without an answer every key in `fm`
201
+ * counts, which is the old behaviour and right for a caller holding authored
202
+ * frontmatter only.
203
+ *
195
204
  * @param {object|null|undefined} fm - Parsed frontmatter.
205
+ * @param {object} [options] - Options.
206
+ * @param {(key: string) => boolean} [options.isAuthored] - Whether the note
207
+ * declares this key at its own top level. Defaults to "every key in `fm`".
196
208
  * @returns {Array<{field: string, locator: {key: string, literal?: string},
197
209
  * message: string}>} One entry per finding, empty for any note that is not a
198
210
  * homepage and declares nothing wrong.
199
211
  */
200
- export function checkHomepageAddressFields(fm) {
212
+ export function checkHomepageAddressFields(fm, { isAuthored } = {}) {
201
213
  if (!isHomepage(fm)) return [];
202
214
  const out = [];
203
215
 
@@ -218,7 +230,9 @@ export function checkHomepageAddressFields(fm) {
218
230
 
219
231
  for (const key of Object.keys(fm)) {
220
232
  const message = HOMEPAGE_REFUSED_FIELDS.get(key);
221
- if (message) out.push({ field: key, locator: { key }, message });
233
+ if (!message) continue;
234
+ if (isAuthored && !isAuthored(key)) continue;
235
+ out.push({ field: key, locator: { key }, message });
222
236
  }
223
237
  return out;
224
238
  }
package/engine/ids.mjs CHANGED
@@ -186,13 +186,22 @@ export function assertTypeNotRetired(type, where) {
186
186
  * reason: a consumer must be able to adopt the new toolchain before its content
187
187
  * moves, and there are some 31,000 references to move.
188
188
  *
189
- * **Why these three (#78).** `armorgear`, `concoctiongear` and `projectilegear`
190
- * named the *SoHL document subtype* a note happened to compile into rather than
191
- * the thing the note is about. A note's `type` sits outside the `sohl:` and
192
- * `hm3:` blocks precisely because it belongs to no system, and HM3 already
193
- * compiles a `projectile` into a `missilegear` so the suffix was never a fact
194
- * about the note. `weapongear` keeps its name: SoHL and HM3 both call the
195
- * document that, and #78's table has no row for it.
189
+ * **Why these three, and why the other way round now.** #78 renamed
190
+ * `armorgear`, `concoctiongear` and `projectilegear` to `armor`, `concoction`
191
+ * and `projectile`, on the argument that the suffix named the *SoHL document
192
+ * subtype* a note compiled into rather than the thing the note is about. That
193
+ * rename is **reversed**: the gear-suffixed spellings are the vocabulary, and
194
+ * the bare ones are what retires.
195
+ *
196
+ * Nothing had adopted the bare spellings, which is what makes the reversal
197
+ * cheap and the direction obvious: across all five content trees, **every note
198
+ * still authors the suffix** — 331 `armorgear` and 18 `projectilegear` in
199
+ * `sohl`, 71 `concoctiongear` in `thalorna`, and not one note anywhere writes
200
+ * `armor`, `concoction` or `projectile`. The rename produced 349 warnings and
201
+ * zero adopters.
202
+ *
203
+ * `weapongear` never moved, and the reversal restores the consistency its
204
+ * staying put had broken: all five gear types spell the suffix again.
196
205
  *
197
206
  * **Keyed by the retired spelling**, which is the opposite of
198
207
  * `RETIRED_FIELD_ALIASES`. The direction follows the operation: a field alias
@@ -210,9 +219,9 @@ export function assertTypeNotRetired(type, where) {
210
219
  * @type {Readonly<Record<string, string>>}
211
220
  */
212
221
  export const RENAMED_TYPES = Object.freeze({
213
- armorgear: "armor",
214
- concoctiongear: "concoction",
215
- projectilegear: "projectile",
222
+ armor: "armorgear",
223
+ concoction: "concoctiongear",
224
+ projectile: "projectilegear",
216
225
  });
217
226
 
218
227
  /**
@@ -590,7 +590,7 @@ export const NOTE_VOCABULARY = Object.freeze({
590
590
  ]),
591
591
  }),
592
592
 
593
- armor: Object.freeze({
593
+ armorgear: Object.freeze({
594
594
  // Quantity is always one, so the specification refuses the key rather
595
595
  // than defaulting it.
596
596
  data: Object.freeze([TEMPLATE_PRIORITY, ...GEAR]),
@@ -605,7 +605,7 @@ export const NOTE_VOCABULARY = Object.freeze({
605
605
  data: Object.freeze([TEMPLATE_PRIORITY]),
606
606
  }),
607
607
 
608
- concoction: Object.freeze({
608
+ concoctiongear: Object.freeze({
609
609
  subTypes: Object.freeze(["mundane", "exotic", "elixir"]),
610
610
  data: Object.freeze([
611
611
  TEMPLATE_PRIORITY,
@@ -680,7 +680,7 @@ export const NOTE_VOCABULARY = Object.freeze({
680
680
  ]),
681
681
  }),
682
682
 
683
- projectile: Object.freeze({
683
+ projectilegear: Object.freeze({
684
684
  subTypes: Object.freeze(["none", "arrow", "bolt", "bullet", "dart", "other"]),
685
685
  data: Object.freeze([TEMPLATE_PRIORITY, ...GEAR, QUANTITY]),
686
686
  }),
@@ -42,12 +42,12 @@
42
42
  * @type {Readonly<Record<string, string>>}
43
43
  */
44
44
  export const HM3_DEFAULT_ITEM_ART = Object.freeze({
45
- armor: "systems/hm3/images/icons/svg/armor.svg",
45
+ armorgear: "systems/hm3/images/icons/svg/armor.svg",
46
46
  armorlocation: "systems/hm3/images/icons/svg/anatomy.svg",
47
47
  containergear: "systems/hm3/images/icons/svg/sack.svg",
48
48
  miscgear: "systems/hm3/images/icons/svg/miscgear.svg",
49
49
  mysticalability: "systems/hm3/images/icons/svg/psionics.svg",
50
- projectile: "systems/hm3/images/icons/svg/arrow.svg",
50
+ projectilegear: "systems/hm3/images/icons/svg/arrow.svg",
51
51
  skill: "systems/hm3/images/icons/svg/skills.svg",
52
52
  trauma: "systems/hm3/images/icons/svg/injury.svg",
53
53
  weapongear: "systems/hm3/images/icons/svg/sword.svg",
@@ -89,7 +89,7 @@ export const HM3_TYPE_KEY = "type";
89
89
  export const HM3_DOCUMENT_SUBTYPES = defineDocumentSubtypes({
90
90
  system: "hm3",
91
91
  types: {
92
- armor: { document: "Item", subType: "armorgear" },
92
+ armorgear: { document: "Item", subType: "armorgear" },
93
93
  // HM3-only: SoHL resolves a hit location from a being's own body
94
94
  // structure, so it has no item of this kind and no note type for one.
95
95
  armorlocation: { document: "Item", subType: "armorlocation" },
@@ -97,7 +97,7 @@ export const HM3_DOCUMENT_SUBTYPES = defineDocumentSubtypes({
97
97
  miscgear: { document: "Item", subType: "miscgear" },
98
98
  // The one outright rename. A note calls it a projectile; HM3 calls the
99
99
  // document a missile.
100
- projectile: { document: "Item", subType: "missilegear" },
100
+ projectilegear: { document: "Item", subType: "missilegear" },
101
101
  skill: { document: "Item", subType: "skill" },
102
102
 
103
103
  // ── the four one-to-many rows ───────────────────────────────────────
@@ -127,7 +127,7 @@ const QUANTITY = Object.freeze({
127
127
  * @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
128
128
  */
129
129
  export const HM3_ITEM_FIELDS = Object.freeze({
130
- armor: Object.freeze([...GEAR_COMMON]),
130
+ armorgear: Object.freeze([...GEAR_COMMON]),
131
131
 
132
132
  // The one subtype that extends the Foundry base directly, with none of the
133
133
  // shared templates. Its content-format table has a single row, and that row
@@ -152,7 +152,7 @@ export const HM3_ITEM_FIELDS = Object.freeze({
152
152
  // A note's `projectile` is HM3's `missilegear`; the fields are the gear
153
153
  // template's, and the aspect, impact and range that make it a missile are
154
154
  // authored under `hm3.system`.
155
- projectile: Object.freeze([...GEAR_COMMON, QUANTITY]),
155
+ projectilegear: Object.freeze([...GEAR_COMMON, QUANTITY]),
156
156
 
157
157
  // `hm3.system.type` — "Craft", "Physical", "Communication", "Combat",
158
158
  // "Magic", "Ritual" — has no shared source: the content format states
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "18.0.0",
3
+ "version": "18.1.1",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -76,11 +76,11 @@ export function isBeing(fm) {
76
76
  */
77
77
  export const GEAR_TYPE_TO_KEY = Object.freeze({
78
78
  weapongear: "weapons",
79
- armor: "armor",
80
- projectile: "projectiles",
79
+ armorgear: "armor",
80
+ projectilegear: "projectiles",
81
81
  miscgear: "misc",
82
82
  containergear: "containers",
83
- concoction: "concoctions",
83
+ concoctiongear: "concoctions",
84
84
  });
85
85
 
86
86
  /** Whether a value is a plain mapping. */
@@ -71,14 +71,14 @@ export const SOHL_DOCUMENT_SUBTYPES = defineDocumentSubtypes({
71
71
  types: {
72
72
  affiliation: { document: "Item", subType: "affiliation" },
73
73
  affliction: { document: "Item", subType: "affliction" },
74
- armor: { document: "Item", subType: "armorgear" },
74
+ armorgear: { document: "Item", subType: "armorgear" },
75
75
  attribute: { document: "Item", subType: "attribute" },
76
- concoction: { document: "Item", subType: "concoctiongear" },
76
+ concoctiongear: { document: "Item", subType: "concoctiongear" },
77
77
  containergear: { document: "Item", subType: "containergear" },
78
78
  miscgear: { document: "Item", subType: "miscgear" },
79
79
  mystery: { document: "Item", subType: "mystery" },
80
80
  mysticalability: { document: "Item", subType: "mysticalability" },
81
- projectile: { document: "Item", subType: "projectilegear" },
81
+ projectilegear: { document: "Item", subType: "projectilegear" },
82
82
  skill: { document: "Item", subType: "skill" },
83
83
  trauma: { document: "Item", subType: "trauma" },
84
84
  weapongear: { document: "Item", subType: "weapongear" },
@@ -416,7 +416,7 @@ export const ITEM_FIELDS = Object.freeze({
416
416
  },
417
417
  ]),
418
418
 
419
- armor: Object.freeze([
419
+ armorgear: Object.freeze([
420
420
  ...GEAR_COMMON,
421
421
  {
422
422
  name: "material",
@@ -531,7 +531,7 @@ export const ITEM_FIELDS = Object.freeze({
531
531
  },
532
532
  ]),
533
533
 
534
- concoction: Object.freeze([
534
+ concoctiongear: Object.freeze([
535
535
  ...GEAR_COMMON,
536
536
  {
537
537
  name: "subType",
@@ -674,7 +674,7 @@ export const ITEM_FIELDS = Object.freeze({
674
674
  },
675
675
  ]),
676
676
 
677
- projectile: Object.freeze([
677
+ projectilegear: Object.freeze([
678
678
  ...GEAR_COMMON,
679
679
  {
680
680
  name: "subType",
@@ -244,7 +244,7 @@ const MAP_FIELDS = Object.freeze([
244
244
  * @type {Readonly<Record<string, readonly import("../engine/field-spec.mjs").FieldSpec[]>>}
245
245
  */
246
246
  const PRESENTATION_FIELDS = Object.freeze({
247
- armor: Object.freeze([
247
+ armorgear: Object.freeze([
248
248
  {
249
249
  name: "craft",
250
250
  ...MAP,
@@ -291,7 +291,7 @@ const PRESENTATION_FIELDS = Object.freeze({
291
291
  "The craft that makes it — `{skill, secondary}` — for knowledgebase grouping.",
292
292
  },
293
293
  ]),
294
- projectile: Object.freeze([
294
+ projectilegear: Object.freeze([
295
295
  {
296
296
  name: "craft",
297
297
  ...MAP,
@@ -42,12 +42,26 @@ export function isHomepage(fm: object | null | undefined): boolean;
42
42
  * belong to the caller. This mirrors {@link module:engine/retired-fields},
43
43
  * whose retired-field messages are likewise positioned by whoever reports them.
44
44
  *
45
+ * **A refused field must be one the note *wrote*.** `resolveNoteId` fills
46
+ * `fm.id` **in place** so every downstream reader sees one derived value —
47
+ * deliberately, and documented as such — and this ran over the same object, so
48
+ * a homepage that authors no `id` was told to delete one that is not there
49
+ * (#319). Since the caller already owns the raw note text, it also answers
50
+ * which keys the note actually declared; without an answer every key in `fm`
51
+ * counts, which is the old behaviour and right for a caller holding authored
52
+ * frontmatter only.
53
+ *
45
54
  * @param {object|null|undefined} fm - Parsed frontmatter.
55
+ * @param {object} [options] - Options.
56
+ * @param {(key: string) => boolean} [options.isAuthored] - Whether the note
57
+ * declares this key at its own top level. Defaults to "every key in `fm`".
46
58
  * @returns {Array<{field: string, locator: {key: string, literal?: string},
47
59
  * message: string}>} One entry per finding, empty for any note that is not a
48
60
  * homepage and declares nothing wrong.
49
61
  */
50
- export function checkHomepageAddressFields(fm: object | null | undefined): Array<{
62
+ export function checkHomepageAddressFields(fm: object | null | undefined, { isAuthored }?: {
63
+ isAuthored?: ((key: string) => boolean) | undefined;
64
+ }): Array<{
51
65
  field: string;
52
66
  locator: {
53
67
  key: string;
@@ -201,13 +201,22 @@ export const RETIRED_TYPES: Readonly<Record<string, string>>;
201
201
  * reason: a consumer must be able to adopt the new toolchain before its content
202
202
  * moves, and there are some 31,000 references to move.
203
203
  *
204
- * **Why these three (#78).** `armorgear`, `concoctiongear` and `projectilegear`
205
- * named the *SoHL document subtype* a note happened to compile into rather than
206
- * the thing the note is about. A note's `type` sits outside the `sohl:` and
207
- * `hm3:` blocks precisely because it belongs to no system, and HM3 already
208
- * compiles a `projectile` into a `missilegear` so the suffix was never a fact
209
- * about the note. `weapongear` keeps its name: SoHL and HM3 both call the
210
- * document that, and #78's table has no row for it.
204
+ * **Why these three, and why the other way round now.** #78 renamed
205
+ * `armorgear`, `concoctiongear` and `projectilegear` to `armor`, `concoction`
206
+ * and `projectile`, on the argument that the suffix named the *SoHL document
207
+ * subtype* a note compiled into rather than the thing the note is about. That
208
+ * rename is **reversed**: the gear-suffixed spellings are the vocabulary, and
209
+ * the bare ones are what retires.
210
+ *
211
+ * Nothing had adopted the bare spellings, which is what makes the reversal
212
+ * cheap and the direction obvious: across all five content trees, **every note
213
+ * still authors the suffix** — 331 `armorgear` and 18 `projectilegear` in
214
+ * `sohl`, 71 `concoctiongear` in `thalorna`, and not one note anywhere writes
215
+ * `armor`, `concoction` or `projectile`. The rename produced 349 warnings and
216
+ * zero adopters.
217
+ *
218
+ * `weapongear` never moved, and the reversal restores the consistency its
219
+ * staying put had broken: all five gear types spell the suffix again.
211
220
  *
212
221
  * **Keyed by the retired spelling**, which is the opposite of
213
222
  * `RETIRED_FIELD_ALIASES`. The direction follows the operation: a field alias
@@ -199,7 +199,7 @@ export const NOTE_VOCABULARY: Readonly<{
199
199
  subTypes: readonly string[];
200
200
  data: readonly DataFieldSpec[];
201
201
  }>;
202
- armor: Readonly<{
202
+ armorgear: Readonly<{
203
203
  data: readonly DataFieldSpec[];
204
204
  }>;
205
205
  armorlocation: Readonly<{
@@ -209,7 +209,7 @@ export const NOTE_VOCABULARY: Readonly<{
209
209
  attribute: Readonly<{
210
210
  data: readonly DataFieldSpec[];
211
211
  }>;
212
- concoction: Readonly<{
212
+ concoctiongear: Readonly<{
213
213
  subTypes: readonly string[];
214
214
  data: readonly (DataFieldSpec | Readonly<{
215
215
  describe: "How many of the thing there are; one when unstated.";
@@ -237,7 +237,7 @@ export const NOTE_VOCABULARY: Readonly<{
237
237
  subTypes: readonly string[];
238
238
  data: readonly DataFieldSpec[];
239
239
  }>;
240
- projectile: Readonly<{
240
+ projectilegear: Readonly<{
241
241
  subTypes: readonly string[];
242
242
  data: readonly (DataFieldSpec | Readonly<{
243
243
  describe: "How many of the thing there are; one when unstated.";