@heroiclands/package-build 23.9.0 → 23.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +2 -0
  3. package/bin/content-commands.mjs +197 -68
  4. package/docs/api.md +365 -70
  5. package/docs/authoring/first-note.md +10 -0
  6. package/docs/authoring/frontmatter.md +1 -1
  7. package/docs/authoring/links-and-markup.md +186 -49
  8. package/docs/commands.md +92 -3
  9. package/docs/content-format.md +10 -3
  10. package/docs/design.md +18 -0
  11. package/docs/guides/book.md +7 -0
  12. package/docs/guides/site.md +7 -0
  13. package/docs/reference/format-details.md +186 -15
  14. package/docs/reference/note-types.md +4 -4
  15. package/engine/anchors.mjs +43 -9
  16. package/engine/base-compiler.mjs +6 -4
  17. package/engine/changelog-group.mjs +58 -34
  18. package/engine/changelog-lint.mjs +6 -2
  19. package/engine/compendiums.mjs +15 -1
  20. package/engine/compile-corpus.mjs +14 -1
  21. package/engine/content-charset.mjs +13 -1
  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 +102 -23
  26. package/engine/content-links.mjs +68 -30
  27. package/engine/content-session.mjs +247 -0
  28. package/engine/content-sources.mjs +110 -0
  29. package/engine/content-spans.mjs +118 -14
  30. package/engine/content-tables.mjs +1 -0
  31. package/engine/content-tasklists.mjs +20 -4
  32. package/engine/diagnostics.mjs +17 -6
  33. package/engine/extension-attributes.mjs +28 -5
  34. package/engine/first-use.mjs +371 -0
  35. package/engine/foundry-entries.mjs +12 -9
  36. package/engine/generate.mjs +13 -4
  37. package/engine/helpers.mjs +47 -16
  38. package/engine/index-records.mjs +1 -0
  39. package/engine/index.mjs +3 -0
  40. package/engine/journals.mjs +9 -5
  41. package/engine/language-data.mjs +2 -2
  42. package/engine/language-rows.mjs +4 -4
  43. package/engine/language-terms.mjs +4 -4
  44. package/engine/lexical-sections.mjs +5 -5
  45. package/engine/markup-validation.mjs +7 -2
  46. package/engine/metadata-index.mjs +1 -0
  47. package/engine/name-markup.mjs +114 -0
  48. package/engine/name-rows.mjs +70 -0
  49. package/engine/names.mjs +460 -0
  50. package/engine/note-addresses.mjs +25 -4
  51. package/engine/path-charset.mjs +24 -1
  52. package/engine/pdf-render.mjs +15 -1
  53. package/engine/site-index.mjs +25 -2
  54. package/engine/sql-tables.mjs +76 -15
  55. package/engine/stub-lint.mjs +4 -3
  56. package/engine/web-wikilinks.mjs +33 -12
  57. package/engine/wikilink-syntax.mjs +65 -8
  58. package/engine/wikilinks.mjs +29 -11
  59. package/engine/yaml-lint.mjs +3 -5
  60. package/hugo-theme/layouts/partials/footer.html +2 -0
  61. package/hugo-theme/static/css/style.css +20 -0
  62. package/hugo-theme/static/js/inline-spoilers.js +23 -0
  63. package/package.json +1 -1
  64. package/types/engine/anchors.d.mts +6 -3
  65. package/types/engine/compendiums.d.mts +5 -1
  66. package/types/engine/compile-corpus.d.mts +5 -2
  67. package/types/engine/content-charset.d.mts +5 -1
  68. package/types/engine/content-html.d.mts +5 -1
  69. package/types/engine/content-icons.d.mts +5 -1
  70. package/types/engine/content-images.d.mts +5 -1
  71. package/types/engine/content-index.d.mts +39 -19
  72. package/types/engine/content-links.d.mts +22 -8
  73. package/types/engine/content-session.d.mts +108 -0
  74. package/types/engine/content-sources.d.mts +32 -0
  75. package/types/engine/content-spans.d.mts +8 -6
  76. package/types/engine/content-tasklists.d.mts +5 -1
  77. package/types/engine/diagnostics.d.mts +10 -0
  78. package/types/engine/extension-attributes.d.mts +16 -2
  79. package/types/engine/first-use.d.mts +31 -0
  80. package/types/engine/foundry-entries.d.mts +11 -4
  81. package/types/engine/generate.d.mts +5 -1
  82. package/types/engine/helpers.d.mts +30 -7
  83. package/types/engine/index.d.mts +1 -0
  84. package/types/engine/journals.d.mts +5 -2
  85. package/types/engine/lexical-sections.d.mts +3 -2
  86. package/types/engine/markup-validation.d.mts +2 -1
  87. package/types/engine/name-markup.d.mts +176 -0
  88. package/types/engine/name-rows.d.mts +35 -0
  89. package/types/engine/names.d.mts +356 -0
  90. package/types/engine/note-addresses.d.mts +3 -3
  91. package/types/engine/path-charset.d.mts +16 -1
  92. package/types/engine/site-build.d.mts +2 -2
  93. package/types/engine/site-index.d.mts +3 -1
  94. package/types/engine/sql-tables.d.mts +7 -2
  95. package/types/engine/stub-lint.d.mts +3 -1
  96. package/types/engine/wikilink-syntax.d.mts +40 -3
