@heroiclands/package-build 20.3.1 → 20.5.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 (195) hide show
  1. package/CHANGELOG.md +351 -0
  2. package/CONTENT.md +322 -110
  3. package/README.md +21 -10
  4. package/bin/content-build.mjs +204 -92
  5. package/bin/package-build.mjs +48 -15
  6. package/config.mjs +6 -8
  7. package/content-config.mjs +518 -151
  8. package/docs/api.md +1352 -0
  9. package/docs/commands.md +1609 -0
  10. package/docs/configuration.md +1432 -0
  11. package/docs/content-format.md +73 -74
  12. package/docs/diagnostics.md +356 -0
  13. package/docs/getting-started.md +813 -0
  14. package/docs/project-setup.md +469 -0
  15. package/e2e.mjs +4 -4
  16. package/engine/actor-compiler.mjs +64 -61
  17. package/engine/address-charset.mjs +6 -6
  18. package/engine/address-diff.mjs +57 -52
  19. package/engine/anchored-sections.mjs +1 -1
  20. package/engine/anchors.mjs +1 -1
  21. package/engine/base-compiler.mjs +42 -38
  22. package/engine/bundle-notes.mjs +4 -4
  23. package/engine/bundles.mjs +17 -8
  24. package/engine/code-fences.mjs +3 -3
  25. package/engine/compendiums.mjs +8 -8
  26. package/engine/compile-corpus.mjs +4 -5
  27. package/engine/content-address.mjs +24 -24
  28. package/engine/content-charset.mjs +1 -1
  29. package/engine/content-format-check.mjs +24 -25
  30. package/engine/content-format.mjs +8 -8
  31. package/engine/content-html.mjs +202 -0
  32. package/engine/content-icons.mjs +196 -165
  33. package/engine/content-index.mjs +61 -40
  34. package/engine/content-links.mjs +74 -41
  35. package/engine/content-lint.mjs +64 -34
  36. package/engine/content-package.mjs +7 -9
  37. package/engine/content-slug.mjs +2 -2
  38. package/engine/content-tables.mjs +38 -34
  39. package/engine/content-tree.mjs +1 -1
  40. package/engine/derived-fields.mjs +174 -0
  41. package/engine/diagnostics.mjs +3 -4
  42. package/engine/document-subtypes.mjs +6 -7
  43. package/engine/field-reference.mjs +8 -8
  44. package/engine/field-spec.mjs +29 -30
  45. package/engine/folder-notes.mjs +18 -16
  46. package/engine/foreign-catalog.mjs +8 -9
  47. package/engine/foundry-entries.mjs +19 -20
  48. package/engine/frontmatter-lint.mjs +95 -103
  49. package/engine/frontmatter.mjs +11 -11
  50. package/engine/generate.mjs +44 -39
  51. package/engine/helpers.mjs +147 -84
  52. package/engine/homepage.mjs +20 -22
  53. package/engine/ids.mjs +15 -15
  54. package/engine/index-records.mjs +4 -4
  55. package/engine/index.mjs +26 -10
  56. package/engine/item-compiler.mjs +54 -29
  57. package/engine/item-docs.mjs +11 -13
  58. package/engine/item-registry.mjs +13 -13
  59. package/engine/journals.mjs +24 -15
  60. package/engine/macros.mjs +3 -3
  61. package/engine/map-notes.mjs +6 -6
  62. package/engine/metadata-index.mjs +5 -5
  63. package/engine/note-claims.mjs +51 -41
  64. package/engine/note-ids.mjs +5 -5
  65. package/engine/note-package.mjs +4 -4
  66. package/engine/note-renames.mjs +3 -3
  67. package/engine/note-schemas.mjs +3 -8
  68. package/engine/note-vocabulary.mjs +56 -54
  69. package/engine/pack-config.mjs +39 -25
  70. package/engine/pack-router.mjs +0 -0
  71. package/engine/pdf-build.mjs +464 -0
  72. package/engine/pdf-fonts.mjs +420 -0
  73. package/engine/pdf-render.mjs +798 -0
  74. package/engine/pdf-toc.mjs +525 -0
  75. package/engine/prose-config.mjs +9 -9
  76. package/engine/prose-lint.mjs +12 -13
  77. package/engine/region-events.mjs +2 -2
  78. package/engine/retired-fields.mjs +24 -22
  79. package/engine/runtime-only-fields.mjs +1 -1
  80. package/engine/scene-levels.mjs +1 -1
  81. package/engine/scenes.mjs +33 -25
  82. package/engine/schema-check.mjs +21 -21
  83. package/engine/schema-extract.mjs +1 -1
  84. package/engine/site-build.mjs +66 -50
  85. package/engine/site-index.mjs +17 -18
  86. package/engine/sql-tables.mjs +21 -13
  87. package/engine/subtype-registry.mjs +5 -4
  88. package/engine/system-block.mjs +18 -19
  89. package/engine/systems.mjs +2 -2
  90. package/engine/web-wikilinks.mjs +41 -38
  91. package/engine/wikilink-syntax.mjs +16 -16
  92. package/engine/wikilinks.mjs +40 -42
  93. package/hm3/actors.mjs +30 -22
  94. package/hm3/document-subtypes.mjs +5 -5
  95. package/hm3/index.mjs +2 -2
  96. package/hm3/item-builders.mjs +1 -1
  97. package/hm3/item-fields.mjs +3 -3
  98. package/hm3/items.mjs +28 -8
  99. package/hm3/template-priority.mjs +2 -2
  100. package/lang.mjs +3 -3
  101. package/manifest.mjs +17 -20
  102. package/package.json +1 -2
  103. package/release.mjs +66 -6
  104. package/sohl/actors.mjs +36 -28
  105. package/sohl/affiliation-standings.mjs +2 -2
  106. package/sohl/being-info.mjs +5 -5
  107. package/sohl/default-item-art.mjs +5 -5
  108. package/sohl/document-subtypes.mjs +5 -5
  109. package/sohl/index.mjs +3 -3
  110. package/sohl/item-builders.mjs +7 -7
  111. package/sohl/item-fields.mjs +8 -9
  112. package/sohl/items.mjs +28 -6
  113. package/sohl/kb-passes.mjs +5 -5
  114. package/sohl/note-schemas.mjs +12 -12
  115. package/sohl/skill-base.mjs +1 -1
  116. package/types/content-config.d.mts +91 -39
  117. package/types/e2e.d.mts +3 -3
  118. package/types/engine/actor-compiler.d.mts +45 -40
  119. package/types/engine/address-charset.d.mts +6 -6
  120. package/types/engine/address-diff.d.mts +60 -6
  121. package/types/engine/base-compiler.d.mts +27 -21
  122. package/types/engine/bundle-notes.d.mts +3 -3
  123. package/types/engine/bundles.d.mts +10 -1
  124. package/types/engine/code-fences.d.mts +3 -3
  125. package/types/engine/compendiums.d.mts +3 -3
  126. package/types/engine/compile-corpus.d.mts +1 -1
  127. package/types/engine/content-address.d.mts +20 -20
  128. package/types/engine/content-format-check.d.mts +6 -6
  129. package/types/engine/content-format.d.mts +2 -2
  130. package/types/engine/content-html.d.mts +78 -0
  131. package/types/engine/content-icons.d.mts +139 -92
  132. package/types/engine/content-index.d.mts +59 -15
  133. package/types/engine/content-links.d.mts +8 -8
  134. package/types/engine/content-lint.d.mts +8 -6
  135. package/types/engine/content-package.d.mts +6 -8
  136. package/types/engine/content-tables.d.mts +49 -18
  137. package/types/engine/derived-fields.d.mts +101 -0
  138. package/types/engine/diagnostics.d.mts +2 -2
  139. package/types/engine/document-subtypes.d.mts +3 -3
  140. package/types/engine/field-spec.d.mts +37 -39
  141. package/types/engine/folder-notes.d.mts +4 -5
  142. package/types/engine/foreign-catalog.d.mts +3 -3
  143. package/types/engine/foundry-entries.d.mts +6 -7
  144. package/types/engine/frontmatter-lint.d.mts +10 -9
  145. package/types/engine/frontmatter.d.mts +7 -7
  146. package/types/engine/generate.d.mts +7 -7
  147. package/types/engine/helpers.d.mts +128 -56
  148. package/types/engine/homepage.d.mts +16 -18
  149. package/types/engine/ids.d.mts +13 -13
  150. package/types/engine/index-records.d.mts +3 -3
  151. package/types/engine/index.d.mts +6 -0
  152. package/types/engine/item-compiler.d.mts +21 -5
  153. package/types/engine/item-docs.d.mts +2 -2
  154. package/types/engine/item-registry.d.mts +6 -6
  155. package/types/engine/journals.d.mts +12 -3
  156. package/types/engine/map-notes.d.mts +2 -2
  157. package/types/engine/metadata-index.d.mts +4 -4
  158. package/types/engine/note-claims.d.mts +30 -22
  159. package/types/engine/note-ids.d.mts +4 -4
  160. package/types/engine/note-package.d.mts +1 -1
  161. package/types/engine/note-renames.d.mts +3 -3
  162. package/types/engine/note-vocabulary.d.mts +31 -203
  163. package/types/engine/pack-config.d.mts +7 -7
  164. package/types/engine/pack-router.d.mts +1 -1
  165. package/types/engine/pdf-build.d.mts +42 -0
  166. package/types/engine/pdf-fonts.d.mts +30 -0
  167. package/types/engine/pdf-render.d.mts +144 -0
  168. package/types/engine/pdf-toc.d.mts +114 -0
  169. package/types/engine/prose-config.d.mts +9 -9
  170. package/types/engine/prose-lint.d.mts +3 -4
  171. package/types/engine/region-events.d.mts +2 -2
  172. package/types/engine/retired-fields.d.mts +10 -9
  173. package/types/engine/scenes.d.mts +10 -1
  174. package/types/engine/schema-check.d.mts +13 -13
  175. package/types/engine/site-build.d.mts +52 -25
  176. package/types/engine/site-index.d.mts +3 -4
  177. package/types/engine/sql-tables.d.mts +11 -5
  178. package/types/engine/subtype-registry.d.mts +3 -3
  179. package/types/engine/system-block.d.mts +3 -3
  180. package/types/engine/web-wikilinks.d.mts +7 -7
  181. package/types/engine/wikilink-syntax.d.mts +17 -17
  182. package/types/engine/wikilinks.d.mts +14 -14
  183. package/types/hm3/actors.d.mts +9 -1
  184. package/types/hm3/document-subtypes.d.mts +1 -1
  185. package/types/hm3/items.d.mts +9 -2
  186. package/types/hm3/template-priority.d.mts +1 -1
  187. package/types/manifest.d.mts +8 -8
  188. package/types/release.d.mts +15 -4
  189. package/types/sohl/actors.d.mts +12 -4
  190. package/types/sohl/affiliation-standings.d.mts +2 -2
  191. package/types/sohl/being-info.d.mts +2 -2
  192. package/types/sohl/document-subtypes.d.mts +1 -1
  193. package/types/sohl/items.d.mts +9 -2
  194. package/types/sohl/note-schemas.d.mts +1 -1
  195. package/MIGRATING.md +0 -608
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  /**
15
- * Emitting this package's content index (#224).
15
+ * Emitting this package's content index.
16
16
  *
17
17
  * Every content build already walks the whole note tree and parses every note's
18
18
  * frontmatter — the pack compilers, the site build, and the content-table
@@ -21,7 +21,7 @@
21
21
  * "which beings carry no `kbcat`?", "what does this table actually select?",
22
22
  * "did that type rename leave anything behind?" have no answer short of writing
23
23
  * a throwaway script that re-walks the tree. Eight dead Bestiary tables shipped
24
- * for weeks behind exactly that gap (#223).
24
+ * for weeks behind exactly that gap.
25
25
  *
26
26
  * This module publishes the walk. One line of JSON per note, in
27
27
  * [JSON Lines](https://jsonlines.org/) — the whole frontmatter, plus where the
@@ -62,7 +62,7 @@
62
62
  * to every player, and a build artifact has no business there.
63
63
  *
64
64
  * **What it deliberately does not carry: the note's text, and positions within
65
- * it.** #243 asks whether the index should record a position for every
65
+ * it.** The question is whether the index should record a position for every
66
66
  * frontmatter key, so that a pass reading the index could report a field defect
67
67
  * without opening the file. It should not, and the numbers are not close: over
68
68
  * `sohl`'s 1,685 notes the index is 3.0 MB and holds 50,598 leaf values, so a
@@ -94,7 +94,7 @@ import unidecode from "unidecode";
94
94
  import { metadataFileName } from "./metadata-index.mjs";
95
95
  import { addressSlug, canonicalKey } from "./content-address.mjs";
96
96
  // One reader for a note's anchors, shared with the link checker and with the
97
- // builds that emit a link (#243). Re-exported because this is where callers
97
+ // builds that emit a link. Re-exported because this is where callers
98
98
  // have always addressed it.
99
99
  import { collectAnchors } from "./anchors.mjs";
100
100
  import { subtypeRow, NO_SYSTEM, systemOf } from "./document-subtypes.mjs";
@@ -105,7 +105,7 @@ import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./note-claims.mjs";
105
105
  *
106
106
  * The specification's word, not this module's: the canonical address carries it
107
107
  * in the same position — `harnadventures-none-being-grod` — so the index and
108
- * the address say "no system" the same way (#59).
108
+ * the address say "no system" the same way.
109
109
  *
110
110
  * @type {string}
111
111
  */
