@heroiclands/package-build 19.0.0 → 20.2.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 +1100 -0
- package/CONTENT.md +264 -33
- package/README.md +43 -4
- package/bin/content-build.mjs +94 -3
- package/config.mjs +9 -1
- package/content-config.mjs +99 -19
- package/docs/content-format.md +394 -72
- package/e2e.mjs +297 -3
- package/engine/actor-compiler.mjs +197 -7
- package/engine/address-charset.mjs +23 -5
- package/engine/base-compiler.mjs +63 -2
- package/engine/bundles.mjs +9 -0
- package/engine/content-address.mjs +92 -1
- package/engine/content-charset.mjs +434 -0
- package/engine/content-format.mjs +102 -0
- package/engine/content-icons.mjs +388 -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 +24 -1
- package/engine/foreign-catalog.mjs +112 -7
- package/engine/foundry-entries.mjs +14 -0
- package/engine/frontmatter-lint.mjs +377 -56
- package/engine/frontmatter.mjs +11 -11
- package/engine/generate.mjs +72 -12
- package/engine/helpers.mjs +96 -10
- package/engine/index.mjs +9 -0
- package/engine/item-compiler.mjs +37 -0
- package/engine/journals.mjs +21 -4
- package/engine/macros.mjs +8 -0
- package/engine/map-notes.mjs +7 -7
- package/engine/note-claims.mjs +208 -5
- package/engine/note-ids.mjs +25 -1
- package/engine/note-vocabulary.mjs +76 -9
- 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/retired-fields.mjs +57 -16
- package/engine/runtime-only-fields.mjs +204 -0
- package/engine/scenes.mjs +12 -19
- package/engine/schema-check.mjs +23 -1
- package/engine/schema-extract.mjs +13 -0
- 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 +63 -13
- 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/config.d.mts +7 -0
- package/types/e2e.d.mts +130 -3
- 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-charset.d.mts +127 -0
- package/types/engine/content-format.d.mts +9 -0
- package/types/engine/content-icons.d.mts +151 -0
- package/types/engine/field-spec.d.mts +271 -3
- package/types/engine/folder-notes.d.mts +20 -0
- package/types/engine/foreign-catalog.d.mts +38 -2
- package/types/engine/foundry-entries.d.mts +6 -0
- package/types/engine/frontmatter-lint.d.mts +164 -30
- package/types/engine/frontmatter.d.mts +11 -11
- package/types/engine/generate.d.mts +27 -0
- package/types/engine/helpers.d.mts +45 -9
- package/types/engine/index.d.mts +3 -0
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/note-claims.d.mts +67 -0
- package/types/engine/note-ids.d.mts +14 -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/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,1105 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 20.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 85fffd6: **The frontmatter lint checks the system blocks a package ships for, instead of
|
|
8
|
+
a block named `sohl`.**
|
|
9
|
+
|
|
10
|
+
A system block is a closed region: a key the system's vocabulary does not
|
|
11
|
+
declare is an error, because the compiler's builders are an allow-list and drop
|
|
12
|
+
it without a word. That held for exactly one block, `sohl:`, and it held whatever
|
|
13
|
+
system the package shipped for — the linter takes the blocks its caller names,
|
|
14
|
+
and the only caller named none, so every tree fell back to the same constant.
|
|
15
|
+
|
|
16
|
+
Both directions of that are wrong once a second system exists, and the second is
|
|
17
|
+
the costlier:
|
|
18
|
+
|
|
19
|
+
| | before | now |
|
|
20
|
+
| ----------------------------- | --------------------------------------------------------- | ------------------------------------ |
|
|
21
|
+
| a package shipping for `sohl` | `sohl:` checked | unchanged |
|
|
22
|
+
| a package shipping for `hm3` | `sohl:` checked — a block it does not carry | `hm3:` checked |
|
|
23
|
+
| an `hm3:` block | **never read**, every key discarded at compile in silence | checked against HM3's own vocabulary |
|
|
24
|
+
| a tree feeding both | one of two blocks checked | each block against its own system |
|
|
25
|
+
|
|
26
|
+
**Which systems a package ships for is already declared**, so this reads that
|
|
27
|
+
rather than asking for it again — in all three places it is written:
|
|
28
|
+
|
|
29
|
+
- `systems:`, which declares them without requiring one;
|
|
30
|
+
- a **pack's** `system:`, which is the same statement per pack and the only one
|
|
31
|
+
some trees make. It is already authoritative at compile, where a note routed
|
|
32
|
+
to such a pack and carrying no such block fails the build, so a lint blind to
|
|
33
|
+
it would refuse a note for want of a block it never checked;
|
|
34
|
+
- `stats.systemId` where neither is written, which has already absorbed every
|
|
35
|
+
remaining spelling: a system package is its own system, and a module takes
|
|
36
|
+
`requiresSystem`, its lone `systems:` entry, or its lone system relationship.
|
|
37
|
+
|
|
38
|
+
**Each block is held to its own system's vocabulary**, and that has two sources.
|
|
39
|
+
A system's `itemBuilders` registry covers its item types — `skill` is one name
|
|
40
|
+
over two data models, so a key SoHL's `skill` declares is not thereby a key
|
|
41
|
+
HM3's. The note schemas cover the rest, `being` above all, which is an actor type
|
|
42
|
+
sitting in no item registry; they are SoHL's, because that is the vocabulary
|
|
43
|
+
`content-build` is built with.
|
|
44
|
+
|
|
45
|
+
A type neither source names is a type that system says nothing about, and its
|
|
46
|
+
block is left alone on such a note rather than reported wholesale. A package
|
|
47
|
+
naming no system anywhere is system-agnostic on purpose — its packs are core
|
|
48
|
+
document types carrying no system data — so it has no system block, and none is
|
|
49
|
+
invented for it.
|
|
50
|
+
|
|
51
|
+
**A block whose vocabulary nothing states is said out loud.** A package
|
|
52
|
+
declaring a system other than SoHL and no `itemBuilders` registry for it has
|
|
53
|
+
nothing that can say what that block may carry, so the block goes unchecked and
|
|
54
|
+
`content-build lint` reports that once, naming the system and the registry to
|
|
55
|
+
declare. A check that quietly does nothing is indistinguishable from one that
|
|
56
|
+
passed, which is the whole subject here.
|
|
57
|
+
|
|
58
|
+
For `harn-ensemble` — the tree this issue is about, declaring both systems
|
|
59
|
+
through its packs — that means its `sohl:` block is checked exactly as before,
|
|
60
|
+
its 2,512 `being` notes included, and its `hm3:` block waits on
|
|
61
|
+
`itemBuilders: [hm3, sohl]`, which the lint now asks for by name.
|
|
62
|
+
|
|
63
|
+
**Nothing changes for a package shipping for SoHL**, which is every consumer
|
|
64
|
+
today: one system, one registry, and the derivation is the identity on it.
|
|
65
|
+
- e084547: The e2e harness no longer reports a run that never started as green.
|
|
66
|
+
|
|
67
|
+
Observed against a licensed container: a concurrent `npm ci` removed
|
|
68
|
+
`node_modules` out from under a run in progress, Cypress died with
|
|
69
|
+
`Cannot find package '.../cypress/index.js'`, and `package-build e2e run`
|
|
70
|
+
**exited 0**. The concurrency was an operator's mistake; the exit code was not.
|
|
71
|
+
A scripted caller, or anyone reading the tail of a log, would have recorded the
|
|
72
|
+
suite as passing when nothing was executed — and the suite is what moves
|
|
73
|
+
`compatibility.verified`, so an exit code that says green when nothing ran makes
|
|
74
|
+
that evidence unfalsifiable in the one direction that matters.
|
|
75
|
+
|
|
76
|
+
The suite is now bracketed rather than trusted on its exit status:
|
|
77
|
+
|
|
78
|
+
| When | Check | What it catches |
|
|
79
|
+
| ------ | ---------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
80
|
+
| Before | Every executable the command names | The runner is not installed — an error naming it, before a container and a world. |
|
|
81
|
+
| Before | The tool behind a package runner | `npx cypress run` resolves **`cypress`**; `npx` is never missing, so it answers nothing. |
|
|
82
|
+
| After | Those executables again | The runner disappeared mid-run, which is the failure reported above. |
|
|
83
|
+
| After | Results written since the spawn | The suite started and produced nothing. Needs the new `results` key. |
|
|
84
|
+
|
|
85
|
+
**New: `packageBuild.e2e.results`.** One path, or a list of them, relative to
|
|
86
|
+
the repository root, naming where the suite writes its results:
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
e2e:
|
|
90
|
+
suite:
|
|
91
|
+
run: [npx, cypress, run]
|
|
92
|
+
results: [cypress/results]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Existence is not the test — a directory the _previous_ run left behind exists,
|
|
96
|
+
and reading that as evidence would make the check agree with exactly the thing
|
|
97
|
+
it was built to catch. What counts is a file modified since the suite was
|
|
98
|
+
spawned. Declaring nothing keeps the previous contract, in which the exit status
|
|
99
|
+
is taken at its word; declaring a path is what buys the distinction between _the
|
|
100
|
+
suite ran and passed_ and _the suite did not run_.
|
|
101
|
+
|
|
102
|
+
**What a consumer may notice.** A `run`, `fast` or `sweep` whose suite is not
|
|
103
|
+
installed now fails immediately with a diagnostic naming the missing program,
|
|
104
|
+
where before it stood a container up and failed later — or, in the reported
|
|
105
|
+
case, did not fail at all. The check only ever makes a verdict _worse_: a suite
|
|
106
|
+
that failed keeps its own exit status, so there is no new way for the harness to
|
|
107
|
+
report a result that did not happen. `open` is untouched, because a person
|
|
108
|
+
decides what to execute there and a session that ran no specs is not a fault.
|
|
109
|
+
|
|
110
|
+
Also exported from `@heroiclands/package-build/e2e`, for a repository that wants
|
|
111
|
+
the same rules elsewhere: `suiteExecutables`, `findExecutable`,
|
|
112
|
+
`missingExecutables`, `freshResults` and the pure `suiteVerdict`.
|
|
113
|
+
- d72c4b2: A note can now name an interface icon instead of drawing one. `:icon-star:`
|
|
114
|
+
renders as the same Font Awesome element the system's own sheets emit, and an
|
|
115
|
+
undeclared name is reported rather than published as literal text.
|
|
116
|
+
|
|
117
|
+
The user guide described Foundry's interface by pasting Unicode lookalikes of
|
|
118
|
+
icons the sheets actually draw — `☆` for the improve flag, `✎` for the formula
|
|
119
|
+
editor — so the note and the screen it described were drifting apart. Those
|
|
120
|
+
characters are also the worst in the corpus to typeset: of eight candidate book
|
|
121
|
+
faces, none carries them.
|
|
122
|
+
|
|
123
|
+
A registry maps a writer's name to a style and a Font Awesome icon, because the
|
|
124
|
+
three surfaces need different artefacts from one name: the journals and the
|
|
125
|
+
website want an `<i class="fa-solid fa-star">`, and a PDF wants a font file and
|
|
126
|
+
a glyph. It also means an icon renamed between Font Awesome major versions costs
|
|
127
|
+
one line rather than a sweep of the corpus.
|
|
128
|
+
|
|
129
|
+
Codepoints are deliberately absent: a renderer embedding the font has to read it
|
|
130
|
+
to subset it, and the font's own `cmap` is the only trustworthy source for which
|
|
131
|
+
glyph a name resolves to.
|
|
132
|
+
|
|
133
|
+
Part of #378.
|
|
134
|
+
- 15fb41f: **A pack's `system:` must resolve to the version its documents are stamped with,
|
|
135
|
+
and a configuration where it resolves to nothing is now refused.**
|
|
136
|
+
|
|
137
|
+
Every document in a pack carries `_stats.systemId` and `_stats.systemVersion`,
|
|
138
|
+
and for a pack declaring `system:` those come from one of exactly two places:
|
|
139
|
+
the `systems:` entry for that system, which carries the verified version, or the
|
|
140
|
+
package-wide stats, which answer for a package whose packs are all for its own
|
|
141
|
+
system.
|
|
142
|
+
|
|
143
|
+
A pack naming a system that resolves to **neither** used to fall through to the
|
|
144
|
+
package-wide value — and a module that declares no system does not have one, so
|
|
145
|
+
both fields were stamped `null`. That is the plausible lie #43 was about,
|
|
146
|
+
reached by the one path the guard did not cover:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
_stats: { systemId: null, systemVersion: null, … }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
on 2,513 compiled actors in a pack whose configuration says `system: sohl` on
|
|
153
|
+
the line above.
|
|
154
|
+
|
|
155
|
+
**The check existed; it was skipped in exactly this case.** `packs.<n>.system`
|
|
156
|
+
was validated against `systems:` only when that block was non-empty — the guard
|
|
157
|
+
read `declaredSystems.size && …` — so an absent block meant no check at all. Its
|
|
158
|
+
sibling ten lines up refuses the same thing for `requiresSystem` and says "the
|
|
159
|
+
`systems:` block is empty or absent" in as many words, and the comment above
|
|
160
|
+
both already described this failure. The suite was green throughout because its
|
|
161
|
+
`harn-ensemble`-shaped fixture declares the `systems:` block the repository does
|
|
162
|
+
not: the fixture was more complete than the configuration it stood for.
|
|
163
|
+
|
|
164
|
+
**What a consumer sees.** A configuration in this shape now fails with the pack
|
|
165
|
+
named and the entry to add:
|
|
166
|
+
|
|
167
|
+
> `packs.actors-hm3.system` names `hm3`, which `systems:` does not declare — the
|
|
168
|
+
> `systems:` block is empty or absent, and this package has no package-wide
|
|
169
|
+
> system either. Every document in the pack is stamped `_stats.systemId` and
|
|
170
|
+
> `systemVersion` from one of those two, so with neither it would be stamped
|
|
171
|
+
> null. Add `systems:` naming `hm3` with a `compatibility.verified` version.
|
|
172
|
+
|
|
173
|
+
**Nothing changes for a package whose packs name no system**, or whose packs name
|
|
174
|
+
its own system — the package-wide stats answer for those exactly as before,
|
|
175
|
+
which is every single-system tree. The package-wide derivation itself is now a
|
|
176
|
+
named function read by both the stamp and the check, so the value validated
|
|
177
|
+
against and the value stamped cannot come to disagree about the case that has no
|
|
178
|
+
answer.
|
|
179
|
+
- c9e7a5a: **A pack default is resolved per system, so one note compiles into one pack per
|
|
180
|
+
system without declaring anything.**
|
|
181
|
+
|
|
182
|
+
This is the routing half of #58, and until now it made the documented
|
|
183
|
+
two-system layout impossible to build. A default was computed per document
|
|
184
|
+
_type_: a type with exactly one pack is that type's default implicitly, and a
|
|
185
|
+
type with several designates one with `default: true`. A tree shipping one Actor
|
|
186
|
+
pack per system has two, so it had neither — and a note feeding both systems
|
|
187
|
+
declares no `pack:` by design, since a block's `pack:` exists to say where one
|
|
188
|
+
system's document goes only when that _differs_.
|
|
189
|
+
|
|
190
|
+
So every note routed nowhere. On `harn-ensemble` that was all 2,519 of them, the
|
|
191
|
+
build failing on each in turn with a message saying the configuration was wrong
|
|
192
|
+
when it was the question being asked that was.
|
|
193
|
+
|
|
194
|
+
| | before | now |
|
|
195
|
+
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
196
|
+
| one Actor pack per system, no flag | every note routes nowhere; build fails | each system's document routes to its own pack |
|
|
197
|
+
| 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` |
|
|
198
|
+
| `hm3.pack:` naming a SoHL pack | routed there, and the HM3 document was lost without a word | refused, naming the note and the pack |
|
|
199
|
+
| 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 |
|
|
200
|
+
|
|
201
|
+
**A system is never answered with another system's pack.** That is the rule the
|
|
202
|
+
four rows share, and the second is the one worth stating twice: it failed
|
|
203
|
+
silently. The pack compiled zero entries, which a build reports only because a
|
|
204
|
+
pack that compiles nothing from a non-empty tree is itself an error.
|
|
205
|
+
|
|
206
|
+
**Marking a default still means what it says** — it designates that _system's_
|
|
207
|
+
default where a system has several packs of one type — and every single-system
|
|
208
|
+
configuration is untouched, since a pack declaring no system belongs to all of
|
|
209
|
+
them and the type-wide default answers exactly as before.
|
|
210
|
+
|
|
211
|
+
On `harn-ensemble` this takes `actors-sohl` from 0 compiled actors to 2,497, and
|
|
212
|
+
`actors-hm3` from routing nothing to claiming every note and reporting what each
|
|
213
|
+
still needs: `hm3.type`, which `being` requires because it is one-to-many into
|
|
214
|
+
`character` and `creature`.
|
|
215
|
+
- 0418fa8: `content-build lint` now holds a content tree to a character allowlist, so a
|
|
216
|
+
book can choose its typeface without discovering at print time that no font
|
|
217
|
+
carries what the notes are written in.
|
|
218
|
+
|
|
219
|
+
Typst does not warn when a glyph is missing — it falls back to whatever system
|
|
220
|
+
font has one and exits 0, so a rules table can set in three unrelated faces and
|
|
221
|
+
the build still reports success. The check moves that failure back to where the
|
|
222
|
+
character is written.
|
|
223
|
+
|
|
224
|
+
The tiers are measured rather than chosen: eight candidate book faces were
|
|
225
|
+
probed over every non-ASCII character in the five content trees, and what is
|
|
226
|
+
admitted is what enough of them carry. Letters and typography are universal;
|
|
227
|
+
Latin Extended Additional is carried by seven of eight; IPA was considered and
|
|
228
|
+
refused at five of eight, because requiring it would cost font freedom rather
|
|
229
|
+
than buy it.
|
|
230
|
+
|
|
231
|
+
Two rules ride along that an allowlist cannot express. Content must be NFC — a
|
|
232
|
+
decomposed letter is a different string to every byte comparison, including
|
|
233
|
+
DuckDB's `=`, so a filter typed one way silently misses a note stored the other.
|
|
234
|
+
And box-drawing, geometric and arrow characters are permitted inside a fenced
|
|
235
|
+
code block only, where the mono face sets them.
|
|
236
|
+
|
|
237
|
+
Part of #377.
|
|
238
|
+
- 571d5bc: **`content-build format` now says which shared Prettier conventions your
|
|
239
|
+
repository is not using.**
|
|
240
|
+
|
|
241
|
+
A consumer's own Prettier config wins **wholesale** — that is Prettier's own
|
|
242
|
+
behaviour and it is not changing — so the conventions this package publishes held
|
|
243
|
+
by convention alone, and lapsed silently in two opposite directions (#133):
|
|
244
|
+
|
|
245
|
+
| what a repository declares | what it actually formatted to |
|
|
246
|
+
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
247
|
+
| `export { default } from "@heroiclands/package-build/prettier"` | the shared conventions |
|
|
248
|
+
| `{ ...PRETTIER_BASE }`, without the `**/*.md` override | markdown at `tabWidth: 4` — every note reindenting away from the form it was written in |
|
|
249
|
+
| a partial `.prettierrc`, e.g. `{"tabWidth": 2}` | Prettier's defaults for `printWidth`, `trailingComma`, `experimentalTernaries`, … |
|
|
250
|
+
| nothing at all | the shared conventions here, Prettier's own in your editor and in `npx prettier` |
|
|
251
|
+
|
|
252
|
+
Every `format` run now reports each disagreement by name, before the per-file
|
|
253
|
+
report:
|
|
254
|
+
|
|
255
|
+
```text
|
|
256
|
+
prettier.config.mjs: warning: markdown `tabWidth` is 4 here; the shared configuration says 2
|
|
257
|
+
.prettierrc: warning: `printWidth` is not set here, so Prettier's own default applies; the shared configuration says 100
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
A repository with no Prettier config is warned too, with the one line that fixes
|
|
261
|
+
it — that case is the sharper one, because the shared conventions then reach this
|
|
262
|
+
command and nothing else, so a bare `npx prettier --check .` and the lint chain
|
|
263
|
+
take turns rewriting the same lines.
|
|
264
|
+
|
|
265
|
+
**Nothing here fails a build.** Every finding is a `warning`, the exit code is
|
|
266
|
+
untouched, and a deliberate local override keeps working exactly as before — it
|
|
267
|
+
just stops being silent.
|
|
268
|
+
|
|
269
|
+
New export: `checkPrettierConventions(root)` from
|
|
270
|
+
`@heroiclands/package-build/engine/prose-lint`, and the pure comparison behind it,
|
|
271
|
+
`sharedPrettierDivergence(resolved, file)` from
|
|
272
|
+
`@heroiclands/package-build/engine/prose-config`.
|
|
273
|
+
- 9f638fa: **A note whose secondary document has no pack is now a finding, instead of
|
|
274
|
+
losing that document in silence.**
|
|
275
|
+
|
|
276
|
+
A note produces more than one document as a matter of course: an item note an
|
|
277
|
+
Item and the JournalEntry its prose becomes, a map note a Scene and a
|
|
278
|
+
JournalEntry, an actor note an Actor and a JournalEntry since #337. Where the
|
|
279
|
+
configuration declares no pack for one of them, that document was dropped while
|
|
280
|
+
the rest of the note compiled into a pack that does exist. The build succeeded,
|
|
281
|
+
the compendium shipped, and the missing half was discoverable only by noticing
|
|
282
|
+
it was not there.
|
|
283
|
+
|
|
284
|
+
#146 already reports a note **nothing** claims, and could not see this: it asks
|
|
285
|
+
one question of the whole configuration — does any pack claim this type — and a
|
|
286
|
+
note that compiles its Item into an Item pack answers yes.
|
|
287
|
+
|
|
288
|
+
> a note of type "being" compiles into a JournalEntry as well as an Actor, and
|
|
289
|
+
> `packs:` declares no JournalEntry pack — so the JournalEntry is dropped with
|
|
290
|
+
> no error while the rest of the note compiles. Declare a JournalEntry pack in
|
|
291
|
+
> package-build.config.yaml, or accept the loss deliberately by not authoring
|
|
292
|
+
> what it would have carried.
|
|
293
|
+
|
|
294
|
+
The message names the note, the class with no pack, and the class that did
|
|
295
|
+
compile — the last because it is what tells the two findings apart at a glance:
|
|
296
|
+
one is a `type:` to correct, this one a pack to declare.
|
|
297
|
+
|
|
298
|
+
**Asked per note, not per type**, which is the difference between a useful
|
|
299
|
+
finding and a useless one. `Journals` declines a doc-carrying note whose body is
|
|
300
|
+
empty — an item with no prose gets no doc — so whether an item note produces a
|
|
301
|
+
JournalEntry is decided by the note. `sohl-kethira-basic` declares no
|
|
302
|
+
JournalEntry pack and ships 393 notes whose descriptions are _deliberately_
|
|
303
|
+
empty under the Fan Material Guidelines its configuration explains at length; a
|
|
304
|
+
type-level answer would report every one of them for losing a document none of
|
|
305
|
+
them produces. It reports none.
|
|
306
|
+
|
|
307
|
+
**It names no system**, so a type one system maps and another does not stays
|
|
308
|
+
silent for the system that declines it, per #79. That holds by construction: the
|
|
309
|
+
`Item` and `Actor` rows fold the systems' maps together before this sees them,
|
|
310
|
+
so a type appears once or not at all and no system is ever named.
|
|
311
|
+
|
|
312
|
+
**What it finds today.** `Song-of-Heroic-Lands-FoundryVTT` and `sohl-thalorna`
|
|
313
|
+
report nothing — every document their notes produce already has a pack.
|
|
314
|
+
`harn-ensemble` reports 2,512: it declares two Actor packs and no JournalEntry
|
|
315
|
+
pack, so every one of its beings has been losing the `{#appearance}` and
|
|
316
|
+
`{#dossier}` prose it carries. When the issue was filed no tree authored the
|
|
317
|
+
affected combination; one does now.
|
|
318
|
+
|
|
319
|
+
### Patch Changes
|
|
320
|
+
|
|
321
|
+
- 6499109: **A build now reads the configuration of the tree it was run in.**
|
|
322
|
+
|
|
323
|
+
The configuration was located by walking up from the installed package's own
|
|
324
|
+
directory. That is the same file as the working directory's in every ordinary
|
|
325
|
+
install — and a different one in a git worktree nested under its parent checkout
|
|
326
|
+
with no `node_modules` of its own. Node's resolution walks parent directories,
|
|
327
|
+
so such a worktree resolves `@heroiclands/package-build` out of the _parent's_
|
|
328
|
+
`node_modules`; the walk started inside the parent and landed on the parent's
|
|
329
|
+
`package-build.config.yaml`. The build then compiled the parent's content tree
|
|
330
|
+
into the parent's `build/`, said so only in absolute paths that are easy to read
|
|
331
|
+
past, and exited 0.
|
|
332
|
+
|
|
333
|
+
Resolution now starts at `process.cwd()` and falls back to the installed
|
|
334
|
+
package's directory only when that finds nothing.
|
|
335
|
+
|
|
336
|
+
| running `content-build package compile` in | before | after |
|
|
337
|
+
| -------------------------------------------- | ----------------------- | ---------------- |
|
|
338
|
+
| a repository, or any directory below it | that repository | unchanged |
|
|
339
|
+
| a nested worktree that has had `npm ci` run | the worktree | unchanged |
|
|
340
|
+
| a nested worktree with **no `node_modules`** | _the parent checkout_ | **the worktree** |
|
|
341
|
+
| a directory outside any repository | the installed package's | unchanged |
|
|
342
|
+
| anywhere, with `PACKAGE_BUILD_CONFIG` set | the file it names | unchanged |
|
|
343
|
+
|
|
344
|
+
Nothing about "a build reads one tree however it was launched" changes: the walk
|
|
345
|
+
climbs, so every directory inside a repository still resolves that repository's
|
|
346
|
+
single configuration.
|
|
347
|
+
|
|
348
|
+
When both walks find a configuration and they disagree, the working directory's
|
|
349
|
+
is read and the ignored one is named in a warning on stderr. The disagreement is
|
|
350
|
+
worth hearing on its own — it is the cheapest signal that this tree is building
|
|
351
|
+
on another checkout's `node_modules`, which is also a masked missing dependency.
|
|
352
|
+
`npm ci` in the worktree silences it properly.
|
|
353
|
+
|
|
354
|
+
**Why this was worth a fix rather than a note.** A silent wrong-tree build does
|
|
355
|
+
not merely fail to prove what was wanted, it produces confident evidence for the
|
|
356
|
+
wrong tree — and on an output-preserving sweep there is no observation that
|
|
357
|
+
distinguishes success from it. The usual tell is a zero diff where a change was
|
|
358
|
+
expected; a sweep that expects zero differences has no tell at all.
|
|
359
|
+
|
|
360
|
+
`resolveConfigFile()` is exported from
|
|
361
|
+
`@heroiclands/package-build/engine/pack-config`, reporting the chosen file and
|
|
362
|
+
each walk's own answer, so a caller can ask which tree it is about to compile
|
|
363
|
+
without re-deriving the resolution and risking disagreement with the loader.
|
|
364
|
+
- c229f2b: Stop shipping a `node_modules` symlink, which had broken every release for a
|
|
365
|
+
day.
|
|
366
|
+
|
|
367
|
+
A worktree's `node_modules` symlink — a 120000 blob holding one developer's
|
|
368
|
+
absolute path — was committed on 2026-09-11. `.gitignore` said
|
|
369
|
+
`/node_modules/`, and a trailing slash matches a directory rather than a
|
|
370
|
+
symlink, so nothing refused it.
|
|
371
|
+
|
|
372
|
+
The release job installs, runs the tests, and then hands over to the changesets
|
|
373
|
+
action, which does `git reset --hard` before versioning. That reset restored the
|
|
374
|
+
symlink over the top of the install, pointing at a path no runner has, so every
|
|
375
|
+
module became unresolvable and the release died on `changeset: not found`. The
|
|
376
|
+
tests had already passed, because they run before the reset.
|
|
377
|
+
|
|
378
|
+
The symlink is untracked, the ignore rule now matches a symlink at any depth,
|
|
379
|
+
and CI refuses a tracked `node_modules` path outright — the release is the only
|
|
380
|
+
thing this breaks, and no pull request check would otherwise notice.
|
|
381
|
+
- 80f40b2: Unblock releasing. Every run of the release workflow had failed at
|
|
382
|
+
`changeset version` with `sh: 1: changeset: not found` since 2026-09-11, so
|
|
383
|
+
nothing reached the registry past 20.0.0 while `main` went on believing itself
|
|
384
|
+
released.
|
|
385
|
+
|
|
386
|
+
The workflow installed `npm@latest` before publishing, to clear an OIDC floor of
|
|
387
|
+
11.5. That was written when Node 24.0–24.4 bundled npm 11.3–11.4; since 24.5 the
|
|
388
|
+
bundled npm has cleared the floor on its own, and the step became a no-op that
|
|
389
|
+
nobody removed. On 2026-09-11 `latest` became npm 12, which stopped putting
|
|
390
|
+
`node_modules/.bin` on the PATH of a run-script's shell, and a release path
|
|
391
|
+
nobody had touched broke.
|
|
392
|
+
|
|
393
|
+
The install is gone rather than pinned: the npm that publishes is now the one
|
|
394
|
+
Node brings, so its version follows `node-version` instead of a number kept in
|
|
395
|
+
step by hand. A check in its place asserts the floor and fails loudly if a
|
|
396
|
+
future Node pin ever drops below it — an assertion cannot go quietly stale the
|
|
397
|
+
way the comment it replaces did. The version script also resolves its binary
|
|
398
|
+
through `npx`, as the publish script already did.
|
|
399
|
+
- 12061da: Release again. Since 2026-09-11 every run of the release workflow had died at
|
|
400
|
+
`changeset version` with `sh: 1: changeset: not found`, so nothing reached the
|
|
401
|
+
registry past 20.0.0.
|
|
402
|
+
|
|
403
|
+
The workflow set `version-script`, which replaces the action's own invocation of
|
|
404
|
+
the changesets CLI with a shell command run through its exec — and under that
|
|
405
|
+
exec a bare `changeset` does not resolve on a runner. Left unset, the action
|
|
406
|
+
resolves the installed package with `require.resolve` and runs it with `node`,
|
|
407
|
+
depending on no PATH at all. The override is removed.
|
|
408
|
+
|
|
409
|
+
The fault was never in this repository's install. A diagnostic run confirmed
|
|
410
|
+
that after `npm ci` a runner has the package, has the bin linked, puts
|
|
411
|
+
`node_modules/.bin` first on a run-script's PATH, and resolves the bare name
|
|
412
|
+
through `npm run` — all in the same job that then failed.
|
|
413
|
+
|
|
414
|
+
The one thing the override bought, refreshing `package-lock.json`'s root
|
|
415
|
+
`version`, is now #385 rather than a reason to keep a step that does not run.
|
|
416
|
+
- 9572517: Finish unblocking the release. Removing the stale `npm install -g npm@latest`
|
|
417
|
+
fixed the npm-12 half, but the same change also swapped the version script's
|
|
418
|
+
bare `changeset` for `npx changeset`, and that turned the failure into `npm
|
|
419
|
+
error could not determine executable to run`.
|
|
420
|
+
|
|
421
|
+
The npx form was belt-and-braces and it was wrong. `npm run` already puts
|
|
422
|
+
`node_modules/.bin` on the PATH — the same mechanism `npm test` uses to reach
|
|
423
|
+
`vitest` earlier in the same job — so the bare name resolves the pinned local
|
|
424
|
+
copy with no lookup. npx instead consults the registry, which this job
|
|
425
|
+
configures for OIDC publishing rather than for reads.
|
|
426
|
+
|
|
427
|
+
The script is back to the bare binary, and the reasoning is recorded beside it
|
|
428
|
+
so the asymmetry with `publish-script` is not mistaken for an oversight again.
|
|
429
|
+
|
|
430
|
+
## 20.1.0
|
|
431
|
+
|
|
432
|
+
### Minor Changes
|
|
433
|
+
|
|
434
|
+
- f6d2a8f: **A fetched item catalogue is read one system at a time** (#58).
|
|
435
|
+
|
|
436
|
+
A pack declaring `system: hm3` already read only this repository's `hm3` and
|
|
437
|
+
system-neutral Item packs. The other half of the same lookup — the catalogue
|
|
438
|
+
fetched from a dependency that declares `itemCatalog: true` — was unscoped, and
|
|
439
|
+
both halves are merged into one address space keyed by `subType:shortcode`. So
|
|
440
|
+
an address that exists in both vocabularies resolved against whichever document
|
|
441
|
+
the dependency's other system happened to supply, and said nothing: `skill:awar`
|
|
442
|
+
is a real address under SoHL and under HM3 and means two different documents.
|
|
443
|
+
|
|
444
|
+
`deps fetch` now records what each extracted pack is, from the dependency's own
|
|
445
|
+
manifest, and `foreignItemCatalogDirs(config, system)` reads only the packs that
|
|
446
|
+
system may see plus the ones declaring no system at all.
|
|
447
|
+
|
|
448
|
+
**What a consumer sees**
|
|
449
|
+
|
|
450
|
+
| | Before | After |
|
|
451
|
+
| ------------------------------------ | ----------------------- | -------------------------------- |
|
|
452
|
+
| a pack with `system: hm3` | reads every cached pack | reads the `hm3` and neutral ones |
|
|
453
|
+
| a single-system build | reads every cached pack | unchanged |
|
|
454
|
+
| a cache filled by an earlier version | used as-is | treated as incomplete |
|
|
455
|
+
|
|
456
|
+
**Refill the cache once.** A cache written before this holds the items but not
|
|
457
|
+
what they are, and neither way of proceeding without that is honest: reading
|
|
458
|
+
every pack is the wrong-document failure above, and reading none fails a build
|
|
459
|
+
that worked. So it is incomplete, and `content-build deps fetch` refills it —
|
|
460
|
+
the command the cold-cache error already names.
|
|
461
|
+
|
|
462
|
+
### Patch Changes
|
|
463
|
+
|
|
464
|
+
- 2b1157e: **The license header is on every shipped module, and CI refuses a `TODO`.**
|
|
465
|
+
|
|
466
|
+
`engine/foreign-catalog.mjs` and `engine/schema-extract.mjs` shipped without the
|
|
467
|
+
GPL-3.0 header every other module carries — 109 of 111 had one, which is the
|
|
468
|
+
state a rule reaches when nothing checks it.
|
|
469
|
+
|
|
470
|
+
The forbidden-marker check now runs here too, through the org-wide
|
|
471
|
+
`HeroicLands/.github/actions/todos` action the other repositories already call.
|
|
472
|
+
It scans the whole checkout rather than a named list of directories: this
|
|
473
|
+
package's modules sit at its root as well as under `bin/`, `ci/`, `engine/`,
|
|
474
|
+
`hm3/` and `sohl/`, so a list would name sixteen root files today and quietly
|
|
475
|
+
stop covering the seventeenth.
|
|
476
|
+
|
|
477
|
+
Nothing a consumer imports changes.
|
|
478
|
+
|
|
479
|
+
## 20.0.0
|
|
480
|
+
|
|
481
|
+
### Major Changes
|
|
482
|
+
|
|
483
|
+
- 1670506: **An omitted address segment now defaults from where the link is written** (#336),
|
|
484
|
+
instead of being wildcarded or searched for. Three resolvers each carried their
|
|
485
|
+
own reading of a partial address and disagreed; there is one rule now, in
|
|
486
|
+
`expandAddress` beside `canonicalKey`.
|
|
487
|
+
|
|
488
|
+
- **package** omitted → the current package.
|
|
489
|
+
- **system** omitted → the **system block the link sits under**: anywhere under
|
|
490
|
+
`sohl:` is `sohl`, anywhere under `hm3:` is `hm3`, and anywhere else —
|
|
491
|
+
top-level frontmatter, `data:`, body prose — is `none`. The enclosing block
|
|
492
|
+
decides at any depth; the field has no say.
|
|
493
|
+
|
|
494
|
+
Every short form therefore expands to exactly one canonical address before
|
|
495
|
+
lookup. Resolution is a `Map.get`, with no candidate set and no single-hit rule.
|
|
496
|
+
|
|
497
|
+
**Under `none`, a system-bearing type addresses its documentation.** A note's
|
|
498
|
+
`none` address _is_ its `doc<type>` journal, so a prose `[[affiliation-x|…]]`
|
|
499
|
+
names the page — what a prose link almost always means. A prose link that means
|
|
500
|
+
the Item states the system: `[[sohl-affiliation-x|…]]`. A `macro` and the map
|
|
501
|
+
types are **not** redirected: their own documents are core ones already at
|
|
502
|
+
`none`, so `macro-x` still names the Macro.
|
|
503
|
+
|
|
504
|
+
**Breaking, in three ways a consumer will notice**
|
|
505
|
+
|
|
506
|
+
| | before | after |
|
|
507
|
+
| ----------------------------------- | -------------------------------------------- | ------------------------------------------------- |
|
|
508
|
+
| a bare prose link to an item type | the Item's UUID | the documentation journal's |
|
|
509
|
+
| a bare address naming no local note | fell through to any dependency publishing it | `unresolved`; qualify it to reach another package |
|
|
510
|
+
| `[[Skill-Climb\|…]]` | resolved, case folded | `not-lowercase` |
|
|
511
|
+
|
|
512
|
+
The first rewrites every such link in every pack. The second is the point of the
|
|
513
|
+
issue: a link resolved into another package only because no local note claimed
|
|
514
|
+
the address, and would have retargeted silently the day one did.
|
|
515
|
+
|
|
516
|
+
**Two defects go with it.** The index's system-blind `type/shortcode` key is
|
|
517
|
+
gone — it was set with a plain `Map.set`, so two notes in one package sharing a
|
|
518
|
+
`(type, shortcode)` across systems silently overwrote each other while both
|
|
519
|
+
canonical keys sat correctly beside it. And cross-package `ambiguous` is now
|
|
520
|
+
unreachable: one expanded address names one package, so a lookup returns one
|
|
521
|
+
entry or none. The finding is retained for older vendored manifests.
|
|
522
|
+
|
|
523
|
+
**An address capitalises nothing but its shortcode.** Package, system and type
|
|
524
|
+
are closed vocabularies with one spelling each; a shortcode is case-sensitive and
|
|
525
|
+
routinely mixed (`Clb`, `LtShoe`), so it keeps its case. Neither tree carried a
|
|
526
|
+
violation — 10,538 authored targets checked. Requiring the shortcode to be
|
|
527
|
+
lowercase too is tracked as #340.
|
|
528
|
+
- c906d54: **A shortcode must now match `^[a-z0-9]+$`** — lowercase letters and digits only
|
|
529
|
+
(#340). So must every other address segment; the charset is one rule with no
|
|
530
|
+
exceptions left in it.
|
|
531
|
+
|
|
532
|
+
`Dgr` beside `dgr` is a distinction nobody can say out loud and can only see by
|
|
533
|
+
looking twice. It was also a silent identity collapse: `canonicalKey` lowercases
|
|
534
|
+
the address it builds, so a note declaring `Clb` published
|
|
535
|
+
`sohl-sohl-weapongear-clb` and derived its `_id` from that — and two notes
|
|
536
|
+
differing only in case shared one address, one `_id` and one URL with nothing to
|
|
537
|
+
report it, because the shortcode check compared shortcodes (genuinely distinct)
|
|
538
|
+
and the address check saw one address.
|
|
539
|
+
|
|
540
|
+
It forced two exceptions elsewhere, and both go: #336 had to exempt the shortcode
|
|
541
|
+
from the lowercase rule it pinned on every other segment, and #346 had to fold
|
|
542
|
+
the shortcode's case in the item catalogue because an address is lowercased when
|
|
543
|
+
it is read.
|
|
544
|
+
|
|
545
|
+
**A consumer with a capital in a shortcode will fail to build**, citing the
|
|
546
|
+
note's file, line and column. The sweep is mechanical: every violation in every
|
|
547
|
+
tree is a capital letter — no underscores, hyphens or other characters occur —
|
|
548
|
+
and **nothing collides when folded**, checked per `(type, shortcode)` in every
|
|
549
|
+
tree.
|
|
550
|
+
|
|
551
|
+
| tree | shortcodes to change |
|
|
552
|
+
| --------------------------------- | --------------------: |
|
|
553
|
+
| `Song-of-Heroic-Lands-FoundryVTT` | 438 |
|
|
554
|
+
| `sohl-thalorna` | 96 |
|
|
555
|
+
| every other tree | 0 — already compliant |
|
|
556
|
+
|
|
557
|
+
**No document changes identity and no URL moves**, because an address and a
|
|
558
|
+
document `_id` already derive from the lowercased form. References need no edit
|
|
559
|
+
either: no authored wikilink target carries a capital (10,538 checked), and
|
|
560
|
+
`model:` addresses are already lowercase.
|
|
561
|
+
|
|
562
|
+
**The emitted packs do change, in three narrow ways**, measured on `sohl`'s 438:
|
|
563
|
+
|
|
564
|
+
| change | count |
|
|
565
|
+
| ----------------------------------------------------- | ----: |
|
|
566
|
+
| `system.shortcode` on an Item document, `Clb` → `clb` | 438 |
|
|
567
|
+
| embedded item `_id` / `_key` on one being | 6 |
|
|
568
|
+
| journal pages showing a shortcode in a content table | 5 |
|
|
569
|
+
| **top-level document `_id`** | **0** |
|
|
570
|
+
| **addresses and published URLs** | **0** |
|
|
571
|
+
|
|
572
|
+
The first is the point: the emitted field now matches the address built from it.
|
|
573
|
+
The six embedded ids move because an embedded id derives from the item's own
|
|
574
|
+
`system.shortcode`, which #346 deliberately does **not** case-fold — folding
|
|
575
|
+
there would re-identify documents rather than look them up.
|
|
576
|
+
- 4890864: **A map note's `image:` is no longer read** (#149). Its art is `img:`, as every
|
|
577
|
+
other note type's is.
|
|
578
|
+
|
|
579
|
+
This is the third and last step of the rename #142 began. Through the retirement
|
|
580
|
+
window both spellings were read, `img` won where a note carried both, and a note
|
|
581
|
+
still writing `image` got a located warning — it compiled to the byte-identical
|
|
582
|
+
document, so failing a build over it would have redded a tree that had done
|
|
583
|
+
nothing wrong. The trees have since been swept, so the alias has nothing left to
|
|
584
|
+
honour and is gone.
|
|
585
|
+
|
|
586
|
+
**No shipped tree is affected.** Every content tree was checked — `sohl`,
|
|
587
|
+
`sohl-thalorna`, `sohl-kethira-basic`, `harn-ensemble` and `harn-adventures` —
|
|
588
|
+
and none writes the retired spelling. The window did its job; this only closes
|
|
589
|
+
it.
|
|
590
|
+
|
|
591
|
+
**What an unswept note now sees.** Two errors rather than one warning, and it
|
|
592
|
+
stops compiling: `image` in a `sohl:` block is reported as a key the type does
|
|
593
|
+
not have, and the `img` the note therefore never declared is reported as
|
|
594
|
+
missing. The fix is the rename, and moving the key to the note's top level while
|
|
595
|
+
you are there — art is not system-specific, so it belongs beside every other
|
|
596
|
+
note's `img` rather than inside a system block.
|
|
597
|
+
|
|
598
|
+
**A tile's `image:` is untouched.** `sohl.tiles.<key>.image` is a nested
|
|
599
|
+
placeable's texture, not the note's own artwork, and was never the retired field:
|
|
600
|
+
the check reads the `sohl:` block's own keys and never descends into one.
|
|
601
|
+
|
|
602
|
+
**Nothing was added to refuse it.** The two findings above are the ordinary
|
|
603
|
+
unknown-key and required-field checks, which is the point of a rename's third
|
|
604
|
+
step — one that had to add a standing refusal would be one whose replacement
|
|
605
|
+
never arrived. No tombstone entry is kept: the absence of an alias is the record.
|
|
606
|
+
- a1c21e8: **A being's items entry names the item it copies with `model:`, an address**
|
|
607
|
+
(#334). The top-level `shortcode:` it replaces is retired.
|
|
608
|
+
|
|
609
|
+
```yaml
|
|
610
|
+
sohl:
|
|
611
|
+
items:
|
|
612
|
+
- { model: skill-wpnc, system: { masteryLevelBase: 52 } } # this package
|
|
613
|
+
- { model: sohl-sohl-weapongear-dgr } # another one
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
The old key was doing two jobs badly. It **selected a template**, while the
|
|
617
|
+
`system.shortcode` beside it **was** the compiled item's identity — one word for
|
|
618
|
+
two things, which the compiler's own error messages had to keep explaining. And
|
|
619
|
+
it could not say **which package** the template came from: `loadItemsMap`
|
|
620
|
+
flattened every local Item pack and every dependency catalogue into one
|
|
621
|
+
`subType:shortcode` space where a local definition silently shadowed a foreign
|
|
622
|
+
one. In `sohl-thalorna`, 25,485 of 26,251 model references reach into `sohl` and
|
|
623
|
+
none of them said so; the day that repository ships its own `weapongear-dgr`,
|
|
624
|
+
every entry citing `dgr` would have retargeted with a green build and no
|
|
625
|
+
diagnostic.
|
|
626
|
+
|
|
627
|
+
**What changes for an author**
|
|
628
|
+
|
|
629
|
+
| | before | after |
|
|
630
|
+
| ------------------------ | ---------------------------------- | ---------------------------------------------- |
|
|
631
|
+
| naming a template | `{ shortcode: wpnc, type: skill }` | `{ model: skill-wpnc }` |
|
|
632
|
+
| reaching another package | impossible | `{ model: sohl-sohl-skill-wpnc }` |
|
|
633
|
+
| `type:` beside it | required | refused — the address names the type |
|
|
634
|
+
| a custom item | `name` + `type` + `system` | unchanged, and `system.shortcode` now required |
|
|
635
|
+
|
|
636
|
+
A `model` is read by the same grammar every wikilink is (#336), so it is written
|
|
637
|
+
at whatever length says what it means and the system segment defaults from the
|
|
638
|
+
block the entry sits in — which is why the short form names an **Item** here
|
|
639
|
+
while the same string in body prose names a page.
|
|
640
|
+
|
|
641
|
+
**The catalogue is package-aware.** Every item is keyed under its own package as
|
|
642
|
+
well as unqualified, so a `model` that states a package resolves to that
|
|
643
|
+
package's item and nothing local can shadow it, while a `model` that states none
|
|
644
|
+
still resolves locally-first exactly as before. `foreignItemCatalogDirs` returns
|
|
645
|
+
`{ dir, package }` rather than a bare path.
|
|
646
|
+
|
|
647
|
+
**Consumer sweeps**: HeroicLands/Song-of-Heroic-Lands-FoundryVTT#1875 (95 beings,
|
|
648
|
+
1,557 entries) and HeroicLands/sohl-thalorna#176 (645 beings, 26,251 entries plus
|
|
649
|
+
938 shortcodes derived for custom gear).
|
|
650
|
+
|
|
651
|
+
### Minor Changes
|
|
652
|
+
|
|
653
|
+
- ccf35a1: **An actor note now publishes documentation, like every other note that compiles
|
|
654
|
+
into a system-bearing document** (#337).
|
|
655
|
+
|
|
656
|
+
A being used to produce its Actor and nothing else. That left it the one
|
|
657
|
+
system-bearing note with no address at `none` — its only address named the
|
|
658
|
+
Actor — so a prose link written `[[being-<shortcode>|Text]]` had no page to land
|
|
659
|
+
on. `sohl-thalorna` alone carries 772 such links.
|
|
660
|
+
|
|
661
|
+
`docEntryTypes` is now `itemTypes` plus the actor types the shipped subtype maps
|
|
662
|
+
declare (derived from those maps, not listed again), plus `macro` and the map
|
|
663
|
+
types. Only `doc` stays outside it, for the reason that actually applies to it:
|
|
664
|
+
its single document _is_ the prose.
|
|
665
|
+
|
|
666
|
+
**What a consumer sees**
|
|
667
|
+
|
|
668
|
+
| Before | After |
|
|
669
|
+
| ------------------------------------- | -------------------------------------------------------------------- |
|
|
670
|
+
| `<pkg>-<system>-being-<shortcode>` | that, **and** `<pkg>-none-docbeing-<shortcode>` |
|
|
671
|
+
| a being's prose reachable only inline | also a JournalEntry, and a page the site publishes |
|
|
672
|
+
| `[[docbeing-x\|Text]]` unresolved | resolves, in the link checker, the pack compilers and the site build |
|
|
673
|
+
|
|
674
|
+
Packs gain one JournalEntry per being **carrying prose**; a being with an empty
|
|
675
|
+
body compiles no entry, exactly as an item with an empty body does. No existing
|
|
676
|
+
document changes.
|
|
677
|
+
|
|
678
|
+
**A being keeps its prose inline as well**, and the asymmetry with items is
|
|
679
|
+
deliberate. `system.appearance` and `system.dossier` stay as rendered text, where
|
|
680
|
+
an item's description is an `@UUID` pointer into its journal. One item is
|
|
681
|
+
embedded across hundreds of beings, so baking its description into every copy
|
|
682
|
+
bloats the compendium by the length of the prose times the number of carriers,
|
|
683
|
+
and the pointer buys that back. An actor is singular, so the same indirection
|
|
684
|
+
would cost a reader a click and save nothing.
|
|
685
|
+
- afb3650: **An asset path's first segment says which package owns it, and the rule is now
|
|
686
|
+
stated and tested** (#331).
|
|
687
|
+
|
|
688
|
+
`img:` and `portrait:` have always answered "which package holds this file?" in
|
|
689
|
+
their first segment, but nothing wrote the rule down and nothing asserted it —
|
|
690
|
+
the only way to confirm it was to compile a package and read `build/packs-json`.
|
|
691
|
+
|
|
692
|
+
| Authored path starts with | Owner | Emitted |
|
|
693
|
+
| ------------------------- | --------------------- | -------------------- |
|
|
694
|
+
| `systems/` | a separate **system** | unchanged |
|
|
695
|
+
| `modules/` | a separate **module** | unchanged |
|
|
696
|
+
| anything else | **this package** | `<assetRoot>/<path>` |
|
|
697
|
+
|
|
698
|
+
**The third row is now true.** The translator prefixed `icons/…` and `images/…`
|
|
699
|
+
and passed everything else through — the same answer for every path any tree
|
|
700
|
+
authors today, and the wrong one for the next directory a package ships.
|
|
701
|
+
`sohl-kethira-basic` keeps art under `assets/artwork/`, so an authored
|
|
702
|
+
`artwork/deity.webp` would have shipped unprefixed: a 404 in Foundry, reported
|
|
703
|
+
by nothing. Ownership is the rule; the directory names inside a package's
|
|
704
|
+
`assets/` tree are that package's business.
|
|
705
|
+
|
|
706
|
+
An address naming no package — an absolute URL, a `data:` URI, a `/`-rooted
|
|
707
|
+
path — passes through, on the same rule rather than as an exception. `worlds/`
|
|
708
|
+
is deliberately not exempt: a package may not ship art out of a world.
|
|
709
|
+
|
|
710
|
+
**No compiled document changes.** Compiling `sohl-thalorna` and this system's
|
|
711
|
+
own tree before and after gives byte-identical `packs-json`; every path either
|
|
712
|
+
tree authors is `icons/`, `images/` or already `systems/`-rooted.
|
|
713
|
+
|
|
714
|
+
**`banner:` is a path that does not follow this rule, and is documented as
|
|
715
|
+
deliberate rather than reconciled.** It reaches no compiled document: it is a
|
|
716
|
+
top-level key the Hugo theme reads, and the theme prefixes a relative value with
|
|
717
|
+
`images/` and joins it onto `params.cdnBaseURL`. The two address different
|
|
718
|
+
places — `img:` a file Foundry serves, `banner:` a file the CDN serves.
|
|
719
|
+
- e3d300c: **An `img:` authored on a type that emits none is now reported instead of
|
|
720
|
+
dropped in silence** (#349).
|
|
721
|
+
|
|
722
|
+
`img` is a shared top-level field — it maps onto `document.img`, so it is legal
|
|
723
|
+
on every note whatever the type — but not every document has one. `doc`, `place`,
|
|
724
|
+
`lore` and `scenario` compile into a JournalEntry, `folder` into a Foundry
|
|
725
|
+
`Folder`, and neither carries artwork; a `homepage` compiles into no compendium
|
|
726
|
+
document at all. On any of those the authored path went nowhere, the note
|
|
727
|
+
validated, the tree compiled clean, and nothing said so.
|
|
728
|
+
|
|
729
|
+
The frontmatter lint now warns, naming the note, the key and what the type
|
|
730
|
+
compiles into. `portrait:` is checked the same way, so an item authoring one is
|
|
731
|
+
told it has nowhere to put a sheet portrait.
|
|
732
|
+
|
|
733
|
+
**What it does not report.** `img: null` — that is the blessed way to say "this
|
|
734
|
+
note names no art", and on a type with no art it is a true and harmless thing to
|
|
735
|
+
say. A warning rather than an error, too: the note still compiles correctly, and
|
|
736
|
+
a note's top level is the generated page's front matter as well, so a site
|
|
737
|
+
template may read there what no document carries.
|
|
738
|
+
|
|
739
|
+
**Which types those are is derived, not listed.** Each pass declares the art it
|
|
740
|
+
writes (`BasePackCompiler.emitsArt`), and `emittedArtFor` walks type → document →
|
|
741
|
+
pass to answer. A second table of iconless types would be free to drift from what
|
|
742
|
+
is actually emitted, which is the defect rather than the check.
|
|
743
|
+
|
|
744
|
+
**What a consumer sees.** New advisory findings on notes already in this state;
|
|
745
|
+
warnings do not fail a build. `sohl-thalorna` has 57 — the `Lore/Totems/` and
|
|
746
|
+
`Lore/Deities/Kemetian/` clusters, which carried an `img:` from when they
|
|
747
|
+
compiled to affiliation items. The other shipped trees are clean.
|
|
748
|
+
- bd5e6cb: **`lore` declares `gathering`, a genre for a scheduled public occasion** (#333).
|
|
749
|
+
|
|
750
|
+
A tournament or martial games, a great market or fair, a religious festival, a
|
|
751
|
+
ceremony or rite: something that happens at a place and a time, on a cycle, and
|
|
752
|
+
that people travel to. The genre had no value, and neither neighbour fitted.
|
|
753
|
+
|
|
754
|
+
| genre | what it covers |
|
|
755
|
+
| ----------- | ---------------------------------------------------------------- |
|
|
756
|
+
| `calendar` | the _reckoning_ — the cycle, the seasons, the dating system |
|
|
757
|
+
| `culture` | a social grouping of people |
|
|
758
|
+
| `gathering` | the occasion itself — who attends, what is contested or observed |
|
|
759
|
+
|
|
760
|
+
A festival's **date** is `calendar`; the festival is not. A tournament is not a
|
|
761
|
+
matter of time-reckoning at all, and a great market is not a grouping of people.
|
|
762
|
+
|
|
763
|
+
**Why it matters beyond labelling.** `site.sections` narrows a section with
|
|
764
|
+
`listSubType`, so a subType is what makes a genre browsable. Without one, a
|
|
765
|
+
consumer declaring a Gatherings section would sweep in the castes, Marriage and
|
|
766
|
+
Personal Names alongside the games — the notes could not be listed as what they
|
|
767
|
+
are.
|
|
768
|
+
|
|
769
|
+
**On the name.** `festival` is too narrow: a tournament is not a festival, a
|
|
770
|
+
great market is a fair, and a rite is not a celebration. `event` is avoided
|
|
771
|
+
because it already names something else in SoHL — the event queue and
|
|
772
|
+
`system.scheduledActions`, where an event is a timed thing that fires in play.
|
|
773
|
+
`gathering` covers the whole set, and matches how the other `lore` genres are
|
|
774
|
+
named: a single lowercase noun for a kind of thing.
|
|
775
|
+
|
|
776
|
+
Nothing existing changes. A note already filed under `lore/culture` keeps
|
|
777
|
+
compiling until its author moves it; `subType` is not an address, so nothing
|
|
778
|
+
resolves through it.
|
|
779
|
+
- 5630e7c: **A note can no longer author a field the document writes in play** (#330).
|
|
780
|
+
|
|
781
|
+
A data model declares everything a document stores, and part of that is runtime
|
|
782
|
+
state — an affliction's `onsetDate` is the world time its onset fired at.
|
|
783
|
+
Writing `sohl.system.onsetDate` reached it as directly as any other field: the
|
|
784
|
+
block is a verbatim passthrough, no declared field claimed the path, and the
|
|
785
|
+
schema check's fatal direction is _undeclared_, which a field the schema really
|
|
786
|
+
does declare satisfies. So a compiled pack could ship one world's play state to
|
|
787
|
+
every world that installed it, with the build reporting success.
|
|
788
|
+
|
|
789
|
+
A field declaration may now say `runtimeOnly`, whose value is the reason — what
|
|
790
|
+
the field holds — the way `topLevelMeans` already works. It states both halves
|
|
791
|
+
of one fact:
|
|
792
|
+
|
|
793
|
+
| declaration | authored | absent |
|
|
794
|
+
| ------------- | ----------- | --------------- |
|
|
795
|
+
| ordinary | emitted | default written |
|
|
796
|
+
| `runtimeOnly` | **refused** | key omitted |
|
|
797
|
+
|
|
798
|
+
The refusal names the note, the line, the whole key and the field's own reason,
|
|
799
|
+
and says that deleting it is the fix — there is no value that makes writing one
|
|
800
|
+
right. Omitting the key rather than emitting `null` is what leaves the data
|
|
801
|
+
model's own `initial` standing.
|
|
802
|
+
|
|
803
|
+
It is a property of the declaration, not a list of names, so it holds for any
|
|
804
|
+
runtime-only field any system adds later. Such an entry declares a `to` and no
|
|
805
|
+
`name`, which keeps it out of the authored vocabulary — the generated field
|
|
806
|
+
reference lists it under **Never authored** with its reason instead of as a row
|
|
807
|
+
an author might fill in — while still claiming the path for the passthrough.
|
|
808
|
+
|
|
809
|
+
**What a consumer sees**
|
|
810
|
+
|
|
811
|
+
- SoHL's six timed-phase dates are declared: `contractDate`, `onsetDate`,
|
|
812
|
+
`treatmentDate` and `resolutionDate` on `affliction`; `contractDate` and
|
|
813
|
+
`treatmentDate` on `trauma`. No tree authors one today, so no compiled
|
|
814
|
+
document changes.
|
|
815
|
+
- The refusal covers both positions a note can reach them from: a note's own
|
|
816
|
+
`<system>.system` block, and an actor's `items:` entry `system:` overlay,
|
|
817
|
+
which merges verbatim and so passed no field declaration at all.
|
|
818
|
+
- A runtime-only path is no longer reported as a field the builder forgot to
|
|
819
|
+
emit — "every compiled document will carry the field's initial value" is what
|
|
820
|
+
the declaration is _for_, so the warning could never be cleared.
|
|
821
|
+
- Regenerate the item frontmatter reference to pick up the new section.
|
|
822
|
+
- cf20488: **The specification's `subType` lists are compared to the declared vocabulary,
|
|
823
|
+
for every type** (#345).
|
|
824
|
+
|
|
825
|
+
`tests/content-format-agreement.test.ts` made `docs/content-format.md`
|
|
826
|
+
executable for the `data` property tables only. The other half of the same
|
|
827
|
+
vocabulary entry — a type's genres, which an author picks from and which a
|
|
828
|
+
note's `subType` is closed against — was prose that nothing read, free to
|
|
829
|
+
disagree with `note-vocabulary.mjs` in either direction. That is the drift #231
|
|
830
|
+
and #232 were filed about, on the half they did not reach. `gathering` (#333)
|
|
831
|
+
guarded `lore` alone, deliberately scoped to the type it changed.
|
|
832
|
+
|
|
833
|
+
**The five spellings converged on one first.** The document stated a type's
|
|
834
|
+
values as `subType`, `subType:`, `**subType**`, `**subType**:` and
|
|
835
|
+
`**subTypes**:`, and a reader that accepted every one of them would accept the
|
|
836
|
+
sixth by reading that section as declaring nothing — the exact failure the
|
|
837
|
+
comparison exists to catch. One shape is now stated in the specification and
|
|
838
|
+
enforced by the parser: `**subType**:` on its own line, then `- <value>` or
|
|
839
|
+
`- <value>: <definition>`, one bullet per value. A type with no `subType`, or
|
|
840
|
+
one whose values are not enumerated yet, writes no marker.
|
|
841
|
+
|
|
842
|
+
| Written | Read as |
|
|
843
|
+
| -------------------------------------------- | ----------------------------------------------------------- |
|
|
844
|
+
| `**subType**:` + a bullet per value | that type's closed value list, in document order |
|
|
845
|
+
| no marker | the type enumerates none — the ordinary case for nine types |
|
|
846
|
+
| any other spelling, or a marker with no list | a build error naming the line |
|
|
847
|
+
|
|
848
|
+
**What the comparison asks**, of every type rather than of `lore`: the values
|
|
849
|
+
the specification lists equal the values `NOTE_VOCABULARY` declares, in the same
|
|
850
|
+
order. `subTypes` stays three-valued — a list is closed, `null` is a `subType`
|
|
851
|
+
whose values are not enumerated, an absent key is a type with no `subType` at
|
|
852
|
+
all — and each reading is compared to what the document states. The `lore`-only
|
|
853
|
+
assertion is folded in.
|
|
854
|
+
|
|
855
|
+
Nothing that compiles changes: the values were already equal everywhere, in both
|
|
856
|
+
shapes, so this is about keeping them that way. `parseContentFormat` gains a
|
|
857
|
+
`subTypes` array per type, and throws on a marker it does not recognise —
|
|
858
|
+
reachable only through `content-format --spec <a copy of the document>`.
|
|
859
|
+
- 312379e: **Afflictions and traumas can declare their timed phases** (#329).
|
|
860
|
+
|
|
861
|
+
SoHL stores each timed phase as `{…DurationFormula, …DurationBase, …Date}`, and
|
|
862
|
+
the two authored thirds were declared by nothing. They were reachable only
|
|
863
|
+
through the raw `system:` passthrough — undocumented, uncoerced, and absent from
|
|
864
|
+
the field list every author-facing surface is built from — so no note in any
|
|
865
|
+
tree wrote one. Every shipped affliction carried `null`, and
|
|
866
|
+
`AfflictionLogic.rollDuration()` opens `if (!formula) return 0`: the timed-phase
|
|
867
|
+
machinery existed, and the content that would drive it could not be written.
|
|
868
|
+
|
|
869
|
+
They could not simply be declared either. `buildFromFields` wrote every declared
|
|
870
|
+
field unconditionally, so a declaration would have stamped `null` onto every
|
|
871
|
+
document — the same outcome, minus the ability to tell "unset" from "authored as
|
|
872
|
+
empty".
|
|
873
|
+
|
|
874
|
+
**`omitWhenAbsent`** is the missing capability: a field declaring it is emitted
|
|
875
|
+
when the note carries one and has its **key left out entirely** when it does
|
|
876
|
+
not, so the DataModel's own `initial` stands. It completes the table
|
|
877
|
+
`runtimeOnly` (#330) opened:
|
|
878
|
+
|
|
879
|
+
| declaration | authored | absent |
|
|
880
|
+
| ---------------- | ----------- | --------------- |
|
|
881
|
+
| ordinary | emitted | default written |
|
|
882
|
+
| `omitWhenAbsent` | emitted | key omitted |
|
|
883
|
+
| `runtimeOnly` | **refused** | key omitted |
|
|
884
|
+
|
|
885
|
+
The decision is made on the **position** a value came from, never on the value:
|
|
886
|
+
a declared `default: null` and an authored `null` are the same value and
|
|
887
|
+
opposite facts. `readFieldEntry` reports the source beside the value so the
|
|
888
|
+
position is resolved once rather than twice.
|
|
889
|
+
|
|
890
|
+
**Twelve fields are now declared vocabulary** — `onset`, `healingCheck` and
|
|
891
|
+
`resolution` on `affliction`; `healingCheck`, `bloodLossAdvance` and `course` on
|
|
892
|
+
`trauma` — each as both a `…DurationFormula` and a `…DurationBase`, in the
|
|
893
|
+
`sohl:` block and in the closed `data:` container alike. Intervals are in
|
|
894
|
+
seconds, and a bare number is a valid formula.
|
|
895
|
+
|
|
896
|
+
**What a consumer sees**
|
|
897
|
+
|
|
898
|
+
- The twelve appear in the generated item-frontmatter reference, with `_omitted_`
|
|
899
|
+
in the Default column rather than a value. Regenerate the page.
|
|
900
|
+
- The `unemitted` warnings these raised against a pinned schema clear.
|
|
901
|
+
- No compiled document changes: no tree authors one yet, and a note that writes
|
|
902
|
+
nothing emits nothing where it previously emitted nothing.
|
|
903
|
+
- `omitWhenAbsent` may not be combined with `default` (contradictory), with
|
|
904
|
+
`required`, or with `runtimeOnly`; the shipped declarations are checked for all
|
|
905
|
+
three.
|
|
906
|
+
|
|
907
|
+
### Patch Changes
|
|
908
|
+
|
|
909
|
+
- b5004f0: **A `model:` now resolves an item whose shortcode carries a capital** (#346). The
|
|
910
|
+
catalogue was keyed on the compiled document's `system.shortcode` exactly, while
|
|
911
|
+
an address is lower-cased when it is read — so `model: weapongear-clb` looked for
|
|
912
|
+
`weapongear:clb` while the document sat under `weapongear:Clb`, and every being
|
|
913
|
+
referencing one of the six mixed-case gear shortcodes in `sohl` failed to
|
|
914
|
+
compile.
|
|
915
|
+
|
|
916
|
+
The catalogue key folds the shortcode's case. The **id-bearing** address does
|
|
917
|
+
not: `itemAddress` seeds `embeddedItemId`, so folding there would change the
|
|
918
|
+
`_id` of every embedded item whose identity carries a capital — silently
|
|
919
|
+
re-identifying documents nothing about which had changed. A catalogue is a lookup
|
|
920
|
+
table; an id is a promise.
|
|
921
|
+
|
|
922
|
+
Verified by compiling both swept trees: `sohl` emits actors, items, macros,
|
|
923
|
+
scenes and adventures byte-identical to its pre-sweep baseline, and
|
|
924
|
+
`sohl-thalorna` differs only by the 938 embedded ids its sweep predicts.
|
|
925
|
+
- 4c57a5a: **A being's `data.portrait` reaches the actor** (#332). It never had: the
|
|
926
|
+
emitters read `blockProperty(fm, "portrait")`, which knows a system block and
|
|
927
|
+
the note's top level and never splits a dotted path, so the position the content
|
|
928
|
+
format names was invisible to them — and the `?? defaultImg` beside it turned
|
|
929
|
+
every miss into the subtype's icon rather than into a complaint. 646
|
|
930
|
+
`sohl-thalorna` beings authored a portrait, 341 of them pointing at art that
|
|
931
|
+
exists on disk, and every one compiled the generic person icon. Nothing warned.
|
|
932
|
+
|
|
933
|
+
`portrait` now resolves through the same declaration `data.species` does, in
|
|
934
|
+
both the `sohl` and `hm3` actor passes.
|
|
935
|
+
|
|
936
|
+
**A `data:` source has a retiring top-level spelling, and step 3b reads it.**
|
|
937
|
+
`data:` did not invent the facts it holds — it gathered them out of the note's
|
|
938
|
+
open top level, where `portrait:` sat beside `img:` — so the pre-`data:`
|
|
939
|
+
spelling of `data.<key>` is `<key>`, and until now nothing read it. That is why
|
|
940
|
+
this is a resolution-order fix and not a one-line emitter fix: `data.portrait`
|
|
941
|
+
had to start working _without_ breaking the top-level `portrait:` that `sohl`'s
|
|
942
|
+
own bestiary writes on every note.
|
|
943
|
+
|
|
944
|
+
The spelling is **derived**, not declared — a second declaration would be a
|
|
945
|
+
second place for one fact to live — and only a `data.` source has one, so
|
|
946
|
+
`protection.blunt` and `impact.die` resolve exactly as they did.
|
|
947
|
+
|
|
948
|
+
**Nothing is dropped in silence any more.** A field read from the retiring
|
|
949
|
+
top-level key emits a warning naming the line, the counterpart to the existing
|
|
950
|
+
in-block report; the note compiles to the identical document either way. The
|
|
951
|
+
frontmatter lint's `portrait: ""` check reads the `data:` position too, which it
|
|
952
|
+
could not see before.
|
|
953
|
+
- 59469d7: **The dependency check read English prose in a comment as an import** (#355).
|
|
954
|
+
|
|
955
|
+
`tests/dependencies-are-declared.test.ts` finds a shipped file's imports with a
|
|
956
|
+
regex over the raw file text. It already reasoned about one false positive — the
|
|
957
|
+
lookbehind stops `["from", "to"]` reading as an import of `", "` — but not about
|
|
958
|
+
comments, where `from` is an ordinary word and the quotes are ordinary quotes.
|
|
959
|
+
Any explanatory comment containing the word `from`, `import` or `require`
|
|
960
|
+
followed by a quoted phrase was reported as an undeclared dependency:
|
|
961
|
+
|
|
962
|
+
```text
|
|
963
|
+
FAIL sohl/item-fields.mjs imports only builtins, itself, or a declared dependency
|
|
964
|
+
+ [ "sohl/item-fields.mjs:455 → this note does not set the phase" ]
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
The message names a real file and a real line and says a dependency is missing,
|
|
968
|
+
so the first reading is that one genuinely is. Nothing in it suggests the culprit
|
|
969
|
+
is a sentence, and the fix — reword the comment — is unrelated to anything the
|
|
970
|
+
message describes. It cost a debugging cycle in #329, and the workaround left the
|
|
971
|
+
trap armed for whoever wrote the next comment.
|
|
972
|
+
|
|
973
|
+
**Comments are now blanked before the regex runs.** They are located by parsing
|
|
974
|
+
the file, not by a second regex, so `//` inside a string literal is still a string
|
|
975
|
+
literal. Each comment's characters are replaced one-for-one with spaces and its
|
|
976
|
+
newlines are left alone, so every offset survives and a finding still points at
|
|
977
|
+
the line a reader opens. The `sohl/item-fields.mjs` comment that provoked this
|
|
978
|
+
reads naturally again, and the suite carries it verbatim.
|
|
979
|
+
- fec6c80: **The generated item-frontmatter examples no longer author an `id:`** (#314).
|
|
980
|
+
|
|
981
|
+
`content-build docs item-fields` emitted `id: <16-character id>` in the worked
|
|
982
|
+
example for every item type — thirteen of them in the `sohl` tree. Since #270
|
|
983
|
+
and #277 a note's document `_id` derives from its canonical address, and the
|
|
984
|
+
authored field is the escape hatch for keeping a document's identity across a
|
|
985
|
+
shortcode rename, not part of the envelope every note carries.
|
|
986
|
+
|
|
987
|
+
The example is the block an author copies as a template, and the page is the
|
|
988
|
+
per-type reference they read while writing the note, so the one place the field
|
|
989
|
+
survived a tree's sweep was the document teaching them to write it. It is now
|
|
990
|
+
omitted, as every other optional envelope field already was; the `type` and
|
|
991
|
+
`shortcode` the derivation reads are unchanged.
|
|
992
|
+
|
|
993
|
+
Consumers should regenerate their item frontmatter reference to drop the line.
|
|
994
|
+
- a48802f: **A folder note's published `id` is now the id its Foundry documents carry**
|
|
995
|
+
(#310).
|
|
996
|
+
|
|
997
|
+
The content index derived every note's id under the `document` namespace. A
|
|
998
|
+
`Folder` is hashed under the `folder` namespace, so the index published one
|
|
999
|
+
value and the packs addressed another — `sohl-none-folder-cookware` was
|
|
1000
|
+
`f5d3dc635b7e799c` in the index and `b92b28b7d06638ed` in every pack.
|
|
1001
|
+
|
|
1002
|
+
`noteDocId` now asks the folder pass for a folder's id instead of deriving a
|
|
1003
|
+
second one, so the two cannot disagree.
|
|
1004
|
+
|
|
1005
|
+
**Why nothing caught it.** Every one of `sohl`'s 79 folder notes pins an `id`,
|
|
1006
|
+
and a pin wins in both paths — so all 65 emitted folder documents agreed by
|
|
1007
|
+
coincidence. It is also invisible from inside a build: no pass reads a folder's
|
|
1008
|
+
id off the index. The published artifact was the only place the wrong value
|
|
1009
|
+
surfaced, and a reader outside the build could neither recompute the right one
|
|
1010
|
+
nor notice the wrong one.
|
|
1011
|
+
|
|
1012
|
+
**The general rule this settles:** for every entry the content index gives an
|
|
1013
|
+
identity to, it publishes both the `id` and the `uuid`, each computed once by
|
|
1014
|
+
whatever owns that entry's derivation. A documentation journal's record
|
|
1015
|
+
accordingly gains its own `id` — it carried the UUID that id ends in, but not
|
|
1016
|
+
the id — so a consumer reads it rather than parsing it back out of the UUID's
|
|
1017
|
+
last segment.
|
|
1018
|
+
|
|
1019
|
+
_No emitted document changes; this corrects what the index says about them._
|
|
1020
|
+
- ebd3a80: **A note-level frontmatter check no longer answers from a system field that
|
|
1021
|
+
merely shares the name** (#312).
|
|
1022
|
+
|
|
1023
|
+
`topLevelMeans` exists because one spelling can name two unrelated quantities. An
|
|
1024
|
+
`affiliation` item's `system.title` is the style of address an office carries —
|
|
1025
|
+
"Ajaw", "Warden"; a note's top-level `title` is the heading its page publishes
|
|
1026
|
+
under. The field declares the collision, and `resolveFieldValue` honours it by
|
|
1027
|
+
refusing to read the top level for that field.
|
|
1028
|
+
|
|
1029
|
+
The frontmatter lint did not. `authoredValue` resolved every check through the
|
|
1030
|
+
`sohl:` block first, so the blank-heading check — whose emitter is `fm.title ??
|
|
1031
|
+
name`, the note's top level and nothing else — read `sohl.title` and found the
|
|
1032
|
+
office's style of address. An office with no style of address is ordinary, and
|
|
1033
|
+
each one was reported as a page published with no heading, sorting to the front
|
|
1034
|
+
of its section.
|
|
1035
|
+
|
|
1036
|
+
**The statement is symmetric, and is now read that way.** If two positions hold
|
|
1037
|
+
unrelated quantities then the in-block position is not the note-level field
|
|
1038
|
+
either, so a note-level check reads past a block key the note's own type claims
|
|
1039
|
+
for something else. The exemption is still the field's own declaration rather
|
|
1040
|
+
than a name the linter knows: `collidingBlockKeys` asks the schemas the caller
|
|
1041
|
+
supplies, so the linter and the resolver cannot disagree about which field
|
|
1042
|
+
declares one.
|
|
1043
|
+
|
|
1044
|
+
**The art fields are checked the same way.** `img` and `portrait` keep resolving
|
|
1045
|
+
through the block, because that is what their emitter does — `blockProperty`
|
|
1046
|
+
reads `sohl.img` first, so a `sohl.img: ""` really does ship a document with no
|
|
1047
|
+
art and is still reported. What changes is that a future system field of either
|
|
1048
|
+
name cannot quietly answer for the note's own art; a map's was `sohl.image`
|
|
1049
|
+
until #142.
|
|
1050
|
+
|
|
1051
|
+
**What a consumer sees.** Twenty-eight fewer warnings on an unswept
|
|
1052
|
+
`sohl-kethira-basic` — every affiliation writing `sohl.title: ""`. All were false
|
|
1053
|
+
positives; their pages took `name.full` throughout. A tree already swept onto
|
|
1054
|
+
`sohl.system.title` was unaffected either way, which is why the findings
|
|
1055
|
+
disappearing looked like a lint regression in that sweep rather than the
|
|
1056
|
+
false positives going away.
|
|
1057
|
+
- f6a8a05: **The specification and four engine docblocks stated the retired folder model**
|
|
1058
|
+
(#358).
|
|
1059
|
+
|
|
1060
|
+
The folder epic replaced a model wholesale — `folder:` named a Foundry id
|
|
1061
|
+
resolved against a per-pack `*-folders.yaml`, and `packFolder` named a path.
|
|
1062
|
+
None of that exists: `folder:` is refused, the YAML is gone, and `packFolder`
|
|
1063
|
+
is a folder note's address. Six passages still described the old shape as the
|
|
1064
|
+
live one.
|
|
1065
|
+
|
|
1066
|
+
**`docs/content-format.md` contradicted itself twice.** The shared-mappings
|
|
1067
|
+
table — the one place eight rows common to all sixteen type tables are stated —
|
|
1068
|
+
offered `` `packFolder` / `folder` ``, so a reader was told to write a value the
|
|
1069
|
+
build rejects, 330 lines before the same document says it is retired. And the
|
|
1070
|
+
argument for deriving a document id cited "`packFolder: <path>` above", where
|
|
1071
|
+
above says address.
|
|
1072
|
+
|
|
1073
|
+
**Four docblocks described the retired resolution path**, and they publish:
|
|
1074
|
+
|
|
1075
|
+
| site | said |
|
|
1076
|
+
| -------------------------- | ----------------------------------------------------------------------------------- |
|
|
1077
|
+
| `engine/generate.mjs` | folder files "referenced from entry frontmatter via `sohl.folder: <id>`" |
|
|
1078
|
+
| `engine/journals.mjs` | the target folder's id "from folders.yaml", resolved against a folders.yaml list |
|
|
1079
|
+
| `engine/base-compiler.mjs` | `folderResolver` "resolves a `sohl.folder` id against this pack's folder hierarchy" |
|
|
1080
|
+
| `engine/frontmatter.mjs` | `folderField` reads "two spellings", `packFolder` winning "where both are present" |
|
|
1081
|
+
|
|
1082
|
+
The last two were the sharpest. `folderField` reads `packFolder` and nothing
|
|
1083
|
+
else, so its docstring described a resolution the function cannot perform and
|
|
1084
|
+
deferred to an issue that had closed. `generate.mjs` disagreed with itself
|
|
1085
|
+
across one file: the module header named `sohl.folder`, while its `resolver`
|
|
1086
|
+
states the rule correctly — "There is one spelling."
|
|
1087
|
+
|
|
1088
|
+
**Prose is the defect the epic was about.** Its argument against
|
|
1089
|
+
`*-folders.yaml` was that a second, unchecked statement of one fact drifts from
|
|
1090
|
+
the first, and nothing compares the two. These six passages were exactly that,
|
|
1091
|
+
and nothing caught them: `lint:content-format` makes a claim only for a
|
|
1092
|
+
`system.*` target, so a row mapping to core Foundry's `folder` yields none, and
|
|
1093
|
+
the source side of a shared row is checked by nothing at all.
|
|
1094
|
+
|
|
1095
|
+
Two assertions now hold the specification to it — the shared-mappings sources
|
|
1096
|
+
name no retired field, and the document never presents `packFolder` as holding
|
|
1097
|
+
a path. The docblocks are held to review instead: a sentence describing
|
|
1098
|
+
`folder:` as _retired_ is correct and must survive, and no assertion separates
|
|
1099
|
+
that from one describing it as live without reading the prose.
|
|
1100
|
+
|
|
1101
|
+
No behaviour changes; the fix is what the documents say.
|
|
1102
|
+
|
|
3
1103
|
## 19.0.0
|
|
4
1104
|
|
|
5
1105
|
### Major Changes
|