package/docs/api.md CHANGED
@@ -467,10 +467,10 @@ What a draft note says about itself, in one sentence, on every surface it reache
467
467
 
468
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.
469
469
 
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 })` | `{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 |
474
474
 
475
475
  ### `engine.placeRelations`
476
476
 
@@ -871,7 +871,7 @@ Emitting this package's cross-package link manifest. `engine/content-address.mjs
871
871
  | Export | Signature | Returns | Use it when |
872
872
  | ----------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
873
873
  | `LEAD_ANCHOR` | `const LEAD_ANCHOR` | — | The reserved anchor name for a journal's **first** page. |
874
- | `anchorsOf` | `function anchorsOf(entryUuid, entryId, body, name)` | {Record<string, string>} The anchors. | Every anchor of a note's journal, as `anchorName → Foundry target`. |
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
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
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
877
  | `foundryIdentities` | `function foundryIdentities(config` | `{{contentPackage: string, foundryPackageId: string, packRouter: object, web: boolean, skipDirectories: readonly string[]}}` | The identities an emission runs against, from configuration. |
@@ -881,24 +881,224 @@ A generated dictionary headword maps to its containing page's UUID followed
881
881
  by its word fragment. Other authored spans keep their containing-page
882
882
  mapping.
883
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.
1069
+
884
1070
  ### `engine.contentIndex`
885
1071
 
886
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.
887
1073
 
888
- | Export | Signature | Returns | Use it when |
889
- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
890
- | `noteAddress` | `function noteAddress(frontmatter, contentPackage)` | {{slug: string, canonical: string}\|null} `slug` is what goes inside `[[…]]` within this package; `canonical` is the fully qualified key the manifest files the note under, carrying the package and the system as well. | The address a wikilink writes to reach a note, or `null` when it has none. |
891
- | `sortKeysDeep` | `function sortKeysDeep(value)` | {unknown} The value with every plain object's keys in sorted order. | Recursively sort an object's keys, so serialization is order-independent. |
892
- | `asciiName` | `function asciiName(name)` | {string\|null} The ASCII form, or `null` when there is no name, or nothing printable survives. | A note's display name reduced to printable 7-bit ASCII. |
893
- | `asciiAliases` | `function asciiAliases(aliases)` | {Array<string>} Possibly empty, never null: a note with no aliases has an empty set of them, which is a fact rather than a missing value, and a consumer iterating it should not have to check first. | A note's `name.aliases` reduced to printable 7-bit ASCII, in order. |
894
- | `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
895
- | `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems, parsed })` | {object[]} One record per note, plus one per documentation entry. | The index records for a content tree, without writing anything. |
896
- | `indexRecordsForNote` | `indexRecordsForNote({ frontmatter, body, bodyLine, relPath, absPath, raw, contentPackage, manifest, addressContext })` | `object[]` | Derive the note's own record and its documentation record from parsed text. |
897
- | `prepareContentIndex` | `async prepareContentIndex({ contentBase, config } = {})` | `Promise<PreparedContentIndex>` — the captured source context and complete expanded bodies, described below | Prepare dynamic publication without writing files. |
898
- | `emitContentIndex` | `emitContentIndex({ contentBase, outDir, config, publication } = {})` | `{{file: string, notes: number, assets: number, records: number, bytes: number, full: number, draft: number, stub: number, dictionary: {file: string, rows: number}, terms: {file: string, rows: number}}}` | Synchronously publish metadata and the dictionary and terms datafiles; dynamic content requires a complete `publication`. |
899
- | `buildIndexRecord` | `buildIndexRecord({ frontmatter, relPath, absPath, raw, contentPackage, body, bodyLine, manifest })` | `Record<string, any>` — the record, with derived fields sorted deep | building one note's content-index record; refuses a note that authors a key the index derives itself (`package` among them) before building anything |
900
- | `collectContentIndex` | `collectContentIndex(contentBase, { contentPackage, skipDirectories, assetsBase, manifest, problems, foreignRecord, parsed })` | `Array<Record<string, any>>` | reading a whole content tree into index records, in a total order independent of directory-read order; an item note yields two records (its own and its documentation journal's), and each asset root yields one per file; each note carries its resolved culture as `data.culture` |
901
- | `resolveCultures` | `resolveCultures(records, { contentPackage, contentBase, foreignRecord, problems })` | `Array<Record<string, any>>` — the same records | writing each note's resolved culture into its record as `data.culture`: its own value, else its parents', else for a polity its seat's and governed places', pooled; parents that disagree give a finding into `problems`, or one thrown Error listing every conflict |
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 |
902
1102
 
903
1103
  `prepareContentIndex` resolves `contentBase` from the configured `paths.content`
904
1104
  when omitted. Its `PreparedContentIndex` contains:
@@ -911,6 +1111,7 @@ when omitted. Its `PreparedContentIndex` contains:
911
1111
  | `parsed` | A `Map` keyed by absolute note file, holding authored `frontmatter`, `body`, `raw`, `bodyLine` and `bodyColumn`. |
912
1112
  | `expandedBodies` | A `Map` keyed by absolute note file, holding `markdown`, its body-relative `lineMap` of `{line, generated}` entries, and optional `generatedFrom`. |
913
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. |
914
1115
 
915
1116
  Preparation expands SQL tables, page lists, automatic sections and scalar
916
1117
  expressions. Prose declarations in `expandedBodies` use each source's audience:
@@ -950,18 +1151,25 @@ scalar expressions, events or other generated sections require
950
1151
  `indexRecordsFor` and `collectContentIndex` collect authored records and pure
951
1152
  lexical sections; use preparation and emission to publish dynamic body targets.
952
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).
1160
+
953
1161
  `engine.contentIndex` also re-exports eight names from neighbouring leaf modules, at this same import path:
954
1162
 
955
- | Export | Signature | Returns | Use it when |
956
- | --------------------- | -------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
957
- | `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 |
958
- | `DERIVED_KEYS` | `const DERIVED_KEYS` | — | reading which keys the content index adds to a record, and which a note therefore may not author itself |
959
- | `noteFile` | `noteFile(contentBase, record)` | `string` — the note's absolute path | composing an index record's `file.path` (recorded relative, for a byte-stable artifact) back into an openable absolute path |
960
- | `authoredFrontmatter` | `authoredFrontmatter(record)` | `Record<string, any>` — the frontmatter without `DERIVED_KEYS` | reading a note's frontmatter back from its index record, its resolved `data.culture` included, so a pass can lint or compile from the index without reasoning about derived keys as if the author wrote them |
961
- | `isNoteRecord` | `isNoteRecord(record)` | `boolean` | telling a note's own record apart from its documentation journal's and from an asset's, when enumerating the corpus |
962
- | `isStub` | `isStub(record)` | `boolean` | telling a stub apart from a note that publishes a page; asked of the absent address, which is the whole signal |
963
- | `isAssetRecord` | `isAssetRecord(record)` | `boolean` | telling a record that addresses a file apart from one that addresses a note; the `asset` block is what marks it |
964
- | `recordPath` | `recordPath(record)` | `string` — the path the record names inside its package | ordering or reporting across both record shapes: a note names the `.md` it was parsed from, an asset the file it is |
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 |
965
1173
 
