@heroiclands/package-build 6.1.0 → 8.0.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 (41) hide show
  1. package/CHANGELOG.md +840 -0
  2. package/CONTENT.md +21 -1
  3. package/bin/content-build.mjs +105 -10
  4. package/bin/package-build.mjs +114 -1
  5. package/config.mjs +62 -3
  6. package/content-config.mjs +254 -22
  7. package/engine/base-compiler.mjs +25 -0
  8. package/engine/content-links.mjs +132 -27
  9. package/engine/diagnostics.mjs +61 -1
  10. package/engine/foreign-catalog.mjs +47 -0
  11. package/engine/generate.mjs +10 -5
  12. package/engine/helpers.mjs +38 -0
  13. package/engine/journals.mjs +8 -1
  14. package/engine/macros.mjs +2 -0
  15. package/engine/pack-config.mjs +143 -13
  16. package/engine/prose-lint.mjs +10 -2
  17. package/engine/scenes.mjs +2 -2
  18. package/engine/schema-check.mjs +332 -0
  19. package/engine/schema-extract.mjs +611 -0
  20. package/engine/web-wikilinks.mjs +13 -4
  21. package/engine/wikilink-syntax.mjs +25 -0
  22. package/engine/wikilinks.mjs +6 -3
  23. package/manifest.mjs +37 -2
  24. package/package.json +5 -3
  25. package/sohl/actors.mjs +23 -10
  26. package/sohl/item-fields.mjs +0 -35
  27. package/sohl/items.mjs +1 -1
  28. package/types/content-config.d.mts +14 -0
  29. package/types/engine/base-compiler.d.mts +18 -1
  30. package/types/engine/content-links.d.mts +11 -3
  31. package/types/engine/diagnostics.d.mts +33 -1
  32. package/types/engine/foreign-catalog.d.mts +15 -0
  33. package/types/engine/generate.d.mts +3 -2
  34. package/types/engine/helpers.d.mts +29 -3
  35. package/types/engine/journals.d.mts +7 -1
  36. package/types/engine/pack-config.d.mts +22 -0
  37. package/types/engine/prose-lint.d.mts +10 -2
  38. package/types/engine/schema-check.d.mts +176 -0
  39. package/types/engine/schema-extract.d.mts +61 -0
  40. package/types/engine/wikilink-syntax.d.mts +24 -0
  41. package/types/sohl/actors.d.mts +3 -3
@@ -93,7 +93,7 @@ import { hasDocEntry, itemDocEntryId } from "./item-docs.mjs";
93
93
  import { replaceOutsideCode } from "./code-fences.mjs";
94
94
  // The syntax lives in `./wikilink-syntax.mjs`, so the web resolver and this
95
95
  // one cannot disagree about what counts as a link.
96
- import { WIKILINK, parseWikilink } from "./wikilink-syntax.mjs";
96
+ import { authoredLabel, WIKILINK, parseWikilink } from "./wikilink-syntax.mjs";
97
97
 
98
98
  export { ITEM_PACK, PACK_BY_TYPE, packForType };
99
99
 
