@heroiclands/package-build 23.8.0 → 23.10.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 (114) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +2 -0
  3. package/bin/content-commands.mjs +207 -70
  4. package/docs/api.md +646 -78
  5. package/docs/authoring/first-note.md +1 -1
  6. package/docs/authoring/links-and-markup.md +253 -102
  7. package/docs/commands.md +111 -13
  8. package/docs/content-format.md +7 -2
  9. package/docs/diagnostics.md +1 -1
  10. package/docs/guides/packs.md +2 -1
  11. package/docs/guides/reference-pages.md +86 -63
  12. package/docs/reference/format-details.md +398 -137
  13. package/docs/reference/note-types.md +188 -174
  14. package/engine/anchors.mjs +15 -26
  15. package/engine/base-compiler.mjs +6 -4
  16. package/engine/changelog-group.mjs +58 -34
  17. package/engine/changelog-lint.mjs +6 -2
  18. package/engine/compendiums.mjs +15 -1
  19. package/engine/compile-corpus.mjs +17 -3
  20. package/engine/content-charset.mjs +13 -1
  21. package/engine/content-format.yaml +1 -0
  22. package/engine/content-html.mjs +18 -4
  23. package/engine/content-icons.mjs +15 -1
  24. package/engine/content-images.mjs +28 -9
  25. package/engine/content-index.mjs +496 -16
  26. package/engine/content-links.mjs +70 -27
  27. package/engine/content-session.mjs +247 -0
  28. package/engine/content-sources.mjs +110 -0
  29. package/engine/content-spans.mjs +87 -8
  30. package/engine/content-tables.mjs +9 -4
  31. package/engine/content-tasklists.mjs +20 -4
  32. package/engine/culture-reach.mjs +61 -34
  33. package/engine/data-keys.mjs +3 -1
  34. package/engine/diagnostics.mjs +17 -6
  35. package/engine/dictionary-layout.mjs +151 -19
  36. package/engine/dictionary-rows.mjs +129 -79
  37. package/engine/extension-attributes.mjs +28 -5
  38. package/engine/first-use.mjs +371 -0
  39. package/engine/foundry-entries.mjs +29 -8
  40. package/engine/generate.mjs +13 -4
  41. package/engine/glossary-entries.mjs +11 -11
  42. package/engine/heading-attributes.mjs +21 -28
  43. package/engine/helpers.mjs +54 -19
  44. package/engine/index-records.mjs +1 -0
  45. package/engine/index.mjs +3 -0
  46. package/engine/journals.mjs +13 -6
  47. package/engine/language-data.mjs +55 -2
  48. package/engine/language-rows.mjs +4 -4
  49. package/engine/language-terms.mjs +90 -4
  50. package/engine/language-words.mjs +2 -2
  51. package/engine/lexical-sections.mjs +247 -0
  52. package/engine/markup-validation.mjs +7 -2
  53. package/engine/metadata-index.mjs +1 -0
  54. package/engine/name-markup.mjs +114 -0
  55. package/engine/name-rows.mjs +70 -0
  56. package/engine/names.mjs +460 -0
  57. package/engine/note-addresses.mjs +25 -4
  58. package/engine/note-claims.mjs +2 -0
  59. package/engine/note-state.mjs +9 -3
  60. package/engine/note-vocabulary.mjs +7 -6
  61. package/engine/path-charset.mjs +24 -1
  62. package/engine/pdf-build.mjs +9 -9
  63. package/engine/pdf-render.mjs +15 -1
  64. package/engine/site-build.mjs +16 -2
  65. package/engine/site-index.mjs +5 -1
  66. package/engine/sql-tables.mjs +324 -44
  67. package/engine/stub-lint.mjs +10 -5
  68. package/engine/web-wikilinks.mjs +8 -6
  69. package/engine/word-rows.mjs +20 -12
  70. package/engine/word-terms.mjs +15 -6
  71. package/engine/yaml-lint.mjs +3 -5
  72. package/hugo-theme/static/css/style.css +4 -0
  73. package/package.json +1 -1
  74. package/types/engine/anchors.d.mts +4 -2
  75. package/types/engine/compendiums.d.mts +5 -1
  76. package/types/engine/compile-corpus.d.mts +5 -2
  77. package/types/engine/content-charset.d.mts +5 -1
  78. package/types/engine/content-html.d.mts +5 -1
  79. package/types/engine/content-icons.d.mts +5 -1
  80. package/types/engine/content-images.d.mts +5 -1
  81. package/types/engine/content-index.d.mts +102 -18
  82. package/types/engine/content-links.d.mts +22 -8
  83. package/types/engine/content-session.d.mts +108 -0
  84. package/types/engine/content-sources.d.mts +32 -0
  85. package/types/engine/content-spans.d.mts +3 -5
  86. package/types/engine/content-tables.d.mts +2 -1
  87. package/types/engine/content-tasklists.d.mts +5 -1
  88. package/types/engine/culture-reach.d.mts +22 -2
  89. package/types/engine/data-keys.d.mts +7 -0
  90. package/types/engine/diagnostics.d.mts +10 -0
  91. package/types/engine/dictionary-layout.d.mts +35 -4
  92. package/types/engine/dictionary-rows.d.mts +9 -0
  93. package/types/engine/extension-attributes.d.mts +16 -2
  94. package/types/engine/first-use.d.mts +31 -0
  95. package/types/engine/foundry-entries.d.mts +14 -5
  96. package/types/engine/generate.d.mts +5 -1
  97. package/types/engine/heading-attributes.d.mts +2 -2
  98. package/types/engine/helpers.d.mts +33 -7
  99. package/types/engine/index.d.mts +1 -0
  100. package/types/engine/journals.d.mts +5 -2
  101. package/types/engine/language-terms.d.mts +7 -0
  102. package/types/engine/lexical-sections.d.mts +96 -0
  103. package/types/engine/markup-validation.d.mts +2 -1
  104. package/types/engine/name-markup.d.mts +176 -0
  105. package/types/engine/name-rows.d.mts +35 -0
  106. package/types/engine/names.d.mts +356 -0
  107. package/types/engine/note-addresses.d.mts +3 -3
  108. package/types/engine/note-state.d.mts +2 -1
  109. package/types/engine/path-charset.d.mts +16 -1
  110. package/types/engine/site-build.d.mts +6 -3
  111. package/types/engine/site-index.d.mts +3 -1
  112. package/types/engine/sql-tables.d.mts +39 -4
  113. package/types/engine/stub-lint.d.mts +3 -1
  114. package/types/engine/word-rows.d.mts +9 -7
package/docs/api.md CHANGED
@@ -426,7 +426,14 @@ Drawing a declared infobox: HTML for separate Foundry pages appended after autho
426
426
 
427
427
  ### `engine.noteState`
428
428
 
429
- 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.
430
437
 
431
438
  | Export | Signature | Returns | Use it when |
432
439
  | --------------------- | ------------------------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -458,12 +465,12 @@ What a draft note says about itself, in one sentence, on every surface it reache
458
465
 
459
466
  ### `engine.stubLint`
460
467
 
461
- 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.
462
469
 
463
- | Export | Signature | Returns | Use it when |
464
- | ------------------ | ---------------------------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------- |
465
- | `SHORT_BODY_WORDS` | `const SHORT_BODY_WORDS` | `number` | reading the word count below which an unmarked body is asked whether it is a draft |
466
- | `lintNoteStates` | `lintNoteStates(contentBase, { records, contentPackage })` | `{findings, counts, byType, oldestDrafts, summary}` | checking a tree's states and counting the ladder, from the corpus the command already derived |
470
+ | Export | Signature | Returns | Use it when |
471
+ | ------------------ | ------------------------------------------------------------------ | --------------------------------------------------- | --------------------------------------------------------------------------------------------- |
472
+ | `SHORT_BODY_WORDS` | `const SHORT_BODY_WORDS` | `number` | reading the word count below which an unmarked body is asked whether it is a draft |
473
+ | `lintNoteStates` | `lintNoteStates(contentBase, { records, contentPackage, parsed })` | `{findings, counts, byType, oldestDrafts, summary}` | checking a tree's states and counting the ladder, from the corpus the command already derived |
467
474
 
468
475
  ### `engine.placeRelations`
469
476
 
@@ -861,45 +868,308 @@ The published content index — the artifact packages exchange addresses through
861
868
 
862
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.
863
870
 