966
1174
  ### `engine.assetTypes`
967
1175
 
@@ -1242,13 +1450,20 @@ One authored pathname, and the four addresses it resolves to. A note names a fil
1242
1450
 
1243
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:
1244
1452
 
1245
- | Export | Signature | Returns | Use it when |
1246
- | -------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- |
1247
- | `anchorsOf` | `function anchorsOf(body)` | {Set<string>} The declared anchor slugs. | Every `{#anchor}` a note declares on a heading. |
1248
- | `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. |
1249
- | `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. |
1250
- | `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. |
1251
- | `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.
1252
1467
 
1253
1468
  ### `engine.webWikilinks`
1254
1469
 
@@ -1259,6 +1474,15 @@ Wikilink resolution for the knowledgebase build. The same authored links the pac
1259
1474
  | `frontmatterWikilinks` | `function frontmatterWikilinks(fm)` | {Array<{path: string, link: string}>} In reading order; `path` is the dotted key path of the offending value (`government.summary`, `aliases.1`). | Every wikilink authored inside a frontmatter value. |
1260
1475
  | `resolveWebWikilinks` | `function resolveWebWikilinks(body, ctx)` | {string} The body with wikilinks rewritten. | Rewrites the wikilinks in a markdown body as KB-local markdown links. |
1261
1476
 
