@heroiclands/package-build 20.4.0 → 20.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +288 -0
  2. package/CONTENT.md +213 -20
  3. package/README.md +19 -1
  4. package/bin/content-build.mjs +135 -32
  5. package/bin/package-build.mjs +46 -13
  6. package/content-config.mjs +345 -101
  7. package/docs/api.md +1352 -0
  8. package/docs/commands.md +1609 -0
  9. package/docs/configuration.md +1432 -0
  10. package/docs/content-format.md +16 -6
  11. package/docs/diagnostics.md +356 -0
  12. package/docs/getting-started.md +813 -0
  13. package/docs/project-setup.md +469 -0
  14. package/engine/actor-compiler.mjs +30 -27
  15. package/engine/address-diff.mjs +45 -41
  16. package/engine/base-compiler.mjs +6 -0
  17. package/engine/bundles.mjs +9 -0
  18. package/engine/content-address.mjs +9 -9
  19. package/engine/content-index.mjs +44 -23
  20. package/engine/content-links.mjs +44 -11
  21. package/engine/content-lint.mjs +44 -10
  22. package/engine/content-tables.mjs +32 -27
  23. package/engine/folder-notes.mjs +4 -2
  24. package/engine/frontmatter-lint.mjs +35 -38
  25. package/engine/generate.mjs +5 -0
  26. package/engine/helpers.mjs +86 -32
  27. package/engine/index.mjs +12 -2
  28. package/engine/journals.mjs +9 -0
  29. package/engine/note-claims.mjs +18 -10
  30. package/engine/note-schemas.mjs +0 -5
  31. package/engine/note-vocabulary.mjs +32 -31
  32. package/engine/pack-config.mjs +26 -12
  33. package/engine/pack-router.mjs +0 -0
  34. package/engine/pdf-build.mjs +464 -0
  35. package/engine/pdf-fonts.mjs +420 -0
  36. package/engine/pdf-render.mjs +876 -0
  37. package/engine/pdf-toc.mjs +525 -0
  38. package/engine/scenes.mjs +14 -5
  39. package/engine/schema-check.mjs +1 -1
  40. package/engine/site-build.mjs +21 -3
  41. package/engine/web-wikilinks.mjs +6 -3
  42. package/engine/wikilinks.mjs +2 -4
  43. package/hm3/actors.mjs +8 -0
  44. package/hm3/items.mjs +8 -0
  45. package/package.json +1 -1
  46. package/release.mjs +63 -3
  47. package/sohl/actors.mjs +8 -0
  48. package/sohl/items.mjs +8 -0
  49. package/sohl/note-schemas.mjs +5 -5
  50. package/types/content-config.d.mts +66 -15
  51. package/types/engine/actor-compiler.d.mts +34 -30
  52. package/types/engine/address-diff.d.mts +57 -3
  53. package/types/engine/base-compiler.d.mts +10 -2
  54. package/types/engine/bundles.d.mts +9 -0
  55. package/types/engine/content-address.d.mts +9 -9
  56. package/types/engine/content-index.d.mts +57 -13
  57. package/types/engine/content-lint.d.mts +6 -4
  58. package/types/engine/content-tables.d.mts +49 -18
  59. package/types/engine/frontmatter-lint.d.mts +3 -2
  60. package/types/engine/helpers.d.mts +105 -31
  61. package/types/engine/index.d.mts +4 -0
  62. package/types/engine/journals.d.mts +9 -0
  63. package/types/engine/note-claims.d.mts +17 -10
  64. package/types/engine/note-vocabulary.d.mts +23 -196
  65. package/types/engine/pack-config.d.mts +4 -4
  66. package/types/engine/pdf-build.d.mts +42 -0
  67. package/types/engine/pdf-fonts.d.mts +30 -0
  68. package/types/engine/pdf-render.d.mts +156 -0
  69. package/types/engine/pdf-toc.d.mts +114 -0
  70. package/types/engine/scenes.d.mts +10 -1
  71. package/types/engine/schema-check.d.mts +2 -2
  72. package/types/engine/site-build.d.mts +34 -6
  73. package/types/engine/wikilinks.d.mts +2 -3
  74. package/types/hm3/actors.d.mts +8 -0
  75. package/types/hm3/items.d.mts +8 -0
  76. package/types/release.d.mts +15 -4
  77. package/types/sohl/actors.d.mts +10 -2
  78. package/types/sohl/items.d.mts +8 -0
