@heroiclands/package-build 22.0.3 → 22.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.
@@ -175,6 +175,10 @@ export const DEFAULT_PATHS = /** @type {const} */ ({
175
175
  // dependency — so nesting one under the other would imply a containment
176
176
  // that does not hold.
177
177
  metadataCache: "build/cache/metadata",
178
+ // Where the site navigation heroiclands.org publishes is fetched to.
179
+ // Beside the other two: it is fetched by the same command and read under
180
+ // the same complete-marker rule.
181
+ navigationCache: "build/cache/navigation",
178
182
  });
179
183
 
180
184
  /**
@@ -387,6 +391,8 @@ export function publishesContentPages(config) {
387
391
  * index is fetched to. Inbound,
388
392
  * for *every* declared dependency, not
389
393
  * only those supplying a catalogue.
394
+ * @property {string} [navigationCache] Where the site navigation is fetched
395
+ * to, for the generated Hugo menu.
390
396
  */
391
397
 
392
398
  /**
@@ -401,6 +407,7 @@ export function publishesContentPages(config) {
401
407
  * @property {string} unpack
402
408
  * @property {string} foreignCache
403
409
  * @property {string} metadataCache
410
+ * @property {string} navigationCache
404
411
  */
405
412
 
406
413
  /**
@@ -496,6 +503,14 @@ export function publishesContentPages(config) {
496
503
  * package this one targets — for a system
497
504
  * relationship, `verified` is what
498
505
  * `_stats.systemVersion` is stamped from.
506
+ * @property {boolean} [contentIndex] Whether `deps fetch` fetches this
507
+ * dependency's content index. Default
508
+ * `true`. `false` declares the dependency
509
+ * for the Foundry manifest only — nothing
510
+ * this tree cites by wikilink — and refuses
511
+ * `itemCatalog: true` on the same entry,
512
+ * since a catalogue is fetched from the same
513
+ * index.
499
514
  */
500
515
 
