@heroiclands/package-build 22.14.2 → 22.14.3

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 (65) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/content-build.mjs +6 -6
  3. package/bin/package-build.mjs +10 -0
  4. package/config.mjs +18 -1
  5. package/content-config.mjs +2 -2
  6. package/docs/api.md +63 -35
  7. package/docs/configuration.md +16 -7
  8. package/docs/content-format.md +150 -181
  9. package/docs/getting-started.md +42 -11
  10. package/docs/project-setup.md +5 -6
  11. package/engine/address.mjs +3 -5
  12. package/engine/asset-bindings.mjs +66 -0
  13. package/engine/calendar-notes.mjs +74 -0
  14. package/engine/changelog-lint.mjs +2 -2
  15. package/engine/content-index.mjs +63 -36
  16. package/engine/content-lint.mjs +1 -2
  17. package/engine/content-package.mjs +2 -2
  18. package/engine/content-tables.mjs +4 -8
  19. package/engine/folder-notes.mjs +1 -2
  20. package/engine/foundry-entries.mjs +21 -6
  21. package/engine/frontmatter-lint.mjs +3 -4
  22. package/engine/frontmatter.mjs +5 -8
  23. package/engine/helpers.mjs +3 -4
  24. package/engine/index.mjs +3 -0
  25. package/engine/infobox.mjs +66 -2
  26. package/engine/item-registry.mjs +2 -2
  27. package/engine/note-vocabulary.mjs +83 -5
  28. package/engine/pack-config.mjs +2 -2
  29. package/engine/pack-router.mjs +2 -2
  30. package/engine/pdf-render.mjs +1 -1
  31. package/engine/pdf-toc.mjs +3 -3
  32. package/engine/retired-fields.mjs +1 -1
  33. package/engine/scene-levels.mjs +4 -4
  34. package/engine/schema-check.mjs +1 -1
  35. package/engine/site-build.mjs +134 -121
  36. package/engine/site-preview.mjs +293 -0
  37. package/engine/site-root.mjs +20 -2
  38. package/engine/social-tie-terms.mjs +11 -0
  39. package/engine/social-ties.mjs +84 -0
  40. package/engine/sql-tables.mjs +4 -9
  41. package/hm3/actors.mjs +1 -1
  42. package/package.json +3 -3
  43. package/sohl/item-fields.mjs +2 -2
  44. package/types/config.d.mts +8 -0
  45. package/types/content-config.d.mts +1 -1
  46. package/types/engine/address.d.mts +3 -5
  47. package/types/engine/asset-bindings.d.mts +6 -0
  48. package/types/engine/calendar-notes.d.mts +12 -0
  49. package/types/engine/content-index.d.mts +11 -0
  50. package/types/engine/content-package.d.mts +2 -2
  51. package/types/engine/folder-notes.d.mts +1 -2
  52. package/types/engine/frontmatter-lint.d.mts +1 -2
  53. package/types/engine/frontmatter.d.mts +2 -4
  54. package/types/engine/helpers.d.mts +3 -4
  55. package/types/engine/index.d.mts +1 -0
  56. package/types/engine/item-registry.d.mts +2 -2
  57. package/types/engine/note-vocabulary.d.mts +5 -6
  58. package/types/engine/pack-config.d.mts +1 -1
  59. package/types/engine/pack-router.d.mts +1 -1
  60. package/types/engine/pdf-render.d.mts +1 -1
  61. package/types/engine/pdf-toc.d.mts +1 -1
  62. package/types/engine/site-build.d.mts +36 -3
  63. package/types/engine/site-preview.d.mts +34 -0
  64. package/types/engine/social-tie-terms.d.mts +5 -0
  65. package/types/engine/social-ties.d.mts +10 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 22.14.3
