@heroiclands/package-build 8.1.0 → 10.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 +748 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +479 -123
- package/bin/package-build.mjs +27 -69
- package/bin/report.mjs +1 -2
- package/bundle.mjs +2 -10
- package/config.mjs +31 -106
- package/container.mjs +13 -57
- package/content-config.mjs +300 -188
- package/coverage.mjs +14 -55
- package/deploy.mjs +4 -13
- package/docs/content-format.md +1418 -0
- package/e2e.mjs +16 -55
- package/engine/address-charset.mjs +62 -0
- package/engine/address-diff.mjs +1 -4
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +203 -31
- package/engine/code-fences.mjs +4 -13
- package/engine/compendiums.mjs +13 -37
- package/engine/content-address.mjs +6 -10
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +144 -99
- package/engine/content-lint.mjs +12 -16
- package/engine/content-slug.mjs +2 -6
- package/engine/content-tables.mjs +26 -79
- package/engine/diagnostics.mjs +37 -15
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-reference.mjs +6 -20
- package/engine/field-spec.mjs +49 -45
- package/engine/foreign-catalog.mjs +7 -22
- package/engine/foreign-manifests.mjs +1 -4
- package/engine/frontmatter-lint.mjs +347 -43
- package/engine/frontmatter.mjs +3 -8
- package/engine/generate.mjs +36 -20
- package/engine/helpers.mjs +54 -81
- package/engine/homepage.mjs +4 -15
- package/engine/ids.mjs +21 -12
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -9
- package/engine/journals.mjs +4 -14
- package/engine/kb-manifest.mjs +41 -24
- package/engine/macros.mjs +2 -10
- package/engine/manifest-emit.mjs +6 -17
- package/engine/map-notes.mjs +53 -87
- package/engine/note-claims.mjs +383 -0
- package/engine/note-package.mjs +1 -4
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +56 -60
- package/engine/pack-router.mjs +18 -8
- package/engine/prose-config.mjs +20 -4
- package/engine/prose-lint.mjs +61 -17
- package/engine/region-events.mjs +1 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scene-levels.mjs +8 -22
- package/engine/scenes.mjs +31 -47
- package/engine/schema-check.mjs +348 -7
- package/engine/schema-extract.mjs +11 -39
- package/engine/site-build.mjs +13 -38
- package/engine/site-index.mjs +40 -35
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +115 -92
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +147 -183
- package/index.mjs +1 -5
- package/lang.mjs +1 -3
- package/manifest.mjs +10 -37
- package/markdownlint-config.mjs +1 -5
- package/package.json +6 -2
- package/sohl/actors.mjs +251 -68
- package/sohl/being-info.mjs +3 -6
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +4 -6
- package/sohl/item-builders.mjs +1 -3
- package/sohl/item-fields.mjs +16 -34
- package/sohl/items.mjs +111 -17
- package/sohl/kb-passes.mjs +29 -39
- package/sohl/note-schemas.mjs +11 -7
- package/sohl/skill-base.mjs +7 -23
- package/stage.mjs +3 -13
- package/templates.mjs +4 -15
- package/types/bundle.d.mts +1 -1
- package/types/container.d.mts +2 -2
- package/types/content-config.d.mts +48 -4
- package/types/coverage.d.mts +1 -1
- package/types/e2e.d.mts +4 -4
- package/types/engine/address-charset.d.mts +45 -0
- package/types/engine/alias-index.d.mts +122 -0
- package/types/engine/base-compiler.d.mts +132 -4
- package/types/engine/content-address.d.mts +2 -2
- package/types/engine/content-format-check.d.mts +163 -0
- package/types/engine/content-format.d.mts +101 -0
- package/types/engine/content-links.d.mts +16 -1
- package/types/engine/content-lint.d.mts +6 -0
- package/types/engine/diagnostics.d.mts +29 -0
- package/types/engine/document-subtypes.d.mts +233 -0
- package/types/engine/field-spec.d.mts +76 -23
- package/types/engine/frontmatter-lint.d.mts +47 -2
- package/types/engine/generate.d.mts +15 -2
- package/types/engine/helpers.d.mts +22 -14
- package/types/engine/ids.d.mts +10 -0
- package/types/engine/index.d.mts +5 -0
- package/types/engine/item-registry.d.mts +21 -2
- package/types/engine/kb-manifest.d.mts +35 -8
- package/types/engine/map-notes.d.mts +21 -11
- package/types/engine/note-claims.d.mts +113 -0
- package/types/engine/note-vocabulary.d.mts +251 -0
- package/types/engine/pack-config.d.mts +4 -3
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/prose-lint.d.mts +6 -2
- package/types/engine/retired-fields.d.mts +73 -2
- package/types/engine/schema-check.d.mts +182 -0
- package/types/engine/schema-extract.d.mts +1 -1
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/system-block.d.mts +281 -0
- package/types/engine/web-wikilinks.d.mts +23 -12
- package/types/engine/wikilink-syntax.d.mts +29 -0
- package/types/manifest.d.mts +1 -1
- package/types/sohl/actors.d.mts +62 -6
- package/types/sohl/document-subtypes.d.mts +14 -0
- package/types/sohl/index.d.mts +1 -0
- package/types/sohl/items.d.mts +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,753 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 10.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 7f5c14b: **The pipe decides how a wikilink resolves** (#131). `[[x]]` is an **alias**;
|
|
8
|
+
`[[x|…]]` is an **address**. Neither falls back to the other, so a target is
|
|
9
|
+
read by the punctuation the author wrote rather than by whether its shape
|
|
10
|
+
happens to look like an address.
|
|
11
|
+
|
|
12
|
+
Both resolvers previously tried the address grammar and fell through to the
|
|
13
|
+
alias index — or the reverse — so one authored link had two chances to land and
|
|
14
|
+
the author could not say which they meant. A note whose _name_ looked like an
|
|
15
|
+
address (`Grukar-ahk`) was read as one, and a genuine address that resolved
|
|
16
|
+
nowhere silently became a name lookup and reported nothing.
|
|
17
|
+
|
|
18
|
+
**What changes for a content tree**
|
|
19
|
+
|
|
20
|
+
| Written | Was | Now |
|
|
21
|
+
| -------------------------- | -------------------------------- | ---------------------------------------- |
|
|
22
|
+
| `[[type-shortcode\|Text]]` | address | address — _unchanged_ |
|
|
23
|
+
| `[[type-shortcode\|]]` | address, shows the target's name | _unchanged_ |
|
|
24
|
+
| `[[Some Name]]` | alias | alias — _unchanged_ |
|
|
25
|
+
| `[[type-shortcode]]` | address | **alias lookup**, so it must gain a `\|` |
|
|
26
|
+
| `[[Some Name\|Text]]` | alias | **address**, so the name must become one |
|
|
27
|
+
|
|
28
|
+
An empty label stays writable and now carries its full weight: `[[x|]]` is the
|
|
29
|
+
one way to write an address that renders the target's _current_ name, so a
|
|
30
|
+
rename shows at every citation with no link edited.
|
|
31
|
+
|
|
32
|
+
**The alias index no longer carries the filename.** Its sources are the
|
|
33
|
+
authored ones — `aliases`, `name.aliases`, `name.full`. `basename(file, ".md")`
|
|
34
|
+
with underscores turned to spaces admitted keys nobody could cite: thirteen
|
|
35
|
+
`_Introduction.md` notes all claimed `" introduction"`, leading space included.
|
|
36
|
+
Measured across five content trees, not one link that resolves today resolves
|
|
37
|
+
through the filename alone.
|
|
38
|
+
|
|
39
|
+
**A same-type alias collision is now a finding, naming every claimant.** It was
|
|
40
|
+
silently deleted, so the pair resolved to nothing and nobody was told. The
|
|
41
|
+
finding is reported at each claiming note, never at the note that merely cites
|
|
42
|
+
the alias — whoever added the second claimant broke every existing citation.
|
|
43
|
+
|
|
44
|
+
**The two failure modes read differently.** A piped target that resolves
|
|
45
|
+
nowhere, or does not parse as an address at all, is an **error**: the pipe says
|
|
46
|
+
the author meant an address. An unpiped target naming no note of the source's
|
|
47
|
+
type is a **warning**, since a bare `[[Name]]` may be a worldbuilding
|
|
48
|
+
placeholder for a note not yet written.
|
|
49
|
+
|
|
50
|
+
Resolution is stated once for both builds: `resolvesAsAddress` in
|
|
51
|
+
`engine/wikilink-syntax.mjs`, and the new `engine/alias-index.mjs` for what may
|
|
52
|
+
be claimed and how a claim is keyed.
|
|
53
|
+
|
|
54
|
+
### Minor Changes
|
|
55
|
+
|
|
56
|
+
- 3e6f3a4: Give an affiliation the ranks and offices it confers, settle the
|
|
57
|
+
`government`/`governance` split on one root, and stop the mapping table claiming
|
|
58
|
+
system fields for things that reach none (#160).
|
|
59
|
+
|
|
60
|
+
**An affiliation now publishes its own social structure.** `governance.ranks` is
|
|
61
|
+
the ladder the body confers — `level`, `title`, `description` per rung — and
|
|
62
|
+
`governance.offices` is a map of named post to what that post does. Level 0 is
|
|
63
|
+
reserved for the excluded: outlawed, expelled, excommunicated is a standing an
|
|
64
|
+
organisation still recognises, so it still has to define it.
|
|
65
|
+
|
|
66
|
+
The reason to put them here rather than on a membership is that a bare `level: 4`
|
|
67
|
+
says nothing on its own. It means _Knight_ only because the polity declared that
|
|
68
|
+
rung, so the ladder is authored once, on the body that confers it, and a member's
|
|
69
|
+
rank is an index into it. Offices are not ranked at all — a Chancellor and a
|
|
70
|
+
Marshal are both great officers and neither is above the other — so an office is a
|
|
71
|
+
key with a description rather than a rung.
|
|
72
|
+
|
|
73
|
+
**The membership fields leave `data:`.** `society`, `office`, `title` and `level`
|
|
74
|
+
described a being's standing in a body, not the body, and all 199 authored
|
|
75
|
+
affiliation notes left them null. They are filled on the affiliation _as embedded
|
|
76
|
+
on a being_, which is where they always belonged.
|
|
77
|
+
|
|
78
|
+
**One root: `governance`.** The specification mixed `governance.model` with
|
|
79
|
+
`government.summary` and `engine/note-vocabulary.mjs` declared exactly that pair,
|
|
80
|
+
with a comment saying the two had to be reconciled somewhere else. They are
|
|
81
|
+
reconciled here, on `governance` — the root that names the concept rather than
|
|
82
|
+
the institution, and the one the new `ranks` and `offices` read naturally under.
|
|
83
|
+
|
|
84
|
+
**`Republic` joins `GovernanceModel`, and the three civic models are given a
|
|
85
|
+
test.** A Roman-shaped republic was none of the existing values exactly: not an
|
|
86
|
+
`Oligarchy`, whose closed group holds authority with no election and no term; not
|
|
87
|
+
a `Democracy`, where any member may hold office. Ask who fills the offices and on
|
|
88
|
+
what terms and the three separate cleanly, which the enum now says. Naming it
|
|
89
|
+
`Republic` rather than `Senatorial Oligarchy` keeps the enum a partition of
|
|
90
|
+
one-word answers to _where does authority rest_ — "senatorial" is a culture's word
|
|
91
|
+
for its ruling order, and culture words belong in a body's rank titles, not in the
|
|
92
|
+
shared vocabulary every culture is described with.
|
|
93
|
+
|
|
94
|
+
**Two renames and a new field.** `domain` becomes `domains`, which is what a list
|
|
95
|
+
of places wants to be called; `languages` becomes `commonSkills`, since what
|
|
96
|
+
members share is not only speech; and `economy` is new — the currencies, banking
|
|
97
|
+
bodies and goods an affiliation's economic life runs on, as wikilinks rather than
|
|
98
|
+
prose.
|
|
99
|
+
|
|
100
|
+
**The mapping table stops overclaiming.** `governance` reaches no `system.*`
|
|
101
|
+
field: SoHL's affiliation item has nowhere to put ranks or offices, and inventing
|
|
102
|
+
a mapping for a field no schema declares is the drift these tables exist to
|
|
103
|
+
catch. The four membership rows go with the fields, leaving 84 mapping claims and
|
|
104
|
+
66 checked against SoHL's published schema.
|
|
105
|
+
|
|
106
|
+
**`mystery` accepts `birthsign`**, which the specification declares and the
|
|
107
|
+
vocabulary did not, and the four gear headings are spelled as the vocabulary
|
|
108
|
+
enforces them — `armorgear`, `concoctiongear`, `projectilegear`, `weapongear` —
|
|
109
|
+
rather than as #78 will rename them.
|
|
110
|
+
- 1c4517f: Let an affiliation cite any lore, and declare its epithet and its symbol (#166).
|
|
111
|
+
|
|
112
|
+
**`peoples` becomes `lore`**, as it did on `place` in #164 and for the same
|
|
113
|
+
reason: peoples are one kind of lore among many, and the target's own subType
|
|
114
|
+
already distinguishes a `folk` from a `law` or a `calendar`.
|
|
115
|
+
|
|
116
|
+
**The case that made it urgent is the deity.** A `faithtradition` is a religion —
|
|
117
|
+
a practice, which can outlive belief in its god, and one god may be venerated by
|
|
118
|
+
several religions that agree on nothing else. The god is `lore` of subType
|
|
119
|
+
`deity`. With only `peoples` available a faith had nowhere to name the god it
|
|
120
|
+
venerates, and the authored corpus worked around it with an undeclared top-level
|
|
121
|
+
`deity` string on 75 notes: a name nothing could follow.
|
|
122
|
+
|
|
123
|
+
**`epithet` and `symbol` are declared.** Seventy-seven affiliations carried them
|
|
124
|
+
at the top level, where nothing checked them and nothing compiled them. Neither
|
|
125
|
+
is a faith's alone — they are what the members call the thing and what they carve,
|
|
126
|
+
so a guild has them as much as a cult does. What a god _is_ belongs on the deity
|
|
127
|
+
note, where every religion that venerates it can point at one account.
|
|
128
|
+
|
|
129
|
+
**A rank names the standing it is.** `Rank` gains `lore`, a link to a `lore` note
|
|
130
|
+
of subType `law`, and the note is **shared**: a Normen kingdom calls it `Thrall`
|
|
131
|
+
and a Vylarian province calls it `Slave`, and they mean one thing — owned
|
|
132
|
+
outright, with no standing at law except through an owner. The title is what this
|
|
133
|
+
body calls it and the description is how this body puts it, but the obligations
|
|
134
|
+
and rights belong to the standing, so they are written once and cited by every
|
|
135
|
+
ladder that confers them. In the authored corpus 237 distinct titles across 2,602
|
|
136
|
+
rank entries resolve onto 43 standings, which is also what makes a rank
|
|
137
|
+
answerable across bodies: asking what a `Naukrátissa` may do no longer means
|
|
138
|
+
reading the Bethûan fleet's ladder.
|
|
139
|
+
|
|
140
|
+
**A being cites lore too.** Its `peoples` becomes `lore` for the same reason
|
|
141
|
+
`place` and `affiliation` did: the people a character is of, the standing they
|
|
142
|
+
hold and the law they live under are all lore, and the target's own subType tells
|
|
143
|
+
a `culture` from a `law`. A being that names its culture and its rank in one list
|
|
144
|
+
is saying two things of one kind, which is what the field is for.
|
|
145
|
+
- 65a4ba1: **Affiliation subTypes: `spirittradition` added, `pantheon` removed.**
|
|
146
|
+
|
|
147
|
+
A totemic or ancestor cult had nowhere to go. `faithtradition` is defined as
|
|
148
|
+
concerning _the divine_, and `sohl-thalorna` carries 47 affiliations that are
|
|
149
|
+
not — 44 animal totems plus `Nyaluba_Spirits`, `The_Kindred` and `Astrokyklos`.
|
|
150
|
+
Folding them into `faithtradition` would also have collapsed the partition
|
|
151
|
+
`MYSTICALABILITY_SUBTYPE` distinguishes the spirit families by, and a picker
|
|
152
|
+
filter is only as useful as the partition it filters on. `spirittradition` is
|
|
153
|
+
worded symmetrically with its two siblings.
|
|
154
|
+
|
|
155
|
+
`pantheon` is gone because it answered a different question from every other
|
|
156
|
+
value. The rest state _what kind of body this is_; `pantheon` stated _where it
|
|
157
|
+
sits in a hierarchy_. A pantheon is a `faithtradition` carrying subordinate
|
|
158
|
+
faith traditions, and that hierarchy is already authored — 77 divine
|
|
159
|
+
affiliations carry a `pantheon:` key holding an affiliation shortcode which
|
|
160
|
+
resolves on all 77, while `parents` is set on none of them.
|
|
161
|
+
|
|
162
|
+
**No content changes.** None of the eleven values is authored anywhere yet, so
|
|
163
|
+
removing one and adding another costs no note an edit.
|
|
164
|
+
|
|
165
|
+
Closes #157.
|
|
166
|
+
- 20e51a7: Write a document's archetype to `system.archetype` instead of `flags.sohl.docArchetype` (part of #126, part of #127).
|
|
167
|
+
|
|
168
|
+
**Requires an unreleased SoHL.** This must not ship before HeroicLands/Song-of-Heroic-Lands-FoundryVTT#1780 declares `system.archetype` on SoHL's shared data schema and that release is out. Foundry discards an undeclared `system` key at construction without a word, so a package built with this against an older system carries an archetype nothing can read. The order is: the field, then this, then rebuild.
|
|
169
|
+
|
|
170
|
+
**What changed.** `sohl/items.mjs` and `sohl/actors.mjs` stop calling `withArchetypeFlag` and write the value into `system` — a number for an archetype at that priority, `null` for a document that is not one. `flags` becomes a plain passthrough of what the note authors, defaulting to `{}` exactly as before. `withArchetypeFlag` is deleted; `resolveArchetype` stays, and so does its required-ness, so an absent `archetype` is still an authoring error rather than a silent "not an archetype".
|
|
171
|
+
|
|
172
|
+
`engine/helpers.mjs` gains `systemArchetype`, which is where `resolveArchetype`'s `undefined` becomes the field's `null`. That conversion has to be somewhere: an emitted `undefined` is dropped by `JSON.stringify`, which would leave the compiled document with no `archetype` at all and a tri-state readable as two.
|
|
173
|
+
|
|
174
|
+
**The falsy trap, held by tests.** `0` means "is an archetype, at priority 0" — the priority SoHL's own archetypes ship at — while `null` means "is not one". `resolveArchetype(fm) || null` passes every other case and turns 1,470 SoHL documents from archetypes into non-archetypes, so the suite asserts `0` through the builder and through a `JSON` round trip.
|
|
175
|
+
|
|
176
|
+
**Compiled output moves, and this is the one change in this stack where it should.** Characterised document by document across three trees, every difference is a `flags.sohl.docArchetype` disappearing and a `system.archetype` appearing with the identical value, plus the now-empty `flags: {}` that the removed flag leaves behind. No `_id` and no `_key` moves anywhere.
|
|
177
|
+
|
|
178
|
+
Counts, with the last four columns the value each **top-level** document carries (embedded items carry it too, as they carried the flag):
|
|
179
|
+
|
|
180
|
+
| tree | compiled | changed | `0` | `1` | `100` | `null` |
|
|
181
|
+
| -------------------- | -------- | ------- | ----- | --- | ----- | ------ |
|
|
182
|
+
| `sohl` | 3,126 | 1,474 | 1,470 | 1 | — | 3 |
|
|
183
|
+
| `sohl-thalorna` | 2,561 | 1,273 | 157 | — | — | 1,116 |
|
|
184
|
+
| `sohl-kethira-basic` | 385 | 363 | 343 | 10 | 10 | — |
|
|
185
|
+
|
|
186
|
+
**One diagnostic moves.** Seven `sohl-thalorna` affiliation notes that already fail to compile now report the missing `archetype` rather than a folder id or a missing `subType`, because the requirement is checked earlier in `buildEntry` than the flag it replaces. Same files, same count, same severity; every other finding in all three trees is unchanged, message for message.
|
|
187
|
+
|
|
188
|
+
**Neither schema check has anything to say about it**, before or after the field is declared. `compareFields` derives what a builder emits from its `itemBuilders` field declarations, and `archetype` is written by the compiler itself — as `shortcode`, `actionDefs`, `notes` and `docHtml` already are — while the note-side check reads only what a note authors under `<system>.system`, and `archetype` is authored at the block's top level. So the ordering constraint above binds at Foundry's silent discard, not at a check that would catch it; the suite records that, so nobody reads the quiet as coverage.
|
|
189
|
+
- 6f59add: Declare `lore` subType `bestiary` and `doc` subType `collection` (#162).
|
|
190
|
+
|
|
191
|
+
**A creature that is not a people had nowhere to go.** `folk` is _related sapient
|
|
192
|
+
beings — kindreds, ancestries_, and `spirit` is the non-divine numinous; a beast
|
|
193
|
+
is neither. `bestiary` is what `folk` is for the sapient, applied to everything
|
|
194
|
+
else — including the made things that were never born.
|
|
195
|
+
|
|
196
|
+
**A note that indexes other notes had nowhere either.** A roster, a table of
|
|
197
|
+
settlements, an index of languages is not `rules`, not `user-guide`, and not
|
|
198
|
+
`reference` — and the distinction is where the content comes from. A reference
|
|
199
|
+
**states** facts of its own; a collection **derives** them from the tree, almost
|
|
200
|
+
always through a query. The author's test is whether the page would still say
|
|
201
|
+
anything if every other note vanished. `reference`'s own description drops
|
|
202
|
+
"indexes" accordingly.
|
|
203
|
+
|
|
204
|
+
**The value earns its keep by making a silent failure nameable.** A collection
|
|
205
|
+
whose query matches nothing renders a header and no rows, which looks exactly
|
|
206
|
+
like a full table until it is read — and `sohl-thalorna` has thirteen in that
|
|
207
|
+
state right now, having filtered on a frontmatter key its content format no
|
|
208
|
+
longer has. Nothing failed and nothing reported it. A checker can only say so
|
|
209
|
+
about a note class it can name.
|
|
210
|
+
- 103c307: Check the `system` keys a compiler writes itself against the receiving DataModel (closes #155, part of #127).
|
|
211
|
+
|
|
212
|
+
**What a consumer sees.** A compile can now fail with a line naming one of its
|
|
213
|
+
notes:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
assets/content/Skills/Social/Charm.md: error: the compiler writes `system.archetype` into every Item of subtype "skill", and no field declaration names it — Item subtype "skill" does not define it at 0.8.2, and Foundry discards an unknown `system` key when the document is constructed, without a warning, so the value is lost at load while the build reports success. No `itemBuilders` change fixes this: declare the field in the receiving system, or hold this package at a build that does not write it
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
It means the build is running **ahead of the system it compiles for**. Nothing
|
|
220
|
+
in the repository's own configuration writes the key, so nothing there can stop
|
|
221
|
+
it; the two real fixes are the ones the message names. For `archetype`
|
|
222
|
+
specifically that is HeroicLands/Song-of-Heroic-Lands-FoundryVTT#1785 — SoHL
|
|
223
|
+
checks against its **own** committed `schema.json`, so merging it is enough and
|
|
224
|
+
no release is needed; a module checks against the cached schema of the SoHL
|
|
225
|
+
release it pins, so it needs that release.
|
|
226
|
+
|
|
227
|
+
**The gap.** The emitted-versus-declared check (#60) derives what a build emits
|
|
228
|
+
from the `itemBuilders` **field declarations**, so a key a compiler writes on its
|
|
229
|
+
own initiative is in neither set it compares and was never compared at all.
|
|
230
|
+
That is not a residue: it is `shortcode`, `actionDefs`, `notes`, `docHtml`, and
|
|
231
|
+
since #126 `archetype`. #145's authored-`system` check does not reach them
|
|
232
|
+
either — it reads `<system>.system`, and these are written rather than authored.
|
|
233
|
+
So #126's ordering constraint, stated in the issue and real, was enforced by
|
|
234
|
+
nothing: get the order wrong and every compiled document silently loses
|
|
235
|
+
`system.archetype`, no check fires, the build is green, and the Create dialog
|
|
236
|
+
simply stops finding archetypes.
|
|
237
|
+
|
|
238
|
+
**Derived by observation, not by a list.** The compilers assemble a `system`
|
|
239
|
+
object, so `compareEmittedSystem` reads the keys off what they produced — after
|
|
240
|
+
the JSON round trip the pack file actually receives, which is why a key whose
|
|
241
|
+
value is `undefined` is correctly not a finding. A compiler that grows a key is
|
|
242
|
+
covered on the next build without anyone remembering to add it anywhere, and a
|
|
243
|
+
`system` block that is checked is the one that was written.
|
|
244
|
+
|
|
245
|
+
**Two conditions, because the fixes differ.** Both name the version, as the
|
|
246
|
+
existing message already does. A key a `fields:` entry declares is the
|
|
247
|
+
consumer's own — change the field's `to`, or get the system to declare it. A key
|
|
248
|
+
the compiler writes has no declaration to correct, and the message says so
|
|
249
|
+
rather than sending a reader looking through `itemBuilders` for something that
|
|
250
|
+
is not there.
|
|
251
|
+
|
|
252
|
+
**An error, deliberately.** The failure is Foundry's silent discard either way,
|
|
253
|
+
and the sibling condition has been an error since #60; making the _less_ fixable
|
|
254
|
+
half the quieter one would invert the point. Measured read-only against five
|
|
255
|
+
consuming trees, the whole cost is 11 findings in
|
|
256
|
+
`Song-of-Heroic-Lands-FoundryVTT` — one `archetype` per subtype, exactly what
|
|
257
|
+
#1785 declares. The satellites are unaffected today: sohl 0.8.2 published no
|
|
258
|
+
`schema.json`, so there is nothing for them to check against and the check stays
|
|
259
|
+
silent, and by the time they pin a release that publishes one the field is in
|
|
260
|
+
it.
|
|
261
|
+
|
|
262
|
+
**A subtree the schema describes no further is not checked.** SoHL's
|
|
263
|
+
`strikeMode` is a discriminated `TypedSchemaField`: published as one path,
|
|
264
|
+
stored flat as `{ type, name, … }`. Walking into it against a schema that
|
|
265
|
+
enumerates nothing beneath it reported all ten of a combat technique's stored
|
|
266
|
+
keys — ten findings, every one wrong, about a document that is correct. What the
|
|
267
|
+
artifact does not describe is left alone, the same stance the check already takes
|
|
268
|
+
on a subtype the artifact does not name.
|
|
269
|
+
|
|
270
|
+
**Compiled output does not move.** This adds a check, not an emission: the SoHL
|
|
271
|
+
tree compiles to the same 3,126 `build/packs-json` files, byte for byte.
|
|
272
|
+
- 808ef55: Add the closed `data:` container, and close `subType` (#128, part of #127).
|
|
273
|
+
|
|
274
|
+
**What it is.** A note's frontmatter has three regions and only one of them is
|
|
275
|
+
open. The top level describes the note as a published artefact and every key of
|
|
276
|
+
it is copied into the generated web page, so an unrecognised key there is a Hugo
|
|
277
|
+
or theme parameter this build has no standing to refuse. `data:` holds the
|
|
278
|
+
type-specific facts about the _subject_ — a weapon's weight, an affliction's
|
|
279
|
+
transmission, a being's species — and every note type declares which keys it may
|
|
280
|
+
carry.
|
|
281
|
+
|
|
282
|
+
**Why it earns its place.** Those facts previously sat at the top level, where
|
|
283
|
+
the pass-through rule applied to them too, so a misspelled `wieght` became a
|
|
284
|
+
theme parameter rather than a finding — indistinguishable, from the outside,
|
|
285
|
+
from a weapon that weighs nothing. Under `data:` the same key is reported where
|
|
286
|
+
it was written, with the key it was probably meant to be, drawn from that type's
|
|
287
|
+
own vocabulary and using the capped edit distance the `sohl:` check already
|
|
288
|
+
applies:
|
|
289
|
+
|
|
290
|
+
```text
|
|
291
|
+
assets/content/Gear/Axe.md:14:5: error: "wieght" is not a `data:` property declared by weapongear; the container is closed, so unlike a top-level key it is not passed through to the page. Did you mean "weight"?
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**`subType` stays at the top level**, and is closed in its own way: a type either
|
|
295
|
+
declares a `subType` or does not, and a type that does declares its values. A
|
|
296
|
+
`weapon` declares none — SoHL distinguishes a weapon's uses by strike mode rather
|
|
297
|
+
than by kind — so `subType` on one is a finding; a `skill` declares ten, so
|
|
298
|
+
`subType: crafte` is a finding naming `craft`.
|
|
299
|
+
|
|
300
|
+
**Additive.** Nothing reads `data:` into a document's `system` block yet — that
|
|
301
|
+
is the passthrough slice — and no note in any tree authors one today, so no
|
|
302
|
+
compiled output changes. `engine/note-vocabulary.mjs` carries the declaration,
|
|
303
|
+
one entry per note type, taken from the content-format specification;
|
|
304
|
+
`lintNote` and `lintFrontmatter` take it as a `vocabulary` option, so the linter
|
|
305
|
+
stays a checker of whatever it is handed rather than gaining type names of its
|
|
306
|
+
own, and a caller that supplies none is checked exactly as before.
|
|
307
|
+
- 3542a73: Commit the content format specification as `docs/content-format.md`, and add
|
|
308
|
+
`content-build content-format` to check it.
|
|
309
|
+
|
|
310
|
+
The specification — how a note becomes a Foundry document and a web page: three
|
|
311
|
+
frontmatter regions, a note vocabulary with its own `type` and `subType`, the
|
|
312
|
+
declared map onto each system's document fields, the precedence between a shared
|
|
313
|
+
source and a system's override, and the wikilink address grammar — lived in a
|
|
314
|
+
gitignored draft. Nothing could link to it, it had no history, and it was
|
|
315
|
+
invisible to everyone but its author while four other issues were being
|
|
316
|
+
implemented against it. It is this package's contract: `content-build` is what
|
|
317
|
+
reads notes and writes documents, so the format belongs beside the code that
|
|
318
|
+
honours it.
|
|
319
|
+
|
|
320
|
+
**The document is checkable, and two throwaway scripts written while drafting it
|
|
321
|
+
are now real commands.**
|
|
322
|
+
|
|
323
|
+
`content-build content-format schema --schema <system>=<path>` resolves every
|
|
324
|
+
`system.*` target the document names against the naming system's published
|
|
325
|
+
`schema.json`, in the `version: 1` shape `package-build schema` emits. This is
|
|
326
|
+
the emitted-versus-declared idea pointed at prose rather than at code: the format
|
|
327
|
+
does not define the `sohl:` or `hm3:` schemas — each system does, and its
|
|
328
|
+
artifact is the authority — so a mapping row is a claim, and a claim naming a
|
|
329
|
+
field no schema declares means the two disagree. There are 88 such claims today.
|
|
330
|
+
|
|
331
|
+
`content-build content-format notes` measures a content tree against the
|
|
332
|
+
per-type `data` tables, counting findings in four classes: a note type the
|
|
333
|
+
document declares no section for, an unknown key in the closed `data:` region, a
|
|
334
|
+
declared shared source written at top level instead, and one written straight
|
|
335
|
+
into a system block.
|
|
336
|
+
|
|
337
|
+
**Both read the document's own tables.** A transcribed list of targets and
|
|
338
|
+
vocabularies would be a second copy of the specification, free to drift from the
|
|
339
|
+
first the moment either was edited — the exact failure these checks exist to
|
|
340
|
+
prevent, moved one level up. No type name, field name or system name is written
|
|
341
|
+
in the code; editing the specification changes what the checks assert.
|
|
342
|
+
|
|
343
|
+
**A target resolves against the union of a system's subtypes.** The mapping
|
|
344
|
+
tables say which field a shared source reaches; _which document subtype receives
|
|
345
|
+
it_ is the note-type → subtype map, which is not built yet. Resolving per subtype
|
|
346
|
+
before that map exists would mean inferring it from the prose around each table,
|
|
347
|
+
which is the transcription the whole design avoids. It narrows when that map
|
|
348
|
+
lands.
|
|
349
|
+
|
|
350
|
+
**A system with no schema supplied is counted unchecked, not passed.** HM3
|
|
351
|
+
publishes no artifact today, so that is the ordinary case for 18 of the 88
|
|
352
|
+
claims, and a check skipping them in silence would read exactly like one that
|
|
353
|
+
passed.
|
|
354
|
+
|
|
355
|
+
**The corpus meter is a report, and `--strict` is the opt-in.** All ~6,210
|
|
356
|
+
authored notes predate the format, so a failing check would be red in every
|
|
357
|
+
repository on the day it landed and would stay red for the length of the
|
|
358
|
+
migration — a check nobody can act on and everybody learns to skip. The counts
|
|
359
|
+
are the migration's progress bar instead, and each class is promoted to fatal, by
|
|
360
|
+
turning the flag on, as it reaches zero.
|
|
361
|
+
|
|
362
|
+
Part of HeroicLands/package-build#127; closes HeroicLands/package-build#130.
|
|
363
|
+
|
|
364
|
+
**Bump**
|
|
365
|
+
|
|
366
|
+
_Minor._ A new command group, a document added to the published files, and no
|
|
367
|
+
change to any existing behaviour.
|
|
368
|
+
- 1120e3f: Close the gaps in the content format specification that the compiler can answer,
|
|
369
|
+
and add `content-build content-format fields` so the hand-written per-type tables
|
|
370
|
+
cannot drift from the declarations that compile them.
|
|
371
|
+
|
|
372
|
+
**A `### type: macro` section, written from what the compiler does.** `macro` was
|
|
373
|
+
named in the note vocabulary with no section of its own, so
|
|
374
|
+
`content-build content-format notes` reported every authored macro note as an
|
|
375
|
+
unknown type — a false finding caused by the document being incomplete rather
|
|
376
|
+
than by the note being wrong. The section states the `{#script}` anchor and its
|
|
377
|
+
three fence rules, why the executable copy is read from the raw markdown, why
|
|
378
|
+
`macroType: chat` is an error, and the two fields (`macroType`, `macroScope`) the
|
|
379
|
+
compiler reads from `sohl:` today and that belong in `data:` for the same reason
|
|
380
|
+
the map fields do. Measured against SoHL's tree, the `unknown-type` count falls
|
|
381
|
+
from 435 to 434 and no new finding appears.
|
|
382
|
+
|
|
383
|
+
**`government.model`, not `governance.model`.** The `affiliation` table carried
|
|
384
|
+
two roots for one concept — `governance.model` beside `government.summary`.
|
|
385
|
+
`government` is the established spelling: `engine/web-wikilinks.mjs` documents
|
|
386
|
+
`government.summary` as its example key path, `tests/web-wikilinks.test.ts`
|
|
387
|
+
fixtures it, and 79 authored notes in `sohl-thalorna` write `government:` while
|
|
388
|
+
none writes `governance:`. Only the odd root moved; `GovernanceModel` remains the
|
|
389
|
+
name of the value's vocabulary.
|
|
390
|
+
|
|
391
|
+
**A map note's art is `image:`, and the document now says so.** The table said
|
|
392
|
+
`img` and a standing note admitted the compiler disagreed. It does:
|
|
393
|
+
`map-notes.mjs` and `scenes.mjs` both read `image`, and all three authored map
|
|
394
|
+
notes write it. Which of the two spellings survives is a decision rather than a
|
|
395
|
+
documentation fix, so the document states what the build reads and points at the
|
|
396
|
+
issue that will settle it.
|
|
397
|
+
|
|
398
|
+
**Two stale counts corrected.** Twenty-seven fields take a `WikiLink`, not
|
|
399
|
+
"roughly forty"; thirteen of an `affiliation`'s properties describe the
|
|
400
|
+
organisation, not four. The "sixteen tables" the shared-mapping section speaks of
|
|
401
|
+
is exact — there are sixteen per-type mapping tables — and is left alone.
|
|
402
|
+
|
|
403
|
+
**The drift guard.** The specification hand-writes a `data` table under most of
|
|
404
|
+
its type sections, which is ground `engine/field-reference.mjs` already generates
|
|
405
|
+
from the `fields` on each `itemBuilders` entry. Generating the document is not
|
|
406
|
+
available: its vocabulary spans note types that produce Scenes, Macros and
|
|
407
|
+
JournalEntries, which no item registry covers. So the two are **checked** where
|
|
408
|
+
they both speak — a mapping row saying `data.weight` reaches `system.weightBase`
|
|
409
|
+
and a declaration writing `weight` to `weightBase` are one statement made twice,
|
|
410
|
+
and a rename that moves only one of them now fails, positioned at the cell in the
|
|
411
|
+
specification that makes the claim.
|
|
412
|
+
|
|
413
|
+
Everything else is reported rather than asserted, because the two vocabularies
|
|
414
|
+
differ by design until the corpus migration lands: fields only one side names come
|
|
415
|
+
back as coverage, and types only one side describes are **named** as out of reach
|
|
416
|
+
rather than skipped in silence. Against the shipped SoHL declarations it compares
|
|
417
|
+
9 types and 26 field pairs, names the 14 the format declares that no
|
|
418
|
+
`itemBuilders` entry covers and the 4 declared types the format has no section
|
|
419
|
+
for, and finds no disagreement. Wired into `npm run lint:content-format`.
|
|
420
|
+
- 7cf58f3: `contentPackage` is validated as an address segment, and `readCanonicalKey` counts segments explicitly.
|
|
421
|
+
|
|
422
|
+
A canonical address (`sohl-skill-clmb`) is read by counting hyphen-separated segments, which is sound only while the hyphen is _purely_ a separator — no segment may contain one. #59 names three charset guarantees behind that and asks for each to be **enforced rather than assumed**. Shortcodes already were; `contentPackage` was not, and its absence had a live cost: `harn-adventures` produced four-segment keys that failed as a `null` return rather than as an error saying what was wrong.
|
|
423
|
+
|
|
424
|
+
**`contentPackage` is now checked twice.** It must be alphanumeric (`^[A-Za-z0-9]+$`), and it must not equal a note type — `doc`, `being`, the map types, and every declared item type with its `doc`-prefixed documentation form. A violation is a build error in the usual `file:line:column: severity: message` form, naming the key's own line in the configuration file (#95). Every package in use today passes: `sohl`, `hm3`, `thalorna`, `kethira`, `harnensemble`, `harnadventures`.
|
|
425
|
+
|
|
426
|
+
**The shortcode rule and the package rule are one constant.** `SHORTCODE_PATTERN` now _is_ `ADDRESS_SEGMENT_PATTERN`, from the new `engine/address-charset.mjs` leaf, rather than a second copy of the same regex free to drift from it.
|
|
427
|
+
|
|
428
|
+
**`readCanonicalKey` states its premise instead of assuming it.** It counts against a named `CANONICAL_KEY_SEGMENTS`, and its documentation says the charset rule is what makes counting sound — rather than restating "nothing contains a hyphen" as a fact about the data that nothing checked. It also distinguishes its two failures: a string that cannot be a key still yields `null`, while an absent or blank input yields `undefined`. Both are falsy and all four call sites test only for truthiness, so no behaviour changes.
|
|
429
|
+
|
|
430
|
+
Deliberately **not** in this change, because they depend on decisions still open in #59: the system segment, `none`, the manifest format-version bump, partial-address resolution, and the single-hit rule. The key format is unchanged — three segments, `<package>-<type>-<shortcode>`.
|
|
431
|
+
|
|
432
|
+
Part of #127. Part of #59.
|
|
433
|
+
- 00706df: Declare the tags that classify, and say that every other tag stays open (#172).
|
|
434
|
+
|
|
435
|
+
`tags:` lives at the open top level, and most tags belong there: a theme, a
|
|
436
|
+
region, a working state is the author's own. **A tag that classifies the subject
|
|
437
|
+
is different, because something queries it.** A settlement tagged `village`
|
|
438
|
+
appears in the list of villages and an untagged one does not, so `vilage` does
|
|
439
|
+
not merely look wrong — it removes the note from an index, silently, while the
|
|
440
|
+
index still renders a table that looks complete. That is the failure the closed
|
|
441
|
+
`data:` container was introduced to end, in a region that is still open.
|
|
442
|
+
|
|
443
|
+
Four groups are declared: a place's **kind** (`city`, `town`, `village`, `port`,
|
|
444
|
+
`fortress`, `hall`, …), its **character** (`fortified`, `temple`, `market`,
|
|
445
|
+
`fishing`, `coastal`, …), its **scale** (`continent`), and a note's **state**
|
|
446
|
+
(`draft`).
|
|
447
|
+
|
|
448
|
+
**Kind and character are separate because one slot could not hold both.** The
|
|
449
|
+
single-valued field these replaced ran to 101 values over 196 notes, 72% of them
|
|
450
|
+
used exactly once, because `Fishing Village` and `Market Town / Seat of Local
|
|
451
|
+
Nobility` each had to be a value of its own — and a query for villages found two
|
|
452
|
+
of the eleven that existed.
|
|
453
|
+
|
|
454
|
+
**A continent is a region carrying a tag, not a subtype**, because structurally it
|
|
455
|
+
is a region: the same fields, the same parent chain, everything but scale.
|
|
456
|
+
|
|
457
|
+
**And the declaration is checked.** `lintFrontmatter` reports a tag that is a near
|
|
458
|
+
miss for a declared one — `vilage` for `village` — while leaving every other tag
|
|
459
|
+
alone, because the region is open and a theme or a region is the author's own.
|
|
460
|
+
The group's **scope** is what makes that sound rather than noisy: distance alone
|
|
461
|
+
was wrong on all eight notes it touched in `sohl-thalorna`, since `azravan`,
|
|
462
|
+
`barter` and `secret` each sit a typo's distance from a place tag while sitting
|
|
463
|
+
on a faith, an economy note and three lore notes. Scoped to the types each group
|
|
464
|
+
applies to, both authored trees report nothing.
|
|
465
|
+
|
|
466
|
+
**A being's station is declared too**, and it is not a rank: which kind of body a
|
|
467
|
+
person belongs to — the clergy, the soldiery, the tradesfolk — is a different
|
|
468
|
+
axis from where they stand inside one, which `data.lore` carries by naming the
|
|
469
|
+
rank. A tag holds the first, because a person may be several at once and because
|
|
470
|
+
nothing ranks `clergy` against `mages`.
|
|
471
|
+
- 215ba23: Key a being's embedded-item references and the predefined-items map on the same vocabulary, and report a reference that resolves to nothing (closes #140, part of #127).
|
|
472
|
+
|
|
473
|
+
**The defect.** Two vocabularies met in embedded-item resolution and disagreed about which one they were speaking. `Actors.loadItemsMap` keyed each predefined item by the **compiled document's** subtype (`doc.type`), while a being's frontmatter addresses its embedded items by the **note's** `type`. They are the same string in every SoHL row today, so the lookup succeeded by coincidence; the note-type → document-subtype map (#79) made the coincidence visible without creating it.
|
|
474
|
+
|
|
475
|
+
The first non-identity row breaks it. #78 introduces exactly that — `armorgear` → `armor` and its two siblings — at which point a being's `armor` reference is looked up in a map keyed `armorgear`, finds nothing, and the item is missing from the compiled actor. `harn-ensemble` alone carries 30,741 such references.
|
|
476
|
+
|
|
477
|
+
**Which vocabulary, and why that one.** The addresses stay keyed on the **document subtype**, and each authored reference is translated forward through the system's map before the lookup. The map is a function from note type to subtype by construction; the reverse is not — two note types may compile into one subtype — and a compiled document records nothing about the note that produced it, so there is no honest way to key the addresses the other way round. The translation lives in one place, `Actors#embeddedSubtype`, over a new `referencedSubtype` in `engine/document-subtypes.mjs`, and both are documented as saying which side translates rather than leaving it implied.
|
|
478
|
+
|
|
479
|
+
| a reference naming… | before | after |
|
|
480
|
+
| --------------------------------------------- | ----------------------------- | -------------------------------------- |
|
|
481
|
+
| a mapped type | looked up verbatim | looked up as the subtype the row names |
|
|
482
|
+
| a type the system does not map | looked up verbatim | unchanged — the consumer's own type |
|
|
483
|
+
| a type the system compiles into another class | resolved to nothing, silently | a finding naming the note |
|
|
484
|
+
| a one-to-many row | resolved to nothing, silently | a finding listing the candidates |
|
|
485
|
+
| a retired spelling | resolved to the old name | a finding naming the replacement |
|
|
486
|
+
|
|
487
|
+
**A stand-alone entry moved too.** An entry carrying no shortcode is built from the reference alone, so the note type became the document's subtype outright — a document of a subtype the system does not define, with nothing said. It now carries the mapped subtype, and the embedded `_id` seed is the subtype as well, so a later note-type rename leaves every embedded id exactly where it was.
|
|
488
|
+
|
|
489
|
+
**Every finding is located.** An unresolved reference is now reported at the line the reference sits on rather than at the note, in the usual `path:line:column: severity: message` form.
|
|
490
|
+
|
|
491
|
+
**Nothing compiled changes, and no tree gains a finding.** SoHL's map is the identity throughout: its 3,126 compiled pack files are byte-identical across the change. Compiled read-only against `harn-ensemble`, `sohl-thalorna`, `sohl-kethira-basic`, `harn-adventures` and `sohl`, the diagnostic output is the same finding for finding — 64 pre-existing `sohl-thalorna` findings gain a line and column, and nothing else moves.
|
|
492
|
+
- 3ea6f64: **A map is one type, and the three spellings are its subTypes.**
|
|
493
|
+
|
|
494
|
+
`docs/content-format.md` has always described a map that way, and said why:
|
|
495
|
+
_the three differ only in the canvas defaults derived for them, which is why
|
|
496
|
+
they are subTypes of one type rather than three types._ The implementation
|
|
497
|
+
declared the opposite, so a map note written to the specification was refused
|
|
498
|
+
with `no schema is declared for content type "map"`.
|
|
499
|
+
|
|
500
|
+
The three names cost three entries in the pack router, three in the claims set,
|
|
501
|
+
three in `NOTE_SCHEMAS`, and three in every consumer's `sections` config — for
|
|
502
|
+
one idea. `mapProfile()` now keys the derived canvas off `subType`, which is
|
|
503
|
+
the one thing the spellings ever decided, and `MAP_SUBTYPES` names them.
|
|
504
|
+
|
|
505
|
+
`battlemap`, `localmap` and `regionalmap` join `RETIRED_TYPES`, so a note or a
|
|
506
|
+
link still writing one is **told what to write instead** rather than routed
|
|
507
|
+
silently to the items pack — the treatment `character` and `creature` got.
|
|
508
|
+
|
|
509
|
+
**`data.place` is declared**, closing a second gap in the same table: the link
|
|
510
|
+
from a map to the place it depicts was specified and not declared, so authoring
|
|
511
|
+
it was an error. It is named on the map and not on the place, because a place
|
|
512
|
+
has several maps and a map depicts one place.
|
|
513
|
+
|
|
514
|
+
Closes #174
|
|
515
|
+
- 6d3ffa1: Stop inferring a Foundry document's subtype from the markdown note's `type`, and look it up in a map each system declares (part of #79, slice 3 of #127).
|
|
516
|
+
|
|
517
|
+
**The defect.** The two vocabularies were the same identifier for one reason: a builder wrote the same string twice. `sohl/actors.mjs` declared `ACTOR_VAULT_TYPE = "being"` and emitted `type: "being"` several hundred lines below it, under a comment reading _"One content type, named for the Foundry actor it produces."_ Nothing related them, so changing one and not the other produced a wrongly-typed document in silence — a wrong-output risk with **one** system, not only with two.
|
|
518
|
+
|
|
519
|
+
**The mechanism** is `engine/document-subtypes.mjs` and the declaration is the system's, which is the `engine/` ÷ `sohl/` line this package draws everywhere else: note-format knowledge in the engine, game-system knowledge in the system half. `sohl/document-subtypes.mjs` declares SoHL's own map — _identity rows included_. `skill` → `skill` is written out rather than derived from the item registry's keys, because deriving it is exactly the coincidence the map exists to remove.
|
|
520
|
+
|
|
521
|
+
| behaviour | before | after |
|
|
522
|
+
| -------------------------------------------------- | ----------------------------------- | ------------------------------------------- |
|
|
523
|
+
| an item's emitted subtype | `fm.type`, verbatim | the row the system declares |
|
|
524
|
+
| an actor's emitted subtype | the literal `"being"` | the row the system declares |
|
|
525
|
+
| which notes the actors pass claims | `fm.type === "being"` | every note type the map sends to an `Actor` |
|
|
526
|
+
| a type the system maps onto another document class | claimed by whichever pass got there | claimed by neither |
|
|
527
|
+
|
|
528
|
+
A markdown type with **no** row for a given system produces no document for that system — silently and correctly, exactly as the thousands of notes belonging to another pass already are. A one-to-many row is resolved by the note, which supplies the discriminator in that system's own block; an absent one is an error that names the note and lists the permitted values, never a default. SoHL has no one-to-many row, so that path is exercised against a fixture system in the suite rather than by inventing one.
|
|
529
|
+
|
|
530
|
+
**Nothing compiled changes.** Every SoHL row is the identity today, so the lookup returns what the inference returned: `sohl`'s 3,126 compiled pack files and the build's whole diagnostic output are byte-identical across the change. The renames the content format calls for (`armorgear` → `armor` and its three siblings) are #78 and stay deferred — when one lands it edits one row here and the notes that address it, which is a data change rather than a mechanism change.
|
|
531
|
+
|
|
532
|
+
Additive throughout: `itemBuilders`, the pack list and every other configured surface are untouched, and a consumer shipping an item type this system does not map keeps compiling it exactly as before.
|
|
533
|
+
- b292fba: Reduce a `place`'s `data` properties to what is true of ground, and let a map name
|
|
534
|
+
the place it depicts (#164).
|
|
535
|
+
|
|
536
|
+
Eight properties become four. Measured against the 246 authored place notes,
|
|
537
|
+
three of the eight were used by **no note at all**, one was declared as the wrong
|
|
538
|
+
type, and two were the wrong end of a relation.
|
|
539
|
+
|
|
540
|
+
**`languages` is a fact about a polity.** A place's languages change when its
|
|
541
|
+
ruler changes, which is what makes them the ruler's property; `commonSkills` on
|
|
542
|
+
the affiliation already holds them. The corpus agrees — of 206 places carrying
|
|
543
|
+
`languages`, 190 were settlements and 16 were regions, and not one was a site, a
|
|
544
|
+
structure or a feature, because a ruin has no language.
|
|
545
|
+
|
|
546
|
+
**`peoples` widens to `lore`.** It was the only lore-pointing property a place
|
|
547
|
+
had, so a place with a calendar, a body of law or a local history had nowhere to
|
|
548
|
+
cite it. The target's own subType already distinguishes a `folk` from a `law`,
|
|
549
|
+
which is the same reason `affiliation` carries no `pantheons`.
|
|
550
|
+
|
|
551
|
+
**`demonym` is a `string`**, which is what all 24 uses are and what
|
|
552
|
+
`affiliation.demonym` has always been.
|
|
553
|
+
|
|
554
|
+
**`summary` duplicated the top-level `description`** — no note carried it.
|
|
555
|
+
|
|
556
|
+
**`affiliations` and `maps` were authored from the wrong end.** `affiliations` is
|
|
557
|
+
the inverse of `affiliation.domains`, which 91 polities populate and no place
|
|
558
|
+
does; a relation authored from both ends drifts the moment one is edited. `maps`
|
|
559
|
+
moves onto the map, which gains a `place` property — optional, because an
|
|
560
|
+
encounter map depicts no named place, but that is the exception the map section
|
|
561
|
+
already describes. A place's maps are now derived: every map whose `place` is
|
|
562
|
+
this one.
|
|
563
|
+
- caea6e3: Map a note's `<system>.system` block onto the document's `system` property, and let one note carry a block per system (part of #58, slice 2 of #127).
|
|
564
|
+
|
|
565
|
+
**The rule.** A note is system-agnostic; the only system-specific things it carries are the properties named after a system. Within one, `<system>.system` maps straight onto `document.system` — the DataModel's own paths, verbatim, with no renaming layer — while `type`, `img`, `items`, `effects` and `flags` map onto their document properties and `pack` is a build directive that maps onto nothing. `archetype`, `kbcat` and the generators `items` and `attributes` are not `system` fields in any system, so they stay directly under the block.
|
|
566
|
+
|
|
567
|
+
**The shared fallback is declared, not name-matched, and that is the load-bearing part.** `sohl.system.portrait` and `hm3.system.bioImage` both default from one shared property — two real fields with different names. SoHL's `Actor.being` and HM3's `Actor.character` share **no field name at all**, so a rule matching on spelling is not a rule with exceptions; it is a rule that never fires. Each field declares its source instead, and a source may be a dotted path (`data.portrait`) as `to` already may on the destination side. Resolution for a system `S`: `S.system.<to>`, else `S.<name>` (the legacy in-block position, kept until the corpus moves off it in #126), else the declared shared property, else the field's default.
|
|
568
|
+
|
|
569
|
+
`FieldSpec.name` is reinterpreted accordingly — it is **the shared property this field draws from**, not "the frontmatter key under `sohl:`", which is the degenerate case where source and destination happen to share a name. `sohlField()` stops being the general rule; `blockField()` generalizes it to any block.
|
|
570
|
+
|
|
571
|
+
**What is checked, that was not.** A key under `<system>.system` that the system's published `schema.json` does not declare for the subtype the note compiles into is an **error naming the note**, located at the offending line — Foundry discards an unknown `system` key at construction without a word, so the alternative is a field the author wrote and nobody ever sees. Unrecognised keys under a system block are reported against **that system's** vocabulary rather than only SoHL's, and a second system's block is checked once the build declares it.
|
|
572
|
+
|
|
573
|
+
**`itemBuilders` becomes a set.** One registry is a ceiling as well as a vocabulary: the accepted types are its keys, so a type only the other system knows — `spell` and `invocation` are HM3's, `mysticalability` is SoHL's — cannot be accepted at all. A configuration may now name several (`itemBuilders: [sohl, hm3]`, or `[{ system, builders }, …]` in code) and the vocabulary is their **union**. A type both declare keeps a builder per system; `itemBuilder(type, system)` and `itemArt(type, system)` take the system that is asking, and asking without one for a contested type throws rather than answering with whichever registry was declared first. **The scalar form is unchanged** and still resolves to exactly the flat registry it always did.
|
|
574
|
+
|
|
575
|
+
**Pack eligibility.** A pack declaring a `system:` compiles only notes carrying that system's block; one that declares none constrains nothing, and a pass whose document is not system data — journals, macros, scenes — is not subject to the rule at all. A violation fails naming the note and the pack, rather than emitting a hollow document with a subtype and none of the fields that subtype exists for. `pack:` itself needed no new mechanism: the block-override rule gives it, `effects` and `flags` their per-system form for free.
|
|
576
|
+
|
|
577
|
+
**`(type, shortcode)` resolves inside one system's catalogue.** A being addresses its embedded items by that pair and never by pack, so the Item packs are read as one address space — which stops being one address space the moment two systems are in the tree, since `skill:sword` exists under both names over different data models. An Actor pass now reads the Item packs of its own system plus the system-neutral ones. The references themselves were never ambiguous; the resolver simply did not know which catalogue it was searching.
|
|
578
|
+
|
|
579
|
+
**Nothing compiled changes.** No consumer authors a `<system>.system` block yet, and no consumer's packs declare a system that its notes do not carry, so this is purely additive to real output: `sohl`, `sohl-thalorna` and `sohl-kethira-basic` all compile byte-identically, and `content-build lint` reports the same findings in the same order. A note carrying no system block at all still compiles its system-neutral documents.
|
|
580
|
+
|
|
581
|
+
The corpus migration that exercises all of this is #126, in the content repositories.
|
|
582
|
+
- b45c0f9: Report a note whose `type:` no configured pack claims, instead of compiling it into nothing in silence (#146, part of #127).
|
|
583
|
+
|
|
584
|
+
**The defect.** Every compile pass answers one question about a note — _is this mine?_ — and a note every pass answers "no" to is skipped as quietly as the thousands that legitimately belong to another pass. Where **no** pass would ever have said yes, that quiet was the whole of the report. `harn-ensemble` declares no `itemBuilders`, so its five `affiliation` notes were a type nothing selected: the journals pass rejected them, the Actor passes rejected them, and no Item pack existed to claim them. They vanished from the build with no error, no warning and no census line — while its 2,512 `being` notes each produced a routing error, which is the correct behaviour. The two cases differed only in whether some pass got far enough to complain, and the quieter one had no owner.
|
|
585
|
+
|
|
586
|
+
**The finding.** A note whose type no pack in the resolved configuration claims now fails the build, named and located at its `type:` key in the project's diagnostic form:
|
|
587
|
+
|
|
588
|
+
```text
|
|
589
|
+
assets/content/Affiliations/fff-901-pentacle.md:6:7: error: no configured pack claims a note of type "affiliation", so it compiles into nothing. The "sohl" system compiles it into an Item, but `packs:` declares no Item pack and no `itemBuilders` registry declares "affiliation" — declare both in package-build.config.yaml, or stop authoring the type.
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**Two conditions, two fixes.** The **vocabulary** — what this toolchain and the systems it ships know a note type to be — is deliberately wider than any one repository's configuration. `affiliation` is a SoHL Item however a given repository is configured, so a tree of `affiliation` notes with no Item pack behind them is a repository that has not finished configuring itself, and the message says which piece is missing. A type in **no** vocabulary is the other finding — nothing anywhere compiles it, so the fix is the note's `type:`, not the configuration. Collapsing the two would have sent `harn-ensemble` to correct five perfectly good notes.
|
|
593
|
+
|
|
594
|
+
**#79's silence is preserved, and is why the question is asked once.** A markdown type with no mapping in a given system produces no document _for that system_, silently and correctly. A per-pass check would report every such type against every system that does not map it, which is exactly the noise that rule forbids — so the question is put once, to the whole configured pack list, and "no system claims it at all" is the only statement made. A type one system maps and another does not stays silent as long as some pack claims it.
|
|
595
|
+
|
|
596
|
+
`engine/note-claims.mjs` holds the claim table, which restates each pass's `selects` in the only form that can be asked of a pack the configuration does **not** declare; the suite compares the two for every type in the vocabulary, so they cannot drift apart. `homepage` is exempt by name: it compiles into a page rather than a compendium document, and its absence from every pack is the intended state.
|
|
597
|
+
|
|
598
|
+
**Measured before it became an error.** Against every content tree in the org, this adds findings to exactly one repository and exactly the notes it was filed for: `harn-ensemble` 2514 → 2519 errors, the five `affiliation` notes; `sohl-thalorna` 150 → 150; `sohl-kethira-basic` 0 → 0; `harn-adventures` 2 → 2; `Song-of-Heroic-Lands-FoundryVTT` 0 → 0, with its 3,126 compiled pack files and its whole diagnostic output byte-identical across the change.
|
|
599
|
+
|
|
600
|
+
**Bump**
|
|
601
|
+
|
|
602
|
+
_Minor._ No consumer that builds green today has to change anything to upgrade — the one repository that gains findings is already red for an unrelated reason, and its five findings are the defect this exists to surface rather than a new demand on it. A repository that was silently shipping nothing for a type will now be told so, which is the correction.
|
|
603
|
+
- ad0cfd8: A map note's background art is `img:`, and `image:` is retired (#142).
|
|
604
|
+
|
|
605
|
+
Every note type names its artwork `img` and carries it at the note's **top
|
|
606
|
+
level**. A map alone named it `image` and read it out of the `sohl:` block, so
|
|
607
|
+
one idea had two spellings with nothing to reconcile them — and the content
|
|
608
|
+
format specification had to hedge rather than state a rule.
|
|
609
|
+
|
|
610
|
+
**`img` is now the name, at the top level.** `buildScene` and the place index
|
|
611
|
+
read `img` through `sohlField`, so it resolves the way every other note's art
|
|
612
|
+
does: the `sohl:` block first, then the note's own top level, which is where it
|
|
613
|
+
belongs. Art is not system-specific — a Scene is a core Foundry document and a
|
|
614
|
+
second system would want the identical one — so the field has no business inside
|
|
615
|
+
a system block. `docs/content-format.md` states that rule now, in place of the
|
|
616
|
+
callout that recorded the disagreement and pointed at this issue.
|
|
617
|
+
|
|
618
|
+
**`image:` still compiles, and is reported.** This is the first of the three
|
|
619
|
+
steps `package:` took (#56): both spellings are read, `img` wins where a note
|
|
620
|
+
carries both, and a note still writing `image` gets a finding naming the file,
|
|
621
|
+
the line and the replacement. The sweep of the authored notes and the eventual
|
|
622
|
+
refusal are separate, later work — nothing has to be renamed to take this
|
|
623
|
+
release.
|
|
624
|
+
|
|
625
|
+
**The finding is a warning, not an error.** A note writing `image` compiles to
|
|
626
|
+
the byte-identical document, so failing a build over it would red a tree that
|
|
627
|
+
has done nothing wrong on a key that still works. It is emitted on both paths an
|
|
628
|
+
author meets — the **compile**, which every consumer runs, and `content-build
|
|
629
|
+
lint` — so it is not a lint-only notice a project might never see.
|
|
630
|
+
|
|
631
|
+
Two consequences worth knowing:
|
|
632
|
+
|
|
633
|
+
| what | before | after |
|
|
634
|
+
| ------------------------ | ---------------------- | ------------------------------------------------------ |
|
|
635
|
+
| `content-build lint` | any finding set exit 1 | only an **error** does; warnings are reported and pass |
|
|
636
|
+
| a map with no art at all | "needs an `image`" | "needs an `img`" |
|
|
637
|
+
|
|
638
|
+
The exit-code change is `reportFindings`' existing rule applied to the lint
|
|
639
|
+
command rather than a second copy of it. Every finding was an error until now,
|
|
640
|
+
so it changed nothing the day it landed.
|
|
641
|
+
|
|
642
|
+
Verified against the `sohl` content tree, whose three map notes still write
|
|
643
|
+
`image`: 3,126 compiled documents, byte-identical before and after, plus the
|
|
644
|
+
three warnings.
|
|
645
|
+
|
|
646
|
+
### Patch Changes
|
|
647
|
+
|
|
648
|
+
- 291f5b3: Declare `section` on a `doc` of subType `collection` (#170).
|
|
649
|
+
|
|
650
|
+
The address engine already reads it — a collection is a section's landing page,
|
|
651
|
+
and `section` is the URL segment that page occupies — but the specification never
|
|
652
|
+
declared it, so nothing could check it and an author had no way to learn it
|
|
653
|
+
existed. Fifteen notes in `sohl-thalorna` carry it.
|
|
654
|
+
|
|
655
|
+
Two things the declaration says that the code alone did not. **Two collections may
|
|
656
|
+
not claim the same segment**, so a collection listing a _subset_ of a section
|
|
657
|
+
names none and falls back to its own slug: five of that tree's collections list
|
|
658
|
+
places and three list affiliations, and they cannot all be `/place/`. And it is
|
|
659
|
+
**authored rather than derived** because a note's title is presentation — a
|
|
660
|
+
collection called "Creatures" heads the `being` section, and slugging the title
|
|
661
|
+
would put it somewhere else.
|
|
662
|
+
- d6a6b0b: Route a `doc` note by its subtype, not by the retired `category` key (#168).
|
|
663
|
+
|
|
664
|
+
`sectionOf()` read `fm.category`, and a `doc` is the one type that routes by its
|
|
665
|
+
subtype label rather than by its type — so when the content format retired
|
|
666
|
+
`category`, every `doc` note began answering `undefined` and, as the function's
|
|
667
|
+
own documentation says, _a `doc` with no section has no address and is not
|
|
668
|
+
published_. `sohl-thalorna` has 24 such notes and `sohl` has 128. The site build
|
|
669
|
+
emitted fewer pages and exited 0.
|
|
670
|
+
|
|
671
|
+
Two further call sites read the same key: `landingOf()` tested
|
|
672
|
+
`category === "collection"`, so a collection stopped being a landing page and its
|
|
673
|
+
authored `section` — which is its URL segment — was ignored; and `site-build`
|
|
674
|
+
fell back to the tree's section rather than the note's.
|
|
675
|
+
|
|
676
|
+
Four tests now cover a `doc` of each subtype, and one asserts that a note
|
|
677
|
+
carrying the retired key gets no section at all — the regression was visible only
|
|
678
|
+
by reading the source, which is what a test is for.
|
|
679
|
+
- 42c03b6: **`content-build format --write` now formats to a fixpoint** (#125)
|
|
680
|
+
|
|
681
|
+
`--write` formatted each file exactly once and reported success. Prettier's
|
|
682
|
+
`format` is _assumed_ idempotent and is not guaranteed to be, so a single pass
|
|
683
|
+
could leave text the next pass would still change — and the run would call such
|
|
684
|
+
a file formatted while `prettier --check` still rejected it. Each file is now
|
|
685
|
+
formatted repeatedly until it stops changing, capped at three passes, so what
|
|
686
|
+
lands on disk is what a second run would have produced.
|
|
687
|
+
|
|
688
|
+
**A file that will not converge is reported, not written.** At the cap the file
|
|
689
|
+
is left exactly as it was and a diagnostic names it, because a formatting the
|
|
690
|
+
command cannot reproduce would otherwise churn the file on every run.
|
|
691
|
+
|
|
692
|
+
**`--write` now surfaces its findings and fails.** It collected them and threw
|
|
693
|
+
them away, so a run that could not parse a file still printed
|
|
694
|
+
`Formatted N of M file(s).` and exited 0. It now emits each diagnostic and exits
|
|
695
|
+
1, which is the same channel the non-convergence report uses.
|
|
696
|
+
|
|
697
|
+
## 9.0.0
|
|
698
|
+
|
|
699
|
+
### Major Changes
|
|
700
|
+
|
|
701
|
+
- 8e48f7e: Raise the shared `printWidth` from 80 to 100.
|
|
702
|
+
|
|
703
|
+
**Breaking for every consumer**, in the one way this configuration always is: a
|
|
704
|
+
repository that updates and runs `lint:format` will find every file reported
|
|
705
|
+
until it reformats. Nothing about a built package, an emitted document or a
|
|
706
|
+
manifest changes — this is hygiene, and it reaches users not at all.
|
|
707
|
+
|
|
708
|
+
**80 was inherited, not chosen.** The usual argument for it is reading measure,
|
|
709
|
+
and that argument does not apply here: `proseWrap` is left at Prettier's default
|
|
710
|
+
of `preserve`, so authored prose is **never reflowed**. Measured across the
|
|
711
|
+
content trees, prose lines run to a p90 of 378 characters and a maximum of
|
|
712
|
+
5,531, entirely untouched by this number. What `printWidth` actually governs is
|
|
713
|
+
TypeScript and the YAML of a note's frontmatter.
|
|
714
|
+
|
|
715
|
+
**Both were measurably cramped.** Reformatting a third of the SoHL source at
|
|
716
|
+
each width:
|
|
717
|
+
|
|
718
|
+
| width | total lines | vs 80 | lines still over width |
|
|
719
|
+
| ----- | ----------- | ----- | ---------------------- |
|
|
720
|
+
| 80 | 31,198 | — | **1,399** |
|
|
721
|
+
| 90 | 30,328 | −2.8% | 193 |
|
|
722
|
+
| 100 | 29,672 | −4.9% | 75 |
|
|
723
|
+
| 120 | 28,850 | −7.5% | 25 |
|
|
724
|
+
|
|
725
|
+
The last column is the argument. At 80, Prettier _cannot_ honour the limit on
|
|
726
|
+
1,399 lines — long string literals, `@src/…` specifiers, generic signatures — so
|
|
727
|
+
those lines are over-width regardless and their surroundings were broken up for
|
|
728
|
+
nothing. At 100 that falls to 75.
|
|
729
|
+
|
|
730
|
+
The same knee appears in content. An item entry written in flow style —
|
|
731
|
+
`{ shortcode: X, type: skill, name: …, system: { … } }` — typically lands in the
|
|
732
|
+
low 90s, so of 318,030 entries across the two largest trees, 90.8% fit on one
|
|
733
|
+
line at 80 and **95.3% at 100**, with almost nothing gained in between. 120 buys
|
|
734
|
+
another 0.8 points and is not worth a second reformat.
|
|
735
|
+
|
|
736
|
+
**This package's own config stops restating the values.** `prettier.config.js`
|
|
737
|
+
carried its own copy of all twelve options with a comment saying they were
|
|
738
|
+
"matched to" the SoHL repository — already the wrong authority once
|
|
739
|
+
`PRETTIER_BASE` existed here, and a copy is a copy: raising the width would have
|
|
740
|
+
left the package that _defines_ the shared style as the one repository not
|
|
741
|
+
written in it. It now re-exports `PRETTIER_CONFIG`, so there is nothing left to
|
|
742
|
+
drift.
|
|
743
|
+
|
|
744
|
+
This release reformats this repository: 147 files, 2,677 lines shorter.
|
|
745
|
+
|
|
746
|
+
**Bump**
|
|
747
|
+
|
|
748
|
+
_Major._ Consumers' `lint:format` fails until they reformat, which is the whole
|
|
749
|
+
of the breakage and is a one-command fix.
|
|
750
|
+
|
|
3
751
|
## 8.1.0
|
|
4
752
|
|
|
5
753
|
### Minor Changes
|