1477
+ Both `resolveWebWikilinks` and Foundry's `convertWikilinks` derive reference
1478
+ spoilers from the resolved note audience and secret-anchor metadata. A trailing
1479
+ `{.show}` bypasses automatic spoiler wrapping, without changing destination
1480
+ access. A web destination excluded by audience retains its label without an
1481
+ href. Same-note secret anchors follow the same rule; a whole-note link to a
1482
+ public note does not inherit individual secret sections. See the
1483
+ [authoring contract](authoring/links-and-markup.md#references-to-secret-subjects)
1484
+ for surface presentation.
1485
+
1262
1486
  ### `engine.contentTables`
1263
1487
 
1264
1488
  Prepared SQL content tables are expanded into Markdown before link resolution.
@@ -1277,7 +1501,7 @@ Shared helpers for the pack compilers in `packages/package-build/`. The HeroicLa
1277
1501
  | ------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1278
1502
  | `md` | `const md` | — | The markdown renderer every surface shares. |
1279
1503
  | `renderFoundryMarkdown` | `function renderFoundryMarkdown(body)` | `string` — HTML with secret sections for GM passages. | Rendering a note body for a Foundry document. |
1280
- | `parseMarkdownFile` | `function parseMarkdownFile(filePath)` | — | Parses a markdown file with YAML frontmatter; returns frontmatter, body, rendered description, raw source, and body coordinates. |
1504
+ | `parseMarkdownFile` | `parseMarkdownFile(filePath, { addressContext, raw, renderDescription } = {})` | — | Parses a markdown file with YAML frontmatter; returns frontmatter, body, rendered description, raw source, and body coordinates. |
1281
1505
  | `assertStatedScope` | `function assertStatedScope(skipDirectories, who)` | {void} | Refuse a corpus read whose scope its caller did not state. |
1282
1506
  | `assertSuppliedCorpus` | `function assertSuppliedCorpus(records, who)` | {void} | Refuse a corpus read whose records its caller did not supply. |
1283
1507
  | `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. |
@@ -1294,13 +1518,41 @@ Shared helpers for the pack compilers in `packages/package-build/`. The HeroicLa
1294
1518
  | `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. |
1295
1519
  | `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. |
1296
1520
  | `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. |
1297
- | `walkMarkdownTree` | `walkMarkdownTree(rootDir, { skipDirectories })` | generator yielding `{frontmatter, body, description, raw, bodyLine?, bodyColumn?, file, absPath}` | walking a content tree and reading every markdown file's frontmatter and body; `skipDirectories` is required rather than defaulted, so two callers cannot silently disagree about the corpus's scope |
1521
+ | `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 |
1298
1522
 
1299
- `buildContentLinkIndex` accepts `{ config?, records, sqlTables? }`.
1523
+ `buildContentLinkIndex` accepts `{ config?, records, sqlTables?, foreign? }`.
1300
1524
  `records` is the required, supplied content corpus. The optional prepared
1301
1525
  `sqlTables` map supplies generated anchors and their Foundry page mappings,
1302
1526
  so glossary word links resolve to their dictionary entries.
1303
1527
 
1528
+ `parseMarkdownFile` reads its supplied exact `raw` string when present. Its
1529
+ `renderDescription` option defaults to true; false leaves `description` empty
1530
+ while retaining authored body and positions. `walkMarkdownTree` accepts a
1531
+ selected `sources` map in walk order and the same rendering option, also true
1532
+ by default. Index collection explicitly omits unused rendered descriptions.
1533
+ `indexRecordsFor` accepts that selected map, a `syntax` projection function
1534
+ and a captured `foreignIndex`. These adapters do not retain state globally.
1535
+
1536
+ The physical wrappers `lintContentCharset`, `lintContentHtml`,
1537
+ `lintContentIcons`, `lintContentImages` and `lintContentTaskLists` accept an
1538
+ optional `sources` map of absolute paths to `{raw}`. Their existing extension,
1539
+ hidden-path and skip filters still apply; without the map they walk and read
1540
+ the tree. `lintPathCharset` accepts optional physical `inventory`, retaining
1541
+ walk roots and order. `lintNoteStates` accepts the authored `parsed` map, with
1542
+ its existing file reader for missing entries.
1543
+ `parseMarkdownFile` accepts LF and CRLF frontmatter delimiters and retains the
1544
+ exact source as `raw`. Its trimmed `body` keeps the authored line endings, with
1545
+ `bodyLine` and `bodyColumn` locating its first character in that original source.
1546
+
1547
+ ### `./engine/diagnostics`
1548
+
1549
+ The `@heroiclands/package-build/engine/diagnostics` wildcard module provides
1550
+ source locations and frontmatter source slicing.
1551
+
1552
+ | Export | Signature | Returns | Use it when |
1553
+ | ------------------ | ----------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
1554
+ | `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. |
1555
+
1304
1556
  ### `engine.itemRegistry`
1305
1557
 
1306
1558
  **The resolved item-type registry** — the consuming repository's `itemBuilders` table, and the type whitelist derived from its keys. Both are read from the one resolved configuration, so they are literally the same object's keys and values: a type cannot be whitelisted for compilation without the builder that compiles it, which is the guarantee this exists for. Where a consumer declares **several** registries, one per system, the vocabulary is their union and every lookup below takes the system that is asking — a type both systems declare has two builders, and answering with one of them because it was declared first is the silent-wrong-output failure this package spends its time removing. The Item compiler dispatches through {@link itemBuilder}, so the table a consumer configured is the table its notes compile with — the whitelist and the dispatch would otherwise come from different places, and a consumer supplying its own registry got the types it asked for and the builders it did not.
@@ -1359,8 +1611,8 @@ What a `[[…]]` **is**, before anything decides where it points. One authored l
1359
1611
  | Export | Signature | Returns | Use it when |
1360
1612
  | -------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
1361
1613
  | `WIKILINK` | `const WIKILINK` | — | A wikilink, as authored. |
1362
- | `parseWikilink` | `function parseWikilink(rawInner)` | {ParsedWikilink} The parts, each trimmed. | Split a wikilink's interior into its parts. |
1363
- | `readWikilink` | `function readWikilink(inner, context)` | Typed target or literal target with diagnostic source. | Parse a link target using complete builder Address context. |
1614
+ | `parseWikilink` | `function parseWikilink(rawInner, literal?)` | {ParsedWikilink} The parts, each trimmed. | Split a wikilink's interior into its parts. |
1615
+ | `readWikilink` | `function readWikilink(inner, context, literal?)` | Typed target or literal target with diagnostic source. | Parse a link target using complete builder Address context. |
1364
1616
  | `authoredLabel` | `authoredLabel({ display })` | {string\|null} The label, or `null` when there is none to show. | The label an author actually supplied, or `null` when they supplied none. |
1365
1617
  | `unlabelledLinkMessage` | `function unlabelledLinkMessage(target)` | {string} The message, unpunctuated at the end as a finding is. | What an author writing an unlabelled link is told, in one place. |
1366
1618
  | `LINK_FINDING_REASONS` | `const LINK_FINDING_REASONS` | — | Every way a link can fail, named once for all three resolvers. |
@@ -1371,6 +1623,15 @@ What a `[[…]]` **is**, before anything decides where it points. One authored l
1371
1623
  | `isEventAnchor` | `isEventAnchor(hit, anchor)` | {boolean} Whether it is an event. | Whether the anchor a wikilink names is one of its target's events, which a wikilink may not name. |
1372
1624
  | `isSamePage` | `isSamePage({ target, anchor })` | {boolean} True when the link is same-page. | Whether a parsed link addresses a section of the page it is written on. |
1373
1625
 
1626
+ `parseWikilink` and `readWikilink` accept the complete matched `literal` to
1627
+ read a trailing attribute block into `attributes`; its `classes` include the
1628
+ explicit `.show` override.
1629
+
1630
+ | Export | Signature | Returns | Use it when |
1631
+ | ------------------- | ------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------- |
1632
+ | `secretDestination` | `secretDestination(destination, anchor?)` | `boolean` | Read the resolved note's GM audience or the referenced anchor's secret metadata. |
1633
+ | `presentWikilink` | `presentWikilink(reference, parsed, destination)` | `string` | Wrap a rendered reference as an inline spoiler unless the parsed link explicitly carries `.show`. |
1634
+
1374
1635
  ### `engine.siteIndex`
1375
1636
 
1376
1637
  **The address index a site build resolves its wikilinks against.**
@@ -1455,15 +1716,15 @@ The content tree, built into a book. The I/O half of the PDF surface: it reads t
1455
1716
 
1456
1717
  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.
1457
1718
 
1458
- | Export | Signature | Returns | Use it when |
1459
- | --------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
1460
- | `splitPages` | `function splitPages(body, leadName` | — | Splits a markdown body into pages by top-level H1 headings. |
1461
- | `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. |
1462
- | `assertUniqueAnchors` | `function assertUniqueAnchors(rawPages, noteName)` | — | The anchor half of {@link assertUniquePages}, under its former name. |
1463
- | `journalPageId` | `function journalPageId(entryId, page)` | {string} A 16-character Foundry id. | The id of one page within its entry. |
1464
- | `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. |
1465
- | `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. |
1466
- | `Journals` | `class Journals extends BasePackCompiler` | — | not called directly — imported and driven by `engine/generate.mjs` as the "journals" pack compiler |
1719
+ | Export | Signature | Returns | Use it when |
1720
+ | --------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
1721
+ | `splitPages` | `splitPages(body, leadName, resolveRole, syntax?)` | — | Splits a markdown body into pages by top-level H1 headings. |
1722
+ | `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. |
1723
+ | `assertUniqueAnchors` | `function assertUniqueAnchors(rawPages, noteName)` | — | The anchor half of {@link assertUniquePages}, under its former name. |
1724
+ | `journalPageId` | `function journalPageId(entryId, page)` | {string} A 16-character Foundry id. | The id of one page within its entry. |
1725
+ | `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. |
1726
+ | `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. |
1727
+ | `Journals` | `class Journals extends BasePackCompiler` | — | not called directly — imported and driven by `engine/generate.mjs` as the "journals" pack compiler |
1467
1728
 
1468
1729
  ### `engine.macros`
1469
1730
 
@@ -1536,28 +1797,28 @@ Adventure pack compiler — `type: bundle` notes → Foundry `Adventure` documen
1536
1797
 
1537
1798
  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.
1538
1799
 
1539
- | Export | Signature | Returns | Use it when |
1540
- | ----------------------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
1541
- | `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. |
1542
- | `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. |
1543
- | `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. |
1544
- | `packJsonDir` | `const packJsonDir` | {string} The pack's JSON directory. | Root of the build-only JSON tree for one pack. |
1545
- | `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. |
1546
- | `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. |
1547
- | `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. |
1548
- | `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. |
1549
- | `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. |
1550
- | `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). |
1800
+ | Export | Signature | Returns | Use it when |
1801
+ | ----------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
1802
+ | `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. |
1803
+ | `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. |
1804
+ | `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. |
1805
+ | `packJsonDir` | `const packJsonDir` | {string} The pack's JSON directory. | Root of the build-only JSON tree for one pack. |
1806
+ | `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. |
1807
+ | `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. |
1808
+ | `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. |
1809
+ | `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. |
1810
+ | `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. |
1811
+ | `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). |
1551
1812
 
