@heroiclands/package-build 16.0.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 17.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - fdd7094: **The linter now implements the format it publishes.** `docs/content-format.md`
8
+ and the vocabulary had drifted, silently and in both directions, and nothing
9
+ compared them.
10
+
11
+ **Five documented types reached no schema** — `place`, `lore`, `scenario`,
12
+ `vehicle` and `armorlocation`. A note using one was reported as having no
13
+ schema and then _skipped entirely_: `lintNote` returns after that finding, so
14
+ the note's `data:`, `subType`, references and system block all went
15
+ unexamined. So the types were legal enough to author and not legal enough to
16
+ check, and using one **suppressed** every other check on the note (#231).
17
+
18
+ **Three documented `data` properties reached no vocabulary** — `epithet` and
19
+ `symbol` on `affiliation`, and `lore` on both `affiliation` and `being`. The
20
+ `data:` container is closed, so a note that followed the specification exactly
21
+ was told its property did not exist _and the value was dropped_ rather than
22
+ reaching the page (#232).
23
+
24
+ `peoples` is **removed**, having widened to `lore` — the specification says so
25
+ outright, and no tree authors it: "Nothing is lost by widening: the target's
26
+ own subType already distinguishes a `folk` from a `law`." That removal is the
27
+ reason this is a major; everything else here turns red trees green.
28
+
29
+ Between them, on `sohl-thalorna`: **2,001 findings become 20**. The 1,981 that
30
+ go were never content defects. The 20 that stay are: 18 embedded shortcode
31
+ collisions and two genuinely stale keys.
32
+
33
+ **The specification is executable now.** `tests/content-format-agreement.test.ts`
34
+ parses `docs/content-format.md` and fails if a documented type has no schema or
35
+ no vocabulary, or if a type's declared `data` properties differ in either
36
+ direction from its documented table. Nothing compared the two before, which is
37
+ why this drift lasted.
38
+
39
+ Also corrects the specification itself: `macro`'s `macroType` and `macroScope`
40
+ were tabled as `data` properties, but the compiler reads them with the
41
+ `sohl`-field accessor — the `sohl:` block, then the note's top level — so
42
+ `data.macroType` is not read at all. They are `sohl` properties, and the table
43
+ now says so.
44
+
3
45
  ## 16.0.0
4
46
 
5
47
  ### Major Changes
@@ -1463,7 +1463,13 @@ shipped as content and run by Foundry's own macro runner under the permission
1463
1463
  model that governs every macro in a world. Nothing evaluates, compiles, or
1464
1464
  revives anything; the compiler copies text from a fence into a JSON field.
1465
1465
 
1466
- | `data` property | Values | Description |
1466
+ **Both settings are `sohl` properties, not `data` ones.** The compiler reads
1467
+ them with the same accessor every `sohl` field uses — the `sohl:` block first,
1468
+ then the note's top level — so `data.macroType` is not read, and a macro is not
1469
+ a journal-only note the way `place`, `lore` and `scenario` are: it produces a
1470
+ Foundry **Macro**, and these two describe that document.
1471
+
1472
+ | `sohl` property | Values | Description |
1467
1473
  | --------------- | --------------------------- | -------------------------------------------------------------------- |
1468
1474
  | `macroType` | `script` | The Foundry macro type. Defaults to `script`, and `chat` is an error |
1469
1475
  | `macroScope` | `global \| actors \| actor` | How far the macro reaches. Defaults to `global` |
@@ -34,6 +34,45 @@
34
34
 
35
35
  import { HOMEPAGE_FIELDS, HOMEPAGE_TYPE } from "./homepage.mjs";
36
36
 
37
+ /**
38
+ * Every engine-level content type, and what a note of that type may write.
39
+ *
40
+ * @type {Readonly<Record<string, readonly import("./field-spec.mjs").FieldSpec[]>>}
41
+ */
42
+ /**
43
+ * A note that compiles to a JournalEntry and nothing else.
44
+ *
45
+ * Empty on purpose, and the emptiness is the declaration. `place`, `lore` and
46
+ * `scenario` produce prose — the JournalEntry every note produces — so none of
47
+ * them writes a `sohl:` field, and the authored corpus agrees: across
48
+ * `sohl-thalorna`'s 249 places, 180 lore notes and 21 scenarios, not one
49
+ * carries a `sohl:` block. Everything they *do* declare is `data:`, which is
50
+ * the closed container `engine/note-vocabulary.mjs` holds them to.
51
+ *
52
+ * Declaring the type with no fields is what distinguishes **a type with no
53
+ * vocabulary** from **a type that is unknown** — two different findings, and
54
+ * only the second is a mistake. Until this landed the specification declared
55
+ * all three and nothing implemented them, so a note using one was reported as
56
+ * having no schema and then *skipped entirely*: `lintNote` returns after that
57
+ * finding, so the note's `data:`, `subType`, references and system block all
58
+ * went unexamined (#231).
59
+ *
60
+ * @type {readonly import("./field-spec.mjs").FieldSpec[]}
61
+ */
62
+ const JOURNAL_ONLY_FIELDS = Object.freeze([]);
63
+
64
+ /**
65
+ * An `armorlocation` note — an HM3 item, and no part of the SoHL registry.
66
+ *
67
+ * It lives here rather than in `sohl/` for the reachability reason that
68
+ * governs this whole file: a package declaring no `itemBuilders` never loads
69
+ * the SoHL registry, and HM3 packages are exactly the ones that author this
70
+ * type.
71
+ *
72
+ * @type {readonly import("./field-spec.mjs").FieldSpec[]}
73
+ */
74
+ const ARMORLOCATION_FIELDS = Object.freeze([]);
75
+
37
76
  /**
38
77
  * Every engine-level content type, and what a note of that type may write.
39
78
  *
@@ -41,4 +80,8 @@ import { HOMEPAGE_FIELDS, HOMEPAGE_TYPE } from "./homepage.mjs";
41
80
  */
42
81
  export const ENGINE_NOTE_SCHEMAS = Object.freeze({
43
82
  [HOMEPAGE_TYPE]: HOMEPAGE_FIELDS,
83
+ place: JOURNAL_ONLY_FIELDS,
84
+ lore: JOURNAL_ONLY_FIELDS,
85
+ scenario: JOURNAL_ONLY_FIELDS,
86
+ armorlocation: ARMORLOCATION_FIELDS,
44
87
  });
@@ -395,7 +395,13 @@ export const NOTE_VOCABULARY = Object.freeze({
395
395
  { name: "archetypes", ...LIST, describe: "Archetypal behaviours the being fits." },
396
396
  { name: "occupation", ...TEXT, describe: "What the being does for a living." },
397
397
  { name: "stations", ...LINKS, describe: "Stations the being holds." },
398
- { name: "peoples", ...LINKS, describe: "Peoples the being belongs to." },
398
+ {
399
+ name: "lore",
400
+ ...LINKS,
401
+ describe:
402
+ "Lore concerning this being — the people it is of, the standing it " +
403
+ "holds, the law it lives under.",
404
+ },
399
405
  { name: "homes", ...LINKS, describe: "Places the being calls home." },
400
406
  {
401
407
  name: "affiliations",
@@ -425,6 +431,14 @@ export const NOTE_VOCABULARY = Object.freeze({
425
431
  ]),
426
432
  }),
427
433
 
434
+ vehicle: Object.freeze({
435
+ subTypes: null,
436
+ data: Object.freeze([
437
+ { name: "portrait", ...TEXT, describe: "Path to the portrait image." },
438
+ TEMPLATE_PRIORITY,
439
+ ]),
440
+ }),
441
+
428
442
  /* ----- items ---------------------------------------------------- */
429
443
 
430
444
  affiliation: Object.freeze({
@@ -448,6 +462,18 @@ export const NOTE_VOCABULARY = Object.freeze({
448
462
  ...TEXT,
449
463
  describe: "What one member is called — a Vylarian.",
450
464
  },
465
+ {
466
+ name: "epithet",
467
+ ...TEXT,
468
+ describe: "The by-name it is known by — a god's, an order's, a company's.",
469
+ },
470
+ {
471
+ name: "symbol",
472
+ ...TEXT,
473
+ describe:
474
+ "Its emblem in words: a feather atop a golden scale, a chisel carving " +
475
+ "a star.",
476
+ },
451
477
  {
452
478
  name: "governance.model",
453
479
  ...TEXT,
@@ -481,7 +507,13 @@ export const NOTE_VOCABULARY = Object.freeze({
481
507
  ...LINKS,
482
508
  describe: "What its economic life runs on — currencies, banking bodies, goods.",
483
509
  },
484
- { name: "peoples", ...LINKS, describe: "Peoples associated with it." },
510
+ {
511
+ name: "lore",
512
+ ...LINKS,
513
+ describe:
514
+ "Lore concerning it — the peoples it draws on, the god a faith " +
515
+ "venerates, its law, its calendar.",
516
+ },
485
517
  { name: "parents", ...LINKS, describe: "Affiliations it is subordinate to." },
486
518
  {
487
519
  name: "relations",
@@ -532,6 +564,11 @@ export const NOTE_VOCABULARY = Object.freeze({
532
564
  data: Object.freeze([TEMPLATE_PRIORITY, ...GEAR]),
533
565
  }),
534
566
 
567
+ armorlocation: Object.freeze({
568
+ subTypes: null,
569
+ data: Object.freeze([TEMPLATE_PRIORITY]),
570
+ }),
571
+
535
572
  attribute: Object.freeze({
536
573
  data: Object.freeze([TEMPLATE_PRIORITY]),
537
574
  }),
@@ -674,6 +711,86 @@ export const NOTE_VOCABULARY = Object.freeze({
674
711
 
675
712
  macro: Object.freeze({ data: Object.freeze([]) }),
676
713
 
714
+ lore: Object.freeze({
715
+ subTypes: Object.freeze([
716
+ "cosmology",
717
+ "deity",
718
+ "theology",
719
+ "arcana",
720
+ "spirit",
721
+ "economy",
722
+ "law",
723
+ "calendar",
724
+ "history",
725
+ "material",
726
+ "folk",
727
+ "culture",
728
+ "bestiary",
729
+ ]),
730
+ // Nothing of its own: a lore note is prose, and what it *is* about is
731
+ // its subType. The specification declares an empty table for it, and
732
+ // the authored corpus writes no `data:` key on any of the 180.
733
+ data: Object.freeze([]),
734
+ }),
735
+
736
+ place: Object.freeze({
737
+ subTypes: Object.freeze(["world", "region", "settlement", "site", "structure", "feature"]),
738
+ data: Object.freeze([
739
+ {
740
+ name: "demonym",
741
+ ...TEXT,
742
+ describe: "What a person from this place is called — a Vylarian.",
743
+ },
744
+ {
745
+ name: "lore",
746
+ ...LINKS,
747
+ describe:
748
+ "Lore concerning this place — its peoples, its law, its calendar, " +
749
+ "its history.",
750
+ },
751
+ {
752
+ name: "parents",
753
+ ...LINKS,
754
+ describe: "Enclosing places this one sits within.",
755
+ },
756
+ {
757
+ name: "population",
758
+ ...NUM,
759
+ describe: "Approximate population, to two significant digits.",
760
+ },
761
+ ]),
762
+ }),
763
+
764
+ scenario: Object.freeze({
765
+ subTypes: Object.freeze(["campaign", "adventure", "encounter"]),
766
+ data: Object.freeze([
767
+ { name: "parents", ...LINKS, describe: "Scenarios this one sits within." },
768
+ { name: "locations", ...LINKS, describe: "Places the scenario takes place in." },
769
+ { name: "cast", ...LINKS, describe: "Beings who appear in it." },
770
+ { name: "factions", ...LINKS, describe: "Affiliations with a stake in it." },
771
+ {
772
+ name: "follows",
773
+ ...LINKS,
774
+ describe: "Scenarios that should be played before this one.",
775
+ },
776
+ {
777
+ name: "status",
778
+ ...TEXT,
779
+ describe: "`draft`, `playtested` or `published`.",
780
+ },
781
+ {
782
+ name: "party.size",
783
+ ...TEXT,
784
+ describe: "`solo`, `small`, `standard`, `large` or `host`.",
785
+ },
786
+ {
787
+ name: "party.archetypes",
788
+ ...LIST,
789
+ describe: "Archetypes the scenario is written for.",
790
+ },
791
+ ]),
792
+ }),
793
+
677
794
  homepage: Object.freeze({ data: Object.freeze([]) }),
678
795
 
679
796
  map: Object.freeze({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "16.0.0",
3
+ "version": "17.0.0",
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",
@@ -87,6 +87,22 @@ const MACRO_FIELDS = Object.freeze([
87
87
  *
88
88
  * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
89
89
  */
90
+ /**
91
+ * A `vehicle` note — a conveyance that carries goods and people.
92
+ *
93
+ * Empty, and the emptiness is the declaration: the specification gives a
94
+ * vehicle two `data` properties and no `sohl:` field of its own, and the
95
+ * closed `data:` container is where `engine/note-vocabulary.mjs` holds them.
96
+ * Declaring the type with no fields distinguishes a type with no vocabulary
97
+ * from a type that is unknown, which are different findings (#231).
98
+ *
99
+ * It is here rather than in the engine because a vehicle is a SoHL actor —
100
+ * the specification maps it to `sohl` and marks it NA for hm3.
101
+ *
102
+ * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
103
+ */
104
+ const VEHICLE_FIELDS = Object.freeze([]);
105
+
90
106
  const BEING_FIELDS = Object.freeze([
91
107
  {
92
108
  name: "body",
@@ -330,5 +346,6 @@ export const NOTE_SCHEMAS = Object.freeze({
330
346
  doc: DOC_FIELDS,
331
347
  macro: MACRO_FIELDS,
332
348
  being: Object.freeze([...BEING_FIELDS, ...PRESENTATION_FIELDS.being]),
349
+ vehicle: VEHICLE_FIELDS,
333
350
  map: MAP_FIELDS,
334
351
  });
@@ -187,6 +187,10 @@ export const NOTE_VOCABULARY: Readonly<{
187
187
  subTypes: null;
188
188
  data: readonly DataFieldSpec[];
189
189
  }>;
190
+ vehicle: Readonly<{
191
+ subTypes: null;
192
+ data: readonly DataFieldSpec[];
193
+ }>;
190
194
  affiliation: Readonly<{
191
195
  subTypes: readonly string[];
192
196
  data: readonly DataFieldSpec[];
@@ -198,6 +202,10 @@ export const NOTE_VOCABULARY: Readonly<{
198
202
  armorgear: Readonly<{
199
203
  data: readonly DataFieldSpec[];
200
204
  }>;
205
+ armorlocation: Readonly<{
206
+ subTypes: null;
207
+ data: readonly DataFieldSpec[];
208
+ }>;
201
209
  attribute: Readonly<{
202
210
  data: readonly DataFieldSpec[];
203
211
  }>;
@@ -256,6 +264,48 @@ export const NOTE_VOCABULARY: Readonly<{
256
264
  macro: Readonly<{
257
265
  data: readonly never[];
258
266
  }>;
267
+ lore: Readonly<{
268
+ subTypes: readonly string[];
269
+ data: readonly never[];
270
+ }>;
271
+ place: Readonly<{
272
+ subTypes: readonly string[];
273
+ data: readonly ({
274
+ describe: string;
275
+ shape: "string";
276
+ kind: "string";
277
+ name: string;
278
+ } | {
279
+ describe: string;
280
+ shape: "list of wikilinks";
281
+ kind: "list";
282
+ name: string;
283
+ } | {
284
+ describe: string;
285
+ shape: "number";
286
+ kind: "number";
287
+ name: string;
288
+ })[];
289
+ }>;
290
+ scenario: Readonly<{
291
+ subTypes: readonly string[];
292
+ data: readonly ({
293
+ describe: string;
294
+ shape: "list of wikilinks";
295
+ kind: "list";
296
+ name: string;
297
+ } | {
298
+ describe: string;
299
+ shape: "string";
300
+ kind: "string";
301
+ name: string;
302
+ } | {
303
+ describe: string;
304
+ shape: "list";
305
+ kind: "list";
306
+ name: string;
307
+ })[];
308
+ }>;
259
309
  homepage: Readonly<{
260
310
  data: readonly never[];
261
311
  }>;