@heroiclands/package-build 9.0.0 → 10.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +694 -0
  2. package/CONTENT.md +273 -13
  3. package/bin/content-build.mjs +437 -7
  4. package/content-config.mjs +259 -28
  5. package/docs/content-format.md +1418 -0
  6. package/engine/address-charset.mjs +62 -0
  7. package/engine/alias-index.mjs +153 -0
  8. package/engine/base-compiler.mjs +194 -4
  9. package/engine/content-address.mjs +4 -4
  10. package/engine/content-format-check.mjs +570 -0
  11. package/engine/content-format.mjs +253 -0
  12. package/engine/content-links.mjs +132 -56
  13. package/engine/content-lint.mjs +8 -1
  14. package/engine/diagnostics.mjs +33 -0
  15. package/engine/document-subtypes.mjs +440 -0
  16. package/engine/field-spec.mjs +49 -43
  17. package/engine/frontmatter-lint.mjs +343 -27
  18. package/engine/generate.mjs +32 -4
  19. package/engine/helpers.mjs +41 -29
  20. package/engine/ids.mjs +19 -1
  21. package/engine/index.mjs +15 -0
  22. package/engine/item-registry.mjs +72 -5
  23. package/engine/kb-manifest.mjs +36 -7
  24. package/engine/map-notes.mjs +34 -18
  25. package/engine/note-claims.mjs +383 -0
  26. package/engine/note-vocabulary.mjs +678 -0
  27. package/engine/pack-config.mjs +39 -22
  28. package/engine/pack-router.mjs +17 -6
  29. package/engine/prose-lint.mjs +55 -3
  30. package/engine/retired-fields.mjs +117 -3
  31. package/engine/scenes.mjs +19 -1
  32. package/engine/schema-check.mjs +347 -3
  33. package/engine/site-build.mjs +1 -1
  34. package/engine/site-index.mjs +38 -21
  35. package/engine/system-block.mjs +513 -0
  36. package/engine/web-wikilinks.mjs +112 -80
  37. package/engine/wikilink-syntax.mjs +30 -0
  38. package/engine/wikilinks.mjs +67 -51
  39. package/package.json +6 -2
  40. package/sohl/actors.mjs +249 -36
  41. package/sohl/document-subtypes.mjs +82 -0
  42. package/sohl/index.mjs +3 -0
  43. package/sohl/items.mjs +110 -14
  44. package/sohl/note-schemas.mjs +11 -7
  45. package/types/content-config.d.mts +48 -4
  46. package/types/engine/address-charset.d.mts +45 -0
  47. package/types/engine/alias-index.d.mts +122 -0
  48. package/types/engine/base-compiler.d.mts +132 -4
  49. package/types/engine/content-address.d.mts +2 -2
  50. package/types/engine/content-format-check.d.mts +163 -0
  51. package/types/engine/content-format.d.mts +101 -0
  52. package/types/engine/content-links.d.mts +16 -1
  53. package/types/engine/content-lint.d.mts +6 -0
  54. package/types/engine/diagnostics.d.mts +29 -0
  55. package/types/engine/document-subtypes.d.mts +233 -0
  56. package/types/engine/field-spec.d.mts +76 -23
  57. package/types/engine/frontmatter-lint.d.mts +47 -2
  58. package/types/engine/generate.d.mts +14 -1
  59. package/types/engine/helpers.d.mts +21 -13
  60. package/types/engine/ids.d.mts +10 -0
  61. package/types/engine/index.d.mts +5 -0
  62. package/types/engine/item-registry.d.mts +21 -2
  63. package/types/engine/kb-manifest.d.mts +35 -8
  64. package/types/engine/map-notes.d.mts +21 -11
  65. package/types/engine/note-claims.d.mts +113 -0
  66. package/types/engine/note-vocabulary.d.mts +251 -0
  67. package/types/engine/pack-config.d.mts +4 -3
  68. package/types/engine/pack-router.d.mts +4 -4
  69. package/types/engine/prose-lint.d.mts +6 -2
  70. package/types/engine/retired-fields.d.mts +73 -2
  71. package/types/engine/schema-check.d.mts +182 -0
  72. package/types/engine/system-block.d.mts +281 -0
  73. package/types/engine/web-wikilinks.d.mts +23 -12
  74. package/types/engine/wikilink-syntax.d.mts +29 -0
  75. package/types/sohl/actors.d.mts +62 -6
  76. package/types/sohl/document-subtypes.d.mts +14 -0
  77. package/types/sohl/index.d.mts +1 -0
  78. package/types/sohl/items.d.mts +21 -0