864
- | Export | Signature | Returns | Use it when |
865
- | ----------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
866
- | `LEAD_ANCHOR` | `const LEAD_ANCHOR` | — | The reserved anchor name for a journal's **first** page. |
867
- | `anchorsOf` | `function anchorsOf(entryUuid, entryId, body, name)` | {Record<string, string>} The anchors. | Every page of a note's journal, as `anchorName → whole UUID`. |
868
- | `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. |
869
- | `collectFoundryEntries` | `function collectFoundryEntries(contentBase, ctx)` | `{{entries: Array<object>, notes: number, skipped: Array<{file: string, reason: string}>}}` | Every note this package publishes, as manifest entries. |
870
- | `foundryIdentities` | `function foundryIdentities(config` | `{{contentPackage: string, foundryPackageId: string, packRouter: object, web: boolean, skipDirectories: readonly string[]}}` | The identities an emission runs against, from configuration. |
871
- | `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` | `anchorsOf(entryUuid, entryId, body, name, syntax?)` | {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.
883
+
884
+ ### `engine.contentSession`
885
+
886
+ One invocation's content capture and lazy prepared views. Import the namespace
887
+ from `@heroiclands/package-build/engine`, or import the functions directly from
888
+ `@heroiclands/package-build/engine/content-session`.
889
+
890
+ | Export | Signature | Returns | Use it when |
891
+ | ---------------------- | ---------------------------------------- | -------------------------- | ---------------------------------------------- |
892
+ | `CONTENT_TARGETS` | `readonly [index, lint, links, compile]` | the closed execution order | Selecting operations in a shared invocation. |
893
+ | `createContentSession` | `createContentSession(options?)` | `Promise<ContentSession>` | Sharing source work across content operations. |
894
+
895
+ Options are the resolved `config`, `contentBase`, `assetsBase`,
896
+ `skipDirectories` and compile `router`. Paths and scope default from that
897
+ configuration. The session binds its exact configuration object, canonical
898
+ roots, directory scope and router. `assertContext(options?)` refuses reuse
899
+ with a different bound value. A new invocation, an input edit, a changed
900
+ dependency cache or a changed configuration needs a fresh session; no state
901
+ is persisted or shared across sessions.
902
+
903
+ The session exposes `config`, `contentBase`, `assetsBase`, `skipDirectories`,
904
+ `router`, physical `inventory`, raw `sources`, derived `records`, authored
905
+ `parsed` notes, preparation `problems` and captured dependency `foreign`
906
+ views. Treat these inputs as read-only. Parsed notes retain exact `raw`
907
+ strings, trimmed `body`, authored `frontmatter` and file coordinates; malformed
908
+ or untyped physical files remain available to raw checks. Derivation and
909
+ publication use their own record projections, including per-system clones.
910
+
911
+ | Method | Result and scope |
912
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------- |
913
+ | `syntax(text)` | Memoized block, alert and span scanner results for that exact string; a different projection has its own positions. |
914
+ | `sqlTables(audience?)` | Prepared SQL and generated sections for `all` (default) or `public`, once per audience in this context. |
915
+ | `linkTables()` | Source-audience results selected for prose-link checking. |
916
+ | `audit(kind?)` | The diagnostic link index for `links` (default) or `lint`; this stays separate from routed native resolution. |
917
+ | `publication()` | The complete `PreparedContentIndex`, including source-audience and native bodies. |
918
+ | `compileCorpus()` | Records, parsed notes, native SQL, scanner projections and the routed compiler link index. |
919
+ | `prepare(targets)` | Prepare selected closed targets in `CONTENT_TARGETS` order before a caller starts a writer; returns the session. |
920
+ | `assertContext(options?)` | Check configuration, roots, scope and router against the captured context. |
921
+ | `dispose()` | Invalidate derived views and methods at the end of the invocation. |
922
+
923
+ SQL query answers remain local to each audience preparation; their databases
924
+ close inside preparation. The session reuses results without sharing a public
925
+ answer with the complete native corpus. Scanner results are also read-only.
926
+ `prepare` refuses writers when shared source preparation or the existing note
927
+ state preflight fails. Ordinary lint findings add no gate to compilation.
928
+
929
+ ```js
930
+ import { contentSession, contentIndex, compendiums } from "@heroiclands/package-build/engine";
931
+
932
+ const session = await contentSession.createContentSession({ config });
933
+ try {
934
+ await session.prepare(["index", "compile"]);
935
+ contentIndex.emitContentIndex({ config, publication: await session.publication() });
936
+ await compendiums.compilePacks({ config, session });
937
+ } finally {
938
+ session.dispose();
939
+ }
940
+ ```
941
+
942
+ ### `engine/content-sources.mjs`
943
+
944
+ Reachable through `@heroiclands/package-build/engine/content-sources`; the
945
+ session uses these physical input adapters without adding a namespace export.
946
+
947
+ | Export | Signature | Returns | Use it when |
948
+ | ----------------------- | -------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------ |
949
+ | `captureContentSources` | `captureContentSources(contentBase, assetsBase, skipDirectories)` | `{sources, indexSources, paths}` | Capturing exact text and physical path walks before parsing. |
950
+ | `capturedSources` | `capturedSources(contentBase, sources, skipDirectories, extensions)` | generator of `[file, raw]` | Selecting a raw checker's physical scope from captured text. |
951
+
952
+ `sources` maps absolute text-file paths to `{raw}`. `indexSources` retains the
953
+ index walk's selected Markdown order. `paths` retains `{root, file, name,
954
+ directory}` for each physical walk, including its root identity so overlapping
955
+ content and asset scopes preserve their own filtering and order.
956
+
957
+ ### `engine/first-use.mjs`
958
+
959
+ Import from `@heroiclands/package-build/engine/first-use`.
960
+
961
+ | Export | Signature | Returns | Use it when |
962
+ | ----------------------- | ----------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------------ |
963
+ | `createFirstUseChecker` | `createFirstUseChecker({ records, index, catalog?, parsed?, syntax? })` | `checker({ file, raw })` | Checking authored prose against the current corpus's name-marking conventions. |
964
+
965
+ Construct one checker with the session's read-only `records` and
966
+ `await session.audit("lint")`. Call it for each existing link-index note.
967
+ It performs no I/O and returns `{file, line, column, severity: "warning",
968
+ rules, message}` diagnostics. `rules` is a list of applicable rule names, merged
969
+ into one warning for each occurrence; the message names the full name or
970
+ explicit label that needs attention. Only `.name` spans and authored link
971
+ labels enter name tracking; plain text is not searched. Indexed full names and
972
+ declared aliases share an identity, while other labels use their whole text.
973
+ A whole bold indexed name receives an advisory without entering name tracking.
974
+ Local and captured dependency names and aliases share the same corpus
975
+ context. The command's lint target constructs
976
+ the checker once, uses exact authored `raw` strings and keeps these findings
977
+ advisory. See the [authoring conventions](authoring/links-and-markup.md#marking-names-in-prose).
978
+
979
+ Supply the invocation's `catalog` to share its full-name and alias lookup with
980
+ the package names report. Optional `parsed` maps absolute files to captured
981
+ `{raw, body, bodyLine?, bodyColumn?}`; `syntax` is the session's scanner
982
+ projection. When the captured raw text matches, the checker rebases the body's
983
+ existing name inventory into its authored raw source coordinates. Standalone
984
+ calls use their supplied raw text.
985
+
986
+ ### `engine/name-markup.mjs`
987
+
988
+ Import from `@heroiclands/package-build/engine/name-markup`.
989
+
990
+ | Export | Signature | Returns | Use it when |
991
+ | ----------------- | --------------------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------- |
992
+ | `inlineText` | `inlineText(tokens)` | `string` | Reading visible name text from actual parsed inline children and their resolved reference context. |
993
+ | `rebaseNameSpans` | `rebaseNameSpans(spans, { raw, bodyLine?, bodyColumn? })` | Copied name spans at original file coordinates | Rebasing a captured body inventory and attribute ranges without parsing it again. |
994
+
995
+ `scanSpans(source)` returns `{spans, names, errors}`. `names` contains the same
996
+ span objects with the exact `name` class, including spans inside links. Each
997
+ span retains `text`, `attrs`, offsets, 1-based line/UTF-16 column and `visible`
998
+ text from actual parsed children. `contentEnd` is the exclusive raw inner-label
999
+ endpoint. `attributePositions` maps each named attribute to `key` and `value`
1000
+ ranges with `{start, end, line, column}`. Offsets are 0-based with exclusive
1001
+ ends. Rebasing copies these values and leaves the captured inventory unchanged.
1002
+
1003
+ The leaf module exports the declaration types `ContentSpan`, `SpanSourceSite`,
1004
+ `SpanAttributePosition`, `SpanFinding` and `SpanScan`. The generic rebasing
1005
+ helper retains additional fields of the captured span subtype.
1006
+
1007
+ `engine/extension-attributes.mjs` exposes the shared attribute grammar:
1008
+
1009
+ | Export | Signature | Returns | Use it when |
1010
+ | -------------------------- | ------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
1011
+ | `parseExtensionAttributes` | `parseExtensionAttributes(source, { idPattern?, positions? } = {})` | `{id, classes, values, problems, positions?}` | Parsing identifiers, ordered classes and named values; `positions: true` retains attribute key/value offsets. |
1012
+
1013
+ Attribute-parser positions are ranges within its supplied attribute source;
1014
+ the span scanner maps them to Markdown coordinates. The optional range map
1015
+ does not change the parsed values.
1016
+
1017
+ ### `engine/name-rows.mjs`
1018
+
1019
+ Import from `@heroiclands/package-build/engine/name-rows`.
1020
+
1021
+ | Export | Signature | Returns | Use it when |
1022
+ | -------------- | -------------------- | --------------------------- | ---------------------------------------------------------------------- |
1023
+ | `NAME_COLUMNS` | `const NAME_COLUMNS` | Frozen ordered column names | Reading `owner`, `name`, `type`, `subType`, `line`, `column`. |
1024
+ | `namesRows` | `namesRows(records)` | `NameRow[]` | Projecting own-note occurrences into SQL rows without I/O or mutation. |
1025
+
1026
+ Rows preserve record and occurrence order. `owner` is the canonical NOTE page
1027
+ derived from the record's canonical Address, including for native Item
1028
+ sources; an unaddressable record keeps `null`. Assets and documentation mirrors
1029
+ contribute no rows. Each row carries the occurrence's visible `name`, nullable
1030
+ `type`/`subType`, and numeric 1-based `line`/UTF-16 `column`. SQL ingestion fixes
1031
+ text columns as `VARCHAR` and positions as `BIGINT`, including empty,
1032
+ entirely-null and late-supplied classifications.
1033
+
1034
+ ### `engine/names.mjs`
1035
+
1036
+ Import from `@heroiclands/package-build/engine/names`.
1037
+
1038
+ | Export | Signature | Returns | Use it when |
1039
+ | ----------------------- | -------------------------------------------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------- |
1040
+ | `normalizeName` | `normalizeName(text)` | `string` | Normalizing NFC, apostrophes and whitespace while preserving case. |
1041
+ | `nameForms` | `nameForms(text)` | `string[]` | Enumerating supported whole-label possessives and regular plurals. |
1042
+ | `createNameCatalog` | `createNameCatalog({ records, index })` | `NameCatalog` | Sharing indexed full-name, alias and resolved Address matching across checks. |
1043
+ | `collectUnwrittenNames` | `collectUnwrittenNames({ records, catalog })` | `{groups, firstUse}` | Grouping every unresolved captured occurrence by supplied classification. |
1044
+ | `analyzeNameSpans` | `analyzeNameSpans(spans, { file?, vocabulary?, types? } = {})` | `{names, findings}` | Validating supplied `data-type` against actual note declarations at attribute-key positions. |
1045
+ | `createNamesReport` | `createNamesReport({ package, records, catalog })` | `NamesReport` | Preparing deterministic current-package unwritten and conflicting groups without I/O. |
1046
+ | `namesReportFinding` | `namesReportFinding(report, { contentBase? } = {})` | A located warning, or `undefined` | Formatting one human package advisory with every reported use. |
1047
+
1048
+ `NameCatalog` provides `recognize(label, preferred?)` and `resolve(target)`;
1049
+ indexed full names and declared aliases share canonical identity, and ambiguous
1050
+ labels retain every candidate. Dependency names come from the captured link
1051
+ index; folder-note identities are excluded. These pure helpers read the
1052
+ invocation's existing corpus.
1053
+
1054
+ `analyzeNameSpans` accepts actual parsed spans at file coordinates, with
1055
+ `visible`, `attrs`, `line`, `column` and optional `attributePositions`. It
1056
+ returns valid five-field `MarkedName` entries and supplied-value error findings.
1057
+ `vocabulary` defaults to `NOTE_VOCABULARY`; `types` supplies additional bare
1058
+ types from configured declarations. Callers propagate findings through their
1059
+ existing preparation error channel.
1060
+
1061
+ `NamesReport` has `{package, unwritten, conflicts}`. Each unwritten group has
1062
+ `{type, subType, names: [{name, uses}]}`. Each conflict has
1063
+ `{name, kinds: [{type, subType, uses}]}`. A use has
1064
+ `{owner, file, line, column}`, with its file relative to the content root and
1065
+ the same NOTE/null owner convention as `namesRows`. Untyped uses are excluded
1066
+ from conflicts; matching bare-type/subtype refinements are compatible.
1067
+ `namesReportFinding` rebases file paths with `contentBase`, anchors the advisory
1068
+ at the earliest reported actual use, and returns no warning for an empty report.
872
1069
 
873
1070
  ### `engine.contentIndex`
874
1071
 
875
- 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.
876
-
877
- | Export | Signature | Returns | Use it when |
878
- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
879
- | `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. |
880
- | `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. |
881
- | `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. |
882
- | `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. |
883
- | `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
884
- | `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. |
885
- | `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. |
886
- | `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. |
887
- | `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 |
888
- | `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` |
889
- | `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 |
1072
+ 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.
1073
+
1074
+ Own-note records carry derived `names` entries in source order as
1075
+ `{name, type, subType, line, column}`. Missing classifications are explicit
1076
+ `null`; no occurrences gives `[]`. Positions refer to original authored span
1077
+ openings, before generated content. Metadata JSONL transports these entries
1078
+ to dependency SQL without an additional artifact. This field is reserved for
1079
+ derived data, and is not an authored frontmatter key.
1080
+
1081
+ `buildIndexRecord` and `indexRecordsForNote` accept `bodyColumn` alongside
1082
+ `bodyLine` to locate the authored body's first UTF-16 character. Their optional
1083
+ `nameTypes` iterable, also accepted by `collectContentIndex` and
1084
+ `indexRecordsFor`, supplies additional bare kinds from configured type
1085
+ declarations. `indexRecordsFor` defaults that iterable to the resolved
1086
+ configuration's `itemTypes`.
1087
+
1088
+ | Export | Signature | Returns | Use it when |
1089
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1090
+ | `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. |
1091
+ | `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. |
1092
+ | `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. |
1093
+ | `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. |
1094
+ | `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
1095
+ | `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems, parsed, sources, syntax, foreignIndex, nameTypes? })` | {object[]} One record per note, plus one per documentation entry. | The index records for a content tree, without writing anything. |
1096
+ | `indexRecordsForNote` | `indexRecordsForNote({ frontmatter, body, bodyLine, bodyColumn?, nameTypes?, relPath, absPath, raw, contentPackage, manifest, addressContext })` | `object[]` | Derive the note's own record and its documentation record from parsed text. |
1097
+ | `prepareContentIndex` | `async prepareContentIndex({ contentBase, config, session } = {})` | `Promise<PreparedContentIndex>` — the captured source context and complete expanded bodies, described below | Prepare dynamic publication without writing files. |
1098
+ | `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`. |
1099
+ | `buildIndexRecord` | `buildIndexRecord({ frontmatter, relPath, absPath, raw, contentPackage, body, bodyLine, bodyColumn?, nameTypes?, 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 |
1100
+ | `collectContentIndex` | `collectContentIndex(contentBase, { contentPackage, skipDirectories, assetsBase, manifest, problems, foreignRecord, parsed, sources, syntax, nameTypes? })` | `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` |
1101
+ | `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 |
1102
+
1103
+ `prepareContentIndex` resolves `contentBase` from the configured `paths.content`
1104
+ when omitted. Its `PreparedContentIndex` contains:
1105
+
1106
+ | Field | Value |
1107
+ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
1108
+ | `config` | The resolved configuration object used for preparation. |
1109
+ | `contentBase` | The absolute source root. |
1110
+ | `records` | The complete note, documentation and asset records, including hidden providers needed for lineage. |
1111
+ | `parsed` | A `Map` keyed by absolute note file, holding authored `frontmatter`, `body`, `raw`, `bodyLine` and `bodyColumn`. |
1112
+ | `expandedBodies` | A `Map` keyed by absolute note file, holding `markdown`, its body-relative `lineMap` of `{line, generated}` entries, and optional `generatedFrom`. |
1113
+ | `foundryBodies` | A `Map` with the same body shape, prepared against the complete native compendium corpus. |
1114
+ | `syntax` | Optional session-local scanner projection function; read-only and valid for that session's lifetime. |
1115
+
1116
+ Preparation expands SQL tables, page lists, automatic sections and scalar
1117
+ expressions. Prose declarations in `expandedBodies` use each source's audience:
1118
+ public notes use public providers and usage; GM notes use the complete corpus.
1119
+ Automatic lexical sections keep that source audience on every surface.
1120
+ Explicit native SQL uses the complete corpus in `foundryBodies`.
1121
+
1122
+ Native own and documentation target maps derive their actual page identities
1123
+ from `foundryBodies`. Each map contains only prose slugs declared in the
1124
+ source-audience body and present in the native body, plus the journal's
1125
+ reserved `$lead` target. A prose target present only on the website remains
1126
+ valid for website links and prose-link checks; a native link to it is refused.
1127
+ Native-only slugs and private heading names stay out of a public source's
1128
+ metadata. Event anchors and authored source positions remain attached to their
1129
+ sources. A published empty prose-anchor list declares no prose targets,
1130
+ including on web-only pages.
1131
+
1132
+ Pass the envelope directly to the synchronous emitter:
1133
+
1134
+ ```js
1135
+ const publication = await prepareContentIndex({ config, contentBase });
1136
+ const result = emitContentIndex({ publication, outDir });
1137
+ ```
1138
+
1139
+ The emitter defaults `config` and `contentBase` from the envelope. If supplied,
1140
+ they must name that same configuration object and source root. It consumes
1141
+ `records`, `parsed`, `expandedBodies` and `foundryBodies` without mutating them;
1142
+ frozen inputs are valid. Every note requires a parsed source and both body
1143
+ entries.
1144
+ Missing or mismatched preparation is refused before creating or replacing any
1145
+ metadata, dictionary or terms file.
1146
+
1147
+ Without `publication`, the synchronous emitter accepts authored bodies and
1148
+ pure frontmatter dictionary or glossary sections. Active SQL, page lists,
1149
+ scalar expressions, events or other generated sections require
1150
+ `prepareContentIndex`. The CLI performs this preparation automatically.
1151
+ `indexRecordsFor` and `collectContentIndex` collect authored records and pure
1152
+ lexical sections; use preparation and emission to publish dynamic body targets.
1153
+
1154
+ On lore and affiliation notes, `data.language` and `data.glossary[].language`
1155
+ require a language NOTE Address. Decoding refuses other recognized system
1156
+ segments at the authored value, naming the complete
1157
+ `<pkg>-note-skill-<shortcode>` to write. A short local form such as
1158
+ `skill-provnzlng` is also accepted. See the [glossary language
1159
+ rules](reference/format-details.md#dictionaries-and-glossaries).
890
1160
 
891
1161
  `engine.contentIndex` also re-exports eight names from neighbouring leaf modules, at this same import path:
892
1162
 
893
- | Export | Signature | Returns | Use it when |
894
- | --------------------- | -------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
895
- | `collectAnchors` | `collectAnchors(body, bodyLine)` | `Array<{slug: string, line: number, kind: string}>` | reading a note body's anchors, each with its kind, shared with the link checker and every build that emits a link |
896
- | `DERIVED_KEYS` | `const DERIVED_KEYS` | — | reading which keys the content index adds to a record, and which a note therefore may not author itself |
897
- | `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 |
898
- | `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 |
899
- | `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 |
900
- | `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 |
901
- | `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 |
902
- | `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 |
1163
+ | Export | Signature | Returns | Use it when |
1164
+ | --------------------- | ------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1165
+ | `collectAnchors` | `collectAnchors(body, bodyLine, resolveRole?, syntax?)` | `Array<{slug: string, line: number, kind: string}>` | reading a note body's anchors, each with its kind, shared with the link checker and every build that emits a link |
1166
+ | `DERIVED_KEYS` | `const DERIVED_KEYS` | — | reading which keys the content index adds to a record, and which a note therefore may not author itself |
1167
+ | `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 |
1168
+ | `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 |
1169
+ | `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 |
1170
+ | `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 |
1171
+ | `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 |
1172
+ | `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 |
903
1173
 
904
1174
  ### `engine.assetTypes`
905
1175
 
@@ -1002,7 +1272,7 @@ content tree can also compile into Foundry compendium packs.
1002
1272
  | `collectContentPages` | `function collectContentPages(contentBase, ctx)` | `{{pages: object[], addressFindings: object[], fmLinkFindings: object[]}}` | The content tree's pages, and what could not be addressed. |
1003
1273
  | `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>/`. |
1004
1274
  | `writeHomepages` | `function writeHomepages(outRoot, pages, config)` | {number} How many pages were written. | Writes each homepage as the package root's `_index.md`. |
1005
- | `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. |
1275
+ | `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. |
1006
1276
  | `emptyGates` | `function emptyGates()` | {object} An all-clear gate result. | The gate result of a build that ran none of them. |
1007
1277
  | `gatesFailed` | `function gatesFailed(gates)` | — | Whether any gate produced a finding. |
1008
1278
  | `tableUniverse` | `function tableUniverse(pages)` | {Map<string, object[]>} Package → the notes it may tabulate. | The universe a generated table searches, grouped by package. |
@@ -1014,6 +1284,10 @@ content tree can also compile into Foundry compendium packs.
1014
1284
  | `sitePageDecorator` | `sitePageDecorator(config, index)` | Function | Add the site's infobox and being metadata to a rendered page. |
1015
1285
  | `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. |
1016
1286
 
1287
+ `siteGates` takes `{ config, records, sqlTables? }`. Pass the prepared
1288
+ `sqlTables` map so anchors emitted by automatic dictionary sections are present in the
1289
+ index for checking page links.
1290
+
1017
1291
  ### `engine.sitePreview`
1018
1292
 
1019
1293
  `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.
@@ -1176,13 +1450,20 @@ One authored pathname, and the four addresses it resolves to. A note names a fil
1176
1450
 
1177
1451
  Resolving every link in a content tree, and reporting the ones that land nowhere. Three link defects survive both content builds silently, so neither the pack compilers nor a site build catches them:
1178
1452
 
1179
- | Export | Signature | Returns | Use it when |
1180
- | -------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- |
1181
- | `anchorsOf` | `function anchorsOf(body)` | {Set<string>} The declared anchor slugs. | Every `{#anchor}` a note declares on a heading. |
1182
- | `buildLinkIndex` | `buildLinkIndex(contentBase, { config, skipDirectories, sqlTables, records, problems })` | {object} The notes, the index, and the resolvers built over it. | Read a content tree into the index a link resolves against. |
1183
- | `auditHomepageLinks` | `function auditHomepageLinks(index)` | {Array<{note: object, field: string, url: string, text: string, occurrence: number, message: string}>} One finding per defect, `text` and `occurrence` locating it in the note's raw source. | Every defect in the addresses a package homepage carries. |
1184
- | `auditLinks` | `function auditLinks(index)` | `{{deadAnchors: object[], deadAddresses: object[], unlabelledLinks: object[], frontmatterLinks: object[], homepageLinks: object[], usedManifest: Set<string>}}` The findings, and which addresses a foreign manifest answered. | Every link in a tree that lands nowhere. |
1185
- | `walkReachability` | `function walkReachability(index,` | `{{root: object, reached: Set<object>, orphans: object[]}}` The root, everything reached from it, and the corpus members that were not. | Walk a corpus from its root and report what nothing links to. |
1453
+ | Export | Signature | Returns | Use it when |
1454
+ | -------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- |
1455
+ | `anchorsOf` | `function anchorsOf(body)` | {Set<string>} The declared anchor slugs. | Every `{#anchor}` a note declares on a heading. |
1456
+ | `buildLinkIndex` | `buildLinkIndex(contentBase, { config, skipDirectories, sqlTables, records, parsed, problems })` | {object} The notes, the index, and the resolvers built over it. | Read a content tree into the index a link resolves against. |
1457
+ | `auditHomepageLinks` | `function auditHomepageLinks(index)` | {Array<{note: object, field: string, url: string, text: string, occurrence: number, message: string}>} One finding per defect, `text` and `occurrence` locating it in the note's raw source. | Every defect in the addresses a package homepage carries. |
1458
+ | `auditLinks` | `function auditLinks(index)` | `{{deadAnchors: object[], deadAddresses: object[], unlabelledLinks: object[], frontmatterLinks: object[], homepageLinks: object[], usedManifest: Set<string>}}` The findings, and which addresses a foreign manifest answered. | Every link in a tree that lands nowhere. |
1459
+ | `walkReachability` | `function walkReachability(index,` | `{{root: object, reached: Set<object>, orphans: object[]}}` The root, everything reached from it, and the corpus members that were not. | Walk a corpus from its root and report what nothing links to. |
1460
+
1461
+ `buildLinkIndex` optionally accepts the `parsed` map captured by `indexRecordsFor`
1462
+ alongside the supplied `records` in the same invocation. It reads each entry's
1463
+ `raw` string by absolute note path and derives the untrimmed body as usual;
1464
+ without a raw string it reads that file. The caller owns this map, and the
1465
+ link index leaves it unchanged. This source capture is separate from published
1466
+ metadata and has no persistent or cross-invocation cache.
1186
1467
 
1187
1468
  ### `engine.webWikilinks`
1188
1469
 
@@ -1211,7 +1492,7 @@ Shared helpers for the pack compilers in `packages/package-build/`. The HeroicLa
1211
1492
  | ------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1212
1493
  | `md` | `const md` | — | The markdown renderer every surface shares. |
1213
1494
  | `renderFoundryMarkdown` | `function renderFoundryMarkdown(body)` | `string` — HTML with secret sections for GM passages. | Rendering a note body for a Foundry document. |
1214
- | `parseMarkdownFile` | `function parseMarkdownFile(filePath)` | — | Parses a markdown file with YAML frontmatter; returns frontmatter, body, rendered description, raw source, and body coordinates. |
1495
+ | `parseMarkdownFile` | `parseMarkdownFile(filePath, { addressContext, raw, renderDescription } = {})` | — | Parses a markdown file with YAML frontmatter; returns frontmatter, body, rendered description, raw source, and body coordinates. |
1215
1496
  | `assertStatedScope` | `function assertStatedScope(skipDirectories, who)` | {void} | Refuse a corpus read whose scope its caller did not state. |
1216
1497
  | `assertSuppliedCorpus` | `function assertSuppliedCorpus(records, who)` | {void} | Refuse a corpus read whose records its caller did not supply. |
1217
1498
  | `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. |
@@ -1224,11 +1505,44 @@ Shared helpers for the pack compilers in `packages/package-build/`. The HeroicLa
1224
1505
  | `buildStats` | `function buildStats(systemVersion` | {object} The `_stats` block. | Default `_stats` block for compiled compendium entries. |
1225
1506
  | `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. |
1226
1507
  | `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. |
1227
- | `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. |
1508
+ | `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. |
1228
1509
  | `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. |
1229
1510
  | `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. |
1230
1511
  | `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. |
1231
- | `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 |
1512
+ | `walkMarkdownTree` | `walkMarkdownTree(rootDir, { skipDirectories, sources, renderDescription })` | 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 |
1513
+
1514
+ `buildContentLinkIndex` accepts `{ config?, records, sqlTables?, foreign? }`.
1515
+ `records` is the required, supplied content corpus. The optional prepared
1516
+ `sqlTables` map supplies generated anchors and their Foundry page mappings,
1517
+ so glossary word links resolve to their dictionary entries.
1518
+
1519
+ `parseMarkdownFile` reads its supplied exact `raw` string when present. Its
1520
+ `renderDescription` option defaults to true; false leaves `description` empty
1521
+ while retaining authored body and positions. `walkMarkdownTree` accepts a
1522
+ selected `sources` map in walk order and the same rendering option, also true
1523
+ by default. Index collection explicitly omits unused rendered descriptions.
1524
+ `indexRecordsFor` accepts that selected map, a `syntax` projection function
1525
+ and a captured `foreignIndex`. These adapters do not retain state globally.
1526
+
1527
+ The physical wrappers `lintContentCharset`, `lintContentHtml`,
1528
+ `lintContentIcons`, `lintContentImages` and `lintContentTaskLists` accept an
1529
+ optional `sources` map of absolute paths to `{raw}`. Their existing extension,
1530
+ hidden-path and skip filters still apply; without the map they walk and read
1531
+ the tree. `lintPathCharset` accepts optional physical `inventory`, retaining
1532
+ walk roots and order. `lintNoteStates` accepts the authored `parsed` map, with
1533
+ its existing file reader for missing entries.
1534
+ `parseMarkdownFile` accepts LF and CRLF frontmatter delimiters and retains the
1535
+ exact source as `raw`. Its trimmed `body` keeps the authored line endings, with
1536
+ `bodyLine` and `bodyColumn` locating its first character in that original source.
1537
+
1538
+ ### `./engine/diagnostics`
1539
+
1540
+ The `@heroiclands/package-build/engine/diagnostics` wildcard module provides
1541
+ source locations and frontmatter source slicing.
1542
+
1543
+ | Export | Signature | Returns | Use it when |
1544
+ | ------------------ | ----------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
1545
+ | `splitFrontmatter` | `splitFrontmatter(raw)` | `{yaml: string, body: string} \| null` | Split opening LF or CRLF frontmatter into exact YAML and untrimmed body slices, without parsing or rewriting the source. |
1232
1546
 
1233
1547
  ### `engine.itemRegistry`
1234
1548
 
@@ -1384,15 +1698,15 @@ The content tree, built into a book. The I/O half of the PDF surface: it reads t
1384
1698
 
1385
1699
  Journals pack compiler — produces JSON pack files for the "journals" Foundry compendium from markdown notes in the `assets/content/` tree. The content root (`contentBase`) is walked recursively; any `.md` file whose frontmatter declares either `type: doc` or a **doc-carrying type** ({@link sohl.utils.packs.docEntryTypes} — every item type, plus `macro`) is compiled into one JournalEntry document. Each note's body is split on top-level H1 headings; the optional content before the first H1 becomes a lead page, and each subsequent H1 starts a new page named after its heading text. All page bodies are rendered to HTML.
1386
1700
 
1387
- | Export | Signature | Returns | Use it when |
1388
- | --------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
1389
- | `splitPages` | `function splitPages(body, leadName` | — | Splits a markdown body into pages by top-level H1 headings. |
1390
- | `assertUniquePages` | `function assertUniquePages(rawPages, noteName)` | — | Two pages in one note that would derive the same id, which the LevelDB packer reports only as an opaque duplicate-key collision. |
1391
- | `assertUniqueAnchors` | `function assertUniqueAnchors(rawPages, noteName)` | — | The anchor half of {@link assertUniquePages}, under its former name. |
1392
- | `journalPageId` | `function journalPageId(entryId, page)` | {string} A 16-character Foundry id. | The id of one page within its entry. |
1393
- | `buildPages` | `function buildPages(rawPages, entryId, noteName)` | {Array<{\_id: string, name: string, type: string, title: {show: boolean, level: number}, text: {format: number, content: string}, \_key: string}>} The page documents, in order. | Compile split pages into JournalEntryPage documents. |
1394
- | `buildJournalEntry` | `buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats })` | {object} The JournalEntry document, keyed for the pack. | Assemble one JournalEntry document from a note's converted markdown. |
1395
- | `Journals` | `class Journals extends BasePackCompiler` | — | not called directly — imported and driven by `engine/generate.mjs` as the "journals" pack compiler |
1701
+ | Export | Signature | Returns | Use it when |
1702
+ | --------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
1703
+ | `splitPages` | `splitPages(body, leadName, resolveRole, syntax?)` | — | Splits a markdown body into pages by top-level H1 headings. |
1704
+ | `assertUniquePages` | `function assertUniquePages(rawPages, noteName)` | — | Two pages in one note that would derive the same id, which the LevelDB packer reports only as an opaque duplicate-key collision. |
1705
+ | `assertUniqueAnchors` | `function assertUniqueAnchors(rawPages, noteName)` | — | The anchor half of {@link assertUniquePages}, under its former name. |
1706
+ | `journalPageId` | `function journalPageId(entryId, page)` | {string} A 16-character Foundry id. | The id of one page within its entry. |
1707
+ | `buildPages` | `function buildPages(rawPages, entryId, noteName)` | {Array<{\_id: string, name: string, type: string, title: {show: boolean, level: number}, text: {format: number, content: string}, \_key: string}>} The page documents, in order. | Compile split pages into JournalEntryPage documents. |
1708
+ | `buildJournalEntry` | `buildJournalEntry({ id, name, markdown, leadName, folder, stats, syntax })` | {object} The JournalEntry document, keyed for the pack. | Assemble one JournalEntry document from a note's converted markdown. |
1709
+ | `Journals` | `class Journals extends BasePackCompiler` | — | not called directly — imported and driven by `engine/generate.mjs` as the "journals" pack compiler |
1396
1710
 
1397
1711
  ### `engine.macros`
1398
1712
 
@@ -1465,28 +1779,28 @@ Adventure pack compiler — `type: bundle` notes → Foundry `Adventure` documen
1465
1779
 
1466
1780
  Pack JSON generation — in-repo Markdown → per-entry JSON (build-only). Reads the authoritative content tree at the configured content root and compiles each pack's entries to per-entry JSON under its build directory (`build/packs-json/<pack>/` in this repository). The JSON is a disposable build intermediate consumed by `build:compiledb` (which turns it into the shipped LevelDB packs) — it is never committed.
1467
1781
 
1468
- | Export | Signature | Returns | Use it when |
1469
- | ----------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
1470
- | `systemCompilers` | `function systemCompilers(passes)` | {Readonly<Record<string, Readonly<Record<string, Function>>>>} System id → document class → pass. | The system-specific compilers, derived from the passes themselves. |
1471
- | `compilerFor` | `function compilerFor(docType, system` | {Function\|undefined} The compiler class, or `undefined` for a document type nothing here compiles — which {@link generatePack} reports rather than defaulting past. | The compiler class a pack of one document type and one system gets. |
1472
- | `emittedArtFor` | `function emittedArtFor(type)` | {{document: string\|null, art: readonly string[]}\|null} What the type compiles into and the art it carries there, or `null` where no claim can be made. | The art fields a note of one content type reaches its document through, and the document it reaches. |
1473
- | `packJsonDir` | `const packJsonDir` | {string} The pack's JSON directory. | Root of the build-only JSON tree for one pack. |
1474
- | `itemPackJsonDirs` | `function itemPackJsonDirs(config` | {string[]} Each Item pack's JSON directory. | The generated JSON of **every** configured Item pack — what the actors pass reads its predefined items from. |
1475
- | `bundleSourceJsonDirs` | `function bundleSourceJsonDirs(config` | {Record<string, string[]>} Each readable pack's JSON directory, by the Foundry document type it holds. | The compiled JSON a bundle may hold copies of, by document type. |
1476
- | `orderPassesByDependency` | `function orderPassesByDependency(packs)` | {object[]} A new list, in compile order. | The passes to run, ordered so that each one follows the output it reads. |
1477
- | `unsatisfiedPassDependencies` | `function unsatisfiedPassDependencies(running, config)` | {string[]} One message per unsatisfiable dependency. | The dependencies this run cannot satisfy by ordering, because the pass that would produce them is not in it. |
1478
- | `emptyPassErrors` | `function emptyPassErrors(passes)` | {string[]} One message per pass that must not have been empty. | The passes that compiled nothing when they were expected to compile something — a build failure, not a quiet no-op. |
1479
- | `generatePacksJson` | `async generatePacksJson({ only, config })` | {Promise<number>} Total error count across the generated packs. | Generate the build-only JSON for every pack (or one, when `only` is given). |
1782
+ | Export | Signature | Returns | Use it when |
1783
+ | ----------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
1784
+ | `systemCompilers` | `function systemCompilers(passes)` | {Readonly<Record<string, Readonly<Record<string, Function>>>>} System id → document class → pass. | The system-specific compilers, derived from the passes themselves. |
1785
+ | `compilerFor` | `function compilerFor(docType, system` | {Function\|undefined} The compiler class, or `undefined` for a document type nothing here compiles — which {@link generatePack} reports rather than defaulting past. | The compiler class a pack of one document type and one system gets. |
1786
+ | `emittedArtFor` | `function emittedArtFor(type)` | {{document: string\|null, art: readonly string[]}\|null} What the type compiles into and the art it carries there, or `null` where no claim can be made. | The art fields a note of one content type reaches its document through, and the document it reaches. |
1787
+ | `packJsonDir` | `const packJsonDir` | {string} The pack's JSON directory. | Root of the build-only JSON tree for one pack. |
1788
+ | `itemPackJsonDirs` | `function itemPackJsonDirs(config` | {string[]} Each Item pack's JSON directory. | The generated JSON of **every** configured Item pack — what the actors pass reads its predefined items from. |
1789
+ | `bundleSourceJsonDirs` | `function bundleSourceJsonDirs(config` | {Record<string, string[]>} Each readable pack's JSON directory, by the Foundry document type it holds. | The compiled JSON a bundle may hold copies of, by document type. |
1790
+ | `orderPassesByDependency` | `function orderPassesByDependency(packs)` | {object[]} A new list, in compile order. | The passes to run, ordered so that each one follows the output it reads. |
1791
+ | `unsatisfiedPassDependencies` | `function unsatisfiedPassDependencies(running, config)` | {string[]} One message per unsatisfiable dependency. | The dependencies this run cannot satisfy by ordering, because the pass that would produce them is not in it. |
1792
+ | `emptyPassErrors` | `function emptyPassErrors(passes)` | {string[]} One message per pass that must not have been empty. | The passes that compiled nothing when they were expected to compile something — a build failure, not a quiet no-op. |
1793
+ | `generatePacksJson` | `generatePacksJson({ only, config, session, reportPreparation })` | {Promise<number>} Total error count across the generated packs. | Generate the build-only JSON for every pack (or one, when `only` is given). |
1480
1794
 
1481
1795
  ### `engine.compendiums`
1482
1796
 
1483
1797
  Compendium pack library — compile / unpack / clean LevelDB packs. Wraps `@foundryvtt/foundryvtt-cli` over the packs a consuming repository declares: - {@link compilePacks}: generates each pack's per-entry JSON from the `assets/content/` Markdown into `build/packs-json/<name>/` (via generate.mjs), then builds LevelDB from it; no committed JSON, no vault. - {@link unpackPacks}: extracts a compiled pack back to per-entry JSON, rebuilding folder paths. - {@link cleanPacks}: normalizes/strips extracted JSON.
1484
1798
 
1485
- | Export | Signature | Returns | Use it when |
1486
- | -------------- | --------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
1487
- | `compilePacks` | `async compilePacks({ config, sourcePacks, stageDest, packName })` | — | Generates each pack's per-entry JSON from `assets/content/` into `build/packs-json/<name>/`, then builds the LevelDB output from it. |
1488
- | `cleanPacks` | `async cleanPacks({ config, packDest, packName, entryName })` | — | Cleans and formats source JSON files, removing unnecessary permissions and flags and adding the proper spacing. |
1489
- | `unpackPacks` | `async unpackPacks({ packs, config, stageDest, packDest, packName })` | — | Extracts compiled LevelDB packs back to per-entry JSON, rebuilding the folder hierarchy as directories. |
1799
+ | Export | Signature | Returns | Use it when |
1800
+ | -------------- | ---------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
1801
+ | `compilePacks` | `compilePacks({ config, sourcePacks, stageDest, packName, session, reportPreparation })` | — | Generates each pack's per-entry JSON from `assets/content/` into `build/packs-json/<name>/`, then builds the LevelDB output from it. |
1802
+ | `cleanPacks` | `async cleanPacks({ config, packDest, packName, entryName })` | — | Cleans and formats source JSON files, removing unnecessary permissions and flags and adding the proper spacing. |
1803
+ | `unpackPacks` | `async unpackPacks({ packs, config, stageDest, packDest, packName })` | — | Extracts compiled LevelDB packs back to per-entry JSON, rebuilding the folder hierarchy as directories. |
1490
1804
 
1491
1805
  ### `engine.regionEvents`
1492
1806
 
@@ -2033,6 +2347,15 @@ The Changesets changelog generator, loaded with `require` — a CommonJS module
2033
2347
  | `getReleaseLine` | `getReleaseLine(changeset, type, changelogOpts)` | `string` — the summary, verbatim, separated from its neighbours by a blank line | writing one changeset's release line with no commit-hash prefix and no bullet |
2034
2348
  | `getDependencyReleaseLine` | `getDependencyReleaseLine(changesets, dependenciesUpdated, changelogOpts)` | `string` — always `""` | satisfying the `ChangelogFunctions` interface; these packages narrate no internal dependency bumps |
2035
2349
 
2350
+ `engine/changelog-group.mjs` exports `groupChangelogText(text, { labels })`,
2351
+ returning `{ text, findings }`. It groups the newest release's summaries
2352
+ within each bump tier, retaining prose, bullets, wrapped continuations,
2353
+ nested item content and literal code. Merged labels' opening paragraphs
2354
+ combine in source order before their items. Exact duplicate items print
2355
+ once; labels follow the configured order, with unlabelled lead items first.
2356
+ Earlier release sections stay byte-identical, and grouping the result
2357
+ again leaves its text unchanged.
2358
+
2036
2359
  ## `./package.json`
2037
2360
 
2038
2361
  Not a JavaScript module — this subpath entry exists so tooling (bundlers, `import.meta.resolve`, a script reading the installed version) can resolve the package's own `package.json` through Node's package-exports resolution instead of reaching outside the declared export map.
@@ -2046,7 +2369,7 @@ The engine wildcard subpath also exposes the body parsers below. Their figure ex
2046
2369
  | `engine/content-figures.mjs` | `FIGURE_NAMES`, `FIGURE_CLASSES`, `scanFigures`, `numberFigures`, `renderFigureBlocks` | Scan leading captions and their next supported blocks; `numbered` distinguishes `:` and `:@`. `type` may override inference; `example` is supported. `FIGURE_CLASSES` retains built-in styled classes, not a closed author vocabulary. |
2047
2370
  | `engine/content-blocks.mjs` | `BLOCK_NAMES`, `BLOCK_CONTAINERS`, `scanBlocks`, `renderBlocks`, `renderWithheldSections` | Read div and secret fences; generic div records have `name: "div"` and no generated title. Scanning includes poetry errors. |
2048
2371
  | `engine/content-poetry.mjs` | `verseLines`, `poetryStanzas`, `inlineVerse`, `poetryMarkdown`, `scanPoetry`, `renderPoetry` | Read `poetry` code fences, validate authored metadata and tabs, and preserve relative indent levels and stanzas. |
2049
- | `engine/content-spans.mjs` | `spanMarkdownPlugin`, `scanSpans`, `renderSpans` | Parse bracket spans after recognized links. The scanner returns spans and located errors; the renderer preserves inline Markdown in attributed HTML spans. |
2372
+ | `engine/content-spans.mjs` | `spanMarkdownPlugin`, `scanSpans`, `renderSpans` | Parse bracket spans after recognized links. The scanner returns spans, the exact `.name` inventory and located errors, retaining visible text and attribute positions; rendering preserves inline Markdown and permitted attributes. |
2050
2373
 
2051
2374
  `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"`.
2052
2375
 
@@ -2056,7 +2379,7 @@ The engine wildcard subpath also exposes the body parsers below. Their figure ex
2056
2379
  | ---------------- | ----------------------------------------------------------------------------------- |
2057
2380
  | `markupFindings` | Validate complete Markdown source and return diagnostics for the supplied filename. |
2058
2381
 
2059
- `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.
2382
+ `markupFindings(source, file, syntax?)` 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.
2060
2383
 
2061
2384
  `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.
2062
2385
 
@@ -2069,8 +2392,253 @@ that map as `parsed` for its document passes. `unclaimedNoteFindings` accepts it
2069
2392
  alongside `records`. `locateFrontmatterKey` and `assertNoDeclaredPackage` accept
2070
2393
  `raw` in their options to locate findings without opening the note again.
2071
2394
 
2395
+ `buildCompileCorpus`, `generatePacksJson` and `compilePacks` accept an optional
2396
+ `session` bound to their resolved configuration, roots, scope and router.
2397
+ `prepareContentIndex` also accepts it. Without a session, the existing entry
2398
+ points derive their own inputs. Generation and pack compilation accept
2399
+ `reportPreparation` (default true); a coordinating caller may set it false
2400
+ only after reporting the shared source findings itself.
2401
+
2402
+ `buildLinkIndex` and `buildContentLinkIndex` accept an optional `foreign` view
2403
+ from the same session; their diagnostic and routed resolver contracts remain
2404
+ distinct. `collectAnchors`, `markupAnchorFindings`, `markupFindings`, journal
2405
+ `splitPages`, Foundry `anchorsOf` and `buildJournalEntry` accept the optional
2406
+ `syntax` function described above. `buildSiteIndex` and `prepareTreeSqlTables`
2407
+ accept it in options, and `insertLexicalSections` accepts it in context.
2408
+ Prepared per-note SQL results carry it to `expandContentTables`. No scan result
2409
+ is reused for a different source projection.
2410
+
2072
2411
  `prepareSqlTables` accepts sources with an optional `authoredSqlBlocks` count,
2073
2412
  set by `joinGeneratedSections`. Subsequent fences are generated event queries
2074
2413
  whose stub-inclusive results are identical; only authored fences receive a
2075
2414
  stub-inclusion comparison. Query answers and comparisons are cached for one
2076
2415
  preparation and kept separate.
2416
+
2417
+ `prepareTreeSqlTables` retains the complete record membership when opening a
2418
+ public database, so derived citations can distinguish hidden notes from
2419
+ unknown addresses. Only public records contribute rendered sources and links.
2420
+
2421
+ | Export | Contract |
2422
+ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
2423
+ | `prepareLinkSqlTables` | Prepare SQL, page-list and automatic lexical sections for link checking, using one preparation per required source audience. |
2424
+
2425
+ `engine/sql-tables.mjs` exports `prepareLinkSqlTables(contentBase, options)`.
2426
+ Its options are `config`, `skipDirectories`, complete `records`, the shared
2427
+ `parsed` map, `foreignIndex` and optional `prepareAudience(audience)` callback. The callback supplies the bound session's existing `public` or `all` preparation. It returns prepared tables with `pageLists`
2428
+ and `inline` maps, or `undefined` when no source needs a directive or automatic
2429
+ lexical section. Public
2430
+ sources use public rows; GM sources use all rows. Query results remain local
2431
+ to each audience preparation, and the caller’s records remain complete for
2432
+ checking authored references. Unresolved derived citations retain the
2433
+ renderer’s plain-label behavior.
2434
+
2435
+ `generatedSectionsFor(db, records, contentBase, options?)` accepts complete
2436
+ lineage records as `options.all` (default `records`). Its result includes
2437
+ `lexical`, a map from note file to `LexicalSection[]`, beside `sections`,
2438
+ `views` and `derived`. `attachGeneratedSections(prepared, generated)` places
2439
+ those lists on each note's prepared results as `lexical`; body expansion
2440
+ inserts them alongside the prepared directives.
2441
+
2442
+ ## Dictionary and glossary rows
2443
+
2444
+ The wildcard engine subpath exposes
2445
+ `@heroiclands/package-build/engine/dictionary-rows` for deriving the
2446
+ `dictionary` and `terms` SQL relations from content-index records:
2447
+
2448
+ | Export | Signature | Returns | Use it when |
2449
+ | ----------------------- | ----------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------- |
2450
+ | `DICTIONARY_COLUMNS` | `const DICTIONARY_COLUMNS` | A readonly list of column names | Declaring the schema of a language's dictionary. |
2451
+ | `TERM_COLUMNS` | `const TERM_COLUMNS` | A readonly list of column names | Declaring the schema of owner-specific glossary terms. |
2452
+ | `ETYMOLOGY_KEYS` | `const ETYMOLOGY_KEYS` | A readonly list of keys | Reading the structure of a dictionary word's etymology. |
2453
+ | `dictionaryLetter` | `dictionaryLetter(text)` | `string` | Deriving a heading letter from a sort key. |
2454
+ | `dictionaryEntryAnchor` | `dictionaryEntryAnchor(language, name)` | `string` | Deriving the stable entry anchor for a word in a language's dictionary. |
2455
+ | `dictionaryAndTermRows` | `dictionaryAndTermRows(records, all = records)` | `{ dictionary: object[], terms: object[] }` | Deriving dictionary words and glossary terms for the selected records. |
2456
+ | `serializeRows` | `serializeRows(rows)` | `string` | Writing deterministic JSON Lines with keys sorted at every depth. |
2457
+
2458
+ `dictionaryEntryAnchor` takes the language's canonical Address and the
2459
+ word's form. Its `word-` anchor is derived from the full SHA-256 digest of
2460
+ that language and the NFC-normalized form, with case and accents preserved.
2461
+ It identifies the word's entry on that language page, including a word
2462
+ inherited from an ancestor.
2463
+
2464
+ `records` is the selected audience; `all` supplies the complete record set for
2465
+ language and ownership resolution. A dictionary word takes its sort key and
2466
+ pronunciation from the language's word declaration. A glossary entry can
2467
+ supply its own sort key and pronunciation, using the dictionary word's
2468
+ values where unstated.
2469
+
2470
+ A term naming a dictionary word carries its `class` and a `word` Address
2471
+ linking to its dictionary entry. The target is in the resolved language's
2472
+ dictionary when that language has own words; otherwise it is in the visible
2473
+ language that defines the word. The term's `language` stays resolved to the
2474
+ owner or entry's language. Without an entry's `definition`, its `senses`
2475
+ contain every dictionary gloss sense with
2476
+ an empty `bodies` list. With a definition, that owner's meaning is the sense
2477
+ instead. A rank or office of the same owner contributes its distinct
2478
+ descriptions after the dictionary senses; the owner's definition replaces
2479
+ both. Ranks, offices and terms outside the dictionary carry their source
2480
+ bodies. `common` belongs to
2481
+ glossary entries and `terms`, with each owner's choice kept on that owner's
2482
+ row. The [format
2483
+ reference](reference/format-details.md#dictionaries-and-glossaries) defines
2484
+ all columns and the rules for culture reach and ranks. The `cultures` list
2485
+ records associations for custom queries. Automatic Glossary sections select
2486
+ only their owner's explicitly declared `data.glossary` entries. A custom
2487
+ view can also select reached rank and office rows, keeping common marks only
2488
+ on its owner's own rows.
2489
+
2490
+ Dictionary rows carry `plural`, the noun's nonempty explicit plural or its
2491
+ listed language's first matching literal suffix replacement, and `verbs`,
2492
+ that language's stated `{invariant: true}` or six-person pattern, otherwise
2493
+ null. These optional facts use no ancestor grammar and perform no word-rule
2494
+ validation. The raw `words` relation's `plural` remains the authored value.
2495
+ Dictionary SQL types are projected only for that relation, retaining its
2496
+ etymology struct, sense list, usage list and integer depth even when all
2497
+ optional grammar values are null.
2498
+
2499
+ `@heroiclands/package-build/engine/language-terms` exposes the person
2500
+ descriptors used by the schema, SQL pattern type and head table:
2501
+
2502
+ | Export | Signature | Returns | Use it when |
2503
+ | -------------------- | -------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------- |
2504
+ | `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. |
2505
+
2506
+ `serializeRows` returns the empty string for no rows; otherwise each object
2507
+ occupies one line and the file ends with a newline.
2508
+
2509
+ ## Dictionary and glossary layouts
2510
+
2511
+ `@heroiclands/package-build/engine/dictionary-layout` renders SQL results as
2512
+ Markdown for the dictionary or glossary on its owner's note:
2513
+
2514
+ | Export | Signature | Returns | Use it when |
2515
+ | ------------------------- | ------------------------------------ | -------------------------- | ----------------------------------------------------------------------------------- |
2516
+ | `LAYOUT_REQUIRED_COLUMNS` | `const LAYOUT_REQUIRED_COLUMNS` | Required columns by layout | Checking the columns a dictionary or glossary query must select. |
2517
+ | `COMMON_MARK` | `const COMMON_MARK` | `string` | Reading the mark a common glossary entry opens with. |
2518
+ | `DICTIONARY_ENTRY_CLASS` | `const DICTIONARY_ENTRY_CLASS` | `string` | Identifying generated headword spans that are native dictionary targets in Foundry. |
2519
+ | `dictionaryEntryPlugin` | `dictionaryEntryPlugin(md)` | `void` | Registering native dictionary headword rendering on the Foundry Markdown parser. |
2520
+ | `renderDictionary` | `renderDictionary(result, options?)` | `string` | Rendering a language's words with classes, etymology, senses and citations. |
2521
+ | `renderGlossary` | `renderGlossary(result, options?)` | `string` | Rendering an owner's glossary entries, ranks and offices. |
2522
+
2523
+ `result` is `{ columns: string[], rows: object[] }`. Both layouts require
2524
+ `name`, `senses` and `usage`; a missing column throws an error naming it.
2525
+ `LayoutOptions` accepts `linkable(ref)`, `sectionLevel`, `addressContext`,
2526
+ `labelOf(address)`, `showKey` (`true`, `false` or `"only"`), `pageLanguage`, `pageOwner`,
2527
+ `verbs` and `entryAnchors` (default `true`). These control link availability, letter-heading level,
2528
+ Address resolution, labels, the abbreviation key and the language whose tag
2529
+ is omitted. `entryAnchors` controls canonical dictionary headword targets;
2530
+ custom SQL dictionary views set it to `false`, leaving those targets to the
2531
+ language's automatic Dictionary section. A dictionary marks nothing common;
2532
+ a glossary prints the mark and its explanation when a selected term carries
2533
+ `common: true`.
2534
+
2535
+ A dictionary entry has a stable word anchor. A glossary headword with a
2536
+ `word` Address links to that entry, and its `class` prints beside the language
2537
+ tag. Glossary rows sharing a headword and language combine into one entry;
2538
+ identical senses print once with all their source bodies. Dictionary-derived
2539
+ senses carry no body label. Dictionary entries print their `field` in italic
2540
+ parentheses and their etymology in brackets.
2541
+
2542
+ Nouns print their optional `plural` beside the class. Each represented
2543
+ language's optional `verbs` column supplies one table after the key and
2544
+ before the first letter, with empty endings shown as "unchanged". For an
2545
+ automatic dictionary, `options.verbs` supplies the owning language's pattern
2546
+ even when its own words contain only excluded classes. The existing
2547
+ nonempty-own-words gate still applies. `showKey: false` suppresses the key
2548
+ and verb table while retaining entries; `showKey: "only"` prints the
2549
+ abbreviation key alone. With the default or `showKey: true`, a stated pattern
2550
+ still prints when the result needs no abbreviation key. Each head-enabled
2551
+ result prints its represented languages' tables independently.
2552
+
2553
+ `pageOwner` is the containing page's canonical note Address. A glossary omits
2554
+ a sense's body label only when every contributing row belongs to that page
2555
+ and attributes the sense solely to itself. Other bodies and shared senses
2556
+ keep their labels. Without `pageOwner` or a selected `owner` column, all body
2557
+ labels remain.
2558
+
2559
+ `DICTIONARY_ENTRY_CLASS` is `"dictionary-entry"`. The Foundry parser's
2560
+ `dictionaryEntryPlugin` promotes the marked headword at the start of a
2561
+ dictionary paragraph to an inline native H6 heading in a div container. It
2562
+ keeps the containing journal page. Site and book rendering retain the
2563
+ headword's span and paragraph. `anchorsOf` maps this dictionary target to
2564
+ its containing page's UUID with the word fragment; generic authored spans
2565
+ keep their containing-page behavior.
2566
+
2567
+ ## Automatic dictionary and glossary sections
2568
+
2569
+ `@heroiclands/package-build/engine/lexical-sections` derives and inserts the
2570
+ same automatic lists for the site, Foundry journal, book and published index:
2571
+
2572
+ | Export | Signature | Returns | Use it when |
2573
+ | ----------------------- | ------------------------------------------------------ | ------------------------------------- | ----------------------------------------------------------------- |
2574
+ | `LEXICAL_SECTIONS` | `const LEXICAL_SECTIONS` | A frozen list of section declarations | Reading section headings, accepted anchors and frontmatter gates. |
2575
+ | `hasLexicalEntries` | `hasLexicalEntries(frontmatter)` | `boolean` | Checking whether a note supplies automatic lexical content. |
2576
+ | `lexicalSectionsFor` | `lexicalSectionsFor(records, all = records)` | `Map<string, LexicalSection[]>` | Deriving lists keyed by their owner's canonical Address. |
2577
+ | `insertLexicalSections` | `insertLexicalSections(markdown, sections?, context?)` | `{markdown, lineMap, generatedFrom?}` | Inserting lists while preserving authored text and source lines. |
2578
+
2579
+ Each registry declaration has `slug`, `heading`, `aliases`, `dataKey` and
2580
+ `types`, whose entries have `type` and an optional `subType`. The Dictionary
2581
+ declaration applies to `skill`/`language` notes with nonempty `data.words`,
2582
+ and accepts `dictionary` and `lexicon` heading anchors. The Glossary
2583
+ declaration applies to cultures (`lore`/`culture`) and affiliations with
2584
+ nonempty `data.glossary`, and accepts `glossary`. `hasLexicalEntries` reads
2585
+ these same type and nonempty-array gates; it does not validate the authored
2586
+ body.
2587
+
2588
+ `records` is the visible audience; `all` supplies complete lineage records.
2589
+ Each `LexicalSection` has `slug`, `heading`, `aliases`, a query-shaped
2590
+ `result: {columns, rows}` and dictionary-layout `options`. Dictionary rows
2591
+ cover the language's full dictionary; glossary rows select its owner's
2592
+ explicit entries only, including a rank or office only when named there.
2593
+ A qualifying words declaration containing only excluded word classes still
2594
+ produces its Dictionary heading, without entries or a key.
2595
+
2596
+ `insertLexicalSections` takes the body after authored directives expand.
2597
+ Pass the original body as `context.authored` together with the expansion's
2598
+ line map, so generated headings cannot select or end an authored section.
2599
+ Without `authored`, the helper treats `markdown` as its source. It inserts a
2600
+ list at the end of the first matching authored anchored section, after its
2601
+ text and before the next authored heading at the same or a higher level.
2602
+ Headings inside comments, raw HTML, literal code or named blocks do not select
2603
+ sections.
2604
+ Without that heading, it appends an H1 with the section's canonical anchor.
2605
+ Letter headings sit one level below the containing heading, up to H6. The
2606
+ optional context is `{authored?, lineMap?, generatedFrom?}`; the returned line map keeps
2607
+ authored positions, marks inserted lines as generated and adjusts the start
2608
+ of appended nonlexical sections. It leaves the supplied line-map array
2609
+ unchanged.
2610
+
2611
+ ## Glossary term validation
2612
+
2613
+ The wildcard module `@heroiclands/package-build/engine/culture-reach`
2614
+ exposes the term lookup used for glossary validation;
2615
+ `@heroiclands/package-build/engine/glossary-entries` exposes its findings:
2616
+
2617
+ | Export | Signature | Returns | Use it when |
2618
+ | --------------- | ------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------- |
2619
+ | `noteTerms` | `noteTerms(index, note, entry?)` | `Map<string, string[]>` | Reading eligible dictionary words and reached rank or office descriptions by NFC name. |
2620
+ | `checkGlossary` | `checkGlossary(note, { index } = {})` | `object[]` | Checking the meanings and language references of a culture's or affiliation's glossary entries. |
2621
+
2622
+ `noteTerms` selects words from the entry's resolved language and its
2623
+ ancestors; without `entry`, it uses the note's own resolved language. It also
2624
+ includes ranks and offices that reach the note. `checkGlossary` returns
2625
+ located findings for entries naming no language, duplicate names, invalid
2626
+ pronunciations and entries without a definition that match neither a word in
2627
+ their resolved language nor a reached rank or office. A definition repeating
2628
+ the only sense it replaces produces a warning. With no `index`, the check
2629
+ validates only what the note states locally.
2630
+
2631
+ ## Built-piece resolution
2632
+
2633
+ The wildcard engine subpath exposes `pieceGlosses` from
2634
+ `@heroiclands/package-build/engine/word-rows`.
2635
+
2636
+ | Export | Signature | Returns | Use it when |
2637
+ | -------------- | ------------------------ | ----------------------------------- | ---------------------------------------------------------------------------- |
2638
+ | `pieceGlosses` | `pieceGlosses(...datas)` | A lookup returning a string or null | Resolving a word's built pieces for lint, word rows or dictionary etymology. |
2639
+
2640
+ Pass language `data` objects nearest first, in `lineageChain` order. Each
2641
+ language contributes words, affixes and elements, in that order. Words and
2642
+ affixes supply `gloss`, including an affix matched by its `front` form. Elements
2643
+ supply `sense` and match their `form` and `variants`. Exact matches precede matches without joining hyphens; case and
2644
+ accents are preserved. A missing gloss returns `null`.