@heroiclands/package-build 20.0.0 → 20.2.1
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 +495 -0
- package/CONTENT.md +185 -25
- package/README.md +43 -4
- package/bin/content-build.mjs +87 -2
- package/config.mjs +9 -1
- package/content-config.mjs +89 -18
- package/e2e.mjs +297 -3
- package/engine/content-charset.mjs +434 -0
- package/engine/content-icons.mjs +388 -0
- package/engine/foreign-catalog.mjs +109 -7
- package/engine/frontmatter-lint.mjs +191 -29
- package/engine/generate.mjs +5 -2
- package/engine/helpers.mjs +10 -1
- package/engine/index.mjs +6 -0
- package/engine/note-claims.mjs +208 -5
- package/engine/pack-config.mjs +102 -12
- package/engine/pack-router.mjs +0 -0
- package/engine/prose-config.mjs +42 -0
- package/engine/prose-lint.mjs +126 -0
- package/engine/schema-extract.mjs +13 -0
- package/package.json +1 -1
- package/types/config.d.mts +7 -0
- package/types/e2e.d.mts +130 -3
- package/types/engine/content-charset.d.mts +127 -0
- package/types/engine/content-icons.d.mts +151 -0
- package/types/engine/foreign-catalog.d.mts +38 -2
- package/types/engine/frontmatter-lint.d.mts +146 -28
- package/types/engine/helpers.d.mts +8 -0
- package/types/engine/index.d.mts +2 -0
- package/types/engine/note-claims.d.mts +67 -0
- package/types/engine/pack-config.d.mts +35 -0
- package/types/engine/prose-config.d.mts +41 -0
- package/types/engine/prose-lint.d.mts +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,500 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 20.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f39c7e8: Restore the lockfile refresh to the release, and re-sync the lockfile 20.2.0
|
|
8
|
+
shipped without.
|
|
9
|
+
|
|
10
|
+
`changeset version` rewrites `package.json` and the CHANGELOG and never touches
|
|
11
|
+
`package-lock.json`, so the lockfile's root `version` keeps the previous
|
|
12
|
+
release's number. The `version-script` input is the only seam the changesets
|
|
13
|
+
action offers between versioning and committing, which is where the refresh has
|
|
14
|
+
to happen for it to land in the same commit.
|
|
15
|
+
|
|
16
|
+
That input was removed for one release on the theory that it was why `changeset
|
|
17
|
+
version` could not find its own binary. It was not: `node_modules` had been
|
|
18
|
+
committed as a symlink to an absolute path, and the action's `git reset --hard`
|
|
19
|
+
restored it over the install, so nothing resolved by any mechanism. With the
|
|
20
|
+
symlink gone the seam works again, and it is back.
|
|
21
|
+
|
|
22
|
+
## 20.2.0
|
|
23
|
+
|
|
24
|
+
### Minor Changes
|
|
25
|
+
|
|
26
|
+
- 85fffd6: **The frontmatter lint checks the system blocks a package ships for, instead of
|
|
27
|
+
a block named `sohl`.**
|
|
28
|
+
|
|
29
|
+
A system block is a closed region: a key the system's vocabulary does not
|
|
30
|
+
declare is an error, because the compiler's builders are an allow-list and drop
|
|
31
|
+
it without a word. That held for exactly one block, `sohl:`, and it held whatever
|
|
32
|
+
system the package shipped for — the linter takes the blocks its caller names,
|
|
33
|
+
and the only caller named none, so every tree fell back to the same constant.
|
|
34
|
+
|
|
35
|
+
Both directions of that are wrong once a second system exists, and the second is
|
|
36
|
+
the costlier:
|
|
37
|
+
|
|
38
|
+
| | before | now |
|
|
39
|
+
| ----------------------------- | --------------------------------------------------------- | ------------------------------------ |
|
|
40
|
+
| a package shipping for `sohl` | `sohl:` checked | unchanged |
|
|
41
|
+
| a package shipping for `hm3` | `sohl:` checked — a block it does not carry | `hm3:` checked |
|
|
42
|
+
| an `hm3:` block | **never read**, every key discarded at compile in silence | checked against HM3's own vocabulary |
|
|
43
|
+
| a tree feeding both | one of two blocks checked | each block against its own system |
|
|
44
|
+
|
|
45
|
+
**Which systems a package ships for is already declared**, so this reads that
|
|
46
|
+
rather than asking for it again — in all three places it is written:
|
|
47
|
+
|
|
48
|
+
- `systems:`, which declares them without requiring one;
|
|
49
|
+
- a **pack's** `system:`, which is the same statement per pack and the only one
|
|
50
|
+
some trees make. It is already authoritative at compile, where a note routed
|
|
51
|
+
to such a pack and carrying no such block fails the build, so a lint blind to
|
|
52
|
+
it would refuse a note for want of a block it never checked;
|
|
53
|
+
- `stats.systemId` where neither is written, which has already absorbed every
|
|
54
|
+
remaining spelling: a system package is its own system, and a module takes
|
|
55
|
+
`requiresSystem`, its lone `systems:` entry, or its lone system relationship.
|
|
56
|
+
|
|
57
|
+
**Each block is held to its own system's vocabulary**, and that has two sources.
|
|
58
|
+
A system's `itemBuilders` registry covers its item types — `skill` is one name
|
|
59
|
+
over two data models, so a key SoHL's `skill` declares is not thereby a key
|
|
60
|
+
HM3's. The note schemas cover the rest, `being` above all, which is an actor type
|
|
61
|
+
sitting in no item registry; they are SoHL's, because that is the vocabulary
|
|
62
|
+
`content-build` is built with.
|
|
63
|
+
|
|
64
|
+
A type neither source names is a type that system says nothing about, and its
|
|
65
|
+
block is left alone on such a note rather than reported wholesale. A package
|
|
66
|
+
naming no system anywhere is system-agnostic on purpose — its packs are core
|
|
67
|
+
document types carrying no system data — so it has no system block, and none is
|
|
68
|
+
invented for it.
|
|
69
|
+
|
|
70
|
+
**A block whose vocabulary nothing states is said out loud.** A package
|
|
71
|
+
declaring a system other than SoHL and no `itemBuilders` registry for it has
|
|
72
|
+
nothing that can say what that block may carry, so the block goes unchecked and
|
|
73
|
+
`content-build lint` reports that once, naming the system and the registry to
|
|
74
|
+
declare. A check that quietly does nothing is indistinguishable from one that
|
|
75
|
+
passed, which is the whole subject here.
|
|
76
|
+
|
|
77
|
+
For `harn-ensemble` — the tree this issue is about, declaring both systems
|
|
78
|
+
through its packs — that means its `sohl:` block is checked exactly as before,
|
|
79
|
+
its 2,512 `being` notes included, and its `hm3:` block waits on
|
|
80
|
+
`itemBuilders: [hm3, sohl]`, which the lint now asks for by name.
|
|
81
|
+
|
|
82
|
+
**Nothing changes for a package shipping for SoHL**, which is every consumer
|
|
83
|
+
today: one system, one registry, and the derivation is the identity on it.
|
|
84
|
+
- e084547: The e2e harness no longer reports a run that never started as green.
|
|
85
|
+
|
|
86
|
+
Observed against a licensed container: a concurrent `npm ci` removed
|
|
87
|
+
`node_modules` out from under a run in progress, Cypress died with
|
|
88
|
+
`Cannot find package '.../cypress/index.js'`, and `package-build e2e run`
|
|
89
|
+
**exited 0**. The concurrency was an operator's mistake; the exit code was not.
|
|
90
|
+
A scripted caller, or anyone reading the tail of a log, would have recorded the
|
|
91
|
+
suite as passing when nothing was executed — and the suite is what moves
|
|
92
|
+
`compatibility.verified`, so an exit code that says green when nothing ran makes
|
|
93
|
+
that evidence unfalsifiable in the one direction that matters.
|
|
94
|
+
|
|
95
|
+
The suite is now bracketed rather than trusted on its exit status:
|
|
96
|
+
|
|
97
|
+
| When | Check | What it catches |
|
|
98
|
+
| ------ | ---------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
99
|
+
| Before | Every executable the command names | The runner is not installed — an error naming it, before a container and a world. |
|
|
100
|
+
| Before | The tool behind a package runner | `npx cypress run` resolves **`cypress`**; `npx` is never missing, so it answers nothing. |
|
|
101
|
+
| After | Those executables again | The runner disappeared mid-run, which is the failure reported above. |
|
|
102
|
+
| After | Results written since the spawn | The suite started and produced nothing. Needs the new `results` key. |
|
|
103
|
+
|
|
104
|
+
**New: `packageBuild.e2e.results`.** One path, or a list of them, relative to
|
|
105
|
+
the repository root, naming where the suite writes its results:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
e2e:
|
|
109
|
+
suite:
|
|
110
|
+
run: [npx, cypress, run]
|
|
111
|
+
results: [cypress/results]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Existence is not the test — a directory the _previous_ run left behind exists,
|
|
115
|
+
and reading that as evidence would make the check agree with exactly the thing
|
|
116
|
+
it was built to catch. What counts is a file modified since the suite was
|
|
117
|
+
spawned. Declaring nothing keeps the previous contract, in which the exit status
|
|
118
|
+
is taken at its word; declaring a path is what buys the distinction between _the
|
|
119
|
+
suite ran and passed_ and _the suite did not run_.
|
|
120
|
+
|
|
121
|
+
**What a consumer may notice.** A `run`, `fast` or `sweep` whose suite is not
|
|
122
|
+
installed now fails immediately with a diagnostic naming the missing program,
|
|
123
|
+
where before it stood a container up and failed later — or, in the reported
|
|
124
|
+
case, did not fail at all. The check only ever makes a verdict _worse_: a suite
|
|
125
|
+
that failed keeps its own exit status, so there is no new way for the harness to
|
|
126
|
+
report a result that did not happen. `open` is untouched, because a person
|
|
127
|
+
decides what to execute there and a session that ran no specs is not a fault.
|
|
128
|
+
|
|
129
|
+
Also exported from `@heroiclands/package-build/e2e`, for a repository that wants
|
|
130
|
+
the same rules elsewhere: `suiteExecutables`, `findExecutable`,
|
|
131
|
+
`missingExecutables`, `freshResults` and the pure `suiteVerdict`.
|
|
132
|
+
- d72c4b2: A note can now name an interface icon instead of drawing one. `:icon-star:`
|
|
133
|
+
renders as the same Font Awesome element the system's own sheets emit, and an
|
|
134
|
+
undeclared name is reported rather than published as literal text.
|
|
135
|
+
|
|
136
|
+
The user guide described Foundry's interface by pasting Unicode lookalikes of
|
|
137
|
+
icons the sheets actually draw — `☆` for the improve flag, `✎` for the formula
|
|
138
|
+
editor — so the note and the screen it described were drifting apart. Those
|
|
139
|
+
characters are also the worst in the corpus to typeset: of eight candidate book
|
|
140
|
+
faces, none carries them.
|
|
141
|
+
|
|
142
|
+
A registry maps a writer's name to a style and a Font Awesome icon, because the
|
|
143
|
+
three surfaces need different artefacts from one name: the journals and the
|
|
144
|
+
website want an `<i class="fa-solid fa-star">`, and a PDF wants a font file and
|
|
145
|
+
a glyph. It also means an icon renamed between Font Awesome major versions costs
|
|
146
|
+
one line rather than a sweep of the corpus.
|
|
147
|
+
|
|
148
|
+
Codepoints are deliberately absent: a renderer embedding the font has to read it
|
|
149
|
+
to subset it, and the font's own `cmap` is the only trustworthy source for which
|
|
150
|
+
glyph a name resolves to.
|
|
151
|
+
|
|
152
|
+
Part of #378.
|
|
153
|
+
- 15fb41f: **A pack's `system:` must resolve to the version its documents are stamped with,
|
|
154
|
+
and a configuration where it resolves to nothing is now refused.**
|
|
155
|
+
|
|
156
|
+
Every document in a pack carries `_stats.systemId` and `_stats.systemVersion`,
|
|
157
|
+
and for a pack declaring `system:` those come from one of exactly two places:
|
|
158
|
+
the `systems:` entry for that system, which carries the verified version, or the
|
|
159
|
+
package-wide stats, which answer for a package whose packs are all for its own
|
|
160
|
+
system.
|
|
161
|
+
|
|
162
|
+
A pack naming a system that resolves to **neither** used to fall through to the
|
|
163
|
+
package-wide value — and a module that declares no system does not have one, so
|
|
164
|
+
both fields were stamped `null`. That is the plausible lie #43 was about,
|
|
165
|
+
reached by the one path the guard did not cover:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
_stats: { systemId: null, systemVersion: null, … }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
on 2,513 compiled actors in a pack whose configuration says `system: sohl` on
|
|
172
|
+
the line above.
|
|
173
|
+
|
|
174
|
+
**The check existed; it was skipped in exactly this case.** `packs.<n>.system`
|
|
175
|
+
was validated against `systems:` only when that block was non-empty — the guard
|
|
176
|
+
read `declaredSystems.size && …` — so an absent block meant no check at all. Its
|
|
177
|
+
sibling ten lines up refuses the same thing for `requiresSystem` and says "the
|
|
178
|
+
`systems:` block is empty or absent" in as many words, and the comment above
|
|
179
|
+
both already described this failure. The suite was green throughout because its
|
|
180
|
+
`harn-ensemble`-shaped fixture declares the `systems:` block the repository does
|
|
181
|
+
not: the fixture was more complete than the configuration it stood for.
|
|
182
|
+
|
|
183
|
+
**What a consumer sees.** A configuration in this shape now fails with the pack
|
|
184
|
+
named and the entry to add:
|
|
185
|
+
|
|
186
|
+
> `packs.actors-hm3.system` names `hm3`, which `systems:` does not declare — the
|
|
187
|
+
> `systems:` block is empty or absent, and this package has no package-wide
|
|
188
|
+
> system either. Every document in the pack is stamped `_stats.systemId` and
|
|
189
|
+
> `systemVersion` from one of those two, so with neither it would be stamped
|
|
190
|
+
> null. Add `systems:` naming `hm3` with a `compatibility.verified` version.
|
|
191
|
+
|
|
192
|
+
**Nothing changes for a package whose packs name no system**, or whose packs name
|
|
193
|
+
its own system — the package-wide stats answer for those exactly as before,
|
|
194
|
+
which is every single-system tree. The package-wide derivation itself is now a
|
|
195
|
+
named function read by both the stamp and the check, so the value validated
|
|
196
|
+
against and the value stamped cannot come to disagree about the case that has no
|
|
197
|
+
answer.
|
|
198
|
+
- c9e7a5a: **A pack default is resolved per system, so one note compiles into one pack per
|
|
199
|
+
system without declaring anything.**
|
|
200
|
+
|
|
201
|
+
This is the routing half of #58, and until now it made the documented
|
|
202
|
+
two-system layout impossible to build. A default was computed per document
|
|
203
|
+
_type_: a type with exactly one pack is that type's default implicitly, and a
|
|
204
|
+
type with several designates one with `default: true`. A tree shipping one Actor
|
|
205
|
+
pack per system has two, so it had neither — and a note feeding both systems
|
|
206
|
+
declares no `pack:` by design, since a block's `pack:` exists to say where one
|
|
207
|
+
system's document goes only when that _differs_.
|
|
208
|
+
|
|
209
|
+
So every note routed nowhere. On `harn-ensemble` that was all 2,519 of them, the
|
|
210
|
+
build failing on each in turn with a message saying the configuration was wrong
|
|
211
|
+
when it was the question being asked that was.
|
|
212
|
+
|
|
213
|
+
| | before | now |
|
|
214
|
+
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
215
|
+
| one Actor pack per system, no flag | every note routes nowhere; build fails | each system's document routes to its own pack |
|
|
216
|
+
| a type-wide `default: true` on `actors-sohl` | returned to the HM3 pass too, which saw a name that was not its own and **skipped every note in silence** | the HM3 pass gets `actors-hm3` |
|
|
217
|
+
| `hm3.pack:` naming a SoHL pack | routed there, and the HM3 document was lost without a word | refused, naming the note and the pack |
|
|
218
|
+
| a shared `pack:` naming a SoHL pack | the HM3 document was lost without a word | does not answer for HM3, which falls through to its own default |
|
|
219
|
+
|
|
220
|
+
**A system is never answered with another system's pack.** That is the rule the
|
|
221
|
+
four rows share, and the second is the one worth stating twice: it failed
|
|
222
|
+
silently. The pack compiled zero entries, which a build reports only because a
|
|
223
|
+
pack that compiles nothing from a non-empty tree is itself an error.
|
|
224
|
+
|
|
225
|
+
**Marking a default still means what it says** — it designates that _system's_
|
|
226
|
+
default where a system has several packs of one type — and every single-system
|
|
227
|
+
configuration is untouched, since a pack declaring no system belongs to all of
|
|
228
|
+
them and the type-wide default answers exactly as before.
|
|
229
|
+
|
|
230
|
+
On `harn-ensemble` this takes `actors-sohl` from 0 compiled actors to 2,497, and
|
|
231
|
+
`actors-hm3` from routing nothing to claiming every note and reporting what each
|
|
232
|
+
still needs: `hm3.type`, which `being` requires because it is one-to-many into
|
|
233
|
+
`character` and `creature`.
|
|
234
|
+
- 0418fa8: `content-build lint` now holds a content tree to a character allowlist, so a
|
|
235
|
+
book can choose its typeface without discovering at print time that no font
|
|
236
|
+
carries what the notes are written in.
|
|
237
|
+
|
|
238
|
+
Typst does not warn when a glyph is missing — it falls back to whatever system
|
|
239
|
+
font has one and exits 0, so a rules table can set in three unrelated faces and
|
|
240
|
+
the build still reports success. The check moves that failure back to where the
|
|
241
|
+
character is written.
|
|
242
|
+
|
|
243
|
+
The tiers are measured rather than chosen: eight candidate book faces were
|
|
244
|
+
probed over every non-ASCII character in the five content trees, and what is
|
|
245
|
+
admitted is what enough of them carry. Letters and typography are universal;
|
|
246
|
+
Latin Extended Additional is carried by seven of eight; IPA was considered and
|
|
247
|
+
refused at five of eight, because requiring it would cost font freedom rather
|
|
248
|
+
than buy it.
|
|
249
|
+
|
|
250
|
+
Two rules ride along that an allowlist cannot express. Content must be NFC — a
|
|
251
|
+
decomposed letter is a different string to every byte comparison, including
|
|
252
|
+
DuckDB's `=`, so a filter typed one way silently misses a note stored the other.
|
|
253
|
+
And box-drawing, geometric and arrow characters are permitted inside a fenced
|
|
254
|
+
code block only, where the mono face sets them.
|
|
255
|
+
|
|
256
|
+
Part of #377.
|
|
257
|
+
- 571d5bc: **`content-build format` now says which shared Prettier conventions your
|
|
258
|
+
repository is not using.**
|
|
259
|
+
|
|
260
|
+
A consumer's own Prettier config wins **wholesale** — that is Prettier's own
|
|
261
|
+
behaviour and it is not changing — so the conventions this package publishes held
|
|
262
|
+
by convention alone, and lapsed silently in two opposite directions (#133):
|
|
263
|
+
|
|
264
|
+
| what a repository declares | what it actually formatted to |
|
|
265
|
+
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
266
|
+
| `export { default } from "@heroiclands/package-build/prettier"` | the shared conventions |
|
|
267
|
+
| `{ ...PRETTIER_BASE }`, without the `**/*.md` override | markdown at `tabWidth: 4` — every note reindenting away from the form it was written in |
|
|
268
|
+
| a partial `.prettierrc`, e.g. `{"tabWidth": 2}` | Prettier's defaults for `printWidth`, `trailingComma`, `experimentalTernaries`, … |
|
|
269
|
+
| nothing at all | the shared conventions here, Prettier's own in your editor and in `npx prettier` |
|
|
270
|
+
|
|
271
|
+
Every `format` run now reports each disagreement by name, before the per-file
|
|
272
|
+
report:
|
|
273
|
+
|
|
274
|
+
```text
|
|
275
|
+
prettier.config.mjs: warning: markdown `tabWidth` is 4 here; the shared configuration says 2
|
|
276
|
+
.prettierrc: warning: `printWidth` is not set here, so Prettier's own default applies; the shared configuration says 100
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
A repository with no Prettier config is warned too, with the one line that fixes
|
|
280
|
+
it — that case is the sharper one, because the shared conventions then reach this
|
|
281
|
+
command and nothing else, so a bare `npx prettier --check .` and the lint chain
|
|
282
|
+
take turns rewriting the same lines.
|
|
283
|
+
|
|
284
|
+
**Nothing here fails a build.** Every finding is a `warning`, the exit code is
|
|
285
|
+
untouched, and a deliberate local override keeps working exactly as before — it
|
|
286
|
+
just stops being silent.
|
|
287
|
+
|
|
288
|
+
New export: `checkPrettierConventions(root)` from
|
|
289
|
+
`@heroiclands/package-build/engine/prose-lint`, and the pure comparison behind it,
|
|
290
|
+
`sharedPrettierDivergence(resolved, file)` from
|
|
291
|
+
`@heroiclands/package-build/engine/prose-config`.
|
|
292
|
+
- 9f638fa: **A note whose secondary document has no pack is now a finding, instead of
|
|
293
|
+
losing that document in silence.**
|
|
294
|
+
|
|
295
|
+
A note produces more than one document as a matter of course: an item note an
|
|
296
|
+
Item and the JournalEntry its prose becomes, a map note a Scene and a
|
|
297
|
+
JournalEntry, an actor note an Actor and a JournalEntry since #337. Where the
|
|
298
|
+
configuration declares no pack for one of them, that document was dropped while
|
|
299
|
+
the rest of the note compiled into a pack that does exist. The build succeeded,
|
|
300
|
+
the compendium shipped, and the missing half was discoverable only by noticing
|
|
301
|
+
it was not there.
|
|
302
|
+
|
|
303
|
+
#146 already reports a note **nothing** claims, and could not see this: it asks
|
|
304
|
+
one question of the whole configuration — does any pack claim this type — and a
|
|
305
|
+
note that compiles its Item into an Item pack answers yes.
|
|
306
|
+
|
|
307
|
+
> a note of type "being" compiles into a JournalEntry as well as an Actor, and
|
|
308
|
+
> `packs:` declares no JournalEntry pack — so the JournalEntry is dropped with
|
|
309
|
+
> no error while the rest of the note compiles. Declare a JournalEntry pack in
|
|
310
|
+
> package-build.config.yaml, or accept the loss deliberately by not authoring
|
|
311
|
+
> what it would have carried.
|
|
312
|
+
|
|
313
|
+
The message names the note, the class with no pack, and the class that did
|
|
314
|
+
compile — the last because it is what tells the two findings apart at a glance:
|
|
315
|
+
one is a `type:` to correct, this one a pack to declare.
|
|
316
|
+
|
|
317
|
+
**Asked per note, not per type**, which is the difference between a useful
|
|
318
|
+
finding and a useless one. `Journals` declines a doc-carrying note whose body is
|
|
319
|
+
empty — an item with no prose gets no doc — so whether an item note produces a
|
|
320
|
+
JournalEntry is decided by the note. `sohl-kethira-basic` declares no
|
|
321
|
+
JournalEntry pack and ships 393 notes whose descriptions are _deliberately_
|
|
322
|
+
empty under the Fan Material Guidelines its configuration explains at length; a
|
|
323
|
+
type-level answer would report every one of them for losing a document none of
|
|
324
|
+
them produces. It reports none.
|
|
325
|
+
|
|
326
|
+
**It names no system**, so a type one system maps and another does not stays
|
|
327
|
+
silent for the system that declines it, per #79. That holds by construction: the
|
|
328
|
+
`Item` and `Actor` rows fold the systems' maps together before this sees them,
|
|
329
|
+
so a type appears once or not at all and no system is ever named.
|
|
330
|
+
|
|
331
|
+
**What it finds today.** `Song-of-Heroic-Lands-FoundryVTT` and `sohl-thalorna`
|
|
332
|
+
report nothing — every document their notes produce already has a pack.
|
|
333
|
+
`harn-ensemble` reports 2,512: it declares two Actor packs and no JournalEntry
|
|
334
|
+
pack, so every one of its beings has been losing the `{#appearance}` and
|
|
335
|
+
`{#dossier}` prose it carries. When the issue was filed no tree authored the
|
|
336
|
+
affected combination; one does now.
|
|
337
|
+
|
|
338
|
+
### Patch Changes
|
|
339
|
+
|
|
340
|
+
- 6499109: **A build now reads the configuration of the tree it was run in.**
|
|
341
|
+
|
|
342
|
+
The configuration was located by walking up from the installed package's own
|
|
343
|
+
directory. That is the same file as the working directory's in every ordinary
|
|
344
|
+
install — and a different one in a git worktree nested under its parent checkout
|
|
345
|
+
with no `node_modules` of its own. Node's resolution walks parent directories,
|
|
346
|
+
so such a worktree resolves `@heroiclands/package-build` out of the _parent's_
|
|
347
|
+
`node_modules`; the walk started inside the parent and landed on the parent's
|
|
348
|
+
`package-build.config.yaml`. The build then compiled the parent's content tree
|
|
349
|
+
into the parent's `build/`, said so only in absolute paths that are easy to read
|
|
350
|
+
past, and exited 0.
|
|
351
|
+
|
|
352
|
+
Resolution now starts at `process.cwd()` and falls back to the installed
|
|
353
|
+
package's directory only when that finds nothing.
|
|
354
|
+
|
|
355
|
+
| running `content-build package compile` in | before | after |
|
|
356
|
+
| -------------------------------------------- | ----------------------- | ---------------- |
|
|
357
|
+
| a repository, or any directory below it | that repository | unchanged |
|
|
358
|
+
| a nested worktree that has had `npm ci` run | the worktree | unchanged |
|
|
359
|
+
| a nested worktree with **no `node_modules`** | _the parent checkout_ | **the worktree** |
|
|
360
|
+
| a directory outside any repository | the installed package's | unchanged |
|
|
361
|
+
| anywhere, with `PACKAGE_BUILD_CONFIG` set | the file it names | unchanged |
|
|
362
|
+
|
|
363
|
+
Nothing about "a build reads one tree however it was launched" changes: the walk
|
|
364
|
+
climbs, so every directory inside a repository still resolves that repository's
|
|
365
|
+
single configuration.
|
|
366
|
+
|
|
367
|
+
When both walks find a configuration and they disagree, the working directory's
|
|
368
|
+
is read and the ignored one is named in a warning on stderr. The disagreement is
|
|
369
|
+
worth hearing on its own — it is the cheapest signal that this tree is building
|
|
370
|
+
on another checkout's `node_modules`, which is also a masked missing dependency.
|
|
371
|
+
`npm ci` in the worktree silences it properly.
|
|
372
|
+
|
|
373
|
+
**Why this was worth a fix rather than a note.** A silent wrong-tree build does
|
|
374
|
+
not merely fail to prove what was wanted, it produces confident evidence for the
|
|
375
|
+
wrong tree — and on an output-preserving sweep there is no observation that
|
|
376
|
+
distinguishes success from it. The usual tell is a zero diff where a change was
|
|
377
|
+
expected; a sweep that expects zero differences has no tell at all.
|
|
378
|
+
|
|
379
|
+
`resolveConfigFile()` is exported from
|
|
380
|
+
`@heroiclands/package-build/engine/pack-config`, reporting the chosen file and
|
|
381
|
+
each walk's own answer, so a caller can ask which tree it is about to compile
|
|
382
|
+
without re-deriving the resolution and risking disagreement with the loader.
|
|
383
|
+
- c229f2b: Stop shipping a `node_modules` symlink, which had broken every release for a
|
|
384
|
+
day.
|
|
385
|
+
|
|
386
|
+
A worktree's `node_modules` symlink — a 120000 blob holding one developer's
|
|
387
|
+
absolute path — was committed on 2026-09-11. `.gitignore` said
|
|
388
|
+
`/node_modules/`, and a trailing slash matches a directory rather than a
|
|
389
|
+
symlink, so nothing refused it.
|
|
390
|
+
|
|
391
|
+
The release job installs, runs the tests, and then hands over to the changesets
|
|
392
|
+
action, which does `git reset --hard` before versioning. That reset restored the
|
|
393
|
+
symlink over the top of the install, pointing at a path no runner has, so every
|
|
394
|
+
module became unresolvable and the release died on `changeset: not found`. The
|
|
395
|
+
tests had already passed, because they run before the reset.
|
|
396
|
+
|
|
397
|
+
The symlink is untracked, the ignore rule now matches a symlink at any depth,
|
|
398
|
+
and CI refuses a tracked `node_modules` path outright — the release is the only
|
|
399
|
+
thing this breaks, and no pull request check would otherwise notice.
|
|
400
|
+
- 80f40b2: Unblock releasing. Every run of the release workflow had failed at
|
|
401
|
+
`changeset version` with `sh: 1: changeset: not found` since 2026-09-11, so
|
|
402
|
+
nothing reached the registry past 20.0.0 while `main` went on believing itself
|
|
403
|
+
released.
|
|
404
|
+
|
|
405
|
+
The workflow installed `npm@latest` before publishing, to clear an OIDC floor of
|
|
406
|
+
11.5. That was written when Node 24.0–24.4 bundled npm 11.3–11.4; since 24.5 the
|
|
407
|
+
bundled npm has cleared the floor on its own, and the step became a no-op that
|
|
408
|
+
nobody removed. On 2026-09-11 `latest` became npm 12, which stopped putting
|
|
409
|
+
`node_modules/.bin` on the PATH of a run-script's shell, and a release path
|
|
410
|
+
nobody had touched broke.
|
|
411
|
+
|
|
412
|
+
The install is gone rather than pinned: the npm that publishes is now the one
|
|
413
|
+
Node brings, so its version follows `node-version` instead of a number kept in
|
|
414
|
+
step by hand. A check in its place asserts the floor and fails loudly if a
|
|
415
|
+
future Node pin ever drops below it — an assertion cannot go quietly stale the
|
|
416
|
+
way the comment it replaces did. The version script also resolves its binary
|
|
417
|
+
through `npx`, as the publish script already did.
|
|
418
|
+
- 12061da: Release again. Since 2026-09-11 every run of the release workflow had died at
|
|
419
|
+
`changeset version` with `sh: 1: changeset: not found`, so nothing reached the
|
|
420
|
+
registry past 20.0.0.
|
|
421
|
+
|
|
422
|
+
The workflow set `version-script`, which replaces the action's own invocation of
|
|
423
|
+
the changesets CLI with a shell command run through its exec — and under that
|
|
424
|
+
exec a bare `changeset` does not resolve on a runner. Left unset, the action
|
|
425
|
+
resolves the installed package with `require.resolve` and runs it with `node`,
|
|
426
|
+
depending on no PATH at all. The override is removed.
|
|
427
|
+
|
|
428
|
+
The fault was never in this repository's install. A diagnostic run confirmed
|
|
429
|
+
that after `npm ci` a runner has the package, has the bin linked, puts
|
|
430
|
+
`node_modules/.bin` first on a run-script's PATH, and resolves the bare name
|
|
431
|
+
through `npm run` — all in the same job that then failed.
|
|
432
|
+
|
|
433
|
+
The one thing the override bought, refreshing `package-lock.json`'s root
|
|
434
|
+
`version`, is now #385 rather than a reason to keep a step that does not run.
|
|
435
|
+
- 9572517: Finish unblocking the release. Removing the stale `npm install -g npm@latest`
|
|
436
|
+
fixed the npm-12 half, but the same change also swapped the version script's
|
|
437
|
+
bare `changeset` for `npx changeset`, and that turned the failure into `npm
|
|
438
|
+
error could not determine executable to run`.
|
|
439
|
+
|
|
440
|
+
The npx form was belt-and-braces and it was wrong. `npm run` already puts
|
|
441
|
+
`node_modules/.bin` on the PATH — the same mechanism `npm test` uses to reach
|
|
442
|
+
`vitest` earlier in the same job — so the bare name resolves the pinned local
|
|
443
|
+
copy with no lookup. npx instead consults the registry, which this job
|
|
444
|
+
configures for OIDC publishing rather than for reads.
|
|
445
|
+
|
|
446
|
+
The script is back to the bare binary, and the reasoning is recorded beside it
|
|
447
|
+
so the asymmetry with `publish-script` is not mistaken for an oversight again.
|
|
448
|
+
|
|
449
|
+
## 20.1.0
|
|
450
|
+
|
|
451
|
+
### Minor Changes
|
|
452
|
+
|
|
453
|
+
- f6d2a8f: **A fetched item catalogue is read one system at a time** (#58).
|
|
454
|
+
|
|
455
|
+
A pack declaring `system: hm3` already read only this repository's `hm3` and
|
|
456
|
+
system-neutral Item packs. The other half of the same lookup — the catalogue
|
|
457
|
+
fetched from a dependency that declares `itemCatalog: true` — was unscoped, and
|
|
458
|
+
both halves are merged into one address space keyed by `subType:shortcode`. So
|
|
459
|
+
an address that exists in both vocabularies resolved against whichever document
|
|
460
|
+
the dependency's other system happened to supply, and said nothing: `skill:awar`
|
|
461
|
+
is a real address under SoHL and under HM3 and means two different documents.
|
|
462
|
+
|
|
463
|
+
`deps fetch` now records what each extracted pack is, from the dependency's own
|
|
464
|
+
manifest, and `foreignItemCatalogDirs(config, system)` reads only the packs that
|
|
465
|
+
system may see plus the ones declaring no system at all.
|
|
466
|
+
|
|
467
|
+
**What a consumer sees**
|
|
468
|
+
|
|
469
|
+
| | Before | After |
|
|
470
|
+
| ------------------------------------ | ----------------------- | -------------------------------- |
|
|
471
|
+
| a pack with `system: hm3` | reads every cached pack | reads the `hm3` and neutral ones |
|
|
472
|
+
| a single-system build | reads every cached pack | unchanged |
|
|
473
|
+
| a cache filled by an earlier version | used as-is | treated as incomplete |
|
|
474
|
+
|
|
475
|
+
**Refill the cache once.** A cache written before this holds the items but not
|
|
476
|
+
what they are, and neither way of proceeding without that is honest: reading
|
|
477
|
+
every pack is the wrong-document failure above, and reading none fails a build
|
|
478
|
+
that worked. So it is incomplete, and `content-build deps fetch` refills it —
|
|
479
|
+
the command the cold-cache error already names.
|
|
480
|
+
|
|
481
|
+
### Patch Changes
|
|
482
|
+
|
|
483
|
+
- 2b1157e: **The license header is on every shipped module, and CI refuses a `TODO`.**
|
|
484
|
+
|
|
485
|
+
`engine/foreign-catalog.mjs` and `engine/schema-extract.mjs` shipped without the
|
|
486
|
+
GPL-3.0 header every other module carries — 109 of 111 had one, which is the
|
|
487
|
+
state a rule reaches when nothing checks it.
|
|
488
|
+
|
|
489
|
+
The forbidden-marker check now runs here too, through the org-wide
|
|
490
|
+
`HeroicLands/.github/actions/todos` action the other repositories already call.
|
|
491
|
+
It scans the whole checkout rather than a named list of directories: this
|
|
492
|
+
package's modules sit at its root as well as under `bin/`, `ci/`, `engine/`,
|
|
493
|
+
`hm3/` and `sohl/`, so a list would name sixteen root files today and quietly
|
|
494
|
+
stop covering the seventeenth.
|
|
495
|
+
|
|
496
|
+
Nothing a consumer imports changes.
|
|
497
|
+
|
|
3
498
|
## 20.0.0
|
|
4
499
|
|
|
5
500
|
### Major Changes
|