@@ -66,9 +66,10 @@
66
66
 
67
67
  import path from "node:path";
68
68
 
69
- // A leaf with no local imports of its own, so naming it here cannot close a
70
- // cycle around a consumer's config file (see `engine/pack-config.mjs`).
71
- import { MAP_TYPES } from "./engine/ids.mjs";
69
+ // Leaves with no local imports of their own, so naming them here cannot close
70
+ // a cycle around a consumer's config file (see `engine/pack-config.mjs`).
71
+ import { isAddressSegment } from "./engine/address-charset.mjs";
72
+ import { MAP_TYPES, PACK_BY_TYPE } from "./engine/ids.mjs";
72
73
 
73
74
  /**
74
75
  * The two kinds of Foundry package a content module can be built into. The
@@ -470,6 +471,18 @@ export function publishesContentPages(config) {
470
471
  * @typedef {((fm: object) => object)|{system: (fm: object) => object, img?: string, fields?: readonly object[]}} ItemBuilderEntry
471
472
  */
472
473
 
474
+ /**
475
+ * One **registry** of a declared set, and the system it belongs to (#58).
476
+ *
477
+ * A repository shipping content for two systems declares one of these per
478
+ * system: the accepted type vocabulary is their union, and a type both declare
479
+ * keeps a builder on each side rather than one of them winning in silence.
480
+ *
481
+ * @typedef {object} ItemRegistrySpec
482
+ * @property {string} system The system id whose vocabulary this registry is.
483
+ * @property {Record<string, ItemBuilderEntry>} builders The registry itself.
484
+ */
485
+
473
486
  /**
474
487
  * The configuration a consumer writes.
475
488
  *
@@ -485,7 +498,8 @@ export function publishesContentPages(config) {
485
498
  * `system.json` / `module.json`.
486
499
  * @property {PackageKind} packageKind Whether the package is a system or a module.
487
500
  * @property {StatsSpec} stats Identity stamped into every document's `_stats`.
488
- * @property {Record<string, ItemBuilderEntry>} [itemBuilders] The consumer's
501
+ * @property {Record<string, ItemBuilderEntry>|readonly ItemRegistrySpec[]} [itemBuilders]
502
+ * The consumer's
489
503
  * item-type registry: each content `type`
490
504
  * that compiles into an Item, paired with
491
505
  * the builder producing its `system` block
@@ -493,7 +507,11 @@ export function publishesContentPages(config) {
493
507
  * note of that type gets when it sets no
494
508
  * `img:` of its own. Default `{}` — a
495
509
  * content module that ships no items
496
- * declares none.
510
+ * declares none. A repository feeding
511
+ * two systems declares a **list** of
512
+ * `{ system, builders }` registries
513
+ * instead, and the accepted type
514
+ * vocabulary is their union (#58).
497
515
  * @property {PackSpec[]} packs Packs to compile. More than one entry
498
516
  * may share a `type`: a note then names
499
517
  * the pack it belongs in with its
@@ -547,10 +565,24 @@ export function publishesContentPages(config) {
547
565
  * declared. Sparse, like `itemArt` — a type
548
566
  * absent here compiles normally and is
549
567
  * simply undocumented (#22).
568
+ * @property {Readonly<Record<string, Readonly<Record<string, Function>>>>} itemBuildersBySystem
569
+ * Derived: the same builders, kept per
570
+ * declaring system. `{}` for the single
571
+ * registry form, which names no system
572
+ * (#58).
573
+ * @property {Readonly<Record<string, Readonly<Record<string, string>>>>} itemArtBySystem
574
+ * Derived: the default art, per system.
575
+ * @property {Readonly<Record<string, Readonly<Record<string, readonly object[]>>>>} itemFieldsBySystem
576
+ * Derived: the declared fields, per system.
577
+ * @property {ReadonlySet<string>} itemTypesBySeveralSystems Derived: the types
578
+ * more than one registry declares — the
579
+ * ones the flat tables cannot answer for
580
+ * without choosing a system for the caller.
550
581
  * @property {ReadonlySet<string>} itemTypes Derived: the keys of
551
582
  * {@link ContentBuildConfigInput.itemBuilders},
552
- * so the accepted item types and the builder
553
- * table are one list (#1504).
583
+ * unioned across every declared registry, so
584
+ * the accepted item types and the builder
585
+ * tables are one list (#1504).
554
586
  * @property {ReadonlySet<string>} docEntryTypes Derived: every type whose prose
555
587
  * compiles into a JournalEntry of its own —
556
588
  * the item types, plus `macro`, plus the map
@@ -610,6 +642,7 @@ const DOC_PAGE_KEYS = ["title", "out", "preamble"];
610
642
  const RELATIONSHIP_KINDS = ["systems", "requires", "recommends", "conflicts"];
611
643
  const RELATIONSHIP_KEYS = ["id", "type", "manifest", "compatibility", "itemCatalog"];
612
644
  const ITEM_BUILDER_KEYS = ["system", "img", "fields"];
645
+ const ITEM_REGISTRY_KEYS = ["system", "builders"];
613
646
  const PACK_KEYS = [
614
647
  "name",
615
648
  "type",
@@ -695,6 +728,65 @@ function requireNonEmptyString(value, field) {
695
728
  return /** @type {string} */ (value);
