@heroiclands/package-build 17.1.0 → 18.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 (153) hide show
  1. package/CHANGELOG.md +1773 -0
  2. package/CONTENT.md +288 -72
  3. package/README.md +1 -1
  4. package/bin/content-build.mjs +218 -137
  5. package/bin/package-build.mjs +43 -0
  6. package/content-config.mjs +62 -67
  7. package/docs/content-format.md +768 -58
  8. package/engine/actor-compiler.mjs +586 -0
  9. package/engine/address-charset.mjs +11 -8
  10. package/engine/address-diff.mjs +266 -22
  11. package/engine/anchored-sections.mjs +83 -0
  12. package/engine/anchors.mjs +83 -0
  13. package/engine/base-compiler.mjs +148 -18
  14. package/engine/bundle-notes.mjs +276 -0
  15. package/engine/bundles.mjs +307 -0
  16. package/engine/code-fences.mjs +103 -0
  17. package/engine/compile-corpus.mjs +89 -0
  18. package/engine/content-address.mjs +267 -5
  19. package/engine/content-format-check.mjs +54 -11
  20. package/engine/content-format.mjs +37 -5
  21. package/engine/content-index.mjs +394 -111
  22. package/engine/content-links.mjs +185 -77
  23. package/engine/content-lint.mjs +232 -14
  24. package/engine/content-package.mjs +2 -1
  25. package/engine/content-tables.mjs +99 -3
  26. package/engine/document-subtypes.mjs +49 -6
  27. package/engine/field-reference.mjs +1 -1
  28. package/engine/field-spec.mjs +56 -6
  29. package/engine/folder-notes.mjs +470 -0
  30. package/engine/foreign-catalog.mjs +195 -4
  31. package/engine/{manifest-emit.mjs → foundry-entries.mjs} +78 -81
  32. package/engine/frontmatter-lint.mjs +245 -19
  33. package/engine/frontmatter.mjs +88 -8
  34. package/engine/generate.mjs +304 -13
  35. package/engine/helpers.mjs +293 -64
  36. package/engine/ids.mjs +137 -2
  37. package/engine/index-records.mjs +126 -0
  38. package/engine/index.mjs +23 -5
  39. package/engine/item-compiler.mjs +349 -0
  40. package/engine/item-docs.mjs +7 -1
  41. package/engine/item-registry.mjs +6 -0
  42. package/engine/journals.mjs +93 -27
  43. package/engine/macros.mjs +4 -2
  44. package/engine/metadata-index.mjs +495 -0
  45. package/engine/note-claims.mjs +146 -25
  46. package/engine/note-ids.mjs +112 -0
  47. package/engine/note-renames.mjs +134 -0
  48. package/engine/note-schemas.mjs +25 -0
  49. package/engine/note-vocabulary.mjs +110 -27
  50. package/engine/pack-config.mjs +4 -0
  51. package/engine/pack-router.mjs +23 -0
  52. package/engine/retired-fields.mjs +137 -3
  53. package/engine/scenes.mjs +66 -34
  54. package/engine/schema-check.mjs +58 -17
  55. package/engine/site-build.mjs +71 -12
  56. package/engine/site-index.mjs +20 -7
  57. package/engine/sql-tables.mjs +485 -0
  58. package/engine/subtype-registry.mjs +102 -0
  59. package/engine/system-block.mjs +96 -12
  60. package/engine/systems.mjs +201 -0
  61. package/engine/web-wikilinks.mjs +50 -20
  62. package/engine/wikilink-syntax.mjs +17 -8
  63. package/engine/wikilinks.mjs +124 -51
  64. package/engine/yaml-lint.mjs +282 -0
  65. package/hm3/actors.mjs +352 -0
  66. package/hm3/default-item-art.mjs +75 -0
  67. package/hm3/document-subtypes.mjs +134 -0
  68. package/hm3/index.mjs +56 -0
  69. package/hm3/item-builders.mjs +84 -0
  70. package/hm3/item-fields.mjs +180 -0
  71. package/hm3/items.mjs +96 -0
  72. package/hm3/template-priority.mjs +85 -0
  73. package/labels.mjs +191 -0
  74. package/manifest.mjs +43 -2
  75. package/package.json +19 -4
  76. package/release.mjs +62 -7
  77. package/sohl/actors.mjs +33 -487
  78. package/sohl/being-info.mjs +16 -7
  79. package/sohl/default-item-art.mjs +14 -3
  80. package/sohl/document-subtypes.mjs +16 -10
  81. package/sohl/item-builders.mjs +14 -5
  82. package/sohl/item-fields.mjs +68 -7
  83. package/sohl/items.mjs +44 -258
  84. package/sohl/note-schemas.mjs +2 -2
  85. package/types/content-config.d.mts +21 -42
  86. package/types/engine/actor-compiler.d.mts +204 -0
  87. package/types/engine/address-charset.d.mts +11 -8
  88. package/types/engine/address-diff.d.mts +53 -5
  89. package/types/engine/anchored-sections.d.mts +21 -0
  90. package/types/engine/anchors.d.mts +20 -0
  91. package/types/engine/base-compiler.d.mts +17 -17
  92. package/types/engine/bundle-notes.d.mts +173 -0
  93. package/types/engine/bundles.d.mts +60 -0
  94. package/types/engine/code-fences.d.mts +43 -0
  95. package/types/engine/compile-corpus.d.mts +32 -0
  96. package/types/engine/content-address.d.mts +205 -5
  97. package/types/engine/content-format-check.d.mts +6 -2
  98. package/types/engine/content-format.d.mts +57 -1
  99. package/types/engine/content-index.d.mts +64 -80
  100. package/types/engine/content-links.d.mts +52 -7
  101. package/types/engine/content-lint.d.mts +10 -1
  102. package/types/engine/content-package.d.mts +2 -1
  103. package/types/engine/content-tables.d.mts +21 -39
  104. package/types/engine/document-subtypes.d.mts +37 -3
  105. package/types/engine/field-spec.d.mts +76 -5
  106. package/types/engine/folder-notes.d.mts +159 -0
  107. package/types/engine/foreign-catalog.d.mts +53 -0
  108. package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +16 -38
  109. package/types/engine/frontmatter-lint.d.mts +10 -2
  110. package/types/engine/frontmatter.d.mts +64 -0
  111. package/types/engine/generate.d.mts +38 -0
  112. package/types/engine/helpers.d.mts +94 -30
  113. package/types/engine/ids.d.mts +96 -0
  114. package/types/engine/index-records.d.mts +68 -0
  115. package/types/engine/index.d.mts +9 -3
  116. package/types/engine/item-compiler.d.mts +131 -0
  117. package/types/engine/journals.d.mts +47 -9
  118. package/types/engine/metadata-index.d.mts +226 -0
  119. package/types/engine/note-claims.d.mts +57 -11
  120. package/types/engine/note-ids.d.mts +38 -0
  121. package/types/engine/note-renames.d.mts +102 -0
  122. package/types/engine/note-vocabulary.d.mts +44 -8
  123. package/types/engine/retired-fields.d.mts +75 -0
  124. package/types/engine/scenes.d.mts +3 -2
  125. package/types/engine/schema-check.d.mts +25 -4
  126. package/types/engine/site-build.d.mts +4 -4
  127. package/types/engine/site-index.d.mts +1 -1
  128. package/types/engine/sql-tables.d.mts +185 -0
  129. package/types/engine/subtype-registry.d.mts +49 -0
  130. package/types/engine/system-block.d.mts +40 -1
  131. package/types/engine/systems.d.mts +106 -0
  132. package/types/engine/web-wikilinks.d.mts +4 -2
  133. package/types/engine/wikilink-syntax.d.mts +10 -3
  134. package/types/engine/wikilinks.d.mts +41 -13
  135. package/types/engine/yaml-lint.d.mts +107 -0
  136. package/types/hm3/actors.d.mts +48 -0
  137. package/types/hm3/default-item-art.d.mts +42 -0
  138. package/types/hm3/document-subtypes.d.mts +24 -0
  139. package/types/hm3/index.d.mts +7 -0
  140. package/types/hm3/item-builders.d.mts +11 -0
  141. package/types/hm3/item-fields.d.mts +12 -0
  142. package/types/hm3/items.d.mts +23 -0
  143. package/types/hm3/template-priority.d.mts +21 -0
  144. package/types/manifest.d.mts +22 -1
  145. package/types/release.d.mts +9 -4
  146. package/types/sohl/actors.d.mts +7 -74
  147. package/types/sohl/being-info.d.mts +8 -27
  148. package/types/sohl/default-item-art.d.mts +5 -3
  149. package/types/sohl/items.d.mts +17 -32
  150. package/engine/foreign-manifests.mjs +0 -126
  151. package/engine/kb-manifest.mjs +0 -490
  152. package/types/engine/foreign-manifests.d.mts +0 -43
  153. package/types/engine/kb-manifest.d.mts +0 -241