501
516
  /**
@@ -578,6 +593,13 @@ export function publishesContentPages(config) {
578
593
  * Refused by a `documentation`
579
594
  * package, which ships no Foundry
580
595
  * package.
596
+ * @property {string} [homepage] `package.json`'s own `homepage` —
597
+ * the site build's `baseURL`. Checked
598
+ * by `checkHomepage` in
599
+ * `config.mjs`.
600
+ * @property {string|{name: string, email?: string, url?: string}} [author]
601
+ * `package.json`'s own `author`, in
602
+ * either of npm's forms.
581
603
  * @property {PackageKind} packageKind Whether the package is a system, a
582
604
  * module, or documentation — the kind
583
605
  * that publishes a site and a book
@@ -637,6 +659,13 @@ export function publishesContentPages(config) {
637
659
  * @property {string} contentPackage
638
660
  * @property {string|null} foundryPackage `null` for a `documentation`
639
661
  * package, which ships no Foundry package.
662
+ * @property {string|null} homepage `package.json`'s own `homepage`,
663
+ * checked by `checkHomepage` in
664
+ * `config.mjs`.
665
+ * @property {Readonly<{name: string, email?: string, url?: string}>|null} author
666
+ * `package.json`'s own `author`, normalised
667
+ * from either of npm's forms; `null` when
668
+ * the package declares none.
640
669
  * @property {PackageKind} packageKind
641
670
  * @property {string|null} assetRoot Derived, and **conditional**: the served
642
671
  * Foundry asset root,
@@ -705,6 +734,8 @@ const CONFIG_KEYS = [
705
734
  "rootDir",
706
735
  "contentPackage",
707
736
  "foundryPackage",
737
+ "homepage",
738
+ "author",
708
739
  "packageKind",
709
740
  "stats",
710
741
  "itemBuilders",
@@ -726,7 +757,6 @@ const SYSTEM_KEYS = ["manifest", "compatibility"];
726
757
  const COMPATIBILITY_KEYS = ["minimum", "verified"];
727
758
  const DOCS_KEYS = ["itemFields"];
728
759
  const SITE_KEYS = [
729
- "out",
730
760
  "base",
731
761
  "assets",
732
762
  "packages",
@@ -737,8 +767,14 @@ const SITE_KEYS = [
737
767
  "pass",
738
768
  "passOptions",
739
769
  "backfillSections",
770
+ "list",
771
+ "notfound",
772
+ "hugo",
740
773
  ];
741
774
  const SITE_TREE_KEYS = ["from", "section"];
775
+ const SITE_LIST_KEYS = ["shortcodes"];
776
+ const SITE_NOTFOUND_KEYS = ["tagline", "sitenoun", "heroimage", "links"];
777
+ const SITE_NOTFOUND_LINK_KEYS = ["title", "url", "text"];
742
778
  const PDF_KEYS = ["title", "subtitle", "document", "out", "front", "fonts", "iconFonts", "binary"];
743
779
  const PDF_FONT_KEYS = ["serif", "sans", "mono", "path"];
744
780
  const EMPTY_PDF_FONTS = Object.freeze({ serif: "", sans: "", mono: "", path: "" });
@@ -752,7 +788,9 @@ const RELATIONSHIP_KEYS = [
752
788
  "manifest",
753
789
  "compatibility",
754
790
  "itemCatalog",
791
+ "contentIndex",
755
792
  ];
793
+ const AUTHOR_KEYS = ["name", "email", "url"];
756
794
  const ITEM_BUILDER_KEYS = ["system", "img", "fields"];
757
795
  const ITEM_REGISTRY_KEYS = ["system", "builders"];
758
796
  const PACK_KEYS = [
@@ -964,6 +1002,56 @@ function optionalString(value, field) {
964
1002
  return value;
965
1003
  }
966
1004
 
1005
+ /**
1006
+ * npm's `author` field, in either of its two forms.
1007
+ *
1008
+ * `package.json` accepts a single string — `"Name <email> (url)"`, with the
1009
+ * email and the URL both optional — or an object carrying the same three
1010
+ * parts. Both normalise to one shape, so the site build reads one field
1011
+ * instead of branching on which form a repository happened to write.
1012
+ *
1013
+ * @type {RegExp}
1014
+ */
1015
+ const AUTHOR_STRING = /^([^<(]*?)\s*(?:<([^>]*)>)?\s*(?:\(([^)]*)\))?\s*$/;
1016
+
1017
+ /**
1018
+ * @param {unknown} value - The declared `author`, or `undefined`.
1019
+ * @returns {Readonly<{name: string, email?: string, url?: string}>|null}
1020
+ * `null` when the package declares none.
1021
+ */
1022
+ function normalizeAuthor(value) {
1023
+ if (value === undefined) return null;
1024
+ if (typeof value === "string") {
1025
+ const match = AUTHOR_STRING.exec(value.trim());
1026
+ const name = match?.[1]?.trim();
1027
+ if (!match || !name) {
1028
+ fail(
1029
+ "author",
1030
+ 'must be `"Name"`, `"Name <email>"`, `"Name (url)"` or ' +
1031
+ '`"Name <email> (url)"` — npm\'s own `author` forms',
1032
+ );
1033
+ }
1034
+ return Object.freeze({
1035
+ name: /** @type {string} */ (name),
1036
+ ...(match[2] ? { email: match[2] } : {}),
1037
+ ...(match[3] ? { url: match[3] } : {}),
1038
+ });
1039
+ }
1040
+ if (!isPlainObject(value)) {
1041
+ fail("author", "must be a string or an object with `name`, `email` and `url`");
1042
+ }
1043
+ const author = /** @type {Record<string, unknown>} */ (value);
1044
+ rejectUnknownKeys(author, AUTHOR_KEYS, "author.");
1045
+ const name = requireNonEmptyString(author.name, "author.name");
1046
+ return Object.freeze({
1047
+ name,
1048
+ ...(author.email !== undefined ?
1049
+ { email: optionalString(author.email, "author.email") }
1050
+ : {}),
1051
+ ...(author.url !== undefined ? { url: optionalString(author.url, "author.url") } : {}),
1052
+ });
1053
+ }
1054
+
967
1055
  /**
968
1056
  * @param {unknown} value
969
1057
  * @param {string} where Field path used in error messages.
@@ -1504,21 +1592,151 @@ function normalizeSectionMap(value, where) {
1504
1592
  return Object.freeze(out);
1505
1593
  }
1506
1594
 
1595
+ /**
1596
+ * Hugo keys a repository may **not** declare under `site.hugo`, because the
1597
+ * site build generates them and would only overwrite what was written.
1598
+ *
1599
+ * The same rule `DERIVED_MANIFEST_KEYS` states for the manifest, for the same
1600
+ * reason: an authored `baseURL` would look authoritative, sit there unread,
1601
+ * and disagree with the site forever. Each key names where its value comes
1602
+ * from. A dotted key names a nested one, and covers everything beneath it —
1603
+ * `params.brand` refuses `params.brand.logo` too — so `site.hugo` reaches only
1604
+ * what the generator does not write.
1605
+ *
1606
+ * @type {Readonly<Record<string, string>>}
1607
+ */
1608
+ export const DERIVED_HUGO_KEYS = Object.freeze({
1609
+ baseURL: "package.json `homepage`",
1610
+ title: "`packageBuild.manifest.title`",
1611
+ locale: "the organisation's locale, in `engine/site-config.mjs`",
1612
+ publishDir: "`contentPackage`, under the deployment root `build/site`",
1613
+ contentDir: "the fixed content mount, `build/hugo/content`",
1614
+ themesDir: "where `@heroiclands/hugo-theme` is installed",
1615
+ theme: "the installed `@heroiclands/hugo-theme`",
1616
+ disableKinds: "whether any note in the tree carries `tags:`, which the site walk discovers",
1617
+ taxonomies: "whether any note in the tree carries `tags:`, which the site walk discovers",
1618
+ outputs: "whether any note in the tree carries `tags:`, which the site walk discovers",
1619
+ "params.description": "package.json `description`",
1620
+ "params.author": "package.json `author`",
1621
+ "params.cdnBaseURL": "`site.assets`",
1622
+ "params.brand": "the organisation's brand links, in `engine/site-config.mjs`",
1623
+ "params.list": "`site.list`",
1624
+ "params.notfound": "`site.notfound`",
1625
+ "markup.goldmark.renderer.unsafe": "the toolchain, whose pages carry raw HTML",
1626
+ menu: "the navigation `content-build deps fetch` caches from heroiclands.org",
1627
+ });
1628
+
1629
+ /**
1630
+ * The `site.list` block — how a listing page renders.
1631
+ *
1632
+ * @param {unknown} value - The block, or `undefined`.
1633
+ * @returns {Readonly<{shortcodes: boolean}>} It, frozen, with every default filled.
1634
+ */
1635
+ function normalizeSiteList(value) {
1636
+ if (value === undefined) return Object.freeze({ shortcodes: false });
1637
+ if (!isPlainObject(value)) fail("site.list", "must be a mapping");
1638
+ const input = /** @type {Record<string, unknown>} */ (value);
1639
+ rejectUnknownKeys(input, SITE_LIST_KEYS, "site.list.");
1640
+ return Object.freeze({
1641
+ shortcodes: optionalBoolean(input.shortcodes, "site.list.shortcodes", false),
1642
+ });
1643
+ }
1644
+
1645
+ /**
1646
+ * The `site.notfound` block — the wording of the "page not found" page.
1647
+ *
1648
+ * The theme renders the page for every site; what a repository supplies is
1649
+ * the tagline, the noun the body prose calls the site, and the routes back.
1650
+ * `tagline` and `sitenoun` are required once the block is present: a block
1651
+ * declaring only links would render the theme's generic wording above a list
1652
+ * of this site's routes, which reads as two sites.
1653
+ *
1654
+ * @param {unknown} value - The block, or `undefined`.
1655
+ * @returns {Readonly<object>|null} It, frozen; `null` when absent.
1656
+ */
1657
+ function normalizeSiteNotfound(value) {
1658
+ if (value === undefined) return null;
1659
+ if (!isPlainObject(value)) fail("site.notfound", "must be a mapping");
1660
+ const input = /** @type {Record<string, unknown>} */ (value);
1661
+ rejectUnknownKeys(input, SITE_NOTFOUND_KEYS, "site.notfound.");
1662
+ const out = {
1663
+ tagline: requireNonEmptyString(input.tagline, "site.notfound.tagline"),
1664
+ sitenoun: requireNonEmptyString(input.sitenoun, "site.notfound.sitenoun"),
1665
+ };
1666
+ if (input.heroimage !== undefined) {
1667
+ out.heroimage = requireNonEmptyString(input.heroimage, "site.notfound.heroimage");
1668
+ }
1669
+ if (input.links !== undefined) {
1670
+ if (!Array.isArray(input.links)) fail("site.notfound.links", "must be a list");
1671
+ out.links = Object.freeze(
1672
+ input.links.map((link, i) => {
1673
+ const where = `site.notfound.links[${i}]`;
1674
+ if (!isPlainObject(link)) fail(where, "must be a mapping");
1675
+ const entry = /** @type {Record<string, unknown>} */ (link);
1676
+ rejectUnknownKeys(entry, SITE_NOTFOUND_LINK_KEYS, `${where}.`);
1677
+ return Object.freeze({
1678
+ title: requireNonEmptyString(entry.title, `${where}.title`),
1679
+ url: requireNonEmptyString(entry.url, `${where}.url`),
1680
+ text: requireNonEmptyString(entry.text, `${where}.text`),
1681
+ });
1682
+ }),
1683
+ );
1684
+ }
1685
+ return Object.freeze(out);
1686
+ }
1687
+
1688
+ /**
1689
+ * The `site.hugo` block — a mapping deep-merged over the generated Hugo
1690
+ * configuration, last.
1691
+ *
1692
+ * The escape hatch for the key nobody anticipated. Every key the generator
1693
+ * writes is refused here by {@link DERIVED_HUGO_KEYS}, naming its source, so
1694
+ * the block cannot grow into a second configuration file.
1695
+ *
1696
+ * @param {unknown} value - The block, or `undefined`.
1697
+ * @returns {Readonly<Record<string, unknown>>} It, frozen; `{}` when absent.
1698
+ */
1699
+ function normalizeSiteHugo(value) {
1700
+ if (value === undefined) return Object.freeze({});
1701
+ if (!isPlainObject(value)) fail("site.hugo", "must be a mapping");
1702
+ const input = /** @type {Record<string, unknown>} */ (value);
1703
+ for (const [dotted, source] of Object.entries(DERIVED_HUGO_KEYS)) {
1704
+ /** @type {unknown} */
1705
+ let at = input;
1706
+ for (const part of dotted.split(".")) {
1707
+ at = isPlainObject(at) ? /** @type {Record<string, unknown>} */ (at)[part] : undefined;
1708
+ if (at === undefined) break;
1709
+ }
1710
+ if (at !== undefined) {
1711
+ fail(
1712
+ `site.hugo.${dotted}`,
1713
+ `is derived from ${source} and must not be declared — it ` +
1714
+ `would be overwritten, and the two would disagree with ` +
1715
+ `nothing to say so`,
1716
+ );
1717
+ }
1718
+ }
1719
+ return Object.freeze(structuredClone(input));
1720
+ }
1721
+
1507
1722
  /**
1508
1723
  * The `site` section — how this repository frames the website it publishes.
1509
1724
  *
1510
- * Everything here is *framing*: where the Hugo tree is written, what a section
1511
- * is called, which extra trees are published beside the content, and which
1512
- * named pass bundle supplies the repository's own body rewrites. How a page gets
1513
- * its **address** is deliberately not here that is `publish.address`, shared
1514
- * with the link manifest so the two cannot disagree about where a page is.
1725
+ * Everything here is *framing*: what a section is called, which extra trees
1726
+ * are published beside the content, which named pass bundle supplies the
1727
+ * repository's own body rewrites, and the residue of the generated Hugo
1728
+ * configuration that is genuinely this repository's own. Where the Hugo tree
1729
+ * is written is not a choice: `content-build site` writes it under
1730
+ * `build/hugo/`, and a `site.out` is refused by name. How a page gets its
1731
+ * **address** is deliberately not here either — that is `publish.address`,
1732
+ * shared with the link manifest so the two cannot disagree about where a
1733
+ * page is.
1515
1734
  *
1516
1735
  * @param {unknown} value - The `site` block, or `undefined`.
1517
1736
  * @returns {Readonly<object>} It, frozen, with every default filled.
1518
1737
  */