696
729
  }
697
730
 
731
+ /**
732
+ * The `contentPackage`, checked against the two rules an address puts on it.
733
+ *
734
+ * It is the first segment of every canonical address this repository publishes
735
+ * (`sohl-skill-clmb`), and an address is read by counting hyphen-separated
736
+ * segments. So the value carries two obligations that the rest of the
737
+ * configuration does not, and #59 asks for both to be **enforced rather than
738
+ * assumed** — the alternative is a package whose addresses are simply
739
+ * unreadable, reported nowhere and discovered as links that resolve to nothing.
740
+ *
741
+ * 1. _Alphanumeric_, so the hyphen stays purely a separator. `harn-adventures`
742
+ * was the one violator, and its keys read as four segments and failed as a
743
+ * `null` return from `readCanonicalKey` — a silence, not an error.
744
+ * 2. _Not a note type_, because the package and the type are adjacent segments
745
+ * drawn from two vocabularies. Keeping them disjoint is what lets a reader
746
+ * take a name at face value instead of deciding which slot it is filling.
747
+ * One such collision is structural and cannot be fixed — `sohl` is both a
748
+ * content package and a system id, because Foundry requires a system
749
+ * package's id to *be* its system id — which is the reason to prevent the
750
+ * ones that are avoidable.
751
+ *
752
+ * @param {unknown} value - The configured `contentPackage`.
753
+ * @param {ReadonlySet<string>} docEntryTypes - Every type whose prose compiles
754
+ * to a documentation entry: the item types plus `macro` and the map types.
755
+ * With {@link PACK_BY_TYPE} and the `doc`-prefixed forms, this is the whole
756
+ * type vocabulary an address may write.
757
+ * @returns {string} The value, unchanged.
758
+ */
759
+ function requireContentPackage(value, docEntryTypes) {
760
+ const pkg = requireNonEmptyString(value, "contentPackage");
761
+ if (!isAddressSegment(pkg)) {
762
+ fail(
763
+ "contentPackage",
764
+ `is \`${pkg}\`, which is not alphanumeric. It is the first ` +
765
+ `segment of every address this package publishes ` +
766
+ `(\`${pkg}-<type>-<shortcode>\`), and an address is read by ` +
767
+ `counting hyphen-separated segments — so anything outside ` +
768
+ "`[A-Za-z0-9]` here makes those addresses unreadable rather " +
769
+ "than merely ugly. `harn-adventures` became `harnadventures`",
770
+ );
771
+ }
772
+ const typeNames = new Set([
773
+ ...Object.keys(PACK_BY_TYPE),
774
+ ...docEntryTypes,
775
+ ...[...docEntryTypes].map((type) => `doc${type}`),
776
+ ]);
777
+ if (typeNames.has(pkg)) {
778
+ fail(
779
+ "contentPackage",
780
+ `is \`${pkg}\`, which is also a note type — \`${pkg}-<shortcode>\` ` +
781
+ "already addresses one. The package and the type are adjacent " +
782
+ "segments of an address, and the two vocabularies are kept " +
783
+ "disjoint so a reader never has to decide which slot a name " +
784
+ "is filling. Rename the package",
785
+ );
786
+ }
787
+ return pkg;
788
+ }
789
+
698
790
  /**
699
791
  * @param {unknown} value
700
792
  * @param {string} field
@@ -1360,19 +1452,18 @@ function normalizePackageBuild(value) {
1360
1452
  * Art now travels with the builder it belongs to, which is the one place a type
1361
1453
  * is already declared.
1362
1454
  *
1363
- * @param {unknown} value
1364
- * @returns {{itemBuilders: Readonly<Record<string, Function>>,
1365
- * itemArt: Readonly<Record<string, string>>}}
1455
+ * @param {unknown} value - One registry: type → entry.
1456
+ * @param {string} at - The configuration path to report against.
1457
+ * @returns {{itemBuilders: Record<string, Function>,
1458
+ * itemArt: Record<string, string>,
1459
+ * itemFields: Record<string, readonly object[]>}}
1366
1460
  * The `system` builder for each type, and the default art for those types
1367
1461
  * that paired one. The art table is deliberately *sparse*: a bare-function
1368
1462
  * entry contributes no key, which is what distinguishes "no default art" from
1369
1463
  * an empty one.
1370
1464
  */
