@heroiclands/package-build 17.1.0 → 18.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 +1773 -0
- package/CONTENT.md +288 -72
- package/README.md +1 -1
- package/bin/content-build.mjs +218 -137
- package/bin/package-build.mjs +43 -0
- package/content-config.mjs +62 -67
- package/docs/content-format.md +768 -58
- package/engine/actor-compiler.mjs +586 -0
- package/engine/address-charset.mjs +11 -8
- package/engine/address-diff.mjs +266 -22
- package/engine/anchored-sections.mjs +83 -0
- package/engine/anchors.mjs +83 -0
- package/engine/base-compiler.mjs +148 -18
- package/engine/bundle-notes.mjs +276 -0
- package/engine/bundles.mjs +307 -0
- package/engine/code-fences.mjs +103 -0
- package/engine/compile-corpus.mjs +89 -0
- package/engine/content-address.mjs +267 -5
- package/engine/content-format-check.mjs +54 -11
- package/engine/content-format.mjs +37 -5
- package/engine/content-index.mjs +394 -111
- package/engine/content-links.mjs +185 -77
- package/engine/content-lint.mjs +232 -14
- package/engine/content-package.mjs +2 -1
- package/engine/content-tables.mjs +99 -3
- package/engine/document-subtypes.mjs +49 -6
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +56 -6
- package/engine/folder-notes.mjs +470 -0
- package/engine/foreign-catalog.mjs +195 -4
- package/engine/{manifest-emit.mjs → foundry-entries.mjs} +78 -81
- package/engine/frontmatter-lint.mjs +245 -19
- package/engine/frontmatter.mjs +88 -8
- package/engine/generate.mjs +304 -13
- package/engine/helpers.mjs +293 -64
- package/engine/ids.mjs +137 -2
- package/engine/index-records.mjs +126 -0
- package/engine/index.mjs +23 -5
- package/engine/item-compiler.mjs +349 -0
- package/engine/item-docs.mjs +7 -1
- package/engine/item-registry.mjs +6 -0
- package/engine/journals.mjs +93 -27
- package/engine/macros.mjs +4 -2
- package/engine/metadata-index.mjs +495 -0
- package/engine/note-claims.mjs +146 -25
- package/engine/note-ids.mjs +112 -0
- package/engine/note-renames.mjs +134 -0
- package/engine/note-schemas.mjs +25 -0
- package/engine/note-vocabulary.mjs +110 -27
- package/engine/pack-config.mjs +4 -0
- package/engine/pack-router.mjs +23 -0
- package/engine/retired-fields.mjs +137 -3
- package/engine/scenes.mjs +66 -34
- package/engine/schema-check.mjs +58 -17
- package/engine/site-build.mjs +71 -12
- package/engine/site-index.mjs +20 -7
- package/engine/sql-tables.mjs +485 -0
- package/engine/subtype-registry.mjs +102 -0
- package/engine/system-block.mjs +96 -12
- package/engine/systems.mjs +201 -0
- package/engine/web-wikilinks.mjs +50 -20
- package/engine/wikilink-syntax.mjs +17 -8
- package/engine/wikilinks.mjs +124 -51
- package/engine/yaml-lint.mjs +282 -0
- package/hm3/actors.mjs +352 -0
- package/hm3/default-item-art.mjs +75 -0
- package/hm3/document-subtypes.mjs +134 -0
- package/hm3/index.mjs +56 -0
- package/hm3/item-builders.mjs +84 -0
- package/hm3/item-fields.mjs +180 -0
- package/hm3/items.mjs +96 -0
- package/hm3/template-priority.mjs +85 -0
- package/labels.mjs +191 -0
- package/manifest.mjs +43 -2
- package/package.json +19 -4
- package/release.mjs +62 -7
- package/sohl/actors.mjs +33 -487
- package/sohl/being-info.mjs +16 -7
- package/sohl/default-item-art.mjs +14 -3
- package/sohl/document-subtypes.mjs +16 -10
- package/sohl/item-builders.mjs +14 -5
- package/sohl/item-fields.mjs +68 -7
- package/sohl/items.mjs +44 -258
- package/sohl/note-schemas.mjs +2 -2
- package/types/content-config.d.mts +21 -42
- package/types/engine/actor-compiler.d.mts +204 -0
- package/types/engine/address-charset.d.mts +11 -8
- package/types/engine/address-diff.d.mts +53 -5
- package/types/engine/anchored-sections.d.mts +21 -0
- package/types/engine/anchors.d.mts +20 -0
- package/types/engine/base-compiler.d.mts +17 -17
- package/types/engine/bundle-notes.d.mts +173 -0
- package/types/engine/bundles.d.mts +60 -0
- package/types/engine/code-fences.d.mts +43 -0
- package/types/engine/compile-corpus.d.mts +32 -0
- package/types/engine/content-address.d.mts +205 -5
- package/types/engine/content-format-check.d.mts +6 -2
- package/types/engine/content-format.d.mts +57 -1
- package/types/engine/content-index.d.mts +64 -80
- package/types/engine/content-links.d.mts +52 -7
- package/types/engine/content-lint.d.mts +10 -1
- package/types/engine/content-package.d.mts +2 -1
- package/types/engine/content-tables.d.mts +21 -39
- package/types/engine/document-subtypes.d.mts +37 -3
- package/types/engine/field-spec.d.mts +76 -5
- package/types/engine/folder-notes.d.mts +159 -0
- package/types/engine/foreign-catalog.d.mts +53 -0
- package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +16 -38
- package/types/engine/frontmatter-lint.d.mts +10 -2
- package/types/engine/frontmatter.d.mts +64 -0
- package/types/engine/generate.d.mts +38 -0
- package/types/engine/helpers.d.mts +94 -30
- package/types/engine/ids.d.mts +96 -0
- package/types/engine/index-records.d.mts +68 -0
- package/types/engine/index.d.mts +9 -3
- package/types/engine/item-compiler.d.mts +131 -0
- package/types/engine/journals.d.mts +47 -9
- package/types/engine/metadata-index.d.mts +226 -0
- package/types/engine/note-claims.d.mts +57 -11
- package/types/engine/note-ids.d.mts +38 -0
- package/types/engine/note-renames.d.mts +102 -0
- package/types/engine/note-vocabulary.d.mts +44 -8
- package/types/engine/retired-fields.d.mts +75 -0
- package/types/engine/scenes.d.mts +3 -2
- package/types/engine/schema-check.d.mts +25 -4
- package/types/engine/site-build.d.mts +4 -4
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +185 -0
- package/types/engine/subtype-registry.d.mts +49 -0
- package/types/engine/system-block.d.mts +40 -1
- package/types/engine/systems.d.mts +106 -0
- package/types/engine/web-wikilinks.d.mts +4 -2
- package/types/engine/wikilink-syntax.d.mts +10 -3
- package/types/engine/wikilinks.d.mts +41 -13
- package/types/engine/yaml-lint.d.mts +107 -0
- package/types/hm3/actors.d.mts +48 -0
- package/types/hm3/default-item-art.d.mts +42 -0
- package/types/hm3/document-subtypes.d.mts +24 -0
- package/types/hm3/index.d.mts +7 -0
- package/types/hm3/item-builders.d.mts +11 -0
- package/types/hm3/item-fields.d.mts +12 -0
- package/types/hm3/items.d.mts +23 -0
- package/types/hm3/template-priority.d.mts +21 -0
- package/types/manifest.d.mts +22 -1
- package/types/release.d.mts +9 -4
- package/types/sohl/actors.d.mts +7 -74
- package/types/sohl/being-info.d.mts +8 -27
- package/types/sohl/default-item-art.d.mts +5 -3
- package/types/sohl/items.d.mts +17 -32
- package/engine/foreign-manifests.mjs +0 -126
- package/engine/kb-manifest.mjs +0 -490
- package/types/engine/foreign-manifests.d.mts +0 -43
- package/types/engine/kb-manifest.d.mts +0 -241
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1778 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 18.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 0d15342: **The cross-package link manifest is gone.** A package now publishes its own
|
|
8
|
+
content index and a consumer fetches the ones it depends on (#239).
|
|
9
|
+
|
|
10
|
+
The manifest was vendored — every repository committed a copy of every other
|
|
11
|
+
repository's file — and that failed three ways, none of them fixable by
|
|
12
|
+
tightening it.
|
|
13
|
+
|
|
14
|
+
**A copy goes stale silently, and one was.**
|
|
15
|
+
`Song-of-Heroic-Lands-FoundryVTT` carried 2,101 `thalorna` entries whose address
|
|
16
|
+
was the old name-derived form, so every cross-package link it rendered pointed
|
|
17
|
+
at a URL the site had stopped publishing. The format-version gate saw nothing,
|
|
18
|
+
because the format had not changed — only the values were wrong.
|
|
19
|
+
|
|
20
|
+
**Mutual vendoring deadlocks.** SoHL vendored thalorna and thalorna vendored
|
|
21
|
+
SoHL, so a format bump stopped both builds until the other had already
|
|
22
|
+
published: neither could go first.
|
|
23
|
+
|
|
24
|
+
**It was a second answer to a settled question.** The content index already
|
|
25
|
+
carried the canonical key, the name, the anchors, the Foundry `uuid` and the web
|
|
26
|
+
address. There was nothing in a manifest entry it did not hold.
|
|
27
|
+
|
|
28
|
+
**What replaces it**
|
|
29
|
+
|
|
30
|
+
- Every package emits `<package>-metadata.jsonl` and advertises it as
|
|
31
|
+
`flags.metadataUrl`, pinned to the version being built.
|
|
32
|
+
- The release publishes it as a third asset, named by that URL so the file
|
|
33
|
+
shipped and the URL advertised cannot disagree.
|
|
34
|
+
- `content-build deps fetch` pulls each dependency's index into
|
|
35
|
+
`build/cache/metadata`, reading the `manifest` URL the relationship already
|
|
36
|
+
declares and taking `flags.metadataUrl` from it. The whole chain is declared;
|
|
37
|
+
nothing holds an address of its own.
|
|
38
|
+
- Cross-package links resolve from that cache.
|
|
39
|
+
|
|
40
|
+
**The dependency set is every declared dependency** — everything in
|
|
41
|
+
`relationships.systems` and `relationships.requires` — and deliberately _not_
|
|
42
|
+
only those declaring `itemCatalog: true`. Citing another package's addresses and
|
|
43
|
+
embedding its items are separate edges: `harn-ensemble` cites no foreign address
|
|
44
|
+
and carries 324,016 embedded item references. `recommends` and `conflicts` are
|
|
45
|
+
not dependencies and are not fetched.
|
|
46
|
+
|
|
47
|
+
**Removed:** `engine/kb-manifest.mjs`, `engine/foreign-manifests.mjs`, the
|
|
48
|
+
`content-build manifest` command, its `--manifests` options, and the
|
|
49
|
+
configuration keys `paths.manifests`, `paths.manifestOut` and
|
|
50
|
+
`publish.manifests`. `engine/manifest-emit.mjs` becomes
|
|
51
|
+
`engine/foundry-entries.mjs`, which is what it always did — `manifest.mjs` and
|
|
52
|
+
`kb-manifest.mjs` both exported `buildManifest` meaning different things, and
|
|
53
|
+
that ends here.
|
|
54
|
+
|
|
55
|
+
**Kept, and moved:** the address grammar — `canonicalKey`, `readCanonicalKey`,
|
|
56
|
+
`CANONICAL_KEY_SEGMENTS`, `PACKAGE_BASE` and the URL helpers — now lives in
|
|
57
|
+
`engine/content-address.mjs` beside `addressSlug`. It was never the manifest's;
|
|
58
|
+
deleting the module without splitting it would have taken the address form too.
|
|
59
|
+
|
|
60
|
+
**Two behaviours moved rather than vanished.** A manifest-completeness gate no
|
|
61
|
+
longer exists: a declared dependency with no fetched index is a hard error
|
|
62
|
+
naming `deps fetch`, so past the load everything is accounted for. And whether a
|
|
63
|
+
package serves pages is now the consumer's `PACKAGE_BASE` rather than the
|
|
64
|
+
producer's entries — a package with no configured base stays citable by UUID and
|
|
65
|
+
simply yields no URL, which is what `kethira` needs.
|
|
66
|
+
|
|
67
|
+
**Consumers must act.** Declare your dependencies in `relationships`, run
|
|
68
|
+
`content-build deps fetch` before a build that resolves cross-package links, and
|
|
69
|
+
delete `assets/manifests/`. A configuration still naming `publish.manifests` or
|
|
70
|
+
`paths.manifests` will be rejected as an unknown key.
|
|
71
|
+
- e5f40c7: **A document's id is derived from its identity, not authored and not keyed on a
|
|
72
|
+
list position.** Three ids changed how they are computed (#270, #268), and every
|
|
73
|
+
compiled id this toolchain emits is affected.
|
|
74
|
+
|
|
75
|
+
**1. A note's document `_id` derives from its canonical address (#270).**
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
_id = makeId("document", "<package>-<system>-<type>-<shortcode>")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`id:` becomes **optional**. A note used to author one — an opaque 16-character
|
|
82
|
+
string, 6,343 of them across the four content trees — that said nothing its
|
|
83
|
+
address did not, could not be read or reviewed, and was guaranteed by nothing:
|
|
84
|
+
`content-lint` refuses a duplicate **address** across every pack of a document
|
|
85
|
+
type, which is exactly the scope a primary document's id must be unique within,
|
|
86
|
+
and it said nothing at all about a duplicate `id`. The derived id inherits a
|
|
87
|
+
guard that already exists. It is the same principle that turned
|
|
88
|
+
`folder: ONXsqZAIZr2qzxTb` into `packFolder: <path>` (#251, #252).
|
|
89
|
+
|
|
90
|
+
**An authored `id` always wins, and pinning one is how a document keeps its
|
|
91
|
+
identity across a shortcode rename.** That is the pattern the map compiler
|
|
92
|
+
already used (`regionDocId(sceneId, key, pinned)`), now applied to primary
|
|
93
|
+
documents too.
|
|
94
|
+
|
|
95
|
+
**2. An actor's embedded item is keyed on its identity, not its index (#268).**
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
_id = makeId(<actor id>, "<subType>:<system.shortcode>")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
An entry's identity is its **own `system.shortcode`**. The entry's top-level
|
|
102
|
+
`shortcode` is a _selector_ naming the template it is written from, is never
|
|
103
|
+
written to the document, and may repeat. **Two entries resolving to one identity
|
|
104
|
+
are now a build error naming both** — the case a position key used to hide by
|
|
105
|
+
compiling them to two documents denoting one entity.
|
|
106
|
+
|
|
107
|
+
**3. An unanchored journal page is keyed on its heading, not its index (#268).**
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
_id = makeId("journal-page", "<entry id>:<name>")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Two sibling pages sharing a heading is likewise a build error, matching the
|
|
114
|
+
`MD024` lint rule that already refuses it. An anchored page is unchanged — it
|
|
115
|
+
never took an index, and is the shape the other two now share.
|
|
116
|
+
|
|
117
|
+
**Why position was wrong.** A Foundry id is how a world refers to a document it
|
|
118
|
+
imported. Keying on position meant reordering a being's item list, or inserting
|
|
119
|
+
a heading into a note, silently renumbered every id after the change — so a
|
|
120
|
+
re-import created new documents beside the old ones. Nothing about those
|
|
121
|
+
documents had changed; only their neighbours had.
|
|
122
|
+
|
|
123
|
+
**What this costs, measured against a real corpus** (`sohl`, 1,606 notes →
|
|
124
|
+
3,094 documents):
|
|
125
|
+
|
|
126
|
+
| id | count | moved |
|
|
127
|
+
| ------------------------- | ----- | ----- |
|
|
128
|
+
| primary documents | 3,094 | **0** |
|
|
129
|
+
| embedded items | 1,337 | 1,337 |
|
|
130
|
+
| journal pages, anchored | 296 | 0 |
|
|
131
|
+
| journal pages, unanchored | 1,648 | 1,648 |
|
|
132
|
+
|
|
133
|
+
No primary id moved **because every note in that tree still authors one**, and a
|
|
134
|
+
pin wins. #270 is inert until a tree drops its authored ids; #268 lands at once.
|
|
135
|
+
Stripping all 1,605 authored ids compiles with no new errors, derives every id
|
|
136
|
+
exactly as the formula above states, and leaves all 5,712 internal `@UUID`
|
|
137
|
+
references as consistent as before.
|
|
138
|
+
|
|
139
|
+
**Consumers must act.**
|
|
140
|
+
|
|
141
|
+
- **Every embedded item id and every unanchored page id changes once.** A world
|
|
142
|
+
that imported these documents will see new ones on re-import.
|
|
143
|
+
- **Removing a tree's authored `id:`s changes every compendium UUID that package
|
|
144
|
+
publishes**, once. Internal references are regenerated in the same build and
|
|
145
|
+
stay consistent with each other; anything outside — a GM's world, a macro, a
|
|
146
|
+
module — will not. Do it deliberately, and per tree.
|
|
147
|
+
- **Fix any duplicate embedded identity first.** Measured across the four
|
|
148
|
+
corpora: 20 entries in 17 notes, every one a defect. `sohl` has 1
|
|
149
|
+
(`Characters/Aldrik_Harvenar.md` carries `skill/swim` twice);
|
|
150
|
+
`sohl-thalorna` has 19; `harn-ensemble` and `sohl-kethira-basic` have none.
|
|
151
|
+
|
|
152
|
+
**One diagnostic narrows.** `content-build addresses` tells a **rename** from a
|
|
153
|
+
**withdrawal** by matching document ids across releases. That rested on the id
|
|
154
|
+
being independent of the shortcode; it no longer is, so for a note that pins no
|
|
155
|
+
`id` both sides move together and a rename is reported as a withdrawal with no
|
|
156
|
+
successor named. It never reports a _wrong_ successor, and stays exact for a
|
|
157
|
+
pinned note.
|
|
158
|
+
|
|
159
|
+
**"Has an id" no longer means "has a compendium document."** Every addressable
|
|
160
|
+
note derives one now, so the types that compile into no _single_ document say so
|
|
161
|
+
themselves rather than resting on an absent `id:`. A **homepage** publishes no
|
|
162
|
+
UUID because it is in no pack, and a **folder** because it may be in several —
|
|
163
|
+
it materialises in every pack holding a document that references it (#276), and
|
|
164
|
+
its id is hashed under the `folder` namespace against its own address. Either
|
|
165
|
+
one would otherwise have published an `Item` UUID at an id no document carries.
|
|
166
|
+
|
|
167
|
+
**Also:** `assertUniqueAnchors` is renamed `assertUniquePages` (the old name
|
|
168
|
+
remains as a deprecated alias) and now checks page names as well as anchors;
|
|
169
|
+
`journalPageId(entryId, page)` no longer takes an index.
|
|
170
|
+
- dcff2f9: **The SoHL passes emit `system.templatePriority`, not `system.archetype`**
|
|
171
|
+
(#266).
|
|
172
|
+
|
|
173
|
+
The read half landed already: `templatePriority` is what a note authors, and
|
|
174
|
+
`archetype` is read only as the retiring spelling. The **emitted** key stayed
|
|
175
|
+
behind, so a compiled document still carried the old name — and that half cannot
|
|
176
|
+
move on its own schedule, because the receiving schema and the emitted key have
|
|
177
|
+
to agree. `Song-of-Heroic-Lands-FoundryVTT#1836` renames the data-model field;
|
|
178
|
+
this is the other side of that single change.
|
|
179
|
+
|
|
180
|
+
**Why it is breaking.** Foundry discards an undeclared `system` key at
|
|
181
|
+
construction _without a warning_, so a build emitting `archetype` into a system
|
|
182
|
+
that declares `templatePriority` reports success and ships documents whose
|
|
183
|
+
priority is silently gone. The emitted-versus-declared check catches exactly this
|
|
184
|
+
and fails the pack build, which is what makes the pairing enforced rather than
|
|
185
|
+
hoped for: **a consumer must take this release together with a SoHL that declares
|
|
186
|
+
`system.templatePriority`** (0.8.4 or later). Taking one without the other fails
|
|
187
|
+
the build with a message naming the field, rather than shipping quietly broken
|
|
188
|
+
packs.
|
|
189
|
+
|
|
190
|
+
`resolveArchetype` and `systemArchetype` are renamed `resolveTemplatePriority`
|
|
191
|
+
and `systemTemplatePriority`; the generated field-reference example authors
|
|
192
|
+
`templatePriority: null`, since the linter now refuses the old spelling in the
|
|
193
|
+
example it tells authors to copy.
|
|
194
|
+
|
|
195
|
+
The HM3 pass is unaffected — it already wrote `flags.hm3.templatePriority`.
|
|
196
|
+
- 79d9503: A top-level `traits:` block is refused (#291).
|
|
197
|
+
|
|
198
|
+
#128 moved a being's description — `gender`, `species`, `age`, `birthday`, `height`,
|
|
199
|
+
`weight`, `frame` and `appearance.*` — out of a top-level `traits:` block and into
|
|
200
|
+
the closed `data:` container the content format declares. **2,533 notes across four
|
|
201
|
+
repositories** have landed, and every content tree now carries zero. This is the third
|
|
202
|
+
and last step of that retirement.
|
|
203
|
+
|
|
204
|
+
**Refusing it matters more than refusing an ordinary dead key.** Top level is
|
|
205
|
+
_deliberately open_: an unrecognised key there is passed straight through to Hugo. So
|
|
206
|
+
a stray `traits:` would not be ignored loudly — it would arrive on the published page
|
|
207
|
+
as a theme parameter, checked by nothing, reading to its author as though it still
|
|
208
|
+
worked. The whole argument for `data:` being closed is the argument for refusing this.
|
|
209
|
+
|
|
210
|
+
The message states the **mapping**, not just the destination, because three of the
|
|
211
|
+
keys reshaped as well as moved: `traits.height.m` → `data.height` (metres),
|
|
212
|
+
`traits.weight.kg` → `data.weight` (kilograms), `traits.build.frame` → `data.frame`.
|
|
213
|
+
A bare _"write `data:` instead"_ would send an author to write `data.height: {m: 1.78}`
|
|
214
|
+
— a declared key holding an undeclared shape.
|
|
215
|
+
|
|
216
|
+
**`sohl.traits` is untouched.** It is a different field that shares the name —
|
|
217
|
+
`projectilegear` declares one and the theme's gear sidebar reads it — so the refusal
|
|
218
|
+
is anchored at column 1 and never reaches inside a system block.
|
|
219
|
+
|
|
220
|
+
Refused from the same two compile paths as `draft:`, `aliases:` and `section:`, and
|
|
221
|
+
reported by the frontmatter lint alongside them.
|
|
222
|
+
- d17dc98: **A `dataview` query that selects no notes is now a build error** (#223).
|
|
223
|
+
|
|
224
|
+
A zero-row table publishes as a bare header and a rule, so a stale query — a
|
|
225
|
+
renamed type, a retired category, a typo'd path — looked exactly like a category
|
|
226
|
+
that is legitimately empty. Both builds emitted it and neither said a word.
|
|
227
|
+
|
|
228
|
+
Where a table is meant to be empty, say so on the fence — `dataview allow-empty`
|
|
229
|
+
in place of `dataview`. The opt-in sits on the fence rather than in the query
|
|
230
|
+
because it is a statement about the directive, not part of the query language.
|
|
231
|
+
The table is still rendered either way: the finding is the point, not
|
|
232
|
+
withholding the output.
|
|
233
|
+
|
|
234
|
+
The finding names the note, the line of the block and **the clause that matched
|
|
235
|
+
nothing**, quoted as authored, because that is the string the author will edit.
|
|
236
|
+
|
|
237
|
+
**The site build's table findings are compiler-parseable too.** They were prose
|
|
238
|
+
with a timestamp where a parser reads the path, so one authored table produced a
|
|
239
|
+
machine-readable diagnostic from the pack build and something ungreppable from
|
|
240
|
+
the site. Both now emit `file:line:column: error: message`.
|
|
241
|
+
|
|
242
|
+
**Major, because a tree carrying a dead table goes red on adoption.**
|
|
243
|
+
`Song-of-Heroic-Lands-FoundryVTT` carries **40**, across eight notes — including
|
|
244
|
+
the eight `type = "creature"` tables in `Rules/Bestiary.md` that the issue was
|
|
245
|
+
filed about, a `sohl.kbcat = "birthsign"` query for a retired concept, and a
|
|
246
|
+
`contains(file.tags, "religous")` that is simply a misspelling. None of them has
|
|
247
|
+
published a row in months.
|
|
248
|
+
- 43a1f4d: **The address grammar is strict, and omission runs left to right** (#59).
|
|
249
|
+
|
|
250
|
+
```text
|
|
251
|
+
[[[[<package>-]<system>-]<type>-]<shortcode>]
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The written forms are exactly the suffixes of the canonical address —
|
|
255
|
+
`type-shortcode`, `system-type-shortcode`, `package-system-type-shortcode` — and
|
|
256
|
+
**`package-type-shortcode` is not one of them**. A link into another package must
|
|
257
|
+
now be fully qualified. That is the price of positional segments, and the
|
|
258
|
+
alternative is a parser that needs a vocabulary to tell a package from a system.
|
|
259
|
+
|
|
260
|
+
`readQualifier` counted at most three segments and never read a system, so it
|
|
261
|
+
accepted a package with its system omitted and every authored target resolved
|
|
262
|
+
system-blind. It now counts segments and assigns each position its field, the
|
|
263
|
+
same rule `readCanonicalKey` follows.
|
|
264
|
+
|
|
265
|
+
**`sohl` is both a package and a system**, and counting is what makes that
|
|
266
|
+
harmless: three segments name a _system_ whatever the first segment could also
|
|
267
|
+
have meant, and four is the full form. Nothing has to guess which sense was
|
|
268
|
+
written.
|
|
269
|
+
|
|
270
|
+
**A stated system is now matched, not merely parsed.** A target naming a system
|
|
271
|
+
resolves against the segments it supplied rather than falling back to the
|
|
272
|
+
system-blind short key, so `[[hm3-skill-clmb]]` no longer silently resolves to
|
|
273
|
+
a `sohl` note.
|
|
274
|
+
|
|
275
|
+
**A hyphenated shortcode is no longer read.** The old parser split at the _first_
|
|
276
|
+
hyphen so a shortcode could contain one (`trauma-self-pro`); #1397 made every
|
|
277
|
+
segment `^[A-Za-z0-9]+$`, and the two rules cannot both hold. The charset rule
|
|
278
|
+
wins — it is enforced, and no tree has used the tolerance: 138,204 authored
|
|
279
|
+
shortcodes across four content trees, none carrying a separator.
|
|
280
|
+
|
|
281
|
+
**Verified against real content, not fixtures.** `content-build links` over
|
|
282
|
+
`Song-of-Heroic-Lands-FoundryVTT` (1,606 notes) and `sohl-thalorna` (1,852 notes,
|
|
283
|
+
which links into SoHL) reports the _same_ findings before and after — 43 and 122
|
|
284
|
+
respectively, identical file sets, no new failure. No authored link in any of the
|
|
285
|
+
four trees uses the retired form.
|
|
286
|
+
|
|
287
|
+
Two messages follow the grammar: an ambiguity now asks for the fully qualified
|
|
288
|
+
form rather than a package-qualified one that would not parse, and a labelled
|
|
289
|
+
non-address names both the local and the cross-package spelling.
|
|
290
|
+
- d17dc98: **A content-index record's Foundry address is keyed by the system that compiles
|
|
291
|
+
it.** `foundry.uuid` becomes `foundry.<system>.uuid` for a document a _game
|
|
292
|
+
system_ defines — an Item or an Actor — while a document the _note format_
|
|
293
|
+
defines keeps the unkeyed form.
|
|
294
|
+
|
|
295
|
+
```json
|
|
296
|
+
"foundry": { "sohl": { "uuid": "Compendium.sohl.items.Item.…" } } // an item
|
|
297
|
+
"foundry": { "none": { "uuid": "Compendium.sohl.journals.JournalEntry.…" } } // a journal
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The vocabulary is the specification's — `sohl`, `hm3`, and **`none`** for a note
|
|
301
|
+
that belongs to no system, the same value the canonical address carries in its
|
|
302
|
+
`<system>` segment (`harnadventures-none-being-grod`, #59). Every record is
|
|
303
|
+
keyed, so "belongs to no system" and "nobody filled this in" are not the same
|
|
304
|
+
shape.
|
|
305
|
+
|
|
306
|
+
**A note may declare more than one system**, and each compiles into its own
|
|
307
|
+
document, of that system's type, in that system's pack: 2,497 of
|
|
308
|
+
`harn-ensemble`'s notes carry both a `sohl:` and an `hm3:` block, and its
|
|
309
|
+
`packs:` declares an `actors-sohl` _and_ an `actors-hm3`. One `uuid` on the
|
|
310
|
+
record cannot name two documents — it named whichever the single shipped map
|
|
311
|
+
produced and said nothing about the other.
|
|
312
|
+
|
|
313
|
+
Only `sohl` can appear today, because `KNOWN_DOCUMENT_SUBTYPE_MAPS` holds one
|
|
314
|
+
map and #139 tracks the missing `hm3/` half. **The shape changes now so that
|
|
315
|
+
adding it is one more key rather than a second breaking change** to an artifact
|
|
316
|
+
consumers have already started reading.
|
|
317
|
+
|
|
318
|
+
**A journal, a macro or a scene is `none`.** Those are the note format's own
|
|
319
|
+
documents rather than a system's, and the documentation journal an item
|
|
320
|
+
compiles beside itself is `none` too — it is one journal however many systems
|
|
321
|
+
the item declares. On `sohl` that is 1,474 records keyed `sohl` and 1,514
|
|
322
|
+
keyed `none`.
|
|
323
|
+
|
|
324
|
+
The uuid _values_ are unchanged: still equal to the link manifest's, verified
|
|
325
|
+
across `sohl`'s 2,988 addresses with no mismatch.
|
|
326
|
+
|
|
327
|
+
**Not synthesized onto the `sohl:`/`hm3:` blocks themselves.** Those are regions
|
|
328
|
+
a note authors, and `DERIVED_KEYS` — which refuses a note that writes over
|
|
329
|
+
derived data — reaches only the top level. A note authoring `sohl.uuid` would
|
|
330
|
+
collide silently, which is the failure this index exists to prevent.
|
|
331
|
+
|
|
332
|
+
### Minor Changes
|
|
333
|
+
|
|
334
|
+
- ad813e1: **`lint:addresses` reads the content index too** (#243) — the second reader
|
|
335
|
+
converted, and the last of the two lints the issue names.
|
|
336
|
+
|
|
337
|
+
`addresses diff` reads the tree twice: once for the renames notes declare, once
|
|
338
|
+
to place its findings against the note that made them. Those were two
|
|
339
|
+
independent walks, each parsing every note, each answering "which files are the
|
|
340
|
+
corpus?" for itself. They are now **one** derivation — `indexRecordsFor`,
|
|
341
|
+
enumerated once by the command and handed to both — so the two halves of a
|
|
342
|
+
single command cannot disagree about the corpus, or about the ids in it.
|
|
343
|
+
|
|
344
|
+
**A latent defect goes with it, and the id is the reason it mattered.**
|
|
345
|
+
`noteFilesById` joins tree-side ids against ids read out of the _compiled
|
|
346
|
+
packs_. Since #270 an id is derived from the canonical address, whose first
|
|
347
|
+
segment is the content package — and the tree side derived it through
|
|
348
|
+
`resolveNoteId(fm)` with no package, which falls back to `contentPackage()` and
|
|
349
|
+
so to whichever configuration the working directory answers with. The compiled
|
|
350
|
+
side is produced by a compiler running on the configuration the _build_
|
|
351
|
+
resolved. Let those differ — under `PACKAGE_BUILD_CONFIG`, in a worktree, in a
|
|
352
|
+
test — and **every id fails to join**: every rename degrades to a withdrawal, and
|
|
353
|
+
every finding loses the note it should have been reported against. The
|
|
354
|
+
configuration is now passed in and both sides derive from the one that was
|
|
355
|
+
resolved.
|
|
356
|
+
|
|
357
|
+
**It is also faster, which is the shape of the win.** Two whole-tree walks
|
|
358
|
+
became one derivation: over `sohl`'s 1,685 notes the pair of reads goes from
|
|
359
|
+
about 2.8s to about 1.4s. Both maps are byte-identical to what the walks
|
|
360
|
+
produced — 1,685 ids, and the declared predecessors of five renames spanning
|
|
361
|
+
five item types, including the `projectilegear`/`missilegear` pair that a
|
|
362
|
+
declaration keys under both maps.
|
|
363
|
+
|
|
364
|
+
The scope requirement moves to a shared `assertStatedScope`, so a reader of the
|
|
365
|
+
content index refuses an unstated scope in the same words `walkMarkdownTree`
|
|
366
|
+
does. A pass reading the index makes the identical claim about which files it is
|
|
367
|
+
looking at and must be held to the identical rule; a quiet default there would
|
|
368
|
+
reintroduce the second answer #243 removed.
|
|
369
|
+
|
|
370
|
+
`declaredPredecessors` and `noteFilesById` take `config` and already-derived
|
|
371
|
+
`records`. Where the walk yielded nothing for a tree that is not there, so do
|
|
372
|
+
they.
|
|
373
|
+
- 9e4861c: An affiliation records what it answers to, where it sits, and what it holds sway
|
|
374
|
+
over (SoHL#1781).
|
|
375
|
+
|
|
376
|
+
**`relation` is now `relations`.** The field holds a _map_ of standings, one per
|
|
377
|
+
affiliation — its own description said so, and `resolveRelation` has always read it
|
|
378
|
+
that way. The singular named the many as one.
|
|
379
|
+
|
|
380
|
+
The retired spelling is still read, underneath the current one, and reported through
|
|
381
|
+
`RETIRED_FIELD_ALIASES` — the same three steps `img`/`image` and
|
|
382
|
+
`templatePriority`/`archetype` take. `relations` wins wherever a note writes both,
|
|
383
|
+
and the thrown message names whichever spelling the note actually used. Only
|
|
384
|
+
`affiliation` declares the field, so `relation` stays an ordinary unknown key on
|
|
385
|
+
every other type.
|
|
386
|
+
|
|
387
|
+
**Three fields are new**, and each was unexpressible before: the content format
|
|
388
|
+
specified them and no declaration could receive them.
|
|
389
|
+
|
|
390
|
+
| authored | emitted | |
|
|
391
|
+
| --------- | ---------------- | ---------------------------------------------------------------------------------------------------- |
|
|
392
|
+
| `parents` | `system.parents` | The bodies it is subordinate to. A list, because an affiliation may sit under more than one at once. |
|
|
393
|
+
| `seat` | `system.seat` | Where its authority sits. |
|
|
394
|
+
| `domains` | `system.domain` | The places it holds sway over. |
|
|
395
|
+
|
|
396
|
+
`seat` is deliberately not `capital` or `headquarters` — each fits about half the
|
|
397
|
+
eleven subTypes, while a seat covers a polity, a guild, an order and a faith alike.
|
|
398
|
+
|
|
399
|
+
`domains` → `domain` is **authored plural, emitted singular**, which is what the
|
|
400
|
+
format's own mapping row states. The declaration carries both spellings so neither
|
|
401
|
+
side has to guess, and a note that authored `domain` would otherwise have compiled
|
|
402
|
+
to nothing.
|
|
403
|
+
|
|
404
|
+
`parents` and `domains` are separate because they are different relations —
|
|
405
|
+
_subordinate to_ against _holds sway over_ — and an earlier `parent.regions` grouped
|
|
406
|
+
the geographic one under the organisational one while also naming regions where a
|
|
407
|
+
guild's domain may be a single town.
|
|
408
|
+
|
|
409
|
+
Both lists ship `[]` rather than null: _refers to nothing_ is a value here — a
|
|
410
|
+
sovereign polity answers to nobody — not an absence. Blank rows, which a cleared
|
|
411
|
+
property editor leaves behind, are dropped rather than emitted.
|
|
412
|
+
|
|
413
|
+
**This needs the schema half to land with it.** SoHL declares `system.relations`,
|
|
414
|
+
`system.parents`, `system.seat` and `system.domain` in the same wave; an emitted key
|
|
415
|
+
the data model does not define is discarded at construction without a warning.
|
|
416
|
+
- 1274ff8: **The markdown note vocabulary drops the `gear` suffix** — `armorgear` → `armor`,
|
|
417
|
+
`concoctiongear` → `concoction`, `projectilegear` → `projectile` (#78).
|
|
418
|
+
|
|
419
|
+
Those three named the **SoHL document subtype** a note happened to compile into rather
|
|
420
|
+
than the thing the note is about. A note's `type` sits outside the `sohl:` and `hm3:`
|
|
421
|
+
blocks precisely because it belongs to no system, and HM3 already compiles a projectile
|
|
422
|
+
into a `missilegear` — so the suffix was never a fact about the note. `weapongear` keeps
|
|
423
|
+
its name: both systems call that document a `weapongear`, so it says nothing
|
|
424
|
+
system-specific. `armorlocation`, which maps to an HM3 Item and to nothing in SoHL at
|
|
425
|
+
all, is a first-class note type: the vocabulary is system-agnostic, not a list of what
|
|
426
|
+
SoHL happens to define.
|
|
427
|
+
|
|
428
|
+
**Nothing in a compiled pack moves.** The emitted document subtypes are unchanged — an
|
|
429
|
+
`armor` note still compiles into an `armorgear` Item — so no world, no compendium and no
|
|
430
|
+
`_id` is affected. Verified by recompiling `Song-of-Heroic-Lands-FoundryVTT` (331
|
|
431
|
+
`armorgear` and 18 `projectilegear` notes) and `sohl-thalorna` (71 `concoctiongear`) with
|
|
432
|
+
the released toolchain and with this one, and diffing every emitted document: 3,091 and
|
|
433
|
+
2,605 files respectively, byte-identical, with the content trees untouched.
|
|
434
|
+
|
|
435
|
+
**Both spellings are read, and the retired one is reported — not refused.** This is the
|
|
436
|
+
first of the three steps `package:` took, and the rule `RETIRED_FIELD_ALIASES` already
|
|
437
|
+
states for a renamed _field_: a note carrying the old spelling compiles into exactly the
|
|
438
|
+
document it always did, so failing a build over it would red a tree that has done nothing
|
|
439
|
+
wrong. There are some 31,000 references to move, overwhelmingly `(type, shortcode)`
|
|
440
|
+
entries inside a being's `items:` list, and a consumer must be able to adopt the new
|
|
441
|
+
toolchain before its content does. `content-build lint` reports each one as a **warning**
|
|
442
|
+
naming the file, the line and what to write instead; the sweep and the refusal come after.
|
|
443
|
+
|
|
444
|
+
**Both sides of a reference resolve.** A being's embedded `(type, shortcode)` reference
|
|
445
|
+
carries the note vocabulary too, and those outnumber notes' own `type:` keys by roughly a
|
|
446
|
+
thousand to one — so `referencedSubtype` normalises alongside the note's own type. A
|
|
447
|
+
window that resolved notes but not references would have dropped 30,000 embedded items in
|
|
448
|
+
silence.
|
|
449
|
+
|
|
450
|
+
**Two lookups where the vocabularies now genuinely differ**, and both are translated
|
|
451
|
+
rather than joined by name:
|
|
452
|
+
|
|
453
|
+
| lookup | keyed by | what changed |
|
|
454
|
+
| --------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
455
|
+
| `sohl/default-item-art.mjs` | document subtype | `SohlItem.getDefaultArtwork` reads it with a Foundry `Item`'s own `type`, so the map stays on the document side and `sohl/item-builders.mjs` translates before asking |
|
|
456
|
+
| the published-schema check | document subtype | `compareFields` gets the `subtypeOf` seam it was given for exactly this, so `armorgear`'s findings still fire |
|
|
457
|
+
|
|
458
|
+
**Planning a sweep? Two things to move with the frontmatter.** A note's canonical address
|
|
459
|
+
— and therefore its document `_id` — carries its `type` as authored, so renaming a note's
|
|
460
|
+
type moves the address it publishes at. Every wikilink into it and every content-table
|
|
461
|
+
query naming the type (`WHERE type = "armorgear"`) has to move in the same change, and a
|
|
462
|
+
package that _links into_ another has to move with it.
|
|
463
|
+
- d17dc98: **A `#section` link is checked by the build that emits it, not only by the
|
|
464
|
+
checker** (#193). `content-build links` reported a dead anchor as an error while
|
|
465
|
+
the pack compilers hashed _any_ slug into a `JournalEntryPage` id and emitted a
|
|
466
|
+
`@UUID` for it — so a link the checker refused still compiled, and dead-ended
|
|
467
|
+
for the reader.
|
|
468
|
+
|
|
469
|
+
A **foreign** anchor was already checked, because a vendored manifest publishes
|
|
470
|
+
an `anchors` map. A local one was not, for the reason #193 gives: neither index
|
|
471
|
+
held the set. The walk that builds the link index yields each note's body and
|
|
472
|
+
nothing read it.
|
|
473
|
+
|
|
474
|
+
It reads it now, through the one anchor reader, and reports `unknown-anchor`
|
|
475
|
+
with the message the foreign path already used — nothing new is named.
|
|
476
|
+
|
|
477
|
+
An index built without anchors still says nothing, which is deliberate: it
|
|
478
|
+
cannot answer the question, and answering it wrongly is what this fixes.
|
|
479
|
+
|
|
480
|
+
`collectAnchors` moves to `engine/anchors.mjs`, a module that imports nothing.
|
|
481
|
+
It has to: the link checker, the content index and the compilers all ask this
|
|
482
|
+
question, and `helpers.mjs` is imported by the compilers while the index imports
|
|
483
|
+
the manifest emitter, which imports them back. A leaf is what lets all three
|
|
484
|
+
share one reader instead of two disagreeing ones.
|
|
485
|
+
- b67a786: **A `bundle` note compiles into a Foundry `Adventure`** (#259).
|
|
486
|
+
|
|
487
|
+
#263 specified and declared the type and left it uncompiled — authoring one said
|
|
488
|
+
so, in as many words — because two decisions came first, neither answerable from
|
|
489
|
+
the specification. Both are settled here, and the pass exists.
|
|
490
|
+
|
|
491
|
+
**Which pack.** Not the `adventures` **companion**. The scenes pass already
|
|
492
|
+
writes one Adventure per pinned place into that pack, and a companion is written
|
|
493
|
+
by its parent pass rather than routed to — the router refuses a note that
|
|
494
|
+
addresses one, and is right to. So a bundle lands in an ordinary Adventure pack,
|
|
495
|
+
routed and defaulted exactly as items and actors are, and the place-adventures
|
|
496
|
+
companion is left alone. A repository that authors bundles declares an Adventure
|
|
497
|
+
pack of its own; one that declares none is now told so by name, because `bundle`
|
|
498
|
+
is in the type table the unclaimed-note check reads.
|
|
499
|
+
|
|
500
|
+
**What a `contents` address names: the note's own document.** That is already
|
|
501
|
+
the router's rule for `pack:`, so there is one answer and not two. A note that
|
|
502
|
+
compiles into _two_ documents — an item and the JournalEntry its prose became —
|
|
503
|
+
puts the second in a bundle only when the bundle names it by its own `doc…`
|
|
504
|
+
address. Nothing is inferred; an address that resolves to nothing fails the
|
|
505
|
+
build, and a `folder` address is refused with a message of its own, since a
|
|
506
|
+
folder materialises in every pack holding something filed in it and so has no
|
|
507
|
+
single copy to take.
|
|
508
|
+
|
|
509
|
+
**An Adventure holds copies, not references**, so `contents` resolves against
|
|
510
|
+
_compiled output_ rather than against the content tree. The pass therefore reads
|
|
511
|
+
every other one, and **says so** — `Bundles.readsPackOutputOf` names Item,
|
|
512
|
+
Actor, JournalEntry, Macro and Scene, from which the generator derives the
|
|
513
|
+
compile order. An Adventure pack declared first in `packs:` still compiles last.
|
|
514
|
+
|
|
515
|
+
**A pack's `system:` constrains what its Adventures may hold**, and the
|
|
516
|
+
constraint is read from what the pack can see rather than computed from a
|
|
517
|
+
member's type — which has no single answer for the types both systems map, most
|
|
518
|
+
of them. A pack declaring `system: hm3` reads the HM3 packs and the neutral
|
|
519
|
+
ones, so a member publishing no HM3 document is **left out rather than
|
|
520
|
+
failing**, with a warning naming it. A pack declaring no system scopes nothing
|
|
521
|
+
away, so a member it cannot find is a dead address and fails.
|
|
522
|
+
|
|
523
|
+
**The note's prose becomes the Adventure's `description`** — the `HTMLField`
|
|
524
|
+
Foundry renders on the import card. A bundle is something you hand someone, so
|
|
525
|
+
its prose belongs on the document itself, which is why it earns no separate
|
|
526
|
+
documentation journal the way an item does. That was #263's third open question.
|
|
527
|
+
|
|
528
|
+
**A prebuilt pack is passed over rather than compiled.** Its per-document JSON
|
|
529
|
+
is checked in, so it has no pass and no note is routed into it — which
|
|
530
|
+
`content-config.mjs` already said by refusing `default: true` beside `prebuilt`.
|
|
531
|
+
It could not matter before: the only prebuilt pack in the wild holds Adventures,
|
|
532
|
+
and no compiler was registered for that document type, so it failed with "no
|
|
533
|
+
compiler for document type" whatever it was asked. Now one is registered, and
|
|
534
|
+
running a pass over it would wipe its generated JSON directory, write nothing,
|
|
535
|
+
and then report the empty pass as an error.
|
|
536
|
+
- 458fd89: **A note declares the shortcode it used to be published under, so a rename stops
|
|
537
|
+
reading as a withdrawal (#278).**
|
|
538
|
+
|
|
539
|
+
`(type, shortcode)` is a published interface — every satellite declaring
|
|
540
|
+
`itemCatalog: true` assembles its beings out of those addresses — and
|
|
541
|
+
`content-build addresses diff` exists to report what a build stopped publishing
|
|
542
|
+
before a release does. To be useful it has to name where an address _went_, and
|
|
543
|
+
it told a **rename** from a **withdrawal** by matching document ids across two
|
|
544
|
+
releases.
|
|
545
|
+
|
|
546
|
+
#270 removed the property that rested on. An id is now derived from the
|
|
547
|
+
canonical address, which carries the shortcode, so renaming a shortcode moves the
|
|
548
|
+
id too: both sides of the join move together, the match finds nothing, and the
|
|
549
|
+
rename is reported as a withdrawal with no successor named. It stayed exact for a
|
|
550
|
+
note that **pins** an `id` — but a pin has to be written _before_ the rename, by
|
|
551
|
+
an author who does not yet know they will make one.
|
|
552
|
+
|
|
553
|
+
An author who has just renamed a shortcode does know, so they say so:
|
|
554
|
+
|
|
555
|
+
```yaml
|
|
556
|
+
type: weapongear
|
|
557
|
+
shortcode: Taburi
|
|
558
|
+
renamedFrom: Tabri
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
**One shortcode or a list**, because renames chain and a released baseline may
|
|
562
|
+
know an address by a name two renames ago. **Transient**: once every baseline a
|
|
563
|
+
build is compared against post-dates the rename, the declaration may be deleted —
|
|
564
|
+
which is what separates it from an `id:` pin, which is permanent. **One key per
|
|
565
|
+
note, at the top level**, however many systems the note compiles into, since a
|
|
566
|
+
shortcode is the note's rather than a system block's.
|
|
567
|
+
|
|
568
|
+
**The diagnostic reports which join it had**, because the two are not equally
|
|
569
|
+
checkable — a matched id is a fact a reader can verify in both artefacts, while a
|
|
570
|
+
declaration is the author's word:
|
|
571
|
+
|
|
572
|
+
```text
|
|
573
|
+
since sohl@0.8.2, weapongear:Tabri is no longer published; the note now
|
|
574
|
+
published as weapongear:Taburi declares it was renamed from Tabri. Every
|
|
575
|
+
package that resolves weapongear:Tabri breaks when it moves past sohl@0.8.2
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Three joins are tried, in that order of authority: the document id, then a
|
|
579
|
+
declaration, then nothing — which remains **withdrawn**. Nothing infers a
|
|
580
|
+
successor from a similar-looking string; a wrong one sends the reader to the
|
|
581
|
+
wrong fix.
|
|
582
|
+
|
|
583
|
+
**`content-lint` holds a declaration to the rules a current address is held to.**
|
|
584
|
+
An entry must be a well-formed shortcode, must not be the note's own, and must
|
|
585
|
+
name an address the package actually vacated: an entry naming an address some
|
|
586
|
+
note still publishes is refused, as are two notes claiming one predecessor, since
|
|
587
|
+
an address had one holder and so has one successor. A repeated entry is a
|
|
588
|
+
warning — the declaration still works.
|
|
589
|
+
|
|
590
|
+
**Also fixed: `addresses diff` threw whenever it had a finding to place.** It
|
|
591
|
+
called `noteFilesById` without `skipDirectories`, which `walkMarkdownTree`
|
|
592
|
+
refuses (#243), so the command worked only when it had nothing to report — the
|
|
593
|
+
one path nobody notices. Both of its tree reads now state the scope from the
|
|
594
|
+
resolved configuration.
|
|
595
|
+
- 4926fb8: **The `bundle` note type is specified and declared** (#259).
|
|
596
|
+
|
|
597
|
+
A bundle is a set of documents taken as a unit — Foundry's `Adventure`, named for
|
|
598
|
+
what it is rather than what Foundry calls it. It declares one property of its
|
|
599
|
+
own, `contents`: the documents it holds.
|
|
600
|
+
|
|
601
|
+
**How many Adventures a bundle makes is decided by its system blocks**, as for
|
|
602
|
+
every other type, rather than by a property of its own. With no system block it
|
|
603
|
+
is one Adventure holding only the `none` documents; with one or more it is one
|
|
604
|
+
Adventure per system, each holding every `none` document plus that system's own,
|
|
605
|
+
and a document of neither is silently left out.
|
|
606
|
+
|
|
607
|
+
Each Adventure is written to the pack the note's `pack` names — **the shared
|
|
608
|
+
routing field, not a property of the bundle**, so there is one spelling and not
|
|
609
|
+
two. It differs only in its default, `adventures`; `<system>.pack` overrides it
|
|
610
|
+
per system exactly as it does everywhere else.
|
|
611
|
+
|
|
612
|
+
That follows from Foundry rather than from taste: **an `Adventure` has no
|
|
613
|
+
`system` field**. A bundle spanning two systems cannot be one document that knows
|
|
614
|
+
it spans them, so it is one document per system and the pack each is written to
|
|
615
|
+
is what carries the system.
|
|
616
|
+
|
|
617
|
+
**It is not a folder**, and the difference is the whole point: an Adventure
|
|
618
|
+
carries **copies**, and importing one creates or updates each document in the
|
|
619
|
+
world, after which they live independently. A folder is a live grouping, by
|
|
620
|
+
reference, that persists in the pack.
|
|
621
|
+
|
|
622
|
+
**Nothing compiles a bundle yet**, and authoring one says so. Two decisions come
|
|
623
|
+
first: the scenes pass already writes an Adventure per place into a _companion_
|
|
624
|
+
pack and the router refuses a note naming a companion in `pack:`, so the
|
|
625
|
+
`adventures` default cannot be that pack as things stand; and since an Adventure
|
|
626
|
+
holds compiled documents rather than references, `contents` has to resolve after
|
|
627
|
+
the passes that produce them.
|
|
628
|
+
- 0df2870: **Every declared note type is routed, or excused for a stated reason** (#243,
|
|
629
|
+
#241) — asserted statically, of the toolchain, so it no longer depends on some
|
|
630
|
+
repository happening to author the type.
|
|
631
|
+
|
|
632
|
+
This is the check #241 needed and nobody had. `place`, `lore` and `scenario`
|
|
633
|
+
were declared, validated, and claimed by no pass; every gate reported success,
|
|
634
|
+
and the only thing that noticed was a downstream repository failing to compile
|
|
635
|
+
450 notes. The claim table was already cross-checked against each pass's
|
|
636
|
+
`selects`, but that agreement holds just as well when **both** say nobody claims
|
|
637
|
+
a type — which was exactly the broken state. The missing property is not
|
|
638
|
+
agreement, it is **coverage**.
|
|
639
|
+
|
|
640
|
+
A declared type must now be one of four things, and the four name different
|
|
641
|
+
reasons rather than being interchangeable: claimed by a pass; **never packed**
|
|
642
|
+
(compiles to no document — `homepage`); **derived packed** (materialises by
|
|
643
|
+
reference in every pack that references it, so no one pass owns it — `folder`);
|
|
644
|
+
or declared by a shipped **system map**, so a configuration carrying that
|
|
645
|
+
system's packs claims it (`armorlocation`, which is HM3's).
|
|
646
|
+
|
|
647
|
+
**`UNIMPLEMENTED_TYPES` is new, and it is stated rather than inferred.** An
|
|
648
|
+
unimplemented type and a forgotten one look identical from outside: documented,
|
|
649
|
+
validating, reaching no pass. Only intent separates them, so intent is written
|
|
650
|
+
down. The obvious inference — "declared, but absent from the configured
|
|
651
|
+
vocabulary" — reads correctly and is worthless, because that vocabulary is
|
|
652
|
+
_derived from the routing_: take a type's route away and it leaves the
|
|
653
|
+
vocabulary too, so the inference excuses precisely the mistake it was meant to
|
|
654
|
+
catch. That was verified by putting `place` back into its #241 state, where the
|
|
655
|
+
inferred form passed and the stated form fails.
|
|
656
|
+
|
|
657
|
+
`vehicle` is its one member, and `unclaimedNoteFindings` now chooses its
|
|
658
|
+
"specified, not implemented" wording from the same set rather than from a second
|
|
659
|
+
reading of the same fact.
|
|
660
|
+
|
|
661
|
+
_No behaviour change: over `sohl`'s tree the compile emits the same 3,085
|
|
662
|
+
documents with identical diagnostics, and `lint` reports the same 354 findings._
|
|
663
|
+
- adb2e90: **A folder is a note** (#256), and `packFolder:` names one by **address** (#255).
|
|
664
|
+
|
|
665
|
+
```yaml
|
|
666
|
+
---
|
|
667
|
+
type: folder
|
|
668
|
+
shortcode: possessionscooking
|
|
669
|
+
name:
|
|
670
|
+
full: Cooking
|
|
671
|
+
data:
|
|
672
|
+
parent: possessionsmiscgear
|
|
673
|
+
color: "#7a4b2a"
|
|
674
|
+
---
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
```yaml
|
|
678
|
+
packFolder: possessionscooking # on any note that files itself there
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
A `Folder` was the last document this package compiled from bespoke
|
|
682
|
+
configuration — `*-folders.yaml`, five files per tree — rather than from a note.
|
|
683
|
+
That was the one hole in the rule #243 establishes, _the compiler follows the
|
|
684
|
+
index_: a pass cannot follow the index for things the index does not contain.
|
|
685
|
+
|
|
686
|
+
**`parent` is an address**, so a dangling one is an ordinary dead-address finding
|
|
687
|
+
rather than a special-cased `Unknown folder id`, and a cycle is refused. Both are
|
|
688
|
+
reported when the tree is read, not when something happens to reference the
|
|
689
|
+
folder that carries them. A folder is addressed `<package>-none-folder-<shortcode>`
|
|
690
|
+
— `none`, because a `Folder` is a core Foundry document like a `JournalEntry`,
|
|
691
|
+
not a system's.
|
|
692
|
+
|
|
693
|
+
**`parent` may be a map keyed by pack.** A folder's identity is one thing and its
|
|
694
|
+
hierarchy is another: the same folder is deliberately filed under different
|
|
695
|
+
parents in different packs, and both large trees rely on it. This repository
|
|
696
|
+
files its three item roots one level deeper in the journals pack (under
|
|
697
|
+
`Rules/Descriptions`, beside `Rules/Combat`); `sohl-thalorna` groups the items
|
|
698
|
+
pack by document kind and the journals pack by setting geography, and 46 of its
|
|
699
|
+
75 shared folders differ. A scalar — the everyday spelling — is exactly
|
|
700
|
+
`{ default: <value> }`, and the folder keeps one id across every pack whatever
|
|
701
|
+
its parent there.
|
|
702
|
+
|
|
703
|
+
**Where a folder materialises is derived from what references it** (#257). Every
|
|
704
|
+
pack holding a document that names a folder gets that folder, and its ancestors
|
|
705
|
+
with it; a folder nothing references materialises nowhere.
|
|
706
|
+
|
|
707
|
+
That removes a live defect rather than reporting it. A documentation journal is
|
|
708
|
+
filed beside the item it describes by putting the item's folder id into the
|
|
709
|
+
_journals_ pack — which only worked where a second folder file mirrored the
|
|
710
|
+
first, and it mirrored in one tree of three:
|
|
711
|
+
|
|
712
|
+
| tree | item folders | in journal folders | missing |
|
|
713
|
+
| --------------------------------- | -----------: | -----------------: | -------------: |
|
|
714
|
+
| `Song-of-Heroic-Lands-FoundryVTT` | 57 | 57 | 0 |
|
|
715
|
+
| `sohl-thalorna` | 132 | 75 | **57** |
|
|
716
|
+
| `sohl-kethira-basic` | 6 | — | **6**, no file |
|
|
717
|
+
|
|
718
|
+
Both emitted documentation journals into folders their own pack never declared,
|
|
719
|
+
silently. With one folder note and one address there is no second file to
|
|
720
|
+
disagree with the first, so the failure is unrepresentable rather than merely
|
|
721
|
+
caught.
|
|
722
|
+
|
|
723
|
+
**A folder's Foundry `_id` is derived from its address** (#258), stable across
|
|
724
|
+
runs, so a new folder needs no invented id. An **authored `id` is kept** where
|
|
725
|
+
one is present — which is what lets a tree sweep its folder YAML into notes
|
|
726
|
+
without a world that already holds those folders losing them, making this a build
|
|
727
|
+
change rather than a world migration. Two folders claiming one id is a build
|
|
728
|
+
error.
|
|
729
|
+
|
|
730
|
+
**`packFolder` was a path for one release and never shipped as one.** #252 landed
|
|
731
|
+
`Possessions/Misc_Gear/Cooking` and its changeset is still pending, so no
|
|
732
|
+
released version ever read a path. A path encoded the hierarchy _in the value_,
|
|
733
|
+
so reparenting a folder rewrote every note naming it; an address is stable under
|
|
734
|
+
reparenting, which is why a note is addressed by `(type, shortcode)` and never by
|
|
735
|
+
`file.path`. The path form is removed rather than deprecated — it had no authors
|
|
736
|
+
to migrate, which is the whole reason the change was cheap enough to make.
|
|
737
|
+
|
|
738
|
+
**`folder:` is untouched**, and every tree still compiles from its
|
|
739
|
+
`*-folders.yaml` exactly as before: the SoHL tree's 3,094 compiled documents are
|
|
740
|
+
byte-identical across this change. Retiring the id spelling and the YAML schema
|
|
741
|
+
is #260, after each tree has swept.
|
|
742
|
+
- 72ce6c2: **The HM3 pass reads the template priority wherever a note states it** (#266).
|
|
743
|
+
|
|
744
|
+
`flags.hm3.templatePriority` was resolved as an ordinary declared field, whose
|
|
745
|
+
shared source is a **single** position — so only a bare top-level
|
|
746
|
+
`templatePriority` ever answered. The one position that did not work was
|
|
747
|
+
`data.templatePriority`: the specified home, the target of the settled mapping
|
|
748
|
+
table, and the home this pass's own docstring already claimed to read.
|
|
749
|
+
|
|
750
|
+
**It failed silently, and could only fail silently.** A note that is not a
|
|
751
|
+
template writes no flag, so an omitted flag is how "not a template" is spelled —
|
|
752
|
+
which makes a priority that was lost and a priority that was deliberately
|
|
753
|
+
withheld the same output. There is no tri-state left for a diagnostic to notice,
|
|
754
|
+
and nothing downstream can tell the two apart.
|
|
755
|
+
|
|
756
|
+
The priority is a **shared, note-level fact** — one statement both systems
|
|
757
|
+
record, SoHL as `system.templatePriority` and HM3 as
|
|
758
|
+
`flags.hm3.templatePriority` — so it is now read through the same resolver the
|
|
759
|
+
SoHL passes use, against the block being compiled: `data:`, this system's block,
|
|
760
|
+
the top level, and the retiring `archetype` spelling in the latter two. A note
|
|
761
|
+
carrying both spellings with different values is refused here exactly as it is
|
|
762
|
+
for SoHL, so the two systems cannot disagree about what a note said.
|
|
763
|
+
|
|
764
|
+
**It is read against _this_ block, not the `sohl:` one.** A tree still stating
|
|
765
|
+
the priority in `sohl:` writes no HM3 flag — `harn-ensemble` is that tree, on
|
|
766
|
+
2,502 notes — and gets one when it sweeps to `data:`, which is step 2 of #266's
|
|
767
|
+
migration. This change is the prerequisite for that sweep rather than a
|
|
768
|
+
substitute for it: without it, a tree that swept to the specified home would
|
|
769
|
+
have gone from a flag that worked by accident to no flag at all.
|
|
770
|
+
|
|
771
|
+
`resolveTemplatePriority` takes the block as an option, and
|
|
772
|
+
`statedTemplatePriority` is its tolerant sibling for a system that treats an
|
|
773
|
+
unstated priority as "not a template" rather than as an authoring error.
|
|
774
|
+
- 209e633: **The `hm3/` half of the toolchain** — a note can now compile an HM3 document (#139).
|
|
775
|
+
|
|
776
|
+
`sohl/` was the only system half this package had, so `itemBuilders: [sohl, hm3]` — an
|
|
777
|
+
arrangement `CONTENT.md` already documented — named a registry that did not exist, and no
|
|
778
|
+
note could produce an HM3 Actor or Item however its frontmatter was written. `hm3/` is now
|
|
779
|
+
a sibling of `sohl/`: its own item vocabulary and builders, its own default art, its own
|
|
780
|
+
note-type → document-subtype map declared through the same `defineDocumentSubtypes`, and
|
|
781
|
+
its own Item and Actor compilers. The two halves import nothing from each other; the only
|
|
782
|
+
thing they share is the engine between them.
|
|
783
|
+
|
|
784
|
+
**A pack's `system:` now selects the compiler.** It already selected the `_stats` stamp, the
|
|
785
|
+
item catalogue a being resolves against and the `itemBuilders` lookup; the Item and Actor
|
|
786
|
+
passes were still SoHL's whatever a pack declared. So a note carrying both a `sohl:` and an
|
|
787
|
+
`hm3:` block compiles **one document in each system**, each shaped by its own builders and
|
|
788
|
+
stamped with its own system version — and a note carrying only one block is passed over by
|
|
789
|
+
the other system's pass rather than failed for a block it was never going to have.
|
|
790
|
+
|
|
791
|
+
**Four HM3 rows are one-to-many, and the note says which.** `mysticalability` becomes a
|
|
792
|
+
`psionic`, a `spell` or an `invocation`; `trauma` an `injury` or a `trait`; `weapongear` a
|
|
793
|
+
`weapongear` or a `missilegear`; `being` a `character` or a `creature`. The note writes
|
|
794
|
+
`hm3.type`; nothing is inferred from its `subType`, and a note that says nothing is an error
|
|
795
|
+
naming the note and listing the permitted values. `docs/content-format.md` is corrected to
|
|
796
|
+
match — it described the `mysticalability` split as derived from a `subType` vocabulary that
|
|
797
|
+
no longer contains the values the derivation named.
|
|
798
|
+
|
|
799
|
+
**The five shared names cannot cross over.** `skill`, `weapongear`, `armorgear`,
|
|
800
|
+
`containergear` and `miscgear` exist in both systems with different data models, so each
|
|
801
|
+
resolves through its own system's map, is built by its own system's registry, and is
|
|
802
|
+
field-checked against **its own** system's published `schema.json`. That last one is new:
|
|
803
|
+
`resolveSchemaArtifact` took the package-wide `stats.systemId`, which is deliberately unset
|
|
804
|
+
in a two-system build — so every schema check in such a build was skipped in silence.
|
|
805
|
+
|
|
806
|
+
**Shared machinery moved to `engine/`, unchanged.** `engine/item-compiler.mjs` and
|
|
807
|
+
`engine/actor-compiler.mjs` now hold what is note-format knowledge rather than game-system
|
|
808
|
+
knowledge, and `engine/anchored-sections.mjs` holds the `{#appearance}` / `{#dossier}`
|
|
809
|
+
convention. `sohl/items.mjs` and `sohl/actors.mjs` are what is left: SoHL's map, and the
|
|
810
|
+
`system` block SoHL's data model wants. Compiled output is byte-identical — verified by
|
|
811
|
+
recompiling `Song-of-Heroic-Lands-FoundryVTT`, `sohl-thalorna` and `sohl-kethira-basic`
|
|
812
|
+
before and after and diffing every emitted document.
|
|
813
|
+
|
|
814
|
+
`npm run lint` now also checks HM3's column of the content format: the specification's
|
|
815
|
+
`→ hm3` mapping claims against HM3's published `schema.json`, and its per-type tables
|
|
816
|
+
against the new field declarations.
|
|
817
|
+
- f2e6e45: **The link check reads the content index instead of walking the tree itself**
|
|
818
|
+
(#243) — the first reader converted, and the one #243 nominated.
|
|
819
|
+
|
|
820
|
+
`buildLinkIndex` answered "which files are the content?" for itself: its own
|
|
821
|
+
walk, its own frontmatter parse, its own address derivation. That is the shape
|
|
822
|
+
#243 is closing — ten passes each deriving the corpus independently, agreeing
|
|
823
|
+
only by inspection. It now reads
|
|
824
|
+
{@link module:engine/content-index.indexRecordsFor}, the same derivation the
|
|
825
|
+
published artifact, the `sql` tables and the compilers already run on.
|
|
826
|
+
|
|
827
|
+
**It still opens each note — for its prose, and nothing else.** The index
|
|
828
|
+
deliberately carries no body, and a link lives in the body. Everything _about_
|
|
829
|
+
the note is in the record. That is one read per note rather than two: the walk
|
|
830
|
+
read the file, and this module then read it again for the raw text.
|
|
831
|
+
|
|
832
|
+
**Two defects go with it.**
|
|
833
|
+
|
|
834
|
+
- _The package a local address carries came from the ambient configuration._
|
|
835
|
+
`buildLinkIndex` was handed a `config` and then called `contentPackage()`,
|
|
836
|
+
which resolves `loadPackConfig()` from the working directory. The two are the
|
|
837
|
+
same object in an ordinary build and different ones under
|
|
838
|
+
`PACKAGE_BUILD_CONFIG`, in a worktree, or in a test — so the checker could
|
|
839
|
+
build canonical addresses for one package while the manifest it was checking
|
|
840
|
+
built them for another, and every cross-package link would resolve nowhere for
|
|
841
|
+
no visible reason. The package now comes from the configuration the caller
|
|
842
|
+
passed.
|
|
843
|
+
- _Each command derived the corpus twice._ `lint`, `links` and `reachability`
|
|
844
|
+
each built a link index _and_ prepared their `sql` tables, and both walked. The
|
|
845
|
+
records are now derived once per command and handed to both, so a table and a
|
|
846
|
+
wikilink cannot disagree about which notes exist. `reachability` also resolved
|
|
847
|
+
its configuration twice and passed neither to the passes below it; it resolves
|
|
848
|
+
one and passes it on.
|
|
849
|
+
|
|
850
|
+
**Findings are unchanged, and their order is now stable.** Over `sohl`'s 1,685
|
|
851
|
+
notes the `links` findings are identical as a set and `lint`'s output is
|
|
852
|
+
identical byte for byte; what moved is that diagnostics now come out in content
|
|
853
|
+
path order rather than directory-read order, which was never a fact about the
|
|
854
|
+
content.
|
|
855
|
+
|
|
856
|
+
`indexRecordsFor` takes the walk's scope as an argument, so a caller that was
|
|
857
|
+
handed one passes it on rather than having it replaced by whichever its
|
|
858
|
+
configuration carries; `prepareTreeSqlTables` and `buildLinkIndex` take
|
|
859
|
+
already-derived `records`. `authoredFrontmatter` and `isNoteRecord` are exported
|
|
860
|
+
beside `DERIVED_KEYS`: a record is a note's frontmatter _plus_ the derived keys,
|
|
861
|
+
and a note authoring one of them fails the walk, so recovering what the author
|
|
862
|
+
wrote is exact rather than best-effort — which is what lets a pass read the
|
|
863
|
+
corpus from the index and still lint what was typed.
|
|
864
|
+
- d17dc98: **`package-build yaml` lints note frontmatter and every YAML file** (#248).
|
|
865
|
+
|
|
866
|
+
Frontmatter carries a note's type, shortcode, address and system blocks, and
|
|
867
|
+
nothing checked it _as YAML_. Worse than unchecked: `parseMarkdownFile` caught a
|
|
868
|
+
parse failure, logged it at `warn`, and returned `{frontmatter: null}` — which is
|
|
869
|
+
not a note with bad frontmatter but, to every pass downstream, a file with no
|
|
870
|
+
frontmatter. A duplicate key did not fail a build; it removed a note from the
|
|
871
|
+
corpus while the build reported success. The parser had detected it all along.
|
|
872
|
+
|
|
873
|
+
**Frontmatter reaches ESLint through a processor**, the mechanism
|
|
874
|
+
`eslint-plugin-markdown` uses for fenced code blocks. Frontmatter is its easy
|
|
875
|
+
case — the block is always at the top of the file, so a finding maps back with a
|
|
876
|
+
constant `+1` for the opening `---` and no offset table.
|
|
877
|
+
|
|
878
|
+
**The rule set is deliberately narrow**, as the markdown and stylesheet ones are.
|
|
879
|
+
Prettier already owns YAML's whitespace, quoting and line breaks, including
|
|
880
|
+
inside a fence, so what is left is the class a formatter cannot see: text that
|
|
881
|
+
parses to something other than what it looks like. Parse errors, plus
|
|
882
|
+
`no-empty-mapping-value`, `no-irregular-whitespace`, `no-empty-key` and
|
|
883
|
+
`no-empty-document`. `folder:` and `folder: null` are one value and two opposite
|
|
884
|
+
statements — a decision, or a key somebody began and did not finish — and a key
|
|
885
|
+
with a block under it is not empty.
|
|
886
|
+
|
|
887
|
+
**GitHub workflows are exempt from the empty-value rule.** `on:` `push:` carries
|
|
888
|
+
its meaning by being present; `push: null` would be worse YAML, not better.
|
|
889
|
+
|
|
890
|
+
**A consumer changes one line** — `"lint:yaml": "package-build yaml"` — and needs
|
|
891
|
+
no `eslint` dependency, no `eslint.config.js` and no rule configuration.
|
|
892
|
+
package-build owns the tool and the config exactly as it owns markdownlint's, and
|
|
893
|
+
`overrideConfigFile: true` leaves a repository's own ESLint unconsulted.
|
|
894
|
+
|
|
895
|
+
Adoption costs 92 findings in total: 65 in `sohl-thalorna`, 20 in
|
|
896
|
+
`sohl-kethira-basic`, 5 in `harn-ensemble`, 2 in
|
|
897
|
+
`Song-of-Heroic-Lands-FoundryVTT`, none in `harn-adventures`. No live content
|
|
898
|
+
tree carries a parse error — every one found was in a `nogit/` archive — so this
|
|
899
|
+
is a missing guard rather than an overdue one.
|
|
900
|
+
- 6368283: **The template priority is read as `templatePriority`, and `archetype` is
|
|
901
|
+
retiring** (#266).
|
|
902
|
+
|
|
903
|
+
The number deciding which of several competing templates the Create dialog
|
|
904
|
+
offers was called `archetype` — one letter from `archetypes`, which is a list of
|
|
905
|
+
what _sort_ a character is. A priority and a taxonomy cannot be told apart by a
|
|
906
|
+
plural `s`.
|
|
907
|
+
|
|
908
|
+
Both spellings are read, `templatePriority` winning, and **the retiring one is a
|
|
909
|
+
lint error**. Unlike the other retired alias, `archetype` is not a field of its
|
|
910
|
+
own — it is `templatePriority` under its prior name, and both sit one letter from
|
|
911
|
+
`archetypes`, which means something else entirely. A tree still on it is one
|
|
912
|
+
where a priority and a taxonomy are told apart by a plural `s`, which is worth
|
|
913
|
+
stopping rather than mentioning.
|
|
914
|
+
|
|
915
|
+
The compile is unaffected — both spellings are read, so every tree keeps
|
|
916
|
+
compiling — but `content-build lint` refuses a tree until it is swept, and
|
|
917
|
+
**5,727 notes across four trees** author the old key.
|
|
918
|
+
|
|
919
|
+
It is read from `data.templatePriority` first, which is where the specification
|
|
920
|
+
puts it and where `sohl-thalorna` already writes it on 941 notes — so a tree that
|
|
921
|
+
has authored forward is read from the key it authored.
|
|
922
|
+
|
|
923
|
+
**A note declaring both spellings with different values is refused.** That is not
|
|
924
|
+
hypothetical: **145 of those 941 say `templatePriority: null` where `archetype:
|
|
925
|
+
0` says the opposite** — "not a template" against "a template at priority 0".
|
|
926
|
+
Preferring either silently would decide that for the author, so the build names
|
|
927
|
+
both values and asks. `0` and `null` are distinct and both valid, which is
|
|
928
|
+
exactly why a note cannot claim both.
|
|
929
|
+
|
|
930
|
+
The emitted field is unchanged: SoHL's data model still declares
|
|
931
|
+
`system.archetype`, and `Song-of-Heroic-Lands-FoundryVTT#1836` renames it there.
|
|
932
|
+
- 18bb812: **The walk's scope is stated by its caller, never resolved from the working
|
|
933
|
+
directory** (#243).
|
|
934
|
+
|
|
935
|
+
`walkMarkdownTree` defaulted `skipDirectories` to `loadPackConfig()`'s — so an
|
|
936
|
+
unscoped caller read whichever configuration resolved from the working directory
|
|
937
|
+
rather than the one it was working under. **Six of its twelve callers were on
|
|
938
|
+
that default**, which means two passes over one tree could disagree about which
|
|
939
|
+
files they were reading. In an ordinary build those are the same object and
|
|
940
|
+
nothing shows; they are not the same when a test injects a configuration, when
|
|
941
|
+
`PACKAGE_BUILD_CONFIG` names one, or when a command runs from a worktree.
|
|
942
|
+
|
|
943
|
+
This is the defect class #240 fixed for `entriesForNote` reading `docEntryTypes`
|
|
944
|
+
from the ambient config — found only because a fixture had been passing on the
|
|
945
|
+
leak for as long as it existed.
|
|
946
|
+
|
|
947
|
+
The parameter is now **required**, so the omission is an error rather than a
|
|
948
|
+
quiet second answer, and `BasePackCompiler` requires it too: a pass that omitted
|
|
949
|
+
it would walk whatever the working directory said, including — in a tree
|
|
950
|
+
configured to skip `Templates`, as `Song-of-Heroic-Lands-FoundryVTT` is — the
|
|
951
|
+
template notes that configuration exists to keep out of the packs.
|
|
952
|
+
|
|
953
|
+
`Scenes` was dropping it between its own constructor and `super`, which is
|
|
954
|
+
exactly the kind of silent gap a default hides and a requirement does not.
|
|
955
|
+
|
|
956
|
+
_No consumer calls `walkMarkdownTree`, so this is internal despite the signature
|
|
957
|
+
change._
|
|
958
|
+
- a5922b7: **The five declarations that re-read the note now see `<system>.system`**
|
|
959
|
+
(#126) — the last mechanism gap before the corpus can move.
|
|
960
|
+
|
|
961
|
+
Most fields take the value `resolveFieldValue` hands them, so they already
|
|
962
|
+
resolve at `<system>.system.<to>` first. Five do not. `subType`, `charges`, a
|
|
963
|
+
mystery's `skillAptitudes`, an affiliation's `relations` and a projectile's
|
|
964
|
+
impact die each validate a **shape spread over several keys**, so their `read`
|
|
965
|
+
re-reads the frontmatter — through `sohlField`, which sees `sohl.<key>` and the
|
|
966
|
+
note's top level and **never inside `sohl.system`**.
|
|
967
|
+
|
|
968
|
+
That was equivalent while every note authored in the block. The moment a note
|
|
969
|
+
authors at the destination instead, those five read as unset: a missing
|
|
970
|
+
`subType` is a thrown build error, and `charges`, `skillAptitudes` and
|
|
971
|
+
`relations` ship empty — in silence, which is the failure class the passthrough
|
|
972
|
+
exists to prevent.
|
|
973
|
+
|
|
974
|
+
`sohlSystemField` reads the destination first and falls back to the legacy
|
|
975
|
+
position, so both spellings work while the corpus moves. It takes an optional
|
|
976
|
+
`legacyKey` for the one pair spelled differently at the two positions — a
|
|
977
|
+
projectile authors `impact.die` and stores `impactBase.die` — which is the same
|
|
978
|
+
split `FieldSpec.name`/`legacyKey` makes, for the same reason: one name cannot
|
|
979
|
+
key two positions.
|
|
980
|
+
|
|
981
|
+
The retired-alias probe in `resolveRelation` moves with it. It asked
|
|
982
|
+
`sohlField` whether `relations` was authored in order to choose between the
|
|
983
|
+
current and retired spelling; a note that had moved to `sohl.system.relations`
|
|
984
|
+
carried the current name where the probe could not see it, so it fell through to
|
|
985
|
+
`relation` and read `{}`.
|
|
986
|
+
|
|
987
|
+
Nothing else changes: every note authoring in the block reads exactly as before,
|
|
988
|
+
which the suite pins alongside the new position.
|
|
989
|
+
- f2bf737: **A note the content index cannot record is reported, not thrown** (#243) —
|
|
990
|
+
with a line, a column, and the right correction.
|
|
991
|
+
|
|
992
|
+
Converting the link check (#290) and the address diff (#292) to read the index
|
|
993
|
+
handed those passes the index's one hard failure. A note authoring a key the
|
|
994
|
+
index derives — {@link DERIVED_KEYS} — aborted the whole pass, so **one
|
|
995
|
+
malformed note took every other finding in the tree with it**, and the reader
|
|
996
|
+
got a bare `Error` where the project's other diagnostics give
|
|
997
|
+
`file:line:column: severity: message`. This is precisely what #243 lists as the
|
|
998
|
+
thing that must not regress, and it did.
|
|
999
|
+
|
|
1000
|
+
Concretely, over a tree holding a legacy `package:` note and an unrelated dead
|
|
1001
|
+
link:
|
|
1002
|
+
|
|
1003
|
+
| | reported |
|
|
1004
|
+
| ----------------------- | -------------------------------------------------------------------- |
|
|
1005
|
+
| _before the conversion_ | the dead link; nothing about `package:` |
|
|
1006
|
+
| _after it_ | ``Skills/Legacy.md: `package:` is derived …`` — and **nothing else** |
|
|
1007
|
+
| _now_ | both, each with its position |
|
|
1008
|
+
|
|
1009
|
+
**A reader collects; the emitter still refuses.** `collectContentIndex` and
|
|
1010
|
+
`indexRecordsFor` take a `problems` array: given one, a note that cannot be
|
|
1011
|
+
recorded is pushed as a diagnostic and skipped, and the derivation continues.
|
|
1012
|
+
Given none they throw exactly as before — which is the contract
|
|
1013
|
+
`emitContentIndex` needs, since an index quietly missing a note asserts that the
|
|
1014
|
+
note does not exist. `buildLinkIndex`, `declaredPredecessors` and
|
|
1015
|
+
`noteFilesById` pass the array through, and `lint`, `links`, `reachability` and
|
|
1016
|
+
`addresses diff` emit what it collects and exit non-zero — an error whatever the
|
|
1017
|
+
command's own strictness flag says, because the note is absent from every answer
|
|
1018
|
+
those commands give.
|
|
1019
|
+
|
|
1020
|
+
**`package:` gets its own words back.** It is on the derived list, but it is not
|
|
1021
|
+
a name collision — it is a **retired field** (#56), and the fix is to delete it,
|
|
1022
|
+
not to rename it. `assertNoDeclaredPackage` has said so, correctly and with a
|
|
1023
|
+
position, since the field was retired, and had **no caller**: the generic
|
|
1024
|
+
"rename the frontmatter field" was the only message anyone saw, and it was
|
|
1025
|
+
wrong. The index now defers to it, so one mistake has one message rather than
|
|
1026
|
+
two that disagree about the fix.
|
|
1027
|
+
|
|
1028
|
+
_No change to any tree that has no such note: `sohl`'s 1,685 notes produce
|
|
1029
|
+
byte-identical `links`, `lint` and address-map output._
|
|
1030
|
+
- d17dc98: **`place`, `lore` and `scenario` compile into JournalEntries.** They are in the
|
|
1031
|
+
published content format and #233 declared them for validation, but nothing
|
|
1032
|
+
routed them: `PACK_BY_TYPE` did not name them, so the open-set default sent
|
|
1033
|
+
them to the items pack, and the journals pass did not select them. A note of
|
|
1034
|
+
one lint-ed clean and then compiled into nothing (#241).
|
|
1035
|
+
|
|
1036
|
+
`sohl-thalorna` could not compile a single pack for exactly this reason — 450
|
|
1037
|
+
errors, and _the same 450_ the linter had reported before it learned the types.
|
|
1038
|
+
The count being identical is the tell: nothing about the content changed, only
|
|
1039
|
+
which gate noticed. It now compiles 642 actors, and what remains are unrelated
|
|
1040
|
+
content faults.
|
|
1041
|
+
|
|
1042
|
+
The three are declared once, as `JOURNAL_TYPES` in `engine/ids.mjs`, and read
|
|
1043
|
+
from there by the pack router, the journals pass and the claim table — so the
|
|
1044
|
+
three cannot disagree about what a journal type is. The drift guard that
|
|
1045
|
+
asserts the claim table and each pass's `selects` agree is what caught them
|
|
1046
|
+
disagreeing while this was written.
|
|
1047
|
+
|
|
1048
|
+
**They carry no synthesized `doc<type>` entry**, which is the distinction the
|
|
1049
|
+
new name makes explicit: a journal type's whole document _is_ the journal, so
|
|
1050
|
+
there is no second document to address and nothing spells `docplace`. That is
|
|
1051
|
+
different from an item, a macro or a map, whose prose becomes a journal
|
|
1052
|
+
_beside_ another document and is addressed as `doc<type>`.
|
|
1053
|
+
- 10522bd: **State the shared mapping rows, and let the checker reach them (#275).**
|
|
1054
|
+
|
|
1055
|
+
`docs/content-format.md` § _Mappings every type shares_ promised eight rows that
|
|
1056
|
+
every one of the sixteen per-type tables omits "on the stated grounds that they
|
|
1057
|
+
appear here", and then stated none: both tables were header-only, from the commit
|
|
1058
|
+
that first committed the specification. The fields they cover were therefore
|
|
1059
|
+
specified nowhere, and the note pointing at them pointed at nothing.
|
|
1060
|
+
|
|
1061
|
+
Both tables now carry their rows. The shared table states `name.full`, `img`,
|
|
1062
|
+
`id`, `packFolder` / `folder`, `shortcode`, `data.templatePriority`, `actionDefs`
|
|
1063
|
+
and `notes`; the actor-types table states `data.portrait`. `being` and `vehicle`
|
|
1064
|
+
had restated `data.portrait` and `data.templatePriority` in their own tables, and
|
|
1065
|
+
no longer do — that duplication is what the section exists to remove.
|
|
1066
|
+
|
|
1067
|
+
**The rows are confirmed rather than asserted.** `parseContentFormat` read
|
|
1068
|
+
mapping tables only inside a `### type:` section, so a table standing before the
|
|
1069
|
+
first one was invisible and its rows were checked by nothing. A mapping table in
|
|
1070
|
+
that position is now the shared one — position is the whole distinction, since
|
|
1071
|
+
the document's own argument for stating these once is that they belong to no type
|
|
1072
|
+
in particular — and its rows become claims like any other.
|
|
1073
|
+
`npm run lint:content-format:schema` confirms 86 where it confirmed 84, the seven
|
|
1074
|
+
new ones being the shared tables' `system.*` targets.
|
|
1075
|
+
|
|
1076
|
+
A shared claim carries `shared: true` and is scoped to `the shared mappings`
|
|
1077
|
+
rather than to a type, which keeps it out of the per-type field-drift check (it
|
|
1078
|
+
has no field declaration to drift from) and reads as prose in a diagnostic:
|
|
1079
|
+
_the format maps `shortcode` in the shared mappings to `system.notAField` in
|
|
1080
|
+
sohl_.
|
|
1081
|
+
|
|
1082
|
+
Two asymmetries the rows exposed are stated beside them rather than smoothed
|
|
1083
|
+
over: `actionDefs` and `notes` are declared on every SoHL Item subtype and on no
|
|
1084
|
+
SoHL Actor, and SoHL's `system.docHtml` is an Item mapping with no shared source
|
|
1085
|
+
to be a row of. A third — HM3 carrying `flags.hm3.templatePriority` on an Actor
|
|
1086
|
+
and not on an Item — is a gap in the pass rather than in the table, and is
|
|
1087
|
+
tracked as #283.
|
|
1088
|
+
- 941b672: **A field's shared source and its legacy in-block key are two declarations**
|
|
1089
|
+
(#305) — so a field can move into `data:` without a flag day.
|
|
1090
|
+
|
|
1091
|
+
`FieldSpec.name` carried both jobs, and they came apart the moment `data:`
|
|
1092
|
+
(#128) put every type-specific fact under a container. `resolveFieldValue` reads
|
|
1093
|
+
four positions, and steps 2 and 3 were **both keyed on `name`**:
|
|
1094
|
+
|
|
1095
|
+
| step | position |
|
|
1096
|
+
| ---- | --------------------------- |
|
|
1097
|
+
| 1 | `<block>.system.<to>` |
|
|
1098
|
+
| 2 | `<block>.<name>` — in-block |
|
|
1099
|
+
| 3 | the shared source `<name>` |
|
|
1100
|
+
| 4 | the field's default |
|
|
1101
|
+
|
|
1102
|
+
So `name: "species"` reached `hm3.species` and could not see `data.species`,
|
|
1103
|
+
while `name: "data.species"` reached the shared source and could not see
|
|
1104
|
+
`hm3.species` — and each yielded the field's **default** wherever only the other
|
|
1105
|
+
position was authored. Silently: the field compiles, the document is emitted,
|
|
1106
|
+
and the value is simply gone.
|
|
1107
|
+
|
|
1108
|
+
Two things followed. The specification's shared→system mapping was implemented
|
|
1109
|
+
_nowhere_ — `grep 'name: "data\.'` returned zero hits across all 58 declarations,
|
|
1110
|
+
so rows the format states (`data.species`, `data.gender`, `data.occupation`)
|
|
1111
|
+
were read by nothing. And no safe transition existed: every other retirement here
|
|
1112
|
+
— `package:`, `image`, `archetype`, `relation` — works because **both spellings
|
|
1113
|
+
are read while the corpus moves**, and one property could not offer that.
|
|
1114
|
+
|
|
1115
|
+
**`legacyKey` separates them.** `name` is the shared source; `legacyKey` is the
|
|
1116
|
+
key the system block still carries, and step 2 keys on it. Absent, it falls back
|
|
1117
|
+
to `name`, so every declaration written before this resolves unchanged.
|
|
1118
|
+
|
|
1119
|
+
- **Both are read, the current position wins.** A note that has not been swept
|
|
1120
|
+
is still saying what it means.
|
|
1121
|
+
- **The legacy read is _reported_**, as a warning — at compile through
|
|
1122
|
+
`legacyKeyMessage`, and in the frontmatter lint — so a sweep has a progress
|
|
1123
|
+
signal, matching `RETIRED_FIELD_ALIASES`. A field that declares no `legacyKey`
|
|
1124
|
+
is not mid-sweep and is never reported; that would put a finding on every
|
|
1125
|
+
field of every note.
|
|
1126
|
+
- The lint accepts the legacy key as a key of the block, rather than reporting
|
|
1127
|
+
`sohl.species` as a property no `being` has against exactly the notes the
|
|
1128
|
+
sweep has not reached.
|
|
1129
|
+
- The format check normalizes `data.` on **both** sides, so a moved declaration
|
|
1130
|
+
still pairs with the specification row that states it.
|
|
1131
|
+
|
|
1132
|
+
**HM3's three actor rows are declared as the sources the format names** —
|
|
1133
|
+
`data.species`, `data.gender`, `data.occupation` — while keeping the in-block
|
|
1134
|
+
key every note writes. Measured over the four content trees: 2,512 `being` notes
|
|
1135
|
+
compile byte-identical to before, and all 2,512 read the legacy position, which
|
|
1136
|
+
is the count #126 has to take to zero.
|
|
1137
|
+
|
|
1138
|
+
This is the mechanism the `data:` half of #126 and all of #129 were blocked on.
|
|
1139
|
+
- 4926fb8: **A note whose type the format specifies but nothing compiles gets a finding of
|
|
1140
|
+
its own.**
|
|
1141
|
+
|
|
1142
|
+
The unclaimed-type check had two messages, and a third thing can be wrong — the
|
|
1143
|
+
only one that is not the author's fault. `docs/content-format.md` documents the
|
|
1144
|
+
type and the vocabulary declares its properties, so a note written against the
|
|
1145
|
+
published specification is correct; this toolchain simply has not implemented it
|
|
1146
|
+
yet.
|
|
1147
|
+
|
|
1148
|
+
It earns its own wording because the other two both mislead there. Naming a
|
|
1149
|
+
missing pack or registry sends an author to `package-build.config.yaml`, where
|
|
1150
|
+
nothing they can write will help; saying the type is unknown flatly contradicts
|
|
1151
|
+
the specification they read it in.
|
|
1152
|
+
|
|
1153
|
+
The message is chosen from the vocabulary rather than from a list of types, so
|
|
1154
|
+
each specified-but-unimplemented type is covered as it is declared.
|
|
1155
|
+
- 28ae4b3: **A content table can be written in SQL, queried over the content index** (#246).
|
|
1156
|
+
|
|
1157
|
+
Tables were written in Dataview's query language, chosen when the corpus lived in
|
|
1158
|
+
an Obsidian vault so a table rendered live while authoring. The vault is gone, and
|
|
1159
|
+
what remained was a hand-written parser and evaluator for someone else's language,
|
|
1160
|
+
kept faithful to semantics nothing checked it against.
|
|
1161
|
+
|
|
1162
|
+
The query is **real SQL, run by DuckDB** — not a dialect maintained here. That is
|
|
1163
|
+
the point: a partial reimplementation would accept some valid SQL and silently
|
|
1164
|
+
misread the rest, which is worse than an unfamiliar language because the boundary
|
|
1165
|
+
is invisible.
|
|
1166
|
+
|
|
1167
|
+
```sql
|
|
1168
|
+
SELECT address.slug AS _ref,
|
|
1169
|
+
sohl.kbcat AS _section,
|
|
1170
|
+
name.full AS "Name",
|
|
1171
|
+
sohl.weight AS "Weight"
|
|
1172
|
+
FROM notes
|
|
1173
|
+
WHERE type = 'miscgear'
|
|
1174
|
+
ORDER BY sohl.kbcat, name.full
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
**`sohl.weight` and `name.full` read in a query exactly as a note authors them.**
|
|
1178
|
+
DuckDB reads the index as JSON and infers a `STRUCT` per nested object; a
|
|
1179
|
+
column-per-path table would force `"sohl.weight"` in quotes and a JSON column
|
|
1180
|
+
`sohl->>'weight'`. `union_by_name` is what makes it work across a corpus where
|
|
1181
|
+
every note type's system block differs.
|
|
1182
|
+
|
|
1183
|
+
**What SQL cannot say, the projection says.** Which column links, and where a
|
|
1184
|
+
section breaks, are decisions about output rather than relational operations, so
|
|
1185
|
+
they ride as underscore-prefixed aliases — `_ref` and `_section` — which are
|
|
1186
|
+
ordinary SQL, need no fence options, and sit where the author is already looking.
|
|
1187
|
+
`_section` is why one query replaces the forty near-identical blocks
|
|
1188
|
+
`Rules/Gear.md` needs today: it emits a headed table per distinct value, in the
|
|
1189
|
+
order the authored `ORDER BY` produced.
|
|
1190
|
+
|
|
1191
|
+
`sql allow-empty` and `sql section-level=3` ride on the fence, as `dataview
|
|
1192
|
+
allow-empty` already does, because both are statements about the directive rather
|
|
1193
|
+
than part of the query.
|
|
1194
|
+
|
|
1195
|
+
**Both spellings work.** Every one of the 177 tables in the corpus is still
|
|
1196
|
+
`dataview`; those now also report a warning naming this issue, and nothing else
|
|
1197
|
+
changes for them. Nothing is opened for a tree with no `sql` directive, so this
|
|
1198
|
+
costs a tree that has not converted one walk and no database.
|
|
1199
|
+
|
|
1200
|
+
_DuckDB is a **build** dependency, not merely a development one — every
|
|
1201
|
+
consumer's CI runs a compile. It adds about 114MB to an install._
|
|
1202
|
+
- 9d89f31: **A `sql` content table takes org-babel header arguments, and a dependency's
|
|
1203
|
+
notes are a schema** (#246).
|
|
1204
|
+
|
|
1205
|
+
**Header arguments.** Statements _about the directive_ — as opposed to the query
|
|
1206
|
+
— were an ad-hoc bare word (`allow-empty`) and a `key=value`
|
|
1207
|
+
(`section-level=3`), each matched by its own regex: a grammar only in the sense
|
|
1208
|
+
that two regexes are one, with no room for a third property that did not also
|
|
1209
|
+
invent a third spelling. They are now org-babel header args, written after the
|
|
1210
|
+
language:
|
|
1211
|
+
|
|
1212
|
+
````
|
|
1213
|
+
```sql :section-level 3 :allow-empty
|
|
1214
|
+
````
|
|
1215
|
+
|
|
1216
|
+
**The language word stays first and stays plain**, so GitHub, Prettier and every
|
|
1217
|
+
other markdown reader still highlight the block as SQL and ignore what follows.
|
|
1218
|
+
|
|
1219
|
+
The grammar is org's, which is a real one with a specification and an editor
|
|
1220
|
+
that completes it: a key is `:name` starting a word (so `:caption Gear: the
|
|
1221
|
+
tables` is one argument); a value runs to the next key, spaces included; a
|
|
1222
|
+
valueless key is `true`; a value may be `"quoted"` to hold a key-like word; a
|
|
1223
|
+
repeated key takes its last value. `parseHeaderArgs` lives in `code-fences.mjs`
|
|
1224
|
+
and every argument reaches the caller, so a property this module makes no use of
|
|
1225
|
+
is still readable — the point of taking a grammar rather than a regex per
|
|
1226
|
+
property.
|
|
1227
|
+
|
|
1228
|
+
The `sql` fence has never shipped, so both old spellings are simply gone rather
|
|
1229
|
+
than retired. `dataview` keeps its bare `allow-empty`: it is the retiring
|
|
1230
|
+
language and its grammar is frozen.
|
|
1231
|
+
|
|
1232
|
+
**A dependency is a schema.** Each package this one depends on is attached as a
|
|
1233
|
+
schema named after it, so a satellite can tabulate what it builds on:
|
|
1234
|
+
|
|
1235
|
+
```sql
|
|
1236
|
+
SELECT name.full AS "Name" FROM sohl.notes WHERE type = 'skill'
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
This package's own notes stay at the unqualified `notes`, and one query may read
|
|
1240
|
+
both — joining your beings against the skills they cite is a `FROM` clause. It
|
|
1241
|
+
costs no fetch and no configuration: every dependency's published index is
|
|
1242
|
+
already in the metadata cache when a compile starts, because resolving addresses
|
|
1243
|
+
across packages needs it.
|
|
1244
|
+
|
|
1245
|
+
Which dataset a query reads is `FROM`'s job, not a fence property naming a file.
|
|
1246
|
+
A path in authored content writes a build artifact's name into the corpus, so
|
|
1247
|
+
renaming the artifact would mean sweeping every note that cited it — and _which
|
|
1248
|
+
dataset_ is exactly what SQL already has a clause for, the same rule that keeps
|
|
1249
|
+
`_ref` and `_section` ordinary SQL rather than fence options.
|
|
1250
|
+
- 3bf9020: **The address lint reads the content index too** (#243) — so
|
|
1251
|
+
`content-build lint` is now **one** derivation of the corpus rather than two.
|
|
1252
|
+
|
|
1253
|
+
The command already derived the index: its link check is built from it and its
|
|
1254
|
+
`sql` tables select over it. Then it walked the whole tree a second time to
|
|
1255
|
+
reach `lintContentTree`. One command, two answers to "which files are the
|
|
1256
|
+
corpus?", and findings reported side by side that were drawn from different
|
|
1257
|
+
ones. The records are now derived once by the command and handed to every pass
|
|
1258
|
+
below it, this one included.
|
|
1259
|
+
|
|
1260
|
+
**It lints what the author wrote.** A record is the note's frontmatter plus the
|
|
1261
|
+
keys the index derives, so the frontmatter is recovered with
|
|
1262
|
+
`authoredFrontmatter` — handing a lint the derived keys would have it reasoning
|
|
1263
|
+
about `address:` and `anchors:` as though someone had typed them.
|
|
1264
|
+
|
|
1265
|
+
**Faster, for the same reason it is more correct.** Over `sohl`'s 1,685 notes
|
|
1266
|
+
`content-build lint` goes from about 1.9s to about 1.3s: the second whole-tree
|
|
1267
|
+
walk is gone. Findings are byte-identical.
|
|
1268
|
+
|
|
1269
|
+
`lintContentTree` takes `config`, already-derived `records`, and the `problems`
|
|
1270
|
+
collector, so a note the index cannot record is reported with its position and
|
|
1271
|
+
the rest of the tree is still linted — the contract every converted reader now
|
|
1272
|
+
shares. It refuses an unstated scope in the same words the others do.
|
|
1273
|
+
- 9972484: **A compile read every note twenty times. Now it reads it twelve** (#243) —
|
|
1274
|
+
and the whole compile runs on one corpus rather than one per pass.
|
|
1275
|
+
|
|
1276
|
+
Measured over `sohl`'s 1,685 notes: **33,700 note reads**, exactly twenty each.
|
|
1277
|
+
Four per pass — the content-wide link index, the table-search corpus, the `sql`
|
|
1278
|
+
directive scan, and the pass's own walk — across five passes that convert
|
|
1279
|
+
wikilinks. Every one of those four is a pure function of the same three things:
|
|
1280
|
+
the tree, the scope, and the pack router. **None of the three varies between the
|
|
1281
|
+
passes of a single compile**, because `generatePacksJson` resolves one router
|
|
1282
|
+
and hands it to all of them. So the passes were computing the same answers over
|
|
1283
|
+
and over, and — the part that matters — each was free to compute a _different_
|
|
1284
|
+
one.
|
|
1285
|
+
|
|
1286
|
+
They are derived once now, in `buildCompileCorpus`, and every pass is handed the
|
|
1287
|
+
result. The compile loop enumerates the index rather than walking, then reads
|
|
1288
|
+
each note for its **prose**: the index deliberately carries no body, and none of
|
|
1289
|
+
the `bodyLine`/`bodyColumn` a diagnostic needs, so that read stays — it is a
|
|
1290
|
+
read the pass was already making. What it no longer does is decide for itself
|
|
1291
|
+
which files to make it over.
|
|
1292
|
+
|
|
1293
|
+
| over `sohl`'s tree | before | after |
|
|
1294
|
+
| ------------------ | -------------------- | ------------------------- |
|
|
1295
|
+
| note reads | 33,700 (20 per note) | **20,220 (12 per note)** |
|
|
1296
|
+
| compile wall time | ~10.5s | **~6.7s** |
|
|
1297
|
+
| emitted documents | 3,091 | **3,091, byte-identical** |
|
|
1298
|
+
| diagnostics | 33 | **33, identical** |
|
|
1299
|
+
|
|
1300
|
+
**The record accessors move to `engine/index-records.mjs`.** Deriving the index
|
|
1301
|
+
reaches the pack router and the manifest emitter, and those reach the compilers
|
|
1302
|
+
— so `engine/helpers.mjs`, which the compilers load, cannot import
|
|
1303
|
+
`content-index.mjs` without closing a cycle. Nothing about _reading_ a record
|
|
1304
|
+
needs that machinery: `noteFile`, `authoredFrontmatter`, `isNoteRecord` and
|
|
1305
|
+
`DERIVED_KEYS` are pure functions over a plain object. `content-index.mjs`
|
|
1306
|
+
re-exports them, so the split is an implementation detail of the import graph
|
|
1307
|
+
rather than a second place to look.
|
|
1308
|
+
|
|
1309
|
+
`buildContentLinkIndex` and `collectContentDocs` now **require** the corpus
|
|
1310
|
+
their caller holds, refused by `assertSuppliedCorpus` the way an unstated scope
|
|
1311
|
+
is refused by `assertStatedScope`. That is not a workaround for the cycle: a
|
|
1312
|
+
compile runs several passes over one tree, and requiring the answer to be handed
|
|
1313
|
+
in makes the sharing structural rather than remembered.
|
|
1314
|
+
|
|
1315
|
+
**Two ambient-configuration reads go with it**, the same class as every other
|
|
1316
|
+
one #243 has turned up: `buildContentLinkIndex` derived each note's id through
|
|
1317
|
+
`resolveNoteId(fm)` with no package, and `collectContentDocs` synthesised each
|
|
1318
|
+
row's `package` through `searchableFrontmatter(fm)` with none — both falling
|
|
1319
|
+
back to whichever configuration the working directory answers with, rather than
|
|
1320
|
+
the one the build resolved.
|
|
1321
|
+
|
|
1322
|
+
A note the index cannot record is reported and counted by the compile exactly as
|
|
1323
|
+
the compile loop reported it when the loop was the first to see it — it is the
|
|
1324
|
+
same refusal, deferring to the same `assertNoDeclaredPackage`; only which pass
|
|
1325
|
+
meets the note first has changed.
|
|
1326
|
+
- 5a476cd: **The rest of the compile reads the index too** (#243). With #300 this takes a
|
|
1327
|
+
compile from **20 note reads each to 9**, and from ~10.5s to ~5.4s over `sohl`'s
|
|
1328
|
+
1,685 notes — with all **3,091 emitted documents byte-identical** and the 33
|
|
1329
|
+
diagnostics unchanged.
|
|
1330
|
+
|
|
1331
|
+
Four whole-tree walks go:
|
|
1332
|
+
|
|
1333
|
+
- **The scenes pass** collected every note to find map notes and item effects.
|
|
1334
|
+
It reads the shared corpus, and opens a file only for a map note's prose —
|
|
1335
|
+
three files in `sohl` rather than 1,685.
|
|
1336
|
+
- **The unclaimed-type check** (`note-claims`) walked before any pass ran, so
|
|
1337
|
+
the check that reports "no pack claims this type" was answering about a
|
|
1338
|
+
different corpus from the one the passes then compiled.
|
|
1339
|
+
- **The `sql` directive scan** discovered which notes carry a directive by
|
|
1340
|
+
walking. It still reads each body — the index carries no note text — but
|
|
1341
|
+
_which files_ is no longer a second answer.
|
|
1342
|
+
- **The folder-note index**, which is the interesting one; see below.
|
|
1343
|
+
|
|
1344
|
+
**Three more ambient-configuration reads**, the same class as every one #243 has
|
|
1345
|
+
turned up: the scenes pass built a fresh `packRouter()` twice rather than using
|
|
1346
|
+
the compile's, and the folder-note index took its package from
|
|
1347
|
+
`contentPackage()` rather than from the configuration the build resolved.
|
|
1348
|
+
|
|
1349
|
+
**A trap worth naming, because the next conversion will meet it.** A record's
|
|
1350
|
+
`id` is not "the id the author wrote". The index fills one in for every
|
|
1351
|
+
addressable note (#270), and `collectFolderNotes` treats `fm.id` as an
|
|
1352
|
+
**authored pin** that wins over the id it derives under the folder namespace —
|
|
1353
|
+
so handing it records would make every folder look pinned and file each one
|
|
1354
|
+
under an id the packs do not address it by. `pack-folder` caught it. The index
|
|
1355
|
+
cannot tell a pin from a derivation, so the folder notes are read from disk:
|
|
1356
|
+
selected by the index, frontmatter from the file. That is the settled rule of
|
|
1357
|
+
#298 doing real work — the file carries what the index deliberately normalises
|
|
1358
|
+
away — and it is 79 reads, not 1,685.
|
|
1359
|
+
|
|
1360
|
+
`unclaimedNoteFindings` now requires the corpus its caller holds, like
|
|
1361
|
+
`buildContentLinkIndex` and `collectContentDocs`.
|
|
1362
|
+
|
|
1363
|
+
_`collectFoundryEntries` keeps its walk deliberately: it has no production
|
|
1364
|
+
caller — the link manifest it served was deleted in #271 — so converting it
|
|
1365
|
+
would change an exported signature for no compile it takes part in._
|
|
1366
|
+
- e0228f5: **The site build reads the content index, which completes #243's conversion.**
|
|
1367
|
+
Every pass that reads this repository's content tree — four checks, the whole
|
|
1368
|
+
compile, and now the site — runs on one derivation of the corpus.
|
|
1369
|
+
|
|
1370
|
+
The site walked the tree **twice**, once for homepages and once for content
|
|
1371
|
+
pages, and each answered "which files are the content?" for itself. It derives
|
|
1372
|
+
the corpus once and hands it to both. The note is still read for its
|
|
1373
|
+
`{fm, body}`: the index carries no note text, and a page _is_ its text.
|
|
1374
|
+
|
|
1375
|
+
**The reordering everyone was warned about does not exist.** This module kept
|
|
1376
|
+
directory order deliberately — "a site's emitted pages should not reorder for no
|
|
1377
|
+
reason" — and index records are content-path ordered, so converting it looked
|
|
1378
|
+
like a change to shipped output. It is not, and this was measured rather than
|
|
1379
|
+
argued: over `sohl`'s tree the emitted mount is **byte-identical, all 1,749
|
|
1380
|
+
files**, and the build's 82 diagnostics are identical **including their order**.
|
|
1381
|
+
Emission order never reaches the mount — each page is written to its own file at
|
|
1382
|
+
an address derived from its frontmatter, and the first-writer-wins fallbacks
|
|
1383
|
+
that once made order load-bearing went with the bare `[[Name]]` form (#180).
|
|
1384
|
+
|
|
1385
|
+
A content-path order is also the better of the two: directory-read order is a
|
|
1386
|
+
fact about the filesystem rather than about the content, so it can differ
|
|
1387
|
+
between two checkouts of one tree.
|
|
1388
|
+
|
|
1389
|
+
**`collectTreePages` is deliberately not converted.** It walks an auxiliary tree
|
|
1390
|
+
(`site.trees`, the developer documentation) which is not the content tree and
|
|
1391
|
+
appears in no record — converting it would be reading the wrong index.
|
|
1392
|
+
|
|
1393
|
+
_The two content-tree walks that remain are both right where they are: the
|
|
1394
|
+
content index's own derivation, and `collectFoundryEntries`, which has had no
|
|
1395
|
+
production caller since the link manifest was deleted in #271._
|
|
1396
|
+
- d17dc98: **`title: ""` is now a warning** (#218).
|
|
1397
|
+
|
|
1398
|
+
The two art fields follow a rule — `null` falls back, `""` is blank on purpose —
|
|
1399
|
+
and `title` was kept off it because the top-level key simultaneously fed an
|
|
1400
|
+
affiliation's `system.title`, so `title: null` compiled the literal `"null"`.
|
|
1401
|
+
That collision is gone: the field declares `topLevelMeans`, and the two spellings
|
|
1402
|
+
no longer meet.
|
|
1403
|
+
|
|
1404
|
+
What is left is the page heading. The emitter is `fm.title ?? name`, so `""`
|
|
1405
|
+
survives, the page publishes with no heading, and it sorts to the front of its
|
|
1406
|
+
section landing ahead of every named page. Fifteen notes in `sohl-thalorna` are
|
|
1407
|
+
in exactly that state.
|
|
1408
|
+
|
|
1409
|
+
A _warning_ rather than an error: the value is legal under the rule, and a page
|
|
1410
|
+
that genuinely wants no heading may keep it — it just has to mean it.
|
|
1411
|
+
|
|
1412
|
+
### Patch Changes
|
|
1413
|
+
|
|
1414
|
+
- 72ce6c2: **`CONTENT.md` names the template priority by its settled name** (#266).
|
|
1415
|
+
|
|
1416
|
+
The shipped specification still described the field as `archetype`, written to
|
|
1417
|
+
`system.archetype`, and its worked example authored `archetype: 1` inside the
|
|
1418
|
+
`sohl:` block. All three were wrong in the same direction, and the example was
|
|
1419
|
+
the worst of them: it told an author to write the exact key the frontmatter
|
|
1420
|
+
linter now refuses, in a position that is no longer the field's home.
|
|
1421
|
+
|
|
1422
|
+
It now describes what the build does — `data.templatePriority` is the shared
|
|
1423
|
+
home, reaching `system.templatePriority` in SoHL and `flags.hm3.templatePriority`
|
|
1424
|
+
in HM3, with the legacy in-block and top-level positions still read and
|
|
1425
|
+
`archetype` read last and refused by the linter. The emitted-keys list names
|
|
1426
|
+
`templatePriority`, and the example authors it under `data:`.
|
|
1427
|
+
|
|
1428
|
+
The specification's one remaining use of `archetype` to mean the priority — an
|
|
1429
|
+
aside comparing a map's misplaced fields to it — says "the template priority"
|
|
1430
|
+
instead, since `archetypes` now means a different thing one letter away.
|
|
1431
|
+
|
|
1432
|
+
The schema fixture's comment no longer reads as though
|
|
1433
|
+
`Song-of-Heroic-Lands-FoundryVTT#1836` were outstanding. It is merged; the gap it
|
|
1434
|
+
describes closes when SoHL 0.8.4 publishes, since the newest published artifact
|
|
1435
|
+
(0.8.3) still declares `archetype`.
|
|
1436
|
+
- f97036b: **`kbcat` is documented** (#264), the last of the five universal keys the
|
|
1437
|
+
specification had never mentioned.
|
|
1438
|
+
|
|
1439
|
+
`pack` and the template priority were described in the previous release; `kbcat`
|
|
1440
|
+
was the sharpest of the three gaps and the one left. It is read 51 times across
|
|
1441
|
+
SoHL's knowledgebase layouts and authored on more than 1,300 notes, and
|
|
1442
|
+
`docs/content-format.md` said nothing about it at all — while already using
|
|
1443
|
+
`sohl.kbcat AS _section` as the worked example of a content table, so the
|
|
1444
|
+
specification demonstrated the key without ever defining it.
|
|
1445
|
+
|
|
1446
|
+
It is now described beside the compendium folder, as the one key in that section
|
|
1447
|
+
that answers _where does this appear_ for the web rather than for Foundry:
|
|
1448
|
+
`pack` and `packFolder` place a document in a compendium, `kbcat` places a page
|
|
1449
|
+
in a list. Nothing compiles it — it reaches a published page because frontmatter
|
|
1450
|
+
is copied onto that page, where a layout groups by it.
|
|
1451
|
+
|
|
1452
|
+
Three things an author cannot infer from the corpus are stated:
|
|
1453
|
+
|
|
1454
|
+
- **It is editorial, and independent of `subType`.** Neither is derived from the
|
|
1455
|
+
other, and `kbcat` both subdivides a subtype — `trauma`/`physcond` lists as
|
|
1456
|
+
`physdisability`, `physfeature` or `physprivations` — and renames one for
|
|
1457
|
+
display, `trauma`/`fear` listing under `phobias`. Most notes carrying a `kbcat`
|
|
1458
|
+
declare no `subType` at all, so a disagreement between the two is not an error
|
|
1459
|
+
to correct.
|
|
1460
|
+
- **The value is free-form and nothing validates it.** There is no configured
|
|
1461
|
+
list of categories, and the frontmatter check knows only that `kbcat` is a key
|
|
1462
|
+
every type may write. So a misspelled category is not a build error and is not
|
|
1463
|
+
dropped — it silently becomes a group of one.
|
|
1464
|
+
- **A note that writes none is dropped from the list entirely.** Grouping is by
|
|
1465
|
+
the key, so a page with no value falls in no group and is simply absent from
|
|
1466
|
+
the list page — not last, not under a fallback heading — with nothing reported
|
|
1467
|
+
at either build. Every note of a listed type in SoHL's tree carries one today
|
|
1468
|
+
and nothing here enforces that, which is one of the questions the content index
|
|
1469
|
+
exists to answer.
|
|
1470
|
+
- 09a2112: **`pack` and the template priority are documented** (#264).
|
|
1471
|
+
|
|
1472
|
+
`docs/content-format.md` is the published statement of what a note may write, and
|
|
1473
|
+
`pack` — one of the five universal keys, which every note type may write and the
|
|
1474
|
+
router has always read — appeared in it only as one incidental sentence inside
|
|
1475
|
+
another type's section.
|
|
1476
|
+
|
|
1477
|
+
It is now described where it belongs, beside the compendium folder: what it
|
|
1478
|
+
names, that `<system>.pack` overrides it for one system, that an unstated one
|
|
1479
|
+
falls back to the pack of its type marked `default: true`, and the three
|
|
1480
|
+
declarations that are refused — a companion pack, a pack nothing answers to, and
|
|
1481
|
+
a pack of another document type.
|
|
1482
|
+
|
|
1483
|
+
The **template priority** is documented too, and it needed more than a field
|
|
1484
|
+
description: it is a priority, and priorities only mean something against the
|
|
1485
|
+
ranges that divide them. Opening a Create dialog gathers candidates from the
|
|
1486
|
+
world and every matching compendium — other modules' included — filters them to
|
|
1487
|
+
the `(type, subType)` being created, dedups by `shortcode`, and takes the highest
|
|
1488
|
+
priority, breaking ties by nearest source and then a stable UUID. So the
|
|
1489
|
+
specification now states the reserved ranges: `0`–`98` for SoHL and HM3, `99`–`999`
|
|
1490
|
+
for other HeroicLands packages, `1000`+ for everyone else. Since the highest wins,
|
|
1491
|
+
anyone else's template always beats content shipped from here — which is the point.
|
|
1492
|
+
|
|
1493
|
+
It also records what the tri-state costs: the field is **required** on every note
|
|
1494
|
+
SoHL compiles into an Item or an Actor, because "not a template" has to be said
|
|
1495
|
+
rather than left out, and `0` is a real priority — the one SoHL's own templates
|
|
1496
|
+
ship at — rather than an absence. And that HM3 keeps it in `flags.hm3`, its data
|
|
1497
|
+
model having no field for it, where SoHL keeps it in `system`.
|
|
1498
|
+
|
|
1499
|
+
**The name is now `templatePriority` on all three sides** — the authored key,
|
|
1500
|
+
`system.templatePriority` and `flags.hm3.templatePriority`. It had been three
|
|
1501
|
+
different things: a note writes `archetype`, the mapping table said
|
|
1502
|
+
`data.templatePriority`, and the SoHL target said `system.template`. The
|
|
1503
|
+
specification and the schema fixture are normalized here; the build reads both
|
|
1504
|
+
spellings through the transition (#266), and the system's own rename is
|
|
1505
|
+
`Song-of-Heroic-Lands-FoundryVTT#1836`. That is more than a rename — it frees the
|
|
1506
|
+
word, because `archetype` is being repurposed for a different idea entirely: the
|
|
1507
|
+
_sort_ a character is, which is a kind and not a priority.
|
|
1508
|
+
|
|
1509
|
+
`kbcat` remains, and #264 tracks it: read 51 times across SoHL's knowledgebase
|
|
1510
|
+
layouts, and the specification has never mentioned it.
|
|
1511
|
+
- 0e8b414: **A folder's `parent` may be a map keyed by pack, and the lint now agrees**
|
|
1512
|
+
(#288). `folderFields()` has read both forms since #276 — a folder's _identity_
|
|
1513
|
+
is one thing and its _hierarchy_ another, and both large trees file the same
|
|
1514
|
+
folder under a different parent in the items pack and the journals pack. The
|
|
1515
|
+
vocabulary typed the field as a bare `LINK`, so `content-build lint` required a
|
|
1516
|
+
scalar and rejected every note using the form the specification prescribes: 46
|
|
1517
|
+
findings against `sohl-thalorna` and 3 here, exactly the notes #276 documents as
|
|
1518
|
+
its motivating cases and no others. Every note using the form was a finding, and
|
|
1519
|
+
no note using it was not.
|
|
1520
|
+
|
|
1521
|
+
`parent` is declared `scalar-or-map` now, and a map written in that form is
|
|
1522
|
+
checked **entry by entry** rather than as one value — the correction an author
|
|
1523
|
+
has to make is one pack's address, not the whole map, and quoting the map back
|
|
1524
|
+
named every entry that was right alongside the one that was not. An explicit `~`
|
|
1525
|
+
under a pack key still means _at the root there_, which is a different statement
|
|
1526
|
+
from saying nothing.
|
|
1527
|
+
|
|
1528
|
+
**A pack key naming no declared pack is a finding of its own.** Nothing checked
|
|
1529
|
+
it before, because the whole value was rejected before it was read. It is not a
|
|
1530
|
+
harmless surplus: the compile asks the map for the pack it is writing and falls
|
|
1531
|
+
back to `default` when there is no such key, so a mistyped `journal:` filed the
|
|
1532
|
+
folder wherever the default put it — exactly the hierarchy the key was written
|
|
1533
|
+
to override, and silently. `content-build lint` passes the configured pack names
|
|
1534
|
+
(companions included) for the check; a caller that supplies none makes no claim
|
|
1535
|
+
about the keys, as it already does for the vocabulary itself.
|
|
1536
|
+
|
|
1537
|
+
The specification's `### type: folder` table types `parent` as the scalar-or-map
|
|
1538
|
+
it is, so the two cannot disagree again from opposite sides of one field.
|
|
1539
|
+
- 655d901: **`content-format notes` reads the content index, and with it every check
|
|
1540
|
+
does** (#243).
|
|
1541
|
+
|
|
1542
|
+
It was the last one measuring the tree for itself, and it is the check whose
|
|
1543
|
+
whole output is a _count_ — 3,812 findings across `sohl`'s 1,685 notes. A report
|
|
1544
|
+
that measures the corpus against the declared vocabulary has to be looking at
|
|
1545
|
+
the corpus the compile will build, or its counts describe a tree nobody ships.
|
|
1546
|
+
|
|
1547
|
+
It measures what the author wrote, recovered with `authoredFrontmatter`: this
|
|
1548
|
+
compares a note's fields against a declared vocabulary, and `address:` is in no
|
|
1549
|
+
vocabulary. It also joins the shared contract the other checks now have — a note
|
|
1550
|
+
the index cannot record is reported with its position and the rest of the tree
|
|
1551
|
+
is still measured.
|
|
1552
|
+
|
|
1553
|
+
`bin/content-build.mjs` no longer walks a content tree at all.
|
|
1554
|
+
|
|
1555
|
+
_Findings are unchanged: 3,812 across 1,685 notes, identical._
|
|
1556
|
+
- 0651b1a: **An HM3 item records its template priority, as an HM3 actor already did**
|
|
1557
|
+
(#283).
|
|
1558
|
+
|
|
1559
|
+
`data.templatePriority` is the shared statement that a note is a starting
|
|
1560
|
+
template, and the specification states it as a row every type maps:
|
|
1561
|
+
`system.templatePriority` in SoHL, `flags.hm3.templatePriority` in HM3. Only
|
|
1562
|
+
HM3's **Actor** pass made that mapping. Its Item pass emitted whatever `flags`
|
|
1563
|
+
the note itself authored and nothing more, so an item note declaring the
|
|
1564
|
+
priority compiled into a SoHL item that knew it was a template and an HM3 item
|
|
1565
|
+
that did not.
|
|
1566
|
+
|
|
1567
|
+
**It was silent on both sides of the build.** The note is well-formed and the
|
|
1568
|
+
pack compiles; an omitted flag is exactly how this system says _not a template_,
|
|
1569
|
+
so a lost priority and a deliberate one are the same output. Nor could the
|
|
1570
|
+
emitted-key check see it — that compares what a pass writes against the
|
|
1571
|
+
receiving **schema**, and a flag is declared by no schema. Every gear, skill and
|
|
1572
|
+
trauma note in a tree carrying an `hm3:` block was affected, which is most of
|
|
1573
|
+
them.
|
|
1574
|
+
|
|
1575
|
+
**The rule now lives in one place.** `hm3/template-priority.mjs` holds it and
|
|
1576
|
+
both passes call it, rather than each carrying a copy — two copies being two
|
|
1577
|
+
chances to diverge again, which is the failure being fixed. `SystemItemCompiler`
|
|
1578
|
+
gains a `commonFlags()` hook alongside `commonSystem()`, defaulting to the
|
|
1579
|
+
authored flags alone, so a system that keeps a shared fact in flags has a seam
|
|
1580
|
+
to say so at.
|
|
1581
|
+
- e4c16ad: **The item catalogue picks the newest cached version numerically, not by string
|
|
1582
|
+
sort** (#272).
|
|
1583
|
+
|
|
1584
|
+
`foreignItemCatalogDirs` chose among several cached versions of a dependency
|
|
1585
|
+
with a plain `sort()` over directory names of the form `<id>@<version>`. That is
|
|
1586
|
+
a **string** comparison, so `sohl@0.8.10` sorts _before_ `sohl@0.8.2` and the
|
|
1587
|
+
build resolved its embedded item references against the older catalogue.
|
|
1588
|
+
|
|
1589
|
+
Nothing reported it, and nothing could: both caches are complete and stamped,
|
|
1590
|
+
and the older one is a perfectly valid catalogue — it simply answers for the
|
|
1591
|
+
wrong version. Several versions coexist whenever a pinned version is raised
|
|
1592
|
+
without clearing `build/cache/foreign`, which is the ordinary case, since a
|
|
1593
|
+
fetch writes the newly declared version beside the old one rather than replacing
|
|
1594
|
+
it.
|
|
1595
|
+
|
|
1596
|
+
The content-index cache already compared version segments numerically for
|
|
1597
|
+
exactly this reason. That comparison is now shared as `newestVersionDir` rather
|
|
1598
|
+
than written once per cache: two copies were two chances to get it wrong, and
|
|
1599
|
+
this is the copy that was wrong.
|
|
1600
|
+
- d17dc98: **One reader for a note's anchors.** `engine/content-links.mjs` kept its own,
|
|
1601
|
+
and it disagreed with the content index's: it matched `{#([a-z0-9-]+)}` where
|
|
1602
|
+
`collectAnchors` matches `{#([^}]+)}`. So an anchor with a capital in it —
|
|
1603
|
+
`{#CalendarFormat}`, three of them in `sohl`'s own content — existed for the
|
|
1604
|
+
index and for the compilers, and did not exist for the check of them.
|
|
1605
|
+
|
|
1606
|
+
Nothing links to one today, so the disagreement was latent. The first link to
|
|
1607
|
+
one would have been reported dead against a heading plainly present in the
|
|
1608
|
+
file, which is the worst shape a finding can take.
|
|
1609
|
+
|
|
1610
|
+
The specification puts no charset on the id — "`#id` represents an id anchor
|
|
1611
|
+
named `id`" — so the narrower pattern was this module's invention rather than a
|
|
1612
|
+
rule it was enforcing. That is the argument for one reader rather than a
|
|
1613
|
+
well-chosen one, and the first thing #243 asks for: the corpus and everything
|
|
1614
|
+
derived from it answered in one place.
|
|
1615
|
+
- e242701: **#241's closing note, checked and pinned.** It asked whether `vehicle` and
|
|
1616
|
+
`armorlocation` were second instances of the trap it reported — a type declared
|
|
1617
|
+
and validated that no pack can route. Neither is, and they are not the same case
|
|
1618
|
+
as each other:
|
|
1619
|
+
|
|
1620
|
+
- **`armorlocation` is HM3's.** The specification says "HM3 only", it has no SoHL
|
|
1621
|
+
form, and `hm3/document-subtypes.mjs` maps it. A SoHL configuration claiming
|
|
1622
|
+
it would be wrong, so its absence from `SOHL_DOCUMENT_SUBTYPES` is the answer
|
|
1623
|
+
rather than a gap. Nothing asserted that, so nothing would have noticed a row
|
|
1624
|
+
appearing there by mistake; now something does.
|
|
1625
|
+
- **`vehicle` is specified but not yet implemented, and says so.** A note of that
|
|
1626
|
+
type is already reported as _"the content format specifies `vehicle`, so the
|
|
1627
|
+
note is not wrong — this toolchain has not implemented the type yet … do not
|
|
1628
|
+
author the type until a release compiles it"_. That is the opposite of #241,
|
|
1629
|
+
where the failure was silent and misattributed to the note.
|
|
1630
|
+
|
|
1631
|
+
No behaviour change: this adds the two assertions and the reasoning, so the
|
|
1632
|
+
next reader does not have to re-derive it from four files.
|
|
1633
|
+
- dadbec3: **`content-build lint` and `content-build site` reach the content again** — they
|
|
1634
|
+
threw on the first note for every consumer.
|
|
1635
|
+
|
|
1636
|
+
#243 made `walkMarkdownTree`'s scope a required argument, which was the point:
|
|
1637
|
+
the default it removed read whichever configuration resolved from the working
|
|
1638
|
+
directory rather than the one the caller was working under. Two CLI callers had
|
|
1639
|
+
been living on that default and were not converted with the rest — so both
|
|
1640
|
+
commands failed immediately, reporting nothing about the tree.
|
|
1641
|
+
|
|
1642
|
+
This repository ships no content, so nothing in its suite had ever _run_ those
|
|
1643
|
+
commands over a tree; every test called the engine directly with arguments it
|
|
1644
|
+
supplied. A test now builds a small content tree and runs `lint`, `links` and
|
|
1645
|
+
`site` against it, asserting they reach the notes at all — and that they honour
|
|
1646
|
+
the configured `skipDirectories` rather than reading a directory the tree said
|
|
1647
|
+
to skip.
|
|
1648
|
+
- ae9ef89: **#243's two open questions, settled** — one by measurement, one by finding it
|
|
1649
|
+
had already been answered.
|
|
1650
|
+
|
|
1651
|
+
**1. The index does not record frontmatter-key positions, and should not.** The
|
|
1652
|
+
question was whether a pass reading the index could report a field defect
|
|
1653
|
+
without opening the note. The numbers are not close: over `sohl`'s 1,685 notes
|
|
1654
|
+
the index is **3.0 MB** and holds **50,598 leaf values**, so a `{line, column}`
|
|
1655
|
+
on each would add roughly **1.6 MB — a 54% larger artifact** — for data read
|
|
1656
|
+
only on the _failing_ path.
|
|
1657
|
+
|
|
1658
|
+
The rule that replaces it is the one the module was already built on: _the index
|
|
1659
|
+
carries what is **about** a note; the file carries the note's text and every
|
|
1660
|
+
position within it._ A pass needing either opens the file the record already
|
|
1661
|
+
names, which costs nothing it was not already paying — a check reads each note
|
|
1662
|
+
once for its body, and a compiler must read the prose regardless, so while it
|
|
1663
|
+
holds the bytes a position is free. An anchor's `line` is the exception that
|
|
1664
|
+
proves it: an anchor is structure a consumer addresses, not a locator for a
|
|
1665
|
+
diagnostic.
|
|
1666
|
+
|
|
1667
|
+
`noteFile(contentBase, record)` is the one composition of a record's absolute
|
|
1668
|
+
path. There were **four** copies of it — one in each reader converted by #290,
|
|
1669
|
+
#292, #294 and #296 — which is the duplication #243 exists to remove, arriving
|
|
1670
|
+
by the back door. The index records the path relatively on purpose (an absolute
|
|
1671
|
+
one is a fact about the build machine, and would put a home directory in a
|
|
1672
|
+
published artifact), so composing it is a real step and belongs in one place.
|
|
1673
|
+
|
|
1674
|
+
**2. "Not everything is a note" no longer holds.** The issue lists folder
|
|
1675
|
+
documents (`item-folders.yaml`) and the adventures that bundle scenes as
|
|
1676
|
+
configuration appearing in no record. Both became notes after it was written —
|
|
1677
|
+
folders in #260/#276, bundles in #263/#286 — and both are indexed today, a
|
|
1678
|
+
folder with its address and id, a bundle with the `Adventure` UUID it compiles
|
|
1679
|
+
into.
|
|
1680
|
+
|
|
1681
|
+
What a folder record does not carry is a Foundry address, and that is the right
|
|
1682
|
+
answer rather than a gap: a folder materialises in **every** pack holding a
|
|
1683
|
+
document that references it, so no one UUID identifies it, and emitting one
|
|
1684
|
+
would publish an `Item` UUID for a `Folder` at an id no document carries. A
|
|
1685
|
+
homepage carries none for the opposite reason — it compiles into no document at
|
|
1686
|
+
all. Both are now pinned at the **record**, which is the level a pass driven by
|
|
1687
|
+
the index reads, with a bundle as the positive control.
|
|
1688
|
+
|
|
1689
|
+
_No behaviour change: `lint`, `links`, `content-format notes` and both address
|
|
1690
|
+
maps are identical over `sohl`'s tree._
|
|
1691
|
+
- 2669b57: **`undeclaredPaths` stops descending at a declared leaf** — a declared path with
|
|
1692
|
+
no children holds _values_, not fields (#126).
|
|
1693
|
+
|
|
1694
|
+
A schema declares a path that has nothing beneath it for two ordinary reasons: a
|
|
1695
|
+
map with **dynamic keys** (a mystery's `skillAptitudes` is skill selector →
|
|
1696
|
+
modifier, an affiliation's `relations` is shortcode → standing) and a
|
|
1697
|
+
**TypedSchemaField** (`strikeModes`, discriminated by `type`). In both, what sits
|
|
1698
|
+
under the path is data an author wrote, not paths the schema names — so walking
|
|
1699
|
+
into one reports every entry as an undeclared `system` key.
|
|
1700
|
+
|
|
1701
|
+
It stayed invisible because those maps are authored _outside_ `<system>.system`
|
|
1702
|
+
today, where nothing walks them. The moment a note authors one at the
|
|
1703
|
+
destination — which is what #126's corpus move does — each entry becomes a
|
|
1704
|
+
finding: `sohl.system.skillAptitudes.zepharis`, `…strikeModes.impale`, one per
|
|
1705
|
+
key. Measured on a migrated `sohl-thalorna`: **324 findings, none of them a
|
|
1706
|
+
defect**, and they would have made the migration look like it had broken 62
|
|
1707
|
+
notes.
|
|
1708
|
+
|
|
1709
|
+
Descent is now conditional on the schema declaring something _beneath_ the path.
|
|
1710
|
+
`body.structure` declares `parts` and `zones`, so it is a real container and an
|
|
1711
|
+
undeclared `adjacent` under it is still reported; `skillAptitudes` declares
|
|
1712
|
+
nothing beneath it, so its contents are a value.
|
|
1713
|
+
|
|
1714
|
+
## 17.2.0
|
|
1715
|
+
|
|
1716
|
+
### Minor Changes
|
|
1717
|
+
|
|
1718
|
+
- ce40274: **The content index now carries every address the link manifest does**, which
|
|
1719
|
+
is the substance of folding the two artifacts into one (#239).
|
|
1720
|
+
|
|
1721
|
+
Each record gains a `foundry` block — `{ uuid, anchors }` — and **an item note
|
|
1722
|
+
now emits two records**: the item, and its documentation journal. The journal is
|
|
1723
|
+
a document in its own right, with its own canonical address
|
|
1724
|
+
(`doc<type>/<shortcode>`), its own UUID and its own pages, so it gets its own
|
|
1725
|
+
record rather than being nested inside the item's. Resolving
|
|
1726
|
+
`docaffliction/blkdth` is then the same lookup as resolving anything else,
|
|
1727
|
+
instead of the one address in the index reachable only by knowing to look
|
|
1728
|
+
somewhere else.
|
|
1729
|
+
|
|
1730
|
+
The two are linked in both directions: the item carries `documentation`, the
|
|
1731
|
+
journal carries `documents`.
|
|
1732
|
+
|
|
1733
|
+
**The journal's record is lean, and deliberately not the note's frontmatter.**
|
|
1734
|
+
The item's `sohl:` block describes the item; copying it onto the journal would
|
|
1735
|
+
assert things about the journal that are not true, and double the file to do it.
|
|
1736
|
+
The journal carries its addresses, its name, its anchors and the file it came
|
|
1737
|
+
from.
|
|
1738
|
+
|
|
1739
|
+
**Derived by the manifest's own code, not a second implementation.** A UUID is a
|
|
1740
|
+
function of the note's `type`, its authored `id` and the pack router —
|
|
1741
|
+
frontmatter and configuration, nothing from a compiled pack — so the index's
|
|
1742
|
+
existing walk already had every input. Verified against `sohl`: **2,988 UUIDs
|
|
1743
|
+
and 1,510 anchor maps, matching the manifest exactly, with none missing on
|
|
1744
|
+
either side.**
|
|
1745
|
+
|
|
1746
|
+
Two smaller changes fall out of it:
|
|
1747
|
+
|
|
1748
|
+
- `emitContentIndex` reports `notes` and `records` separately, because an item
|
|
1749
|
+
note is one note and two records and reporting one as the other overstates the
|
|
1750
|
+
tree. Records sort by path, then canonical address, then id — the address
|
|
1751
|
+
before the id, because an item's two records share a file and only one carries
|
|
1752
|
+
an id.
|
|
1753
|
+
- `entriesForNote` takes `docEntryTypes` from its context instead of reading the
|
|
1754
|
+
ambient configuration. It is what `manifestContext` already promised — "the
|
|
1755
|
+
pass itself is a pure function of its context" — and it was not true: the
|
|
1756
|
+
emitter consulted whichever configuration `loadPackConfig()` found, not the one
|
|
1757
|
+
it was handed. A fixture that never declared the type it asserted was passing
|
|
1758
|
+
on that leak.
|
|
1759
|
+
|
|
1760
|
+
### Patch Changes
|
|
1761
|
+
|
|
1762
|
+
- 6a3d083: **`package-build labels check` crashed for every consumer in 17.1.0.**
|
|
1763
|
+
`labels.mjs` was added and not listed in `package.json` `files`, which is an
|
|
1764
|
+
explicit whitelist, so the module never reached the tarball and the command
|
|
1765
|
+
threw `ERR_MODULE_NOT_FOUND` on the import the release notes had just
|
|
1766
|
+
announced.
|
|
1767
|
+
|
|
1768
|
+
Adds the module to `files`, and a guard so the class cannot recur: a new test
|
|
1769
|
+
reads the root-relative imports out of `bin/` and requires each one to be
|
|
1770
|
+
published, by name or by a containing directory entry. Verified by removing
|
|
1771
|
+
`labels.mjs` from `files` again and watching it fail.
|
|
1772
|
+
|
|
1773
|
+
The failure could only appear in a consumer, after publish — locally the file
|
|
1774
|
+
is simply there, so every check passed.
|
|
1775
|
+
|
|
3
1776
|
## 17.1.0
|
|
4
1777
|
|
|
5
1778
|
### Minor Changes
|