@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,845 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 8.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - d324b5a: Stop emitting five `system` fields no SoHL DataModel declares (#60).
8
+
9
+ The comparison this release adds was run against sohl's published schema at
10
+ 0.8.2 and found five, on its first run:
11
+
12
+ | type | emitted, undeclared |
13
+ | ---------------- | -------------------------------------------------------- |
14
+ | `affliction` | `isTreated` |
15
+ | `trauma` | `isTreated`, `isBleeding` |
16
+ | `projectilegear` | `impactBase.overrideDice`, `impactBase.overrideModifier` |
17
+
18
+ Every one was discarded when the document was constructed, on every compiled
19
+ document, with nothing said — which is the whole of what #60 is about.
20
+
21
+ **Two of them were never storable.** `isTreated` and `isBleeding` are _derived_
22
+ on the logic classes: `AfflictionLogic.isTreated` is `treatmentDate != null`,
23
+ and `TraumaLogic.isBleeding` is `bloodLossAdvanceDurationBase != null`. So the
24
+ builder wrote a constant Foundry threw away while the field it is computed from
25
+ went unwritten — both directions of the same defect, on the same field. Nothing
26
+ replaces them: an untreated affliction is one whose `treatmentDate` is unset,
27
+ which is already the initial value.
28
+
29
+ **Three were authored fields that vanished.** `trauma.isTreated`,
30
+ `trauma.isBleeding` and the two projectile overrides carried a frontmatter
31
+ `name`, so a note could write them — and the value went nowhere.
32
+
33
+ **The projectile overrides are removed rather than reported upstream as missing
34
+ fields**, because nothing anywhere wants them: no DataModel declares them, no
35
+ logic class reads them, no localization key names them. A
36
+ launcher-versus-ammunition override may be worth having, but it would have to be
37
+ designed in the system first, and a content builder cannot be where it is
38
+ invented.
39
+
40
+ **Checked before removing, because three were authored.** Dropping an authored
41
+ field turns a note that writes it from a silent loss into an unknown-key error,
42
+ so all six content trees were searched first: `sohl`, `thalorna`, `kethira`,
43
+ `harnensemble`, `harnadventures` and `hm3` write none of the five.
44
+
45
+ **Verified.** Against sohl 0.8.2's published schema, `undeclared` falls from
46
+ five to zero. The twelve remaining findings are the advisory direction —
47
+ fields a subtype declares that no builder emits, `treatmentDate` among them —
48
+ and are reported rather than fatal.
49
+
50
+ **Bump**
51
+
52
+ _Major._ Three of the five were part of the authored frontmatter vocabulary, and
53
+ a note writing one is now an unknown-key error rather than a value silently
54
+ dropped. No content in this organisation writes them, but a consumer outside it
55
+ would have to delete the keys — and would find its documents unchanged, since
56
+ they never reached a saved document in the first place.
57
+
58
+ ### Minor Changes
59
+
60
+ - d324b5a: Run the emitted-versus-declared field comparison in `content-build lint` (#60).
61
+
62
+ The comparison shipped in 7.0.0 with nothing calling it, because no system had
63
+ published its field sets yet. `sohl` now does, so this reads the artifact and
64
+ runs both directions.
65
+
66
+ **Which system, at which version, is already settled.** `stats.systemId` and
67
+ `stats.systemVersion` are derived rather than authored (#48) — a system package
68
+ is its own system, a module takes the one it requires, and the version is the
69
+ `compatibility.verified` it pins. So there is no second piece of configuration
70
+ to disagree with the first about whose schema to check against.
71
+
72
+ Two places to find it:
73
+
74
+ - **A system** reads its own `schema.json`, generated from its `src/`.
75
+ - **A module** reads the copy `content-build deps fetch` caches from the archive
76
+ of the version it pins — which is what makes the comparison happen at
77
+ `verified` rather than against whatever the system's `main` holds today.
78
+
79
+ **The fetch now keeps the schema.** Both fetch paths already unpacked the
80
+ dependency's archive, but only one kept the result: a download unzips into
81
+ `<cache>/package/` and leaves it, while `deps fetch --from` unzips into a
82
+ temporary directory and deletes it. A reader looking in the unpacked tree would
83
+ have found the schema for one and not the other — so it is copied to one known
84
+ place beside the extracted items instead.
85
+
86
+ **An absent schema is announced, not skipped in silence.** A system before its
87
+ first schema build, and a module pinning a version released before the artifact
88
+ existed, both have nothing to check against. That is not an error — but a check
89
+ that quietly does nothing is indistinguishable from one that passed, and this
90
+ issue exists because a defect went unnoticed for a release. So the run says so:
91
+
92
+ ```text
93
+ No published schema for sohl@0.8.2, so emitted `system` fields are unchecked.
94
+ A system generates its own; a module gets one from `content-build deps fetch`.
95
+ ```
96
+
97
+ **Also lands the five fixes the comparison found.** They were pushed after
98
+ #116's merge and so were not part of it: `affliction` and `trauma` emitted
99
+ `isTreated`, `trauma` emitted `isBleeding`, and `projectilegear` emitted
100
+ `impactBase.overrideDice` and `impactBase.overrideModifier` — none of which any
101
+ DataModel declares. Without them this change would have turned `sohl`'s own
102
+ lint red on the defects it was written to find.
103
+
104
+ **Verified against both shapes.** Against `sohl`, the run reports zero errors
105
+ and twelve advisory warnings, and `lint` passes. Against `sohl-kethira-basic`,
106
+ whose pinned 0.8.2 archive predates the artifact, it announces the skip and
107
+ reports only that repository's pre-existing findings.
108
+
109
+ **Bump**
110
+
111
+ _Minor._ New reporting on an existing command, and a fetch that keeps one more
112
+ file. The error direction can fail a build that passed before — but only for a
113
+ package whose dependency publishes a schema, which no released version does yet.
114
+ - ee9a9a8: Compare a builder's emitted `system` fields against the receiving DataModel
115
+ (#60) — the comparison half.
116
+
117
+ Foundry discards an unknown `system` key when a document is constructed, and
118
+ says nothing: the value is absent at load while the build that wrote it reported
119
+ success. Both directions of that mismatch have already happened here, both
120
+ compiled clean, and both were found by set-subtracting compiled documents'
121
+ `system` keys against `defineSchema()` **by hand**.
122
+
123
+ **The emitted half needs neither compilation nor parsing.** `field-spec.mjs`
124
+ already makes the field list the only statement of the mapping — "the
125
+ declaration is the builder" — so every `system` path a type can emit is
126
+ `field.to`, known statically. Nothing compiles a document to find out.
127
+
128
+ **The declared half arrives as data, pinned to the declared version.** A system
129
+ publishes its field sets as an artifact and this reads it, the shape the link
130
+ manifest already uses for addresses. Against `compatibility.verified`, never the
131
+ system's `main`: `affiliation.subType` _is_ defined on sohl `main` and simply
132
+ unreleased, while `sohl-kethira-basic` pins `0.8.2` — so a check against `main`
133
+ passes and the field still evaporates for all 21 of its deities.
134
+
135
+ **`own` and `inherited` are recorded apart, and the two directions read
136
+ different sets.** A subtype's schema spreads its parent's, so `notes`, `docHtml`
137
+ and the rest land on every subtype; they are the system's own runtime concerns
138
+ and no content builder is expected to emit them.
139
+
140
+ | direction | read against | severity |
141
+ | --------------------- | ------------------------------------------------------------------ | -------- |
142
+ | emitted, not declared | `own` ∪ `inherited` — the field must exist somewhere | error |
143
+ | declared, not emitted | `own` only — what the subtype adds is what its builder answers for | report |
144
+
145
+ Collapsing them would report every inherited field on every type: a wall of
146
+ findings that are all correct and none actionable.
147
+
148
+ **A false positive the real schema caught before this shipped.** Run against
149
+ sohl's actual `mysticalability`, the _declared, not emitted_ direction reported
150
+ `charges.value` and `charges.max` on a type that populates them correctly — the
151
+ builder writes `charges` as a whole object and never names the leaves beneath
152
+ it. A declared path is now covered when the builder emits any ancestor. The
153
+ first real schema tried produced two false findings, which is exactly the kind
154
+ that teaches people to ignore a report.
155
+
156
+ **Verified against the real declarations.** With sohl's `mysticalability`
157
+ schema transcribed from source, the comparison reports nothing; with #35's
158
+ `assocMysteryCode` reinstated, it reports exactly that field.
159
+
160
+ **What this does not do yet.** It does not read an artifact from disk, and
161
+ nothing runs it in a build — those wait on a system actually publishing its
162
+ schemas, which is sohl's half and a separate change. The comparison, the format
163
+ and both regression cases are pinned here so that half has something to satisfy.
164
+
165
+ **Bump**
166
+
167
+ _Minor._ New surface — `engine/schema-check.mjs` and its exports — and nothing
168
+ existing changes behaviour. No consumer runs the comparison until an artifact
169
+ exists to run it against.
170
+ - 14cd092: Add `package-build schema`, so a system publishes its DataModel field sets from
171
+ here rather than from its own copy of an extractor.
172
+
173
+ The consuming half of this contract shipped in 7.0.0: `content-build lint`
174
+ subtracts what a package's builders emit from what a document will actually
175
+ receive, because Foundry discards an unknown `system` key at construction and
176
+ says nothing about it. The producing half lived in the first system that needed
177
+ it, which meant the second system to need it would have copied 491 lines — and,
178
+ worse, would have copied a hardcoded `SCHEMA_ARTIFACT_VERSION`, a constant this
179
+ package owns. Two producers stamping a third repository's constant by hand is
180
+ the drift worth removing before it happens rather than after: the version is now
181
+ imported by the producer, not restated.
182
+
183
+ **Why here and not in each system.** A DataModel's schema is only introspectable
184
+ inside Foundry — `defineSchema()` returns field classes that do not exist in
185
+ Node — so the field sets have to be read out of the source as an AST.
186
+ TypeScript's parser reads plain JavaScript too, and this package already pins
187
+ that compiler for `coverage.mjs`. Putting the reader here means a
188
+ JavaScript-only system does not acquire a TypeScript pin merely to describe its
189
+ own data models.
190
+
191
+ **Declared, because the two layouts in use disagree.**
192
+
193
+ ```yaml
194
+ packageBuild:
195
+ schema:
196
+ Item: { from: module/data/item-models.js, registry: itemModels }
197
+ Actor: { from: module/data/actor-models.js, registry: actorModels }
198
+ ```
199
+
200
+ One system keeps both registries in a single configuration module; the other
201
+ keeps one per file. Neither layout is more correct, and a convention guessing
202
+ between them would fail by reading _nothing_ rather than by complaining — which
203
+ is the worst failure available here, since an empty schema passes every check.
204
+ A registry that maps nothing is refused for the same reason.
205
+
206
+ **Four spellings of inheritance, all followed.** `...Super.defineSchema()`,
207
+ `...super.defineSchema()`, `Object.assign(super.defineSchema(), {…})`, and a
208
+ subclass with no `defineSchema()` at all. The last is a real and complete
209
+ declaration — `class MiscGearModel extends GearModel {}` — and reading it as
210
+ "declares nothing" would make every field of a whole subtype look undeclared.
211
+ `SchemaField` nesting is recorded as dotted paths whether written bare or as
212
+ `fields.SchemaField`, since both spellings are in use.
213
+
214
+ **A schema with nothing to compare against now says so.** The emitted side of
215
+ the check is the `fields:` of `itemBuilders`, so a package whose compendium
216
+ content is committed JSON rather than built from field declarations has an empty
217
+ one — and every field the system declares would have been reported as unemitted.
218
+ That is hundreds of findings whose only content is that the package does not
219
+ build documents that way, which is not news and not a defect. It is announced
220
+ once instead, for the same reason the absent-schema case is: a check that quietly
221
+ does nothing reads exactly like one that passed. The moment a builder declares
222
+ `fields:`, the comparison starts running on its own.
223
+
224
+ **Bump**
225
+
226
+ _Minor._ A new command, a new optional configuration key, and a `lint` that
227
+ reports strictly less than before.
228
+
229
+ ## 7.0.0
230
+
231
+ ### Major Changes
232
+
233
+ - a136429: Separate declaring a system from requiring one, and stamp `_stats` per pack
234
+ (#48).
235
+
236
+ A module shipping content for two systems could not say so. The only place to
237
+ state a system version was `relationships.systems`, and that list is a
238
+ **restriction**: Foundry's `supportsSystem` drops a package from any world whose
239
+ system it does not name. So the two needs were in direct conflict — name your
240
+ systems and become unloadable elsewhere, or stay loadable and stamp nothing.
241
+
242
+ **`harn-ensemble` is the case, and it is live.** It declares an `actors-hm3`
243
+ pack and an `actors-sohl` pack, and resolves to:
244
+
245
+ ```json
246
+ { "statsSystemId": null, "statsSystemVersion": null }
247
+ ```
248
+
249
+ Nothing stamped, on content that was certainly built against `hm3 1.6.3` and
250
+ `sohl 0.8.2`. It takes that path deliberately, because declaring the two systems
251
+ would hide the module from every other world — including the ones that want only
252
+ its system-neutral journals pack.
253
+
254
+ **The split.**
255
+
256
+ | | describes | gates |
257
+ | ----------------- | -------------------------------------------- | --------------------------- |
258
+ | `systems:` | which systems this package can stamp against | **nothing** |
259
+ | `requiresSystem:` | — | where the package will load |
260
+
261
+ Naming a system under `systems:` restricts nothing. `requiresSystem` is separate
262
+ and optional, and emits the `relationships.systems` entry Foundry reads —
263
+ _reusing_ the declaration rather than restating it, because
264
+ `stats.systemVersion` sat at `0.6.0` for four releases when a transcription was
265
+ free to disagree with what it copied.
266
+
267
+ ```yaml
268
+ systems:
269
+ hm3: { compatibility: { minimum: "1.6.3", verified: "1.6.3" } }
270
+ sohl: { compatibility: { minimum: "0.8.2", verified: "0.8.2" } }
271
+ requiresSystem: null # optional; omitted, the package loads anywhere
272
+ ```
273
+
274
+ **A pack's `system:` now selects what stamps its documents.** `_stats` was one
275
+ memoised block for the whole package, so every document in every pack was
276
+ stamped identically. It is per pack now: `statsForPack()` resolves the pack's
277
+ declared system through `systems:`, and `BasePackCompiler` exposes it as
278
+ `this.stats`, memoised per instance — one pass, one pack, one system.
279
+
280
+ **`systemId` travels with `systemVersion`.** They are one decision, so where one
281
+ is omitted both are. Stamping a per-pack version against a package-wide id would
282
+ emit `systemId: sohl, systemVersion: 1.6.3` on HM3 documents — a plausible lie,
283
+ which is worse than the missing value #43 fixed, because nothing about it looks
284
+ wrong.
285
+
286
+ **A name that resolves to nothing is an error.** A pack's `system:` must name a
287
+ declared system; `requiresSystem` must too; and with a gate set, a pack naming a
288
+ _different_ system is refused outright — Foundry would hide the whole package
289
+ from any world that pack could have appeared in, so it would ship and be
290
+ unreachable.
291
+
292
+ **Nothing that ships today moves.** All six consumers were resolved before and
293
+ after; none declares a `systems:` block yet, so every pack still falls through to
294
+ the package-wide block it used before:
295
+
296
+ | package | `systems:` | `requiresSystem` | stamps | packs unchanged |
297
+ | -------------------- | ---------: | ---------------- | -------------- | --------------- |
298
+ | `sohl` | 0 | — | `sohl / 0.8.2` | yes |
299
+ | `sohl-thalorna` | 0 | — | `sohl / 0.8.2` | yes |
300
+ | `sohl-kethira-basic` | 0 | — | `sohl / 0.8.2` | yes |
301
+ | `harn-ensemble` | 0 | — | `— / —` | yes |
302
+ | `harn-adventures` | 0 | — | `— / —` | yes |
303
+ | `hm3` | 0 | — | `hm3 / 1.6.3` | yes |
304
+
305
+ **`stats.systemId` and `stats.systemVersion` are refused outright.** Authoring a
306
+ derived value is an error rather than an override, which is the rule this
307
+ configuration already applies elsewhere — and the reason is the same one that let
308
+ `stats.systemVersion` sit at `0.6.0` for four releases: a transcribed copy is
309
+ free to drift from what it copied, and nothing reads a stamped `_stats` until
310
+ something migrates on it. The refusal names the key, its line, and what supplies
311
+ it now.
312
+
313
+ The value still has to reach the validator from the loader, which is the half
314
+ that may read the adjacent `package.json`. It travels under a **symbol**, so the
315
+ channel is not a second, forgeable spelling of the key just refused: a symbol
316
+ cannot be written in YAML and does not appear in `Object.keys`.
317
+
318
+ **`relationships.systems` keeps working, and still answers.** It carries
319
+ `itemCatalog` — a separate concern this split does not replace — so a repository
320
+ using it would otherwise have to restate its compatibility under `systems:`
321
+ purely to keep stamping, which is the duplication the change exists to remove. A
322
+ lone relationship is a declaration as much as a gate, so it derives `systemId`
323
+ too. Several have no single answer and get none.
324
+
325
+ **Every consumer was migrated in the same change.** One line each:
326
+
327
+ ```diff
328
+ stats:
329
+ - systemId: sohl
330
+ lastModifiedBy: sohlbuilder00000
331
+ ```
332
+
333
+ `sohl` and `hm3` are systems and are their own system by construction;
334
+ `sohl-thalorna` and `sohl-kethira-basic` derive it from the single system
335
+ relationship they already declare; `harn-ensemble` and `harn-adventures` declared
336
+ none and were already clean.
337
+
338
+ **Bump**
339
+
340
+ _Major._ `stats.systemId` and `stats.systemVersion` were accepted and are now
341
+ refused, so a configuration that resolved before can fail — which is the
342
+ definition this repository uses. Every HeroicLands consumer is migrated in
343
+ lockstep and verified to stamp exactly what it stamped before, but a consumer
344
+ outside that set must delete the two keys.
345
+
346
+ Part of #57, the third of its three keys that both describe and gate. #56 is
347
+ done, #49 shipped in 6.2.0, and this is #48.
348
+
349
+ ### Minor Changes
350
+
351
+ - db40b24: Address another package's landing without naming a host (#87).
352
+
353
+ A link from one package's page to another package's landing had no form but a
354
+ hardcoded absolute URL, and the homepage link check (#54) exempted one — a
355
+ finding whose fix does not exist is noise. `sohl-kethira-basic`'s homepage writes
356
+ `https://www.heroiclands.org/sohl/` twice.
357
+
358
+ **The premise the exemption rested on is true, and does not lead where it looked
359
+ like it led.** A landing is in no link manifest: it compiles to no document and
360
+ is entered in no index. The reading that follows is that nothing can resolve it.
361
+ But a landing's address is not a _note's_ address — it is the **package's**, and
362
+ `PACKAGE_BASE` has recorded where each package is served all along. Consulting it
363
+ walks no tree, reads no manifest and builds no index, which is exactly why the
364
+ mechanism survives the fence: `kethira` and `harnadventures` publish a homepage
365
+ and nothing else, and that mode never walks a content tree.
366
+
367
+ **So the authored form is the absolute URL with the host struck off.** `/sohl/`
368
+ in a body, or `href: /sohl/` in a card. Both were already accepted here — nothing
369
+ had ever named one as the form to use, which is the whole of what was missing.
370
+
371
+ | Field | Cross-package landing | Why |
372
+ | ------- | --------------------- | ----------------------------------------- |
373
+ | body | `[SoHL](/sohl/)` | emitted verbatim, resolved by the browser |
374
+ | `href:` | `/sohl/` | "already resolved, used verbatim" |
375
+ | `url:` | **not expressible** | package-relative by construction |
376
+
377
+ `url:` cannot leave its own package, so a `url:` naming another package's landing
378
+ is now reported with the field as the fix rather than a path — previously it was
379
+ told to "write `/`", which is nonsense arrived at by taking the path after the
380
+ prefix when there is nothing after the prefix.
381
+
382
+ **The base comes from the roster, not from the prefix.** The finding names
383
+ `PACKAGE_BASE[pkg]`, so a package the roster relocates (`/setting/thalorna/`) is
384
+ addressed where it actually is. That is the property the manifest enforces
385
+ everywhere else — the address published is the address emitted — reaching these
386
+ links for the first time.
387
+
388
+ **Roster for landings only.** Widening the package set the other rules read would
389
+ have them offer manifest-based advice about packages no manifest is vendored for.
390
+ An in-site path naming no known package is still left alone: several surfaces a
391
+ landing routes to are built by other tools, and this build does not hold the set
392
+ of published pages.
393
+
394
+ **Measured across every homepage authored today.** All five were run before and
395
+ after, each under its own `package-build.config.yaml`:
396
+
397
+ | Package | Before | After | |
398
+ | ------------------------- | ------ | ----- | ----------------------- |
399
+ | `sohl-kethira-basic` | 0 | **2** | the two the issue names |
400
+ | `sohl-thalorna` | 0 | 0 | |
401
+ | `harn-ensemble` | 0 | 0 | |
402
+ | `harn-adventures` | 0 | 0 | |
403
+ | `HarnMaster-3-FoundryVTT` | 0 | 0 | |
404
+
405
+ Kethira vendors no manifest at all — it is homepage-only — so it is also the
406
+ proof that the roster reaches where an index does not. Applying the fix the
407
+ finding names takes it back to 0.
408
+
409
+ **Sequencing, stated because it decides the bump.** These are hard errors:
410
+ `severity: "error"`, counted into `failures`, `exitCode 1`. By this repository's
411
+ own rule — a new hard error is breaking only if it fails a previously-passing
412
+ consumer — this is minor **only once kethira's two lines are converted**, and
413
+ major if it ships before them. The conversion does not wait on this release:
414
+ `/sohl/` already passes under the current published version, verified, so it can
415
+ land in `sohl-kethira-basic` immediately and independently. It must land first.
416
+
417
+ **Bump**
418
+
419
+ _Minor, not patch, and not major._ Not patch: a check that previously passed a
420
+ page can now fail it. Not major, on the condition above — with kethira converted,
421
+ every homepage authored today passes before and after, and no export, option or
422
+ emitted document changes shape.
423
+ - 35bde43: Locate a configuration error in the file it was written in (#95).
424
+
425
+ Every check across `content-config.mjs` and `config.mjs` — 81 of them — reports
426
+ through one `fail()`, which named the offending key's **dotted path** and
427
+ nothing else. That is a good description and a bad locator: nothing in the line
428
+ is a path an editor can open or a CI annotator can resolve, in a file that runs
429
+ to 400 lines with fourteen sibling entries under `sections:` alone, several of
430
+ them flow-mapped onto one line.
431
+
432
+ The path now rides on the error as a `field`, and the loader that read the file
433
+ resolves it — through `positionOfYamlPath`, the same locator `manifest`'s
434
+ `packFolders` findings already use — so all 81 come out in the
435
+ `file:line:column: severity: message` form every other finding uses, path first:
436
+
437
+ ```text
438
+ package-build.config.yaml:382:64: error: package-build config: `site.sections.being.descrption` is not a recognized option (expected one of: title, banner, description).
439
+ ```
440
+
441
+ **Located at the boundary, not at the check.** `content-config.mjs` is the leaf
442
+ an `.mjs` configuration imports and performs no I/O, so it attaches the path and
443
+ `configFromData` — which knows the file — formats it. `config.mjs`'s pure
444
+ `resolvePackageBuildConfig` is unchanged for the same reason;
445
+ `loadPackageBuildConfig` is where its findings are located.
446
+
447
+ **Positions are dropped, never guessed.** A required key the file never declared
448
+ has no node of its own, so the position names the **mapping it belongs in**, one
449
+ level up and no further — that entry is a real node and the one the reader must
450
+ edit. A missing _top-level_ key has nothing above it but the document, and an
451
+ `.mjs` configuration has no YAML to resolve a path against at all (parsing
452
+ JavaScript as YAML would resolve some paths to lines that mean nothing). Both
453
+ report `package-build.config.yaml: error: …` — the file, without a line.
454
+
455
+ Both command lines print a located failure unprefixed, since `package-build: `
456
+ and `loglevel`'s `[timestamp] [ERROR]:` occupy exactly the position a parser
457
+ reads the path from.
458
+
459
+ **Additive.** A valid configuration resolves exactly as before; only the text of
460
+ a rejection changes, and it keeps the body it had. `positionOfYamlPath` gains an
461
+ optional `{ key: true }` — report where a key is _declared_ rather than where
462
+ its value sits, which is what a message naming a field wants — and
463
+ `engine/diagnostics` gains `yamlKeyPath`, the one translation between a dotted
464
+ path and a YAML key path.
465
+ - b7ad450: Compile actors without an Item pack of this package's own (#49).
466
+
467
+ The actors pass threw unless the package declared at least one pack of type
468
+ `Item`. An Item pack is system-bound by construction — Foundry requires `system`
469
+ on Item packs and on few others — so the guard asked a deliberately
470
+ system-agnostic module to declare the very thing it exists not to depend on.
471
+
472
+ **This is not hypothetical, and it is not a future case.** `harn-ensemble`
473
+ declares two Actor packs and no Item pack at all in its
474
+ `package-build.config.yaml` today. Run against that configuration, the compiler
475
+ does not start:
476
+
477
+ ```text
478
+ packs declared : actors-hm3(Actor), actors-sohl(Actor)
479
+ itemPackJsonDirs : []
480
+ Actors : THREW — Actors compiler requires `itemsSourceDirs` …
481
+ ```
482
+
483
+ Its 2,512 beings resolve their embedded items — `skill:awar`, `attribute:str`,
484
+ `weapongear:…` — against the `sohl` and `hm3` catalogues through
485
+ `foreignSourceDirs`. The optional mechanism is the one that matters there; the
486
+ mandatory one had nothing to contribute.
487
+
488
+ **The guard did not test what it claimed.** It counted _declared directories_,
489
+ not resolvable items. An Item pack containing no documents satisfied it, while a
490
+ being naming an item nothing defines still failed later — so it neither
491
+ prevented the failure it named nor reported it where it happened. And its
492
+ remedy, "declare at least one pack of type `Item`", is the opposite of the fix
493
+ for a system-agnostic package.
494
+
495
+ **The condition actually cared about was already checked, at the site of the
496
+ mistake.** `resolveEmbedded` reports each unresolved `(type, shortcode)` by
497
+ name, with the being as context, and counts it — and those counts aggregate into
498
+ `totalErrors`, so a package genuinely missing an item still fails the build:
499
+
500
+ ```text
501
+ Bandit: no predefined item for "skill:awar"
502
+ ```
503
+
504
+ That is the same line #43 and the frontmatter-lint work drew: a structural
505
+ precondition that is cheap to state is not the condition you care about, and
506
+ reporting where the mistake is beats refusing to start.
507
+
508
+ **Nothing tightens.** `itemsSourceDirs` defaults to `[]` and is otherwise
509
+ unchanged; `foreignSourceDirs` is untouched; `itemPackJsonDirs` already returned
510
+ an empty list for a repository with no Item packs, and only the constructor
511
+ rejected it. A package that declares Item packs behaves exactly as before — the
512
+ 1,647 existing tests pass unchanged, with four added for the empty case and for
513
+ the point-of-use error that replaces the guard.
514
+
515
+ **Bump**
516
+
517
+ _Minor, not patch._ No consumer that compiled before fails now — the change only
518
+ removes a refusal — but a configuration that was rejected is now supported, which
519
+ is new surface rather than a repair to existing surface. Not major for the same
520
+ reason: nothing that was accepted stops being accepted.
521
+
522
+ Part of #57, which is the same defect in three places: a key that both describes
523
+ and gates, so the legitimate case cannot be expressed. Here the gate is removed
524
+ and the description — which Item packs this package ships — is left saying only
525
+ that.
526
+
527
+ ### Patch Changes
528
+
529
+ - 8aab711: Bump `glob` from 11.1.0 to 13.0.6.
530
+
531
+ **Both majors are changes to glob's command-line program, not its library.**
532
+
533
+ - **12** removed the unsafe `--shell` option, keeping it only on shells where it
534
+ can be implemented safely (the remediation for GHSA-5j98-mcp5-4vw2, whose
535
+ mitigation 11.1 had introduced).
536
+ - **13** moved the CLI out to a separate `glob-bin` package.
537
+
538
+ This repository never invokes that program. The single use of the library is
539
+ `globSync(patterns, { cwd, absolute: true })` in `readMatching`
540
+ (`bin/package-build.mjs`), which is untouched across both majors — no workflow,
541
+ hook, or script calls `glob` from a shell, so the removed binary is not a
542
+ dependency this package had.
543
+
544
+ **Verified rather than assumed.** `globSync` was exercised under 13.0.6 with the
545
+ options `readMatching` actually passes, returning the same absolute paths; the
546
+ repository's 1,642 tests pass unchanged.
547
+
548
+ **Bump**
549
+
550
+ _Patch, not minor._ Nothing in this package's surface moves, and the majors are
551
+ the library's own. Worth noting for a consumer only in one case: a repository
552
+ that installed `glob` transitively through this package and relied on
553
+ `node_modules/.bin/glob` being present must now depend on `glob-bin` directly.
554
+ That was never a supported edge of this package, and no HeroicLands repository
555
+ does it.
556
+ - 8e0778e: Bump `markdown-it` from 14.3.0 to 15.0.0.
557
+
558
+ This package's entire use of the library is `markdownit({ html: true })` and
559
+ `md.render(body)`, at three call sites — `engine/helpers.mjs`,
560
+ `engine/journals.mjs` and `sohl/actors.mjs`. Nothing overrides a renderer rule,
561
+ installs a plugin, or reaches into the parser.
562
+
563
+ **Every breaking change in 15.0.0 lands outside that surface.**
564
+
565
+ | Breaking change | Why it does not reach here |
566
+ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
567
+ | `linkify-it` → v6: no fuzzy links, no auth check, CJK link termination | `linkify` defaults to `false` and is never enabled, so the linkifier never runs |
568
+ | Package-internal subpath exports (`markdown-it/lib/*`) removed | Only the package root is imported |
569
+ | `validateLink`/`normalizeLink`/`normalizeLinkText` moved to prototype | None is read, assigned, or overridden |
570
+ | `StateBlock#ddIndent` removed | No plugin is installed, `markdown-it-deflist` included |
571
+ | Root now resolves to prebuilt `dist/` rather than raw sources | Import is by package name; the resolved path was never depended on |
572
+
573
+ **Verified rather than assumed.** The same corpus was rendered through 14.3.0
574
+ and 15.0.0 and diffed byte-for-byte, covering exactly the constructs the release
575
+ touched — bare URLs, autolinks, email-shaped text, reference links and their
576
+ definitions, CJK adjacent to a URL, hard line breaks, raw HTML, tables, nested
577
+ lists, and both fenced and indented code. Output is identical. The repository's
578
+ 1,642 tests pass unchanged.
579
+
580
+ **Bump**
581
+
582
+ _Patch, not minor._ No export, option, or emitted document changes shape, and no
583
+ behaviour a consumer can observe moves. The version is a major on the library's
584
+ own surface, none of which this package presents onward.
585
+ - 6318722: Add `.github/dependabot.yml`. This repository had none, so nothing proposed a
586
+ dependency update — not npm, not GitHub Actions.
587
+
588
+ That matters more here than in a consumer: this is the toolchain every other
589
+ HeroicLands package builds with, and it is published to npm, so its dependency
590
+ tree reaches every consumer's build and everyone who installs it. The
591
+ consuming repositories are already covered; this producer was the one link in
592
+ the chain nothing watched.
593
+
594
+ `@foundryvtt/foundryvtt-cli` and `classic-level` are each an ungrouped
595
+ single-package entry, ordered ahead of the housekeeping catch-all, for every
596
+ update type including minor and patch. Both sit directly on the compendium
597
+ pack pipeline, and a version bump that silently changes how a pack compiles is
598
+ exactly the failure this repository's byte-level output comparisons exist to
599
+ catch — grouping either into a housekeeping pull request would hide which
600
+ dependency caused a regression.
601
+
602
+ No `ignore` entries: nothing here has a known-bad range to record.
603
+ `typescript` is a direct dependency, but `build.yml`'s "Declaration emit" step
604
+ already gates a breaking bump on every pull request.
605
+
606
+ Closes #100.
607
+ - cd32ea1: Render `[[target|]]` as the target's name on the web, as the packs already do
608
+ (#113).
609
+
610
+ A wikilink with an explicit but empty label produced an **empty anchor** —
611
+ `[](/rules/sohl-shock/)`, a link with no clickable text — silently, through
612
+ every site build:
613
+
614
+ ```text
615
+ [[doc-shock]] => [Shock](/rules/sohl-shock/)
616
+ [[doc-shock|]] => [](/rules/sohl-shock/) ← before
617
+ [[doc-shock|]] => [Shock](/rules/sohl-shock/) ← after
618
+ [[#sec|]] => [](#sec) ← before
619
+ ```
620
+
621
+ **The two resolvers had drawn the same line in two places, differently.**
622
+ `parseWikilink` distinguishes "no label" from "empty label" deliberately, and
623
+ its docstring says so — _"`null` and `""` differ: an author may write
624
+ `[[x|]]`"_. The packs resolver honoured that by testing falsiness. The web
625
+ resolver used `??`, which falls through on `null` only, so `""` survived to the
626
+ output at three sites: the same-page anchor, the resolved link, and the
627
+ unresolved link.
628
+
629
+ That is the third instance of exactly the drift `wikilink-syntax.mjs` was
630
+ created to stop — its module docstring opens on the two copies of the link
631
+ parser having already diverged. The parse was centralised; the _interpretation
632
+ of the parsed parts_ was not, and it drifted in the one case the docstring
633
+ names.
634
+
635
+ **So the reading is stated once.** `authoredLabel()` joins the syntax module and
636
+ both resolvers consult it, rather than each deciding what an empty label means.
637
+ `labelled` is untouched and still separates `[[x]]` from `[[x|]]`, which is what
638
+ #1409 actually depends on — an unlabelled `[[Shock State]]` still shows the
639
+ author's own prose rather than the canonical name.
640
+
641
+ **Why it is worth a release rather than waiting.** `[[x|]]` becomes load-bearing
642
+ under the four-segment address grammar (#59): the pipe is what distinguishes an
643
+ address lookup from an alias lookup, so `[[target|]]` is the canonical way to
644
+ write an address that displays its target's name — and the planned migration
645
+ rewrites every authored address link into that form. Converting the corpus into
646
+ a form that renders an empty anchor would be a corpus-wide regression, so this
647
+ has to land first.
648
+
649
+ **Bump**
650
+
651
+ _Patch._ A defect fix with no new surface. `authoredLabel` is exported because
652
+ both resolvers import it, not as a feature for consumers; no option, address, or
653
+ emitted document changes shape, and no link that renders correctly today renders
654
+ differently after.
655
+ - bc2c5c8: Bump `@types/node` in the lockfile for development tooling maintenance.
656
+ - 1a4d882: Hold `typescript` at major 6 in Dependabot, because TypeScript 7 removes the
657
+ compiler API `coverage.mjs` parses with.
658
+
659
+ Dependabot proposed 6.0.3 → 7.0.2 (#105). It cannot be taken.
660
+
661
+ **TypeScript 7 is the native port, and its npm package no longer ships the
662
+ JavaScript compiler API.** The `"."` export resolves to `lib/version.cjs`, whose
663
+ entire surface is `version` and `versionMajorMinor`.
664
+
665
+ `coverage.mjs` uses that API as a _parser_, not as a compiler: it reads
666
+ localization keys out of a consumer's `src/**/*.{ts,mjs}` by walking a real AST,
667
+ deliberately down one path so JavaScript and TypeScript cannot drift. Under 7.0.2
668
+ the AST **vocabulary** survives behind `typescript/unstable/ast` — `ScriptTarget`
669
+ and every `isX` guard the scan uses — but the three things that actually drive it
670
+ exist nowhere in the JS surface:
671
+
672
+ | Needed by `coverage.mjs` | In 7.0.2 |
673
+ | ------------------------------------------------------------- | ------------------------------------------ |
674
+ | `createSourceFile` | **missing** — only a factory of that name¹ |
675
+ | `forEachChild` | **missing** |
676
+ | `flattenDiagnosticMessageText` | **missing** |
677
+ | `ScriptTarget`, `isStringLiteral`, `isPropertyDeclaration`, … | present, behind `unstable/ast` |
678
+
679
+ ¹ 7's `createSourceFile` assembles a SourceFile from statements already parsed.
680
+ It is not a parser.
681
+
682
+ **The gate this file predicted would catch it did not.** The previous comment in
683
+ `dependabot.yml` recorded no ignore entry on the reasoning that `build.yml`'s
684
+ "Declaration emit" step already guards a breaking `typescript` bump. That step
685
+ passes clean under 7.0.2 — `tsc` still emits every `.d.mts`. What failed was
686
+ `npm test`: 11 failures in `tests/coverage.test.ts`, all
687
+ `TypeError: Cannot read properties of undefined (reading 'Latest')`. A dependency
688
+ can be load-bearing in two unrelated ways at once, and the file named only one of
689
+ them. That correction is now written where the wrong prediction was.
690
+
691
+ **Why an ignore rather than a migration.** Parsing in 7 lives in the Go binary,
692
+ reachable only through `unstable/sync`'s Project/Program API. Adopting it would
693
+ put a subprocess and a virtual filesystem inside a module whose stated contract
694
+ is "everything here is pure — source text in, references or findings out", in
695
+ exchange for an API whose own export path says `unstable`. Acorn is not a
696
+ substitute either: the default scan glob is `src/**/*.{ts,mjs}`, and the largest
697
+ consumer's sources are TypeScript.
698
+
699
+ That migration is worth doing when the API stabilises. It is not a dependency
700
+ bump, and it should not arrive as one.
701
+
702
+ **Scope of the hold.** Majors only — minor and patch releases within 6 still
703
+ arrive on the weekly schedule. The entry names the two conditions that lift it:
704
+ a stable in-process parse entry point, or a `coverage.mjs` that no longer needs
705
+ one.
706
+
707
+ **Bump**
708
+
709
+ _Patch._ Nothing shipped changes. `.github/dependabot.yml` is this repository's
710
+ own automation and sits outside `files`; the `typescript` range in
711
+ `package.json` is untouched, because `^6.0.3` already excludes 7 — the entry
712
+ stops the pull request being reopened, it does not change what resolves.
713
+ - f3167c3: Read the changesets action's `pr-number` output, so the step that marks the
714
+ Version Packages pull request stops being dead code (#84).
715
+
716
+ `release.yml` pins `changesets/action@v2` but read v1's `pullRequestNumber`. An
717
+ unset output evaluates to the empty string rather than erroring, so the guard was
718
+ `'' != ''` — always false. The step has never run once.
719
+
720
+ **The failure was silent by construction, which is why it survived three
721
+ releases.** A misspelled output does not fail a workflow; it disappears. `v3.4.0`,
722
+ `v4.0.0` and `v5.0.0` were all cut with this step skipped, and every run reported
723
+ green.
724
+
725
+ **Only one of the three names actually moved.** Checked against v2's own
726
+ `action.yml` rather than against the assumption that v2 kebab-cased everything:
727
+
728
+ | v1 | v2 | Used here |
729
+ | ------------------- | -------------------- | ------------------------------------- |
730
+ | `pullRequestNumber` | `pr-number` | yes — the two lines this change fixes |
731
+ | `publishedPackages` | `published-packages` | no |
732
+ | `hasChangesets` | `has-changesets` | no |
733
+ | `published` | `published` | yes — **unchanged**, and left alone |
734
+
735
+ `published` is the one name v2 kept. A blanket kebab-case sweep of this file —
736
+ the obvious reading of "rename the v1 outputs" — would have broken the one step
737
+ that was working.
738
+
739
+ **Bracket notation, not `outputs.pr-number`.** A hyphen is the subtraction
740
+ operator in an Actions expression, so the dotted form parses as
741
+ `outputs.pr - number`: a second silent-ish defect sitting directly behind the
742
+ first. `steps.changesets.outputs['pr-number']` is the form that means what it
743
+ reads as.
744
+
745
+ **What this repairs.** The Version Packages pull request is opened by
746
+ `GITHUB_TOKEN`, and GitHub deliberately starts no workflow runs from that token,
747
+ so the required `Changeset declared` context never reports on it. This step
748
+ exists to post that status. With it inert, every Version Packages pull request
749
+ has needed the check waived or force-merged by hand.
750
+
751
+ **Not yet demonstrated running, and stated rather than glossed.** The acceptance
752
+ criterion asks for a run where the step is not `skipped`, and that can only
753
+ happen on `main`, on the next release that opens a Version Packages pull request
754
+ — this change cannot produce one from a branch. The expression is verified by
755
+ parsing the workflow and by v2's manifest; the live proof arrives with the next
756
+ bump.
757
+
758
+ **Sibling repositories are fixed individually, not swept from here.** The same
759
+ defect is open on `harn-ensemble` (#13) and `HarnMaster-3-FoundryVTT` (#427),
760
+ where it is more severe — there the misspelling gates the release itself, and no
761
+ release is ever cut. Each repository owns its own copy of `release.yml`; this one
762
+ is not a reusable workflow. Turning it into one is a real improvement and a
763
+ separate change, and folding it into a two-line fix would put a shared release
764
+ pipeline into production on the back of a typo correction.
765
+
766
+ **Bump**
767
+
768
+ _Patch, not minor._ Nothing this package exports, emits, or documents for a
769
+ consumer changes. The file is this repository's own release plumbing, and it is
770
+ not shipped: `.github` is outside `files`.
771
+ - 2bdc792: Make `lint:markdown` pass, and run it in CI (#92).
772
+
773
+ This repository provides `content-build markdown` and was the one repository that
774
+ never ran it. The script existed, failed, and was executed by nothing — no
775
+ aggregate `lint` script, and no workflow naming it.
776
+
777
+ **The finding count in the issue is wrong, and the correction changes the
778
+ decision.** 117 is what `npx markdownlint-cli2 CHANGELOG-content-build.md`
779
+ reports — markdownlint's _default_ rule set, which this toolchain deliberately
780
+ turns off (`default: false`, then each rule enabled by name). Under the rules the
781
+ repository actually uses, `npm run lint:markdown` reports **three**: two
782
+ `MD001` heading skips and one `MD034` bare URL. The issue's "fix it — 117
783
+ mechanical findings" and "exclude it — 117 is too many to fix" were both
784
+ arguments about a number that was never the repository's.
785
+
786
+ **Excluded anyway, and not because three is still too many.**
787
+ `CHANGELOG-content-build.md` is the published changelog of
788
+ `@heroiclands/content-build` — a **deprecated repository**, absorbed into this
789
+ one at 3.0.0 (#32). It is frozen, not merely generated: exactly one commit has
790
+ ever touched it, nothing regenerates it, and it ships only because `files` lists
791
+ it. Its three findings are facts about what content-build published. Rewriting
792
+ them would edit a historical record to satisfy a rule about prose nobody will
793
+ write again, and would put a style-only commit in the blame of a file whose whole
794
+ value is being what was published. It is the same class as `CHANGELOG.md`, which
795
+ the shared default already ignores for the weaker reason that the next release
796
+ rewrites it.
797
+
798
+ **Declared locally, so no consumer moves.** The exclusion is a new
799
+ `.markdownlint-cli2.jsonc` in this repository, not an entry added to the shared
800
+ `MARKDOWN_IGNORES`. One repository's retired filename does not belong in
801
+ configuration six repositories consume, and the shared rule set is unchanged for
802
+ all of them.
803
+
804
+ **What that file had to get right, and what it documents.** `content-build
805
+ markdown` passes the shared rules as markdownlint-cli2's `optionsDefault`, and a
806
+ consumer file merges over them **key by key, each key wholesale**:
807
+
808
+ | Declared locally | Effect on the shared default |
809
+ | ---------------- | --------------------------------------------------------------------- |
810
+ | only `ignores` | rule set survives intact — `default: false` and every per-rule option |
811
+ | `ignores` | **replaces** `MARKDOWN_IGNORES`; it does not extend it |
812
+
813
+ So the local file restates `CHANGELOG.md`. Verified rather than assumed — with
814
+ that entry dropped, `lint:markdown` reports ten findings in `CHANGELOG.md`; with
815
+ a probe file present, `MD049` still fires with its shared `underscore` option
816
+ while `MD013` stays silent, which is what proves the rule set was not replaced.
817
+
818
+ `engine/prose-lint.mjs` said a consumer config "replaces it", which is the
819
+ reading that would send the next person to add the ignore to the shared default
820
+ or to a full config copy. Its docstring now states the key-by-key rule and the
821
+ `ignores` trap by name.
822
+
823
+ **Wired as a separate CI step, not folded into the chain.** `npm run lint` is
824
+ added for running both checks locally in one command, but `build.yml` gets a
825
+ `Markdown` step of its own beside `Formatting`. The aggregate chains with `&&`,
826
+ so a formatting failure would short-circuit it and hide every markdown finding
827
+ behind it; as two steps the failing one is named in the checks UI.
828
+ `lint:markdown:fix` is added too — the issue referred to it, and it did not exist.
829
+
830
+ **The exclusion cannot rot silently.** If a future markdownlint-cli2 bump changes
831
+ those merge semantics, the restated `CHANGELOG.md` entry stops applying and its
832
+ ten findings reappear — in the CI step this change adds. The guard is checked by
833
+ the thing it guards.
834
+
835
+ **Bump**
836
+
837
+ _Patch._ Nothing a consumer imports, calls, or configures changes behaviour. The
838
+ one shipped file touched is a docstring in `engine/prose-lint.mjs`, which reaches
839
+ consumers through the emitted declarations; everything else — the workflow, the
840
+ local lint config, the scripts — is this repository's own plumbing and is outside
841
+ `files`.
842
+
3
843
  ## 6.1.0
4
844
 
5
845
  ### Minor Changes