1371
- function normalizeItemBuilders(value) {
1372
- if (value === undefined) {
1373
- return { itemBuilders: Object.freeze({}), itemArt: Object.freeze({}) };
1374
- }
1375
- if (!isPlainObject(value)) fail("itemBuilders", "must be an object");
1465
+ function normalizeOneRegistry(value, at) {
1466
+ if (!isPlainObject(value)) fail(at, "must be an object");
1376
1467
  const input = /** @type {Record<string, unknown>} */ (value);
1377
1468
 
1378
1469
  /** @type {Record<string, Function>} */
@@ -1389,43 +1480,167 @@ function normalizeItemBuilders(value) {
1389
1480
  }
1390
1481
  if (!isPlainObject(entry)) {
1391
1482
  fail(
1392
- `itemBuilders.${type}`,
1483
+ `${at}.${type}`,
1393
1484
  "must be a builder function, or an object with a `system` builder",
1394
1485
  );
1395
1486
  }
1396
1487
  const paired = /** @type {Record<string, unknown>} */ (entry);
1397
- rejectUnknownKeys(paired, ITEM_BUILDER_KEYS, `itemBuilders.${type}.`);
1488
+ rejectUnknownKeys(paired, ITEM_BUILDER_KEYS, `${at}.${type}.`);
1398
1489
  if (typeof paired.system !== "function") {
1399
- fail(`itemBuilders.${type}.system`, "must be a function");
1490
+ fail(`${at}.${type}.system`, "must be a function");
1400
1491
  }
1401
1492
  itemBuilders[type] = /** @type {Function} */ (paired.system);
1402
1493
  if (paired.img !== undefined) {
1403
- itemArt[type] = requireNonEmptyString(paired.img, `itemBuilders.${type}.img`);
1494
+ itemArt[type] = requireNonEmptyString(paired.img, `${at}.${type}.img`);
1404
1495
  }
1405
1496
  if (paired.fields !== undefined) {
1406
1497
  if (!Array.isArray(paired.fields)) {
1407
- fail(`itemBuilders.${type}.fields`, "must be an array");
1498
+ fail(`${at}.${type}.fields`, "must be an array");
1408
1499
  }
1409
1500
  for (const [index, field] of paired.fields.entries()) {
1410
1501
  if (!isPlainObject(field)) {
1411
- fail(
1412
- `itemBuilders.${type}.fields[${index}]`,
1413
- "must be a field declaration object",
1414
- );
1502
+ fail(`${at}.${type}.fields[${index}]`, "must be a field declaration object");
1415
1503
  }
1416
1504
  requireNonEmptyString(
1417
1505
  /** @type {Record<string, unknown>} */ (field).to,
1418
- `itemBuilders.${type}.fields[${index}].to`,
1506
+ `${at}.${type}.fields[${index}].to`,
1419
1507
  );
1420
1508
  }
1421
1509
  itemFields[type] = Object.freeze([...paired.fields]);
1422
1510
  }
1423
1511
  }
1424
1512
 
1513
+ return { itemBuilders, itemArt, itemFields };
1514
+ }
1515
+
1516
+ /**
1517
+ * The declared item-builder registries, and the vocabulary their union gives
1518
+ * (#58).
1519
+ *
1520
+ * **One registry is a ceiling, not a default.** The accepted type list is the
1521
+ * registry's keys, which is what makes a type impossible to accept without a
1522
+ * builder behind it (#1504) — and, with one registry, impossible to accept a
1523
+ * type a *second* system declares. A tree feeding two systems has both:
1524
+ * `spell`, `invocation` and `psionic` are HM3's, `mysticalability` and
1525
+ * `projectilegear` are SoHL's, and `skill` is both systems' under one name and
1526
+ * two data models.
1527
+ *
1528
+ * So `itemBuilders` accepts either form:
1529
+ *
1530
+ * - **A registry** — `{ skill: fn, … }`. Unchanged, and what every existing
1531
+ * configuration declares. It names no system, because there is only one.
1532
+ * - **A list of registries** — `[{ system: "sohl", builders: {…} }, …]`. The
1533
+ * vocabulary is the **union** of their keys; a type more than one declares
1534
+ * keeps a builder per system, so nothing is chosen for the build silently.
1535
+ *
1536
+ * The **flat** tables — `itemBuilders`, `itemArt`, `itemFields` — are the union
1537
+ * with the first declaring registry winning a collision. They answer a
1538
+ * single-system build, where a collision cannot arise; a build with two systems
1539
+ * asks by system, and `itemTypesBySeveralSystems` names the types where asking
1540
+ * flatly would be answering the wrong question. See `engine/item-registry.mjs`,
1541
+ * which refuses exactly those without a system.
1542
+ *
1543
+ * @param {unknown} value - The declared `itemBuilders`.
1544
+ * @returns {{itemBuilders: Readonly<Record<string, Function>>,
1545
+ * itemArt: Readonly<Record<string, string>>,
1546
+ * itemFields: Readonly<Record<string, readonly object[]>>,
1547
+ * itemBuildersBySystem: Readonly<Record<string, Readonly<Record<string, Function>>>>,
1548
+ * itemArtBySystem: Readonly<Record<string, Readonly<Record<string, string>>>>,
1549
+ * itemFieldsBySystem: Readonly<Record<string, Readonly<Record<string, readonly object[]>>>>,
1550
+ * itemTypesBySeveralSystems: ReadonlySet<string>}}
1551
+ * The flat tables, the per-system ones, and the contested types.
1552
+ */
1553
+ function normalizeItemBuilders(value) {
1554
+ const empty = Object.freeze({});
1555
+ if (value === undefined) {
1556
+ return {
1557
+ itemBuilders: empty,
1558
+ itemArt: empty,
1559
+ itemFields: empty,
1560
+ itemBuildersBySystem: empty,
1561
+ itemArtBySystem: empty,
1562
+ itemFieldsBySystem: empty,
1563
+ itemTypesBySeveralSystems: Object.freeze(new Set()),
1564
+ };
1565
+ }
1566
+
1567
+ /** @type {{system: string|null, tables: ReturnType<typeof normalizeOneRegistry>}[]} */
1568
+ const registries = [];
1569
+
1570
+ if (Array.isArray(value)) {
1571
+ const seen = new Set();
1572
+ for (const [index, entry] of value.entries()) {
1573
+ const at = `itemBuilders[${index}]`;
1574
+ if (!isPlainObject(entry)) {
1575
+ fail(
1576
+ at,
1577
+ "must be `{ system, builders }` — a registry and the system it belongs to",
1578
+ );
1579
+ }
1580
+ const declared = /** @type {Record<string, unknown>} */ (entry);
1581
+ rejectUnknownKeys(declared, ITEM_REGISTRY_KEYS, `${at}.`);
1582
+ const system = requireNonEmptyString(declared.system, `${at}.system`);
1583
+ if (seen.has(system)) {
1584
+ fail(
1585
+ at,
1586
+ `declares a second registry for \`${system}\` — a system has one ` +
1587
+ `item vocabulary, so merge them at their source`,
1588
+ );
1589
+ }
1590
+ seen.add(system);
1591
+ registries.push({
1592
+ system,
1593
+ tables: normalizeOneRegistry(declared.builders, `${at}.builders`),
1594
+ });
1595
+ }
1596
+ } else {
1597
+ registries.push({ system: null, tables: normalizeOneRegistry(value, "itemBuilders") });
1598
+ }
1599
+
1600
+ /** @type {Record<string, Function>} */
1601
+ const itemBuilders = {};
1602
+ /** @type {Record<string, string>} */
1603
+ const itemArt = {};
1604
+ /** @type {Record<string, readonly object[]>} */
1605
+ const itemFields = {};
1606
+ /** @type {Record<string, Readonly<Record<string, Function>>>} */
1607
+ const itemBuildersBySystem = {};
1608
+ /** @type {Record<string, Readonly<Record<string, string>>>} */
1609
+ const itemArtBySystem = {};
1610
+ /** @type {Record<string, Readonly<Record<string, readonly object[]>>>} */
1611
+ const itemFieldsBySystem = {};
1612
+ /** @type {Map<string, number>} */
1613
+ const declaringSystems = new Map();
1614
+
1615
+ for (const { system, tables } of registries) {
1616
+ for (const [type, builder] of Object.entries(tables.itemBuilders)) {
1617
+ declaringSystems.set(type, (declaringSystems.get(type) ?? 0) + 1);
1618
+ if (!(type in itemBuilders)) itemBuilders[type] = builder;
1619
+ }
1620
+ for (const [type, art] of Object.entries(tables.itemArt)) {
1621
+ if (!(type in itemArt)) itemArt[type] = art;
1622
+ }
1623
+ for (const [type, fields] of Object.entries(tables.itemFields)) {
1624
+ if (!(type in itemFields)) itemFields[type] = fields;
1625
+ }
1626
+ if (system === null) continue;
1627
+ itemBuildersBySystem[system] = Object.freeze(tables.itemBuilders);
1628
+ itemArtBySystem[system] = Object.freeze(tables.itemArt);
1629
+ itemFieldsBySystem[system] = Object.freeze(tables.itemFields);
1630
+ }
1631
+
1425
1632
  return {
1426
1633
  itemBuilders: Object.freeze(itemBuilders),
1427
1634
  itemArt: Object.freeze(itemArt),
1428
1635
  itemFields: Object.freeze(itemFields),
1636
+ itemBuildersBySystem: Object.freeze(itemBuildersBySystem),
1637
+ itemArtBySystem: Object.freeze(itemArtBySystem),
1638
+ itemFieldsBySystem: Object.freeze(itemFieldsBySystem),
1639
+ itemTypesBySeveralSystems: Object.freeze(
1640
+ new Set(
1641
+ [...declaringSystems.entries()].filter(([, count]) => count > 1).map(([t]) => t),
1642
+ ),
1643
+ ),
1429
1644
  };
1430
1645
  }