@@ -498,8 +498,11 @@ export function convertWikilinks(markdown, { type, id, pack, docPack, index }) {
498
498
  const { labelled } = parsed;
499
499
  let target = parsed.target;
500
500
  // An unlabelled link shows its interior verbatim, anchor included;
501
- // a labelled one shows its label.
502
- let text = labelled ? (parsed.display ?? "") : parsed.inner;
501
+ // a labelled one shows its label. An *empty* label is not a label
502
+ // `[[x|]]` means "show the target's name" and that reading
503
+ // comes from {@link authoredLabel} so the web resolver cannot draw
504
+ // the line somewhere else (#113).
505
+ let text = labelled ? (authoredLabel(parsed) ?? "") : parsed.inner;
503
506
  const slug = parsed.anchor || null;
504
507
 
505
508
  // Resolve the document: same-page (empty target), type-shortcode, or alias.
package/manifest.mjs CHANGED
@@ -430,8 +430,43 @@ export function buildManifest({ config, packageJson, artifact, flags }) {
430
430
  ...releaseUrls({ repoUrl, version: packageJson.version, artifact }),
431
431
  };
432
432
  if (config.compatibility) derived.compatibility = config.compatibility;
433
- if (config.relationships && Object.keys(config.relationships).length) {
434
- derived.relationships = publishedRelationships(config.relationships);
433
+
434
+ // `requiresSystem` is the gate half of the declare/require split (#48). It
435
+ // emits the `relationships.systems` entry Foundry's `supportsSystem` reads,
436
+ // reusing the `systems:` declaration rather than restating it — a second
437
+ // transcription is free to disagree with what it copied, which is how
438
+ // `stats.systemVersion` came to sit at `0.6.0` for four releases.
439
+ //
440
+ // Declaring a system emits nothing on its own. That is the point: a module
441
+ // shipping content for two systems names both under `systems:`, stamps each
442
+ // pack accordingly, and stays loadable everywhere because it requires
443
+ // neither.
444
+ const relationships = { ...(config.relationships ?? {}) };
445
+ if (config.requiresSystem) {
446
+ const declaredSystem = config.systems?.[config.requiresSystem];
447
+ const entry = {
448
+ id: config.requiresSystem,
449
+ type: "system",
450
+ ...(declaredSystem?.manifest ?
451
+ { manifest: declaredSystem.manifest }
452
+ : {}),
453
+ ...(declaredSystem?.compatibility ?
454
+ {
455
+ compatibility: Object.fromEntries(
456
+ Object.entries(declaredSystem.compatibility).filter(
457
+ ([, v]) => v != null,
458
+ ),
459
+ ),
460
+ }
461
+ : {}),
462
+ };
463
+ // An explicit `relationships.systems` still wins, so a repository
464
+ // mid-migration is never told two different things about itself.
465
+ relationships.systems =
466
+ relationships.systems?.length ? relationships.systems : [entry];
467
+ }
468
+ if (Object.keys(relationships).length) {
469
+ derived.relationships = publishedRelationships(relationships);
435
470
  }
436
471
 
437
472
  const merged = { ...declared, ...derived };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heroiclands/package-build",
3
- "version": "6.1.0",
3
+ "version": "8.0.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",
@@ -122,12 +122,12 @@
122
122
  "classic-level": "^3.0.0",
123
123
  "dotenv": "^17.2.3",
124
124
  "fflate": "^0.8.3",
125
- "glob": "^11.0.3",
125
+ "glob": "^13.0.6",
126
126
  "gray-matter": "^4.0.3",
127
127
  "handlebars": "^4.7.9",
128
128
  "loglevel": "^1.9.2",
129
129
  "loglevel-plugin-prefix": "^0.8.4",
130
- "markdown-it": "^14.1.0",
130
+ "markdown-it": "^15.0.0",
131
131
  "markdownlint-cli2": "^0.23.2",
132
132
  "prettier": "^3.9.6",
133
133
  "ssh2-sftp-client": "^12.1.1",
@@ -148,7 +148,9 @@
148
148
  "build:types": "tsc -p tsconfig.dts.json",
149
149
  "format": "prettier --write .",
150
150
  "format:check": "prettier --check .",
151
+ "lint": "npm run format:check && npm run lint:markdown",
151
152
  "lint:markdown": "node bin/content-build.mjs markdown",
153
+ "lint:markdown:fix": "node bin/content-build.mjs markdown --fix",
152
154
  "changeset": "changeset",
153
155
  "changeset:check": "changeset status --since=origin/main",
154
156
  "changeset:version": "changeset version && npm install --package-lock-only",
package/sohl/actors.mjs CHANGED
@@ -327,20 +327,33 @@ export class Actors extends BasePackCompiler {
327
327
  itemsSourceDirs;
328
328
  foreignSourceDirs;
329
329
 
330
- constructor({ itemsSourceDirs, foreignSourceDirs = [], ...options }) {
330
+ constructor({ itemsSourceDirs = [], foreignSourceDirs = [], ...options }) {
331
331
  super(options);
332
332
  // Where the items passes wrote their JSON. Stated by the caller rather
333
333
  // than assumed to be this pack's sibling: the packs' locations are
334
334
  // configuration, and a consumer may put them anywhere (#1508). Every
335
335
  // Item pack, because a repository may ship more than one (#1566).
336
- if (!itemsSourceDirs?.length) {
337
- throw new Error(
338
- "Actors compiler requires `itemsSourceDirs` the generated JSON " +
339
- "of every Item pack, which each being's embedded items are " +
340
- "resolved against. Declare at least one pack of type " +
341
- '"Item" in package-build.config.yaml.',
342
- );
343
- }
336
+ //
337
+ // **Optional, and empty is a legitimate package (#49).** This used to
338
+ // throw unless at least one Item pack was declared, which asked a
339
+ // package to declare the very thing it may exist not to have. An Item
340
+ // pack is system-bound by construction Foundry requires `system` on
341
+ // Item packs — so a deliberately system-agnostic module could satisfy
342
+ // the guard only by naming a system. `harn-ensemble` is the case:
343
+ // 2,512 beings whose embedded items address the `sohl` and `hm3`
344
+ // catalogues, and five affiliation notes of its own.
345
+ //
346
+ // The guard also did not test what it claimed. It counted *declared
347
+ // directories*, not resolvable items, so an empty Item pack satisfied
348
+ // it while a being naming a missing item still failed later. The
349
+ // condition actually cared about is checked where it can be reported
350
+ // precisely: {@link Actors#resolveEmbedded} already errors per
351
+ // unresolved `(type, shortcode)`, naming the being. A package whose
352
+ // beings embed nothing, or whose every address resolves against a
353
+ // dependency catalogue through `foreignSourceDirs`, now compiles with
354
+ // no Item pack at all — and one that is genuinely missing an item
355
+ // still fails, saying which item and which actor rather than which
356
+ // pack is absent.
344
357
  Object.defineProperty(this, "itemsSourceDirs", {
345
358
  value: Object.freeze([...itemsSourceDirs]),
346
359
  writable: false,
@@ -641,7 +654,7 @@ export class Actors extends BasePackCompiler {
641
654
  // `sohl.archetype` (required nullable number) drives
642
655
  // `flags.sohl.docArchetype` (#640 / archetype contract #604).
643
656
  flags: withArchetypeFlag(fm, fm.flags, ctx),
644
- _stats: defaultStats(),
657
+ _stats: this.stats,
645
658
  _key: `!actors!${id}`,
646
659
  };
647
660
  }
@@ -311,11 +311,6 @@ export const ITEM_FIELDS = Object.freeze({
311
311
  value: false,
312
312
  describe: "Whether it is currently dormant. Play state.",
313
313
  },
314
- {
315
- to: "isTreated",
316
- value: false,
317
- describe: "Whether it has been treated. Play state.",
318
- },
319
314
  {
320
315
  name: "levelBase",
321
316
  to: "levelBase",
@@ -633,22 +628,6 @@ export const ITEM_FIELDS = Object.freeze({
633
628
  required: true,
634
629
  describe: "What kind of projectile it is.",
635
630
  },
636
- {
637
- name: "impact.overrideDice",
638
- to: "impactBase.overrideDice",
639
- shape: "boolean",
640
- read: (raw, { fm }) => Boolean(raw ?? impactDie(fm) > 0),
641
- describe:
642
- "Whether the projectile's dice replace the launching weapon's. Defaults to true when the projectile declares a die.",
643
- },
644
- {
645
- name: "impact.overrideModifier",
646
- to: "impactBase.overrideModifier",
647
- shape: "boolean",
648
- read: (raw) => Boolean(raw ?? false),
649
- describe:
650
- "Whether the projectile's modifier replaces the launching weapon's.",
651
- },
652
631
  {
653
632
  to: "impactBase.numDice",
654
633
  value: (fm) => (impactDie(fm) > 0 ? 1 : 0),
@@ -781,20 +760,6 @@ export const ITEM_FIELDS = Object.freeze({
781
760
  describe:
782
761
  "How the injury was inflicted. Unset on a descriptive condition.",
783
762
  },
784
- {
785
- name: "isTreated",
786
- to: "isTreated",
787
- ...BOOLEAN,
788
- default: false,
789
- describe: "Whether it has been treated.",
790
- },
791
- {
792
- name: "isBleeding",
793
- to: "isBleeding",
794
- ...BOOLEAN,
795
- default: false,
796
- describe: "Whether it is bleeding.",
797
- },
798
763
  {
799
764
  name: "bodyLocationCode",
800
765
  to: "bodyLocationCode",
package/sohl/items.mjs CHANGED
@@ -167,7 +167,7 @@ export class Items extends BasePackCompiler {
167
167
  // `sohl.archetype` (required nullable number) drives
168
168
  // `flags.sohl.docArchetype` (#640 / archetype contract #604).
169
169
  flags: withArchetypeFlag(fm, fm.flags, `item "${name}"`),
170
- _stats: defaultStats(),
170
+ _stats: this.stats,
171
171
  ownership: { default: 0 },
172
172
  folder,
173
173
  _key: `!items!${id}`,
@@ -131,6 +131,20 @@ export const DEFAULT_ADDRESS_SCHEME: Readonly<{
131
131
  * @satisfies {readonly SiteMode[]}
132
132
  */
133
133
  export const SITE_MODES: readonly ["homepage", "content"];
134
+ /**
135
+ * How the loader hands {@link defineConfig} the system version it resolved.
136
+ *
137
+ * A **Symbol**, deliberately. `stats.systemVersion` is refused from an authored
138
+ * configuration (#48), but the value still has to reach here from the loader —
139
+ * which is the half that may do I/O, and which reads a system package's version
140
+ * out of the adjacent `package.json`. A string key would be a second spelling of
141
+ * the refused one, forgeable from YAML and reachable by `rejectUnknownKeys`; a
142
+ * symbol key cannot be written in YAML at all and does not appear in
143
+ * `Object.keys`, so the refusal has no back door.
144
+ *
145
+ * @type {symbol}
146
+ */
147
+ export const DERIVED_SYSTEM_VERSION: symbol;
134
148
  /**
135
149
  * How much of a package reaches the web.
136
150
  *
@@ -92,7 +92,7 @@ export class BasePackCompiler {
92
92
  * @param {boolean} [options.routingReporter] - Whether this pass reports a
93
93
  * note of its type that routes nowhere.
94
94
  */
95
- constructor({ contentBase, dest, folderResolver, packName, docType, router, routingReporter, }?: {
95
+ constructor({ contentBase, dest, folderResolver, packName, packSystem, docType, router, routingReporter, }?: {
96
96
  contentBase: string;
97
97
  dest: string;
98
98
  folderResolver?: ((path: string | null) => string | null) | undefined;
@@ -153,6 +153,22 @@ export class BasePackCompiler {
153
153
  * @type {number}
154
154
  */
155
155
  unresolvedLinks: number;
156
+ packSystem: any;
157
+ /**
158
+ * The `_stats` block every entry this pass emits is stamped with (#48).
159
+ *
160
+ * Per pack rather than per package, because a module may ship the same
161
+ * content for two systems — `harn-ensemble` has an `actors-hm3` pack and an
162
+ * `actors-sohl` pack — and those documents were built against different
163
+ * system versions. A single global block stamped both identically.
164
+ *
165
+ * Memoised on the instance: one pass, one pack, one system, so the block is
166
+ * constant for the life of the compiler. The previous module-level memo
167
+ * could not be, because it was shared across passes for different packs.
168
+ *
169
+ * @returns {object} The block, built once per compiler.
170
+ */
171
+ get stats(): object;
156
172
  /**
157
173
  * Whether this pass's pack is the one a claimed note belongs in.
158
174
  *
@@ -326,6 +342,7 @@ export class BasePackCompiler {
326
342
  bodyLine: number | undefined;
327
343
  bodyColumn: number | undefined;
328
344
  } | undefined;
345
+ #private;
329
346
  }
330
347
  /**
331
348
  * The tallies one pass accumulates while walking the tree.
@@ -46,9 +46,17 @@ export function buildLinkIndex(contentBase: string, { manifestDir, skipDirectori
46
46
  * and what replaced it, so this is a fact rather than a guess — and it is
47
47
  * exactly the SoHL defect.
48
48
  * - A **hardcoded absolute URL** into this package's own prefix, or into one a
49
- * vendored manifest names. Both have a better form to write, which is why they
50
- * are reported; a bare `/<package>/` is left alone, because a package
51
- * homepage is in no manifest and there is nothing better to write.
49
+ * vendored manifest names. Every one of them has a better form to write, which
50
+ * is why every one is reported including a bare `/<package>/`, which names
51
+ * another package's landing (#87).
52
+ *
53
+ * That last case was exempt until the better form was identified, on the
54
+ * reasoning that a landing is in no link manifest so nothing could resolve it.
55
+ * True, and beside the point: it does not need resolving. A landing's address
56
+ * *is* its package prefix, so `/<package>/` is the absolute URL with the host
57
+ * struck off — host-free, emitted verbatim, and needing no index, which is
58
+ * what lets it hold in homepage-only mode where the tree is never walked. The
59
+ * form was already accepted here; nothing had ever named it as the one to use.
52
60
  * - A **root-relative `url:`**, which the theme's `relURL` prefixes a second
53
61
  * time. `href:` means "already resolved, use verbatim", so the same leading
54
62
  * slash is correct there and is not reported.
@@ -169,12 +169,44 @@ export function positionOfLiteral(text: string, needle: string, occurrence?: num
169
169
  * resolves to nothing — yields `{}`, so a caller spreads the result and the
170
170
  * position is dropped rather than guessed.
171
171
  *
172
+ * **`key: true` addresses the declaration rather than the value.** A finding
173
+ * about a *value* — this pack name is not in `packs[]` — belongs on the value,
174
+ * which is the default. A finding that names a **field** — `\`site.sections.x\`
175
+ * is not a recognized option` — sends the reader to look for that field, so the
176
+ * position should be the field's own, and in a flow mapping
177
+ * (`{ title: X, banner: Y }`) the two are different columns on one line. The
178
+ * key's node is found through the same parse, so this stays one locator rather
179
+ * than a second one free to disagree with it.
180
+ *
172
181
  * @param {string} text - The document's contents.
173
182
  * @param {ReadonlyArray<string|number>} keyPath - Path to the node: map keys as
174
183
  * strings, sequence entries as numbers.
184
+ * @param {object} [opts]
185
+ * @param {boolean} [opts.key=false] - Report where the last segment is
186
+ * *declared* rather than where its value sits. Ignored for a sequence entry,
187
+ * which has no key.
175
188
  * @returns {{line?: number, column?: number}} Spreadable position fields.
176
189
  */
177
- export function positionOfYamlPath(text: string, keyPath: ReadonlyArray<string | number>): {
190
+ export function positionOfYamlPath(text: string, keyPath: ReadonlyArray<string | number>, { key }?: {
191
+ key?: boolean | undefined;
192
+ }): {
178
193
  line?: number;
179
194
  column?: number;
180
195
  };
196
+ /**
197
+ * The YAML key path a **dotted field path** addresses.
198
+ *
199
+ * Configuration checks report the offending key as the path a reader would
200
+ * write it — `packs[1].name`, `site.sections.affliction.title` — because that
201
+ * is what the message has to say. {@link positionOfYamlPath} addresses a node
202
+ * by segments instead, so this is the one translation between them: `.`
203
+ * separates map keys, and a bracketed suffix is a sequence index.
204
+ *
205
+ * A path this cannot parse yields `[]`, which {@link positionOfYamlPath} in
206
+ * turn resolves to no position — dropped rather than guessed, as everything
207
+ * else here is.
208
+ *
209
+ * @param {string} field - The dotted path, as a diagnostic spells it.
210
+ * @returns {Array<string|number>} Its segments, sequence indices as numbers.
211
+ */
212
+ export function yamlKeyPath(field: string): Array<string | number>;
@@ -23,6 +23,15 @@ export function itemCatalogRelationships(config: object): Array<{
23
23
  * @returns {string} The directory.
24
24
  */
25
25
  export function catalogDir(config: object, id: string, version: string): string;
26
+ /**
27
+ * Where a cached dependency's published schema sits, if it shipped one.
28
+ *
29
+ * @param {object} config - The resolved configuration.
30
+ * @param {string} id - The dependency's package id.
31
+ * @param {string} version - Its resolved version.
32
+ * @returns {string} The path, whether or not it exists.
33
+ */
34
+ export function cachedSchemaPath(config: object, id: string, version: string): string;
26
35
  /**
27
36
  * Write an unzipped archive's entries under `dest`.
28
37
  *
@@ -101,3 +110,9 @@ export function fetchAllCatalogs(config: object): Promise<number>;
101
110
  * @returns {string[]} Every cached dependency's item directories.
102
111
  */
103
112
  export function foreignItemCatalogDirs(config: object): string[];
113
+ /**
114
+ * The file a system publishes its `system` field sets as (#60).
115
+ *
116
+ * @type {string}
117
+ */
118
+ export const SCHEMA_ARTIFACT_FILE: string;
@@ -13,8 +13,9 @@
13
13
  * @param {object} [config] - The resolved build configuration. Defaults to this
14
14
  * repository's.
15
15
  * @returns {string[]} Each Item pack's JSON directory. Empty when the
16
- * repository ships no items at all the actors pass, which is the only
17
- * caller that needs one, refuses that itself.
16
+ * repository ships no items at all, which is a legitimate package: the actors
17
+ * pass accepts an empty list and reports an item it cannot resolve per
18
+ * `(type, shortcode)` instead, naming the being (#49).
18
19
  */
19
20
  export function itemPackJsonDirs(config?: object): string[];
20
21
  /**
@@ -19,7 +19,7 @@ export function parseMarkdownFile(filePath: any): {
19
19
  } | {
20
20
  frontmatter: any;
21
21
  body: string;
22
- description: any;
22
+ description: string;
23
23
  bodyLine: number;
24
24
  bodyColumn: number;
25
25
  };
@@ -56,7 +56,7 @@ export function walkMarkdownTree(rootDir: string, { skipDirectories }?: {
56
56
  absPath: string;
57
57
  frontmatter: any;
58
58
  body: string;
59
- description: any;
59
+ description: string;
60
60
  bodyLine: number;
61
61
  bodyColumn: number;
62
62
  }, void, unknown>;
@@ -190,6 +190,32 @@ export function buildStats(systemVersion?: string, config?: {
190
190
  packageManifest: string;
191
191
  };
192
192
  }): object;
193
+ /**
194
+ * The `_stats` block for one pack, stamped with the system that pack is for
195
+ * (#48).
196
+ *
197
+ * **`systemId` travels with `systemVersion`.** They are one decision, so where
198
+ * one is omitted both are. Stamping a per-pack version against a package-wide
199
+ * id would emit `systemId: sohl, systemVersion: 1.6.3` on HM3 documents — a
200
+ * *plausible lie*, which is worse than the missing value #43 fixed, because
201
+ * nothing about it looks wrong.
202
+ *
203
+ * Resolution, in order:
204
+ *
205
+ * 1. The pack's own `system:`, looked up in the `systems:` block. That is the
206
+ * case a module shipping for two systems needs, and the one no
207
+ * package-wide value could express.
208
+ * 2. Failing that, the package-wide `stats` — a package whose packs are all for
209
+ * one system, which is every package that worked before this existed.
210
+ *
211
+ * A pack naming a system is validated against `systems:` at configuration time,
212
+ * so an unresolvable name never reaches here.
213
+ *
214
+ * @param {string|null|undefined} packSystem - The pack's declared `system:`.
215
+ * @param {object} [config] - The resolved configuration.
216
+ * @returns {object} The `_stats` block for that pack.
217
+ */
218
+ export function statsForPack(packSystem: string | null | undefined, config?: object): object;
193
219
  /**
194
220
  * The `_stats` block every compiler stamps on an entry it emits, built once.
195
221
  *
@@ -342,7 +368,7 @@ export function folderFilename(name: any, id: any): string;
342
368
  * pack, `"JournalEntry"` for the journals pack.
343
369
  */
344
370
  export function writeFolderDocs(folders: any, stats: any, destDir: any, documentType: any): void;
345
- export const md: any;
371
+ export const md: import("markdown-it").MarkdownIt;
346
372
  export { slugify } from "./content-slug.mjs";
347
373
  export { makeId } from "./ids.mjs";
348
374
  export { getFrontmatter, sohlField, resolveCharges, resolveSkillAptitudes, resolveRelation, requireSubType, parseValueDesc } from "./frontmatter.mjs";
@@ -93,15 +93,21 @@ export function buildPages(rawPages: Array<object>, entryId: string, noteName: s
93
93
  * heading; see {@link splitPages}.
94
94
  * @param {string|null} [params.folder] - The folder id, or `null`.
95
95
  * @param {object} [params.flags] - Document flags.
96
+ * @param {object} [params.stats] - The `_stats` block to stamp. Passed by the
97
+ * caller because it is a property of the *pack* being written, not of the
98
+ * entry: a module may ship the same content for two systems, and each pack's
99
+ * documents record the system version they were built against (#48). A
100
+ * caller with no pack in hand gets the package-wide block.
96
101
  * @returns {object} The JournalEntry document, keyed for the pack.
97
102
  */
98
- export function buildJournalEntry({ id, name, markdown, leadName, folder, flags, }: {
103
+ export function buildJournalEntry({ id, name, markdown, leadName, folder, flags, stats, }: {
99
104
  id: string;
100
105
  name: string;
101
106
  markdown: string;
102
107
  leadName?: string | undefined;
103
108
  folder?: string | null | undefined;
104
109
  flags?: object | undefined;
110
+ stats?: object | undefined;
105
111
  }): object;
106
112
  export class Journals extends BasePackCompiler {
107
113
  /**
@@ -14,6 +14,28 @@
14
14
  * {@link CONFIG_FILENAMES}.
15
15
  */
16
16
  export function findConfigFile(from: string): string | undefined;
17
+ /**
18
+ * Attach the position of the key a configuration error names.
19
+ *
20
+ * Eighty-one checks across `content-config.mjs` and `config.mjs` report through
21
+ * one `fail()`, which knows the offending key's dotted path and nothing
22
+ * about where it was written. Locating one of them and not the rest would be
23
+ * worse than locating none — a reader would learn that some configuration
24
+ * errors carry a position and could not predict which — so the path rides on
25
+ * the error and every one of them is located here, at the boundary that knows
26
+ * which file was read (#95).
27
+ *
28
+ * The message keeps its body and gains the `file:line:column: error: ` prefix
29
+ * every other finding in this build already uses, so nothing a reader has today
30
+ * is lost. `located` marks it done, so an error crossing two boundaries is
31
+ * decorated once; the fields are also left on the error, for a caller that
32
+ * wants to re-render it.
33
+ *
34
+ * @param {unknown} err - What was thrown.
35
+ * @param {string} [configPath] - The configuration file that was read.
36
+ * @returns {unknown} The same error, decorated when it named a field.
37
+ */
38
+ export function locateConfigError(err: unknown, configPath?: string): unknown;
17
39
  /**
18
40
  * Turn a parsed YAML configuration into the frozen one the engine reads.
19
41
  *
@@ -36,8 +36,16 @@ export function checkFormatting(root: string, opts?: {
36
36
  *
37
37
  * {@link MARKDOWNLINT_CONFIG} is passed as markdownlint's `optionsDefault`,
38
38
  * which is precisely the "shipped default, consumer overrides" behaviour the
39
- * command promises: a `.markdownlint-cli2.jsonc` found in the tree replaces it,
40
- * and a repository with none gets these rules.
39
+ * command promises: a repository with no configuration of its own gets these
40
+ * rules, and a `.markdownlint-cli2.jsonc` found in the tree overrides them.
41
+ *
42
+ * The override is **key by key, and each key wholesale** — which is not the same
43
+ * as "replaces it", and the difference is the one worth stating. A consumer file
44
+ * declaring only `ignores` keeps this rule set intact, including `default: false`
45
+ * and every per-rule option; but its `ignores` *replaces*
46
+ * {@link MARKDOWN_IGNORES} rather than extending it, so such a file must restate
47
+ * every shared entry it still wants. Omitting `CHANGELOG.md` there silently
48
+ * starts linting a generated file.
41
49
  *
42
50
  * @param {string} root - Repository to lint.
43
51
  * @param {object} [opts]