4
+
5
+ ### Patch Changes
6
+
7
+ Editors can preview a content page from unsaved text, including its tables, links, images, and GM passages.
8
+
9
+ Setting orientation pages can be authored as setting guides, with the same links, tables, and GM passages as other guides.
10
+
11
+ **Content authoring**
12
+
13
+ - Beings can describe defining social ties, and place notes can identify their practical purpose.
14
+ - Calendar notes can set their own display formats, and being birth and death dates receive date validation.
15
+ - Structured place and affiliation relationships appear as linked rows in the infobox.
16
+
17
+ **Published output**
18
+
19
+ - Content indexes identify each system's Actor document separately when a note produces more than one.
20
+ - Alternative art builds report image names that have no matching note in the related package.
21
+ - Site search archives include every indexed page.
22
+
3
23
  ## 22.14.2
4
24
 
5
25
  ### Patch Changes
@@ -24,7 +24,7 @@
24
24
  *
25
25
  * The side effects that need *configuration* live inside the command handler,
26
26
  * not at module scope, so `--version` and `--help` answer in a directory that
27
- * has neither a `package-build.config.yaml` nor a package manifest (#2).
27
+ * has neither a `package-build.config.yaml` nor a package manifest.
28
28
  * Running an actual command still resolves both, and still fails loudly when
29
29
  * either is missing.
30
30
  *
@@ -158,11 +158,11 @@ import { loadMapWorld } from "../engine/map-places.mjs";
158
158
  * knows them. It does not come out of the shipped manifest — a second
159
159
  * declaration of the same list, in a second format, with nothing checking that
160
160
  * the two agreed. The manifest is generated from this list now
161
- * (package-build#9), so reading it back would be a round trip through an
161
+ *, so reading it back would be a round trip through an
162
162
  * artifact that need not exist.
163
163
  *
164
164
  * Read on demand rather than at load, so `--version` and `--help` still answer
165
- * with no configuration present (#2).
165
+ * with no configuration present.
166
166
  *
167
167
  * @returns {Array<{name: string}>}
168
168
  */
@@ -1979,8 +1979,8 @@ function reachabilityCommand() {
1979
1979
  */
1980
1980
  async function fetchFromLocalArtifact(config, argv) {
1981
1981
  // Two caches, two dependency sets: an index is fetched for every declared
1982
- // dependency, a catalogue only for those declaring `itemCatalog: true`
1983
- // (#239). `--from` fills whichever of them this dependency belongs to, so
1982
+ // dependency, a catalogue only for those declaring `itemCatalog: true`.
1983
+ // `--from` fills whichever of them this dependency belongs to, so
1984
1984
  // that testing against an unreleased build behaves like a release would.
1985
1985
  const rels = metadataRelationships(config);
1986
1986
  const named = rels.map((r) => r.id).join(", ") || "none";
@@ -2307,7 +2307,7 @@ function packageCommand() {
2307
2307
  // The one directory the pipeline creates rather than expects:
2308
2308
  // `unpack` writes the extracted JSON there and `compile` reads
2309
2309
  // it back. Created here rather than at module scope so that
2310
- // asking the CLI its version needs no configuration (#2).
2310
+ // asking the CLI its version needs no configuration.
2311
2311
  fs.mkdirSync(config.paths.unpack, {
2312
2312
  recursive: true,
2313
2313
  });
@@ -80,6 +80,8 @@ import yargs from "yargs";
80
80
  import { hideBin } from "yargs/helpers";
81
81
 
82
82
  import { loadPackageBuildConfig } from "../config.mjs";
83
+ import { checkForeignAssetBindings } from "../engine/asset-bindings.mjs";
84
+ import { formatDiagnostic } from "../engine/diagnostics.mjs";
83
85
  import { writeSiteRoot } from "../engine/site-root.mjs";
84
86
  import { DEPLOY_ROOT } from "../engine/site-config.mjs";
85
87
  import { compilesFoundryDocuments } from "../content-config.mjs";
@@ -271,6 +273,14 @@ function assetsCommand() {
271
273
  console.log("package-build: no `packageBuild.assets` declared; nothing to stage.");
272
274
  return;
273
275
  }
276
+ const bindings = checkForeignAssetBindings(loadPackConfig(), {
277
+ assets: config.assets,
278
+ });
279
+ if (bindings.length) {
280
+ const error = new Error(bindings.map(formatDiagnostic).join("\n"));
281
+ error.located = true;
282
+ die(error);
283
+ }
274
284
 
275
285
  let transform;
276
286
  if (config.assetTransform) {
package/config.mjs CHANGED
@@ -100,7 +100,7 @@ export const DERIVED_MANIFEST_KEYS = Object.freeze({
100
100
  relationships: "the top level of package-build.config.yaml",
101
101
  packs: "the `packs` list at the top level of package-build.config.yaml",
102
102
  });
103
- const ASSET_KEYS = ["from", "to"];
103
+ const ASSET_KEYS = ["from", "to", "bindsTo"];
104
104
  const CLEAN_KEYS = ["extra"];
105
105
  const LANG_KEYS = [
106
106
  "sources",
@@ -210,6 +210,8 @@ function requireNonEmptyString(value, where) {
210
210
  * @typedef {object} AssetSpec
211
211
  * @property {string} from Source path, relative to the repository root.
212
212
  * @property {string} to Destination, relative to the staged package root.
213
+ * @property {{package: string, system: string, type: string}|undefined} [bindsTo]
214
+ * The foreign Address each file's basename claims, without its shortcode.
213
215
  */
214
216
 
215
217
  /**
@@ -222,9 +224,24 @@ function normalizeAsset(value, index) {
222
224
  if (!isMapping(value)) fail(where, "must be a mapping");
223
225
  const asset = /** @type {Record<string, unknown>} */ (value);
224
226
  rejectUnknownKeys(asset, ASSET_KEYS, `${where}.`);
227
+ let bindsTo;
228
+ if (asset.bindsTo !== undefined) {
229
+ if (!isMapping(asset.bindsTo)) fail(`${where}.bindsTo`, "must be a mapping");
230
+ const binding = /** @type {Record<string, unknown>} */ (asset.bindsTo);
231
+ rejectUnknownKeys(binding, ["package", "system", "type"], `${where}.bindsTo.`);
232
+ bindsTo = Object.freeze({
233
+ package: requireNonEmptyString(binding.package, `${where}.bindsTo.package`),
234
+ system:
235
+ binding.system === undefined ?
236
+ "note"
237
+ : requireNonEmptyString(binding.system, `${where}.bindsTo.system`),
238
+ type: requireNonEmptyString(binding.type, `${where}.bindsTo.type`),
239
+ });
240
+ }
225
241
  return Object.freeze({
226
242
  from: requireNonEmptyString(asset.from, `${where}.from`),
227
243
  to: requireNonEmptyString(asset.to, `${where}.to`),
244
+ ...(bindsTo ? { bindsTo } : {}),
228
245
  });
229
246
  }
230
247
 
@@ -688,7 +688,7 @@ export function publishesContentPages(config) {
688
688
  * @property {Readonly<Record<string, string>>} itemArt Derived: the default art
689
689
  * of each entry that paired one. Sparse — a
690
690
  * type absent here has no default, and a note
691
- * of it must carry `img:` (#7).
691
+ * of it must carry `img:`.
692
692
  * @property {Readonly<Record<string, readonly object[]>>} itemFields Derived:
693
693
  * the frontmatter fields each entry
694
694
  * declared. Sparse, like `itemArt` — a type
@@ -2202,7 +2202,7 @@ function normalizePackageBuild(value) {
2202
2202
  * travel by different routes: `itemTypes` was derived from these keys, while
2203
2203
  * art was looked up in `sohl/default-item-art.mjs` — a table a consumer cannot
2204
2204
  * add to. A consumer's own item type was therefore configurable while its
2205
- * default art was not, so its notes all had to carry an explicit `img:` (#7).
2205
+ * default art was not, so its notes all had to carry an explicit `img:`.
2206
2206
  * Art now travels with the builder it belongs to, which is the one place a type
2207
2207
  * is already declared.
2208
2208
  *
package/docs/api.md CHANGED
@@ -5,15 +5,11 @@ subpath entries, declared in `package.json`'s `exports` map. Every function
5
5
  and value documented here is a supported contract, not an internal seam —
6
6
  `./sohl` and `./hm3` included.
7
7
 
8
- **Everything exported anywhere in this package is pure.** A function takes
9
- source text or already-loaded data and returns findings or values;
10
- discovery, I/O and reporting stay with the caller. That is what lets one
11
- rule set serve a lint script, a build step and a unit test without any of
12
- them having to agree on how files are found or how findings are printed. A
13
- handful of packaging functions — deploying, staging, running a container,
14
- packing a release — necessarily touch the filesystem or a subprocess; each
15
- is named for it in the tables below, and the pure half of its module is kept
16
- separate from it.
8
+ Rule functions take source text or already-loaded data and return findings or
9
+ values. That lets one rule set serve a lint script, a build step and a unit
10
+ test without tying diagnostics to a particular reporter. Context builders,
11
+ content walks, site rendering, and packaging functions read or write files as
12
+ their signatures and descriptions state.
17
13
 
18
14
  This reference is organized by subpath entry, matching how a consumer
19
15
  imports: `import * as engine from "@heroiclands/package-build/engine"` reads
@@ -640,19 +636,22 @@ A date on a note is one authored string — `[~][-]YYYY[/MM[/DD]] [<calendar sho
640
636
 
641
637
  A calendar is a `lore` note with `subType: calendar`, declaring the months it keeps, the weekdays it names, the seasons it marks and the eras it counts years in. How long the year is, how the day divides and how the moon moves are facts about the **world**, written on its `place` note and its moon's, and no calendar may restate them — which is why the months summing to the world's year is the only arithmetic a calendar note is held to. One definition is emitted per calendar, in array shape: Foundry core prunes the keys outside its schema, and Calendaria converts the arrays to its own keyed collections.
642
638
 
643
- | Export | Signature | Returns | Use it when |
644
- | -------------------- | ----------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------- |
645
- | `CALENDAR_SUBTYPE` | `const CALENDAR_SUBTYPE` | — | naming the `lore` subType whose notes are calendars |
646
- | `INVARIANT_SUBTYPES` | `const INVARIANT_SUBTYPES` | — | naming the `place` subTypes a world fact may be written on |
647
- | `CALENDAR_FIELDS` | `const CALENDAR_FIELDS` | — | reading the closed list of what a calendar note may declare |
648
- | `INVARIANT_FIELDS` | `const INVARIANT_FIELDS` | — | reading the closed list of what a body may declare about itself |
649
- | `checkCalendarNote` | `checkCalendarNote(note, opts)` | `object[]` — findings | holding a `lore` note to the family and to the year the world keeps |
650
- | `checkWorldFacts` | `checkWorldFacts(note, opts)` | `object[]` — findings | holding a `place` note's world facts to one body, one year and one moon |
651
- | `worldInvariants` | `worldInvariants(index)` | `{year, present, moon, body, moonName, …}` | reading the year, the present and the moon a package's own notes state |
652
- | `quarterDays` | `quarterDays(daysPerYear)` | `number[]` — four days of the year | asking which days the year turns on |
653
- | `compileCalendar` | `compileCalendar({note, invariants, contentPackage})` | `object` — the definition | building the one object both a core consumer and Calendaria read |
654
- | `compileCalendars` | `compileCalendars(index, opts)` | `{calendars, invariants}` | building every calendar a package declares, or none where it states no year |
655
- | `calendariaEnvelope` | `calendariaEnvelope(definition, opts)` | `object` — the import envelope | wrapping a definition for a hand import through Calendaria's settings |
639
+ | Export | Signature | Returns | Use it when |
640
+ | --------------------------- | ----------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------- |
641
+ | `CALENDAR_SUBTYPE` | `const CALENDAR_SUBTYPE` | — | naming the `lore` subType whose notes are calendars |
642
+ | `INVARIANT_SUBTYPES` | `const INVARIANT_SUBTYPES` | — | naming the `place` subTypes a world fact may be written on |
643
+ | `CALENDAR_FIELDS` | `const CALENDAR_FIELDS` | — | reading the closed list of what a calendar note may declare |
644
+ | `CALENDAR_DATE_FORMAT_KEYS` | `const CALENDAR_DATE_FORMAT_KEYS` | — | reading the supported Calendaria display slots |
645
+ | `CALENDAR_FORMAT_TOKENS` | `const CALENDAR_FORMAT_TOKENS` | — | checking bare date tokens in a format string |
646
+ | `INVARIANT_FIELDS` | `const INVARIANT_FIELDS` | — | reading the closed list of what a body may declare about itself |
647
+ | `checkCalendarNote` | `checkCalendarNote(note, opts)` | `object[]` — findings | holding a `lore` note to the family and to the year the world keeps |
648
+ | `checkCalendarDateFormats` | `checkCalendarDateFormats(note)` | `object[]` — findings | checking display slots and escaped literal words |
649
+ | `checkWorldFacts` | `checkWorldFacts(note, opts)` | `object[]` — findings | holding a `place` note's world facts to one body, one year and one moon |
650
+ | `worldInvariants` | `worldInvariants(index)` | `{year, present, moon, body, moonName, …}` | reading the year, the present and the moon a package's own notes state |
651
+ | `quarterDays` | `quarterDays(daysPerYear)` | `number[]` — four days of the year | asking which days the year turns on |
652
+ | `compileCalendar` | `compileCalendar({note, invariants, contentPackage})` | `object` — the definition | building the one object both a core consumer and Calendaria read |
653
+ | `compileCalendars` | `compileCalendars(index, opts)` | `{calendars, invariants}` | building every calendar a package declares, or none where it states no year |
654
+ | `calendariaEnvelope` | `calendariaEnvelope(definition, opts)` | `object` — the import envelope | wrapping a definition for a hand import through Calendaria's settings |
656
655
 
657
656
  ### `engine.beingAge`
658
657
 
@@ -769,17 +768,18 @@ Emitting this package's cross-package link manifest. `engine/content-address.mjs
769
768
 
770
769
  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.
771
770
 
772
- | Export | Signature | Returns | Use it when |
773
- | ----------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
774
- | `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. |
775
- | `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. |
776
- | `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. |
777
- | `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. |
778
- | `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
779
- | `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems })` | {object[]} One record per note, plus one per documentation entry. | The index records for a content tree, without writing anything. |
780
- | `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. |
781
- | `buildIndexRecord` | `buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest })` | `Record<string, any>` — the record, with derived fields sorted deep | building one note's content-index record; refuses a note that authors a key the index derives itself (`package` among them) before building anything |
782
- | `collectContentIndex` | `collectContentIndex(contentBase, { contentPackage, skipDirectories, assetsBase, manifest, problems })` | `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 |
771
+ | Export | Signature | Returns | Use it when |
772
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
773
+ | `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. |
774
+ | `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. |
775
+ | `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. |
776
+ | `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. |
777
+ | `serializeContentIndex` | `function serializeContentIndex(records)` | {string} One compact JSON object per line, newline-terminated. | Serialize records as JSON Lines. |
778
+ | `indexRecordsFor` | `indexRecordsFor({ contentBase, config, skipDirectories, problems })` | {object[]} One record per note, plus one per documentation entry. | The index records for a content tree, without writing anything. |
779
+ | `indexRecordsForNote` | `indexRecordsForNote({ frontmatter, body, bodyLine, relPath, absPath, contentPackage, manifest, addressContext })` | `object[]` | Derive the note's own record and its documentation record from parsed text. |
780
+ | `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. |
781
+ | `buildIndexRecord` | `buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest })` | `Record<string, any>` — the record, with derived fields sorted deep | building one note's content-index record; refuses a note that authors a key the index derives itself (`package` among them) before building anything |
782
+ | `collectContentIndex` | `collectContentIndex(contentBase, { contentPackage, skipDirectories, assetsBase, manifest, problems })` | `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 |
783
783
 
784
784
  `engine.contentIndex` also re-exports eight names from neighbouring leaf modules, at this same import path:
785
785
 
@@ -860,10 +860,38 @@ Publishing a content tree as a website. Compiling a content tree into compendium
860
860
  | `tableUniverse` | `function tableUniverse(pages)` | {Map<string, object[]>} Package → the notes it may tabulate. | The universe a generated table searches, grouped by package. |
861
861
  | `pageFrontmatter` | `function pageFrontmatter(page,` | {object} The frontmatter to write. | The frontmatter a page publishes with. |
862
862
  | `pageDestination` | `function pageDestination(page, { bundle })` | — | Where a page is written, relative to the output root: flat, or as a leaf bundle when it carries a file beside itself. |
863
+ | `renderSitePage` | `renderSitePage(page, options)` | `{body, data, resolved, tableErrors, secretErrors, wikiErrors, imageErrors}` | Transform one page with the same passes the site writer uses. |
863
864
  | `renderPages` | `function renderPages(pages, options)` | {{written: number, byKind: Record<string, number>, tableErrors: object[], wikiErrors: object[]}} | Renders and writes every page. |
864
865
  | `resolveSitePass` | `function resolveSitePass(name, options)` | {{beforeLinks?: Function}} The bundle. | Resolves `site.pass` to its bundle. |
866
+ | `sitePageDecorator` | `sitePageDecorator(config, index)` | Function | Add the site's infobox and being metadata to a rendered page. |
865
867
  | `buildSite` | `buildSite({ config, sqlTables, locate })` | {{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. |
866
868
 
869
+ ### `engine.sitePreview`
870
+
871
+ `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.
872
+
873
+ | Export | Signature | Returns | Use it when |
874
+ | -------------------- | -------------------------------- | ------------------------------------------ | -------------------------------------------------- |
875
+ | `prepareSitePreview` | `prepareSitePreview({ config })` | `{render(file, text), refresh(), close()}` | An editor needs a private, reusable page renderer. |
876
+
877
+ The context keeps other notes and foreign packages at their saved state. Call `refresh()` after a save to read them again, and `close()` when the editor releases the project. Rendering and refreshing never write the authored notes or the site's `build/hugo/content` mount. The editor owns any private HTML artifact and keeps its last complete artifact when a render fails.
878
+
879
+ ```js
880
+ import { sitePreview } from "@heroiclands/package-build/engine";
881
+
882
+ const preview = await sitePreview.prepareSitePreview({ config });
883
+ try {
884
+ const result = await preview.render(absoluteFile, currentBufferText);
885
+ if (result.ok) {
886
+ // Convert result.markdown and result.frontmatter to the editor's HTML preview.
887
+ } else {
888
+ showDiagnostics(result.findings);
889
+ }
890
+ } finally {
891
+ await preview.close();
892
+ }
893
+ ```
894
+
867
895
  ### `engine.contentLint`
868
896
 
869
897
  Linting a content tree's **addresses** — the rules every package's notes are authored against, wherever those notes live. These rules do not live in a consumer's `utils/`, which has two consequences and no upside. `thalorna` and `kethira` notes were checked by nothing at all, so the packages most likely to carry authoring mistakes were the ones nothing inspected. And one rule with two implementations can disagree without anything detecting it, which the canonical-separator handling already did once on each side.
@@ -996,7 +1024,7 @@ Wikilink resolution for the knowledgebase build. The same authored links the pac
996
1024
 
997
1025
  ### `engine.contentTables`
998
1026
 
999
- Generated content tables — Obsidian **Dataview** `TABLE` queries. A catalog table (every cloth armour, every animal's abilities) is data that already lives in the frontmatter of the notes it describes. Authoring such a table by hand duplicates that data and guarantees drift, so a content body instead declares what it wants tabulated, in a fenced `dataview` block:
1027
+ Generated content tables from fenced `dataview` `TABLE` queries. A catalog table (every cloth armour, every animal's abilities) is data that already lives in the frontmatter of the notes it describes. Authoring such a table by hand duplicates that data and guarantees drift, so a content body instead declares what it wants tabulated, in a fenced `dataview` block:
1000
1028
 
1001
1029
  | Export | Signature | Returns | Use it when |
1002
1030
  | --------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -1116,7 +1144,7 @@ What a `[[…]]` **is**, before anything decides where it points. One authored l
1116
1144
 
1117
1145
  ### `engine.pdfToc`
1118
1146
 
1119
- The document tree a PDF is built from, and the plan it resolves to (#316). The packs and the website both render the _whole_ content tree: every note becomes a document and a page, and the three surfaces agreeing about what the content is, is the point. **A book is not that.** It is a selection — a declared structure whose leaves pick notes out of the corpus by a `WHERE` clause, interleaved with prose that may not live in the content tree at all.
1147
+ The document tree a PDF is built from, and the plan it resolves to. The packs and the website both render the _whole_ content tree: every note becomes a document and a page, and the three surfaces agreeing about what the content is, is the point. **A book is not that.** It is a selection — a declared structure whose leaves pick notes out of the corpus by a `WHERE` clause, interleaved with prose that may not live in the content tree at all.
1120
1148
 
1121
1149
  | Export | Signature | Returns | Use it when |
1122
1150
  | ------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
@@ -529,8 +529,8 @@ Any other key is refused:
529
529
 
530
530
  **Type:** string[] · **Optional** · default `[]`.
531
531
 
532
- Directory names the content walk ignores wherever they appear — Obsidian's
533
- `Templates`, for instance.
532
+ Directory names the content walk ignores wherever they appear — an author's
533
+ `Templates` directory, for instance.
534
534
 
535
535
  ```yaml
536
536
  skipDirectories: [Templates]
@@ -1465,10 +1465,11 @@ table says `lang`, not `build/stage/lang`.
1465
1465
  `assets` is the table of staging copies: source path in the repository,
1466
1466
  destination under the staged package root.
1467
1467
 
1468
- | Key | Type | Required |
1469
- | ---------------------------- | ------ | -------- |
1470
- | `packageBuild.assets[].from` | string | yes |
1471
- | `packageBuild.assets[].to` | string | yes |
1468
+ | Key | Type | Required |
1469
+ | ------------------------------- | -------------------------- | -------- |
1470
+ | `packageBuild.assets[].from` | string | yes |
1471
+ | `packageBuild.assets[].to` | string | yes |
1472
+ | `packageBuild.assets[].bindsTo` | `{package, type, system?}` | no |
1472
1473
 
1473
1474
  > ``package-build config: `packageBuild.stageDir` must be a non-empty string.``
1474
1475
 
@@ -1478,7 +1479,15 @@ destination under the staged package root.
1478
1479
 
1479
1480
  > ``package-build config: `packageBuild.assets[<index>].from` must be a non-empty string.``
1480
1481
 
1481
- > ``package-build config: `packageBuild.assets[<index>].<key>` is not a recognised key (expected one of: from, to).``
1482
+ > ``package-build config: `packageBuild.assets[<index>].<key>` is not a recognised key (expected one of: from, to, bindsTo).``
1483
+
1484
+ `bindsTo` says that every file under this copy's `from` directory uses its
1485
+ filename, without the extension, as a foreign Address shortcode. `package`
1486
+ and `type` are required; `system` defaults to `note`. The relationship with
1487
+ that package must provide a content index for the build to check the binding.
1488
+ The check reads the cached index, reports an unresolved file as an error, and
1489
+ does not fetch during compilation. Run `content-build deps fetch` to fill a
1490
+ cold cache.
1482
1491
 
1483
1492
  ### `packageBuild.assetTransform` and `packageBuild.manifestFlags`
1484
1493