@heroiclands/package-build 18.2.0 → 20.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 +692 -0
- package/CONTENT.md +81 -10
- package/bin/content-build.mjs +7 -1
- package/ci/ci-docker.mjs +21 -0
- package/content-config.mjs +26 -24
- package/docs/content-format.md +408 -85
- package/engine/actor-compiler.mjs +197 -7
- package/engine/address-charset.mjs +23 -5
- package/engine/base-compiler.mjs +65 -2
- package/engine/bundles.mjs +9 -0
- package/engine/content-address.mjs +92 -1
- package/engine/content-format.mjs +102 -0
- package/engine/content-index.mjs +11 -8
- package/engine/content-links.mjs +37 -21
- package/engine/field-reference.mjs +57 -5
- package/engine/field-spec.mjs +214 -7
- package/engine/folder-notes.mjs +88 -1
- package/engine/foreign-catalog.mjs +4 -1
- package/engine/foundry-entries.mjs +16 -0
- package/engine/frontmatter-lint.mjs +215 -28
- package/engine/frontmatter.mjs +12 -12
- package/engine/generate.mjs +78 -46
- package/engine/helpers.mjs +87 -128
- package/engine/index.mjs +3 -0
- package/engine/item-compiler.mjs +44 -9
- package/engine/journals.mjs +27 -16
- package/engine/macros.mjs +8 -0
- package/engine/map-notes.mjs +7 -7
- package/engine/note-ids.mjs +25 -1
- package/engine/note-vocabulary.mjs +76 -9
- package/engine/retired-fields.mjs +57 -16
- package/engine/runtime-only-fields.mjs +204 -0
- package/engine/scenes.mjs +19 -28
- package/engine/schema-check.mjs +23 -1
- package/engine/site-index.mjs +17 -0
- package/engine/subtype-registry.mjs +30 -0
- package/engine/system-block.mjs +81 -3
- package/engine/web-wikilinks.mjs +33 -27
- package/engine/wikilink-syntax.mjs +7 -0
- package/engine/wikilinks.mjs +74 -16
- package/hm3/actors.mjs +70 -23
- package/package.json +2 -2
- package/sohl/actors.mjs +106 -7
- package/sohl/item-fields.mjs +203 -0
- package/sohl/note-schemas.mjs +6 -3
- package/types/content-config.d.mts +0 -7
- package/types/engine/actor-compiler.d.mts +83 -3
- package/types/engine/address-charset.d.mts +22 -4
- package/types/engine/base-compiler.d.mts +54 -3
- package/types/engine/content-address.d.mts +64 -0
- package/types/engine/content-format.d.mts +9 -0
- package/types/engine/field-spec.d.mts +271 -3
- package/types/engine/folder-notes.d.mts +59 -0
- package/types/engine/foundry-entries.d.mts +6 -0
- package/types/engine/frontmatter-lint.d.mts +18 -2
- package/types/engine/frontmatter.d.mts +11 -11
- package/types/engine/generate.d.mts +27 -0
- package/types/engine/helpers.d.mts +37 -38
- package/types/engine/index.d.mts +1 -0
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/note-ids.d.mts +14 -0
- package/types/engine/retired-fields.d.mts +29 -13
- package/types/engine/runtime-only-fields.d.mts +102 -0
- package/types/engine/schema-check.d.mts +10 -1
- package/types/engine/subtype-registry.d.mts +21 -0
- package/types/engine/system-block.d.mts +28 -2
- package/types/sohl/actors.d.mts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,697 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 20.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 1670506: **An omitted address segment now defaults from where the link is written** (#336),
|
|
8
|
+
instead of being wildcarded or searched for. Three resolvers each carried their
|
|
9
|
+
own reading of a partial address and disagreed; there is one rule now, in
|
|
10
|
+
`expandAddress` beside `canonicalKey`.
|
|
11
|
+
|
|
12
|
+
- **package** omitted → the current package.
|
|
13
|
+
- **system** omitted → the **system block the link sits under**: anywhere under
|
|
14
|
+
`sohl:` is `sohl`, anywhere under `hm3:` is `hm3`, and anywhere else —
|
|
15
|
+
top-level frontmatter, `data:`, body prose — is `none`. The enclosing block
|
|
16
|
+
decides at any depth; the field has no say.
|
|
17
|
+
|
|
18
|
+
Every short form therefore expands to exactly one canonical address before
|
|
19
|
+
lookup. Resolution is a `Map.get`, with no candidate set and no single-hit rule.
|
|
20
|
+
|
|
21
|
+
**Under `none`, a system-bearing type addresses its documentation.** A note's
|
|
22
|
+
`none` address _is_ its `doc<type>` journal, so a prose `[[affiliation-x|…]]`
|
|
23
|
+
names the page — what a prose link almost always means. A prose link that means
|
|
24
|
+
the Item states the system: `[[sohl-affiliation-x|…]]`. A `macro` and the map
|
|
25
|
+
types are **not** redirected: their own documents are core ones already at
|
|
26
|
+
`none`, so `macro-x` still names the Macro.
|
|
27
|
+
|
|
28
|
+
**Breaking, in three ways a consumer will notice**
|
|
29
|
+
|
|
30
|
+
| | before | after |
|
|
31
|
+
| ----------------------------------- | -------------------------------------------- | ------------------------------------------------- |
|
|
32
|
+
| a bare prose link to an item type | the Item's UUID | the documentation journal's |
|
|
33
|
+
| a bare address naming no local note | fell through to any dependency publishing it | `unresolved`; qualify it to reach another package |
|
|
34
|
+
| `[[Skill-Climb\|…]]` | resolved, case folded | `not-lowercase` |
|
|
35
|
+
|
|
36
|
+
The first rewrites every such link in every pack. The second is the point of the
|
|
37
|
+
issue: a link resolved into another package only because no local note claimed
|
|
38
|
+
the address, and would have retargeted silently the day one did.
|
|
39
|
+
|
|
40
|
+
**Two defects go with it.** The index's system-blind `type/shortcode` key is
|
|
41
|
+
gone — it was set with a plain `Map.set`, so two notes in one package sharing a
|
|
42
|
+
`(type, shortcode)` across systems silently overwrote each other while both
|
|
43
|
+
canonical keys sat correctly beside it. And cross-package `ambiguous` is now
|
|
44
|
+
unreachable: one expanded address names one package, so a lookup returns one
|
|
45
|
+
entry or none. The finding is retained for older vendored manifests.
|
|
46
|
+
|
|
47
|
+
**An address capitalises nothing but its shortcode.** Package, system and type
|
|
48
|
+
are closed vocabularies with one spelling each; a shortcode is case-sensitive and
|
|
49
|
+
routinely mixed (`Clb`, `LtShoe`), so it keeps its case. Neither tree carried a
|
|
50
|
+
violation — 10,538 authored targets checked. Requiring the shortcode to be
|
|
51
|
+
lowercase too is tracked as #340.
|
|
52
|
+
- c906d54: **A shortcode must now match `^[a-z0-9]+$`** — lowercase letters and digits only
|
|
53
|
+
(#340). So must every other address segment; the charset is one rule with no
|
|
54
|
+
exceptions left in it.
|
|
55
|
+
|
|
56
|
+
`Dgr` beside `dgr` is a distinction nobody can say out loud and can only see by
|
|
57
|
+
looking twice. It was also a silent identity collapse: `canonicalKey` lowercases
|
|
58
|
+
the address it builds, so a note declaring `Clb` published
|
|
59
|
+
`sohl-sohl-weapongear-clb` and derived its `_id` from that — and two notes
|
|
60
|
+
differing only in case shared one address, one `_id` and one URL with nothing to
|
|
61
|
+
report it, because the shortcode check compared shortcodes (genuinely distinct)
|
|
62
|
+
and the address check saw one address.
|
|
63
|
+
|
|
64
|
+
It forced two exceptions elsewhere, and both go: #336 had to exempt the shortcode
|
|
65
|
+
from the lowercase rule it pinned on every other segment, and #346 had to fold
|
|
66
|
+
the shortcode's case in the item catalogue because an address is lowercased when
|
|
67
|
+
it is read.
|
|
68
|
+
|
|
69
|
+
**A consumer with a capital in a shortcode will fail to build**, citing the
|
|
70
|
+
note's file, line and column. The sweep is mechanical: every violation in every
|
|
71
|
+
tree is a capital letter — no underscores, hyphens or other characters occur —
|
|
72
|
+
and **nothing collides when folded**, checked per `(type, shortcode)` in every
|
|
73
|
+
tree.
|
|
74
|
+
|
|
75
|
+
| tree | shortcodes to change |
|
|
76
|
+
| --------------------------------- | --------------------: |
|
|
77
|
+
| `Song-of-Heroic-Lands-FoundryVTT` | 438 |
|
|
78
|
+
| `sohl-thalorna` | 96 |
|
|
79
|
+
| every other tree | 0 — already compliant |
|
|
80
|
+
|
|
81
|
+
**No document changes identity and no URL moves**, because an address and a
|
|
82
|
+
document `_id` already derive from the lowercased form. References need no edit
|
|
83
|
+
either: no authored wikilink target carries a capital (10,538 checked), and
|
|
84
|
+
`model:` addresses are already lowercase.
|
|
85
|
+
|
|
86
|
+
**The emitted packs do change, in three narrow ways**, measured on `sohl`'s 438:
|
|
87
|
+
|
|
88
|
+
| change | count |
|
|
89
|
+
| ----------------------------------------------------- | ----: |
|
|
90
|
+
| `system.shortcode` on an Item document, `Clb` → `clb` | 438 |
|
|
91
|
+
| embedded item `_id` / `_key` on one being | 6 |
|
|
92
|
+
| journal pages showing a shortcode in a content table | 5 |
|
|
93
|
+
| **top-level document `_id`** | **0** |
|
|
94
|
+
| **addresses and published URLs** | **0** |
|
|
95
|
+
|
|
96
|
+
The first is the point: the emitted field now matches the address built from it.
|
|
97
|
+
The six embedded ids move because an embedded id derives from the item's own
|
|
98
|
+
`system.shortcode`, which #346 deliberately does **not** case-fold — folding
|
|
99
|
+
there would re-identify documents rather than look them up.
|
|
100
|
+
- 4890864: **A map note's `image:` is no longer read** (#149). Its art is `img:`, as every
|
|
101
|
+
other note type's is.
|
|
102
|
+
|
|
103
|
+
This is the third and last step of the rename #142 began. Through the retirement
|
|
104
|
+
window both spellings were read, `img` won where a note carried both, and a note
|
|
105
|
+
still writing `image` got a located warning — it compiled to the byte-identical
|
|
106
|
+
document, so failing a build over it would have redded a tree that had done
|
|
107
|
+
nothing wrong. The trees have since been swept, so the alias has nothing left to
|
|
108
|
+
honour and is gone.
|
|
109
|
+
|
|
110
|
+
**No shipped tree is affected.** Every content tree was checked — `sohl`,
|
|
111
|
+
`sohl-thalorna`, `sohl-kethira-basic`, `harn-ensemble` and `harn-adventures` —
|
|
112
|
+
and none writes the retired spelling. The window did its job; this only closes
|
|
113
|
+
it.
|
|
114
|
+
|
|
115
|
+
**What an unswept note now sees.** Two errors rather than one warning, and it
|
|
116
|
+
stops compiling: `image` in a `sohl:` block is reported as a key the type does
|
|
117
|
+
not have, and the `img` the note therefore never declared is reported as
|
|
118
|
+
missing. The fix is the rename, and moving the key to the note's top level while
|
|
119
|
+
you are there — art is not system-specific, so it belongs beside every other
|
|
120
|
+
note's `img` rather than inside a system block.
|
|
121
|
+
|
|
122
|
+
**A tile's `image:` is untouched.** `sohl.tiles.<key>.image` is a nested
|
|
123
|
+
placeable's texture, not the note's own artwork, and was never the retired field:
|
|
124
|
+
the check reads the `sohl:` block's own keys and never descends into one.
|
|
125
|
+
|
|
126
|
+
**Nothing was added to refuse it.** The two findings above are the ordinary
|
|
127
|
+
unknown-key and required-field checks, which is the point of a rename's third
|
|
128
|
+
step — one that had to add a standing refusal would be one whose replacement
|
|
129
|
+
never arrived. No tombstone entry is kept: the absence of an alias is the record.
|
|
130
|
+
- a1c21e8: **A being's items entry names the item it copies with `model:`, an address**
|
|
131
|
+
(#334). The top-level `shortcode:` it replaces is retired.
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
sohl:
|
|
135
|
+
items:
|
|
136
|
+
- { model: skill-wpnc, system: { masteryLevelBase: 52 } } # this package
|
|
137
|
+
- { model: sohl-sohl-weapongear-dgr } # another one
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The old key was doing two jobs badly. It **selected a template**, while the
|
|
141
|
+
`system.shortcode` beside it **was** the compiled item's identity — one word for
|
|
142
|
+
two things, which the compiler's own error messages had to keep explaining. And
|
|
143
|
+
it could not say **which package** the template came from: `loadItemsMap`
|
|
144
|
+
flattened every local Item pack and every dependency catalogue into one
|
|
145
|
+
`subType:shortcode` space where a local definition silently shadowed a foreign
|
|
146
|
+
one. In `sohl-thalorna`, 25,485 of 26,251 model references reach into `sohl` and
|
|
147
|
+
none of them said so; the day that repository ships its own `weapongear-dgr`,
|
|
148
|
+
every entry citing `dgr` would have retargeted with a green build and no
|
|
149
|
+
diagnostic.
|
|
150
|
+
|
|
151
|
+
**What changes for an author**
|
|
152
|
+
|
|
153
|
+
| | before | after |
|
|
154
|
+
| ------------------------ | ---------------------------------- | ---------------------------------------------- |
|
|
155
|
+
| naming a template | `{ shortcode: wpnc, type: skill }` | `{ model: skill-wpnc }` |
|
|
156
|
+
| reaching another package | impossible | `{ model: sohl-sohl-skill-wpnc }` |
|
|
157
|
+
| `type:` beside it | required | refused — the address names the type |
|
|
158
|
+
| a custom item | `name` + `type` + `system` | unchanged, and `system.shortcode` now required |
|
|
159
|
+
|
|
160
|
+
A `model` is read by the same grammar every wikilink is (#336), so it is written
|
|
161
|
+
at whatever length says what it means and the system segment defaults from the
|
|
162
|
+
block the entry sits in — which is why the short form names an **Item** here
|
|
163
|
+
while the same string in body prose names a page.
|
|
164
|
+
|
|
165
|
+
**The catalogue is package-aware.** Every item is keyed under its own package as
|
|
166
|
+
well as unqualified, so a `model` that states a package resolves to that
|
|
167
|
+
package's item and nothing local can shadow it, while a `model` that states none
|
|
168
|
+
still resolves locally-first exactly as before. `foreignItemCatalogDirs` returns
|
|
169
|
+
`{ dir, package }` rather than a bare path.
|
|
170
|
+
|
|
171
|
+
**Consumer sweeps**: HeroicLands/Song-of-Heroic-Lands-FoundryVTT#1875 (95 beings,
|
|
172
|
+
1,557 entries) and HeroicLands/sohl-thalorna#176 (645 beings, 26,251 entries plus
|
|
173
|
+
938 shortcodes derived for custom gear).
|
|
174
|
+
|
|
175
|
+
### Minor Changes
|
|
176
|
+
|
|
177
|
+
- ccf35a1: **An actor note now publishes documentation, like every other note that compiles
|
|
178
|
+
into a system-bearing document** (#337).
|
|
179
|
+
|
|
180
|
+
A being used to produce its Actor and nothing else. That left it the one
|
|
181
|
+
system-bearing note with no address at `none` — its only address named the
|
|
182
|
+
Actor — so a prose link written `[[being-<shortcode>|Text]]` had no page to land
|
|
183
|
+
on. `sohl-thalorna` alone carries 772 such links.
|
|
184
|
+
|
|
185
|
+
`docEntryTypes` is now `itemTypes` plus the actor types the shipped subtype maps
|
|
186
|
+
declare (derived from those maps, not listed again), plus `macro` and the map
|
|
187
|
+
types. Only `doc` stays outside it, for the reason that actually applies to it:
|
|
188
|
+
its single document _is_ the prose.
|
|
189
|
+
|
|
190
|
+
**What a consumer sees**
|
|
191
|
+
|
|
192
|
+
| Before | After |
|
|
193
|
+
| ------------------------------------- | -------------------------------------------------------------------- |
|
|
194
|
+
| `<pkg>-<system>-being-<shortcode>` | that, **and** `<pkg>-none-docbeing-<shortcode>` |
|
|
195
|
+
| a being's prose reachable only inline | also a JournalEntry, and a page the site publishes |
|
|
196
|
+
| `[[docbeing-x\|Text]]` unresolved | resolves, in the link checker, the pack compilers and the site build |
|
|
197
|
+
|
|
198
|
+
Packs gain one JournalEntry per being **carrying prose**; a being with an empty
|
|
199
|
+
body compiles no entry, exactly as an item with an empty body does. No existing
|
|
200
|
+
document changes.
|
|
201
|
+
|
|
202
|
+
**A being keeps its prose inline as well**, and the asymmetry with items is
|
|
203
|
+
deliberate. `system.appearance` and `system.dossier` stay as rendered text, where
|
|
204
|
+
an item's description is an `@UUID` pointer into its journal. One item is
|
|
205
|
+
embedded across hundreds of beings, so baking its description into every copy
|
|
206
|
+
bloats the compendium by the length of the prose times the number of carriers,
|
|
207
|
+
and the pointer buys that back. An actor is singular, so the same indirection
|
|
208
|
+
would cost a reader a click and save nothing.
|
|
209
|
+
- afb3650: **An asset path's first segment says which package owns it, and the rule is now
|
|
210
|
+
stated and tested** (#331).
|
|
211
|
+
|
|
212
|
+
`img:` and `portrait:` have always answered "which package holds this file?" in
|
|
213
|
+
their first segment, but nothing wrote the rule down and nothing asserted it —
|
|
214
|
+
the only way to confirm it was to compile a package and read `build/packs-json`.
|
|
215
|
+
|
|
216
|
+
| Authored path starts with | Owner | Emitted |
|
|
217
|
+
| ------------------------- | --------------------- | -------------------- |
|
|
218
|
+
| `systems/` | a separate **system** | unchanged |
|
|
219
|
+
| `modules/` | a separate **module** | unchanged |
|
|
220
|
+
| anything else | **this package** | `<assetRoot>/<path>` |
|
|
221
|
+
|
|
222
|
+
**The third row is now true.** The translator prefixed `icons/…` and `images/…`
|
|
223
|
+
and passed everything else through — the same answer for every path any tree
|
|
224
|
+
authors today, and the wrong one for the next directory a package ships.
|
|
225
|
+
`sohl-kethira-basic` keeps art under `assets/artwork/`, so an authored
|
|
226
|
+
`artwork/deity.webp` would have shipped unprefixed: a 404 in Foundry, reported
|
|
227
|
+
by nothing. Ownership is the rule; the directory names inside a package's
|
|
228
|
+
`assets/` tree are that package's business.
|
|
229
|
+
|
|
230
|
+
An address naming no package — an absolute URL, a `data:` URI, a `/`-rooted
|
|
231
|
+
path — passes through, on the same rule rather than as an exception. `worlds/`
|
|
232
|
+
is deliberately not exempt: a package may not ship art out of a world.
|
|
233
|
+
|
|
234
|
+
**No compiled document changes.** Compiling `sohl-thalorna` and this system's
|
|
235
|
+
own tree before and after gives byte-identical `packs-json`; every path either
|
|
236
|
+
tree authors is `icons/`, `images/` or already `systems/`-rooted.
|
|
237
|
+
|
|
238
|
+
**`banner:` is a path that does not follow this rule, and is documented as
|
|
239
|
+
deliberate rather than reconciled.** It reaches no compiled document: it is a
|
|
240
|
+
top-level key the Hugo theme reads, and the theme prefixes a relative value with
|
|
241
|
+
`images/` and joins it onto `params.cdnBaseURL`. The two address different
|
|
242
|
+
places — `img:` a file Foundry serves, `banner:` a file the CDN serves.
|
|
243
|
+
- e3d300c: **An `img:` authored on a type that emits none is now reported instead of
|
|
244
|
+
dropped in silence** (#349).
|
|
245
|
+
|
|
246
|
+
`img` is a shared top-level field — it maps onto `document.img`, so it is legal
|
|
247
|
+
on every note whatever the type — but not every document has one. `doc`, `place`,
|
|
248
|
+
`lore` and `scenario` compile into a JournalEntry, `folder` into a Foundry
|
|
249
|
+
`Folder`, and neither carries artwork; a `homepage` compiles into no compendium
|
|
250
|
+
document at all. On any of those the authored path went nowhere, the note
|
|
251
|
+
validated, the tree compiled clean, and nothing said so.
|
|
252
|
+
|
|
253
|
+
The frontmatter lint now warns, naming the note, the key and what the type
|
|
254
|
+
compiles into. `portrait:` is checked the same way, so an item authoring one is
|
|
255
|
+
told it has nowhere to put a sheet portrait.
|
|
256
|
+
|
|
257
|
+
**What it does not report.** `img: null` — that is the blessed way to say "this
|
|
258
|
+
note names no art", and on a type with no art it is a true and harmless thing to
|
|
259
|
+
say. A warning rather than an error, too: the note still compiles correctly, and
|
|
260
|
+
a note's top level is the generated page's front matter as well, so a site
|
|
261
|
+
template may read there what no document carries.
|
|
262
|
+
|
|
263
|
+
**Which types those are is derived, not listed.** Each pass declares the art it
|
|
264
|
+
writes (`BasePackCompiler.emitsArt`), and `emittedArtFor` walks type → document →
|
|
265
|
+
pass to answer. A second table of iconless types would be free to drift from what
|
|
266
|
+
is actually emitted, which is the defect rather than the check.
|
|
267
|
+
|
|
268
|
+
**What a consumer sees.** New advisory findings on notes already in this state;
|
|
269
|
+
warnings do not fail a build. `sohl-thalorna` has 57 — the `Lore/Totems/` and
|
|
270
|
+
`Lore/Deities/Kemetian/` clusters, which carried an `img:` from when they
|
|
271
|
+
compiled to affiliation items. The other shipped trees are clean.
|
|
272
|
+
- bd5e6cb: **`lore` declares `gathering`, a genre for a scheduled public occasion** (#333).
|
|
273
|
+
|
|
274
|
+
A tournament or martial games, a great market or fair, a religious festival, a
|
|
275
|
+
ceremony or rite: something that happens at a place and a time, on a cycle, and
|
|
276
|
+
that people travel to. The genre had no value, and neither neighbour fitted.
|
|
277
|
+
|
|
278
|
+
| genre | what it covers |
|
|
279
|
+
| ----------- | ---------------------------------------------------------------- |
|
|
280
|
+
| `calendar` | the _reckoning_ — the cycle, the seasons, the dating system |
|
|
281
|
+
| `culture` | a social grouping of people |
|
|
282
|
+
| `gathering` | the occasion itself — who attends, what is contested or observed |
|
|
283
|
+
|
|
284
|
+
A festival's **date** is `calendar`; the festival is not. A tournament is not a
|
|
285
|
+
matter of time-reckoning at all, and a great market is not a grouping of people.
|
|
286
|
+
|
|
287
|
+
**Why it matters beyond labelling.** `site.sections` narrows a section with
|
|
288
|
+
`listSubType`, so a subType is what makes a genre browsable. Without one, a
|
|
289
|
+
consumer declaring a Gatherings section would sweep in the castes, Marriage and
|
|
290
|
+
Personal Names alongside the games — the notes could not be listed as what they
|
|
291
|
+
are.
|
|
292
|
+
|
|
293
|
+
**On the name.** `festival` is too narrow: a tournament is not a festival, a
|
|
294
|
+
great market is a fair, and a rite is not a celebration. `event` is avoided
|
|
295
|
+
because it already names something else in SoHL — the event queue and
|
|
296
|
+
`system.scheduledActions`, where an event is a timed thing that fires in play.
|
|
297
|
+
`gathering` covers the whole set, and matches how the other `lore` genres are
|
|
298
|
+
named: a single lowercase noun for a kind of thing.
|
|
299
|
+
|
|
300
|
+
Nothing existing changes. A note already filed under `lore/culture` keeps
|
|
301
|
+
compiling until its author moves it; `subType` is not an address, so nothing
|
|
302
|
+
resolves through it.
|
|
303
|
+
- 5630e7c: **A note can no longer author a field the document writes in play** (#330).
|
|
304
|
+
|
|
305
|
+
A data model declares everything a document stores, and part of that is runtime
|
|
306
|
+
state — an affliction's `onsetDate` is the world time its onset fired at.
|
|
307
|
+
Writing `sohl.system.onsetDate` reached it as directly as any other field: the
|
|
308
|
+
block is a verbatim passthrough, no declared field claimed the path, and the
|
|
309
|
+
schema check's fatal direction is _undeclared_, which a field the schema really
|
|
310
|
+
does declare satisfies. So a compiled pack could ship one world's play state to
|
|
311
|
+
every world that installed it, with the build reporting success.
|
|
312
|
+
|
|
313
|
+
A field declaration may now say `runtimeOnly`, whose value is the reason — what
|
|
314
|
+
the field holds — the way `topLevelMeans` already works. It states both halves
|
|
315
|
+
of one fact:
|
|
316
|
+
|
|
317
|
+
| declaration | authored | absent |
|
|
318
|
+
| ------------- | ----------- | --------------- |
|
|
319
|
+
| ordinary | emitted | default written |
|
|
320
|
+
| `runtimeOnly` | **refused** | key omitted |
|
|
321
|
+
|
|
322
|
+
The refusal names the note, the line, the whole key and the field's own reason,
|
|
323
|
+
and says that deleting it is the fix — there is no value that makes writing one
|
|
324
|
+
right. Omitting the key rather than emitting `null` is what leaves the data
|
|
325
|
+
model's own `initial` standing.
|
|
326
|
+
|
|
327
|
+
It is a property of the declaration, not a list of names, so it holds for any
|
|
328
|
+
runtime-only field any system adds later. Such an entry declares a `to` and no
|
|
329
|
+
`name`, which keeps it out of the authored vocabulary — the generated field
|
|
330
|
+
reference lists it under **Never authored** with its reason instead of as a row
|
|
331
|
+
an author might fill in — while still claiming the path for the passthrough.
|
|
332
|
+
|
|
333
|
+
**What a consumer sees**
|
|
334
|
+
|
|
335
|
+
- SoHL's six timed-phase dates are declared: `contractDate`, `onsetDate`,
|
|
336
|
+
`treatmentDate` and `resolutionDate` on `affliction`; `contractDate` and
|
|
337
|
+
`treatmentDate` on `trauma`. No tree authors one today, so no compiled
|
|
338
|
+
document changes.
|
|
339
|
+
- The refusal covers both positions a note can reach them from: a note's own
|
|
340
|
+
`<system>.system` block, and an actor's `items:` entry `system:` overlay,
|
|
341
|
+
which merges verbatim and so passed no field declaration at all.
|
|
342
|
+
- A runtime-only path is no longer reported as a field the builder forgot to
|
|
343
|
+
emit — "every compiled document will carry the field's initial value" is what
|
|
344
|
+
the declaration is _for_, so the warning could never be cleared.
|
|
345
|
+
- Regenerate the item frontmatter reference to pick up the new section.
|
|
346
|
+
- cf20488: **The specification's `subType` lists are compared to the declared vocabulary,
|
|
347
|
+
for every type** (#345).
|
|
348
|
+
|
|
349
|
+
`tests/content-format-agreement.test.ts` made `docs/content-format.md`
|
|
350
|
+
executable for the `data` property tables only. The other half of the same
|
|
351
|
+
vocabulary entry — a type's genres, which an author picks from and which a
|
|
352
|
+
note's `subType` is closed against — was prose that nothing read, free to
|
|
353
|
+
disagree with `note-vocabulary.mjs` in either direction. That is the drift #231
|
|
354
|
+
and #232 were filed about, on the half they did not reach. `gathering` (#333)
|
|
355
|
+
guarded `lore` alone, deliberately scoped to the type it changed.
|
|
356
|
+
|
|
357
|
+
**The five spellings converged on one first.** The document stated a type's
|
|
358
|
+
values as `subType`, `subType:`, `**subType**`, `**subType**:` and
|
|
359
|
+
`**subTypes**:`, and a reader that accepted every one of them would accept the
|
|
360
|
+
sixth by reading that section as declaring nothing — the exact failure the
|
|
361
|
+
comparison exists to catch. One shape is now stated in the specification and
|
|
362
|
+
enforced by the parser: `**subType**:` on its own line, then `- <value>` or
|
|
363
|
+
`- <value>: <definition>`, one bullet per value. A type with no `subType`, or
|
|
364
|
+
one whose values are not enumerated yet, writes no marker.
|
|
365
|
+
|
|
366
|
+
| Written | Read as |
|
|
367
|
+
| -------------------------------------------- | ----------------------------------------------------------- |
|
|
368
|
+
| `**subType**:` + a bullet per value | that type's closed value list, in document order |
|
|
369
|
+
| no marker | the type enumerates none — the ordinary case for nine types |
|
|
370
|
+
| any other spelling, or a marker with no list | a build error naming the line |
|
|
371
|
+
|
|
372
|
+
**What the comparison asks**, of every type rather than of `lore`: the values
|
|
373
|
+
the specification lists equal the values `NOTE_VOCABULARY` declares, in the same
|
|
374
|
+
order. `subTypes` stays three-valued — a list is closed, `null` is a `subType`
|
|
375
|
+
whose values are not enumerated, an absent key is a type with no `subType` at
|
|
376
|
+
all — and each reading is compared to what the document states. The `lore`-only
|
|
377
|
+
assertion is folded in.
|
|
378
|
+
|
|
379
|
+
Nothing that compiles changes: the values were already equal everywhere, in both
|
|
380
|
+
shapes, so this is about keeping them that way. `parseContentFormat` gains a
|
|
381
|
+
`subTypes` array per type, and throws on a marker it does not recognise —
|
|
382
|
+
reachable only through `content-format --spec <a copy of the document>`.
|
|
383
|
+
- 312379e: **Afflictions and traumas can declare their timed phases** (#329).
|
|
384
|
+
|
|
385
|
+
SoHL stores each timed phase as `{…DurationFormula, …DurationBase, …Date}`, and
|
|
386
|
+
the two authored thirds were declared by nothing. They were reachable only
|
|
387
|
+
through the raw `system:` passthrough — undocumented, uncoerced, and absent from
|
|
388
|
+
the field list every author-facing surface is built from — so no note in any
|
|
389
|
+
tree wrote one. Every shipped affliction carried `null`, and
|
|
390
|
+
`AfflictionLogic.rollDuration()` opens `if (!formula) return 0`: the timed-phase
|
|
391
|
+
machinery existed, and the content that would drive it could not be written.
|
|
392
|
+
|
|
393
|
+
They could not simply be declared either. `buildFromFields` wrote every declared
|
|
394
|
+
field unconditionally, so a declaration would have stamped `null` onto every
|
|
395
|
+
document — the same outcome, minus the ability to tell "unset" from "authored as
|
|
396
|
+
empty".
|
|
397
|
+
|
|
398
|
+
**`omitWhenAbsent`** is the missing capability: a field declaring it is emitted
|
|
399
|
+
when the note carries one and has its **key left out entirely** when it does
|
|
400
|
+
not, so the DataModel's own `initial` stands. It completes the table
|
|
401
|
+
`runtimeOnly` (#330) opened:
|
|
402
|
+
|
|
403
|
+
| declaration | authored | absent |
|
|
404
|
+
| ---------------- | ----------- | --------------- |
|
|
405
|
+
| ordinary | emitted | default written |
|
|
406
|
+
| `omitWhenAbsent` | emitted | key omitted |
|
|
407
|
+
| `runtimeOnly` | **refused** | key omitted |
|
|
408
|
+
|
|
409
|
+
The decision is made on the **position** a value came from, never on the value:
|
|
410
|
+
a declared `default: null` and an authored `null` are the same value and
|
|
411
|
+
opposite facts. `readFieldEntry` reports the source beside the value so the
|
|
412
|
+
position is resolved once rather than twice.
|
|
413
|
+
|
|
414
|
+
**Twelve fields are now declared vocabulary** — `onset`, `healingCheck` and
|
|
415
|
+
`resolution` on `affliction`; `healingCheck`, `bloodLossAdvance` and `course` on
|
|
416
|
+
`trauma` — each as both a `…DurationFormula` and a `…DurationBase`, in the
|
|
417
|
+
`sohl:` block and in the closed `data:` container alike. Intervals are in
|
|
418
|
+
seconds, and a bare number is a valid formula.
|
|
419
|
+
|
|
420
|
+
**What a consumer sees**
|
|
421
|
+
|
|
422
|
+
- The twelve appear in the generated item-frontmatter reference, with `_omitted_`
|
|
423
|
+
in the Default column rather than a value. Regenerate the page.
|
|
424
|
+
- The `unemitted` warnings these raised against a pinned schema clear.
|
|
425
|
+
- No compiled document changes: no tree authors one yet, and a note that writes
|
|
426
|
+
nothing emits nothing where it previously emitted nothing.
|
|
427
|
+
- `omitWhenAbsent` may not be combined with `default` (contradictory), with
|
|
428
|
+
`required`, or with `runtimeOnly`; the shipped declarations are checked for all
|
|
429
|
+
three.
|
|
430
|
+
|
|
431
|
+
### Patch Changes
|
|
432
|
+
|
|
433
|
+
- b5004f0: **A `model:` now resolves an item whose shortcode carries a capital** (#346). The
|
|
434
|
+
catalogue was keyed on the compiled document's `system.shortcode` exactly, while
|
|
435
|
+
an address is lower-cased when it is read — so `model: weapongear-clb` looked for
|
|
436
|
+
`weapongear:clb` while the document sat under `weapongear:Clb`, and every being
|
|
437
|
+
referencing one of the six mixed-case gear shortcodes in `sohl` failed to
|
|
438
|
+
compile.
|
|
439
|
+
|
|
440
|
+
The catalogue key folds the shortcode's case. The **id-bearing** address does
|
|
441
|
+
not: `itemAddress` seeds `embeddedItemId`, so folding there would change the
|
|
442
|
+
`_id` of every embedded item whose identity carries a capital — silently
|
|
443
|
+
re-identifying documents nothing about which had changed. A catalogue is a lookup
|
|
444
|
+
table; an id is a promise.
|
|
445
|
+
|
|
446
|
+
Verified by compiling both swept trees: `sohl` emits actors, items, macros,
|
|
447
|
+
scenes and adventures byte-identical to its pre-sweep baseline, and
|
|
448
|
+
`sohl-thalorna` differs only by the 938 embedded ids its sweep predicts.
|
|
449
|
+
- 4c57a5a: **A being's `data.portrait` reaches the actor** (#332). It never had: the
|
|
450
|
+
emitters read `blockProperty(fm, "portrait")`, which knows a system block and
|
|
451
|
+
the note's top level and never splits a dotted path, so the position the content
|
|
452
|
+
format names was invisible to them — and the `?? defaultImg` beside it turned
|
|
453
|
+
every miss into the subtype's icon rather than into a complaint. 646
|
|
454
|
+
`sohl-thalorna` beings authored a portrait, 341 of them pointing at art that
|
|
455
|
+
exists on disk, and every one compiled the generic person icon. Nothing warned.
|
|
456
|
+
|
|
457
|
+
`portrait` now resolves through the same declaration `data.species` does, in
|
|
458
|
+
both the `sohl` and `hm3` actor passes.
|
|
459
|
+
|
|
460
|
+
**A `data:` source has a retiring top-level spelling, and step 3b reads it.**
|
|
461
|
+
`data:` did not invent the facts it holds — it gathered them out of the note's
|
|
462
|
+
open top level, where `portrait:` sat beside `img:` — so the pre-`data:`
|
|
463
|
+
spelling of `data.<key>` is `<key>`, and until now nothing read it. That is why
|
|
464
|
+
this is a resolution-order fix and not a one-line emitter fix: `data.portrait`
|
|
465
|
+
had to start working _without_ breaking the top-level `portrait:` that `sohl`'s
|
|
466
|
+
own bestiary writes on every note.
|
|
467
|
+
|
|
468
|
+
The spelling is **derived**, not declared — a second declaration would be a
|
|
469
|
+
second place for one fact to live — and only a `data.` source has one, so
|
|
470
|
+
`protection.blunt` and `impact.die` resolve exactly as they did.
|
|
471
|
+
|
|
472
|
+
**Nothing is dropped in silence any more.** A field read from the retiring
|
|
473
|
+
top-level key emits a warning naming the line, the counterpart to the existing
|
|
474
|
+
in-block report; the note compiles to the identical document either way. The
|
|
475
|
+
frontmatter lint's `portrait: ""` check reads the `data:` position too, which it
|
|
476
|
+
could not see before.
|
|
477
|
+
- 59469d7: **The dependency check read English prose in a comment as an import** (#355).
|
|
478
|
+
|
|
479
|
+
`tests/dependencies-are-declared.test.ts` finds a shipped file's imports with a
|
|
480
|
+
regex over the raw file text. It already reasoned about one false positive — the
|
|
481
|
+
lookbehind stops `["from", "to"]` reading as an import of `", "` — but not about
|
|
482
|
+
comments, where `from` is an ordinary word and the quotes are ordinary quotes.
|
|
483
|
+
Any explanatory comment containing the word `from`, `import` or `require`
|
|
484
|
+
followed by a quoted phrase was reported as an undeclared dependency:
|
|
485
|
+
|
|
486
|
+
```text
|
|
487
|
+
FAIL sohl/item-fields.mjs imports only builtins, itself, or a declared dependency
|
|
488
|
+
+ [ "sohl/item-fields.mjs:455 → this note does not set the phase" ]
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
The message names a real file and a real line and says a dependency is missing,
|
|
492
|
+
so the first reading is that one genuinely is. Nothing in it suggests the culprit
|
|
493
|
+
is a sentence, and the fix — reword the comment — is unrelated to anything the
|
|
494
|
+
message describes. It cost a debugging cycle in #329, and the workaround left the
|
|
495
|
+
trap armed for whoever wrote the next comment.
|
|
496
|
+
|
|
497
|
+
**Comments are now blanked before the regex runs.** They are located by parsing
|
|
498
|
+
the file, not by a second regex, so `//` inside a string literal is still a string
|
|
499
|
+
literal. Each comment's characters are replaced one-for-one with spaces and its
|
|
500
|
+
newlines are left alone, so every offset survives and a finding still points at
|
|
501
|
+
the line a reader opens. The `sohl/item-fields.mjs` comment that provoked this
|
|
502
|
+
reads naturally again, and the suite carries it verbatim.
|
|
503
|
+
- fec6c80: **The generated item-frontmatter examples no longer author an `id:`** (#314).
|
|
504
|
+
|
|
505
|
+
`content-build docs item-fields` emitted `id: <16-character id>` in the worked
|
|
506
|
+
example for every item type — thirteen of them in the `sohl` tree. Since #270
|
|
507
|
+
and #277 a note's document `_id` derives from its canonical address, and the
|
|
508
|
+
authored field is the escape hatch for keeping a document's identity across a
|
|
509
|
+
shortcode rename, not part of the envelope every note carries.
|
|
510
|
+
|
|
511
|
+
The example is the block an author copies as a template, and the page is the
|
|
512
|
+
per-type reference they read while writing the note, so the one place the field
|
|
513
|
+
survived a tree's sweep was the document teaching them to write it. It is now
|
|
514
|
+
omitted, as every other optional envelope field already was; the `type` and
|
|
515
|
+
`shortcode` the derivation reads are unchanged.
|
|
516
|
+
|
|
517
|
+
Consumers should regenerate their item frontmatter reference to drop the line.
|
|
518
|
+
- a48802f: **A folder note's published `id` is now the id its Foundry documents carry**
|
|
519
|
+
(#310).
|
|
520
|
+
|
|
521
|
+
The content index derived every note's id under the `document` namespace. A
|
|
522
|
+
`Folder` is hashed under the `folder` namespace, so the index published one
|
|
523
|
+
value and the packs addressed another — `sohl-none-folder-cookware` was
|
|
524
|
+
`f5d3dc635b7e799c` in the index and `b92b28b7d06638ed` in every pack.
|
|
525
|
+
|
|
526
|
+
`noteDocId` now asks the folder pass for a folder's id instead of deriving a
|
|
527
|
+
second one, so the two cannot disagree.
|
|
528
|
+
|
|
529
|
+
**Why nothing caught it.** Every one of `sohl`'s 79 folder notes pins an `id`,
|
|
530
|
+
and a pin wins in both paths — so all 65 emitted folder documents agreed by
|
|
531
|
+
coincidence. It is also invisible from inside a build: no pass reads a folder's
|
|
532
|
+
id off the index. The published artifact was the only place the wrong value
|
|
533
|
+
surfaced, and a reader outside the build could neither recompute the right one
|
|
534
|
+
nor notice the wrong one.
|
|
535
|
+
|
|
536
|
+
**The general rule this settles:** for every entry the content index gives an
|
|
537
|
+
identity to, it publishes both the `id` and the `uuid`, each computed once by
|
|
538
|
+
whatever owns that entry's derivation. A documentation journal's record
|
|
539
|
+
accordingly gains its own `id` — it carried the UUID that id ends in, but not
|
|
540
|
+
the id — so a consumer reads it rather than parsing it back out of the UUID's
|
|
541
|
+
last segment.
|
|
542
|
+
|
|
543
|
+
_No emitted document changes; this corrects what the index says about them._
|
|
544
|
+
- ebd3a80: **A note-level frontmatter check no longer answers from a system field that
|
|
545
|
+
merely shares the name** (#312).
|
|
546
|
+
|
|
547
|
+
`topLevelMeans` exists because one spelling can name two unrelated quantities. An
|
|
548
|
+
`affiliation` item's `system.title` is the style of address an office carries —
|
|
549
|
+
"Ajaw", "Warden"; a note's top-level `title` is the heading its page publishes
|
|
550
|
+
under. The field declares the collision, and `resolveFieldValue` honours it by
|
|
551
|
+
refusing to read the top level for that field.
|
|
552
|
+
|
|
553
|
+
The frontmatter lint did not. `authoredValue` resolved every check through the
|
|
554
|
+
`sohl:` block first, so the blank-heading check — whose emitter is `fm.title ??
|
|
555
|
+
name`, the note's top level and nothing else — read `sohl.title` and found the
|
|
556
|
+
office's style of address. An office with no style of address is ordinary, and
|
|
557
|
+
each one was reported as a page published with no heading, sorting to the front
|
|
558
|
+
of its section.
|
|
559
|
+
|
|
560
|
+
**The statement is symmetric, and is now read that way.** If two positions hold
|
|
561
|
+
unrelated quantities then the in-block position is not the note-level field
|
|
562
|
+
either, so a note-level check reads past a block key the note's own type claims
|
|
563
|
+
for something else. The exemption is still the field's own declaration rather
|
|
564
|
+
than a name the linter knows: `collidingBlockKeys` asks the schemas the caller
|
|
565
|
+
supplies, so the linter and the resolver cannot disagree about which field
|
|
566
|
+
declares one.
|
|
567
|
+
|
|
568
|
+
**The art fields are checked the same way.** `img` and `portrait` keep resolving
|
|
569
|
+
through the block, because that is what their emitter does — `blockProperty`
|
|
570
|
+
reads `sohl.img` first, so a `sohl.img: ""` really does ship a document with no
|
|
571
|
+
art and is still reported. What changes is that a future system field of either
|
|
572
|
+
name cannot quietly answer for the note's own art; a map's was `sohl.image`
|
|
573
|
+
until #142.
|
|
574
|
+
|
|
575
|
+
**What a consumer sees.** Twenty-eight fewer warnings on an unswept
|
|
576
|
+
`sohl-kethira-basic` — every affiliation writing `sohl.title: ""`. All were false
|
|
577
|
+
positives; their pages took `name.full` throughout. A tree already swept onto
|
|
578
|
+
`sohl.system.title` was unaffected either way, which is why the findings
|
|
579
|
+
disappearing looked like a lint regression in that sweep rather than the
|
|
580
|
+
false positives going away.
|
|
581
|
+
- f6a8a05: **The specification and four engine docblocks stated the retired folder model**
|
|
582
|
+
(#358).
|
|
583
|
+
|
|
584
|
+
The folder epic replaced a model wholesale — `folder:` named a Foundry id
|
|
585
|
+
resolved against a per-pack `*-folders.yaml`, and `packFolder` named a path.
|
|
586
|
+
None of that exists: `folder:` is refused, the YAML is gone, and `packFolder`
|
|
587
|
+
is a folder note's address. Six passages still described the old shape as the
|
|
588
|
+
live one.
|
|
589
|
+
|
|
590
|
+
**`docs/content-format.md` contradicted itself twice.** The shared-mappings
|
|
591
|
+
table — the one place eight rows common to all sixteen type tables are stated —
|
|
592
|
+
offered `` `packFolder` / `folder` ``, so a reader was told to write a value the
|
|
593
|
+
build rejects, 330 lines before the same document says it is retired. And the
|
|
594
|
+
argument for deriving a document id cited "`packFolder: <path>` above", where
|
|
595
|
+
above says address.
|
|
596
|
+
|
|
597
|
+
**Four docblocks described the retired resolution path**, and they publish:
|
|
598
|
+
|
|
599
|
+
| site | said |
|
|
600
|
+
| -------------------------- | ----------------------------------------------------------------------------------- |
|
|
601
|
+
| `engine/generate.mjs` | folder files "referenced from entry frontmatter via `sohl.folder: <id>`" |
|
|
602
|
+
| `engine/journals.mjs` | the target folder's id "from folders.yaml", resolved against a folders.yaml list |
|
|
603
|
+
| `engine/base-compiler.mjs` | `folderResolver` "resolves a `sohl.folder` id against this pack's folder hierarchy" |
|
|
604
|
+
| `engine/frontmatter.mjs` | `folderField` reads "two spellings", `packFolder` winning "where both are present" |
|
|
605
|
+
|
|
606
|
+
The last two were the sharpest. `folderField` reads `packFolder` and nothing
|
|
607
|
+
else, so its docstring described a resolution the function cannot perform and
|
|
608
|
+
deferred to an issue that had closed. `generate.mjs` disagreed with itself
|
|
609
|
+
across one file: the module header named `sohl.folder`, while its `resolver`
|
|
610
|
+
states the rule correctly — "There is one spelling."
|
|
611
|
+
|
|
612
|
+
**Prose is the defect the epic was about.** Its argument against
|
|
613
|
+
`*-folders.yaml` was that a second, unchecked statement of one fact drifts from
|
|
614
|
+
the first, and nothing compares the two. These six passages were exactly that,
|
|
615
|
+
and nothing caught them: `lint:content-format` makes a claim only for a
|
|
616
|
+
`system.*` target, so a row mapping to core Foundry's `folder` yields none, and
|
|
617
|
+
the source side of a shared row is checked by nothing at all.
|
|
618
|
+
|
|
619
|
+
Two assertions now hold the specification to it — the shared-mappings sources
|
|
620
|
+
name no retired field, and the document never presents `packFolder` as holding
|
|
621
|
+
a path. The docblocks are held to review instead: a sentence describing
|
|
622
|
+
`folder:` as _retired_ is correct and must survive, and no assertion separates
|
|
623
|
+
that from one describing it as live without reading the prose.
|
|
624
|
+
|
|
625
|
+
No behaviour changes; the fix is what the documents say.
|
|
626
|
+
|
|
627
|
+
## 19.0.0
|
|
628
|
+
|
|
629
|
+
### Major Changes
|
|
630
|
+
|
|
631
|
+
- e46a791: **`folder:` and the `*-folders.yaml` schema are retired** (#260) — the last step
|
|
632
|
+
of #254, once every tree is green on the new spelling.
|
|
633
|
+
|
|
634
|
+
They go together. The `folder:` Foundry-id spelling had nothing left to resolve
|
|
635
|
+
against once the YAML was gone, and the YAML had no reader once the spelling was
|
|
636
|
+
refused. What replaces both landed in #255–#258: a folder is a note
|
|
637
|
+
(`type: folder`), `packFolder:` names one by **address**, the packs a folder
|
|
638
|
+
materialises in are derived from what references it, and its Foundry id is
|
|
639
|
+
hashed from its canonical address.
|
|
640
|
+
|
|
641
|
+
**What is gone**
|
|
642
|
+
|
|
643
|
+
| Removed | Replaced by |
|
|
644
|
+
| ------------------------------------------------------------ | ---------------------------------------------------------------------------- |
|
|
645
|
+
| `*-folders.yaml`, five files per tree | `type: folder` notes, resolved through the address index |
|
|
646
|
+
| `loadFolders`, `buildFolderResolver` and its five invariants | `collectFolderNotes` / `buildFolderNoteIndex` |
|
|
647
|
+
| `writeFolderDocs` | `writeFolderNoteDocs`, written after the pass that learns what it references |
|
|
648
|
+
| The `folders:` pack-configuration key | nothing — a pack materialises what its documents reference |
|
|
649
|
+
| `folder:` on a note | `packFolder:` |
|
|
650
|
+
|
|
651
|
+
**Both are refused, not ignored.** A retired field left ignored reads to its
|
|
652
|
+
author as though it still works: the note says one thing and the build does
|
|
653
|
+
another, and nothing says so. A note that writes `folder:` fails the build
|
|
654
|
+
naming `packFolder` and the line to rewrite, and the frontmatter lint reports
|
|
655
|
+
every one of them in the tree at once — which is what a tree still to sweep
|
|
656
|
+
needs. A pack configuration that still names a `folders:` file is refused the
|
|
657
|
+
same way, saying where the folders went rather than merely "no such key".
|
|
658
|
+
|
|
659
|
+
**Presence is the whole test**, in both positions a note wrote it — top-level
|
|
660
|
+
and inside the `sohl:` block. An empty `folder:` parses as `null` and is still
|
|
661
|
+
the field, and a value that happens to match a folder note's id is still the
|
|
662
|
+
retired spelling. There is no value that makes writing it correct, so each
|
|
663
|
+
message says what to write instead rather than which value to change.
|
|
664
|
+
|
|
665
|
+
**Why this is a major.** A tree that has not swept goes red on adoption, by
|
|
666
|
+
design — that is the signal, and it is the reason the removal waited until the
|
|
667
|
+
support and the sweeps had landed. Removing either half earlier would have
|
|
668
|
+
broken a repository mid-migration, which is the failure the `image:` → `img:`
|
|
669
|
+
migration (#142, #149) was staged to avoid: read both, sweep, then remove.
|
|
670
|
+
|
|
671
|
+
_The adventure-configuration path is untouched here — see #259, deferred._
|
|
672
|
+
|
|
673
|
+
### Patch Changes
|
|
674
|
+
|
|
675
|
+
- 829363c: **The container's export is a git repository**, so a workflow step that shells
|
|
676
|
+
out to git behaves as it does on the runner.
|
|
677
|
+
|
|
678
|
+
`git archive HEAD` yields a bare directory with no `.git`. A step that asks git
|
|
679
|
+
something — `git ls-files` in a tracked-artifact check, for instance — then
|
|
680
|
+
fails for want of a repository, which reads as _the check failing_ rather than
|
|
681
|
+
as this harness lacking something the runner has. GitHub's own first step is a
|
|
682
|
+
**checkout**, so the faithful export is one too.
|
|
683
|
+
|
|
684
|
+
Found by `HarnMaster-3-FoundryVTT`, whose `check-no-compiled-packs.mjs` lists
|
|
685
|
+
tracked files that way: it passed on the host and failed in the container, and
|
|
686
|
+
the pre-push hook duly refused a push over a defect that was entirely this
|
|
687
|
+
harness's — exactly the false positive that teaches people to reach for
|
|
688
|
+
`--no-verify`.
|
|
689
|
+
|
|
690
|
+
Initialised and staged rather than committed: `git ls-files` reads the index, so
|
|
691
|
+
staging every extracted file reproduces exactly the set the runner would see,
|
|
692
|
+
and nothing needs a configured identity to commit with. Where `git init` cannot
|
|
693
|
+
run, it says so and carries on rather than pretending.
|
|
694
|
+
|
|
3
695
|
## 18.2.0
|
|
4
696
|
|
|
5
697
|
### Minor Changes
|