@heroiclands/package-build 20.4.0 → 20.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +288 -0
  2. package/CONTENT.md +213 -20
  3. package/README.md +19 -1
  4. package/bin/content-build.mjs +135 -32
  5. package/bin/package-build.mjs +46 -13
  6. package/content-config.mjs +345 -101
  7. package/docs/api.md +1352 -0
  8. package/docs/commands.md +1609 -0
  9. package/docs/configuration.md +1432 -0
  10. package/docs/content-format.md +16 -6
  11. package/docs/diagnostics.md +356 -0
  12. package/docs/getting-started.md +813 -0
  13. package/docs/project-setup.md +469 -0
  14. package/engine/actor-compiler.mjs +30 -27
  15. package/engine/address-diff.mjs +45 -41
  16. package/engine/base-compiler.mjs +6 -0
  17. package/engine/bundles.mjs +9 -0
  18. package/engine/content-address.mjs +9 -9
  19. package/engine/content-index.mjs +44 -23
  20. package/engine/content-links.mjs +44 -11
  21. package/engine/content-lint.mjs +44 -10
  22. package/engine/content-tables.mjs +32 -27
  23. package/engine/folder-notes.mjs +4 -2
  24. package/engine/frontmatter-lint.mjs +35 -38
  25. package/engine/generate.mjs +5 -0
  26. package/engine/helpers.mjs +86 -32
  27. package/engine/index.mjs +12 -2
  28. package/engine/journals.mjs +9 -0
  29. package/engine/note-claims.mjs +18 -10
  30. package/engine/note-schemas.mjs +0 -5
  31. package/engine/note-vocabulary.mjs +32 -31
  32. package/engine/pack-config.mjs +26 -12
  33. package/engine/pack-router.mjs +0 -0
  34. package/engine/pdf-build.mjs +464 -0
  35. package/engine/pdf-fonts.mjs +420 -0
  36. package/engine/pdf-render.mjs +876 -0
  37. package/engine/pdf-toc.mjs +525 -0
  38. package/engine/scenes.mjs +14 -5
  39. package/engine/schema-check.mjs +1 -1
  40. package/engine/site-build.mjs +21 -3
  41. package/engine/web-wikilinks.mjs +6 -3
  42. package/engine/wikilinks.mjs +2 -4
  43. package/hm3/actors.mjs +8 -0
  44. package/hm3/items.mjs +8 -0
  45. package/package.json +1 -1
  46. package/release.mjs +63 -3
  47. package/sohl/actors.mjs +8 -0
  48. package/sohl/items.mjs +8 -0
  49. package/sohl/note-schemas.mjs +5 -5
  50. package/types/content-config.d.mts +66 -15
  51. package/types/engine/actor-compiler.d.mts +34 -30
  52. package/types/engine/address-diff.d.mts +57 -3
  53. package/types/engine/base-compiler.d.mts +10 -2
  54. package/types/engine/bundles.d.mts +9 -0
  55. package/types/engine/content-address.d.mts +9 -9
  56. package/types/engine/content-index.d.mts +57 -13
  57. package/types/engine/content-lint.d.mts +6 -4
  58. package/types/engine/content-tables.d.mts +49 -18
  59. package/types/engine/frontmatter-lint.d.mts +3 -2
  60. package/types/engine/helpers.d.mts +105 -31
  61. package/types/engine/index.d.mts +4 -0
  62. package/types/engine/journals.d.mts +9 -0
  63. package/types/engine/note-claims.d.mts +17 -10
  64. package/types/engine/note-vocabulary.d.mts +23 -196
  65. package/types/engine/pack-config.d.mts +4 -4
  66. package/types/engine/pdf-build.d.mts +42 -0
  67. package/types/engine/pdf-fonts.d.mts +30 -0
  68. package/types/engine/pdf-render.d.mts +156 -0
  69. package/types/engine/pdf-toc.d.mts +114 -0
  70. package/types/engine/scenes.d.mts +10 -1
  71. package/types/engine/schema-check.d.mts +2 -2
  72. package/types/engine/site-build.d.mts +34 -6
  73. package/types/engine/wikilinks.d.mts +2 -3
  74. package/types/hm3/actors.d.mts +8 -0
  75. package/types/hm3/items.d.mts +8 -0
  76. package/types/release.d.mts +15 -4
  77. package/types/sohl/actors.d.mts +10 -2
  78. package/types/sohl/items.d.mts +8 -0
package/hm3/actors.mjs CHANGED
@@ -189,6 +189,14 @@ function defaultActorImg(subType) {
189
189
  return img;
190
190
  }
191
191
 