@@ -115,12 +115,12 @@ import { entriesForNote, foundryIdentities } from "./foundry-entries.mjs";
115
115
  import { walkMarkdownTree } from "./helpers.mjs";
116
116
  import { resolveNoteId } from "./note-ids.mjs";
117
117
  // The retired-field refusal and the key locator, so a note authoring a derived
118
- // key is reported where it is rather than as a bare abort (#243).
118
+ // key is reported where it is rather than as a bare abort.
119
119
  import { assertNoDeclaredPackage } from "./note-package.mjs";
120
120
  import { locateFrontmatterKey } from "./retired-fields.mjs";
121
121
  import { loadPackConfig } from "./pack-config.mjs";
122
122
  // The record accessors, which live apart so that a module the compilers load
123
- // can read a record without importing this one and closing a cycle (#243).
123
+ // can read a record without importing this one and closing a cycle.
124
124
  // Re-exported because this is where callers have always addressed them.
125
125
  import { authoredFrontmatter, DERIVED_KEYS, isNoteRecord, noteFile } from "./index-records.mjs";
126
126
 
@@ -259,19 +259,6 @@ export function asciiAliases(aliases) {
259
259
  return aliases.map((alias) => asciiName(alias)).filter((alias) => alias !== null);
260
260
  }
261
261
 