1431
1646
 
@@ -1660,12 +1875,24 @@ export function defineConfig(config) {
1660
1875
 
1661
1876
  const foundryPackage = requireNonEmptyString(input.foundryPackage, "foundryPackage");
1662
1877
 
1663
- const { itemBuilders, itemArt, itemFields } = normalizeItemBuilders(input.itemBuilders);
1878
+ const {
1879
+ itemBuilders,
1880
+ itemArt,
1881
+ itemFields,
1882
+ itemBuildersBySystem,
1883
+ itemArtBySystem,
1884
+ itemFieldsBySystem,
1885
+ itemTypesBySeveralSystems,
1886
+ } = normalizeItemBuilders(input.itemBuilders);
1887
+ // The union across every declared registry (#58) — the flat table already
1888
+ // holds every key any of them declares, so this stays "the registry's keys"
1889
+ // rather than becoming a second list to keep in step (#1504).
1664
1890
  const itemTypes = Object.freeze(new Set(Object.keys(itemBuilders)));
1891
+ const docEntryTypes = Object.freeze(new Set([...itemTypes, "macro", ...MAP_TYPES]));
1665
1892
 
1666
1893
  return Object.freeze({
1667
1894
  rootDir,
1668
- contentPackage: requireNonEmptyString(input.contentPackage, "contentPackage"),
1895
+ contentPackage: requireContentPackage(input.contentPackage, docEntryTypes),
1669
1896
  foundryPackage,
1670
1897
  packageKind: /** @type {PackageKind} */ (packageKind),
1671
1898
  // Foundry serves a package's files from `<kind>/<id>/`, so this is the
@@ -1712,6 +1939,10 @@ export function defineConfig(config) {
1712
1939
  itemBuilders,
1713
1940
  itemArt,
1714
1941
  itemFields,
1942
+ itemBuildersBySystem,
1943
+ itemArtBySystem,
1944
+ itemFieldsBySystem,
1945
+ itemTypesBySeveralSystems,
1715
1946
  // Resolved once, here, and read everywhere through
1716
1947
  // `loadPackConfig()`. The doc-entry *concept* is the engine's —
1717
1948
  // a note that carries documentation is not a SoHL idea — but the
@@ -1719,7 +1950,7 @@ export function defineConfig(config) {
1719
1950
  // runtime. Two would drift, which is the whole reason the composition
1720
1951
  // was written down in one place to begin with.
1721
1952
  itemTypes,
1722
- docEntryTypes: Object.freeze(new Set([...itemTypes, "macro", ...MAP_TYPES])),
1953
+ docEntryTypes,
1723
1954
  skipDirectories: Object.freeze(skipDirectories),
1724
1955
  packs: Object.freeze(packs),
1725
1956
  packDirectories: Object.freeze(packDirectories),