@heroiclands/package-build 20.3.0 → 20.4.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 (178) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/CONTENT.md +111 -92
  3. package/README.md +2 -9
  4. package/bin/content-build.mjs +70 -61
  5. package/bin/package-build.mjs +2 -2
  6. package/config.mjs +6 -8
  7. package/content-config.mjs +187 -64
  8. package/docs/content-format.md +57 -68
  9. package/e2e.mjs +4 -4
  10. package/engine/actor-compiler.mjs +36 -36
  11. package/engine/address-charset.mjs +6 -6
  12. package/engine/address-diff.mjs +12 -11
  13. package/engine/anchored-sections.mjs +1 -1
  14. package/engine/anchors.mjs +1 -1
  15. package/engine/base-compiler.mjs +36 -38
  16. package/engine/bundle-notes.mjs +4 -4
  17. package/engine/bundles.mjs +8 -8
  18. package/engine/code-fences.mjs +3 -3
  19. package/engine/compendiums.mjs +8 -8
  20. package/engine/compile-corpus.mjs +4 -5
  21. package/engine/content-address.mjs +15 -15
  22. package/engine/content-charset.mjs +1 -1
  23. package/engine/content-format-check.mjs +24 -25
  24. package/engine/content-format.mjs +8 -8
  25. package/engine/content-html.mjs +202 -0
  26. package/engine/content-icons.mjs +225 -98
  27. package/engine/content-index.mjs +17 -17
  28. package/engine/content-links.mjs +30 -30
  29. package/engine/content-lint.mjs +20 -24
  30. package/engine/content-package.mjs +7 -9
  31. package/engine/content-slug.mjs +2 -2
  32. package/engine/content-tables.mjs +7 -8
  33. package/engine/content-tree.mjs +1 -1
  34. package/engine/derived-fields.mjs +174 -0
  35. package/engine/diagnostics.mjs +3 -4
  36. package/engine/document-subtypes.mjs +6 -7
  37. package/engine/field-reference.mjs +8 -8
  38. package/engine/field-spec.mjs +29 -30
  39. package/engine/folder-notes.mjs +14 -14
  40. package/engine/foreign-catalog.mjs +8 -9
  41. package/engine/foundry-entries.mjs +19 -20
  42. package/engine/frontmatter-lint.mjs +64 -69
  43. package/engine/frontmatter.mjs +11 -11
  44. package/engine/generate.mjs +39 -39
  45. package/engine/helpers.mjs +63 -54
  46. package/engine/homepage.mjs +20 -22
  47. package/engine/ids.mjs +15 -15
  48. package/engine/index-records.mjs +4 -4
  49. package/engine/index.mjs +14 -8
  50. package/engine/item-compiler.mjs +54 -29
  51. package/engine/item-docs.mjs +11 -13
  52. package/engine/item-registry.mjs +13 -13
  53. package/engine/journals.mjs +15 -15
  54. package/engine/macros.mjs +3 -3
  55. package/engine/map-notes.mjs +6 -6
  56. package/engine/metadata-index.mjs +5 -5
  57. package/engine/note-claims.mjs +36 -34
  58. package/engine/note-ids.mjs +5 -5
  59. package/engine/note-package.mjs +4 -4
  60. package/engine/note-renames.mjs +3 -3
  61. package/engine/note-schemas.mjs +3 -3
  62. package/engine/note-vocabulary.mjs +27 -26
  63. package/engine/pack-config.mjs +13 -13
  64. package/engine/pack-router.mjs +0 -0
  65. package/engine/prose-config.mjs +9 -9
  66. package/engine/prose-lint.mjs +12 -13
  67. package/engine/region-events.mjs +2 -2
  68. package/engine/retired-fields.mjs +24 -22
  69. package/engine/runtime-only-fields.mjs +1 -1
  70. package/engine/scene-levels.mjs +1 -1
  71. package/engine/scenes.mjs +19 -20
  72. package/engine/schema-check.mjs +20 -20
  73. package/engine/schema-extract.mjs +1 -1
  74. package/engine/site-build.mjs +45 -47
  75. package/engine/site-index.mjs +17 -18
  76. package/engine/sql-tables.mjs +21 -13
  77. package/engine/subtype-registry.mjs +5 -4
  78. package/engine/system-block.mjs +18 -19
  79. package/engine/systems.mjs +2 -2
  80. package/engine/web-wikilinks.mjs +35 -35
  81. package/engine/wikilink-syntax.mjs +16 -16
  82. package/engine/wikilinks.mjs +39 -39
  83. package/hm3/actors.mjs +22 -22
  84. package/hm3/document-subtypes.mjs +5 -5
  85. package/hm3/index.mjs +2 -2
  86. package/hm3/item-builders.mjs +1 -1
  87. package/hm3/item-fields.mjs +3 -3
  88. package/hm3/items.mjs +20 -8
  89. package/hm3/template-priority.mjs +2 -2
  90. package/lang.mjs +3 -3
  91. package/manifest.mjs +17 -20
  92. package/package.json +1 -2
  93. package/release.mjs +3 -3
  94. package/sohl/actors.mjs +28 -28
  95. package/sohl/affiliation-standings.mjs +2 -2
  96. package/sohl/being-info.mjs +5 -5
  97. package/sohl/default-item-art.mjs +5 -5
  98. package/sohl/document-subtypes.mjs +5 -5
  99. package/sohl/index.mjs +3 -3
  100. package/sohl/item-builders.mjs +7 -7
  101. package/sohl/item-fields.mjs +8 -9
  102. package/sohl/items.mjs +20 -6
  103. package/sohl/kb-passes.mjs +5 -5
  104. package/sohl/note-schemas.mjs +7 -7
  105. package/sohl/skill-base.mjs +1 -1
  106. package/types/content-config.d.mts +25 -24
  107. package/types/e2e.d.mts +3 -3
  108. package/types/engine/actor-compiler.d.mts +14 -13
  109. package/types/engine/address-charset.d.mts +6 -6
  110. package/types/engine/address-diff.d.mts +3 -3
  111. package/types/engine/base-compiler.d.mts +17 -19
  112. package/types/engine/bundle-notes.d.mts +3 -3
  113. package/types/engine/bundles.d.mts +1 -1
  114. package/types/engine/code-fences.d.mts +3 -3
  115. package/types/engine/compendiums.d.mts +3 -3
  116. package/types/engine/compile-corpus.d.mts +1 -1
  117. package/types/engine/content-address.d.mts +11 -11
  118. package/types/engine/content-format-check.d.mts +6 -6
  119. package/types/engine/content-format.d.mts +2 -2
  120. package/types/engine/content-html.d.mts +78 -0
  121. package/types/engine/content-icons.d.mts +150 -62
  122. package/types/engine/content-index.d.mts +2 -2
  123. package/types/engine/content-links.d.mts +8 -8
  124. package/types/engine/content-lint.d.mts +2 -2
  125. package/types/engine/content-package.d.mts +6 -8
  126. package/types/engine/derived-fields.d.mts +101 -0
  127. package/types/engine/diagnostics.d.mts +2 -2
  128. package/types/engine/document-subtypes.d.mts +3 -3
  129. package/types/engine/field-spec.d.mts +37 -39
  130. package/types/engine/folder-notes.d.mts +4 -5
  131. package/types/engine/foreign-catalog.d.mts +3 -3
  132. package/types/engine/foundry-entries.d.mts +6 -7
  133. package/types/engine/frontmatter-lint.d.mts +7 -7
  134. package/types/engine/frontmatter.d.mts +7 -7
  135. package/types/engine/generate.d.mts +7 -7
  136. package/types/engine/helpers.d.mts +25 -27
  137. package/types/engine/homepage.d.mts +16 -18
  138. package/types/engine/ids.d.mts +13 -13
  139. package/types/engine/index-records.d.mts +3 -3
  140. package/types/engine/index.d.mts +2 -0
  141. package/types/engine/item-compiler.d.mts +21 -5
  142. package/types/engine/item-docs.d.mts +2 -2
  143. package/types/engine/item-registry.d.mts +6 -6
  144. package/types/engine/journals.d.mts +3 -3
  145. package/types/engine/map-notes.d.mts +2 -2
  146. package/types/engine/metadata-index.d.mts +4 -4
  147. package/types/engine/note-claims.d.mts +16 -15
  148. package/types/engine/note-ids.d.mts +4 -4
  149. package/types/engine/note-package.d.mts +1 -1
  150. package/types/engine/note-renames.d.mts +3 -3
  151. package/types/engine/note-vocabulary.d.mts +9 -8
  152. package/types/engine/pack-config.d.mts +3 -3
  153. package/types/engine/pack-router.d.mts +1 -1
  154. package/types/engine/prose-config.d.mts +9 -9
  155. package/types/engine/prose-lint.d.mts +3 -4
  156. package/types/engine/region-events.d.mts +2 -2
  157. package/types/engine/retired-fields.d.mts +10 -9
  158. package/types/engine/schema-check.d.mts +11 -11
  159. package/types/engine/site-build.d.mts +18 -19
  160. package/types/engine/site-index.d.mts +3 -4
  161. package/types/engine/sql-tables.d.mts +11 -5
  162. package/types/engine/subtype-registry.d.mts +3 -3
  163. package/types/engine/system-block.d.mts +3 -3
  164. package/types/engine/web-wikilinks.d.mts +7 -7
  165. package/types/engine/wikilink-syntax.d.mts +17 -17
  166. package/types/engine/wikilinks.d.mts +13 -12
  167. package/types/hm3/actors.d.mts +1 -1
  168. package/types/hm3/document-subtypes.d.mts +1 -1
  169. package/types/hm3/items.d.mts +1 -2
  170. package/types/hm3/template-priority.d.mts +1 -1
  171. package/types/manifest.d.mts +8 -8
  172. package/types/sohl/actors.d.mts +2 -2
  173. package/types/sohl/affiliation-standings.d.mts +2 -2
  174. package/types/sohl/being-info.d.mts +2 -2
  175. package/types/sohl/document-subtypes.d.mts +1 -1
  176. package/types/sohl/items.d.mts +1 -2
  177. package/types/sohl/note-schemas.d.mts +1 -1
  178. package/MIGRATING.md +0 -608