1519
1738
  function normalizeSite(value) {
1520
1739
  const empty = Object.freeze({
1521
- out: "",
1522
1740
  base: "",
1523
1741
  assets: "",
1524
1742
  packages: Object.freeze([]),
@@ -1529,10 +1747,23 @@ function normalizeSite(value) {
1529
1747
  pass: "",
1530
1748
  passOptions: Object.freeze({}),
1531
1749
  backfillSections: false,
1750
+ list: Object.freeze({ shortcodes: false }),
1751
+ notfound: null,
1752
+ hugo: Object.freeze({}),
1532
1753
  });
1533
1754
  if (value === undefined) return empty;
1534
1755
  if (!isPlainObject(value)) fail("site", "must be a mapping");
1535
1756
  const input = /** @type {Record<string, unknown>} */ (value);
1757
+ // Refused by name, ahead of the vocabulary check: the useful thing to say
1758
+ // is not "no such key" but that the location is fixed.
1759
+ if (input.out !== undefined) {
1760
+ fail(
1761
+ "site.out",
1762
+ "is retired — the site build writes its content mount at " +
1763
+ "`build/hugo/content`, beside the generated `hugo.toml`, and " +
1764
+ "the location is not configurable. Remove the key",
1765
+ );
1766
+ }
1536
1767
  rejectUnknownKeys(input, SITE_KEYS, "site.");
1537
1768
 
1538
1769
  const trees = [];
@@ -1574,7 +1805,6 @@ function normalizeSite(value) {
1574
1805
  }
1575
1806
 
1576
1807
  return Object.freeze({
1577
- out: input.out === undefined ? "" : requireNonEmptyString(input.out, "site.out"),
1578
1808
  base: input.base === undefined ? "" : requireNonEmptyString(input.base, "site.base"),
1579
1809
  assets: normalizeSiteAssets(input.assets),
1580
1810
  packages: Object.freeze(packages),
@@ -1588,6 +1818,9 @@ function normalizeSite(value) {
1588
1818
  Object.freeze({})
1589
1819
  : Object.freeze({ ...input.passOptions }),
1590
1820
  backfillSections: optionalBoolean(input.backfillSections, "site.backfillSections", false),
1821
+ list: normalizeSiteList(input.list),
1822
+ notfound: normalizeSiteNotfound(input.notfound),
1823
+ hugo: normalizeSiteHugo(input.hugo),
1591
1824
  });
1592
1825
  }
1593
1826
 
@@ -1925,6 +2158,24 @@ function normalizeRelationships(value) {
1925
2158
  }
1926
2159
  spec.itemCatalog = rel.itemCatalog;
1927
2160
  }
2161
+ // Opt-out: declares the dependency for the Foundry manifest
2162
+ // only, so `deps fetch` fetches no content index for it and a
2163
+ // wikilink into it is refused rather than silently dead. A
2164
+ // catalogue is fetched from the same index, so it cannot be
2165
+ // declared alongside `itemCatalog: true`.
2166
+ if (rel.contentIndex !== undefined) {
2167
+ if (typeof rel.contentIndex !== "boolean") {
2168
+ fail(`${at}.contentIndex`, "must be true or false");
2169
+ }
2170
+ if (rel.contentIndex === false && spec.itemCatalog) {
2171
+ fail(
2172
+ `${at}.contentIndex`,
2173
+ "cannot be false together with `itemCatalog: true` — a catalogue is " +
2174
+ "fetched from the same index",
2175
+ );
2176
+ }
2177
+ spec.contentIndex = rel.contentIndex;
2178
+ }
1928
2179
  return Object.freeze(spec);
1929
2180
  }),