@@ -119,6 +119,15 @@ export function loadBundleSources(sourceDirs) {
119
119
  return documents;
120
120
  }
121
121
 
122
+ /**
123
+ * Adventure pack compiler.
124
+ *
125
+ * Walks the content tree and compiles every `type: bundle` note into one
126
+ * Adventure per system, holding **copies** of the compiled documents its
127
+ * `contents` addresses name. Reading other passes' output is what
128
+ * {@link Bundles.readsPackOutputOf} declares, and what the generator orders the
129
+ * compile by.
130
+ */
122
131
  export class Bundles extends BasePackCompiler {
123
132
  static id = "bundles";
124
133
  static label = "bundle";
@@ -81,11 +81,11 @@ export const KB_PREFIX = "kb/";
81
81
  * consumer deriving a `path` from a key drops the *package and the system*, not
82
82
  * the package alone.
83
83
  *
84
- * The hyphen is a separator and never occurs inside a segment: a shortcode is
85
- * `^[A-Za-z0-9]+$` (`ADDRESS_SEGMENT_PATTERN`, enforced by `content-lint.mjs`)
86
- * and a type is a bare word. That is the same charset guarantee positional key
87
- * parsing rests on, so the address and the key cannot disagree about where one
88
- * ends and the other begins.
84
+ * The hyphen is a separator and never occurs inside a segment: a shortcode
85
+ * matches `ADDRESS_SEGMENT_PATTERN` (`engine/address-charset.mjs`, enforced by
86
+ * `content-lint.mjs`) and a type is a bare word. That is the same charset
87
+ * guarantee positional key parsing rests on, so the address and the key
88
+ * cannot disagree about where one ends and the other begins.
89
89
  *
90
90
  * @param {object} fm - Parsed frontmatter.
91
91
  * @returns {string} The address segment, e.g. `weapongear-dagger`.
@@ -311,10 +311,10 @@ export const CANONICAL_KEY_SEGMENTS = 4;
311
311
  *
312
312
  * Parsing is plain positional counting: split on the separator, require
313
313
  * {@link CANONICAL_KEY_SEGMENTS} of them, and assign each position its field.
314
- * **The charset rule is what makes that sound** — every segment is
315
- * `^[A-Za-z0-9]+$` (`ADDRESS_SEGMENT_PATTERN` in `engine/address-charset.mjs`),
316
- * so the hyphen is purely a separator and the count alone determines every
317
- * field. That is enforced at each of the three sources rather than assumed of
314
+ * **The charset rule is what makes that sound** — every segment matches
315
+ * `ADDRESS_SEGMENT_PATTERN` (`engine/address-charset.mjs`), so the hyphen is
316
+ * purely a separator and the count alone determines every field. That is
317
+ * enforced at each of the three sources rather than assumed of
318
318
  * the data: shortcodes by `content-lint.mjs`, `contentPackage` by
319
319
  * `defineConfig`, and types are bare words. Were any of them free to
320
320
  * carry a hyphen, no amount of counting would recover the fields and the reader
@@ -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
  *
@@ -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
  *
@@ -531,6 +531,27 @@ 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 },
@@ -301,17 +301,6 @@ 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.
@@ -351,6 +340,20 @@ 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;
@@ -379,6 +382,9 @@ export function buildLinkIndex(
379
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) {
@@ -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,
@@ -434,6 +466,7 @@ export function buildLinkIndex(
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
  };
@@ -22,10 +22,10 @@
22
22
  * disagree without anything detecting it, which the canonical-separator
23
23
  * handling already did once on each side.
24
24
  *
25
- * Four rules, all about a note's identity:
25
+ * Five rules, all about a note's identity:
26
26
  *
27
- * 1. **Shape** — a `shortcode` is strictly ASCII-alphanumeric. It is the
28
- * identity key referenced from saved world data, and it is half of the
27
+ * 1. **Shape** — a `shortcode` is strictly lowercase ASCII-alphanumeric. It is
28
+ * the identity key referenced from saved world data, and it is half of the
29
29
  * `type-shortcode` address, whose parse depends on the separating hyphen
30
30
  * being the only hyphen in the string.
31
31
  * 2. **Uniqueness** — `(type, shortcode)` names one note.
@@ -39,6 +39,10 @@
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
+ * 5. **The vocabulary a package's kind leaves it** — a package compiling no
43
+ * Foundry documents publishes `doc` and `homepage` notes and nothing else.
44
+ * It is here rather than with the claim check in `note-claims.mjs` because
45
+ * that check runs at compile, and this is the case where no compile runs.
42
46
  *
43
47
  * **Nothing here writes.** A check reports and an author fixes.
44
48
  *
@@ -61,6 +65,9 @@ import fs from "node:fs";
61
65
  import path from "node:path";
62
66
 
63
67
  import { ADDRESS_SEGMENT_PATTERN } from "./address-charset.mjs";
68
+ // The kind that compiles nothing, and the vocabulary that leaves a tree with.
69
+ import { DOCUMENTATION_KIND, compilesFoundryDocuments } from "../content-config.mjs";
70
+ import { DOCUMENTATION_NOTE_TYPES } from "./note-claims.mjs";
64
71
  import { positionInFrontmatter } from "./diagnostics.mjs";
65
72
  import { assertStatedScope } from "./helpers.mjs";
66
73
  // The corpus, read from the one pass that derives it.
@@ -69,7 +76,8 @@ import { checkHomepageCount, isHomepage } from "./homepage.mjs";
69
76
  import { declaresRenamedFrom, renamedFrom, renamedFromEntries } from "./note-renames.mjs";
70
77
 
71
78
  /**
72
- * The shape every `shortcode` must match: ASCII letters and digits only.
79
+ * The shape every `shortcode` must match: lowercase ASCII letters and digits
80
+ * only.
73
81
  *
74
82
  * This is {@link ADDRESS_SEGMENT_PATTERN}, not a second copy of it. A shortcode
75
83
  * is the last segment of a canonical address, and the rule it is held to is the
@@ -77,9 +85,10 @@ import { declaresRenamedFrom, renamedFrom, renamedFromEntries } from "./note-ren
77
85
  * free to drift apart. The name survives because this is where the rule
78
86
  * is applied to a note.
79
87
  *
80
- * Case is deliberately **not** constrained: hundreds of authored shortcodes are
81
- * mixed-case and collide with nothing, so tightening that is a separate
82
- * decision from this one.
88
+ * Case is held to that rule with no exception: two shortcodes differing only
89
+ * in case are two names nobody can tell apart, and `canonicalKey` lowercases
90
+ * every address it builds regardless, so a mixed-case shortcode addresses the
91
+ * same document as its lowercase spelling.
83
92
  *
84
93
  * A consuming system's *runtime* keeps its own copy of this pattern — it cannot
85
94
  * import a build-time dependency into shipped code — and is expected to pin the
@@ -225,8 +234,9 @@ function checkRenamedFrom({ fm, file }, raw) {
225
234
  ...at(value),
226
235
  severity: "error",
227
236
  message:
228
- `\`renamedFrom: ${value}\` is not strictly alphanumeric, so ` +
229
- `it is not an address this package ever published a ` +
237
+ `\`renamedFrom: ${value}\` is not strictly alphanumeric ` +
238
+ `lowercase letters and digits only (${ADDRESS_SEGMENT_PATTERN.source}) — ` +
239
+ `so it is not an address this package ever published — a ` +
230
240
  `shortcode is held to one charset whether it is current or past`,
231
241
  });
232
242
  continue;
@@ -285,6 +295,12 @@ export function lintContentTree(
285
295
  ) {
286
296
  const findings = [];
287
297
  const notes = collectNotes(contentBase, { skipDirectories, config, records, problems });
298
+ // Whether this package's note vocabulary is the narrowed one. Asked of the
299
+ // configuration once rather than per note, and defaulted to the wide
300
+ // vocabulary when a caller supplies none — an unconfigured lint holds a
301
+ // tree to the rules every package shares.
302
+ const narrowed =
303
+ Boolean(config) && !compilesFoundryDocuments(/** @type {{packageKind: string}} */ (config));
288
304
 