1552
1813
  ### `engine.compendiums`
1553
1814
 
1554
1815
  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.
1555
1816
 
1556
- | Export | Signature | Returns | Use it when |
1557
- | -------------- | --------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
1558
- | `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. |
1559
- | `cleanPacks` | `async cleanPacks({ config, packDest, packName, entryName })` | — | Cleans and formats source JSON files, removing unnecessary permissions and flags and adding the proper spacing. |
1560
- | `unpackPacks` | `async unpackPacks({ packs, config, stageDest, packDest, packName })` | — | Extracts compiled LevelDB packs back to per-entry JSON, rebuilding the folder hierarchy as directories. |
1817
+ | Export | Signature | Returns | Use it when |
1818
+ | -------------- | ---------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
1819
+ | `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. |
1820
+ | `cleanPacks` | `async cleanPacks({ config, packDest, packName, entryName })` | — | Cleans and formats source JSON files, removing unnecessary permissions and flags and adding the proper spacing. |
1821
+ | `unpackPacks` | `async unpackPacks({ packs, config, stageDest, packDest, packName })` | — | Extracts compiled LevelDB packs back to per-entry JSON, rebuilding the folder hierarchy as directories. |
1561
1822
 
1562
1823
  ### `engine.regionEvents`
1563
1824
 