1930
2181
  );
@@ -2493,6 +2744,11 @@ export function defineConfig(config) {
2493
2744
  rootDir,
2494
2745
  contentPackage: requireContentPackage(input.contentPackage, docEntryTypes),
2495
2746
  foundryPackage,
2747
+ // `package.json`'s own address and byline. `homepage` is checked by
2748
+ // `checkHomepage` in `config.mjs`.
2749
+ homepage:
2750
+ input.homepage === undefined ? null : requireNonEmptyString(input.homepage, "homepage"),
2751
+ author: normalizeAuthor(input.author),
2496
2752
  packageKind: /** @type {PackageKind} */ (packageKind),
2497
2753
  // Foundry serves a package's files from `<kind>/<id>/`, so this is the
2498
2754
  // one place `systems/sohl` (or `modules/sohl-thalorna`) is spelled.
package/docs/api.md CHANGED
@@ -342,6 +342,7 @@ The closed half of a note's frontmatter: the `data:` container and each type's `
342
342
  | `applicableTagGroups` | `applicableTagGroups(type, groups)` | `object[]` | reading the declared tag groups that apply to a note type |
343
343
  | `exclusiveTagGroups` | `exclusiveTagGroups(type, groups)` | `Array<{slot, tags}>` | reading the single-valued tag slots a note type has, such as a being's kind |
344
344
  | `hasTag` | `hasTag(fm, tag)` | `boolean` | checking whether a note carries a given tag, whatever scalar-or-list form it was authored in |
345
+ | `hasAnyTag` | `hasAnyTag(fm)` | `boolean` | checking whether a note carries any `tags:` at all, for the site build's taxonomy decision |
345
346
  | `isDraftNote` | `isDraftNote(fm)` | `boolean` | checking whether a note is tagged as an unfinished draft |
346
347
  | `subTypeCharsetMessage` | `subTypeCharsetMessage(value)` | `string` | building the message for a `subType` outside the address charset |
347
348
  | `typeCharsetMessage` | `typeCharsetMessage(type)` | `string` | building the message for a `type` outside the address charset |
@@ -491,21 +492,22 @@ The shortcodes a note declares it used to be published under. A package's `(type
491
492
 
492
493
  The published content index — the artifact packages exchange addresses through. **A package publishes its own index; a consumer fetches the ones it depends on.** That is the whole mechanism, and it replaces a vendored link manifest that each repository committed a copy of every other repository's file into. Vendoring failed three ways, and only the last is about staleness:
493
494
 
494
- | Export | Signature | Returns | Use it when |
495
- | ------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
496
- | `METADATA_RELATIONSHIP_KINDS` | `const METADATA_RELATIONSHIP_KINDS` | — | The relationship kinds that are dependencies, and therefore citable. |
497
- | `metadataFileName` | `function metadataFileName(pkg)` | {string} The file name, e.g. | What a package's content index is called, wherever it is written or fetched. |
498
- | `metadataRelationships` | `function metadataRelationships(config)` | {Array<{id: string, manifest: string, kind: string, verified: string\|undefined}>} The dependencies, in declaration order. | Every dependency whose published index this build resolves addresses through. |
499
- | `metadataCacheDir` | `function metadataCacheDir(config, id, version)` | {string} The directory. | The cache directory for one dependency's index at one version. |
500
- | `isComplete` | `const isComplete` | {boolean} True when it was fetched to completion. | Whether a dependency's cache is present and complete. |
501
- | `markComplete` | `function markComplete(dir)` | {void} | Mark a dependency's cache complete. |
502
- | `cachedMetadataFiles` | `function cachedMetadataFiles(config)` | {string[]} One index file per declared dependency. | The fetched index files this build resolves foreign addresses against. |
503
- | `cachedMetadataIndexes` | `function cachedMetadataIndexes(config)` | {Array<{id: string, file: string}>} One entry per declared dependency. | The same fetched indexes, each paired with the package that published it. |
504
- | `newestVersionDir` | `function newestVersionDir(dirs)` | {string} The newest one. | The newest cached version among several version-keyed cache directories. |
505
- | `loadForeignIndexes` | `function loadForeignIndexes(config, localPackages, bases` | {{index: Map<string, object>, packages: Set<string>, stale: Array<{package: string, reason: string}>}} The resolved addresses, which packages contributed, and what could not be read. | Resolve every foreign address this build can cite, from the fetched indexes. |
506
- | `cachedIndexPath` | `function cachedIndexPath(config, pkg)` | {string} A path to name in a diagnostic. | Where a dependency's fetched index sits, for naming it in a diagnostic. |
507
- | `unaddressableForeignPackages` | `function unaddressableForeignPackages(foreignIndex)` | {Array<{package: string, entries: number, sampleKey: string}>} One finding per drifted package, in the order the index first names each. | Whether a fetched index can still be _addressed_, as distinct from read. |
508
- | `formatUnaddressableFinding` | `function formatUnaddressableFinding(finding, config)` | {string} The formatted diagnostic, path first on the line. | One finding, in the standard `file:line:column: severity: message` form. |
495
+ | Export | Signature | Returns | Use it when |
496
+ | ------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
497
+ | `METADATA_RELATIONSHIP_KINDS` | `const METADATA_RELATIONSHIP_KINDS` | — | The relationship kinds that are dependencies, and therefore citable. |
498
+ | `metadataFileName` | `function metadataFileName(pkg)` | {string} The file name, e.g. | What a package's content index is called, wherever it is written or fetched. |
499
+ | `metadataRelationships` | `function metadataRelationships(config)` | {Array<{id: string, manifest: string, kind: string, verified: string\|undefined}>} The dependencies, in declaration order. | Every dependency whose published index this build resolves addresses through. |
500
+ | `noContentIndexPackages` | `function noContentIndexPackages(config)` | {ReadonlySet<string>} The content package names. | Every package a relationship declares `contentIndex: false` on, keyed by the content package name a link into it would use. |
501
+ | `metadataCacheDir` | `function metadataCacheDir(config, id, version)` | {string} The directory. | The cache directory for one dependency's index at one version. |
502
+ | `isComplete` | `const isComplete` | {boolean} True when it was fetched to completion. | Whether a dependency's cache is present and complete. |
503
+ | `markComplete` | `function markComplete(dir)` | {void} | Mark a dependency's cache complete. |
504
+ | `cachedMetadataFiles` | `function cachedMetadataFiles(config)` | {string[]} One index file per declared dependency. | The fetched index files this build resolves foreign addresses against. |
505
+ | `cachedMetadataIndexes` | `function cachedMetadataIndexes(config)` | {Array<{id: string, file: string}>} One entry per declared dependency. | The same fetched indexes, each paired with the package that published it. |
506
+ | `newestVersionDir` | `function newestVersionDir(dirs)` | {string} The newest one. | The newest cached version among several version-keyed cache directories. |
507
+ | `loadForeignIndexes` | `function loadForeignIndexes(config, localPackages, bases` | {{index: Map<string, object>, packages: Set<string>, stale: Array<{package: string, reason: string}>}} The resolved addresses, which packages contributed, and what could not be read. | Resolve every foreign address this build can cite, from the fetched indexes. |
508
+ | `cachedIndexPath` | `function cachedIndexPath(config, pkg)` | {string} A path to name in a diagnostic. | Where a dependency's fetched index sits, for naming it in a diagnostic. |
509
+ | `unaddressableForeignPackages` | `function unaddressableForeignPackages(foreignIndex)` | {Array<{package: string, entries: number, sampleKey: string}>} One finding per drifted package, in the order the index first names each. | Whether a fetched index can still be _addressed_, as distinct from read. |
510
+ | `formatUnaddressableFinding` | `function formatUnaddressableFinding(finding, config)` | {string} The formatted diagnostic, path first on the line. | One finding, in the standard `file:line:column: severity: message` form. |
509
511
 
510
512
  ### `engine.foundryEntries`
511
513
 
@@ -621,8 +623,7 @@ Publishing a content tree as a website. Compiling a content tree into compendium
621
623
  | `writeSectionLandings` | `function writeSectionLandings(outRoot,` | {number} How many landings were written. | Writes the Hugo sections a published tree declares. |
622
624
  | `pluralTitle` | `function pluralTitle(name)` | {string} The display title. | A section landing's title, from its directory name — `macro` → `Macros`. |
623
625
  | `resolveSitePass` | `function resolveSitePass(name, options)` | {{beforeLinks?: Function, afterLinks?: Function}} The bundle. | Resolves `site.pass` to its bundle. |
624
- | `resolveOutputRoot` | `function resolveOutputRoot(rootDir, out)` | {string} The absolute output root. | The output root, having established that it is safe to delete. |
625
- | `buildSite` | `buildSite({ config, outRoot, sqlTables })` | {{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], manifests: object\|null}} | Builds a Hugo content tree from a content tree, and reports what it found. |
626
+ | `buildSite` | `buildSite({ config, sqlTables })` | {{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], manifests: object\|null}} | Builds a Hugo content tree from a content tree, and reports what it found. |
626
627
 