289
305
  /** @type {Map<string, Array<{file: string, absPath: string}>>} */
290
306
  const byKey = new Map();
@@ -299,6 +315,23 @@ export function lintContentTree(
299
315
  // tree costs one pass rather than two.
300
316
  const raw = () => fs.readFileSync(absPath, "utf8");
301
317
 
318
+ // Rule 5, and it is the whole of the check for a package that compiles
319
+ // nothing: no pass downstream would report the note, because the pass
320
+ // that reports an unclaimed type is a compile pass and none runs.
321
+ const type = typeof fm.type === "string" ? fm.type.trim() : "";
322
+ if (narrowed && type && !DOCUMENTATION_NOTE_TYPES.has(type)) {
323
+ findings.push({
324
+ file,
325
+ ...positionInFrontmatter(raw(), "type", type),
326
+ severity: "error",
327
+ message:
328
+ `\`type: ${type}\` compiles to a Foundry document, and a ` +
329
+ `\`${DOCUMENTATION_KIND}\` package compiles none — so the ` +
330
+ `note has no destination. Its vocabulary is ` +
331
+ `${[...DOCUMENTATION_NOTE_TYPES].map((t) => `\`${t}\``).join(" and ")}`,
332
+ });
333
+ }
334
+
302
335
  // Before the keyless `continue` below, because a note declaring a
303
336
  // rename while carrying no address of its own is exactly one of the
304
337
  // things this reports — and reaching it after the skip would mean it
@@ -332,7 +365,8 @@ export function lintContentTree(
332
365
  ...positionInFrontmatter(raw(), "shortcode", String(shortcode)),
333
366
  severity: "error",
334
367
  message:
335
- `shortcode "${shortcode}" is not strictly alphanumeric; it ` +
368
+ `shortcode "${shortcode}" is not strictly alphanumeric ` +
369
+ `lowercase letters and digits only (${ADDRESS_SEGMENT_PATTERN.source}); it ` +
336
370
  `is the identity key and half of the ` +
337
371
  `"${fm.type}-${shortcode}" address, whose parse needs the ` +
338
372
  `separator to be the only hyphen`,
@@ -1059,17 +1059,32 @@ export function renderContentTable(spec, rows, linkable, self) {
1059
1059
  /* ------------------------------------------------------------------------ */
1060
1060
 
1061
1061
  /**
1062
- * Expand every fenced `dataview` block in a markdown body.
1062
+ * The `WHERE` clause of a query, as authored, for a message that has to name
1063
+ * what matched nothing.
1064
+ *
1065
+ * The parsed spec holds an expression tree, and rendering that back to text
1066
+ * would be a second dialect of the query language to keep in step. The authored
1067
+ * line is what the author wrote and what they will edit, so it is what the
1068
+ * finding quotes.
1069
+ *
1070
+ * @param {string} query - The fence's contents.
1071
+ * @returns {string} The clause, or "" when the query has none.
1072
+ */
1073
+ function whereText(query) {
1074
+ const match = /^\s*WHERE\s+(.+?)\s*$/im.exec(String(query ?? ""));
1075
+ return match ? `\`${match[1]}\`` : "";
1076
+ }
1077
+
1078
+ /**
1079
+ * Expand every fenced `dataview` and `sql` block in a markdown body.
1063
1080
  *
1064
1081
  * A block that cannot be honoured — malformed or unsupported — is left in the
1065
1082
  * body verbatim and reported in `errors`, so the failure is visible in the
1066
1083
  * output as well as on the console. Every other code fence, and every code
1067
1084
  * span, is left alone (that is how the syntax is documented).
1068
1085
  *
1069
- * A query that matches **no** note is not an error: it renders as an empty
1070
- * table (headers only), which is what the author already sees in Obsidian, and
1071
- * a category with no content yet is a normal state of the corpus rather than a
1072
- * broken build.
1086
+ * A query that selects **no** note is an error unless the fence says
1087
+ * `allow-empty`, which states that an empty table is the intended result.
1073
1088
  *
1074
1089
  * @param {string} markdown - The note body, frontmatter already stripped.
1075
1090
  * @param {object} ctx
@@ -1080,30 +1095,20 @@ export function renderContentTable(spec, rows, linkable, self) {
1080
1095
  * @param {string} [ctx.source] - The note being expanded, for error reports.
1081
1096
  * @param {ContentTableDoc} [ctx.self] - The note being expanded, as a searchable
1082
1097
  * doc: what a query's `this` reads.
1098
+ * @param {object[]} [ctx.sqlTables] - This note's prepared `sql` results, in
1099
+ * document order, from
1100
+ * {@link module:engine/sql-tables.prepareSqlTables}. An `sql` directive with
1101
+ * no prepared result is an error: nothing here runs a query.
1083
1102
  * @returns {{markdown: string, errors: Array<{source: string, directive: string,
1084
- * reason: string, line: number}>, lineMap: Array<{line: number,
1085
- * generated: boolean}>}} `lineMap` is parallel to the emitted lines and says
1086
- * which authored line each came from, so a diagnostic about the expanded
1087
- * body can name an authored position. An `errors` entry carries the
1088
- * 0-based line of the directive that failed, for the same reason.
1103
+ * reason: string, line: number, column?: number}>,
1104
+ * warnings: Array<{source: string, line: number, column: number,
1105
+ * reason: string}>, lineMap: Array<{line: number, generated: boolean}>}}
1106
+ * `lineMap` is parallel to the emitted lines and says which authored line
1107
+ * each came from, so a diagnostic about the expanded body can name an
1108
+ * authored position. An `errors` entry carries the 0-based line of the
1109
+ * directive that failed, for the same reason. `warnings` holds one entry per
1110
+ * `dataview` directive the body still authors.
1089
1111
  */
1090
- /**
1091
- * The `WHERE` clause of a query, as authored, for a message that has to name
1092
- * what matched nothing.
1093
- *
1094
- * The parsed spec holds an expression tree, and rendering that back to text
1095
- * would be a second dialect of the query language to keep in step. The authored
1096
- * line is what the author wrote and what they will edit, so it is what the
1097
- * finding quotes.
1098
- *
1099
- * @param {string} query - The fence's contents.
1100
- * @returns {string} The clause, or "" when the query has none.
1101
- */
1102
- function whereText(query) {
1103
- const match = /^\s*WHERE\s+(.+?)\s*$/im.exec(String(query ?? ""));
1104
- return match ? `\`${match[1]}\`` : "";
1105
- }
1106
-
1107
1112
  export function expandContentTables(
1108
1113
  markdown,
1109
1114
  {
@@ -54,7 +54,7 @@ import path from "node:path";
54
54
  import log from "loglevel";
55
55
 
56
56
  import { NO_SYSTEM, canonicalKey } from "./content-address.mjs";
57
- import { isAddressSegment } from "./address-charset.mjs";
57
+ import { ADDRESS_SEGMENT_PATTERN, isAddressSegment } from "./address-charset.mjs";
58
58
  import { makeId } from "./ids.mjs";
59
59
  import { locateFrontmatterKey } from "./retired-fields.mjs";
60
60
 
@@ -242,7 +242,9 @@ export function collectFolderNotes(notes, pkg) {
242
242
  throw Object.assign(
243
243
  new Error(
244
244
  `folder shortcode "${shortcode}" is not strictly ` +
245
- `alphanumeric, so its address would not parse: ${absPath}`,
245
+ `alphanumeric lowercase letters and digits only ` +
246
+ `(${ADDRESS_SEGMENT_PATTERN.source}) — so its address ` +
247
+ `would not parse: ${absPath}`,
246
248
  ),
247
249
  { absPath },
248
250
  );
@@ -583,9 +583,9 @@ function checkKeyedMap(note, { field, segments, entries, raw, packs }) {
583
583
  *
584
584
  * **Two checks, in this order** — the charset, then the closed set. The
585
585
  * charset is first because it is the more general statement about
586
- * the same value: a value outside `^[A-Za-z0-9]+$` is refused whatever the type
587
- * declares, and only once it is a well-formed term is the type's own list the
588
- * reason to refuse it.
586
+ * the same value: a value outside `ADDRESS_SEGMENT_PATTERN` is refused
587
+ * whatever the type declares, and only once it is a well-formed term is the
588
+ * type's own list the reason to refuse it.
589
589
  *
590
590
  * There is deliberately no third, retired-spelling check ahead of them
591
591
  * accepting `user-guide` as a warning naming `userguide`. Every consumer tree
@@ -674,7 +674,8 @@ function checkSubType(note, { type, entry }) {
674
674
  *
675
675
  * @param {object} note - The note.
676
676
  * @param {object} opts
677
- * @param {readonly string[]} opts.tags - Every declared tag, flattened.
677
+ * @param {string} opts.type - The note's declared `type`, which scopes the
678
+ * groups checked against.
678
679
  * @returns {object[]} Findings.
679
680
  */
680
681
  function checkTags(note, { type }) {
@@ -729,6 +730,27 @@ const ART_FIELDS = Object.freeze([
729
730
  Object.freeze({ key: "portrait", inData: true }),
730
731
  ]);
731
732
 
733
+ /**
734
+ * The keys a field declaration is authored at **inside a system block**.
735
+ *
736
+ * The first segment of each field's in-block key: `impact.die` is authored as
737
+ * `impact`, and a field whose shared source moved under `data:` is authored at
738
+ * the `legacyKey` it declares rather than at its dotted name. Keying on
739
+ * the name instead would report `sohl.species` as a property no `being` has,
740
+ * against exactly the notes the sweep has not reached yet.
741
+ *
742
+ * Written once and read twice: the note type's own declaration answers for the
743
+ * system whose vocabulary the caller's `schemas` are, and a second system's
744
+ * registry answers for its block. Two derivations of one thing would be
745
+ * free to disagree about which position a note authors.
746
+ *
747
+ * @param {readonly object[]|null|undefined} schema - A type's declarations.
748
+ * @returns {Set<string>} The in-block keys.
749
+ */
750
+ function inBlockKeys(schema) {
751
+ return new Set(authoredFields(schema ?? []).map((f) => legacyKeyOf(f).split(".")[0]));
752
+ }
753
+
732
754
  /**
733
755
  * The in-block keys a type's own declarations claim for a *different* quantity.
734
756
  *
@@ -756,27 +778,6 @@ const ART_FIELDS = Object.freeze([
756
778
  * @returns {Set<string>} The in-block keys that are not the note-level field of
757
779
  * the same name.
758
780
  */
759
- /**
760
- * The keys a field declaration is authored at **inside a system block**.
761
- *
762
- * The first segment of each field's in-block key: `impact.die` is authored as
763
- * `impact`, and a field whose shared source moved under `data:` is authored at
764
- * the `legacyKey` it declares rather than at its dotted name. Keying on
765
- * the name instead would report `sohl.species` as a property no `being` has,
766
- * against exactly the notes the sweep has not reached yet.
767
- *
768
- * Written once and read twice: the note type's own declaration answers for the
769
- * system whose vocabulary the caller's `schemas` are, and a second system's
770
- * registry answers for its block. Two derivations of one thing would be
771
- * free to disagree about which position a note authors.
772
- *
773
- * @param {readonly object[]|null|undefined} schema - A type's declarations.
774
- * @returns {Set<string>} The in-block keys.
775
- */
776
- function inBlockKeys(schema) {
777
- return new Set(authoredFields(schema ?? []).map((f) => legacyKeyOf(f).split(".")[0]));
778
- }
779
-
780
781
  function collidingBlockKeys(schema) {
781
782
  const keys = new Set();
782
783
  if (!Array.isArray(schema)) return keys;
@@ -909,8 +910,9 @@ function checkEmbeddedShortcodes(note, blockName) {
909
910
  * @param {object} note - A note from the link index (`{fm, file, raw, type}`).
910
911
  * @param {object} opts
911
912
  * @param {Record<string, readonly object[]>} opts.schemas - Type → declaration.
912
- * @param {object} [opts.index] - The link index, for the reference check. Its
913
- * absence skips that check rather than reporting every reference as dead.
913
+ * @param {object} [opts.index] - The link index, for the reference check, which
914
+ * runs through its `referenceHit`. Its absence skips that check rather than
915
+ * reporting every reference as dead.
914
916
  * @param {Record<string, object>} [opts.vocabulary] - Type → the closed regions
915
917
  * it declares, as `engine/note-vocabulary.mjs` states them. Supplied
916
918
  * by the caller for the same reason `schemas` is: this module validates a
@@ -1466,19 +1468,14 @@ export function lintNote(
1466
1468
  continue;
1467
1469
  }
1468
1470
 
1469
- // A reference names another note by shortcode. Resolved through the
1470
- // link index's own resolver, so a cross-package reference answered by a
1471
- // fetched index lands exactly as the same address in a wikilink
1472
- // would rather than through a second, subtly different rule.
1473
- //
1474
- // **As an address, always** — which is now the only namespace there
1475
- // is. A frontmatter reference is a bare address by construction:
1476
- // there is no pipe to read intent from, and the field supplies the
1477
- // type. The resolver once took a namespace argument, and omitting it
1478
- // read every `ref:` value as an alias, which `type-shortcode` never was.
1471
+ // A reference names another note by shortcode, and the field supplies
1472
+ // the type, so the resolver is handed the whole `type-shortcode` pair.
1473
+ // It resolves in any reachable package: the value is persisted as
1474
+ // written and looked up at runtime among one actor's embedded items,
1475
+ // which come from every package the actor draws on.
1479
1476
  if (field.ref && index && typeof value === "string" && value) {
1480
1477
  const target = `${field.ref}-${value}`;
1481
- if (!index.resolve(target) && !index.manifestHit(target)) {
1478
+ if (!index.referenceHit(target)) {
1482
1479
  findings.push({
1483
1480
  file: note.file,
1484
1481
  ...at(head, value),
@@ -403,6 +403,11 @@ export function unsatisfiedPassDependencies(running, config) {
403
403
  * document type that routes nowhere. True for the first configured pack of
404
404
  * the type, so one unroutable note yields one error rather than one per
405
405
  * pack.
406
+ * @param {object} folderNotes - The folder index, which resolves a note's
407
+ * `packFolder` address to a Foundry folder id and its ancestor chain.
408
+ * @param {object} corpus - The corpus this compile derived once, shared by
409
+ * every pass so that no two answer "which files are the corpus?"
410
+ * differently.
406
411
  * @returns {Promise<{errors: number, compiled: number}>} The compiler's error
407
412
  * count (0 on success) and the number of entries it wrote.
408
413
  */