@heroiclands/package-build 6.1.0 → 7.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,619 @@
1
1
  # @heroiclands/package-build
2
2
 
3
+ ## 7.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - a136429: Separate declaring a system from requiring one, and stamp `_stats` per pack
8
+ (#48).
9
+
10
+ A module shipping content for two systems could not say so. The only place to
11
+ state a system version was `relationships.systems`, and that list is a
12
+ **restriction**: Foundry's `supportsSystem` drops a package from any world whose
13
+ system it does not name. So the two needs were in direct conflict — name your
14
+ systems and become unloadable elsewhere, or stay loadable and stamp nothing.
15
+
16
+ **`harn-ensemble` is the case, and it is live.** It declares an `actors-hm3`
17
+ pack and an `actors-sohl` pack, and resolves to:
18
+
19
+ ```json
20
+ { "statsSystemId": null, "statsSystemVersion": null }
21
+ ```
22
+
23
+ Nothing stamped, on content that was certainly built against `hm3 1.6.3` and
24
+ `sohl 0.8.2`. It takes that path deliberately, because declaring the two systems
25
+ would hide the module from every other world — including the ones that want only
26
+ its system-neutral journals pack.
27
+
28
+ **The split.**
29
+
30
+ | | describes | gates |
31
+ | ----------------- | -------------------------------------------- | --------------------------- |
32
+ | `systems:` | which systems this package can stamp against | **nothing** |
33
+ | `requiresSystem:` | — | where the package will load |
34
+
35
+ Naming a system under `systems:` restricts nothing. `requiresSystem` is separate
36
+ and optional, and emits the `relationships.systems` entry Foundry reads —
37
+ _reusing_ the declaration rather than restating it, because
38
+ `stats.systemVersion` sat at `0.6.0` for four releases when a transcription was
39
+ free to disagree with what it copied.
40
+
41
+ ```yaml
42
+ systems:
43
+ hm3: { compatibility: { minimum: "1.6.3", verified: "1.6.3" } }
44
+ sohl: { compatibility: { minimum: "0.8.2", verified: "0.8.2" } }
45
+ requiresSystem: null # optional; omitted, the package loads anywhere
46
+ ```
47
+
48
+ **A pack's `system:` now selects what stamps its documents.** `_stats` was one
49
+ memoised block for the whole package, so every document in every pack was
50
+ stamped identically. It is per pack now: `statsForPack()` resolves the pack's
51
+ declared system through `systems:`, and `BasePackCompiler` exposes it as
52
+ `this.stats`, memoised per instance — one pass, one pack, one system.
53
+
54
+ **`systemId` travels with `systemVersion`.** They are one decision, so where one
55
+ is omitted both are. Stamping a per-pack version against a package-wide id would
56
+ emit `systemId: sohl, systemVersion: 1.6.3` on HM3 documents — a plausible lie,
57
+ which is worse than the missing value #43 fixed, because nothing about it looks
58
+ wrong.
59
+
60
+ **A name that resolves to nothing is an error.** A pack's `system:` must name a
61
+ declared system; `requiresSystem` must too; and with a gate set, a pack naming a
62
+ _different_ system is refused outright — Foundry would hide the whole package
63
+ from any world that pack could have appeared in, so it would ship and be
64
+ unreachable.
65
+
66
+ **Nothing that ships today moves.** All six consumers were resolved before and
67
+ after; none declares a `systems:` block yet, so every pack still falls through to
68
+ the package-wide block it used before:
69
+
70
+ | package | `systems:` | `requiresSystem` | stamps | packs unchanged |
71
+ | -------------------- | ---------: | ---------------- | -------------- | --------------- |
72
+ | `sohl` | 0 | — | `sohl / 0.8.2` | yes |
73
+ | `sohl-thalorna` | 0 | — | `sohl / 0.8.2` | yes |
74
+ | `sohl-kethira-basic` | 0 | — | `sohl / 0.8.2` | yes |
75
+ | `harn-ensemble` | 0 | — | `— / —` | yes |
76
+ | `harn-adventures` | 0 | — | `— / —` | yes |
77
+ | `hm3` | 0 | — | `hm3 / 1.6.3` | yes |
78
+
79
+ **`stats.systemId` and `stats.systemVersion` are refused outright.** Authoring a
80
+ derived value is an error rather than an override, which is the rule this
81
+ configuration already applies elsewhere — and the reason is the same one that let
82
+ `stats.systemVersion` sit at `0.6.0` for four releases: a transcribed copy is
83
+ free to drift from what it copied, and nothing reads a stamped `_stats` until
84
+ something migrates on it. The refusal names the key, its line, and what supplies
85
+ it now.
86
+
87
+ The value still has to reach the validator from the loader, which is the half
88
+ that may read the adjacent `package.json`. It travels under a **symbol**, so the
89
+ channel is not a second, forgeable spelling of the key just refused: a symbol
90
+ cannot be written in YAML and does not appear in `Object.keys`.
91
+
92
+ **`relationships.systems` keeps working, and still answers.** It carries
93
+ `itemCatalog` — a separate concern this split does not replace — so a repository
94
+ using it would otherwise have to restate its compatibility under `systems:`
95
+ purely to keep stamping, which is the duplication the change exists to remove. A
96
+ lone relationship is a declaration as much as a gate, so it derives `systemId`
97
+ too. Several have no single answer and get none.
98
+
99
+ **Every consumer was migrated in the same change.** One line each:
100
+
101
+ ```diff
102
+ stats:
103
+ - systemId: sohl
104
+ lastModifiedBy: sohlbuilder00000
105
+ ```
106
+
107
+ `sohl` and `hm3` are systems and are their own system by construction;
108
+ `sohl-thalorna` and `sohl-kethira-basic` derive it from the single system
109
+ relationship they already declare; `harn-ensemble` and `harn-adventures` declared
110
+ none and were already clean.
111
+
112
+ **Bump**
113
+
114
+ _Major._ `stats.systemId` and `stats.systemVersion` were accepted and are now
115
+ refused, so a configuration that resolved before can fail — which is the
116
+ definition this repository uses. Every HeroicLands consumer is migrated in
117
+ lockstep and verified to stamp exactly what it stamped before, but a consumer
118
+ outside that set must delete the two keys.
119
+
120
+ Part of #57, the third of its three keys that both describe and gate. #56 is
121
+ done, #49 shipped in 6.2.0, and this is #48.
122
+
123
+ ### Minor Changes
124
+
125
+ - db40b24: Address another package's landing without naming a host (#87).
126
+
127
+ A link from one package's page to another package's landing had no form but a
128
+ hardcoded absolute URL, and the homepage link check (#54) exempted one — a
129
+ finding whose fix does not exist is noise. `sohl-kethira-basic`'s homepage writes
130
+ `https://www.heroiclands.org/sohl/` twice.
131
+
132
+ **The premise the exemption rested on is true, and does not lead where it looked
133
+ like it led.** A landing is in no link manifest: it compiles to no document and
134
+ is entered in no index. The reading that follows is that nothing can resolve it.
135
+ But a landing's address is not a _note's_ address — it is the **package's**, and
136
+ `PACKAGE_BASE` has recorded where each package is served all along. Consulting it
137
+ walks no tree, reads no manifest and builds no index, which is exactly why the
138
+ mechanism survives the fence: `kethira` and `harnadventures` publish a homepage
139
+ and nothing else, and that mode never walks a content tree.
140
+
141
+ **So the authored form is the absolute URL with the host struck off.** `/sohl/`
142
+ in a body, or `href: /sohl/` in a card. Both were already accepted here — nothing
143
+ had ever named one as the form to use, which is the whole of what was missing.
144
+
145
+ | Field | Cross-package landing | Why |
146
+ | ------- | --------------------- | ----------------------------------------- |
147
+ | body | `[SoHL](/sohl/)` | emitted verbatim, resolved by the browser |
148
+ | `href:` | `/sohl/` | "already resolved, used verbatim" |
149
+ | `url:` | **not expressible** | package-relative by construction |
150
+
151
+ `url:` cannot leave its own package, so a `url:` naming another package's landing
152
+ is now reported with the field as the fix rather than a path — previously it was
153
+ told to "write `/`", which is nonsense arrived at by taking the path after the
154
+ prefix when there is nothing after the prefix.
155
+
156
+ **The base comes from the roster, not from the prefix.** The finding names
157
+ `PACKAGE_BASE[pkg]`, so a package the roster relocates (`/setting/thalorna/`) is
158
+ addressed where it actually is. That is the property the manifest enforces
159
+ everywhere else — the address published is the address emitted — reaching these
160
+ links for the first time.
161
+
162
+ **Roster for landings only.** Widening the package set the other rules read would
163
+ have them offer manifest-based advice about packages no manifest is vendored for.
164
+ An in-site path naming no known package is still left alone: several surfaces a
165
+ landing routes to are built by other tools, and this build does not hold the set
166
+ of published pages.
167
+
168
+ **Measured across every homepage authored today.** All five were run before and
169
+ after, each under its own `package-build.config.yaml`:
170
+
171
+ | Package | Before | After | |
172
+ | ------------------------- | ------ | ----- | ----------------------- |
173
+ | `sohl-kethira-basic` | 0 | **2** | the two the issue names |
174
+ | `sohl-thalorna` | 0 | 0 | |
175
+ | `harn-ensemble` | 0 | 0 | |
176
+ | `harn-adventures` | 0 | 0 | |
177
+ | `HarnMaster-3-FoundryVTT` | 0 | 0 | |
178
+
179
+ Kethira vendors no manifest at all — it is homepage-only — so it is also the
180
+ proof that the roster reaches where an index does not. Applying the fix the
181
+ finding names takes it back to 0.
182
+
183
+ **Sequencing, stated because it decides the bump.** These are hard errors:
184
+ `severity: "error"`, counted into `failures`, `exitCode 1`. By this repository's
185
+ own rule — a new hard error is breaking only if it fails a previously-passing
186
+ consumer — this is minor **only once kethira's two lines are converted**, and
187
+ major if it ships before them. The conversion does not wait on this release:
188
+ `/sohl/` already passes under the current published version, verified, so it can
189
+ land in `sohl-kethira-basic` immediately and independently. It must land first.
190
+
191
+ **Bump**
192
+
193
+ _Minor, not patch, and not major._ Not patch: a check that previously passed a
194
+ page can now fail it. Not major, on the condition above — with kethira converted,
195
+ every homepage authored today passes before and after, and no export, option or
196
+ emitted document changes shape.
197
+ - 35bde43: Locate a configuration error in the file it was written in (#95).
198
+
199
+ Every check across `content-config.mjs` and `config.mjs` — 81 of them — reports
200
+ through one `fail()`, which named the offending key's **dotted path** and
201
+ nothing else. That is a good description and a bad locator: nothing in the line
202
+ is a path an editor can open or a CI annotator can resolve, in a file that runs
203
+ to 400 lines with fourteen sibling entries under `sections:` alone, several of
204
+ them flow-mapped onto one line.
205
+
206
+ The path now rides on the error as a `field`, and the loader that read the file
207
+ resolves it — through `positionOfYamlPath`, the same locator `manifest`'s
208
+ `packFolders` findings already use — so all 81 come out in the
209
+ `file:line:column: severity: message` form every other finding uses, path first:
210
+
211
+ ```text
212
+ 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).
213
+ ```
214
+
215
+ **Located at the boundary, not at the check.** `content-config.mjs` is the leaf
216
+ an `.mjs` configuration imports and performs no I/O, so it attaches the path and
217
+ `configFromData` — which knows the file — formats it. `config.mjs`'s pure
218
+ `resolvePackageBuildConfig` is unchanged for the same reason;
219
+ `loadPackageBuildConfig` is where its findings are located.
220
+
221
+ **Positions are dropped, never guessed.** A required key the file never declared
222
+ has no node of its own, so the position names the **mapping it belongs in**, one
223
+ level up and no further — that entry is a real node and the one the reader must
224
+ edit. A missing _top-level_ key has nothing above it but the document, and an
225
+ `.mjs` configuration has no YAML to resolve a path against at all (parsing
226
+ JavaScript as YAML would resolve some paths to lines that mean nothing). Both
227
+ report `package-build.config.yaml: error: …` — the file, without a line.
228
+
229
+ Both command lines print a located failure unprefixed, since `package-build: `
230
+ and `loglevel`'s `[timestamp] [ERROR]:` occupy exactly the position a parser
231
+ reads the path from.
232
+
233
+ **Additive.** A valid configuration resolves exactly as before; only the text of
234
+ a rejection changes, and it keeps the body it had. `positionOfYamlPath` gains an
235
+ optional `{ key: true }` — report where a key is _declared_ rather than where
236
+ its value sits, which is what a message naming a field wants — and
237
+ `engine/diagnostics` gains `yamlKeyPath`, the one translation between a dotted
238
+ path and a YAML key path.
239
+ - b7ad450: Compile actors without an Item pack of this package's own (#49).
240
+
241
+ The actors pass threw unless the package declared at least one pack of type
242
+ `Item`. An Item pack is system-bound by construction — Foundry requires `system`
243
+ on Item packs and on few others — so the guard asked a deliberately
244
+ system-agnostic module to declare the very thing it exists not to depend on.
245
+
246
+ **This is not hypothetical, and it is not a future case.** `harn-ensemble`
247
+ declares two Actor packs and no Item pack at all in its
248
+ `package-build.config.yaml` today. Run against that configuration, the compiler
249
+ does not start:
250
+
251
+ ```text
252
+ packs declared : actors-hm3(Actor), actors-sohl(Actor)
253
+ itemPackJsonDirs : []
254
+ Actors : THREW — Actors compiler requires `itemsSourceDirs` …
255
+ ```
256
+
257
+ Its 2,512 beings resolve their embedded items — `skill:awar`, `attribute:str`,
258
+ `weapongear:…` — against the `sohl` and `hm3` catalogues through
259
+ `foreignSourceDirs`. The optional mechanism is the one that matters there; the
260
+ mandatory one had nothing to contribute.
261
+
262
+ **The guard did not test what it claimed.** It counted _declared directories_,
263
+ not resolvable items. An Item pack containing no documents satisfied it, while a
264
+ being naming an item nothing defines still failed later — so it neither
265
+ prevented the failure it named nor reported it where it happened. And its
266
+ remedy, "declare at least one pack of type `Item`", is the opposite of the fix
267
+ for a system-agnostic package.
268
+
269
+ **The condition actually cared about was already checked, at the site of the
270
+ mistake.** `resolveEmbedded` reports each unresolved `(type, shortcode)` by
271
+ name, with the being as context, and counts it — and those counts aggregate into
272
+ `totalErrors`, so a package genuinely missing an item still fails the build:
273
+
274
+ ```text
275
+ Bandit: no predefined item for "skill:awar"
276
+ ```
277
+
278
+ That is the same line #43 and the frontmatter-lint work drew: a structural
279
+ precondition that is cheap to state is not the condition you care about, and
280
+ reporting where the mistake is beats refusing to start.
281
+
282
+ **Nothing tightens.** `itemsSourceDirs` defaults to `[]` and is otherwise
283
+ unchanged; `foreignSourceDirs` is untouched; `itemPackJsonDirs` already returned
284
+ an empty list for a repository with no Item packs, and only the constructor
285
+ rejected it. A package that declares Item packs behaves exactly as before — the
286
+ 1,647 existing tests pass unchanged, with four added for the empty case and for
287
+ the point-of-use error that replaces the guard.
288
+
289
+ **Bump**
290
+
291
+ _Minor, not patch._ No consumer that compiled before fails now — the change only
292
+ removes a refusal — but a configuration that was rejected is now supported, which
293
+ is new surface rather than a repair to existing surface. Not major for the same
294
+ reason: nothing that was accepted stops being accepted.
295
+
296
+ Part of #57, which is the same defect in three places: a key that both describes
297
+ and gates, so the legitimate case cannot be expressed. Here the gate is removed
298
+ and the description — which Item packs this package ships — is left saying only
299
+ that.
300
+
301
+ ### Patch Changes
302
+
303
+ - 8aab711: Bump `glob` from 11.1.0 to 13.0.6.
304
+
305
+ **Both majors are changes to glob's command-line program, not its library.**
306
+
307
+ - **12** removed the unsafe `--shell` option, keeping it only on shells where it
308
+ can be implemented safely (the remediation for GHSA-5j98-mcp5-4vw2, whose
309
+ mitigation 11.1 had introduced).
310
+ - **13** moved the CLI out to a separate `glob-bin` package.
311
+
312
+ This repository never invokes that program. The single use of the library is
313
+ `globSync(patterns, { cwd, absolute: true })` in `readMatching`
314
+ (`bin/package-build.mjs`), which is untouched across both majors — no workflow,
315
+ hook, or script calls `glob` from a shell, so the removed binary is not a
316
+ dependency this package had.
317
+
318
+ **Verified rather than assumed.** `globSync` was exercised under 13.0.6 with the
319
+ options `readMatching` actually passes, returning the same absolute paths; the
320
+ repository's 1,642 tests pass unchanged.
321
+
322
+ **Bump**
323
+
324
+ _Patch, not minor._ Nothing in this package's surface moves, and the majors are
325
+ the library's own. Worth noting for a consumer only in one case: a repository
326
+ that installed `glob` transitively through this package and relied on
327
+ `node_modules/.bin/glob` being present must now depend on `glob-bin` directly.
328
+ That was never a supported edge of this package, and no HeroicLands repository
329
+ does it.
330
+ - 8e0778e: Bump `markdown-it` from 14.3.0 to 15.0.0.
331
+
332
+ This package's entire use of the library is `markdownit({ html: true })` and
333
+ `md.render(body)`, at three call sites — `engine/helpers.mjs`,
334
+ `engine/journals.mjs` and `sohl/actors.mjs`. Nothing overrides a renderer rule,
335
+ installs a plugin, or reaches into the parser.
336
+
337
+ **Every breaking change in 15.0.0 lands outside that surface.**
338
+
339
+ | Breaking change | Why it does not reach here |
340
+ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
341
+ | `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 |
342
+ | Package-internal subpath exports (`markdown-it/lib/*`) removed | Only the package root is imported |
343
+ | `validateLink`/`normalizeLink`/`normalizeLinkText` moved to prototype | None is read, assigned, or overridden |
344
+ | `StateBlock#ddIndent` removed | No plugin is installed, `markdown-it-deflist` included |
345
+ | Root now resolves to prebuilt `dist/` rather than raw sources | Import is by package name; the resolved path was never depended on |
346
+
347
+ **Verified rather than assumed.** The same corpus was rendered through 14.3.0
348
+ and 15.0.0 and diffed byte-for-byte, covering exactly the constructs the release
349
+ touched — bare URLs, autolinks, email-shaped text, reference links and their
350
+ definitions, CJK adjacent to a URL, hard line breaks, raw HTML, tables, nested
351
+ lists, and both fenced and indented code. Output is identical. The repository's
352
+ 1,642 tests pass unchanged.
353
+
354
+ **Bump**
355
+
356
+ _Patch, not minor._ No export, option, or emitted document changes shape, and no
357
+ behaviour a consumer can observe moves. The version is a major on the library's
358
+ own surface, none of which this package presents onward.
359
+ - 6318722: Add `.github/dependabot.yml`. This repository had none, so nothing proposed a
360
+ dependency update — not npm, not GitHub Actions.
361
+
362
+ That matters more here than in a consumer: this is the toolchain every other
363
+ HeroicLands package builds with, and it is published to npm, so its dependency
364
+ tree reaches every consumer's build and everyone who installs it. The
365
+ consuming repositories are already covered; this producer was the one link in
366
+ the chain nothing watched.
367
+
368
+ `@foundryvtt/foundryvtt-cli` and `classic-level` are each an ungrouped
369
+ single-package entry, ordered ahead of the housekeeping catch-all, for every
370
+ update type including minor and patch. Both sit directly on the compendium
371
+ pack pipeline, and a version bump that silently changes how a pack compiles is
372
+ exactly the failure this repository's byte-level output comparisons exist to
373
+ catch — grouping either into a housekeeping pull request would hide which
374
+ dependency caused a regression.
375
+
376
+ No `ignore` entries: nothing here has a known-bad range to record.
377
+ `typescript` is a direct dependency, but `build.yml`'s "Declaration emit" step
378
+ already gates a breaking bump on every pull request.
379
+
380
+ Closes #100.
381
+ - cd32ea1: Render `[[target|]]` as the target's name on the web, as the packs already do
382
+ (#113).
383
+
384
+ A wikilink with an explicit but empty label produced an **empty anchor** —
385
+ `[](/rules/sohl-shock/)`, a link with no clickable text — silently, through
386
+ every site build:
387
+
388
+ ```text
389
+ [[doc-shock]] => [Shock](/rules/sohl-shock/)
390
+ [[doc-shock|]] => [](/rules/sohl-shock/) ← before
391
+ [[doc-shock|]] => [Shock](/rules/sohl-shock/) ← after
392
+ [[#sec|]] => [](#sec) ← before
393
+ ```
394
+
395
+ **The two resolvers had drawn the same line in two places, differently.**
396
+ `parseWikilink` distinguishes "no label" from "empty label" deliberately, and
397
+ its docstring says so — _"`null` and `""` differ: an author may write
398
+ `[[x|]]`"_. The packs resolver honoured that by testing falsiness. The web
399
+ resolver used `??`, which falls through on `null` only, so `""` survived to the
400
+ output at three sites: the same-page anchor, the resolved link, and the
401
+ unresolved link.
402
+
403
+ That is the third instance of exactly the drift `wikilink-syntax.mjs` was
404
+ created to stop — its module docstring opens on the two copies of the link
405
+ parser having already diverged. The parse was centralised; the _interpretation
406
+ of the parsed parts_ was not, and it drifted in the one case the docstring
407
+ names.
408
+
409
+ **So the reading is stated once.** `authoredLabel()` joins the syntax module and
410
+ both resolvers consult it, rather than each deciding what an empty label means.
411
+ `labelled` is untouched and still separates `[[x]]` from `[[x|]]`, which is what
412
+ #1409 actually depends on — an unlabelled `[[Shock State]]` still shows the
413
+ author's own prose rather than the canonical name.
414
+
415
+ **Why it is worth a release rather than waiting.** `[[x|]]` becomes load-bearing
416
+ under the four-segment address grammar (#59): the pipe is what distinguishes an
417
+ address lookup from an alias lookup, so `[[target|]]` is the canonical way to
418
+ write an address that displays its target's name — and the planned migration
419
+ rewrites every authored address link into that form. Converting the corpus into
420
+ a form that renders an empty anchor would be a corpus-wide regression, so this
421
+ has to land first.
422
+
423
+ **Bump**
424
+
425
+ _Patch._ A defect fix with no new surface. `authoredLabel` is exported because
426
+ both resolvers import it, not as a feature for consumers; no option, address, or
427
+ emitted document changes shape, and no link that renders correctly today renders
428
+ differently after.
429
+ - bc2c5c8: Bump `@types/node` in the lockfile for development tooling maintenance.
430
+ - 1a4d882: Hold `typescript` at major 6 in Dependabot, because TypeScript 7 removes the
431
+ compiler API `coverage.mjs` parses with.
432
+
433
+ Dependabot proposed 6.0.3 → 7.0.2 (#105). It cannot be taken.
434
+
435
+ **TypeScript 7 is the native port, and its npm package no longer ships the
436
+ JavaScript compiler API.** The `"."` export resolves to `lib/version.cjs`, whose
437
+ entire surface is `version` and `versionMajorMinor`.
438
+
439
+ `coverage.mjs` uses that API as a _parser_, not as a compiler: it reads
440
+ localization keys out of a consumer's `src/**/*.{ts,mjs}` by walking a real AST,
441
+ deliberately down one path so JavaScript and TypeScript cannot drift. Under 7.0.2
442
+ the AST **vocabulary** survives behind `typescript/unstable/ast` — `ScriptTarget`
443
+ and every `isX` guard the scan uses — but the three things that actually drive it
444
+ exist nowhere in the JS surface:
445
+
446
+ | Needed by `coverage.mjs` | In 7.0.2 |
447
+ | ------------------------------------------------------------- | ------------------------------------------ |
448
+ | `createSourceFile` | **missing** — only a factory of that name¹ |
449
+ | `forEachChild` | **missing** |
450
+ | `flattenDiagnosticMessageText` | **missing** |
451
+ | `ScriptTarget`, `isStringLiteral`, `isPropertyDeclaration`, … | present, behind `unstable/ast` |
452
+
453
+ ¹ 7's `createSourceFile` assembles a SourceFile from statements already parsed.
454
+ It is not a parser.
455
+
456
+ **The gate this file predicted would catch it did not.** The previous comment in
457
+ `dependabot.yml` recorded no ignore entry on the reasoning that `build.yml`'s
458
+ "Declaration emit" step already guards a breaking `typescript` bump. That step
459
+ passes clean under 7.0.2 — `tsc` still emits every `.d.mts`. What failed was
460
+ `npm test`: 11 failures in `tests/coverage.test.ts`, all
461
+ `TypeError: Cannot read properties of undefined (reading 'Latest')`. A dependency
462
+ can be load-bearing in two unrelated ways at once, and the file named only one of
463
+ them. That correction is now written where the wrong prediction was.
464
+
465
+ **Why an ignore rather than a migration.** Parsing in 7 lives in the Go binary,
466
+ reachable only through `unstable/sync`'s Project/Program API. Adopting it would
467
+ put a subprocess and a virtual filesystem inside a module whose stated contract
468
+ is "everything here is pure — source text in, references or findings out", in
469
+ exchange for an API whose own export path says `unstable`. Acorn is not a
470
+ substitute either: the default scan glob is `src/**/*.{ts,mjs}`, and the largest
471
+ consumer's sources are TypeScript.
472
+
473
+ That migration is worth doing when the API stabilises. It is not a dependency
474
+ bump, and it should not arrive as one.
475
+
476
+ **Scope of the hold.** Majors only — minor and patch releases within 6 still
477
+ arrive on the weekly schedule. The entry names the two conditions that lift it:
478
+ a stable in-process parse entry point, or a `coverage.mjs` that no longer needs
479
+ one.
480
+
481
+ **Bump**
482
+
483
+ _Patch._ Nothing shipped changes. `.github/dependabot.yml` is this repository's
484
+ own automation and sits outside `files`; the `typescript` range in
485
+ `package.json` is untouched, because `^6.0.3` already excludes 7 — the entry
486
+ stops the pull request being reopened, it does not change what resolves.
487
+ - f3167c3: Read the changesets action's `pr-number` output, so the step that marks the
488
+ Version Packages pull request stops being dead code (#84).
489
+
490
+ `release.yml` pins `changesets/action@v2` but read v1's `pullRequestNumber`. An
491
+ unset output evaluates to the empty string rather than erroring, so the guard was
492
+ `'' != ''` — always false. The step has never run once.
493
+
494
+ **The failure was silent by construction, which is why it survived three
495
+ releases.** A misspelled output does not fail a workflow; it disappears. `v3.4.0`,
496
+ `v4.0.0` and `v5.0.0` were all cut with this step skipped, and every run reported
497
+ green.
498
+
499
+ **Only one of the three names actually moved.** Checked against v2's own
500
+ `action.yml` rather than against the assumption that v2 kebab-cased everything:
501
+
502
+ | v1 | v2 | Used here |
503
+ | ------------------- | -------------------- | ------------------------------------- |
504
+ | `pullRequestNumber` | `pr-number` | yes — the two lines this change fixes |
505
+ | `publishedPackages` | `published-packages` | no |
506
+ | `hasChangesets` | `has-changesets` | no |
507
+ | `published` | `published` | yes — **unchanged**, and left alone |
508
+
509
+ `published` is the one name v2 kept. A blanket kebab-case sweep of this file —
510
+ the obvious reading of "rename the v1 outputs" — would have broken the one step
511
+ that was working.
512
+
513
+ **Bracket notation, not `outputs.pr-number`.** A hyphen is the subtraction
514
+ operator in an Actions expression, so the dotted form parses as
515
+ `outputs.pr - number`: a second silent-ish defect sitting directly behind the
516
+ first. `steps.changesets.outputs['pr-number']` is the form that means what it
517
+ reads as.
518
+
519
+ **What this repairs.** The Version Packages pull request is opened by
520
+ `GITHUB_TOKEN`, and GitHub deliberately starts no workflow runs from that token,
521
+ so the required `Changeset declared` context never reports on it. This step
522
+ exists to post that status. With it inert, every Version Packages pull request
523
+ has needed the check waived or force-merged by hand.
524
+
525
+ **Not yet demonstrated running, and stated rather than glossed.** The acceptance
526
+ criterion asks for a run where the step is not `skipped`, and that can only
527
+ happen on `main`, on the next release that opens a Version Packages pull request
528
+ — this change cannot produce one from a branch. The expression is verified by
529
+ parsing the workflow and by v2's manifest; the live proof arrives with the next
530
+ bump.
531
+
532
+ **Sibling repositories are fixed individually, not swept from here.** The same
533
+ defect is open on `harn-ensemble` (#13) and `HarnMaster-3-FoundryVTT` (#427),
534
+ where it is more severe — there the misspelling gates the release itself, and no
535
+ release is ever cut. Each repository owns its own copy of `release.yml`; this one
536
+ is not a reusable workflow. Turning it into one is a real improvement and a
537
+ separate change, and folding it into a two-line fix would put a shared release
538
+ pipeline into production on the back of a typo correction.
539
+
540
+ **Bump**
541
+
542
+ _Patch, not minor._ Nothing this package exports, emits, or documents for a
543
+ consumer changes. The file is this repository's own release plumbing, and it is
544
+ not shipped: `.github` is outside `files`.
545
+ - 2bdc792: Make `lint:markdown` pass, and run it in CI (#92).
546
+
547
+ This repository provides `content-build markdown` and was the one repository that
548
+ never ran it. The script existed, failed, and was executed by nothing — no
549
+ aggregate `lint` script, and no workflow naming it.
550
+
551
+ **The finding count in the issue is wrong, and the correction changes the
552
+ decision.** 117 is what `npx markdownlint-cli2 CHANGELOG-content-build.md`
553
+ reports — markdownlint's _default_ rule set, which this toolchain deliberately
554
+ turns off (`default: false`, then each rule enabled by name). Under the rules the
555
+ repository actually uses, `npm run lint:markdown` reports **three**: two
556
+ `MD001` heading skips and one `MD034` bare URL. The issue's "fix it — 117
557
+ mechanical findings" and "exclude it — 117 is too many to fix" were both
558
+ arguments about a number that was never the repository's.
559
+
560
+ **Excluded anyway, and not because three is still too many.**
561
+ `CHANGELOG-content-build.md` is the published changelog of
562
+ `@heroiclands/content-build` — a **deprecated repository**, absorbed into this
563
+ one at 3.0.0 (#32). It is frozen, not merely generated: exactly one commit has
564
+ ever touched it, nothing regenerates it, and it ships only because `files` lists
565
+ it. Its three findings are facts about what content-build published. Rewriting
566
+ them would edit a historical record to satisfy a rule about prose nobody will
567
+ write again, and would put a style-only commit in the blame of a file whose whole
568
+ value is being what was published. It is the same class as `CHANGELOG.md`, which
569
+ the shared default already ignores for the weaker reason that the next release
570
+ rewrites it.
571
+
572
+ **Declared locally, so no consumer moves.** The exclusion is a new
573
+ `.markdownlint-cli2.jsonc` in this repository, not an entry added to the shared
574
+ `MARKDOWN_IGNORES`. One repository's retired filename does not belong in
575
+ configuration six repositories consume, and the shared rule set is unchanged for
576
+ all of them.
577
+
578
+ **What that file had to get right, and what it documents.** `content-build
579
+ markdown` passes the shared rules as markdownlint-cli2's `optionsDefault`, and a
580
+ consumer file merges over them **key by key, each key wholesale**:
581
+
582
+ | Declared locally | Effect on the shared default |
583
+ | ---------------- | --------------------------------------------------------------------- |
584
+ | only `ignores` | rule set survives intact — `default: false` and every per-rule option |
585
+ | `ignores` | **replaces** `MARKDOWN_IGNORES`; it does not extend it |
586
+
587
+ So the local file restates `CHANGELOG.md`. Verified rather than assumed — with
588
+ that entry dropped, `lint:markdown` reports ten findings in `CHANGELOG.md`; with
589
+ a probe file present, `MD049` still fires with its shared `underscore` option
590
+ while `MD013` stays silent, which is what proves the rule set was not replaced.
591
+
592
+ `engine/prose-lint.mjs` said a consumer config "replaces it", which is the
593
+ reading that would send the next person to add the ignore to the shared default
594
+ or to a full config copy. Its docstring now states the key-by-key rule and the
595
+ `ignores` trap by name.
596
+
597
+ **Wired as a separate CI step, not folded into the chain.** `npm run lint` is
598
+ added for running both checks locally in one command, but `build.yml` gets a
599
+ `Markdown` step of its own beside `Formatting`. The aggregate chains with `&&`,
600
+ so a formatting failure would short-circuit it and hide every markdown finding
601
+ behind it; as two steps the failing one is named in the checks UI.
602
+ `lint:markdown:fix` is added too — the issue referred to it, and it did not exist.
603
+
604
+ **The exclusion cannot rot silently.** If a future markdownlint-cli2 bump changes
605
+ those merge semantics, the restated `CHANGELOG.md` entry stops applying and its
606
+ ten findings reappear — in the CI step this change adds. The guard is checked by
607
+ the thing it guards.
608
+
609
+ **Bump**
610
+
611
+ _Patch._ Nothing a consumer imports, calls, or configures changes behaviour. The
612
+ one shipped file touched is a docstring in `engine/prose-lint.mjs`, which reaches
613
+ consumers through the emitted declarations; everything else — the workflow, the
614
+ local lint config, the scripts — is this repository's own plumbing and is outside
615
+ `files`.
616
+
3
617
  ## 6.1.0
4
618
 
5
619
  ### Minor Changes
package/CONTENT.md CHANGED
@@ -155,7 +155,9 @@ the file sits in, fills the optional halves with their defaults
155
155
  manifest switches off and `publish.site` at its `homepage` floor),
156
156
  derives `assetRoot`, `packDirectories`, `itemTypes` and `docEntryTypes`, and
157
157
  freezes the result. A malformed configuration throws a `TypeError` naming the
158
- offending field, so it fails at load rather than as an empty pack much later.
158
+ offending field and the line and column it was written on, in the
159
+ [located form](#diagnostics) — so it fails at load rather than as an empty pack
160
+ much later.
159
161
 
160
162
  **Four values are derived rather than authored**, because each is something a
161
163
  file can be asked for rather than told:
@@ -1160,6 +1162,24 @@ Two rules keep it that way, both in `engine/diagnostics.mjs`:
1160
1162
  meaningless, `file: …` when only the note is known. Nothing defaults to
1161
1163
  `1:1`, which would send a reader to the frontmatter every time.
1162
1164
 
1165
+ **A configuration error is located the same way.** Every check in
1166
+ `content-config.mjs` and `config.mjs` reports through one `fail()`, naming the
1167
+ offending key's dotted path — a good description and a bad locator, in a file
1168
+ that runs to hundreds of lines with sibling entries flow-mapped onto one. The
1169
+ path now rides on the error, and the loader that read the file resolves it
1170
+ against the YAML, so all of them come out located:
1171
+
1172
+ ```text
1173
+ package-build.config.yaml:382:64: error: package-build config: `site.sections.being.descrption` is not a recognized option (expected one of: title, banner).
1174
+ ```
1175
+
1176
+ The same two rules apply. A key the file never declares — a required one that is
1177
+ simply missing — has no node of its own, so the position names the **mapping it
1178
+ belongs in** and no further out; a missing _top-level_ key has nothing above it
1179
+ but the document, and an `.mjs` configuration has no YAML to resolve a path
1180
+ against at all. Both report `package-build.config.yaml: error: …`, the file
1181
+ without a line, rather than a line that would be wrong.
1182
+
1163
1183
  Establishing a position at all takes three corrections, applied only where they
1164
1184
  hold — see `positionInBody`. A body offset is not a file line until the
1165
1185
  frontmatter's lines are added (`bodyLine`); the trim that strips the body can