@@ -2104,6 +2365,15 @@ The Changesets changelog generator, loaded with `require` — a CommonJS module
2104
2365
  | `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 |
2105
2366
  | `getDependencyReleaseLine` | `getDependencyReleaseLine(changesets, dependenciesUpdated, changelogOpts)` | `string` — always `""` | satisfying the `ChangelogFunctions` interface; these packages narrate no internal dependency bumps |
2106
2367
 
2368
+ `engine/changelog-group.mjs` exports `groupChangelogText(text, { labels })`,
2369
+ returning `{ text, findings }`. It groups the newest release's summaries
2370
+ within each bump tier, retaining prose, bullets, wrapped continuations,
2371
+ nested item content and literal code. Merged labels' opening paragraphs
2372
+ combine in source order before their items. Exact duplicate items print
2373
+ once; labels follow the configured order, with unlabelled lead items first.
2374
+ Earlier release sections stay byte-identical, and grouping the result
2375
+ again leaves its text unchanged.
2376
+
2107
2377
  ## `./package.json`
2108
2378
 
2109
2379
  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.
@@ -2117,7 +2387,16 @@ The engine wildcard subpath also exposes the body parsers below. Their figure ex
2117
2387
  | `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. |
2118
2388
  | `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. |
2119
2389
  | `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. |