262
- /**
263
- * Build one index record from a note's frontmatter and its place in the tree.
264
- *
265
- * @param {object} options - Options.
266
- * @param {Record<string, any>} options.frontmatter - The note's parsed frontmatter.
267
- * @param {string} options.relPath - Its path below the content root, POSIX-separated.
268
- * @param {string} options.contentPackage - The package the tree compiles as.
269
- * @param {string} [options.body] - The note's markdown body, for its anchors.
270
- * @param {number} [options.bodyLine] - The 1-based file line the body starts on.
271
- * @returns {Record<string, any>} The record, keys sorted at every depth.
272
- * @throws {Error} When the note carries a key this module derives, which would
273
- * otherwise be overwritten without a word.
274
- */
275
262
  /**
276
263
  * This note's Foundry addresses, or `null` where it has none.
277
264
  *
@@ -281,7 +268,7 @@ export function asciiAliases(aliases) {
281
268
  * plus the pack router — frontmatter and configuration, nothing from a compiled
282
269
  * pack — so the index's frontmatter walk already has every input. Deriving it
283
270
  * twice is how two artifacts describing one note start disagreeing, which is
284
- * the failure the merge is meant to end (#239).
271
+ * the failure the merge is meant to end.
285
272
  *
286
273
  * The shape flattens the manifest's *two* entries for an item note onto the one
287
274
  * record the index keeps per note. An item compiles into a document **and** a
@@ -339,7 +326,7 @@ function foundryEntries({ frontmatter, address, body, manifest }) {
339
326
  * shipped map produced and said nothing about the other.
340
327
  *
341
328
  * Only `sohl` can appear today, because `KNOWN_DOCUMENT_SUBTYPE_MAPS` holds one
342
- * map and #139 tracks the missing `hm3/` half. The shape is system-keyed now so
329
+ * map, the `hm3/` half being separate. The shape is system-keyed so
343
330
  * that adding it is one more key rather than a second breaking change to an
344
331
  * artifact consumers have already started reading.
345
332
  *
@@ -369,7 +356,7 @@ function foundryBlock(entry, system) {
369
356
  * Refuse a note that authors a key the index derives, and say where.
370
357
  *
371
358
  * **Located, because every reader of the index is now a reporter of this.**
372
- * Until #243 the only pass that built a record was the emitter, so aborting
359
+ * With the emitter the only pass building a record, aborting
373
360
  * with a bare message was the whole story. Now the link check and the address
374
361
  * diff read the index too, and a bare abort in one of them reports *nothing*
375
362
  * about the tree — the one malformed note takes every other finding with it,
@@ -378,7 +365,7 @@ function foundryBlock(entry, system) {
378
365
  * can emit `file:line:column: error: …` like any other finding.
379
366
  *
380
367
  * **`package:` keeps its own words.** It is not a name collision but a *retired
381
- * field* (#56), and the correction is to delete it, not to rename it — which is
368
+ * field*, and the correction is to delete it, not to rename it — which is
382
369
  * what {@link module:engine/note-package.assertNoDeclaredPackage} has always
383
370
  * said, and had no caller to say it to. Deferring to it means one message for
384
371
  * one mistake rather than two that contradict each other about the fix.
@@ -410,6 +397,29 @@ function assertNoDerivedKeys(frontmatter, relPath, absPath, contentPackage) {
410
397
  }
411
398
  }
412
399
 
400
+ /**
401
+ * Build one index record from a note's frontmatter and its place in the tree.
402
+ *
403
+ * The frontmatter as authored, plus what the index derives from it: the package
404
+ * it compiles as, its address, ASCII folds of its name and aliases, the anchors
405
+ * of its body, its Foundry block, the address of its documentation journal, and
406
+ * where the file sits within the tree.
407
+ *
408
+ * @param {object} options - Options.
409
+ * @param {Record<string, any>} options.frontmatter - The note's parsed frontmatter.
410
+ * @param {string} options.relPath - Its path below the content root, POSIX-separated.
411
+ * @param {string} [options.absPath] - The file, read only on the failing path to
412
+ * locate the offending key.
413
+ * @param {string} options.contentPackage - The package the tree compiles as.
414
+ * @param {string} [options.body] - The note's markdown body, for its anchors.
415
+ * @param {number} [options.bodyLine] - The 1-based file line the body starts on.
416
+ * @param {object} [options.manifest] - The package manifest, which the Foundry
417
+ * entries are derived against.
418
+ * @returns {Record<string, any>} The record, keys sorted at every depth.
419
+ * @throws {Error} When the note carries a key this module derives, which would
420
+ * otherwise be overwritten without a word. `file` and, where the file was
421
+ * read, `position` ride on the error.
422
+ */
413
423
  export function buildIndexRecord({
414
424
  frontmatter,
415
425
  relPath,
@@ -466,16 +476,6 @@ export function buildIndexRecord({
466
476
  );
467
477
  }
468
478
 
469
- /**
470
- * Read a content tree into index records, in the order they will be written.
471
- *
472
- * @param {string} contentBase - The content tree to walk.
473
- * @param {object} options - Options.
474
- * @param {string} options.contentPackage - The package the tree compiles as.
475
- * @param {Array<string>} [options.skipDirectories] - Directory names to skip.
476
- * @returns {Array<Record<string, any>>} The records, in a total order that does
477
- * not depend on directory-read order.
478
- */
479
479
  /**
480
480
  * The record for an item note's **documentation journal**.
481
481
  *
@@ -517,7 +517,7 @@ function buildDocRecord({ frontmatter, address, entry, file, contentPackage, anc
517
517
  // The journal's own `_id`, taken from the entry rather than
518
518
  // re-derived: every entry the index gives an identity to publishes
519
519
  // both the id and the UUID, computed once by whatever owns that
520
- // entry's derivation (#310).
520
+ // entry's derivation.
521
521
  id: entry.id,
522
522
  nameAscii: asciiName(frontmatter?.name?.full),
523
523
  address: { slug: address.slug, canonical: entry.key },
@@ -531,13 +531,34 @@ function buildDocRecord({ frontmatter, address, entry, file, contentPackage, anc
531
531
  );
532
532
  }
533
533
 
534
+ /**
535
+ * Read a content tree into index records, in the order they will be written.
536
+ *
537
+ * An item note yields two records — the item, and the documentation journal
538
+ * that is a document in its own right.
539
+ *
540
+ * @param {string} contentBase - The content tree to walk.
541
+ * @param {object} options - Options.
542
+ * @param {string} options.contentPackage - The package the tree compiles as.
543
+ * @param {readonly string[]} options.skipDirectories - The walk's scope, stated
544
+ * by the caller. An absent one is the caller's omission, and
545
+ * {@link module:engine/helpers.walkMarkdownTree} throws on it.
546
+ * @param {object} [options.manifest] - The package manifest, which the Foundry
547
+ * entries are derived against.
548
+ * @param {object[]} [options.problems] - Supplied by a **reader**: a note that
549
+ * cannot be recorded is pushed here as a diagnostic and skipped. Omitted, the
550
+ * note throws — the contract the emitter needs, since an index missing a note
551
+ * asserts that it does not exist.
552
+ * @returns {Array<Record<string, any>>} The records, in a total order that does
553
+ * not depend on directory-read order.
554
+ */
534
555
  export function collectContentIndex(
535
556
  contentBase,
536
557
  { contentPackage, skipDirectories, manifest, problems },
537
558
  ) {
538
559
  const records = [];
539
560
  // Passed through rather than defaulted away: an absent scope is the
540
- // caller's omission, and `walkMarkdownTree` says so (#243).
561
+ // caller's omission, and `walkMarkdownTree` says so.
541
562
  const walkOpts = { skipDirectories };
542
563
 
543
564
  for (const { frontmatter, body, bodyLine, absPath } of walkMarkdownTree(
@@ -545,7 +566,7 @@ export function collectContentIndex(
545
566
  walkOpts,
546
567
  )) {
547
568
  const fm = frontmatter ?? {};
548
- // The id the note's document is filed under (#270), resolved before
569
+ // The id the note's document is filed under, resolved before
549
570
  // the record is built so the index publishes the address *and* the id
550
571
  // that address derives.
551
572
  resolveNoteId(fm, { pkg: contentPackage });
@@ -579,7 +600,7 @@ export function collectContentIndex(
579
600
  }
580
601
  records.push(record);
581
602
 
582
- // An item note is two documents, so it is two records (#239).
603
+ // An item note is two documents, so it is two records.
583
604
  const doc = foundryEntries({
584
605
  frontmatter: fm,
585
606
  address: record.address,
@@ -637,7 +658,7 @@ export function serializeContentIndex(records) {
637
658
  *
638
659
  * The half of {@link emitContentIndex} that derives rather than emits, so a
639
660
  * pass that needs the corpus in memory — a SQL content table, the link check,
640
- * and in time every reader #243 converts — builds it the same way the artifact
661
+ * and in time every converted reader — builds it the same way the artifact
641
662
  * is built, rather than by walking and parsing again with its own idea of the
642
663
  * scope.
643
664
  *
@@ -648,7 +669,7 @@ export function serializeContentIndex(records) {
648
669
  * caller that resolved one of its own; defaults to the resolved
649
670
  * configuration's. Stated separately from `config` because a caller that was
650
671
  * *handed* a scope must be able to pass it on rather than have it silently
651
- * replaced by the one its configuration happens to carry (#243).
672
+ * replaced by the one its configuration happens to carry.
652
673
  * @param {object[]} [opts.problems] - Supplied by a **reader**: a note that
653
674
  * cannot be recorded is pushed here as a diagnostic and skipped, instead of
654
675
  * aborting the derivation. Omitted, the note throws — which is the contract
@@ -27,9 +27,9 @@
27
27
  * written target is a *partial* address — the segments it omits are
28
28
  * wildcards, and the package it omits is this one — so a target resolving to
29
29
  * *several* notes is an ambiguity rather than a first match, and is reported
30
- * naming every candidate (#59).
30
+ * naming every candidate.
31
31
  * 3. **An unlabelled link.** `[[x]]` addresses nothing: the alias namespace it
32
- * used to name is retired (#180), and a shortcode is an address rather than
32
+ * once named is retired, and a shortcode is an address rather than
33
33
  * prose, so the link has neither a resolvable target nor text to show. The
34
34
  * correction is always `[[type-shortcode|Text]]`.
35
35
  * 4. **A wikilink authored in frontmatter.** Both builds walk a note's *body*
@@ -49,7 +49,7 @@
49
49
  * document is reachable from the book's root" — is a statement about what one
50
50
  * package publishes, not about the note format, so it belongs with the
51
51
  * publishing it describes; so does a retired hostname. Both are served by the
52
- * link graph returned here rather than implemented here (#20).
52
+ * link graph returned here rather than implemented here.
53
53
  *
54
54
  * @module
55
55
  */
@@ -61,7 +61,7 @@ import { matchAllOutsideCode } from "./code-fences.mjs";
61
61
  import { expandContentTables } from "./content-tables.mjs";
62
62
  import { collectAnchors } from "./anchors.mjs";
63
63
  // The corpus, and everything derived from it, read from the one place that
64
- // derives it (#243). Nothing in the index's own import graph reaches this
64
+ // derives it. Nothing in the index's own import graph reaches this
65
65
  // module, so this is a plain static import rather than the deferred one
66
66
  // `sql-tables` needs to keep out of the compilers' cycle.
67
67
  import { authoredFrontmatter, indexRecordsFor, isNoteRecord, noteFile } from "./content-index.mjs";
@@ -88,7 +88,7 @@ import { readQualifier } from "./wikilinks.mjs";
88
88
  * Every `{#anchor}` a note declares on a heading.
89
89
  *
90
90
  * **Read from the content index's reader, not a second one.** This module kept
91
- * its own until #243, and the two disagreed: it matched `{#([a-z0-9-]+)}` while
91
+ * its own, and the two disagreed: it matched `{#([a-z0-9-]+)}` while
92
92
  * {@link module:engine/content-index.collectAnchors} matches `{#([^}]+)}`, so
93
93
  * an anchor with a capital in it — `{#CalendarFormat}` — existed for the index
94
94
  * and for the compiler and did not exist for the link checker. Nothing links to
@@ -110,7 +110,7 @@ export function anchorsOf(body) {
110
110
  * Read a content tree into the index a link resolves against.
111
111
  *
112
112
  * **The corpus comes from the content index, not from a walk of this module's
113
- * own** (#243). Every pass used to answer "which files are the content?" for
113
+ * own**. Every pass used to answer "which files are the content?" for
114
114
  * itself and throw the answer away; this one now reads
115
115
  * {@link module:engine/content-index.indexRecordsFor}, which is the same
116
116
  * derivation the published artifact and the compilers are driven from. So a
@@ -118,7 +118,7 @@ export function anchorsOf(body) {
118
118
  * anchors it resolves against are the ones every other pass will emit — rather
119
119
  * than a second derivation that agrees with them only by inspection. That was
120
120
  * not hypothetical: this module carried its own anchor reader until the anchor
121
- * half of #243, and the two disagreed about which anchors existed.
121
+ * anchor half, and the two disagreed about which anchors existed.
122
122
  *
123
123
  * **The file is opened for its bytes and nothing else.** The index deliberately
124
124
  * carries no note *body*, and a link lives in the body — so each note is read
@@ -138,7 +138,7 @@ export function anchorsOf(body) {
138
138
  * @param {string} contentBase - Root of the content tree.
139
139
  * @param {object} [opts]
140
140
  * @param {object} [opts.config] - The resolved build configuration, whose
141
- * fetched dependency indexes foreign addresses resolve through (#239), and
141
+ * fetched dependency indexes foreign addresses resolve through, and
142
142
  * whose `contentPackage` every local address is built from. Omitted, the
143
143
  * ambient configuration is resolved and no cross-package address resolves.
144
144
  * @param {readonly string[]} [opts.skipDirectories] - The walk's scope, passed
@@ -162,10 +162,10 @@ export function buildLinkIndex(
162
162
 
163
163
  // The one package every note in this tree belongs to. Taken from the
164
164
  // configuration this build resolved — never from a note (`package:` is
165
- // retired, so there is no second source an address could disagree with,
166
- // #56) and never from the ambient one, which is a different configuration
165
+ // retired, so there is no second source an address could disagree with)
166
+ // and never from the ambient one, which is a different configuration
167
167
  // whenever a test injects one, `PACKAGE_BUILD_CONFIG` names one, or the
168
- // command runs from a worktree (#243).
168
+ // command runs from a worktree.
169
169
  const resolved = config ?? loadPackConfig();
170
170
  const pkg = resolved.contentPackage;
171
171
 
@@ -197,12 +197,12 @@ export function buildLinkIndex(
197
197
  notes.push(note);
198
198
 
199
199
  // The anchors the index recorded, rather than a second reading of the
200
- // same headings — the disagreement #243's anchor half removed.
200
+ // same headings — the disagreement the anchor half removes.
201
201
  anchors.set(note, new Set((record.anchors ?? []).map((a) => a.slug)));
202
202
 
203
203
  if (typeof fm.shortcode === "string" && fm.shortcode) {
204
204
  // Canonical addresses only. Every written target expands to one
205
- // before lookup (#336), so there is nothing left for a short key to
205
+ // before lookup, so there is nothing left for a short key to
206
206
  // answer — and the short key was harmful: `type/shortcode` is
207
207
  // system-blind, set with a plain `Map.set`, so two notes in one
208
208
  // package sharing a `(type, shortcode)` across systems silently
@@ -245,7 +245,7 @@ export function buildLinkIndex(
245
245
  /** The searchable universe a `dataview` table draws its rows from. */
246
246
  const tableDocs = notes.map((n) => ({
247
247
  // Package present for a `WHERE … package = "…"` clause, synthesised
248
- // rather than authored — see {@link searchableFrontmatter} (#56).
248
+ // rather than authored — see {@link searchableFrontmatter}.
249
249
  fm: searchableFrontmatter(n.fm, pkg),
250
250
  path: n.rel,
251
251
  tld: n.rel.split("/")[0],
@@ -259,14 +259,14 @@ export function buildLinkIndex(
259
259
  * @returns {Array<{target: string, anchor: string, text: string,
260
260
  * occurrence: number, labelled: boolean}>} `target` is `""` for a
261
261
  * same-page `[[#anchor]]`; `labelled` says whether the link carries the
262
- * `|` every link must have (#180).
262
+ * `|` every link must have.
263
263
  */
264
264
  function linksOf(note) {
265
265
  let body = note.body;
266
266
  if (/^[ \t]*(?:`{3,}|~{3,})[ \t]*(?:dataview|sql)\b/im.test(body)) {
267
267
  body = expandContentTables(body, {
268
268
  // Unfiltered: every note in the tree is this package's, so
269
- // there is no other package's note to exclude (#56).
269
+ // there is no other package's note to exclude.
270
270
  docs: tableDocs,
271
271
  linkable: (d) => Boolean(d.fm.shortcode),
272
272
  source: note.file,
@@ -301,22 +301,11 @@ export function buildLinkIndex(
301
301
  return out;
302
302
  }
303
303
 
304
- /**
305
- * The note an **address** names, or `undefined`.
306
- *
307
- * The qualifier is read with {@link readQualifier} rather than a second
308
- * copy of the rule, so this cannot drift from what the builds do — the two
309
- * separators, the first-hyphen split, and the optional leading package
310
- * segment.
311
- *
312
- * @param {string} target - The link target, anchor already removed.
313
- * @returns {object|undefined} The note it addresses.
314
- */
315
304
  /**
316
305
  * Every indexed entry an address names, matching only the segments it
317
306
  * supplies.
318
307
  *
319
- * This is the whole of #59's resolution rule in one place: a written
308
+ * This is the whole resolution rule in one place: a written
320
309
  * address is a *partial* one, unsupplied segments are wildcards, and the
321
310
  * caller requires exactly one hit. Nothing here decides an ambiguity — zero
322
311
  * and many are different findings with different fixes, so the count is
@@ -351,10 +340,24 @@ export function buildLinkIndex(
351
340
  return matchAddress([...byKey], q).map(([, v]) => v);
352
341
  }
353
342
 
343
+ /**
344
+ * The note an **address** names, or `undefined`.
345
+ *
346
+ * The qualifier is read with {@link readQualifier} rather than a second
347
+ * copy of the rule, so this cannot drift from what the builds do — the two
348
+ * separators, the first-hyphen split, and the optional leading package
349
+ * segment.
350
+ *
351
+ * @param {string} target - The link target, anchor already removed.
352
+ * @param {string} [keyPath] - The dotted frontmatter key path the link sits
353
+ * under, which supplies the system an omitted segment defaults to — see
354
+ * {@link blockSystem}. Body prose has none.
355
+ * @returns {object|undefined} The note it addresses.
356
+ */
354
357
  function resolveAddress(target, keyPath) {
355
358
  const qualified = readQualifier(target, types, packages);
356
359
  if (!qualified || qualified.reason) return undefined;
357
- // Every omitted segment defaults from where the link is written (#336),
360
+ // Every omitted segment defaults from where the link is written,
358
361
  // so the target expands to exactly one canonical address and this is a
359
362
  // plain lookup. There is no candidate set, and therefore no single-hit
360
363
  // rule and no ambiguity to report.
@@ -370,21 +373,24 @@ export function buildLinkIndex(
370
373
  * Every foreign manifest entry an address names, in package order.
371
374
  *
372
375
  * A written target is a **partial** address, so this matches on the
373
- * segments it supplies and wildcards the rest (#59). A target naming a
376
+ * segments it supplies and wildcards the rest. A target naming a
374
377
  * package necessarily names its system too — omission runs left to right —
375
378
  * so the fully qualified form matches at most one entry; a shorter one
376
379
  * names no package, and resolves against any foreign package that
377
380
  * publishes it. Either way only exactly one hit resolves. Two claimants make it ambiguous, which is a different finding
378
381
  * from resolving nowhere and has a different fix, so the count is returned
379
- * rather than collapsed here (#184).
382
+ * rather than collapsed here.
380
383
  *
381
384
  * @param {string} target - The link target.
385
+ * @param {string} [keyPath] - The dotted frontmatter key path the link sits
386
+ * under, which supplies the system an omitted segment defaults to — see
387
+ * {@link blockSystem}. Body prose has none.
382
388
  * @returns {object[]} The foreign entries, each carrying its `package`.
383
389
  */
384
390
  function foreignHits(target, keyPath) {
385
391
  const q = readQualifier(target, types, packages);
386
392
  if (!q || q.reason) return [];
387
- // An omitted package means *this* package (#336), so a short form
393
+ // An omitted package means *this* package, so a short form
388
394
  // addresses nothing foreign and never reaches a dependency's index.
389
395
  // Reaching another package is the fully qualified form's job, and
390
396
  // saying so is the whole point: a link that resolved into `sohl` only
@@ -411,6 +417,32 @@ export function buildLinkIndex(
411
417
  return hits.length === 1 ? hits[0] : null;
412
418
  }
413
419
 
420
+ /**
421
+ * The note or foreign entry a **frontmatter reference** names, or null.
422
+ *
423
+ * A `ref:` field holds a shortcode, not an address. The system persists it
424
+ * verbatim and resolves it at runtime against the items embedded on one
425
+ * actor, where packages do not exist: an actor assembled from several
426
+ * packages carries their items side by side. So a reference resolves when
427
+ * *any* reachable package declares the `(type, shortcode)` pair — package
428
+ * and system wildcarded, local notes first, then the fetched indexes.
429
+ *
430
+ * Distinct from {@link resolveAddress}, which defaults an omitted package
431
+ * to this one. That is the rule for a link, whose target is a document to
432
+ * point at; a reference names an item to stand beside.
433
+ *
434
+ * @param {string} target - The reference as `type-shortcode`.
435
+ * @returns {object|null} The note or foreign entry declaring it.
436
+ */
437
+ function referenceHit(target) {
438
+ const q = readQualifier(target, types, packages);
439
+ if (!q || q.reason) return null;
440
+ const local = matchAddress([...byKey], q);
441
+ if (local.length) return local[0][1];
442
+ const abroad = matchAddress([...foreign.index], q);
443
+ return abroad.length ? abroad[0][1] : null;
444
+ }
445
+
414
446
  return {
415
447
  notes,
416
448
  frontmatterLinks,
@@ -428,12 +460,13 @@ export function buildLinkIndex(
428
460
  /**
429
461
  * Resolve a link target the way both builds do, or `undefined`. Every
430
462
  * link is an address, so this is {@link resolveAddress} under the name
431
- * the walkers use (#180).
463
+ * the walkers use.
432
464
  */
433
465
  resolve: resolveAddress,
434
466
  resolveAddress,
435
467
  manifestHit,
436
468
  foreignHits,
469
+ referenceHit,
437
470
  /** Whether a target reads as a qualified address at all. */
438
471
  isAddress: (target) => Boolean(readQualifier(target, types, packages)),
439
472
  };
@@ -453,7 +486,7 @@ export function buildLinkIndex(
453
486
  const SITE_HOST = /^(?:[a-z0-9-]+\.)*heroiclands\.org$/i;
454
487
 
455
488
  /**
456
- * Every package landing this build can name, as `package` → base (#87).
489
+ * Every package landing this build can name, as `package` → base.
457
490
  *
458
491
  * **A landing needs no manifest, and that is what makes it work.** The link
459
492
  * manifest indexes content notes, and a homepage is deliberately not one — it
@@ -578,13 +611,13 @@ function readAddress(url, packages) {
578
611
  * **What is checkable, stated plainly.** Only an address into this site is, and
579
612
  * only against facts this build already holds:
580
613
  *
581
- * - A **retired content type** in the path. The engine knows what used to exist
614
+ * - A **retired content type** in the path. The engine knows the retired names
582
615
  * and what replaced it, so this is a fact rather than a guess — and it is
583
616
  * exactly the SoHL defect.
584
617
  * - A **hardcoded absolute URL** into this package's own prefix, or into one a
585
618
  * a fetched index names. Every one of them has a better form to write, which
586
619
  * is why every one is reported — including a bare `/<package>/`, which names
587
- * another package's landing (#87).
620
+ * another package's landing.
588
621
  *
589
622
  * That last case was exempt until the better form was identified, on the
590
623
  * reasoning that a landing is in no link manifest so nothing could resolve it.
@@ -658,7 +691,7 @@ export function auditHomepageLinks(index) {
658
691
  // Landings first, and by the roster rather than by the manifest
659
692
  // package set: a landing is addressable in a repository that
660
693
  // has fetched no index at all, which is the case the fence creates
661
- // and the case this rule exists for (#87).
694
+ // and the case this rule exists for.
662
695
  const landing = landingTarget(url, bases);
663
696
  if (landing) {
664
697
  report(
@@ -749,7 +782,7 @@ export function auditHomepageLinks(index) {
749
782
  *
750
783
  * **How the link is *written* is a separate finding from where it points**, and
751
784
  * the two are kept apart because the corrections differ. An unlabelled link
752
- * (#180) has to become `[[type-shortcode|Text]]`; a labelled one whose target
785
+ * has to become `[[type-shortcode|Text]]`; a labelled one whose target
753
786
  * resolves nowhere has a shortcode to fix. Reporting a bare `[[Name]]` as a
754
787
  * dead address would send an author hunting for a note that was never named.
755
788
  *
@@ -761,7 +794,7 @@ export function auditHomepageLinks(index) {
761
794
  * carries a `reason` from {@link LINK_FINDING_REASONS} —
762
795
  * `"not-an-address"`, `"unknown-type"`, `"ambiguous"` (with the claiming
763
796
  * `packages`), or `"unresolved"` — and every one of them is an **error**:
764
- * the three resolvers agree on severity for every class (#184).
797
+ * the three resolvers agree on severity for every class.
765
798
  */
766
799
  export function auditLinks(index) {
767
800
  const { notes, anchors, linksOf, resolve, manifestHit, isAddress } = index;
@@ -792,7 +825,7 @@ export function auditLinks(index) {
792
825
  for (const note of notes) {
793
826
  for (const { target, anchor, text, occurrence, labelled } of linksOf(note)) {
794
827
  // The label is required whatever the link part is, an anchor
795
- // included — so this is tested before the same-page form (#180).
828
+ // included — so this is tested before the same-page form.
796
829
  if (!labelled) {
797
830
  unlabelledLinks.push({
798
831
  note,
@@ -824,7 +857,7 @@ export function auditLinks(index) {
824
857
  // Two packages publish the short address, so it names neither.
825
858
  // Reported as its own class: "no document has that identity" is
826
859
  // false here — two do — and the fix is the qualified form
827
- // rather than a corrected shortcode (#184).
860
+ // rather than a corrected shortcode.
828
861
  deadAddresses.push({
829
862
  ...at,
830
863
  reason: "ambiguous",