@@ -39,7 +39,7 @@
39
39
  *
40
40
  * **The rest of the emission is only observable.** A compiler writes keys of
41
41
  * its own alongside the declared fields — `shortcode`, `actionDefs`, `notes`,
42
- * `docHtml`, and since #126 `archetype` — and those appear in neither set
42
+ * `docHtml`, and since #126 `templatePriority` — and those appear in neither set
43
43
  * {@link compareFields} compares, so nothing compared them at all (#155). They
44
44
  * cannot be listed here without the list going stale the next time a compiler
45
45
  * grows a key, so they are read off the `system` object the compiler produced:
@@ -357,7 +357,7 @@ function undeclaredEmittedPaths(data, declared, enumerated, prefix = "") {
357
357
  * *observed*. {@link compareFields} reads the `itemBuilders` declarations and
358
358
  * {@link checkAuthoredSystemData} reads a note's `<system>.system`; between them
359
359
  * they miss every key a compiler writes on its own initiative, which is not a
360
- * residue — it is `shortcode`, `actionDefs`, `notes`, `docHtml` and `archetype`.
360
+ * residue — it is `shortcode`, `actionDefs`, `notes`, `docHtml` and `templatePriority`.
361
361
  *
362
362
  * **The keys come from the object the compiler built, after a JSON round trip.**
363
363
  * That is exactly what the pack file receives, so a key whose value is
@@ -455,12 +455,25 @@ export function compareEmittedSystem({
455
455
  * stamps no system at all, and a system that has not adopted the artifact yet
456
456
  * is simply unchecked. Neither is an error, and the caller says which it was.
457
457
  *
458
+ * **A build may have more than one system, and then the caller names it (#139).**
459
+ * `stats.systemId` is the package-wide answer, and a repository shipping content
460
+ * for two systems has no package-wide answer — it is deliberately `null` there,
461
+ * because a module feeding both `sohl` and `hm3` targets neither. Left at that,
462
+ * every schema check in such a build would be skipped in silence, which is the
463
+ * state #60 exists to remove: the five type names the two systems *share* are
464
+ * exactly the ones a wrong-system emission hides in. So a pass supplies the
465
+ * system its pack declares, and the version comes from that system's own
466
+ * `systems:` entry rather than from a package-wide stamp.
467
+ *
458
468
  * @param {object} config - The resolved build configuration.
469
+ * @param {string|null} [system] - The system whose schema is wanted. Defaults
470
+ * to the package-wide `stats.systemId`, which is every single-system build
471
+ * and the behaviour this always had.
459
472
  * @returns {{artifact: SchemaArtifact, source: string}|null} The schema and
460
473
  * where it was read from.
461
474
  */
462
- export function resolveSchemaArtifact(config) {
463
- const systemId = config?.stats?.systemId;
475
+ export function resolveSchemaArtifact(config, system = undefined) {
476
+ const systemId = system === undefined ? config?.stats?.systemId : system;
464
477
  if (!systemId) return null;
465
478
 
466
479
  const read = (file) => ({
@@ -474,7 +487,13 @@ export function resolveSchemaArtifact(config) {
474
487
  return fs.existsSync(own) ? read(own) : null;
475
488
  }
476
489
 
477
- const version = config?.stats?.systemVersion;
490
+ // The version this build compiles *against*, for this system. A `systems:`
491
+ // entry is the per-system statement and wins; `stats.systemVersion` is the
492
+ // package-wide one and answers only for the package-wide system, which is
493
+ // what keeps a module still deriving from `relationships.systems` working.
494
+ const version =
495
+ config?.systems?.[systemId]?.compatibility?.verified ??
496
+ (systemId === config?.stats?.systemId ? config?.stats?.systemVersion : null);
478
497
  if (!version) return null;
479
498
  const cached = cachedSchemaPath(config, systemId, version);
480
499
  return fs.existsSync(cached) ? read(cached) : null;
@@ -556,22 +575,29 @@ export function emittedUndeclaredMessage(finding) {
556
575
  );
557
576
  }
558
577
 
559
- /** One resolved schema artifact per resolved configuration. */
578
+ /** One resolved schema artifact per configuration **and system**. */
560
579
  const artifacts = new WeakMap();
561
580
 
562
581
  /**
563
- * {@link resolveSchemaArtifact}, read once per configuration.
582
+ * {@link resolveSchemaArtifact}, read once per configuration and system.
564
583
  *
565
- * The per-note check below runs thousands of times in a build and the artifact
584
+ * The per-note check below runs thousands of times in a build and an artifact
566
585
  * never changes inside one, so reading and parsing it per note would be a
567
586
  * megabyte of JSON per hundred documents for an answer that is already known.
587
+ * Keyed by system as well as by configuration since #139: a build with two
588
+ * systems has two artifacts, and caching one of them under the configuration
589
+ * alone would hand every pass whichever system asked first.
568
590
  *
569
591
  * @param {object} config - The resolved build configuration.
592
+ * @param {string|null} [system] - The system whose schema is wanted.
570
593
  * @returns {{artifact: SchemaArtifact, source: string}|null} The schema.
571
594
  */
572
- function schemaFor(config) {
573
- if (!artifacts.has(config)) artifacts.set(config, resolveSchemaArtifact(config));
574
- return artifacts.get(config);
595
+ function schemaFor(config, system = undefined) {
596
+ let bySystem = artifacts.get(config);
597
+ if (!bySystem) artifacts.set(config, (bySystem = new Map()));
598
+ const key = system === undefined ? null : system;
599
+ if (!bySystem.has(key)) bySystem.set(key, resolveSchemaArtifact(config, system));
600
+ return bySystem.get(key);
575
601
  }
576
602
 
577
603
  /**
@@ -600,18 +626,21 @@ function schemaFor(config) {
600
626
  * @param {string} opts.block - The system block to read, e.g. `"sohl"`.
601
627
  * @param {string} opts.documentType - `Item`, `Actor`, …
602
628
  * @param {string} opts.subType - The document subtype the note compiles into.
629
+ * @param {string} [opts.system] - The system whose published schema to read,
630
+ * where a build has more than one (#139). Defaults to the package-wide
631
+ * `stats.systemId`.
603
632
  * @param {object} [opts.config] - The resolved build configuration.
604
633
  * @returns {{path: string, message: string}[]} One finding per undeclared path,
605
634
  * shallowest-first.
606
635
  */
607
636
  export function checkAuthoredSystemData(
608
637
  fm,
609
- { block, documentType, subType, config = loadPackConfig() },
638
+ { block, documentType, subType, system = undefined, config = loadPackConfig() },
610
639
  ) {
611
640
  const data = systemData(fm, block);
612
641
  if (!Object.keys(data).length) return [];
613
642
 
614
- const schema = schemaFor(config);
643
+ const schema = schemaFor(config, system);
615
644
  if (!schema) return [];
616
645
  const declared = declaredFields(schema.artifact, documentType, subType);
617
646
  if (!declared) return [];
@@ -651,18 +680,30 @@ export function checkAuthoredSystemData(
651
680
  * @param {string} opts.type - The note's content type, for the message.
652
681
  * @param {readonly {to?: string}[]} [opts.fields] - The type's field
653
682
  * declaration, which decides each finding's origin.
683
+ * @param {string} [opts.system] - The system whose published schema to read,
684
+ * where a build has more than one (#139). Defaults to the package-wide
685
+ * `stats.systemId`.
654
686
  * @param {object} [opts.config] - The resolved build configuration.
655
687
  * @returns {(EmissionFinding & {message: string})[]} One per undeclared path.
656
688
  */
657
689
  export function checkEmittedSystemData(
658
- system,
659
- { fm, block, documentType, subType, type, fields, config = loadPackConfig() },
690
+ emitted,
691
+ {
692
+ fm,
693
+ block,
694
+ documentType,
695
+ subType,
696
+ type,
697
+ fields,
698
+ system = undefined,
699
+ config = loadPackConfig(),
700
+ },
660
701
  ) {
661
- const schema = schemaFor(config);
702
+ const schema = schemaFor(config, system);
662
703
  if (!schema) return [];
663
704
 
664
705
  return compareEmittedSystem({
665
- system,
706
+ system: emitted,
666
707
  artifact: schema.artifact,
667
708
  documentType,
668
709
  subtype: subType,
@@ -55,11 +55,14 @@ import { protectCode } from "./code-fences.mjs";
55
55
  import { expandContentTables } from "./content-tables.mjs";
56
56
  import { buildSiteIndex, wikiContext } from "./site-index.mjs";
57
57
  import { frontmatterWikilinks, resolveWebWikilinks } from "./web-wikilinks.mjs";
58
- import { loadForeignManifests, manifestsComplete } from "./kb-manifest.mjs";
59
- import { formatUnaddressableFinding, unaddressableForeignPackages } from "./foreign-manifests.mjs";
58
+ import { loadForeignIndexes } from "./metadata-index.mjs";
59
+ import { formatUnaddressableFinding, unaddressableForeignPackages } from "./metadata-index.mjs";
60
60
  import { deriveBeingInfo, isBeing } from "../sohl/being-info.mjs";
61
61
  import { loadPackConfig } from "./pack-config.mjs";
62
62
  import { searchableFrontmatter } from "./note-package.mjs";
63
+ // The corpus, from the one pass that derives it (#243).
64
+ import { indexRecordsFor } from "./content-index.mjs";
65
+ import { isNoteRecord, noteFile } from "./index-records.mjs";
63
66
  import {
64
67
  checkHomepageCount,
65
68
  homepageDestination,
@@ -102,6 +105,50 @@ export function walkSiteTree(dir, skip = []) {
102
105
  return out;
103
106
  }
104
107
 
108
+ /**
109
+ * The content-tree files this build publishes from, in the order it emits them.
110
+ *
111
+ * **The corpus comes from the content index** (#243) — the same derivation the
112
+ * packs are compiled from — so the site and the packs cannot disagree about
113
+ * which files are the content. The note is still read for its `{fm, body}`: the
114
+ * index carries no note text, and a page *is* its text.
115
+ *
116
+ * **The order changes, and that is the point of stating it here.** The walk this
117
+ * replaces yielded directory order, and this module kept it deliberately —
118
+ * "a site's emitted pages should not reorder for no reason". Records are in
119
+ * content-path order, which is the same set in a different sequence. Nothing
120
+ * downstream depends on it any more: the first-writer-wins fallbacks that made
121
+ * order load-bearing went with the bare `[[Name]]` form (#180), each page is
122
+ * emitted to its own file at an address derived from its frontmatter, and the
123
+ * one place order could still show — a section's page list — is sorted by the
124
+ * theme. Verified rather than argued: over `sohl`'s tree the emitted mount is
125
+ * byte-identical, all 1,749 files.
126
+ *
127
+ * A **content-path** order is also the better of the two. Directory-read order
128
+ * is a fact about the filesystem, not about the content, so it can differ
129
+ * between two checkouts of one tree; this order cannot.
130
+ *
131
+ * `collectTreePages` is deliberately **not** converted: it walks an auxiliary
132
+ * tree (`site.trees`, the developer docs), which is not the content tree and
133
+ * appears in no record.
134
+ *
135
+ * @param {string} contentBase - Root of the content tree.
136
+ * @param {object} ctx - The build context. `ctx.records` is the corpus when the
137
+ * caller already derived it — the site build derives one and hands it to both
138
+ * collectors, so the two cannot disagree.
139
+ * @returns {string[]} Absolute paths, in emission order.
140
+ */
141
+ function siteCorpusFiles(contentBase, ctx) {
142
+ const records =
143
+ ctx.records ??
144
+ indexRecordsFor({
145
+ contentBase,
146
+ config: ctx.config,
147
+ skipDirectories: ctx.skipDirectories,
148
+ });
149
+ return records.filter(isNoteRecord).map((record) => noteFile(contentBase, record));
150
+ }
151
+
105
152
  /**
106
153
  * Reads a note, returning `null` for one that cannot be parsed.
107
154
  *
@@ -145,7 +192,7 @@ export function collectContentPages(contentBase, ctx) {
145
192
  const addressFindings = [];
146
193
  const fmLinkFindings = [];
147
194
 
148
- for (const file of walkSiteTree(contentBase, ctx.skipDirectories)) {
195
+ for (const file of siteCorpusFiles(contentBase, ctx)) {
149
196
  const note = readNote(file);
150
197
  if (!note) continue;
151
198
  const { fm, body } = note;
@@ -304,7 +351,7 @@ export function collectTreePages(tree, ctx) {
304
351
  export function collectHomepages(contentBase, ctx) {
305
352
  const pages = [];
306
353
  const addressFindings = [];
307
- for (const file of walkSiteTree(contentBase, ctx.skipDirectories)) {
354
+ for (const file of siteCorpusFiles(contentBase, ctx)) {
308
355
  const note = readNote(file);
309
356
  if (!note || !isHomepage(note.fm)) continue;
310
357
  try {
@@ -380,10 +427,10 @@ export function writeHomepages(outRoot, pages, config) {
380
427
  * @param {object[]} pages - Every page, from both walks.
381
428
  * @param {object} findings - `{ addressFindings, fmLinkFindings }` from
382
429
  * collection.
383
- * @param {object} options - `{ manifestDir }`.
430
+ * @param {object} options - `{ config }`.
384
431
  * @returns {object} The gate results and, when they pass, the built index.
385
432
  */
386
- export function siteGates(pages, findings, { manifestDir }) {
433
+ export function siteGates(pages, findings, { config }) {
387
434
  const out = {
388
435
  // Always empty here: the homepage count is decided in `buildSite`
389
436
  // before the content walk, and a failing count returns without ever
@@ -406,7 +453,7 @@ export function siteGates(pages, findings, { manifestDir }) {
406
453
  // configured list instead silently discarded the manifest of any package
407
454
  // the list named but the tree did not contain.
408
455
  const localPackages = new Set(content.map((p) => p.pkg));
409
- const foreign = loadForeignManifests(manifestDir, localPackages);
456
+ const foreign = loadForeignIndexes(config, localPackages);
410
457
  out.foreign = foreign;
411
458
  if (foreign.stale.length) {
412
459
  out.staleManifests = foreign.stale;
@@ -416,7 +463,6 @@ export function siteGates(pages, findings, { manifestDir }) {
416
463
  out.unaddressable = unaddressableForeignPackages(foreign.index);
417
464
  if (out.unaddressable.length) return out;
418
465
 
419
- out.manifests = manifestsComplete(localPackages, foreign.packages);
420
466
  const index = buildSiteIndex(pages, { foreignIndex: foreign.index });
421
467
  out.conflicts = index.conflicts;
422
468
  if (out.conflicts.length) return out;
@@ -658,6 +704,7 @@ export function renderPages(pages, options) {
658
704
  readmeSections,
659
705
  decorate,
660
706
  linkable = (d) => Boolean(d.fm.shortcode),
707
+ sqlTables,
661
708
  } = options;
662
709
 
663
710
  const tableErrors = [];
@@ -692,6 +739,10 @@ export function renderPages(pages, options) {
692
739
  docs: universe.get(page.pkg) ?? [],
693
740
  linkable,
694
741
  source: src,
742
+ // Prepared before this render began — DuckDB is async and this
743
+ // is not. Keyed by the note's own file, absolute here as in
744
+ // every other pass, so the three cannot disagree about a note.
745
+ sqlTables: sqlTables?.get(page.file),
695
746
  self: {
696
747
  fm: searchableFrontmatter(page.fm, page.pkg),
697
748
  path: page.relPath,
@@ -898,7 +949,7 @@ export function resolveOutputRoot(rootDir, out) {
898
949
  * @returns {{gates: object, stats: object|null, tableErrors: object[],
899
950
  * wikiErrors: object[], manifests: object|null}}
900
951
  */
901
- export function buildSite({ config, outRoot } = {}) {
952
+ export function buildSite({ config, outRoot, sqlTables } = {}) {
902
953
  const resolved = config ?? loadPackConfig();
903
954
  const site = resolved.site;
904
955
  const scheme = resolved.publish.address;
@@ -940,6 +991,15 @@ export function buildSite({ config, outRoot } = {}) {
940
991
  // retired, so this is the only source of it (#56).
941
992
  contentPackage: resolved.contentPackage,
942
993
  skipDirectories: resolved.skipDirectories,
994
+ config: resolved,
995
+ // The corpus, derived once for this build and handed to both
996
+ // collectors — the homepage pass and the content pass read one answer
997
+ // about which files the content is, rather than walking twice (#243).
998
+ records: indexRecordsFor({
999
+ contentBase: resolved.paths.content,
1000
+ config: resolved,
1001
+ skipDirectories: resolved.skipDirectories,
1002
+ }),
943
1003
  // Where the package is served, which is where an addressed page
944
1004
  // publishes: an address is `(type, shortcode)`, a package-wide
945
1005
  // identity that takes no content mount (#181).
@@ -1041,7 +1101,7 @@ export function buildSite({ config, outRoot } = {}) {
1041
1101
  const gates = siteGates(
1042
1102
  [...pages, ...homepageEntries],
1043
1103
  { ...content, fmLinkFindings },
1044
- { manifestDir: resolved.paths.manifests },
1104
+ { config: resolved },
1045
1105
  );
1046
1106
  if (gatesFailed(gates)) {
1047
1107
  return {
@@ -1049,7 +1109,6 @@ export function buildSite({ config, outRoot } = {}) {
1049
1109
  stats: null,
1050
1110
  tableErrors: [],
1051
1111
  wikiErrors: [],
1052
- manifests: gates.manifests,
1053
1112
  };
1054
1113
  }
1055
1114
 
@@ -1060,6 +1119,7 @@ export function buildSite({ config, outRoot } = {}) {
1060
1119
 
1061
1120
  const rendered = renderPages(pages, {
1062
1121
  outRoot: out,
1122
+ sqlTables,
1063
1123
  index: gates.index,
1064
1124
  foreign: gates.foreign,
1065
1125
  universe: tableUniverse(pages),
@@ -1088,7 +1148,6 @@ export function buildSite({ config, outRoot } = {}) {
1088
1148
 
1089
1149
  return {
1090
1150
  gates,
1091
- manifests: gates.manifests,
1092
1151
  tableErrors: rendered.tableErrors,
1093
1152
  wikiErrors: rendered.wikiErrors,
1094
1153
  stats: {
@@ -30,8 +30,11 @@
30
30
  * **Two key spaces, one map**, and both are addresses. `section/slug` and
31
31
  * `type/shortcode` are unique by construction, so they always resolve.
32
32
  * `type/shortcode` is the authored form; the canonical
33
- * `package-type-shortcode` is set alongside it, which is what a cross-package
34
- * link and every merged foreign entry use (#1499).
33
+ * `package-system-type-shortcode` is set alongside it, which is what a
34
+ * cross-package link and every merged foreign entry use (#1499, #59). A
35
+ * cross-package target states its package and usually not its system, so it is
36
+ * matched by the segments it supplies rather than fetched by an exact key —
37
+ * one hit resolves, and anything else is a finding.
35
38
  *
36
39
  * **A page's *name* is not a key** (#180). It was, as one of a set of
37
40
  * collision-aware fallbacks a bare `[[Name]]` was looked up in — which is what
@@ -48,7 +51,9 @@
48
51
 
49
52
  import path from "node:path";
50
53
 
51
- import { canonicalKey, readCanonicalKey } from "./kb-manifest.mjs";
54
+ import { canonicalKey, readCanonicalKey } from "./content-address.mjs";
55
+ import { systemOf } from "./document-subtypes.mjs";
56
+ import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./note-claims.mjs";
52
57
  import { hasDocEntry } from "./item-docs.mjs";
53
58
  import { contentPackage } from "./content-package.mjs";
54
59
  // The declared tag vocabulary (#172), which is where `draft` is stated.
@@ -158,7 +163,7 @@ function mergeForeign(index, foreignIndex) {
158
163
  * each already knowing its own `url`.
159
164
  * @param {object} [options] - Cross-package inputs.
160
165
  * @param {Map<string, {package: string, type?: string}>} [options.foreignIndex]
161
- * The merged index from `loadForeignManifests`. Omit when the build publishes
166
+ * The merged index from `loadForeignIndexes`. Omit when the build publishes
162
167
  * no cross-package links.
163
168
  * @returns {SiteIndex} The index, and what could not be addressed unambiguously.
164
169
  */
@@ -208,8 +213,8 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
208
213
  }
209
214
 
210
215
  // Merged *before* the local type-scoped pass below, so a local page always
211
- // ends up owning its own canonical `package-type-shortcode` address: the
212
- // local write lands last and wins. `loadForeignManifests` already excludes
216
+ // ends up owning its own canonical `package-system-type-shortcode` address: the
217
+ // local write lands last and wins. `loadForeignIndexes` already excludes
213
218
  // the local packages, so a manifest should never carry one — this is what
214
219
  // makes that a belt-and-braces rather than the only thing standing between
215
220
  // a stale vendored manifest and a shadowed local page.
@@ -241,7 +246,15 @@ export function buildSiteIndex(entries, { foreignIndex = new Map() } = {}) {
241
246
  // The page's package is the configured one — the site collection
242
247
  // resolves it and records it as `pkg`. Never read out of
243
248
  // frontmatter: `package:` is retired (#56).
244
- index.set(canonicalKey(e.pkg ?? ownPackage, type, shortcode), value);
249
+ index.set(
250
+ canonicalKey(
251
+ e.pkg ?? ownPackage,
252
+ systemOf(type, KNOWN_DOCUMENT_SUBTYPE_MAPS),
253
+ type,
254
+ shortcode,
255
+ ),
256
+ value,
257
+ );
245
258
  if (e.pkg) packages.add(e.pkg);
246
259
  // In Foundry an item and its documentation are two documents, so
247
260
  // `skill/wpnc` and `docskill/wpnc` are two UUIDs (#1362). Here the