2120
- | `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. |
2390
+ | `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. |
2391
+
2392
+ Inline `.secret` spans preserve their inline content, name classes and
2393
+ attributes. Site rendering uses a separate show/hide button and content span,
2394
+ so a link inside the content does not become a nested control. Foundry renders
2395
+ visible inline text with a **Secret:** label, without native secret-section
2396
+ concealment; book rendering uses static secret styling and the same label.
2397
+ `collectAnchors` includes `secret: true` on secret anchors for destination
2398
+ presentation. Block and heading secrecy retain their separate scope and
2399
+ Foundry permission handling.
2121
2400
 
2122
2401
  `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"`.
2123
2402
 
@@ -2127,7 +2406,7 @@ The engine wildcard subpath also exposes the body parsers below. Their figure ex
2127
2406
  | ---------------- | ----------------------------------------------------------------------------------- |
2128
2407
  | `markupFindings` | Validate complete Markdown source and return diagnostics for the supplied filename. |
2129
2408
 
2130
- `markupFindings(source, file)` masks YAML frontmatter without moving source positions and returns block and alert scanner errors as `{file, line, column, severity, message}` diagnostics. Content lint, Markdown checks and formatting preflight share this validation; malformed files are not rewritten by formatting or Markdown fixes.
2409
+ `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.
2131
2410
 
2132
2411
  `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.
