@heroiclands/package-build 23.7.1 → 23.9.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 (96) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +32 -0
  3. package/bin/content-commands.mjs +32 -10
  4. package/ci/check-banners.mjs +37 -19
  5. package/container.mjs +48 -10
  6. package/docs/api.md +402 -58
  7. package/docs/authoring/first-note.md +1 -1
  8. package/docs/authoring/frontmatter.md +30 -1
  9. package/docs/authoring/links-and-markup.md +154 -56
  10. package/docs/commands.md +31 -12
  11. package/docs/content-format.md +25 -2
  12. package/docs/design.md +31 -0
  13. package/docs/diagnostics.md +1 -1
  14. package/docs/guides/e2e.md +37 -1
  15. package/docs/guides/packs.md +2 -1
  16. package/docs/guides/reference-pages.md +86 -63
  17. package/docs/project-setup.md +30 -15
  18. package/docs/reference/format-details.md +313 -132
  19. package/docs/reference/note-types.md +191 -174
  20. package/e2e.mjs +89 -2
  21. package/engine/anchors.mjs +7 -20
  22. package/engine/base-compiler.mjs +11 -18
  23. package/engine/compile-corpus.mjs +5 -3
  24. package/engine/content-alerts.mjs +4 -2
  25. package/engine/content-blocks.mjs +18 -36
  26. package/engine/content-format.yaml +1 -0
  27. package/engine/content-index.mjs +447 -33
  28. package/engine/content-links.mjs +43 -15
  29. package/engine/content-poetry.mjs +9 -0
  30. package/engine/content-tables.mjs +8 -4
  31. package/engine/culture-reach.mjs +61 -34
  32. package/engine/data-keys.mjs +3 -1
  33. package/engine/diagnostics.mjs +24 -11
  34. package/engine/dictionary-layout.mjs +151 -19
  35. package/engine/dictionary-rows.mjs +129 -79
  36. package/engine/foundry-entries.mjs +21 -3
  37. package/engine/frontmatter-lint.mjs +1 -1
  38. package/engine/generate.mjs +12 -22
  39. package/engine/glossary-entries.mjs +11 -11
  40. package/engine/heading-attributes.mjs +21 -28
  41. package/engine/helpers.mjs +20 -12
  42. package/engine/infobox.mjs +3 -0
  43. package/engine/journals.mjs +15 -2
  44. package/engine/language-data.mjs +53 -0
  45. package/engine/language-terms.mjs +86 -0
  46. package/engine/language-words.mjs +2 -2
  47. package/engine/lexical-sections.mjs +247 -0
  48. package/engine/markdownlint-run.mjs +12 -2
  49. package/engine/markup-validation.mjs +34 -0
  50. package/engine/note-addresses.mjs +1 -0
  51. package/engine/note-claims.mjs +21 -10
  52. package/engine/note-package.mjs +3 -5
  53. package/engine/note-state.mjs +9 -3
  54. package/engine/note-vocabulary.mjs +36 -7
  55. package/engine/pdf-build.mjs +17 -10
  56. package/engine/prose-lint.mjs +12 -6
  57. package/engine/related-pages.mjs +22 -3
  58. package/engine/see-also.mjs +150 -0
  59. package/engine/site-build.mjs +21 -3
  60. package/engine/site-preview.mjs +4 -2
  61. package/engine/sql-tables.mjs +287 -53
  62. package/engine/standings.mjs +14 -12
  63. package/engine/stub-lint.mjs +6 -2
  64. package/engine/web-wikilinks.mjs +8 -6
  65. package/engine/word-rows.mjs +20 -12
  66. package/engine/word-terms.mjs +15 -6
  67. package/hugo-theme/layouts/partials/related.html +25 -8
  68. package/package.json +2 -1
  69. package/types/container.d.mts +4 -3
  70. package/types/e2e.d.mts +28 -0
  71. package/types/engine/base-compiler.d.mts +2 -2
  72. package/types/engine/compile-corpus.d.mts +2 -1
  73. package/types/engine/content-index.d.mts +85 -8
  74. package/types/engine/content-tables.d.mts +2 -1
  75. package/types/engine/culture-reach.d.mts +22 -2
  76. package/types/engine/data-keys.d.mts +7 -0
  77. package/types/engine/diagnostics.d.mts +7 -6
  78. package/types/engine/dictionary-layout.d.mts +35 -4
  79. package/types/engine/dictionary-rows.d.mts +9 -0
  80. package/types/engine/foundry-entries.d.mts +3 -1
  81. package/types/engine/heading-attributes.d.mts +2 -2
  82. package/types/engine/helpers.d.mts +11 -5
  83. package/types/engine/language-terms.d.mts +7 -0
  84. package/types/engine/lexical-sections.d.mts +95 -0
  85. package/types/engine/markdownlint-run.d.mts +3 -1
  86. package/types/engine/markup-validation.d.mts +14 -0
  87. package/types/engine/note-addresses.d.mts +10 -0
  88. package/types/engine/note-claims.d.mts +7 -1
  89. package/types/engine/note-package.d.mts +3 -1
  90. package/types/engine/note-state.d.mts +2 -1
  91. package/types/engine/note-vocabulary.d.mts +1 -0
  92. package/types/engine/related-pages.d.mts +10 -1
  93. package/types/engine/see-also.d.mts +55 -0
  94. package/types/engine/site-build.d.mts +4 -1
  95. package/types/engine/sql-tables.d.mts +40 -6
  96. package/types/engine/word-rows.d.mts +9 -7