627
628
  ### `engine.contentLint`
628
629
 
@@ -835,14 +836,14 @@ Shared helpers for the pack compilers in `packages/content-build/`. The HeroicLa
835
836
 
836
837
  Wikilink resolution for the pack compilers. Content notes link to one another with wikilinks rather than file paths:
837
838
 
838
- | Export | Signature | Returns | Use it when |
839
- | -------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
840
- | `resolveItemDocType` | `function resolveItemDocType(qualifier, types)` | {string\|null} The underlying document type, or `null` when the qualifier is not a virtual one. | Reads a qualifier as the **virtual `doc<type>`** form, or reports that it is not one. |
841
- | `readQualifier` | `function readQualifier(target, types, packages)` | {{type: string, shortcode: string, itemDoc: boolean, package?: string, system?: string, reason?: undefined} \| {reason: "unknown-type"} \| null} The resolved qualifier; a `reason` when the target is definitely qualified but names no known type; or `null` when it is not an address at all. | Read a link target as a **qualified** `type-shortcode` reference, or report that it does not parse as one. |
842
- | `anchorPageId` | `function anchorPageId(noteId, anchorSlug)` | {string} A 16-character alphanumeric id. | The deterministic JournalEntryPage id for one anchor: SHA-256 of `"<noteId>-<anchorSlug>"`, base64-encoded, reduced to the 16 alphanumeric characters a Foundry id allows. |
843
- | `buildWikilinkIndex` | `function buildWikilinkIndex(docs, packageId, foreign, contentPackage)` | {{byShortcode: Map<string, object>, types: Set<string>}} `types` is every type the tree actually contains, so a qualifier naming no real type can be told apart from a missing target. | Builds the link-resolution tables for a content tree. |
844
- | `resolveReference` | `function resolveReference(index, ref, hint)` | {{name?: string, uuid?: string, address?: string, subType?: string}\|undefined} The target, or `undefined` where nothing answers. | Resolve one reference — a bare shortcode, a short address or a canonical one — to what a compendium can use. |
845
- | `convertWikilinks` | `function convertWikilinks(markdown,` | {{markdown: string, unresolved: Array<{link: string, target: string, offset: number, reason: string, packages?: string[], anchor?: string}>}} Each `reason` is one of {@link LINK_FINDING_REASONS}, the vocabulary all three resolvers share — `ambiguous` carries the claiming `packages` and `unknown-anchor` the section it named. | Rewrites every wikilink in a markdown body as a Foundry UUID enricher. |
839
+ | Export | Signature | Returns | Use it when |
840
+ | -------------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
841
+ | `resolveItemDocType` | `function resolveItemDocType(qualifier, types)` | {string\|null} The underlying document type, or `null` when the qualifier is not a virtual one. | Reads a qualifier as the **virtual `doc<type>`** form, or reports that it is not one. |
842
+ | `readQualifier` | `function readQualifier(target, types, packages, noIndexPackages)` | {{type: string, shortcode: string, itemDoc: boolean, package?: string, system?: string, reason?: undefined} \| {reason: "unknown-type"\|"no-content-index", package?: string} \| null} The resolved qualifier; a `reason` when the target is definitely qualified but names no known type or no fetched index; or `null` when it is not an address at all. | Read a link target as a **qualified** `type-shortcode` reference, or report that it does not parse as one. |
843
+ | `anchorPageId` | `function anchorPageId(noteId, anchorSlug)` | {string} A 16-character alphanumeric id. | The deterministic JournalEntryPage id for one anchor: SHA-256 of `"<noteId>-<anchorSlug>"`, base64-encoded, reduced to the 16 alphanumeric characters a Foundry id allows. |
844
+ | `buildWikilinkIndex` | `function buildWikilinkIndex(docs, packageId, foreign, contentPackage)` | {{byShortcode: Map<string, object>, types: Set<string>}} `types` is every type the tree actually contains, so a qualifier naming no real type can be told apart from a missing target. | Builds the link-resolution tables for a content tree. |
845
+ | `resolveReference` | `function resolveReference(index, ref, hint)` | {{name?: string, uuid?: string, address?: string, subType?: string}\|undefined} The target, or `undefined` where nothing answers. | Resolve one reference — a bare shortcode, a short address or a canonical one — to what a compendium can use. |
846
+ | `convertWikilinks` | `function convertWikilinks(markdown,` | {{markdown: string, unresolved: Array<{link: string, target: string, offset: number, reason: string, packages?: string[], anchor?: string}>}} Each `reason` is one of {@link LINK_FINDING_REASONS}, the vocabulary all three resolvers share — `ambiguous` carries the claiming `packages` and `unknown-anchor` the section it named. | Rewrites every wikilink in a markdown body as a Foundry UUID enricher. |
846
847
 