@@ -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,
@@ -316,7 +316,7 @@ export function buildLinkIndex(
316
316
  * Every indexed entry an address names, matching only the segments it
317
317
  * supplies.
318
318
  *
319
- * This is the whole of #59's resolution rule in one place: a written
319
+ * This is the whole resolution rule in one place: a written
320
320
  * address is a *partial* one, unsupplied segments are wildcards, and the
321
321
  * caller requires exactly one hit. Nothing here decides an ambiguity — zero
322
322
  * and many are different findings with different fixes, so the count is
@@ -354,7 +354,7 @@ export function buildLinkIndex(
354
354
  function resolveAddress(target, keyPath) {
355
355
  const qualified = readQualifier(target, types, packages);
356
356
  if (!qualified || qualified.reason) return undefined;
357
- // Every omitted segment defaults from where the link is written (#336),
357
+ // Every omitted segment defaults from where the link is written,
358
358
  // so the target expands to exactly one canonical address and this is a
359
359
  // plain lookup. There is no candidate set, and therefore no single-hit
360
360
  // rule and no ambiguity to report.
@@ -370,13 +370,13 @@ export function buildLinkIndex(
370
370
  * Every foreign manifest entry an address names, in package order.
371
371
  *
372
372
  * 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
373
+ * segments it supplies and wildcards the rest. A target naming a
374
374
  * package necessarily names its system too — omission runs left to right —
375
375
  * so the fully qualified form matches at most one entry; a shorter one
376
376
  * names no package, and resolves against any foreign package that
377
377
  * publishes it. Either way only exactly one hit resolves. Two claimants make it ambiguous, which is a different finding
378
378
  * from resolving nowhere and has a different fix, so the count is returned
379
- * rather than collapsed here (#184).
379
+ * rather than collapsed here.
380
380
  *
381
381
  * @param {string} target - The link target.
382
382
  * @returns {object[]} The foreign entries, each carrying its `package`.
@@ -384,7 +384,7 @@ export function buildLinkIndex(
384
384
  function foreignHits(target, keyPath) {
385
385
  const q = readQualifier(target, types, packages);
386
386
  if (!q || q.reason) return [];
387
- // An omitted package means *this* package (#336), so a short form
387
+ // An omitted package means *this* package, so a short form
388
388
  // addresses nothing foreign and never reaches a dependency's index.
389
389
  // Reaching another package is the fully qualified form's job, and
390
390
  // saying so is the whole point: a link that resolved into `sohl` only
@@ -428,7 +428,7 @@ export function buildLinkIndex(
428
428
  /**
429
429
  * Resolve a link target the way both builds do, or `undefined`. Every
430
430
  * link is an address, so this is {@link resolveAddress} under the name
431
- * the walkers use (#180).
431
+ * the walkers use.
432
432
  */
433
433
  resolve: resolveAddress,
434
434
  resolveAddress,
@@ -453,7 +453,7 @@ export function buildLinkIndex(
453
453
  const SITE_HOST = /^(?:[a-z0-9-]+\.)*heroiclands\.org$/i;
454
454
 
455
455
  /**
456
- * Every package landing this build can name, as `package` → base (#87).
456
+ * Every package landing this build can name, as `package` → base.
457
457
  *
458
458
  * **A landing needs no manifest, and that is what makes it work.** The link
459
459
  * manifest indexes content notes, and a homepage is deliberately not one — it
@@ -578,13 +578,13 @@ function readAddress(url, packages) {
578
578
  * **What is checkable, stated plainly.** Only an address into this site is, and
579
579
  * only against facts this build already holds:
580
580
  *
581
- * - A **retired content type** in the path. The engine knows what used to exist
581
+ * - A **retired content type** in the path. The engine knows the retired names
582
582
  * and what replaced it, so this is a fact rather than a guess — and it is
583
583
  * exactly the SoHL defect.
584
584
  * - A **hardcoded absolute URL** into this package's own prefix, or into one a
585
585
  * a fetched index names. Every one of them has a better form to write, which
586
586
  * is why every one is reported — including a bare `/<package>/`, which names
587
- * another package's landing (#87).
587
+ * another package's landing.
588
588
  *
589
589
  * That last case was exempt until the better form was identified, on the
590
590
  * reasoning that a landing is in no link manifest so nothing could resolve it.
@@ -658,7 +658,7 @@ export function auditHomepageLinks(index) {
658
658
  // Landings first, and by the roster rather than by the manifest
659
659
  // package set: a landing is addressable in a repository that
660
660
  // has fetched no index at all, which is the case the fence creates
661
- // and the case this rule exists for (#87).
661
+ // and the case this rule exists for.
662
662
  const landing = landingTarget(url, bases);
663
663
  if (landing) {
664
664
  report(
@@ -749,7 +749,7 @@ export function auditHomepageLinks(index) {
749
749
  *
750
750
  * **How the link is *written* is a separate finding from where it points**, and
751
751
  * 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
752
+ * has to become `[[type-shortcode|Text]]`; a labelled one whose target
753
753
  * resolves nowhere has a shortcode to fix. Reporting a bare `[[Name]]` as a
754
754
  * dead address would send an author hunting for a note that was never named.
755
755
  *
@@ -761,7 +761,7 @@ export function auditHomepageLinks(index) {
761
761
  * carries a `reason` from {@link LINK_FINDING_REASONS} —
762
762
  * `"not-an-address"`, `"unknown-type"`, `"ambiguous"` (with the claiming
763
763
  * `packages`), or `"unresolved"` — and every one of them is an **error**:
764
- * the three resolvers agree on severity for every class (#184).
764
+ * the three resolvers agree on severity for every class.
765
765
  */
766
766
  export function auditLinks(index) {
767
767
  const { notes, anchors, linksOf, resolve, manifestHit, isAddress } = index;
@@ -792,7 +792,7 @@ export function auditLinks(index) {
792
792
  for (const note of notes) {
793
793
  for (const { target, anchor, text, occurrence, labelled } of linksOf(note)) {
794
794
  // The label is required whatever the link part is, an anchor
795
- // included — so this is tested before the same-page form (#180).
795
+ // included — so this is tested before the same-page form.
796
796
  if (!labelled) {
797
797
  unlabelledLinks.push({
798
798
  note,
@@ -824,7 +824,7 @@ export function auditLinks(index) {
824
824
  // Two packages publish the short address, so it names neither.
825
825
  // Reported as its own class: "no document has that identity" is
826
826
  // false here — two do — and the fix is the qualified form
827
- // rather than a corrected shortcode (#184).
827
+ // rather than a corrected shortcode.
828
828
  deadAddresses.push({
829
829
  ...at,
830
830
  reason: "ambiguous",
@@ -15,8 +15,8 @@
15
15
  * Linting a content tree's **addresses** — the rules every package's notes are
16
16
  * authored against, wherever those notes live.
17
17
  *
18
- * These rules used to live in the SoHL repository's `utils/`, which had two
19
- * consequences and no upside (#20). `thalorna` and `kethira` notes were checked
18
+ * These rules do not live in a consumer's `utils/`, which has two
19
+ * consequences and no upside. `thalorna` and `kethira` notes were checked
20
20
  * by nothing at all, so the packages most likely to carry authoring mistakes
21
21
  * were the ones nothing inspected. And one rule with two implementations can
22
22
  * disagree without anything detecting it, which the canonical-separator
@@ -30,28 +30,24 @@
30
30
  * being the only hyphen in the string.
31
31
  * 2. **Uniqueness** — `(type, shortcode)` names one note.
32
32
  * 3. **The package's own address** — exactly one note claims `/<package>/`,
33
- * which is {@link checkHomepageCount} (#52). It belongs here for the same
33
+ * which is {@link checkHomepageCount}. It belongs here for the same
34
34
  * reason the other two do: it is a statement about which note holds which
35
35
  * address, it needs no `site:` configuration to decide, and a package with
36
36
  * no front page is misconfigured whether or not anyone runs a site build.
37
37
  * 4. **Vacated addresses** — a `renamedFrom:` entry names an address this note
38
- * used to hold and nothing holds now (#278). It is the same statement as
38
+ * once held and nothing holds now. It is the same statement as
39
39
  * rule 2 read backwards, and it needs the same whole-tree view: an entry can
40
40
  * only be checked against every *other* note's address, and two notes
41
41
  * claiming one predecessor is the uniqueness rule applied to the past.
42
42
  *
43
43
  * **Nothing here writes.** A check reports and an author fixes.
44
44
  *
45
- * **A third rule was retired (#79).** Every note used to be required to repeat
46
- * its own `type-shortcode` address in the top-level `aliases:` list. That
47
- * served exactly one reader — **Obsidian**, so `[[type-shortcode]]` resolved in
48
- * the editor — and nothing else ever read it: both resolvers parse the hyphen
49
- * qualifier themselves. The project no longer authors in Obsidian, so the rule
50
- * required a line of frontmatter per note for a reader that does not exist. The
51
- * field itself is retired now (#180), refused from `retired-fields.mjs`. Removing it was
52
- * verified output-neutral beforehand: across 1,735 stripped notes,
53
- * `package compile` produced byte-identical `build/packs-json` and the site
54
- * build byte-identical `site/content`.
45
+ * **There is deliberately no third rule** requiring every note to repeat its
46
+ * own `type-shortcode` address in a top-level `aliases:` list. It would serve
47
+ * exactly one reader — **Obsidian**, so `[[type-shortcode]]` resolves in the
48
+ * editor — and nothing in the build reads it: both resolvers parse the hyphen
49
+ * qualifier themselves. The field is retired, refused from
50
+ * `retired-fields.mjs`.
55
51
  *
56
52
  * **What is deliberately absent.** Corpus reachability — "every Rules document
57
53
  * is reachable from the book's root" — is a statement about what one package
@@ -67,7 +63,7 @@ import path from "node:path";
67
63
  import { ADDRESS_SEGMENT_PATTERN } from "./address-charset.mjs";
68
64
  import { positionInFrontmatter } from "./diagnostics.mjs";
69
65
  import { assertStatedScope } from "./helpers.mjs";
70
- // The corpus, read from the one pass that derives it (#243).
66
+ // The corpus, read from the one pass that derives it.
71
67
  import { authoredFrontmatter, indexRecordsFor, isNoteRecord, noteFile } from "./content-index.mjs";
72
68
  import { checkHomepageCount, isHomepage } from "./homepage.mjs";
73
69
  import { declaresRenamedFrom, renamedFrom, renamedFromEntries } from "./note-renames.mjs";
@@ -78,7 +74,7 @@ import { declaresRenamedFrom, renamedFrom, renamedFromEntries } from "./note-ren
78
74
  * This is {@link ADDRESS_SEGMENT_PATTERN}, not a second copy of it. A shortcode
79
75
  * is the last segment of a canonical address, and the rule it is held to is the
80
76
  * rule *every* segment is held to — so the two are one constant rather than two
81
- * free to drift apart (#59). The name survives because this is where the rule
77
+ * free to drift apart. The name survives because this is where the rule
82
78
  * is applied to a note.
83
79
  *
84
80
  * Case is deliberately **not** constrained: hundreds of authored shortcodes are
@@ -112,7 +108,7 @@ export function isValidShortcode(value) {
112
108
  * `Templates/`, a `README`, a repository's own `CLAUDE.md` — has no type, is
113
109
  * neither addressed nor addressable, and would fail rules it can never satisfy.
114
110
  *
115
- * **Read from the index, not from a walk of this pass's own** (#243). The
111
+ * **Read from the index, not from a walk of this pass's own**. The
116
112
  * `lint` command already derives the index — its link check and its `sql`
117
113
  * tables are built from it — and then walked the tree a second time to get
118
114
  * here, so one command held two answers to "which files are the corpus?" and
@@ -162,7 +158,7 @@ function collectNotes(contentBase, { skipDirectories, config, records, problems
162
158
  }
163
159
 
164
160
  /**
165
- * What one note's `renamedFrom:` says, checked against itself (#278).
161
+ * What one note's `renamedFrom:` says, checked against itself.
166
162
  *
167
163
  * The entries a note can be wrong about on its own: a value that is not a
168
164
  * shortcode, one naming the address the note holds *now*, one written twice.
@@ -276,7 +272,7 @@ function checkRenamedFrom({ fm, file }, raw) {
276
272
  * @param {object} [opts.config] - The resolved build configuration, which the
277
273
  * corpus is derived against.
278
274
  * @param {readonly object[]} [opts.records] - Index records the caller already
279
- * derived, so a command reads one corpus (#243).
275
+ * derived, so a command reads one corpus.
280
276
  * @param {object[]} [opts.problems] - Collects the notes the index cannot
281
277
  * record, instead of letting one of them silence the lint.
282
278
  * @returns {{findings: Array<{file: string, line?: number, column?: number,
@@ -348,17 +344,17 @@ export function lintContentTree(
348
344
  // what a tree that failed to check out produces — so the lint would go
349
345
  // green on the one state it most needs to catch.
350
346
  //
351
- // The state that catches is an **empty walk**, not an empty key set (#77).
347
+ // The state that catches is an **empty walk**, not an empty key set.
352
348
  // Notes may be keyless: a folder document carries no `shortcode`, and a
353
349
  // tree of them is populated, correct, and unkeyed. Reporting that as a
354
350
  // missing checkout trains its author to stop reading the output — the one
355
351
  // thing this guard needs them to do. A tree holding notes is therefore a
356
352
  // tree; only a tree holding none is the absent one.
357
353
  //
358
- // The homepage used to be the headline example, because it was addressed
354
+ // The homepage is not the headline example, though it was once addressed
359
355
  // by the package rather than by a slug — so a `publish.site: homepage`
360
356
  // package had a tree with exactly one note and no key at all. It carries an
361
- // address like every other note now (#182); the guard is unchanged, because
357
+ // address like every other note now; the guard is unchanged, because
362
358
  // what it reads was never the key count.
363
359
  if (notes.length === 0) {
364
360
  findings.push({
@@ -401,8 +397,8 @@ export function lintContentTree(
401
397
  }
402
398
  }
403
399
 
404
- // The two questions about a declared rename that need the whole tree
405
- // (#278). Both are the uniqueness rule above, applied to the past: an
400
+ // The two questions about a declared rename that need the whole tree.
401
+ // Both are the uniqueness rule above, applied to the past: an
406
402
  // address has one holder, so it has one successor and it cannot be both
407
403
  // vacated and occupied.
408
404
  for (const [claim, claimants] of claimedPredecessors) {
@@ -16,7 +16,7 @@
16
16
  * ships them — read from the repository's `package-build.config.yaml`.
17
17
  *
18
18
  * Both values are **derived**, not declared: `config.mjs` is the single place
19
- * the configuration is resolved (#1508). This module survives as the import
19
+ * the configuration is resolved. This module survives as the import
20
20
  * path the link resolver and the compilers have always used, so that the values
21
21
  * can still be mocked in one place and so no consumer has to learn a new
22
22
  * spelling for them.
@@ -35,13 +35,11 @@ import { loadPackConfig } from "./pack-config.mjs";
35
35
  * repository's identity in the address space, not a switch — and never dead
36
36
  * configuration, whatever else changes.
37
37
  *
38
- * It was also, until #56, a **selector**: a note declared the same value in its
39
- * `package:` frontmatter and the compilers kept the ones that matched. Every
40
- * content tree is single-package each is single-sourced in the repository that
41
- * ships it so the field restated this constant once per note while a value
42
- * that matched nothing filtered the whole tree out in silence. That field is
43
- * retired and declaring it now fails the build; this value stays, here, where
44
- * it is declared once.
38
+ * It is not a **selector**. Every content tree is single-package each is
39
+ * single-sourced in the repository that ships it so a `package:` field on a
40
+ * note would restate this constant once per note, and a value matching nothing
41
+ * would filter the whole tree out in silence. That field is refused, and this
42
+ * value stays here, declared once.
45
43
  *
46
44
  * Stable across compilation targets. If this content were ever compiled for a
47
45
  * second game system, it would still be published as `sohl` — only the Foundry
@@ -65,7 +63,7 @@ export function contentPackage() {
65
63
  * here: a note is published under `sohl` and its documents are addressed as
66
64
  * `Compendium.sohl.<pack>.<Type>.<id>`. In `sohl-thalorna` the two differ
67
65
  * (`thalorna` vs `sohl-thalorna`), which is why they are separate values rather
68
- * than one — treating them as interchangeable is what #1498 was.
66
+ * than one — treating them as interchangeable is the mistake.
69
67
  *
70
68
  * Configured rather than read from the manifest so the link resolver stays
71
69
  * filesystem-free and unit-testable. `assertPackageIdMatchesManifestFile` in
@@ -17,9 +17,9 @@
17
17
  * {@link slugify} reduces a piece of prose — a heading, a document name — to a
18
18
  * URL-safe token. It is **not** how a page is addressed: a note's URL is its
19
19
  * address, `type-shortcode`, derived in `engine/content-address.mjs` and
20
- * touching no display string at all (#181).
20
+ * touching no display string at all.
21
21
  *
22
- * That used to be the other way round. This module carried a `contentSlug` that
22
+ * It is not the other way round: a `contentSlug` here that
23
23
  * derived a page's URL from `name.full`, abbreviating it through a table of 200
24
24
  * words so the result stayed short, and a `findSlugCollisions` to catch the two
25
25
  * notes that would then claim one URL. Its own header justified the readability
@@ -59,7 +59,7 @@
59
59
 
60
60
  // The fence syntax is stated once, in `code-fences.mjs`, so the scanner that
61
61
  // *skips* code and the one that expands a `dataview` block cannot disagree
62
- // about where a block begins and ends (#1505).
62
+ // about where a block begins and ends.
63
63
  import { FENCE_LINE } from "./code-fences.mjs";
64
64
 
65
65
  /** What a cell shows when its expression resolves to nothing. */
@@ -1084,7 +1084,7 @@ export function renderContentTable(spec, rows, linkable, self) {
1084
1084
  * reason: string, line: number}>, lineMap: Array<{line: number,
1085
1085
  * generated: boolean}>}} `lineMap` is parallel to the emitted lines and says
1086
1086
  * which authored line each came from, so a diagnostic about the expanded
1087
- * body can name an authored position (#17). An `errors` entry carries the
1087
+ * body can name an authored position. An `errors` entry carries the
1088
1088
  * 0-based line of the directive that failed, for the same reason.
1089
1089
  */
1090
1090
  /**
@@ -1121,12 +1121,12 @@ export function expandContentTables(
1121
1121
  let sqlOrdinal = 0;
1122
1122
  // One entry per `dataview` directive still authored, so a caller can say how
1123
1123
  // much of the corpus is still on the retiring language without this pass
1124
- // failing a build over it (#246).
1124
+ // failing a build over it.
1125
1125
  const warnings = [];
1126
1126
  const lines = String(markdown ?? "").split("\n");
1127
1127
  const out = [];
1128
1128
  // Which authored line each emitted line came from, so a later pass can
1129
- // report an authored position for text this one moved (#17). A generated
1129
+ // report an authored position for text this one moved. A generated
1130
1130
  // row has no authored line of its own, so it is blamed on the directive
1131
1131
  // that produced it — the thing an author can actually edit — and marked,
1132
1132
  // because its columns mean nothing.
@@ -1203,8 +1203,7 @@ export function expandContentTables(
1203
1203
  line: i,
1204
1204
  column: indent.length + 1,
1205
1205
  reason:
1206
- "`dataview` content tables are being replaced by `sql` over the " +
1207
- "content index (#246)",
1206
+ "`dataview` content tables are being replaced by `sql` over the " + "content index",
1208
1207
  });
1209
1208
  // `dataview allow-empty` says a table selecting nothing is the intended
1210
1209
  // state. Spelled on the fence rather than in the query, because it is a
@@ -1219,7 +1218,7 @@ export function expandContentTables(
1219
1218
  // a header with nothing under it makes that indistinguishable from
1220
1219
  // a category that is legitimately empty. Eight tables in `sohl`'s
1221
1220
  // `Rules/Bestiary.md` published that way for months after the
1222
- // `creature` → `being` rename, and no build said a word (#223).
1221
+ // `creature` → `being` rename, and no build said a word.
1223
1222
  if (rows.length === 0 && !allowEmpty) {
1224
1223
  errors.push({
1225
1224
  source,
@@ -1239,7 +1238,7 @@ export function expandContentTables(
1239
1238
  directive: block.join("\n"),
1240
1239
  reason: err.message,
1241
1240
  // The fence's own line, so a table error is as locatable as
1242
- // everything else the build reports (#17).
1241
+ // everything else the build reports.
1243
1242
  line: i,
1244
1243
  });
1245
1244
  block.forEach((text, k) => emit(text, i + k));
@@ -20,7 +20,7 @@
20
20
  * notices — the LevelDB packs are written, the deploy mirrors them, and the
21
21
  * defect surfaces as "my compendiums are blank" long after the build went green.
22
22
  *
23
- * `assets/content/` is this repository's own source (#1445), but an empty or
23
+ * `assets/content/` is this repository's own source, but an empty or
24
24
  * partial tree is still possible — a bad checkout, or a sparse clone. This
25
25
  * counts what is actually there so the build can refuse.
26
26
  */