package/docs/api.md CHANGED
@@ -348,6 +348,20 @@ An addressed content note accepts exactly these top-level keys, in this order: `
348
348
  | `dataFields` | `dataFields(type, vocabulary)` | `readonly DataFieldSpec[] \| undefined` | looking up the `data:` keys a note type may carry |
349
349
  | `subTypes` | `subTypes(type, vocabulary)` | `readonly string[] \| null \| undefined` | looking up the closed `subType` values a note type declares |
350
350
 
351
+ ### `./engine/see-also`
352
+
353
+ Import `@heroiclands/package-build/engine/see-also` for the deliberate
354
+ cross-references declared by `data.seeAlso`. Entries preserve authored order;
355
+ the caller supplies the medium's reference resolver and link renderer.
356
+
357
+ | Export | Signature | Returns | Use it when |
358
+ | ----------------- | ----------------------------------------------- | ----------------------------- | ------------------------------------------------------------------- |
359
+ | `checkSeeAlso` | `checkSeeAlso(note, { index, addressContext })` | `object[]` | locating malformed, unresolved, duplicate or self references |
360
+ | `seeAlsoEntries` | `seeAlsoEntries(fm, resolve)` | `object[]` | resolving names and destinations with optional glosses |
361
+ | `seeAlsoMarkdown` | `seeAlsoMarkdown(entries, link, level = 2)` | `string` | emitting an ordered See also section, empty when no entries resolve |
362
+ | `seeAlsoWebLink` | `seeAlsoWebLink(entry)` | `string` | writing a resolved book reference as a Markdown link |
363
+ | `seeAlsoPages` | `seeAlsoPages(pages, resolve)` | `Map<string, RelatedEntry[]>` | deriving ordered site connections for both builds and live previews |
364
+
351
365
  ### `engine.infobox`
352
366
 
353
367
  The declared infobox: what a note's summary panel holds, decided once and rendered by each medium. The note box's fields are the type's own `data:` vocabulary, in its declared order, so a key added to a type appears everywhere with no second edit; the overlay declares only the label and the handful of keys that carry no row.
@@ -412,7 +426,14 @@ Drawing a declared infobox: HTML for separate Foundry pages appended after autho
412
426
 
413
427
  ### `engine.noteState`
414
428
 
415
- The three states of a note — **stub**, **draft**, **full** — and the one observable fact they rest on. A note whose body is empty is a stub: it carries its facts into the index and is queryable there, and it has no page, no address and no link target. Nothing is stored: the ladder is derived once in the SQL `entries` view and no note can assert its own state.
429
+ The three states of a note — **stub**, **draft**, **full** — describe its page
430
+ content. A stubbable note with an empty body and no generated Dictionary or
431
+ Glossary content is a stub: its facts remain queryable in the index, but it
432
+ has no page, address or link target. `isStubNote` recognizes nonempty lexical
433
+ frontmatter as generated content, so collectors retain the resulting page.
434
+ That lexical frontmatter supplies content without an authored body.
435
+ The ladder is derived in the SQL `entries` view; a note cannot assert its own
436
+ state.
416
437
 
417
438
  | Export | Signature | Returns | Use it when |
418
439
  | --------------------- | ------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -444,7 +465,7 @@ What a draft note says about itself, in one sentence, on every surface it reache
444
465
 
445
466
  ### `engine.stubLint`
446
467
 
447
- Every typed note must have a nonempty body. Empty and whitespace-only bodies are errors regardless of description or `draft` tag, including structural types such as `folder` and `homepage`. Placeholder-only bodies also produce errors. Nonempty bodies with fewer than 25 prose words produce warnings unless tagged `draft`. A `folder` note is held only to the nonempty rule: any body, placeholder or short, passes with or without `draft`. Untyped vault scaffolding is excluded. The lint, content-index, package compile, site and PDF commands enforce body errors before publishing. The state classification helpers still describe incomplete records for inspection; their classifications do not exempt notes from validation.
468
+ Nonempty `data.words` on a `skill`/`language` note, or `data.glossary` on a culture or affiliation, supplies content without an authored body. Absent or empty arrays supply no such exemption. Every other typed note must have a nonempty body: empty and whitespace-only bodies are errors regardless of description or `draft` tag, including `folder` and `homepage`. Nonempty placeholder-only bodies still produce errors, including on lexical notes. Nonempty bodies with fewer than 25 prose words produce warnings unless tagged `draft`. A `folder` note is held only to the nonempty rule: any body, placeholder or short, passes with or without `draft`. Untyped vault scaffolding is excluded. The lint, content-index, package compile, site and PDF commands enforce body errors before publishing.
448
469
 
449
470
  | Export | Signature | Returns | Use it when |
450
471
  | ------------------ | ---------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------- |
@@ -847,32 +868,87 @@ The published content index — the artifact packages exchange addresses through
847
868
 
848
869
  Emitting this package's cross-package link manifest. `engine/content-address.mjs` owns the address _grammar_ — how a key is version is read, how a foreign file resolves. This module owns the _pass_: walking a content tree and deriving, for every note it publishes, the addresses that entry states. The two halves were split across the format module and a hand-written script in each consuming repository, which is how the two scripts came to differ in ways nobody chose — one routes its UUIDs through the pack router and one does not, and neither knew.
849
870
 
850
- | Export | Signature | Returns | Use it when |
851
- | ----------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
852
- | `LEAD_ANCHOR` | `const LEAD_ANCHOR` | — | The reserved anchor name for a journal's **first** page. |
853
- | `anchorsOf` | `function anchorsOf(entryUuid, entryId, body, name)` | {Record<string, string>} The anchors. | Every page of a note's journal, as `anchorName → whole UUID`. |
854
- | `entriesForNote` | `function entriesForNote(fm, name, address, body, ctx)` | {Array<object>} One or two entries, in {@link buildManifest}'s shape. | The manifest entries a single note produces. |
855
- | `collectFoundryEntries` | `function collectFoundryEntries(contentBase, ctx)` | `{{entries: Array<object>, notes: number, skipped: Array<{file: string, reason: string}>}}` | Every note this package publishes, as manifest entries. |
856
- | `foundryIdentities` | `function foundryIdentities(config` | `{{contentPackage: string, foundryPackageId: string, packRouter: object, web: boolean, skipDirectories: readonly string[]}}` | The identities an emission runs against, from configuration. |
857
- | `entryContext` | `function entryContext(config` | `{{contentPackage: string, foundryPackageId: string, packRouter: object, web: boolean, skipDirectories: readonly string[]}}` | The identities an emission runs against, from configuration. |
871
+ | Export | Signature | Returns | Use it when |
872
+ | ----------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
873
+ | `LEAD_ANCHOR` | `const LEAD_ANCHOR` | — | The reserved anchor name for a journal's **first** page. |
874
+ | `anchorsOf` | `function anchorsOf(entryUuid, entryId, body, name)` | {Record<string, string>} The anchors. | Every anchor of a note's journal, as `anchorName → Foundry target`. |
875
+ | `entriesForNote` | `function entriesForNote(fm, name, address, body, ctx)` | {Array<object>} One or two entries, in {@link buildManifest}'s shape. | The manifest entries a single note produces. |
876
+ | `collectFoundryEntries` | `function collectFoundryEntries(contentBase, ctx)` | `{{entries: Array<object>, notes: number, skipped: Array<{file: string, reason: string}>}}` | Every note this package publishes, as manifest entries. |
877
+ | `foundryIdentities` | `function foundryIdentities(config` | `{{contentPackage: string, foundryPackageId: string, packRouter: object, web: boolean, skipDirectories: readonly string[]}}` | The identities an emission runs against, from configuration. |
878
+ | `entryContext` | `function entryContext(config` | `{{contentPackage: string, foundryPackageId: string, packRouter: object, web: boolean, skipDirectories: readonly string[]}}` | The identities an emission runs against, from configuration. |
879
+
880
+ A generated dictionary headword maps to its containing page's UUID followed
881
+ by its word fragment. Other authored spans keep their containing-page
882
+ mapping.
858
883
 
859
884
  ### `engine.contentIndex`
860
885
 
861
- Emitting this package's content index. Every content build already walks the whole note tree and parses every note's frontmatter — the pack compilers, the site build, and the content-table expander each do it — and every one of them throws the result away when it finishes. So nothing outside a build can ask a question about the content: "which beings carry no `kbcat`?", "what does this table actually select?", "did that type rename leave anything behind?" have no answer short of writing a throwaway script that re-walks the tree. Eight dead Bestiary tables shipped for weeks behind exactly that gap.
862
-
863
- | Export | Signature | Returns | Use it when |
864
- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
865
- | `noteAddress` | `function noteAddress(frontmatter, contentPackage)` | {{slug: string, canonical: string}\|null} `slug` is what goes inside `[[…]]` within this package; `canonical` is the fully qualified key the manifest files the note under, carrying the package and the system as well. | The address a wikilink writes to reach a note, or `null` when it has none. |
866
- | `sortKeysDeep` | `function sortKeysDeep(value)` | {unknown} The value with every plain object's keys in sorted order. | Recursively sort an object's keys, so serialization is order-independent. |
867
- | `asciiName` | `function asciiName(name)` | {string\|null} The ASCII form, or `null` when there is no name, or nothing printable survives. | A note's display name reduced to printable 7-bit ASCII. |
868
- | `asciiAliases` | `function asciiAliases(aliases)` | {Array<string>} Possibly empty, never null: a note with no aliases has an empty set of them, which is a fact rather than a missing value, and a consumer iterating it should not have to check first. | A note's `name.aliases` reduced to printable 7-bit ASCII, in order. |
869
- | `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
870
- | `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems })` | {object[]} One record per note, plus one per documentation entry. | The index records for a content tree, without writing anything. |
871
- | `indexRecordsForNote` | `indexRecordsForNote({ frontmatter, body, bodyLine, relPath, absPath, contentPackage, manifest, addressContext })` | `object[]` | Derive the note's own record and its documentation record from parsed text. |
872
- | `emitContentIndex` | `emitContentIndex({ contentBase, outDir, config })` | `{{file: string, notes: number, assets: number, records: number, bytes: number, full: number, draft: number, stub: number}}` Where it was written, how many notes and how many assets it holds, how many records that is in all, its size, and the ladder those notes sit on. | Emit this package's content index. |
873
- | `buildIndexRecord` | `buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest })` | `Record<string, any>` — the record, with derived fields sorted deep | building one note's content-index record; refuses a note that authors a key the index derives itself (`package` among them) before building anything |
874
- | `collectContentIndex` | `collectContentIndex(contentBase, { contentPackage, skipDirectories, assetsBase, manifest, problems, foreignRecord })` | `Array<Record<string, any>>` | reading a whole content tree into index records, in a total order independent of directory-read order; an item note yields two records (its own and its documentation journal's), and each asset root yields one per file; each note carries its resolved culture as `data.culture` |
875
- | `resolveCultures` | `resolveCultures(records, { contentPackage, contentBase, foreignRecord, problems })` | `Array<Record<string, any>>` — the same records | writing each note's resolved culture into its record as `data.culture`: its own value, else its parents', else for a polity its seat's and governed places', pooled; parents that disagree give a finding into `problems`, or one thrown Error listing every conflict |
886
+ Collecting and publishing this package's content index. Records describe note frontmatter, assets, prose and event anchors, and the document targets those notes produce. Dynamic publication prepares the same generated Markdown and scalar expressions as the rendering surfaces, using each source note's audience.
887
+
888
+ | Export | Signature | Returns | Use it when |
889
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
890
+ | `noteAddress` | `function noteAddress(frontmatter, contentPackage)` | {{slug: string, canonical: string}\|null} `slug` is what goes inside `[[…]]` within this package; `canonical` is the fully qualified key the manifest files the note under, carrying the package and the system as well. | The address a wikilink writes to reach a note, or `null` when it has none. |
891
+ | `sortKeysDeep` | `function sortKeysDeep(value)` | {unknown} The value with every plain object's keys in sorted order. | Recursively sort an object's keys, so serialization is order-independent. |
892
+ | `asciiName` | `function asciiName(name)` | {string\|null} The ASCII form, or `null` when there is no name, or nothing printable survives. | A note's display name reduced to printable 7-bit ASCII. |
893
+ | `asciiAliases` | `function asciiAliases(aliases)` | {Array<string>} Possibly empty, never null: a note with no aliases has an empty set of them, which is a fact rather than a missing value, and a consumer iterating it should not have to check first. | A note's `name.aliases` reduced to printable 7-bit ASCII, in order. |
894
+ | `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
895
+ | `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems, parsed })` | {object[]} One record per note, plus one per documentation entry. | The index records for a content tree, without writing anything. |
896
+ | `indexRecordsForNote` | `indexRecordsForNote({ frontmatter, body, bodyLine, relPath, absPath, raw, contentPackage, manifest, addressContext })` | `object[]` | Derive the note's own record and its documentation record from parsed text. |
897
+ | `prepareContentIndex` | `async prepareContentIndex({ contentBase, config } = {})` | `Promise<PreparedContentIndex>` — the captured source context and complete expanded bodies, described below | Prepare dynamic publication without writing files. |
898
+ | `emitContentIndex` | `emitContentIndex({ contentBase, outDir, config, publication } = {})` | `{{file: string, notes: number, assets: number, records: number, bytes: number, full: number, draft: number, stub: number, dictionary: {file: string, rows: number}, terms: {file: string, rows: number}}}` | Synchronously publish metadata and the dictionary and terms datafiles; dynamic content requires a complete `publication`. |
899
+ | `buildIndexRecord` | `buildIndexRecord({ frontmatter, relPath, absPath, raw, contentPackage, body, bodyLine, manifest })` | `Record<string, any>` — the record, with derived fields sorted deep | building one note's content-index record; refuses a note that authors a key the index derives itself (`package` among them) before building anything |
900
+ | `collectContentIndex` | `collectContentIndex(contentBase, { contentPackage, skipDirectories, assetsBase, manifest, problems, foreignRecord, parsed })` | `Array<Record<string, any>>` | reading a whole content tree into index records, in a total order independent of directory-read order; an item note yields two records (its own and its documentation journal's), and each asset root yields one per file; each note carries its resolved culture as `data.culture` |
901
+ | `resolveCultures` | `resolveCultures(records, { contentPackage, contentBase, foreignRecord, problems })` | `Array<Record<string, any>>` — the same records | writing each note's resolved culture into its record as `data.culture`: its own value, else its parents', else for a polity its seat's and governed places', pooled; parents that disagree give a finding into `problems`, or one thrown Error listing every conflict |
902
+
903
+ `prepareContentIndex` resolves `contentBase` from the configured `paths.content`
904
+ when omitted. Its `PreparedContentIndex` contains:
905
+
906
+ | Field | Value |
907
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
908
+ | `config` | The resolved configuration object used for preparation. |
909
+ | `contentBase` | The absolute source root. |
910
+ | `records` | The complete note, documentation and asset records, including hidden providers needed for lineage. |
911
+ | `parsed` | A `Map` keyed by absolute note file, holding authored `frontmatter`, `body`, `raw`, `bodyLine` and `bodyColumn`. |
912
+ | `expandedBodies` | A `Map` keyed by absolute note file, holding `markdown`, its body-relative `lineMap` of `{line, generated}` entries, and optional `generatedFrom`. |
913
+ | `foundryBodies` | A `Map` with the same body shape, prepared against the complete native compendium corpus. |
914
+
915
+ Preparation expands SQL tables, page lists, automatic sections and scalar
916
+ expressions. Prose declarations in `expandedBodies` use each source's audience:
917
+ public notes use public providers and usage; GM notes use the complete corpus.
918
+ Automatic lexical sections keep that source audience on every surface.
919
+ Explicit native SQL uses the complete corpus in `foundryBodies`.
920
+
921
+ Native own and documentation target maps derive their actual page identities
922
+ from `foundryBodies`. Each map contains only prose slugs declared in the
923
+ source-audience body and present in the native body, plus the journal's
924
+ reserved `$lead` target. A prose target present only on the website remains
925
+ valid for website links and prose-link checks; a native link to it is refused.
926
+ Native-only slugs and private heading names stay out of a public source's
927
+ metadata. Event anchors and authored source positions remain attached to their
928
+ sources. A published empty prose-anchor list declares no prose targets,
929
+ including on web-only pages.
930
+
931
+ Pass the envelope directly to the synchronous emitter:
932
+
933
+ ```js
934
+ const publication = await prepareContentIndex({ config, contentBase });
935
+ const result = emitContentIndex({ publication, outDir });
936
+ ```
937
+
938
+ The emitter defaults `config` and `contentBase` from the envelope. If supplied,
939
+ they must name that same configuration object and source root. It consumes
940
+ `records`, `parsed`, `expandedBodies` and `foundryBodies` without mutating them;
941
+ frozen inputs are valid. Every note requires a parsed source and both body
942
+ entries.
943
+ Missing or mismatched preparation is refused before creating or replacing any
944
+ metadata, dictionary or terms file.
945
+
946
+ Without `publication`, the synchronous emitter accepts authored bodies and
947
+ pure frontmatter dictionary or glossary sections. Active SQL, page lists,
948
+ scalar expressions, events or other generated sections require
949
+ `prepareContentIndex`. The CLI performs this preparation automatically.
950
+ `indexRecordsFor` and `collectContentIndex` collect authored records and pure
951
+ lexical sections; use preparation and emission to publish dynamic body targets.
876
952
 
877
953
  `engine.contentIndex` also re-exports eight names from neighbouring leaf modules, at this same import path:
878
954
 
@@ -883,7 +959,7 @@ Emitting this package's content index. Every content build already walks the who
883
959
  | `noteFile` | `noteFile(contentBase, record)` | `string` — the note's absolute path | composing an index record's `file.path` (recorded relative, for a byte-stable artifact) back into an openable absolute path |
884
960
  | `authoredFrontmatter` | `authoredFrontmatter(record)` | `Record<string, any>` — the frontmatter without `DERIVED_KEYS` | reading a note's frontmatter back from its index record, its resolved `data.culture` included, so a pass can lint or compile from the index without reasoning about derived keys as if the author wrote them |
885
961
  | `isNoteRecord` | `isNoteRecord(record)` | `boolean` | telling a note's own record apart from its documentation journal's and from an asset's, when enumerating the corpus |
886
- | `isStub` | `isStub(record)` | `boolean` | telling a stub — a note with an empty body — apart from a note that publishes a page; asked of the absent address, which is the whole signal |
962
+ | `isStub` | `isStub(record)` | `boolean` | telling a stub apart from a note that publishes a page; asked of the absent address, which is the whole signal |
887
963
  | `isAssetRecord` | `isAssetRecord(record)` | `boolean` | telling a record that addresses a file apart from one that addresses a note; the `asset` block is what marks it |
888
964
  | `recordPath` | `recordPath(record)` | `string` — the path the record names inside its package | ordering or reporting across both record shapes: a note names the `.md` it was parsed from, an asset the file it is |
889
965
 
@@ -988,7 +1064,7 @@ content tree can also compile into Foundry compendium packs.
988
1064
  | `collectContentPages` | `function collectContentPages(contentBase, ctx)` | `{{pages: object[], addressFindings: object[], fmLinkFindings: object[]}}` | The content tree's pages, and what could not be addressed. |
989
1065
  | `collectHomepages` | `function collectHomepages(contentBase, ctx)` | `{{pages: object[], addressFindings: object[]}}` The homepage notes, in walk order, and the ones among them that could not be addressed. | The package's homepage notes — the authored page at `/<contentPackage>/`. |
990
1066
  | `writeHomepages` | `function writeHomepages(outRoot, pages, config)` | {number} How many pages were written. | Writes each homepage as the package root's `_index.md`. |
991
- | `siteGates` | `function siteGates(pages, findings,` | {object} The gate results and, when they pass, the built index. | The integrity gates a site build runs before it writes anything. |
1067
+ | `siteGates` | `siteGates(pages, findings, options)` | {object} The gate results and, when they pass, the built index. | The integrity gates a site build runs before it writes anything. |
992
1068
  | `emptyGates` | `function emptyGates()` | {object} An all-clear gate result. | The gate result of a build that ran none of them. |
993
1069
  | `gatesFailed` | `function gatesFailed(gates)` | — | Whether any gate produced a finding. |
994
1070
  | `tableUniverse` | `function tableUniverse(pages)` | {Map<string, object[]>} Package → the notes it may tabulate. | The universe a generated table searches, grouped by package. |
@@ -1000,6 +1076,10 @@ content tree can also compile into Foundry compendium packs.
1000
1076
  | `sitePageDecorator` | `sitePageDecorator(config, index)` | Function | Add the site's infobox and being metadata to a rendered page. |
1001
1077
  | `buildSite` | `buildSite({ config, sqlTables })` | `{{gates: object, stats: object\|null, tableErrors: object[], wikiErrors: object[], mapFindings: object[], manifests: object\|null}}` | Builds a Hugo content tree from a content tree, and reports what it found. |
1002
1078
 
1079
+ `siteGates` takes `{ config, records, sqlTables? }`. Pass the prepared
1080
+ `sqlTables` map so anchors emitted by automatic dictionary sections are present in the
1081
+ index for checking page links.
1082
+
1003
1083
  ### `engine.sitePreview`
1004
1084
 
1005
1085
  `prepareSitePreview({ config })` reads the saved content tree and declared dependency indexes, prepares its SQL corpus, and returns a private preview context. Pass the active note's absolute path and complete buffer text to `render(file, text)`. A successful result has `{ ok: true, markdown, frontmatter, findings: [] }`; a failed result has `{ ok: false, findings }` and no replacement content. Findings identify the source file and carry a line and column when the offending text can be located.
@@ -1197,7 +1277,7 @@ Shared helpers for the pack compilers in `packages/package-build/`. The HeroicLa
1197
1277
  | ------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1198
1278
  | `md` | `const md` | — | The markdown renderer every surface shares. |
1199
1279
  | `renderFoundryMarkdown` | `function renderFoundryMarkdown(body)` | `string` — HTML with secret sections for GM passages. | Rendering a note body for a Foundry document. |
1200
- | `parseMarkdownFile` | `function parseMarkdownFile(filePath)` | — | Parses a markdown file with YAML frontmatter. |
1280
+ | `parseMarkdownFile` | `function parseMarkdownFile(filePath)` | — | Parses a markdown file with YAML frontmatter; returns frontmatter, body, rendered description, raw source, and body coordinates. |
1201
1281
  | `assertStatedScope` | `function assertStatedScope(skipDirectories, who)` | {void} | Refuse a corpus read whose scope its caller did not state. |
1202
1282
  | `assertSuppliedCorpus` | `function assertSuppliedCorpus(records, who)` | {void} | Refuse a corpus read whose records its caller did not supply. |
1203
1283
  | `statedTemplatePriority` | `function statedTemplatePriority(fm, label,` | {number\|null} The priority, or `null` when the note is not a template or states nothing. | The template priority a note states, for a system that treats an unstated one as "not a template" rather than as an authoring error. |
@@ -1210,11 +1290,16 @@ Shared helpers for the pack compilers in `packages/package-build/`. The HeroicLa
1210
1290
  | `buildStats` | `function buildStats(systemVersion` | {object} The `_stats` block. | Default `_stats` block for compiled compendium entries. |
1211
1291
  | `statsForPack` | `function statsForPack(packSystem, config` | {object} The `_stats` block for that pack. | The `_stats` block for one pack, stamped with the system that pack is for. |
1212
1292
  | `defaultStats` | `function defaultStats()` | {object} The default `_stats` block, shared by every compiler. | The `_stats` block every compiler stamps on an entry it emits, built once. |
1213
- | `buildContentLinkIndex` | `buildContentLinkIndex(contentBase, router, { skipDirectories, config, records, problems })` | `{{byShortcode: Map, types: Set}}` From `buildWikilinkIndex`. | Indexes **every** note in the content tree so any pack compiler can resolve a wikilink to any other document. |
1293
+ | `buildContentLinkIndex` | `buildContentLinkIndex(contentBase, router?, options?)` | `{{byShortcode: Map, types: Set}}` From `buildWikilinkIndex`. | Indexes **every** note in the content tree so any pack compiler can resolve a wikilink to any other document. |
1214
1294
  | `convertNoteWikilinks` | `convertNoteWikilinks(body, { type, id, pack, docPack, index, name, file, bodyLine, bodyColumn, lineMap })` | `{{markdown: string, unresolved: Array<object>}}` | Converts the wikilinks in one note's markdown, reporting any that have no target in the content tree. |
1215
1295
  | `expandNoteTables` | `function expandNoteTables(body,` | `{{markdown: string, lineMap: Array<{line: number, generated: boolean}>}}` The body with every table expanded, and where each emitted line came from — which is what lets a diagnostic about the expanded body name an authored position. | Expand prepared SQL tables in one note's markdown, before wikilinks are resolved — so a generated cell may itself be a wikilink. |
1216
1296
  | `folderFilename` | `function folderFilename(name, id)` | — | Builds a compendium-source filename for a folder JSON document: `folder_Name_id.json` with non-alphanumeric runs replaced by underscores. |
1217
- | `walkMarkdownTree` | `walkMarkdownTree(rootDir, { skipDirectories })` | generator yielding `{frontmatter, body, description, bodyLine?, bodyColumn?, file, absPath}` | walking a content tree and reading every markdown file's frontmatter and body; `skipDirectories` is required rather than defaulted, so two callers cannot silently disagree about the corpus's scope |
1297
+ | `walkMarkdownTree` | `walkMarkdownTree(rootDir, { skipDirectories })` | generator yielding `{frontmatter, body, description, raw, bodyLine?, bodyColumn?, file, absPath}` | walking a content tree and reading every markdown file's frontmatter and body; `skipDirectories` is required rather than defaulted, so two callers cannot silently disagree about the corpus's scope |
1298
+
1299
+ `buildContentLinkIndex` accepts `{ config?, records, sqlTables? }`.
1300
+ `records` is the required, supplied content corpus. The optional prepared
1301
+ `sqlTables` map supplies generated anchors and their Foundry page mappings,
1302
+ so glossary word links resolve to their dictionary entries.
1218
1303
 
1219
1304
  ### `engine.itemRegistry`
1220
1305
 
@@ -1807,6 +1892,12 @@ console.log(dataEnvVar("dev"));
1807
1892
  | `removeContainer` | `removeContainer(name, log = () => {})` | — | stopping and removing a container, tolerating "not running" and "no such container" |
1808
1893
  | `containerAction` | `containerAction({ action, stage, config, env, log })` | `number` | performing one `CONTAINER_ACTIONS` action for a stage end to end |
1809
1894
 
1895
+ `ensureS3` requires readable regular configuration files. Reuse requires an
1896
+ inspectable identities bind pointing to the current file; an incompatible bind
1897
+ throws with a scoped recovery command and leaves the server and bucket data
1898
+ intact. Both new and reused servers must answer their S3 HTTP health endpoint
1899
+ before the bucket is ensured.
1900
+
1810
1901
  ## `./coverage`
1811
1902
 
1812
1903
  Whether the localization keys a package **references** and the keys it **declares** are the same set. A referenced-but-undeclared key is an error — it renders as its raw key string to a player; a declared-but-unreferenced key is advisory only, since no scan can see every way a key might be reached. Generic Foundry reference shapes (`{{localize}}`, `game.i18n.localize`, `LOCALIZATION_PREFIXES`, template literals) are read here directly; a repository that mints keys by its own convention contributes a named module through configuration. Everything exported is pure — source text in, references or findings out.
@@ -1854,7 +1945,7 @@ console.log(parseRemote("user@example.org:/srv/foundry"));
1854
1945
 
1855
1946
  ## `./e2e`
1856
1947
 
1857
- The end-to-end harness: a disposable Foundry world, served from a container, with a browser suite driven against it. The harness does not know what the suite is — what runs against the served world is named in `packageBuild.e2e.suite` — it only stands the licensed Foundry up, seeds a world with a known Gamemaster password, waits for that world to be _active_, and tears it down again. Three shapes of run answer different questions: `run` (from scratch, the only path that may change Foundry build), `fast` (the iteration loop — rebuild, redeploy, cycle, re-run), and `sweep` (the same full run against a build the repository does not pin, so `compatibility.verified` can be evidence rather than hope).
1948
+ The end-to-end harness: a disposable Foundry world, served from a container, with a browser suite driven against it. The suite is named in `packageBuild.e2e.suite`. For recognizable Cypress commands, the harness supplies pinned Chrome for Testing unless the arguments select a browser. It stands the licensed Foundry up, seeds a world with a known Gamemaster password, waits for that world to be _active_, and tears it down again. Three shapes of run answer different questions: `run` (from scratch, the only path that may change Foundry build), `fast` (the iteration loop — rebuild, redeploy, cycle, re-run), and `sweep` (the same full run against a build the repository does not pin, so `compatibility.verified` can be evidence rather than hope).
1858
1949
 
1859
1950
  ```js
1860
1951
  import { hashPassword, E2E_MODES } from "@heroiclands/package-build/e2e";
@@ -1865,33 +1956,36 @@ console.log(E2E_MODES);
1865
1956
  // -> ["run", "open"]
1866
1957
  ```
1867
1958
 
1868
- | Export | Signature | Returns | Use it when |
1869
- | ----------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1870
- | `E2E_GM_ID` | `const E2E_GM_ID` | — | reading the fixed Gamemaster document id every seeded world carries |
1871
- | `E2E_SCENE_ID` | `const E2E_SCENE_ID` | — | reading the seeded, pre-activated default scene's id |
1872
- | `E2E_MODES` | `const E2E_MODES` | — | the closed set of modes the suite may run under (`"run"` / `"open"`) |
1873
- | `hashPassword` | `hashPassword(password, salt)` | `string` — the hex hash | hashing a password the way Foundry's own `core/auth.mjs` does, to seed a login that actually works |
1874
- | `resolveE2EWorld` | `resolveE2EWorld(config, env = process.env)` | `E2EWorld` | resolving the seeded world's identity from configuration, with an environment override under the repository's own variable prefix |
1875
- | `worldManifest` | `worldManifest({ worldId, worldTitle, worldDescription, systemId, systemVersion, coreVersion })` | `object` — the world manifest | building the `world.json` a seeded world carries |
1876
- | `gmDocument` | `gmDocument({ id, name, password, salt })` | `object` — the user document | building the seeded world's single Gamemaster user document |
1877
- | `defaultSceneDocument` | `defaultSceneDocument()` | `object` — the scene document | building the one pre-activated scene every seeded world carries |
1878
- | `moduleConfigurationDocument` | `moduleConfigurationDocument(packageId)` | `object` — the setting document | building the world setting that switches a module package on (a module must be activated; a system does not) |
1879
- | `isWorldActive` | `isWorldActive(body)` | `boolean` | checking whether a `/join` response body shows a world that is actually active, not merely reachable |
1880
- | `resolveSweepVersion` | `resolveSweepVersion(argv)` | `string` — the exact build, trimmed | reading the exact Foundry build a sweep was asked to run against; there is deliberately no default |
1881
- | `parseFastArgs` | `parseFastArgs(argv, build)` | `FastArgs` | parsing the fast loop's CLI arguments against the repository's declared build table, in declaration order |
1882
- | `seedTestWorld` | `async seedTestWorld({ config, packageJson, env = process.env, log = () => {}, runScript })` | `Promise<{worldDir: string, world: E2EWorld}>` | seeding the disposable world into the end-to-end stage's data root, replacing the world directory through a throwaway container so the Foundry container's user owns it |
1883
- | `deployForContainer` | `async deployForContainer({ config, env = process.env, log = () => {}, runScript })` | `Promise<void>` | deploying the staged package to the end-to-end stage, removing the old install and handing the new one to the Foundry container's user through a throwaway container |
1884
- | `waitForWorld` | `async waitForWorld({ url, container, stage, timeoutMs = 180_000, log = () => {} })` | `Promise<void>` | polling until a seeded world is active, or failing with a diagnosis (a licence failure is detected from the container log, not a timeout) |
1885
- | `suiteExecutables` | `suiteExecutables(command)` | `string[]` | reading every executable a suite command needs to exist before it can run at all |
1886
- | `findExecutable` | `findExecutable(name, { cwd, env = process.env } = {})` | `string\|null` | finding an executable the way the child process would (`node_modules/.bin` first, then `PATH`) |
1887
- | `missingExecutables` | `missingExecutables({ command, cwd, env = process.env })` | `string[]` | checking which of a suite command's executables are not there — asked again mid-run, since an install can pull the runner out from under a run in progress |
1888
- | `freshResults` | `freshResults({ paths, since, cwd })` | `string[]` | finding which declared result paths a suite run actually wrote to since it was spawned (existence alone is not evidence) |
1889
- | `suiteVerdict` | `suiteVerdict({ status, vanished = [], declared = [], fresh = [] })` | `SuiteVerdict` | deciding what a finished suite run is worth given its exit status and what it left behind; can only ever downgrade a verdict, never upgrade one |
1890
- | `runSuite` | `runSuite({ command, args = [], cwd, results = [], env = process.env, log = () => {} })` | `number` — the suite's exit status | running the repository's suite, bracketed by before/after executable checks and a fresh-results check rather than trusting the exit status alone |
1891
- | `suiteCommand` | `suiteCommand(config, mode)` | `string[]` | reading the declared suite command for a mode (`"run"` or `"open"`), or failing clearly when none is declared |
1892
- | `e2eRun` | `async e2eRun({ config, packageJson, mode = "run", suiteArgs = [], env = process.env, log = () => {} })` | `Promise<number>` | running a full, from-scratch end-to-end pass: deploy, reseed, recreate the container, wait, run the suite, tear down (or leave it serving in `"open"` mode) |
1893
- | `e2eFast` | `async e2eFast({ config, argv = [], env = process.env, log = () => {} })` | `Promise<number>` | iterating quickly during development: rebuild what changed, redeploy, cycle the world, wait, re-run |
1894
- | `e2eSweep` | `async e2eSweep({ config, packageJson, argv = [], env = process.env, log = () => {} })` | `Promise<number>` | running the full suite against a build the repository does not pin, to license moving `compatibility.verified` forward |
1959
+ | Export | Signature | Returns | Use it when |
1960
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1961
+ | `E2E_GM_ID` | `const E2E_GM_ID` | — | reading the fixed Gamemaster document id every seeded world carries |
1962
+ | `E2E_SCENE_ID` | `const E2E_SCENE_ID` | — | reading the seeded, pre-activated default scene's id |
1963
+ | `E2E_MODES` | `const E2E_MODES` | — | the closed set of modes the suite may run under (`"run"` / `"open"`) |
1964
+ | `hashPassword` | `hashPassword(password, salt)` | `string` — the hex hash | hashing a password the way Foundry's own `core/auth.mjs` does, to seed a login that actually works |
1965
+ | `resolveE2EWorld` | `resolveE2EWorld(config, env = process.env)` | `E2EWorld` | resolving the seeded world's identity from configuration, with an environment override under the repository's own variable prefix |
1966
+ | `worldManifest` | `worldManifest({ worldId, worldTitle, worldDescription, systemId, systemVersion, coreVersion })` | `object` — the world manifest | building the `world.json` a seeded world carries |
1967
+ | `gmDocument` | `gmDocument({ id, name, password, salt })` | `object` — the user document | building the seeded world's single Gamemaster user document |
1968
+ | `defaultSceneDocument` | `defaultSceneDocument()` | `object` — the scene document | building the one pre-activated scene every seeded world carries |
1969
+ | `moduleConfigurationDocument` | `moduleConfigurationDocument(packageId)` | `object` — the setting document | building the world setting that switches a module package on (a module must be activated; a system does not) |
1970
+ | `isWorldActive` | `isWorldActive(body)` | `boolean` | checking whether a `/join` response body shows a world that is actually active, not merely reachable |
1971
+ | `resolveSweepVersion` | `resolveSweepVersion(argv)` | `string` — the exact build, trimmed | reading the exact Foundry build a sweep was asked to run against; there is deliberately no default |
1972
+ | `parseFastArgs` | `parseFastArgs(argv, build)` | `FastArgs` | parsing the fast loop's CLI arguments against the repository's declared build table, in declaration order |
1973
+ | `seedTestWorld` | `async seedTestWorld({ config, packageJson, env = process.env, log = () => {}, runScript })` | `Promise<{worldDir: string, world: E2EWorld}>` | seeding the disposable world into the end-to-end stage's data root, replacing the world directory through a throwaway container so the Foundry container's user owns it |
1974
+ | `deployForContainer` | `async deployForContainer({ config, env = process.env, log = () => {}, runScript })` | `Promise<void>` | deploying the staged package to the end-to-end stage, removing the old install and handing the new one to the Foundry container's user through a throwaway container |
1975
+ | `waitForWorld` | `async waitForWorld({ url, container, stage, timeoutMs = 180_000, log = () => {} })` | `Promise<void>` | polling until a seeded world is active, or failing with a diagnosis (a licence failure is detected from the container log, not a timeout) |
1976
+ | `CHROME_BUILD_ID` | `const CHROME_BUILD_ID` | `string` | reading the exact Chrome for Testing build used by default for Cypress suites |
1977
+ | `browserCacheDir` | `browserCacheDir(env = process.env)` | `string` | locating the persistent browser cache: `HL_BROWSER_CACHE`, a sibling of `FOUNDRYVTT_CACHE`, under `HL_E2E_ROOT`, or the user's cache directory |
1978
+ | `prepareSuiteBrowser` | `async prepareSuiteBrowser({ command, args = [], env = process.env, platform = process.platform, arch = process.arch, log = () => {} })` | `Promise<string[]>` | appending the pinned Chrome executable to Cypress arguments, installing it when absent; explicit browser arguments and other suites pass through unchanged |
1979
+ | `suiteExecutables` | `suiteExecutables(command)` | `string[]` | reading every executable a suite command needs to exist before it can run at all |
1980
+ | `findExecutable` | `findExecutable(name, { cwd, env = process.env } = {})` | `string\|null` | finding an executable the way the child process would (`node_modules/.bin` first, then `PATH`) |
1981
+ | `missingExecutables` | `missingExecutables({ command, cwd, env = process.env })` | `string[]` | checking which of a suite command's executables are not there — asked again mid-run, since an install can pull the runner out from under a run in progress |
1982
+ | `freshResults` | `freshResults({ paths, since, cwd })` | `string[]` | finding which declared result paths a suite run actually wrote to since it was spawned (existence alone is not evidence) |
1983
+ | `suiteVerdict` | `suiteVerdict({ status, vanished = [], declared = [], fresh = [] })` | `SuiteVerdict` | deciding what a finished suite run is worth given its exit status and what it left behind; can only ever downgrade a verdict, never upgrade one |
1984
+ | `runSuite` | `runSuite({ command, args = [], cwd, results = [], env = process.env, log = () => {} })` | `number` — the suite's exit status | running the repository's suite, bracketed by before/after executable checks and a fresh-results check rather than trusting the exit status alone |
1985
+ | `suiteCommand` | `suiteCommand(config, mode)` | `string[]` | reading the declared suite command for a mode (`"run"` or `"open"`), or failing clearly when none is declared |
1986
+ | `e2eRun` | `async e2eRun({ config, packageJson, mode = "run", suiteArgs = [], env = process.env, log = () => {} })` | `Promise<number>` | running a full, from-scratch end-to-end pass: deploy, reseed, recreate the container, wait, run the suite, tear down (or leave it serving in `"open"` mode) |
1987
+ | `e2eFast` | `async e2eFast({ config, argv = [], env = process.env, log = () => {} })` | `Promise<number>` | iterating quickly during development: rebuild what changed, redeploy, cycle the world, wait, re-run |
1988
+ | `e2eSweep` | `async e2eSweep({ config, packageJson, argv = [], env = process.env, log = () => {} })` | `Promise<number>` | running the full suite against a build the repository does not pin, to license moving `compatibility.verified` forward |
1895
1989
 
1896
1990
  ## `./lang`
1897
1991
 
@@ -2027,6 +2121,256 @@ The engine wildcard subpath also exposes the body parsers below. Their figure ex
2027
2121
 
2028
2122
  `scanFigures` retains `line` (1-based caption line), `bodyStart`/`bodyEnd` (0-based, exclusive-end target range), and `close` (last consumed target line), plus caption attributes, kind, label and number. Unnumbered captions have an empty label and consume no number. `collectAnchors` discovers heading, caption, div, poem and span identifiers; `markupAnchorFindings(body)` reports collisions involving non-page identifiers; Foundry maps non-page anchors to the actual containing page UUID. An unnumbered `ref` requires `form="title"` or `form="full"`.
2029
2123
 
2124
+ `engine/markup-validation.mjs` provides shared syntax validation:
2125
+
2126
+ | Export | Purpose |
2127
+ | ---------------- | ----------------------------------------------------------------------------------- |
2128
+ | `markupFindings` | Validate complete Markdown source and return diagnostics for the supplied filename. |
2129
+
2130
+ `markupFindings(source, file)` masks YAML frontmatter without moving source positions and returns block and alert scanner errors as `{file, line, column, severity, message}` diagnostics. Content lint, Markdown checks and formatting preflight share this validation; malformed files are not rewritten by formatting or Markdown fixes.
2131
+
2030
2132
  `engine/content-alerts.mjs` exposes `ALERT_TYPES`, `scanAlerts(source)` and `renderAlerts(source, target)` and `alertMarkdownPlugin(md)`. The scanner returns `{blocks, errors}`; alert records carry `type`, `id`, `classes`, `attributes`, `title`, `body` and inclusive zero-based `start`/`end` source lines. The renderer produces attributed `aside.alert` wrappers while leaving alert body Markdown in the document's scope. Alert validation and anchor discovery run in site, Foundry and book pipelines.
2031
2133
 
2032
2134
  Alert `title` is the displayed heading override; absent, it defaults to the label in `ALERT_TYPES`. It does not change the alert type’s icon or color.
2135
+
2136
+ The `parsed` map accepted by content-index collection is filled by absolute
2137
+ note path with `{ frontmatter, body, raw, bodyLine?, bodyColumn? }`. Frontmatter
2138
+ is the authored value before index derivation. `buildCompileCorpus` returns
2139
+ that map as `parsed` for its document passes. `unclaimedNoteFindings` accepts it
2140
+ alongside `records`. `locateFrontmatterKey` and `assertNoDeclaredPackage` accept
2141
+ `raw` in their options to locate findings without opening the note again.
2142
+
2143
+ `prepareSqlTables` accepts sources with an optional `authoredSqlBlocks` count,
2144
+ set by `joinGeneratedSections`. Subsequent fences are generated event queries
2145
+ whose stub-inclusive results are identical; only authored fences receive a
2146
+ stub-inclusion comparison. Query answers and comparisons are cached for one
2147
+ preparation and kept separate.
2148
+
2149
+ `prepareTreeSqlTables` retains the complete record membership when opening a
2150
+ public database, so derived citations can distinguish hidden notes from
2151
+ unknown addresses. Only public records contribute rendered sources and links.
2152
+
2153
+ | Export | Contract |
2154
+ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
2155
+ | `prepareLinkSqlTables` | Prepare SQL, page-list and automatic lexical sections for link checking, using one preparation per required source audience. |
2156
+
2157
+ `engine/sql-tables.mjs` exports `prepareLinkSqlTables(contentBase, options)`.
2158
+ Its options are `config`, `skipDirectories`, complete `records`, the shared
2159
+ `parsed` map and `foreignIndex`. It returns prepared tables with `pageLists`
2160
+ and `inline` maps, or `undefined` when no source needs a directive or automatic
2161
+ lexical section. Public
2162
+ sources use public rows; GM sources use all rows. Query results remain local
2163
+ to each audience preparation, and the caller’s records remain complete for
2164
+ checking authored references. Unresolved derived citations retain the
2165
+ renderer’s plain-label behavior.
2166
+
2167
+ `generatedSectionsFor(db, records, contentBase, options?)` accepts complete
2168
+ lineage records as `options.all` (default `records`). Its result includes
2169
+ `lexical`, a map from note file to `LexicalSection[]`, beside `sections`,
2170
+ `views` and `derived`. `attachGeneratedSections(prepared, generated)` places
2171
+ those lists on each note's prepared results as `lexical`; body expansion
2172
+ inserts them alongside the prepared directives.
2173
+
2174
+ ## Dictionary and glossary rows
2175
+
2176
+ The wildcard engine subpath exposes
2177
+ `@heroiclands/package-build/engine/dictionary-rows` for deriving the
2178
+ `dictionary` and `terms` SQL relations from content-index records:
2179
+
2180
+ | Export | Signature | Returns | Use it when |
2181
+ | ----------------------- | ----------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------- |
2182
+ | `DICTIONARY_COLUMNS` | `const DICTIONARY_COLUMNS` | A readonly list of column names | Declaring the schema of a language's dictionary. |
2183
+ | `TERM_COLUMNS` | `const TERM_COLUMNS` | A readonly list of column names | Declaring the schema of owner-specific glossary terms. |
2184
+ | `ETYMOLOGY_KEYS` | `const ETYMOLOGY_KEYS` | A readonly list of keys | Reading the structure of a dictionary word's etymology. |
2185
+ | `dictionaryLetter` | `dictionaryLetter(text)` | `string` | Deriving a heading letter from a sort key. |
2186
+ | `dictionaryEntryAnchor` | `dictionaryEntryAnchor(language, name)` | `string` | Deriving the stable entry anchor for a word in a language's dictionary. |
2187
+ | `dictionaryAndTermRows` | `dictionaryAndTermRows(records, all = records)` | `{ dictionary: object[], terms: object[] }` | Deriving dictionary words and glossary terms for the selected records. |
2188
+ | `serializeRows` | `serializeRows(rows)` | `string` | Writing deterministic JSON Lines with keys sorted at every depth. |
2189
+
2190
+ `dictionaryEntryAnchor` takes the language's canonical Address and the
2191
+ word's form. Its `word-` anchor is derived from the full SHA-256 digest of
2192
+ that language and the NFC-normalized form, with case and accents preserved.
2193
+ It identifies the word's entry on that language page, including a word
2194
+ inherited from an ancestor.
2195
+
2196
+ `records` is the selected audience; `all` supplies the complete record set for
2197
+ language and ownership resolution. A dictionary word takes its sort key and
2198
+ pronunciation from the language's word declaration. A glossary entry can
2199
+ supply its own sort key and pronunciation, using the dictionary word's
2200
+ values where unstated.
2201
+
2202
+ A term naming a dictionary word carries its `class` and a `word` Address
2203
+ linking to its dictionary entry. The target is in the resolved language's
2204
+ dictionary when that language has own words; otherwise it is in the visible
2205
+ language that defines the word. The term's `language` stays resolved to the
2206
+ owner or entry's language. Without an entry's `definition`, its `senses`
2207
+ contain every dictionary gloss sense with
2208
+ an empty `bodies` list. With a definition, that owner's meaning is the sense
2209
+ instead. A rank or office of the same owner contributes its distinct
2210
+ descriptions after the dictionary senses; the owner's definition replaces
2211
+ both. Ranks, offices and terms outside the dictionary carry their source
2212
+ bodies. `common` belongs to
2213
+ glossary entries and `terms`, with each owner's choice kept on that owner's
2214
+ row. The [format
2215
+ reference](reference/format-details.md#dictionaries-and-glossaries) defines
2216
+ all columns and the rules for culture reach and ranks. The `cultures` list
2217
+ records associations for custom queries. Automatic Glossary sections select
2218
+ only their owner's explicitly declared `data.glossary` entries. A custom
2219
+ view can also select reached rank and office rows, keeping common marks only
2220
+ on its owner's own rows.
2221
+
2222
+ Dictionary rows carry `plural`, the noun's nonempty explicit plural or its
2223
+ listed language's first matching literal suffix replacement, and `verbs`,
2224
+ that language's stated `{invariant: true}` or six-person pattern, otherwise
2225
+ null. These optional facts use no ancestor grammar and perform no word-rule
2226
+ validation. The raw `words` relation's `plural` remains the authored value.
2227
+ Dictionary SQL types are projected only for that relation, retaining its
2228
+ etymology struct, sense list, usage list and integer depth even when all
2229
+ optional grammar values are null.
2230
+
2231
+ `@heroiclands/package-build/engine/language-terms` exposes the person
2232
+ descriptors used by the schema, SQL pattern type and head table:
2233
+
2234
+ | Export | Signature | Returns | Use it when |
2235
+ | -------------------- | -------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------- |
2236
+ | `VERB_PERSON_FIELDS` | `const VERB_PERSON_FIELDS` | Readonly inner-key descriptors | Reading the six present person names, labels and ending-string shapes in presentation order. |
2237
+
2238
+ `serializeRows` returns the empty string for no rows; otherwise each object
2239
+ occupies one line and the file ends with a newline.
2240
+
2241
+ ## Dictionary and glossary layouts
2242
+
2243
+ `@heroiclands/package-build/engine/dictionary-layout` renders SQL results as
2244
+ Markdown for the dictionary or glossary on its owner's note:
2245
+
2246
+ | Export | Signature | Returns | Use it when |
2247
+ | ------------------------- | ------------------------------------ | -------------------------- | ----------------------------------------------------------------------------------- |
2248
+ | `LAYOUT_REQUIRED_COLUMNS` | `const LAYOUT_REQUIRED_COLUMNS` | Required columns by layout | Checking the columns a dictionary or glossary query must select. |
2249
+ | `COMMON_MARK` | `const COMMON_MARK` | `string` | Reading the mark a common glossary entry opens with. |
2250
+ | `DICTIONARY_ENTRY_CLASS` | `const DICTIONARY_ENTRY_CLASS` | `string` | Identifying generated headword spans that are native dictionary targets in Foundry. |
2251
+ | `dictionaryEntryPlugin` | `dictionaryEntryPlugin(md)` | `void` | Registering native dictionary headword rendering on the Foundry Markdown parser. |
2252
+ | `renderDictionary` | `renderDictionary(result, options?)` | `string` | Rendering a language's words with classes, etymology, senses and citations. |
2253
+ | `renderGlossary` | `renderGlossary(result, options?)` | `string` | Rendering an owner's glossary entries, ranks and offices. |
2254
+
2255
+ `result` is `{ columns: string[], rows: object[] }`. Both layouts require
2256
+ `name`, `senses` and `usage`; a missing column throws an error naming it.
2257
+ `LayoutOptions` accepts `linkable(ref)`, `sectionLevel`, `addressContext`,
2258
+ `labelOf(address)`, `showKey` (`true`, `false` or `"only"`), `pageLanguage`, `pageOwner`,
2259
+ `verbs` and `entryAnchors` (default `true`). These control link availability, letter-heading level,
2260
+ Address resolution, labels, the abbreviation key and the language whose tag
2261
+ is omitted. `entryAnchors` controls canonical dictionary headword targets;
2262
+ custom SQL dictionary views set it to `false`, leaving those targets to the
2263
+ language's automatic Dictionary section. A dictionary marks nothing common;
2264
+ a glossary prints the mark and its explanation when a selected term carries
2265
+ `common: true`.
2266
+
2267
+ A dictionary entry has a stable word anchor. A glossary headword with a
2268
+ `word` Address links to that entry, and its `class` prints beside the language
2269
+ tag. Glossary rows sharing a headword and language combine into one entry;
2270
+ identical senses print once with all their source bodies. Dictionary-derived
2271
+ senses carry no body label. Dictionary entries print their `field` in italic
2272
+ parentheses and their etymology in brackets.
2273
+
2274
+ Nouns print their optional `plural` beside the class. Each represented
2275
+ language's optional `verbs` column supplies one table after the key and
2276
+ before the first letter, with empty endings shown as "unchanged". For an
2277
+ automatic dictionary, `options.verbs` supplies the owning language's pattern
2278
+ even when its own words contain only excluded classes. The existing
2279
+ nonempty-own-words gate still applies. `showKey: false` suppresses the key
2280
+ and verb table while retaining entries; `showKey: "only"` prints the
2281
+ abbreviation key alone. With the default or `showKey: true`, a stated pattern
2282
+ still prints when the result needs no abbreviation key. Each head-enabled
2283
+ result prints its represented languages' tables independently.
2284
+
2285
+ `pageOwner` is the containing page's canonical note Address. A glossary omits
2286
+ a sense's body label only when every contributing row belongs to that page
2287
+ and attributes the sense solely to itself. Other bodies and shared senses
2288
+ keep their labels. Without `pageOwner` or a selected `owner` column, all body
2289
+ labels remain.
2290
+
2291
+ `DICTIONARY_ENTRY_CLASS` is `"dictionary-entry"`. The Foundry parser's
2292
+ `dictionaryEntryPlugin` promotes the marked headword at the start of a
2293
+ dictionary paragraph to an inline native H6 heading in a div container. It
2294
+ keeps the containing journal page. Site and book rendering retain the
2295
+ headword's span and paragraph. `anchorsOf` maps this dictionary target to
2296
+ its containing page's UUID with the word fragment; generic authored spans
2297
+ keep their containing-page behavior.
2298
+
2299
+ ## Automatic dictionary and glossary sections
2300
+
2301
+ `@heroiclands/package-build/engine/lexical-sections` derives and inserts the
2302
+ same automatic lists for the site, Foundry journal, book and published index:
2303
+
2304
+ | Export | Signature | Returns | Use it when |
2305
+ | ----------------------- | ------------------------------------------------------ | ------------------------------------- | ----------------------------------------------------------------- |
2306
+ | `LEXICAL_SECTIONS` | `const LEXICAL_SECTIONS` | A frozen list of section declarations | Reading section headings, accepted anchors and frontmatter gates. |
2307
+ | `hasLexicalEntries` | `hasLexicalEntries(frontmatter)` | `boolean` | Checking whether a note supplies automatic lexical content. |
2308
+ | `lexicalSectionsFor` | `lexicalSectionsFor(records, all = records)` | `Map<string, LexicalSection[]>` | Deriving lists keyed by their owner's canonical Address. |
2309
+ | `insertLexicalSections` | `insertLexicalSections(markdown, sections?, context?)` | `{markdown, lineMap, generatedFrom?}` | Inserting lists while preserving authored text and source lines. |
2310
+
2311
+ Each registry declaration has `slug`, `heading`, `aliases`, `dataKey` and
2312
+ `types`, whose entries have `type` and an optional `subType`. The Dictionary
2313
+ declaration applies to `skill`/`language` notes with nonempty `data.words`,
2314
+ and accepts `dictionary` and `lexicon` heading anchors. The Glossary
2315
+ declaration applies to cultures (`lore`/`culture`) and affiliations with
2316
+ nonempty `data.glossary`, and accepts `glossary`. `hasLexicalEntries` reads
2317
+ these same type and nonempty-array gates; it does not validate the authored
2318
+ body.
2319
+
2320
+ `records` is the visible audience; `all` supplies complete lineage records.
2321
+ Each `LexicalSection` has `slug`, `heading`, `aliases`, a query-shaped
2322
+ `result: {columns, rows}` and dictionary-layout `options`. Dictionary rows
2323
+ cover the language's full dictionary; glossary rows select its owner's
2324
+ explicit entries only, including a rank or office only when named there.
2325
+ A qualifying words declaration containing only excluded word classes still
2326
+ produces its Dictionary heading, without entries or a key.
2327
+
2328
+ `insertLexicalSections` takes the body after authored directives expand.
2329
+ Pass the original body as `context.authored` together with the expansion's
2330
+ line map, so generated headings cannot select or end an authored section.
2331
+ Without `authored`, the helper treats `markdown` as its source. It inserts a
2332
+ list at the end of the first matching authored anchored section, after its
2333
+ text and before the next authored heading at the same or a higher level.
2334
+ Headings inside comments, raw HTML, literal code or named blocks do not select
2335
+ sections.
2336
+ Without that heading, it appends an H1 with the section's canonical anchor.
2337
+ Letter headings sit one level below the containing heading, up to H6. The
2338
+ optional context is `{authored?, lineMap?, generatedFrom?}`; the returned line map keeps
2339
+ authored positions, marks inserted lines as generated and adjusts the start
2340
+ of appended nonlexical sections. It leaves the supplied line-map array
2341
+ unchanged.
2342
+
2343
+ ## Glossary term validation
2344
+
2345
+ The wildcard module `@heroiclands/package-build/engine/culture-reach`
2346
+ exposes the term lookup used for glossary validation;
2347
+ `@heroiclands/package-build/engine/glossary-entries` exposes its findings:
2348
+
2349
+ | Export | Signature | Returns | Use it when |
2350
+ | --------------- | ------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------- |
2351
+ | `noteTerms` | `noteTerms(index, note, entry?)` | `Map<string, string[]>` | Reading eligible dictionary words and reached rank or office descriptions by NFC name. |
2352
+ | `checkGlossary` | `checkGlossary(note, { index } = {})` | `object[]` | Checking the meanings and language references of a culture's or affiliation's glossary entries. |
2353
+
2354
+ `noteTerms` selects words from the entry's resolved language and its
2355
+ ancestors; without `entry`, it uses the note's own resolved language. It also
2356
+ includes ranks and offices that reach the note. `checkGlossary` returns
2357
+ located findings for entries naming no language, duplicate names, invalid
2358
+ pronunciations and entries without a definition that match neither a word in
2359
+ their resolved language nor a reached rank or office. A definition repeating
2360
+ the only sense it replaces produces a warning. With no `index`, the check
2361
+ validates only what the note states locally.
2362
+
2363
+ ## Built-piece resolution
2364
+
2365
+ The wildcard engine subpath exposes `pieceGlosses` from
2366
+ `@heroiclands/package-build/engine/word-rows`.
2367
+
2368
+ | Export | Signature | Returns | Use it when |
2369
+ | -------------- | ------------------------ | ----------------------------------- | ---------------------------------------------------------------------------- |
2370
+ | `pieceGlosses` | `pieceGlosses(...datas)` | A lookup returning a string or null | Resolving a word's built pieces for lint, word rows or dictionary etymology. |
2371
+
2372
+ Pass language `data` objects nearest first, in `lineageChain` order. Each
2373
+ language contributes words, affixes and elements, in that order. Words and
2374
+ affixes supply `gloss`, including an affix matched by its `front` form. Elements
2375
+ supply `sense` and match their `form` and `variants`. Exact matches precede matches without joining hyphens; case and
2376
+ accents are preserved. A missing gloss returns `null`.