847
848
  ### `engine.wikilinkSyntax`
848
849
 
@@ -1269,6 +1270,7 @@ const config = defineConfig({
1269
1270
  | `SITE_MODES` | `const SITE_MODES` | — | reading the publishing modes `publish.site` may name, weakest first |
1270
1271
  | `DERIVED_SYSTEM_VERSION` | `const DERIVED_SYSTEM_VERSION` | — | the loader-only symbol key `defineConfig` uses internally to receive a resolved system version; not something a configuration author writes |
1271
1272
  | `publishesContentPages` | `publishesContentPages(config)` | `boolean` | checking whether a resolved configuration publishes the pages its content tree compiles to — the one question the site build and the content index both need answered identically |
1273
+ | `DERIVED_HUGO_KEYS` | `const DERIVED_HUGO_KEYS` | — | reading which Hugo keys `site.hugo` may not declare because the site build generates them, each naming its source (declaring one is an error naming the key); a dotted key covers everything beneath it |
1272
1274
 
1273
1275
  ## `./config`
1274
1276
 
@@ -1282,11 +1284,12 @@ const config = loadPackageBuildConfig();
1282
1284
  console.log(config.stageDir);
1283
1285
  ```
1284
1286
 
1285
- | Export | Signature | Returns | Use it when |
1286
- | --------------------------- | ----------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
1287
- | `DERIVED_MANIFEST_KEYS` | `const DERIVED_MANIFEST_KEYS` | — | reading which manifest keys a repository may not declare because the build derives them (declaring one is an error naming the key) |
1288
- | `resolvePackageBuildConfig` | `resolvePackageBuildConfig(shared)` | `Readonly<PackageBuildConfig>` | validating the `packageBuild:` section from an already-loaded shared configuration — the pure half, usable without touching disk |
1289
- | `loadPackageBuildConfig` | `loadPackageBuildConfig()` | `Readonly<PackageBuildConfig>` | reading and validating the repository's resolved package-build configuration from disk, read fresh on each call rather than cached at import |
1287
+ | Export | Signature | Returns | Use it when |
1288
+ | --------------------------- | ----------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1289
+ | `DERIVED_MANIFEST_KEYS` | `const DERIVED_MANIFEST_KEYS` | — | reading which manifest keys a repository may not declare because the build derives them (declaring one is an error naming the key) |
1290
+ | `resolvePackageBuildConfig` | `resolvePackageBuildConfig(shared)` | `Readonly<PackageBuildConfig>` | validating the `packageBuild:` section from an already-loaded shared configuration — the pure half, usable without touching disk |
1291
+ | `loadPackageBuildConfig` | `loadPackageBuildConfig()` | `Readonly<PackageBuildConfig>` | reading and validating the repository's resolved package-build configuration from disk, read fresh on each call rather than cached at import |
1292
+ | `checkHomepage` | `checkHomepage(homepage, contentPackage)` | `void` | validating a resolved `homepage` against `contentPackage` — called by `content-build site` before the generated `baseURL` is written, not by `resolvePackageBuildConfig` itself |
1290
1293
 
1291
1294
  ## `./prettier`
1292
1295