192
+ /**
193
+ * HM3's Actor compile pass.
194
+ *
195
+ * Declares HM3's note-type → document-subtype map and builds a `being` note's
196
+ * document: the subtype the note authors in `hm3.type`, the mapped fields and
197
+ * the two anchored prose sections, and the embedded items `hm3.items` names.
198
+ * Everything else is {@link module:engine/actor-compiler}'s.
199
+ */
192
200
  export class Hm3Actors extends SystemActorCompiler {
193
201
  /**
194
202
  * HM3's note-type → document-subtype map — the one declaration that says
package/hm3/items.mjs CHANGED
@@ -54,6 +54,14 @@ import { SystemItemCompiler } from "../engine/item-compiler.mjs";
54
54
  import { HM3_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
55
55
  import { templateFlags } from "./template-priority.mjs";
56
56
 
57
+ /**
58
+ * HM3's Item compile pass.
59
+ *
60
+ * Declares HM3's note-type → document-subtype map, which decides the notes this
61
+ * pass claims and what each becomes, the one `system` key HM3 writes from prose,
62
+ * and the template-priority flag HM3's data model has no field for. Everything
63
+ * else is {@link module:engine/item-compiler}'s.
64
+ */
57
65
  export class Hm3Items extends SystemItemCompiler {
58
66
  /**
59
67
  * HM3's note-type → document-subtype map — the one declaration that says
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "20.4.0",
3
+ "version": "20.6.0",
4
4
  "description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
package/release.mjs CHANGED
@@ -57,10 +57,17 @@ import { ZipArchive } from "archiver";
57
57
  * Determines both asset names.
58
58
  * @param {string} [opts.metadataDir] - Where the build writes its content
59
59
  * index, consulted when the advertised file was not staged.
60
+ * @param {boolean} [opts.pdf] - Whether to build the book that ships beside the
61
+ * archive. `true` by default; `false` skips the build and reports the skip.
60
62
  * @returns {Promise<{zip: string, manifest: string, metadata?: string,
61
- * bytes: number, version: string}>} The paths written, the archive's size,
62
- * and the version the manifest declares. `metadata` is absent when the
63
- * manifest advertises no content index.
63
+ * pdf?: string, pdfFindings: object[], pdfSkipped: string|null,
64
+ * bytes: number, version: string}>} The paths written, what the book build
65
+ * found, the archive's size, and the version the manifest declares.
66
+ * `metadata` is absent when the manifest advertises no content index, and
67
+ * `pdf` is absent when no book was written. `pdfSkipped` is `null` when a
68
+ * book was built, and otherwise the reason none was — itself `null` when the
69
+ * book builder could not be loaded, which is reported through
70
+ * `pdfFindings`.
64
71
  * @throws {Error} When the stage has no manifest — there is nothing to release,
65
72
  * and an archive without one installs as nothing.
66
73
  */
@@ -69,6 +76,7 @@ export async function packRelease({
69
76
  outDir = "build/dist",
70
77
  artifact = "system",
71
78
  metadataDir = "build/content-index",
79
+ pdf = true,
72
80
  } = {}) {
73
81
  const stage = path.resolve(stageDir);
74
82
  const out = path.resolve(outDir);
@@ -112,15 +120,67 @@ export async function packRelease({
112
120
 
113
121
  const metadata = await publishMetadataIndex({ manifest, stage, out, metadataDir });
114
122
 
123
+ // Last, and never fatal: the archive and the manifest are the release, and
124
+ // a book that failed to set is a reported problem rather than a reason to
125
+ // publish neither.
126
+ const book =
127
+ pdf ?
128
+ await packReleasePdf({ out, version: manifest.version })
129
+ : { pdf: null, findings: [], reason: "the release was asked not to build one" };
130
+
115
131
  return {
116
132
  zip: zipPath,
117
133
  manifest: path.join(out, manifestName),
118
134
  ...(metadata ? { metadata } : {}),
135
+ ...(book.pdf ? { pdf: book.pdf } : {}),
136
+ pdfFindings: book.findings,
137
+ pdfSkipped: book.pdf ? null : book.reason,
119
138
  bytes: archive.pointer(),
120
139
  version: manifest.version,
121
140
  };
122
141
  }
123
142
 
143
+ /**
144
+ * Build the book that ships beside the archive.
145
+ *
146
+ * **Imported when it is used, not when this module is.** The book build pulls
147
+ * in a markdown parser, DuckDB and the whole content engine; `release.mjs`
148
+ * otherwise exists to zip a directory, and every consumer that publishes no
149
+ * book would pay for that graph on `import`. A dynamic import inside the one
150
+ * function that needs it keeps the cost where the benefit is.
151
+ *
152
+ * **Not building is the normal case and never an error.** A package publishing
153
+ * only a homepage, one with no `pdf:` block and one with no content tree have
154
+ * each said they publish no book. Four of the six packages that install this
155
+ * toolchain are in exactly that position, so a release that failed for the
156
+ * absence of a PDF would break more releases than it helped.
157
+ *
158
+ * @param {object} opts - Options.
159
+ * @param {string} opts.out - The release directory.
160
+ * @param {string} opts.version - The version the manifest declares.
161
+ * @returns {Promise<{pdf: string|null, findings: object[], reason: string|null}>}
162
+ * What was built, and what was found on the way.
163
+ */
164
+ async function packReleasePdf({ out, version }) {
165
+ let buildPdf;
166
+ try {
167
+ ({ buildPdf } = await import("./engine/pdf-build.mjs"));
168
+ } catch (err) {
169
+ return {
170
+ pdf: null,
171
+ findings: [
172
+ {
173
+ severity: "warning",
174
+ message: `the book builder could not be loaded: ${err.message}`,
175
+ },
176
+ ],
177
+ reason: null,
178
+ };
179
+ }
180
+ const result = await buildPdf({ out, version });
181
+ return { pdf: result.pdf, findings: result.findings, reason: result.reason };
182
+ }
183
+
124
184
  /**
125
185
  * Place the content index the manifest advertises beside the archive.
126
186
  *
package/sohl/actors.mjs CHANGED
@@ -190,6 +190,14 @@ function extractBodyAndMovement(fm) {
190
190
  };
191
191
  }
192
192
 
193
+ /**
194
+ * SoHL's Actor compile pass.
195
+ *
196
+ * Declares SoHL's note-type → document-subtype map and builds a `being` note's
197
+ * document: the body structure and its movement profiles, the embedded
198
+ * attribute and item documents the frontmatter names, and the `system` block.
199
+ * Everything else is {@link module:engine/actor-compiler}'s.
200
+ */
193
201
  export class Actors extends SystemActorCompiler {
194
202
  /**
195
203
  * SoHL's note-type → document-subtype map — the one declaration that says
package/sohl/items.mjs CHANGED
@@ -41,6 +41,14 @@ import { systemTemplatePriority } from "../engine/helpers.mjs";
41
41
  import { SystemItemCompiler } from "../engine/item-compiler.mjs";
42
42
  import { SOHL_DOCUMENT_SUBTYPES } from "./document-subtypes.mjs";
43
43
 
44
+ /**
45
+ * SoHL's Item compile pass.
46
+ *
47
+ * Declares SoHL's note-type → document-subtype map, which decides the notes
48
+ * this pass claims and what each becomes, and the `system` keys SoHL writes on
49
+ * every item whatever its type. Everything else is
50
+ * {@link module:engine/item-compiler}'s.
51
+ */
44
52
  export class Items extends SystemItemCompiler {
45
53
  /**
46
54
  * SoHL's note-type → document-subtype map — the one declaration that says
@@ -82,11 +82,6 @@ const MACRO_FIELDS = Object.freeze([
82
82
  },
83
83
  ]);
84
84
 
85
- /**
86
- * A `being` note — an actor, with its body, movement and embedded items.
87
- *
88
- * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
89
- */
90
85
  /**
91
86
  * A `vehicle` note — a conveyance that carries goods and people.
92
87
  *
@@ -103,6 +98,11 @@ const MACRO_FIELDS = Object.freeze([
103
98
  */
104
99
  const VEHICLE_FIELDS = Object.freeze([]);
105
100
 
101
+ /**
102
+ * A `being` note — an actor, with its body, movement and embedded items.
103
+ *
104
+ * @type {readonly import("../engine/field-spec.mjs").FieldSpec[]}
105
+ */
106
106
  const BEING_FIELDS = Object.freeze([
107
107
  {
108
108
  name: "body",
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Whether this package compiles Foundry documents at all.
3
+ *
4
+ * The one question every Foundry-side reader asks — the manifest writer, to
5
+ * decide whether there is a package for Foundry to install, and the pack
6
+ * compilers, to decide whether there is anything to compile.
7
+ *
8
+ * @param {{packageKind: string}} config - A resolved configuration.
9
+ * @returns {boolean} Whether the package compiles Foundry documents.
10
+ */
11
+ export function compilesFoundryDocuments(config: {
12
+ packageKind: string;
13
+ }): boolean;
1
14
  /**
2
15
  * Whether this package publishes the pages its content tree compiles to.
3
16
  *
@@ -30,13 +43,27 @@ export function publishesContentPages(config: {
30
43
  */
31
44
  export function defineConfig(config: ContentBuildConfigInput): ContentBuildConfig;
32
45
  /**
33
- * The two kinds of Foundry package a content module can be built into. The
34
- * value is also the directory Foundry installs the package under, which is why
35
- * it is plural.
46
+ * What kind of package this is.
47
+ *
48
+ * `systems` and `modules` are the two Foundry answers, and the value is also
49
+ * the directory Foundry installs the package under, which is why they are
50
+ * plural. `documentation` is the answer "not a Foundry package at all": it
51
+ * publishes a site and a book from its notes, installs into no Foundry data
52
+ * directory and compiles no compendium.
36
53
  *
37
54
  * @satisfies {readonly PackageKind[]}
38
55
  */
39
- export const PACKAGE_KINDS: readonly ["systems", "modules"];
56
+ export const PACKAGE_KINDS: readonly ["systems", "modules", "documentation"];
57
+ /**
58
+ * The kind that compiles no Foundry documents.
59
+ *
60
+ * Spelled once and read wherever a pass asks whether it applies, so the
61
+ * validator, the CLI and the compile passes cannot come to disagree about what
62
+ * the value means.
63
+ *
64
+ * @type {string}
65
+ */
66
+ export const DOCUMENTATION_KIND: string;
40
67
  export namespace DEFAULT_PATHS {
41
68
  let content: "assets/content";
42
69
  let contentIndex: "build/content-index";
@@ -179,7 +206,7 @@ export const DERIVED_SYSTEM_VERSION: symbol;
179
206
  * means what it said.
180
207
  */
181
208
  export type SiteMode = "homepage" | "content";
182
- export type PackageKind = "systems" | "modules";
209
+ export type PackageKind = "systems" | "modules" | "documentation";
183
210
  export type PackDocumentType = "Actor" | "Adventure" | "Item" | "JournalEntry" | "Macro" | "Scene";
184
211
  /**
185
212
  * One compendium pack the build compiles, named exactly as it is declared in
@@ -542,17 +569,27 @@ export type ContentBuildConfigInput = {
542
569
  contentPackage: string;
543
570
  /**
544
571
  * Foundry package id, as it appears in
545
- * `system.json` / `module.json`.
572
+ * `system.json` / `module.json`.
573
+ * Refused by a `documentation`
574
+ * package, which ships no Foundry
575
+ * package.
546
576
  */
547
- foundryPackage: string;
577
+ foundryPackage?: string | undefined;
548
578
  /**
549
- * Whether the package is a system or a module.
579
+ * Whether the package is a system, a
580
+ * module, or documentation — the kind
581
+ * that publishes a site and a book
582
+ * while compiling nothing.
550
583
  */
551
584
  packageKind: PackageKind;
552
585
  /**
553
- * Identity stamped into every document's `_stats`.
586
+ * Identity stamped into every
587
+ * document's `_stats`. Required of a
588
+ * package that compiles documents, and
589
+ * refused by a `documentation` one,
590
+ * which compiles none.
554
591
  */
555
- stats: StatsSpec;
592
+ stats?: StatsSpec | undefined;
556
593
  /**
557
594
  * The consumer's
558
595
  * item-type registry: each content `type`
@@ -626,15 +663,29 @@ export type ContentBuildConfigInput = {
626
663
  export type ContentBuildConfig = {
627
664
  rootDir: string;
628
665
  contentPackage: string;
629
- foundryPackage: string;
666
+ /**
667
+ * `null` for a `documentation`
668
+ * package, which ships no Foundry package.
669
+ */
670
+ foundryPackage: string | null;
630
671
  packageKind: PackageKind;
631
672
  /**
632
- * Derived: the served Foundry asset root,
633
- * `<packageKind>/<foundryPackage>/assets`.
673
+ * Derived, and **conditional**: the served
674
+ * Foundry asset root,
675
+ * `<packageKind>/<foundryPackage>/assets`,
676
+ * for a package Foundry installs — and
677
+ * `null` for a `documentation` package,
678
+ * which Foundry serves no files for. See
679
+ * {@link module :engine/helpers.resolveImg},
680
+ * the one reader of it.
634
681
  */
635
- assetRoot: string;
682
+ assetRoot: string | null;
636
683
  paths: Readonly<ResolvedPaths>;
637
- stats: Readonly<StatsSpec>;
684
+ /**
685
+ * `null` for a `documentation`
686
+ * package, which stamps no `_stats`.
687
+ */
688
+ stats: Readonly<StatsSpec> | null;
638
689
  /**
639
690
  * Derived: the
640
691
  * `system` builder of each entry, whichever
@@ -224,33 +224,6 @@ export class SystemActorCompiler extends BasePackCompiler {
224
224
  * subtype, or why the reference names none.
225
225
  */
226
226
  embeddedSubtype(type: string): import("./document-subtypes.mjs").ReferencedSubtype;
227
- /**
228
- * Resolve one embedded item from a `(type, shortcode?, overlay)`
229
- * descriptor. If `shortcode` is given, the predefined item is fetched
230
- * from `itemsMap` and the overlay deep-merged on top. If absent, the
231
- * descriptor must carry enough fields to stand alone. The embedded
232
- * item's `_id` is regenerated deterministically from
233
- * `(actorId, subType, shortcode, indexKey)` so re-exports are stable —
234
- * from the **document subtype**, so that renaming a note type leaves
235
- * every embedded id exactly where it was.
236
- * Returns null if the descriptor cannot be resolved.
237
- *
238
- * @param {Map<string, object>} itemsMap - The predefined items, by address.
239
- * @param {string} actorId - The owning actor's id, seeding embedded ids.
240
- * @param {string} type - The **note** type the reference names.
241
- * @param {string|null} shortcode - The referenced item's shortcode, or
242
- * `null` for a stand-alone entry.
243
- * @param {object} [overlay] - The entry's remaining properties.
244
- * @param {string} indexKey - Where the reference sits, for a diagnostic.
245
- * It no longer reaches the id — it names the entry in a message.
246
- * @param {string} ctx - Diagnostic context (the actor's label).
247
- * @param {object} [at] - Where to locate a finding.
248
- * @param {string} [at.fmKey] - The frontmatter key the reference sits
249
- * under, so an unresolved one is reported at the reference rather than
250
- * at the note.
251
- * @returns {object|null} The embedded item, or null when it resolved to
252
- * nothing — always with a finding emitted.
253
- */
254
227
  /**
255
228
  * Read an entry's `model:` — the address of the item it is a copy of.
256
229
  *
@@ -276,9 +249,40 @@ export class SystemActorCompiler extends BasePackCompiler {
276
249
  shortcode: string;
277
250
  package: string | null;
278
251
  } | null;
279
- resolveEmbedded(itemsMap: any, actorId: any, type: any, shortcode: any, overlay: any, indexKey: any, ctx: any, { fmKey, modelPackage }?: {
280
- modelPackage?: null | undefined;
281
- }): any;
252
+ /**
253
+ * Resolve one embedded item from a `(type, shortcode?, overlay)`
254
+ * descriptor. If `shortcode` is given, the predefined item is fetched
255
+ * from `itemsMap` and the overlay deep-merged on top. If absent, the
256
+ * descriptor must carry enough fields to stand alone. The embedded
257
+ * item's `_id` is regenerated deterministically from
258
+ * `(actorId, subType, shortcode, indexKey)` so re-exports are stable —
259
+ * from the **document subtype**, so that renaming a note type leaves
260
+ * every embedded id exactly where it was.
261
+ * Returns null if the descriptor cannot be resolved.
262
+ *
263
+ * @param {Map<string, object>} itemsMap - The predefined items, by address.
264
+ * @param {string} actorId - The owning actor's id, seeding embedded ids.
265
+ * @param {string} type - The **note** type the reference names.
266
+ * @param {string|null} shortcode - The referenced item's shortcode, or
267
+ * `null` for a stand-alone entry.
268
+ * @param {object} overlay - The entry's remaining properties.
269
+ * @param {string} indexKey - Where the reference sits, for a diagnostic.
270
+ * It names the entry in a message and does not reach the id.
271
+ * @param {string} ctx - Diagnostic context (the actor's label).
272
+ * @param {object} [at] - Where to locate a finding.
273
+ * @param {string} [at.fmKey] - The frontmatter key the reference sits
274
+ * under, so an unresolved one is reported at the reference rather than
275
+ * at the note.
276
+ * @param {string|null} [at.modelPackage] - The package a `model:` names,
277
+ * where the template comes from another package's catalogue. Given one,
278
+ * the packaged address is what resolves and nothing local shadows it.
279
+ * @returns {object|null} The embedded item, or null when it resolved to
280
+ * nothing — always with a finding emitted.
281
+ */
282
+ resolveEmbedded(itemsMap: Map<string, object>, actorId: string, type: string, shortcode: string | null, overlay: object, indexKey: string, ctx: string, { fmKey, modelPackage }?: {
283
+ fmKey?: string | undefined;
284
+ modelPackage?: string | null | undefined;
285
+ }): object | null;
282
286
  #private;
283
287
  }
284
288
  import { BasePackCompiler } from "./base-compiler.mjs";
@@ -39,9 +39,60 @@ export function readItemAddresses(dirs: readonly string[]): Map<string, {
39
39
  shortcode: string;
40
40
  file: string;
41
41
  }>;
42
- export function declaredPredecessors(contentBase: any, { skipDirectories, maps, config, records, problems }?: {
43
- maps?: readonly import("./document-subtypes.mjs").DocumentSubtypeMap[] | undefined;
44
- }): Map<any, any>;
42
+ /**
43
+ * Every rename the tree's notes **declare**, as old address → where it went.
44
+ *
45
+ * Read from the content tree rather than from compiled output, because a
46
+ * declaration is authored and the compiled document does not carry it: nothing
47
+ * downstream consumes `renamedFrom:`, so emitting it into every pack to let one
48
+ * diagnostic read it back would put a build-time note in shipped data forever.
49
+ * The tree is already read by this module for the same reason
50
+ * ({@link noteFilesById}) — to place a finding where its author can fix it.
51
+ *
52
+ * **A declaration is keyed by document subtype, not by note type.** The address
53
+ * space is the one consumers resolve against, and it is spelled in compiled
54
+ * documents: `hm3` compiles a `projectile` note into a `missilegear` item,
55
+ * so that is the address a rename of it moves. {@link referencedSubtype} is the
56
+ * function that already answers this for a being's embedded `(type, shortcode)`
57
+ * references, so both sides read the same rule rather than a second copy of it.
58
+ *
59
+ * **An entry is emitted for every system that maps the type**, whether or not
60
+ * the note declares that system's block. Over-emitting is inert — the diff uses
61
+ * an entry only when the baseline published the old address *and* this build
62
+ * publishes the new one, and a system the note does not compile for satisfies
63
+ * neither — while asking which blocks a note declares would put a second,
64
+ * subtly different answer to that question in a third place.
65
+ *
66
+ * **First claim wins on a collision.** Two notes naming one predecessor is a
67
+ * contradiction — an address has one successor — and it is reported as an error
68
+ * by `engine/content-lint.mjs`, where both notes are in hand and can both be
69
+ * named. Picking one here keeps this a map; it is not a resolution, and nothing
70
+ * rests on which one it picked.
71
+ *
72
+ * @param {string} contentBase - Root of the content tree.
73
+ * @param {object} opts
74
+ * @param {readonly string[]} [opts.skipDirectories] - The corpus scope. Stated
75
+ * by the caller, never defaulted — see {@link addressCorpus}.
76
+ * @param {readonly object[]} [opts.maps] - The document-subtype maps.
77
+ * @param {object} [opts.config] - The resolved build configuration.
78
+ * @param {readonly object[]} [opts.records] - Index records the caller already
79
+ * derived, shared with {@link noteFilesById} so one command reads one corpus.
80
+ * @param {object[]} [opts.problems] - Collects the notes the index cannot
81
+ * record, so one of them does not abort the diff before it reports.
82
+ * @returns {Map<string, {to: string, file: string, shortcode: string}>} Old
83
+ * address → the address the declaring note publishes at now, and that note.
84
+ */
85
+ export function declaredPredecessors(contentBase: string, { skipDirectories, maps, config, records, problems }?: {
86
+ skipDirectories?: readonly string[] | undefined;
87
+ maps?: readonly object[] | undefined;
88
+ config?: object | undefined;
89
+ records?: readonly object[] | undefined;
90
+ problems?: object[] | undefined;
91
+ }): Map<string, {
92
+ to: string;
93
+ file: string;
94
+ shortcode: string;
95
+ }>;
45
96
  /**
46
97
  * Every address the baseline published that this build does not.
47
98
  *
@@ -85,12 +136,15 @@ export function diffItemAddresses(baseline: Map<string, object>, current: Map<st
85
136
  * id is derived against. See {@link addressCorpus} for why that matters.
86
137
  * @param {readonly object[]} [opts.records] - Index records the caller already
87
138
  * derived, shared with {@link declaredPredecessors}.
139
+ * @param {object[]} [opts.problems] - Collects the notes the index cannot
140
+ * record, so one of them does not abort the diff before it reports.
88
141
  * @returns {Map<string, string>} Document id → the note's absolute path.
89
142
  */
90
143
  export function noteFilesById(contentBase: string, { skipDirectories, config, records, problems }?: {
91
144
  skipDirectories?: readonly string[] | undefined;
92
145
  config?: object | undefined;
93
146
  records?: readonly object[] | undefined;
147
+ problems?: object[] | undefined;
94
148
  }): Map<string, string>;
95
149
  /**
96
150
  * Where to send the reader for one finding.
@@ -136,11 +136,17 @@ export class BasePackCompiler {
136
136
  * Resolves a `packFolder` — a folder note's address — to the Foundry
137
137
  * folder id it materialises as in this pack.
138
138
  * @param {string} [options.packName] - The pack this pass writes.
139
+ * @param {string|null} [options.packSystem] - The system that pack declares,
140
+ * which decides the `_stats` it stamps and the system block a note must
141
+ * carry to reach it. `null` for a pack that declares none.
139
142
  * @param {string} [options.docType] - The Foundry document type it holds.
140
143
  * @param {{resolve: Function}} [options.router] - The pack router. Omit it
141
144
  * — as the unit suite does — and every claimed note is compiled here.
142
145
  * @param {boolean} [options.routingReporter] - Whether this pass reports a
143
146
  * note of its type that routes nowhere.
147
+ * @param {object} [options.corpus] - The corpus this compile is running
148
+ * over, derived once and shared by every pass. A pass handed none derives
149
+ * its own in `prepare`, and reports that corpus's problems itself.
144
150
  */
145
151
  constructor({ contentBase, dest, skipDirectories, folderResolver, packName, packSystem, docType, router, routingReporter, corpus, }?: {
146
152
  contentBase: string;
@@ -148,11 +154,13 @@ export class BasePackCompiler {
148
154
  skipDirectories: readonly string[];
149
155
  folderResolver?: ((address: string | null) => string | null) | undefined;
150
156
  packName?: string | undefined;
157
+ packSystem?: string | null | undefined;
151
158
  docType?: string | undefined;
152
159
  router?: {
153
160
  resolve: Function;
154
161
  } | undefined;
155
162
  routingReporter?: boolean | undefined;
163
+ corpus?: object | undefined;
156
164
  });
157
165
  /** @type {string} */
158
166
  contentBase: string;
@@ -219,8 +227,8 @@ export class BasePackCompiler {
219
227
  * @type {number}
220
228
  */
221
229
  unresolvedLinks: number;
222
- packSystem: any;
223
- corpus: any;
230
+ packSystem: string | null;
231
+ corpus: object | undefined;
224
232
  /**
225
233
  * The `_stats` block every entry this pass emits is stamped with.
226
234
  *
@@ -20,6 +20,15 @@
20
20
  * pack, which reordering cannot fix.
21
21
  */
22
22
  export function loadBundleSources(sourceDirs: Readonly<Record<string, readonly string[]>>): Map<string, object>;
23
+ /**
24
+ * Adventure pack compiler.
25
+ *
26
+ * Walks the content tree and compiles every `type: bundle` note into one
27
+ * Adventure per system, holding **copies** of the compiled documents its
28
+ * `contents` addresses name. Reading other passes' output is what
29
+ * {@link Bundles.readsPackOutputOf} declares, and what the generator orders the
30
+ * compile by.
31
+ */
23
32
  export class Bundles extends BasePackCompiler {
24
33
  /**
25
34
  * @param {object} options - As {@link BasePackCompiler}, plus:
@@ -14,11 +14,11 @@
14
14
  * consumer deriving a `path` from a key drops the *package and the system*, not
15
15
  * the package alone.
16
16
  *
17
- * The hyphen is a separator and never occurs inside a segment: a shortcode is
18
- * `^[A-Za-z0-9]+$` (`ADDRESS_SEGMENT_PATTERN`, enforced by `content-lint.mjs`)
19
- * and a type is a bare word. That is the same charset guarantee positional key
20
- * parsing rests on, so the address and the key cannot disagree about where one
21
- * ends and the other begins.
17
+ * The hyphen is a separator and never occurs inside a segment: a shortcode
18
+ * matches `ADDRESS_SEGMENT_PATTERN` (`engine/address-charset.mjs`, enforced by
19
+ * `content-lint.mjs`) and a type is a bare word. That is the same charset
20
+ * guarantee positional key parsing rests on, so the address and the key
21
+ * cannot disagree about where one ends and the other begins.
22
22
  *
23
23
  * @param {object} fm - Parsed frontmatter.
24
24
  * @returns {string} The address segment, e.g. `weapongear-dagger`.
@@ -182,10 +182,10 @@ export function expandAddress(read: {
182
182
  *
183
183
  * Parsing is plain positional counting: split on the separator, require
184
184
  * {@link CANONICAL_KEY_SEGMENTS} of them, and assign each position its field.
185
- * **The charset rule is what makes that sound** — every segment is
186
- * `^[A-Za-z0-9]+$` (`ADDRESS_SEGMENT_PATTERN` in `engine/address-charset.mjs`),
187
- * so the hyphen is purely a separator and the count alone determines every
188
- * field. That is enforced at each of the three sources rather than assumed of
185
+ * **The charset rule is what makes that sound** — every segment matches
186
+ * `ADDRESS_SEGMENT_PATTERN` (`engine/address-charset.mjs`), so the hyphen is
187
+ * purely a separator and the count alone determines every field. That is
188
+ * enforced at each of the three sources rather than assumed of
189
189
  * the data: shortcodes by `content-lint.mjs`, `contentPackage` by
190
190
  * `defineConfig`, and types are bare words. Were any of them free to
191
191
  * carry a hyphen, no amount of counting would recover the fields and the reader
@@ -91,21 +91,65 @@ export function asciiName(name: unknown): string | null;
91
91
  * a consumer iterating it should not have to check first.
92
92
  */
93
93
  export function asciiAliases(aliases: unknown): Array<string>;
94
+ /**
95
+ * Build one index record from a note's frontmatter and its place in the tree.
96
+ *
97
+ * The frontmatter as authored, plus what the index derives from it: the package
98
+ * it compiles as, its address, ASCII folds of its name and aliases, the anchors
99
+ * of its body, its Foundry block, the address of its documentation journal, and
100
+ * where the file sits within the tree.
101
+ *
102
+ * @param {object} options - Options.
103
+ * @param {Record<string, any>} options.frontmatter - The note's parsed frontmatter.
104
+ * @param {string} options.relPath - Its path below the content root, POSIX-separated.
105
+ * @param {string} [options.absPath] - The file, read only on the failing path to
106
+ * locate the offending key.
107
+ * @param {string} options.contentPackage - The package the tree compiles as.
108
+ * @param {string} [options.body] - The note's markdown body, for its anchors.
109
+ * @param {number} [options.bodyLine] - The 1-based file line the body starts on.
110
+ * @param {object} [options.manifest] - The package manifest, which the Foundry
111
+ * entries are derived against.
112
+ * @returns {Record<string, any>} The record, keys sorted at every depth.
113
+ * @throws {Error} When the note carries a key this module derives, which would
114
+ * otherwise be overwritten without a word. `file` and, where the file was
115
+ * read, `position` ride on the error.
116
+ */
94
117
  export function buildIndexRecord({ frontmatter, relPath, absPath, contentPackage, body, bodyLine, manifest, }: {
95
- frontmatter: any;
96
- relPath: any;
97
- absPath: any;
98
- contentPackage: any;
99
- body: any;
100
- bodyLine: any;
101
- manifest: any;
118
+ frontmatter: Record<string, any>;
119
+ relPath: string;
120
+ absPath?: string | undefined;
121
+ contentPackage: string;
122
+ body?: string | undefined;
123
+ bodyLine?: number | undefined;
124
+ manifest?: object | undefined;
102
125
  }): Record<string, any>;
103
- export function collectContentIndex(contentBase: any, { contentPackage, skipDirectories, manifest, problems }: {
104
- contentPackage: any;
105
- skipDirectories: any;
106
- manifest: any;
107
- problems: any;
108
- }): Record<string, any>[];
126
+ /**
127
+ * Read a content tree into index records, in the order they will be written.
128
+ *
129
+ * An item note yields two records — the item, and the documentation journal
130
+ * that is a document in its own right.
131
+ *
132
+ * @param {string} contentBase - The content tree to walk.
133
+ * @param {object} options - Options.
134
+ * @param {string} options.contentPackage - The package the tree compiles as.
135
+ * @param {readonly string[]} options.skipDirectories - The walk's scope, stated
136
+ * by the caller. An absent one is the caller's omission, and
137
+ * {@link module:engine/helpers.walkMarkdownTree} throws on it.
138
+ * @param {object} [options.manifest] - The package manifest, which the Foundry
139
+ * entries are derived against.
140
+ * @param {object[]} [options.problems] - Supplied by a **reader**: a note that
141
+ * cannot be recorded is pushed here as a diagnostic and skipped. Omitted, the
142
+ * note throws — the contract the emitter needs, since an index missing a note
143
+ * asserts that it does not exist.
144
+ * @returns {Array<Record<string, any>>} The records, in a total order that does
145
+ * not depend on directory-read order.
146
+ */
147
+ export function collectContentIndex(contentBase: string, { contentPackage, skipDirectories, manifest, problems }: {
148
+ contentPackage: string;
149
+ skipDirectories: readonly string[];
150
+ manifest?: object | undefined;
151
+ problems?: object[] | undefined;
152
+ }): Array<Record<string, any>>;
109
153
  /**
110
154
  * Serialize records as JSON Lines.
111
155
  *