2133
2412
 
@@ -2140,6 +2419,22 @@ that map as `parsed` for its document passes. `unclaimedNoteFindings` accepts it
2140
2419
  alongside `records`. `locateFrontmatterKey` and `assertNoDeclaredPackage` accept
2141
2420
  `raw` in their options to locate findings without opening the note again.
2142
2421
 
2422
+ `buildCompileCorpus`, `generatePacksJson` and `compilePacks` accept an optional
2423
+ `session` bound to their resolved configuration, roots, scope and router.
2424
+ `prepareContentIndex` also accepts it. Without a session, the existing entry
2425
+ points derive their own inputs. Generation and pack compilation accept
2426
+ `reportPreparation` (default true); a coordinating caller may set it false
2427
+ only after reporting the shared source findings itself.
2428
+
2429
+ `buildLinkIndex` and `buildContentLinkIndex` accept an optional `foreign` view
2430
+ from the same session; their diagnostic and routed resolver contracts remain
2431
+ distinct. `collectAnchors`, `markupAnchorFindings`, `markupFindings`, journal
2432
+ `splitPages`, Foundry `anchorsOf` and `buildJournalEntry` accept the optional
2433
+ `syntax` function described above. `buildSiteIndex` and `prepareTreeSqlTables`
2434
+ accept it in options, and `insertLexicalSections` accepts it in context.
2435
+ Prepared per-note SQL results carry it to `expandContentTables`. No scan result
2436
+ is reused for a different source projection.
2437
+
2143
2438
  `prepareSqlTables` accepts sources with an optional `authoredSqlBlocks` count,
2144
2439
  set by `joinGeneratedSections`. Subsequent fences are generated event queries
2145
2440
  whose stub-inclusive results are identical; only authored fences receive a
@@ -2156,7 +2451,7 @@ unknown addresses. Only public records contribute rendered sources and links.
2156
2451
 
2157
2452
  `engine/sql-tables.mjs` exports `prepareLinkSqlTables(contentBase, options)`.
2158
2453
  Its options are `config`, `skipDirectories`, complete `records`, the shared
2159
- `parsed` map and `foreignIndex`. It returns prepared tables with `pageLists`
2454
+ `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`
2160
2455
  and `inline` maps, or `undefined` when no source needs a directive or automatic
2161
2456
  lexical section. Public
2162
2457
  sources use public rows; GM sources use all rows. Query results remain local