@heroiclands/package-build 20.3.1 → 20.5.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 +351 -0
- package/CONTENT.md +322 -110
- package/README.md +21 -10
- package/bin/content-build.mjs +204 -92
- package/bin/package-build.mjs +48 -15
- package/config.mjs +6 -8
- package/content-config.mjs +518 -151
- package/docs/api.md +1352 -0
- package/docs/commands.md +1609 -0
- package/docs/configuration.md +1432 -0
- package/docs/content-format.md +73 -74
- package/docs/diagnostics.md +356 -0
- package/docs/getting-started.md +813 -0
- package/docs/project-setup.md +469 -0
- package/e2e.mjs +4 -4
- package/engine/actor-compiler.mjs +64 -61
- package/engine/address-charset.mjs +6 -6
- package/engine/address-diff.mjs +57 -52
- package/engine/anchored-sections.mjs +1 -1
- package/engine/anchors.mjs +1 -1
- package/engine/base-compiler.mjs +42 -38
- package/engine/bundle-notes.mjs +4 -4
- package/engine/bundles.mjs +17 -8
- package/engine/code-fences.mjs +3 -3
- package/engine/compendiums.mjs +8 -8
- package/engine/compile-corpus.mjs +4 -5
- package/engine/content-address.mjs +24 -24
- package/engine/content-charset.mjs +1 -1
- package/engine/content-format-check.mjs +24 -25
- package/engine/content-format.mjs +8 -8
- package/engine/content-html.mjs +202 -0
- package/engine/content-icons.mjs +196 -165
- package/engine/content-index.mjs +61 -40
- package/engine/content-links.mjs +74 -41
- package/engine/content-lint.mjs +64 -34
- package/engine/content-package.mjs +7 -9
- package/engine/content-slug.mjs +2 -2
- package/engine/content-tables.mjs +38 -34
- package/engine/content-tree.mjs +1 -1
- package/engine/derived-fields.mjs +174 -0
- package/engine/diagnostics.mjs +3 -4
- package/engine/document-subtypes.mjs +6 -7
- package/engine/field-reference.mjs +8 -8
- package/engine/field-spec.mjs +29 -30
- package/engine/folder-notes.mjs +18 -16
- package/engine/foreign-catalog.mjs +8 -9
- package/engine/foundry-entries.mjs +19 -20
- package/engine/frontmatter-lint.mjs +95 -103
- package/engine/frontmatter.mjs +11 -11
- package/engine/generate.mjs +44 -39
- package/engine/helpers.mjs +147 -84
- package/engine/homepage.mjs +20 -22
- package/engine/ids.mjs +15 -15
- package/engine/index-records.mjs +4 -4
- package/engine/index.mjs +26 -10
- package/engine/item-compiler.mjs +54 -29
- package/engine/item-docs.mjs +11 -13
- package/engine/item-registry.mjs +13 -13
- package/engine/journals.mjs +24 -15
- package/engine/macros.mjs +3 -3
- package/engine/map-notes.mjs +6 -6
- package/engine/metadata-index.mjs +5 -5
- package/engine/note-claims.mjs +51 -41
- package/engine/note-ids.mjs +5 -5
- package/engine/note-package.mjs +4 -4
- package/engine/note-renames.mjs +3 -3
- package/engine/note-schemas.mjs +3 -8
- package/engine/note-vocabulary.mjs +56 -54
- package/engine/pack-config.mjs +39 -25
- package/engine/pack-router.mjs +0 -0
- package/engine/pdf-build.mjs +464 -0
- package/engine/pdf-fonts.mjs +420 -0
- package/engine/pdf-render.mjs +798 -0
- package/engine/pdf-toc.mjs +525 -0
- package/engine/prose-config.mjs +9 -9
- package/engine/prose-lint.mjs +12 -13
- package/engine/region-events.mjs +2 -2
- package/engine/retired-fields.mjs +24 -22
- package/engine/runtime-only-fields.mjs +1 -1
- package/engine/scene-levels.mjs +1 -1
- package/engine/scenes.mjs +33 -25
- package/engine/schema-check.mjs +21 -21
- package/engine/schema-extract.mjs +1 -1
- package/engine/site-build.mjs +66 -50
- package/engine/site-index.mjs +17 -18
- package/engine/sql-tables.mjs +21 -13
- package/engine/subtype-registry.mjs +5 -4
- package/engine/system-block.mjs +18 -19
- package/engine/systems.mjs +2 -2
- package/engine/web-wikilinks.mjs +41 -38
- package/engine/wikilink-syntax.mjs +16 -16
- package/engine/wikilinks.mjs +40 -42
- package/hm3/actors.mjs +30 -22
- package/hm3/document-subtypes.mjs +5 -5
- package/hm3/index.mjs +2 -2
- package/hm3/item-builders.mjs +1 -1
- package/hm3/item-fields.mjs +3 -3
- package/hm3/items.mjs +28 -8
- package/hm3/template-priority.mjs +2 -2
- package/lang.mjs +3 -3
- package/manifest.mjs +17 -20
- package/package.json +1 -2
- package/release.mjs +66 -6
- package/sohl/actors.mjs +36 -28
- package/sohl/affiliation-standings.mjs +2 -2
- package/sohl/being-info.mjs +5 -5
- package/sohl/default-item-art.mjs +5 -5
- package/sohl/document-subtypes.mjs +5 -5
- package/sohl/index.mjs +3 -3
- package/sohl/item-builders.mjs +7 -7
- package/sohl/item-fields.mjs +8 -9
- package/sohl/items.mjs +28 -6
- package/sohl/kb-passes.mjs +5 -5
- package/sohl/note-schemas.mjs +12 -12
- package/sohl/skill-base.mjs +1 -1
- package/types/content-config.d.mts +91 -39
- package/types/e2e.d.mts +3 -3
- package/types/engine/actor-compiler.d.mts +45 -40
- package/types/engine/address-charset.d.mts +6 -6
- package/types/engine/address-diff.d.mts +60 -6
- package/types/engine/base-compiler.d.mts +27 -21
- package/types/engine/bundle-notes.d.mts +3 -3
- package/types/engine/bundles.d.mts +10 -1
- package/types/engine/code-fences.d.mts +3 -3
- package/types/engine/compendiums.d.mts +3 -3
- package/types/engine/compile-corpus.d.mts +1 -1
- package/types/engine/content-address.d.mts +20 -20
- package/types/engine/content-format-check.d.mts +6 -6
- package/types/engine/content-format.d.mts +2 -2
- package/types/engine/content-html.d.mts +78 -0
- package/types/engine/content-icons.d.mts +139 -92
- package/types/engine/content-index.d.mts +59 -15
- package/types/engine/content-links.d.mts +8 -8
- package/types/engine/content-lint.d.mts +8 -6
- package/types/engine/content-package.d.mts +6 -8
- package/types/engine/content-tables.d.mts +49 -18
- package/types/engine/derived-fields.d.mts +101 -0
- package/types/engine/diagnostics.d.mts +2 -2
- package/types/engine/document-subtypes.d.mts +3 -3
- package/types/engine/field-spec.d.mts +37 -39
- package/types/engine/folder-notes.d.mts +4 -5
- package/types/engine/foreign-catalog.d.mts +3 -3
- package/types/engine/foundry-entries.d.mts +6 -7
- package/types/engine/frontmatter-lint.d.mts +10 -9
- package/types/engine/frontmatter.d.mts +7 -7
- package/types/engine/generate.d.mts +7 -7
- package/types/engine/helpers.d.mts +128 -56
- package/types/engine/homepage.d.mts +16 -18
- package/types/engine/ids.d.mts +13 -13
- package/types/engine/index-records.d.mts +3 -3
- package/types/engine/index.d.mts +6 -0
- package/types/engine/item-compiler.d.mts +21 -5
- package/types/engine/item-docs.d.mts +2 -2
- package/types/engine/item-registry.d.mts +6 -6
- package/types/engine/journals.d.mts +12 -3
- package/types/engine/map-notes.d.mts +2 -2
- package/types/engine/metadata-index.d.mts +4 -4
- package/types/engine/note-claims.d.mts +30 -22
- package/types/engine/note-ids.d.mts +4 -4
- package/types/engine/note-package.d.mts +1 -1
- package/types/engine/note-renames.d.mts +3 -3
- package/types/engine/note-vocabulary.d.mts +31 -203
- package/types/engine/pack-config.d.mts +7 -7
- package/types/engine/pack-router.d.mts +1 -1
- package/types/engine/pdf-build.d.mts +42 -0
- package/types/engine/pdf-fonts.d.mts +30 -0
- package/types/engine/pdf-render.d.mts +144 -0
- package/types/engine/pdf-toc.d.mts +114 -0
- package/types/engine/prose-config.d.mts +9 -9
- package/types/engine/prose-lint.d.mts +3 -4
- package/types/engine/region-events.d.mts +2 -2
- package/types/engine/retired-fields.d.mts +10 -9
- package/types/engine/scenes.d.mts +10 -1
- package/types/engine/schema-check.d.mts +13 -13
- package/types/engine/site-build.d.mts +52 -25
- package/types/engine/site-index.d.mts +3 -4
- package/types/engine/sql-tables.d.mts +11 -5
- package/types/engine/subtype-registry.d.mts +3 -3
- package/types/engine/system-block.d.mts +3 -3
- package/types/engine/web-wikilinks.d.mts +7 -7
- package/types/engine/wikilink-syntax.d.mts +17 -17
- package/types/engine/wikilinks.d.mts +14 -14
- package/types/hm3/actors.d.mts +9 -1
- package/types/hm3/document-subtypes.d.mts +1 -1
- package/types/hm3/items.d.mts +9 -2
- package/types/hm3/template-priority.d.mts +1 -1
- package/types/manifest.d.mts +8 -8
- package/types/release.d.mts +15 -4
- package/types/sohl/actors.d.mts +12 -4
- package/types/sohl/affiliation-standings.d.mts +2 -2
- package/types/sohl/being-info.d.mts +2 -2
- package/types/sohl/document-subtypes.d.mts +1 -1
- package/types/sohl/items.d.mts +9 -2
- package/types/sohl/note-schemas.d.mts +1 -1
- package/MIGRATING.md +0 -608
package/CONTENT.md
CHANGED
|
@@ -25,8 +25,10 @@ A consuming repository declares one `package-build.config.yaml` at its root:
|
|
|
25
25
|
# every canonical address, the name of the content index it emits, and the
|
|
26
26
|
# package a cross-package wikilink writes to reach one of its notes.
|
|
27
27
|
contentPackage: thalorna
|
|
28
|
-
#
|
|
29
|
-
# asset root a note's `img:` resolves
|
|
28
|
+
# What kind of package this is. "systems" and "modules" are where Foundry
|
|
29
|
+
# installs it, and each decides the served asset root a note's `img:` resolves
|
|
30
|
+
# to — `modules/sohl-thalorna/assets/…`. "documentation" is a package Foundry
|
|
31
|
+
# never installs; see "A package that compiles nothing" below.
|
|
30
32
|
packageKind: modules
|
|
31
33
|
|
|
32
34
|
# The Foundry core range this package supports. `minimum` is stamped into every
|
|
@@ -65,6 +67,35 @@ itemBuilders: sohl
|
|
|
65
67
|
# Directory names the content walk ignores wherever they appear.
|
|
66
68
|
skipDirectories: [Templates]
|
|
67
69
|
|
|
70
|
+
# Optional. The icon fonts this package ships and the names its notes draw from
|
|
71
|
+
# them. Nothing is supplied by default: an entry is a promise that a glyph will
|
|
72
|
+
# render, and only the package shipping the font can keep it.
|
|
73
|
+
#
|
|
74
|
+
# Either the registry itself, or a path to a file holding it — the file form is
|
|
75
|
+
# what a real package wants, because a registry is generated from what the
|
|
76
|
+
# interface draws and a generated document inlined here conflicts on every
|
|
77
|
+
# regeneration.
|
|
78
|
+
icons: assets/icon-registry.yaml
|
|
79
|
+
#
|
|
80
|
+
# The same, written inline:
|
|
81
|
+
#
|
|
82
|
+
# icons:
|
|
83
|
+
# families:
|
|
84
|
+
# # `class` is the stylesheet prefix; `styles` are the weights the font
|
|
85
|
+
# # ships, and `[]` means it has none.
|
|
86
|
+
# fontawesome: { class: fa, styles: [solid, regular, brands], describe: Font Awesome Free }
|
|
87
|
+
# game-icons: { class: ginf, styles: [], describe: the Game-Icons webfont }
|
|
88
|
+
# # Which family an entry with no `family` belongs to. Optional where exactly
|
|
89
|
+
# # one is declared, since then there is nothing to choose between.
|
|
90
|
+
# defaultFamily: fontawesome
|
|
91
|
+
# icons:
|
|
92
|
+
# # A name is what a note writes between the colons, in the charset an
|
|
93
|
+
# # address segment uses. `fixedWidth` asks for a full advance, which a
|
|
94
|
+
# # glyph like an ellipsis needs to sit in a column of controls.
|
|
95
|
+
# being: { style: solid, icon: user, label: being }
|
|
96
|
+
# context-menu: { style: solid, icon: ellipsis-vertical, fixedWidth: true, label: context menu }
|
|
97
|
+
# vehicle: { family: game-icons, icon: old-wagon, label: vehicle }
|
|
98
|
+
|
|
68
99
|
# Optional; each path is relative to this file's directory and defaults to the
|
|
69
100
|
# conventional layout shown here.
|
|
70
101
|
paths:
|
|
@@ -132,13 +163,13 @@ packageBuild:
|
|
|
132
163
|
assets:
|
|
133
164
|
- { from: assets/icons, to: assets/icons }
|
|
134
165
|
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
166
|
+
# How much of the package reaches the web, plus the address scheme both
|
|
167
|
+
# `manifest` and `site` derive addresses under. `site` is a mode, not a
|
|
168
|
+
# boolean: `homepage` (the default) publishes the authored homepage and no
|
|
169
|
+
# other page; `content` publishes it plus every page the tree compiles to, and
|
|
170
|
+
# is also what builds the book.
|
|
139
171
|
publish:
|
|
140
172
|
site: content
|
|
141
|
-
manifests: { publish: true, consume: true }
|
|
142
173
|
address:
|
|
143
174
|
prefix: kb/
|
|
144
175
|
|
|
@@ -153,8 +184,8 @@ site:
|
|
|
153
184
|
|
|
154
185
|
The loader validates the document, resolves every path against the directory
|
|
155
186
|
the file sits in, fills the optional halves with their defaults
|
|
156
|
-
(`skipDirectories: []`, `packageBuild: {}`, the conventional `paths
|
|
157
|
-
|
|
187
|
+
(`skipDirectories: []`, `packageBuild: {}`, the conventional `paths` and
|
|
188
|
+
`publish.site` at its `homepage` floor),
|
|
158
189
|
derives `assetRoot`, `packDirectories`, `itemTypes` and `docEntryTypes`, and
|
|
159
190
|
freezes the result. A malformed configuration throws a `TypeError` naming the
|
|
160
191
|
offending field — and the line and column it was written on, in the
|
|
@@ -183,6 +214,69 @@ never existed. A module declaring no usable system relationship fails the build
|
|
|
183
214
|
rather than guessing — a wrong `_stats.systemVersion` is invisible until
|
|
184
215
|
something migrates on it.
|
|
185
216
|
|
|
217
|
+
### A package that compiles nothing
|
|
218
|
+
|
|
219
|
+
`packageKind: documentation` is the answer "not a Foundry package at all". It
|
|
220
|
+
publishes a website and the book built from the same notes, and compiles no
|
|
221
|
+
compendium: nothing is installed into a Foundry data directory, so there is no
|
|
222
|
+
manifest, no pack and no document.
|
|
223
|
+
|
|
224
|
+
```yaml
|
|
225
|
+
contentPackage: toolkit
|
|
226
|
+
packageKind: documentation
|
|
227
|
+
|
|
228
|
+
# Required, and `content`: publishing the tree is the whole of what this kind
|
|
229
|
+
# does. `homepage` would leave one authored page, no book and nothing compiled.
|
|
230
|
+
publish:
|
|
231
|
+
site: content
|
|
232
|
+
address: { prefix: guide/ }
|
|
233
|
+
|
|
234
|
+
site:
|
|
235
|
+
out: site/content
|
|
236
|
+
|
|
237
|
+
pdf:
|
|
238
|
+
title: The Toolkit
|
|
239
|
+
document: book.yaml
|
|
240
|
+
fonts: { serif: Libertinus Serif }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`contentPackage`, `paths`, `skipDirectories`, `icons`, `site`, `pdf` and
|
|
244
|
+
`packageBuild` mean exactly what they mean anywhere else. **Every key that
|
|
245
|
+
describes a Foundry package is refused**, by name, with the line and column it
|
|
246
|
+
was written on — `packs`, `itemBuilders`, `docs`, `compatibility`,
|
|
247
|
+
`relationships`, `systems`, `requiresSystem`, `stats` and `foundryPackage`:
|
|
248
|
+
|
|
249
|
+
```text
|
|
250
|
+
package-build.config.yaml:7:1: error: package-build config: `packs` is refused in a `documentation` package, which compiles no compendium, so there are no packs to declare.
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
A key that cannot mean anything in this shape fails at load rather than being
|
|
254
|
+
read and ignored, which is the difference between a configuration that is wrong
|
|
255
|
+
and a build that is quietly wrong.
|
|
256
|
+
|
|
257
|
+
What follows from the kind:
|
|
258
|
+
|
|
259
|
+
- **`foundryPackage` is not derived.** For every other kind the loader reads it
|
|
260
|
+
from the adjacent `package.json`; there is no Foundry package here to carry an
|
|
261
|
+
id, so `foundryPackage` and `assetRoot` are both `null`. A note's `img:`
|
|
262
|
+
therefore names the owning package (`systems/…`, `modules/…`) or a URL — a
|
|
263
|
+
path this package would have to serve itself is refused, because Foundry
|
|
264
|
+
serves no files for a package it does not install.
|
|
265
|
+
- **`package-build manifest` refuses**, rather than emitting a `module.json`
|
|
266
|
+
advertising an installable package with no id, no packs and no compatibility
|
|
267
|
+
range. So does `content-build package compile`, which would otherwise exit 0
|
|
268
|
+
having compiled nothing.
|
|
269
|
+
- **Notes need no `id:`.** An id is derived from a note's address, and a
|
|
270
|
+
hand-assigned one is only ever a pin for a document that already shipped —
|
|
271
|
+
which needs a compendium, and there is none.
|
|
272
|
+
- **The note vocabulary is `doc` and `homepage`.** Every other type exists to
|
|
273
|
+
become a Foundry document, so a note carrying one has no destination;
|
|
274
|
+
`content-build lint` reports it at its `type:` line.
|
|
275
|
+
- **A content index is still published.** `content-build content-index` emits
|
|
276
|
+
`<contentPackage>-metadata.jsonl` exactly as it does elsewhere, so another
|
|
277
|
+
package can resolve an address into this one and a wikilink from its notes can
|
|
278
|
+
reach a page here.
|
|
279
|
+
|
|
186
280
|
### A note's package is the repository's, not the note's
|
|
187
281
|
|
|
188
282
|
`contentPackage` is the **address namespace** every note in the tree is
|
|
@@ -211,7 +305,7 @@ package's id to _be_ its system id.
|
|
|
211
305
|
Because it is a segment of an address, the value is **validated** rather than
|
|
212
306
|
taken as written, and a violation fails the build naming the line it is on:
|
|
213
307
|
|
|
214
|
-
- **
|
|
308
|
+
- **Lowercase alphanumeric** (`^[a-z0-9]+$`). An address is read by counting
|
|
215
309
|
hyphen-separated segments, so the hyphen has to be purely a separator — which
|
|
216
310
|
is why `harn-adventures` is configured as `harnadventures`. This is the same
|
|
217
311
|
rule `shortcode` is already held to, and the two are one constant.
|
|
@@ -228,7 +322,7 @@ taken as written, and a violation fails the build naming the line it is on:
|
|
|
228
322
|
refused.
|
|
229
323
|
|
|
230
324
|
```text
|
|
231
|
-
package-build.config.yaml:1:1: error: package-build config: `contentPackage` is `harn-adventures`, which is not alphanumeric. It is the first segment of every address this package publishes (`harn-adventures-<system>-<type>-<shortcode>`), and an address is read by counting hyphen-separated segments — so anything outside
|
|
325
|
+
package-build.config.yaml:1:1: error: package-build config: `contentPackage` is `harn-adventures`, which is not lowercase alphanumeric (^[a-z0-9]+$). It is the first segment of every address this package publishes (`harn-adventures-<system>-<type>-<shortcode>`), and an address is read by counting hyphen-separated segments — so anything outside that here makes those addresses unreadable rather than merely ugly. `harn-adventures` became `harnadventures`.
|
|
232
326
|
```
|
|
233
327
|
|
|
234
328
|
**`package:` in a note's frontmatter is retired, and declaring it fails the
|
|
@@ -283,7 +377,7 @@ export default defineConfig({
|
|
|
283
377
|
foundryPackage: "sohl-kethira-basic",
|
|
284
378
|
packageKind: "modules",
|
|
285
379
|
compatibility: { minimum: "14.359", verified: "14.364" },
|
|
286
|
-
stats: {
|
|
380
|
+
stats: { lastModifiedBy: "…" },
|
|
287
381
|
itemBuilders: ITEM_BUILDERS,
|
|
288
382
|
packs: [{ name: "items", type: "Item" }],
|
|
289
383
|
});
|
|
@@ -349,7 +443,7 @@ pack: mysteries
|
|
|
349
443
|
that type's default implicitly; a type with several designates one with
|
|
350
444
|
`default: true`. Where several exist and none is marked, a declaration is
|
|
351
445
|
**mandatory** and an undeclared note fails the build.
|
|
352
|
-
- **A default is per system, not merely per type
|
|
446
|
+
- **A default is per system, not merely per type**. The rule above counts
|
|
353
447
|
every pack of a type together, so a tree shipping one Actor pack per system has
|
|
354
448
|
two and would need a flag — except that asked _per system_ the layout is
|
|
355
449
|
unambiguous, one pack each, for the same reason a single-pack type needs no
|
|
@@ -373,11 +467,11 @@ pack: mysteries
|
|
|
373
467
|
it — an item's or a macro's prose, which compiles into a JournalEntry of its
|
|
374
468
|
own — lands in the default pack of _that_ type.
|
|
375
469
|
- **Every document a note produces needs a pack, and one that has none is a
|
|
376
|
-
finding
|
|
470
|
+
finding**. A note produces more than one document as a matter of
|
|
377
471
|
course: an item note an Item and the JournalEntry its prose becomes, a map
|
|
378
472
|
note a Scene and a JournalEntry, an actor note an Actor and a JournalEntry
|
|
379
|
-
|
|
380
|
-
document
|
|
473
|
+
too. Where the configuration declares no pack for one of them, that
|
|
474
|
+
document would be dropped while the rest of the note compiled into a pack
|
|
381
475
|
that does exist — a build that succeeds and ships half of what was written.
|
|
382
476
|
The finding names the note, the class with no pack, and the class that did
|
|
383
477
|
compile, which is what distinguishes it from a note **nothing** claims: that
|
|
@@ -388,7 +482,7 @@ pack: mysteries
|
|
|
388
482
|
no doc — so a tree of deliberately description-less items loses nothing by
|
|
389
483
|
having no JournalEntry pack, and is told nothing. And it names no system, so a
|
|
390
484
|
type one system maps and another does not stays silent for the system that
|
|
391
|
-
declines it
|
|
485
|
+
declines it.
|
|
392
486
|
|
|
393
487
|
**The configuration is found by walking up from the working directory, and from
|
|
394
488
|
the installed package only if that finds nothing.** `engine/pack-config.mjs`
|
|
@@ -402,7 +496,7 @@ with **no `node_modules` of its own** resolves `@heroiclands/package-build` out
|
|
|
402
496
|
of the parent's, because Node's resolution walks parent directories. Climbing
|
|
403
497
|
from the module then landed on the _parent's_ configuration, and the build
|
|
404
498
|
compiled the parent's content tree into the parent's `build/` and exited 0
|
|
405
|
-
|
|
499
|
+
. When both walks find a configuration and they disagree, the working
|
|
406
500
|
directory's is read and the ignored one is named in a warning — that
|
|
407
501
|
disagreement is also the cheapest signal that this tree is building on another
|
|
408
502
|
checkout's `node_modules`. Run `npm ci` in the worktree to give it its own.
|
|
@@ -435,7 +529,7 @@ resolved set at runtime; the compilers and the link-manifest emitter cannot come
|
|
|
435
529
|
to disagree about which notes carry documentation.
|
|
436
530
|
|
|
437
531
|
**Every note compiling into a system-bearing document carries documentation, and
|
|
438
|
-
that includes actors
|
|
532
|
+
that includes actors**. `docEntryTypes` is `itemTypes` plus the actor
|
|
439
533
|
types the shipped subtype maps declare (`ACTOR_TYPES`, derived from them rather
|
|
440
534
|
than listed again), plus `macro` and the map types. Only `doc` is outside it, for
|
|
441
535
|
the reason that actually applies to it: its single document _is_ the prose.
|
|
@@ -487,7 +581,7 @@ than one registry declares, **throws** rather than answering with whichever was
|
|
|
487
581
|
declared first.
|
|
488
582
|
|
|
489
583
|
**`hm3` is a real registry, and the rest of the pipeline follows the same
|
|
490
|
-
field.**
|
|
584
|
+
field.** This package ships two system halves, `sohl/` and `hm3/`,
|
|
491
585
|
each with its own builders, its own default art and its own note-type →
|
|
492
586
|
document-subtype map; they share the engine between them and import nothing from
|
|
493
587
|
each other. A pack's `system:` is what selects among them — the Item and Actor
|
|
@@ -513,10 +607,9 @@ what its documents are stamped `_stats.systemId` and `systemVersion` from, and
|
|
|
513
607
|
the version can only come from that block — or, for a package whose packs are
|
|
514
608
|
all for its own system, from the package-wide stats. A pack naming a system that
|
|
515
609
|
resolves to neither is refused at configuration time, naming the pack and the
|
|
516
|
-
entry to add.
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
configuration says `system: sohl` on the line above.
|
|
610
|
+
entry to add. Falling through and stamping `null` for both is a plausible lie:
|
|
611
|
+
a pack whose configuration says `system: sohl` on the line above ships every
|
|
612
|
+
compiled actor with no system at all.
|
|
520
613
|
|
|
521
614
|
No `default: true` anywhere, because each system has exactly one pack of each
|
|
522
615
|
type and a default is resolved per system. Marking one is still allowed and
|
|
@@ -536,12 +629,12 @@ note says which by writing `hm3.type`. Nothing is inferred from the note's own
|
|
|
536
629
|
`subType`, and a note that says nothing is an error naming the note and listing
|
|
537
630
|
the permitted values.
|
|
538
631
|
|
|
539
|
-
**Configuration is the source, and the manifest is generated from it.**
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
632
|
+
**Configuration is the source, and the manifest is generated from it.** The
|
|
633
|
+
floor is the top-level `compatibility.minimum`, the id is derived from
|
|
634
|
+
`package.json` `name`, and `@heroiclands/package-build` writes the manifest from
|
|
635
|
+
this file. There is no `paths.packageManifest` pointing at a hand-authored
|
|
636
|
+
`system.template.json` for a package-id guard or a `_stats.coreVersion` stamp to
|
|
637
|
+
read out of.
|
|
545
638
|
|
|
546
639
|
### Declaration order is presentation, not compile order
|
|
547
640
|
|
|
@@ -572,11 +665,11 @@ passes that had to move. When the two orders differ the build says so:
|
|
|
572
665
|
another's output compiles after it, whatever order `packs:` declares.
|
|
573
666
|
```
|
|
574
667
|
|
|
575
|
-
This
|
|
668
|
+
This is not the author's problem, and it would be a nasty one: an Actor pack declaring
|
|
576
669
|
first compiled only where an earlier run had already left `build/packs-json`
|
|
577
670
|
populated. `build/` is gitignored, so it was green on every local tree that had
|
|
578
671
|
built once and exit 1 on every fresh checkout and CI runner, over a message that
|
|
579
|
-
named a missing directory rather than the ordering that caused it
|
|
672
|
+
named a missing directory rather than the ordering that caused it. A
|
|
580
673
|
consumer registering a compiler of its own declares its dependencies the same
|
|
581
674
|
way; a type no pack of which is declared is simply not waited for.
|
|
582
675
|
|
|
@@ -630,7 +723,9 @@ and there are exactly three answers:
|
|
|
630
723
|
|
|
631
724
|
`assetRoot` is derived, never authored: it is
|
|
632
725
|
`<packageKind>/<foundryPackage>/assets`, and it is the one place `systems/sohl`
|
|
633
|
-
(or `modules/sohl-thalorna`) is ever spelled.
|
|
726
|
+
(or `modules/sohl-thalorna`) is ever spelled. The derivation is **conditional on
|
|
727
|
+
the kind** — a `documentation` package has no asset root, and the third row is
|
|
728
|
+
refused there rather than rooted. So `icons/relic.svg` in a module's
|
|
634
729
|
registry compiles to `modules/sohl-relics/assets/icons/relic.svg`, and the same
|
|
635
730
|
string in the system's compiles to `systems/sohl/assets/icons/relic.svg`. An
|
|
636
731
|
already-served `systems/sohl/assets/icons/…` passes through untouched — which is
|
|
@@ -654,7 +749,7 @@ address different places — `img:` a file Foundry serves, `banner:` a file the
|
|
|
654
749
|
CDN serves — so they are stated apart rather than reconciled. See the
|
|
655
750
|
[content format specification](docs/content-format.md#banner-addresses-the-cdn-not-the-foundry-install).
|
|
656
751
|
|
|
657
|
-
#### "Names no art" and "wants no art" are different
|
|
752
|
+
#### "Names no art" and "wants no art" are different
|
|
658
753
|
|
|
659
754
|
A note has two ways to leave `img:` empty, and they mean opposite things:
|
|
660
755
|
|
|
@@ -668,7 +763,7 @@ A note has two ways to leave `img:` empty, and they mean opposite things:
|
|
|
668
763
|
`resolveImg` returns `null` for the first two and `""` for the third, and every
|
|
669
764
|
caller pairs its default with **nullish** coalescing — `resolveImg(fm.img) ?? itemArt(type)`.
|
|
670
765
|
Never `||`: that collapses a deliberate blank back into the default and takes the
|
|
671
|
-
distinction away again
|
|
766
|
+
distinction away again.
|
|
672
767
|
|
|
673
768
|
**`portrait` is the same field twice over.** A being carries `img` (its token
|
|
674
769
|
art) and `portrait` (its sheet portrait) independently, and both resolve through
|
|
@@ -681,13 +776,11 @@ rather than two.
|
|
|
681
776
|
> **The rule is `img`'s, and does not extend to `title`.** `title` is not art
|
|
682
777
|
> and never reaches `resolveImg`, so nothing here applies to it.
|
|
683
778
|
>
|
|
684
|
-
> The
|
|
685
|
-
>
|
|
686
|
-
>
|
|
687
|
-
>
|
|
688
|
-
>
|
|
689
|
-
> `"null"`. The field declares `topLevelMeans` now, so the top-level key is no
|
|
690
|
-
> longer a source for it (#218).
|
|
779
|
+
> The field declares `topLevelMeans`, so the top-level key is not a source for
|
|
780
|
+
> an `affiliation` item's `system.title`. Were it one, a single authored key
|
|
781
|
+
> would feed two unrelated destinations that disagree about what empty means,
|
|
782
|
+
> and `title: null` would stringify into the compiled document as the literal
|
|
783
|
+
> `"null"`.
|
|
691
784
|
>
|
|
692
785
|
> So `title: null` is a note declining to state a heading, and the site emitter's
|
|
693
786
|
> `fm.title ?? name` falls back to `name.full`. `title: ""` publishes a
|
|
@@ -699,9 +792,9 @@ rather than two.
|
|
|
699
792
|
> not answer for the page's heading. Twenty-eight `sohl-kethira-basic`
|
|
700
793
|
> affiliations write `sohl.title: ""`, meaning an office with no style of
|
|
701
794
|
> address, and every one of them was reported as publishing a blank heading
|
|
702
|
-
> until the check honoured the declaration
|
|
795
|
+
> until the check honoured the declaration.
|
|
703
796
|
|
|
704
|
-
Because `""`
|
|
797
|
+
Because `""` reads as "unset" elsewhere, a note carrying that spelling has
|
|
705
798
|
quietly changed meaning, and the frontmatter lint says so — for either art
|
|
706
799
|
field:
|
|
707
800
|
|
|
@@ -736,7 +829,7 @@ one failed the build with an error naming a module in someone else's package.
|
|
|
736
829
|
|
|
737
830
|
Widening that map was not the fix. It is deliberately SoHL data, shared with the
|
|
738
831
|
runtime's `SohlItem.getDefaultArtwork` so that the build-time and runtime
|
|
739
|
-
defaults are one list and cannot drift
|
|
832
|
+
defaults are one list and cannot drift. Pairing art with the
|
|
740
833
|
builder instead moves it onto the seam a type is _already_ declared through, and
|
|
741
834
|
costs the `sohl` package nothing: `ITEM_BUILDERS` reads each entry's image out of
|
|
742
835
|
that same map, so there is still exactly one map — and the drift a test used to
|
|
@@ -770,7 +863,7 @@ to be written at.
|
|
|
770
863
|
reaches `sohl.system.templatePriority` and `hm3.flags.hm3.templatePriority` —
|
|
771
864
|
exactly as `portrait` reaches two differently-named fields from one shared
|
|
772
865
|
property. A number is a template at that priority, `null` is not a template, and
|
|
773
|
-
absent is an authoring error
|
|
866
|
+
absent is an authoring error. The legacy in-block and top-level
|
|
774
867
|
positions are still read, in that order after `data:`, so a tree sweeps on its
|
|
775
868
|
own schedule; `archetype` is the retiring spelling of the same field, still read
|
|
776
869
|
last but **refused by the frontmatter linter** — a priority and the `archetypes`
|
|
@@ -809,7 +902,7 @@ system `S` is:
|
|
|
809
902
|
a **dotted path** (`data.portrait`) rather than a sibling key;
|
|
810
903
|
4. the field's own default.
|
|
811
904
|
|
|
812
|
-
`FieldSpec.name` is that declared source.
|
|
905
|
+
`FieldSpec.name` is that declared source. Read as "frontmatter key under
|
|
813
906
|
`sohl:`", which is the degenerate case where source and destination happen to
|
|
814
907
|
share a name.
|
|
815
908
|
|
|
@@ -821,7 +914,7 @@ is _the title of the note_ — the heading its page publishes under, which the
|
|
|
821
914
|
site emitter reads. An `affiliation` item's `system.title` is _the style of
|
|
822
915
|
address the office carries_ — Ajaw, Warden, a person's style within the body.
|
|
823
916
|
They are unrelated quantities, and step 3 used to feed the second from the first
|
|
824
|
-
|
|
917
|
+
.
|
|
825
918
|
|
|
826
919
|
That was not merely untidy, because **step 3 answers without applying
|
|
827
920
|
`field.default`** — only step 2 does — so an authored `title: null` reached the
|
|
@@ -841,7 +934,7 @@ side too.** If the two positions hold unrelated quantities, then the _in-block_
|
|
|
841
934
|
position is not the note-level field either — so a check about a note-level field
|
|
842
935
|
(the page's heading, an art path) reads past a block key the note's own type
|
|
843
936
|
claims for something else. Declaring `topLevelMeans` settles both directions at
|
|
844
|
-
once
|
|
937
|
+
once. Reading it for the emitted field alone is how an
|
|
845
938
|
affiliation's office style came to answer for its page heading.
|
|
846
939
|
|
|
847
940
|
**An exempted field is still authorable**, at the two positions that describe the
|
|
@@ -951,9 +1044,10 @@ npx content-build lint some/tree # or a tree named outright
|
|
|
951
1044
|
Checks the three rules every note's **identity** is authored against, and reports
|
|
952
1045
|
each finding in the located form below:
|
|
953
1046
|
|
|
954
|
-
- **Shape** — a `shortcode` is strictly ASCII-alphanumeric. It is
|
|
955
|
-
key referenced from saved world data, and half of the
|
|
956
|
-
address, whose parse needs the separating hyphen to be the
|
|
1047
|
+
- **Shape** — a `shortcode` is strictly lowercase ASCII-alphanumeric. It is
|
|
1048
|
+
the identity key referenced from saved world data, and half of the
|
|
1049
|
+
`type-shortcode` address, whose parse needs the separating hyphen to be the
|
|
1050
|
+
only hyphen.
|
|
957
1051
|
- **Uniqueness** — `(type, shortcode)` names one note. A document is addressed
|
|
958
1052
|
across _every_ pack of its document type, so routing two same-address notes to
|
|
959
1053
|
different packs with `pack:` does not separate them.
|
|
@@ -968,14 +1062,14 @@ is exactly what a tree that failed to check out produces.
|
|
|
968
1062
|
What that guard reports is an **empty walk**, not an empty set of addresses. A
|
|
969
1063
|
note may be keyless — a folder document carries no `shortcode` — so a tree of
|
|
970
1064
|
them is populated, correct and unkeyed. That tree passes; a tree holding no
|
|
971
|
-
notes at all still fails. (The homepage
|
|
1065
|
+
notes at all still fails. (The homepage is not the headline example, though it was once
|
|
972
1066
|
it was addressed by the package rather than by a slug. It carries an address
|
|
973
|
-
like every other note now
|
|
1067
|
+
like every other note now; the guard is unchanged, because what it reads
|
|
974
1068
|
was never the key count.)
|
|
975
1069
|
|
|
976
1070
|
### Exactly one homepage
|
|
977
1071
|
|
|
978
|
-
A content tree declares **exactly one** `type: homepage` note
|
|
1072
|
+
A content tree declares **exactly one** `type: homepage` note. Zero is an
|
|
979
1073
|
error and two is an error, at the same severity, because they are one defect: a
|
|
980
1074
|
package whose front page is not the page a person chose.
|
|
981
1075
|
|
|
@@ -983,10 +1077,8 @@ package whose front page is not the page a person chose.
|
|
|
983
1077
|
authored homepage exists to prevent, and a silent one: the site build reports
|
|
984
1078
|
`wrote 0 homepage(s)` and exits 0.
|
|
985
1079
|
- _Two_ and it serves a page nobody chose. **This is a cardinality rule, and
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
enforced it as a side effect. A homepage is written at its own address now, so
|
|
989
|
-
two of them publish two pages and collide over nothing: the duplicate-address
|
|
1080
|
+
only that.** A homepage is written at its own address, so two of them publish
|
|
1081
|
+
two pages and collide over nothing: the duplicate-address
|
|
990
1082
|
check catches only the pair that happen to share a shortcode, and says nothing
|
|
991
1083
|
at all about a `homepage-root` beside a `homepage-front`. Which of the two the
|
|
992
1084
|
redirect at `/<package>/` should name is a question nothing here can answer,
|
|
@@ -1022,7 +1114,7 @@ assets/content/homepage.md:3:7: error: duplicate `type: homepage` note, also dec
|
|
|
1022
1114
|
### Frontmatter, against the schema its type declares
|
|
1023
1115
|
|
|
1024
1116
|
The same command also checks that each note's `sohl:` block is what its **type**
|
|
1025
|
-
allows
|
|
1117
|
+
allows. Five classes, all of them mistakes that were previously reported
|
|
1026
1118
|
somewhere other than where they were made, or not at all:
|
|
1027
1119
|
|
|
1028
1120
|
- **Unknown or retired type** — a note on a retired spelling is told what
|
|
@@ -1055,7 +1147,7 @@ Nothing here writes. A check reports and an author fixes.
|
|
|
1055
1147
|
|
|
1056
1148
|
### The `data:` container is closed; the top level is not
|
|
1057
1149
|
|
|
1058
|
-
A note's frontmatter has three regions, and only one of them is open
|
|
1150
|
+
A note's frontmatter has three regions, and only one of them is open:
|
|
1059
1151
|
|
|
1060
1152
|
| region | describes | an unknown key is |
|
|
1061
1153
|
| ---------------- | -------------------------------------------- | ------------------ |
|
|
@@ -1071,7 +1163,7 @@ the page's description.
|
|
|
1071
1163
|
|
|
1072
1164
|
**`data:` is deliberately closed**, and that is the point of having it. The
|
|
1073
1165
|
type-specific facts about a subject — a weapon's weight, an affliction's
|
|
1074
|
-
transmission, a being's species —
|
|
1166
|
+
transmission, a being's species — do not sit at the top level, where the
|
|
1075
1167
|
pass-through rule applied to them too. So a misspelled `wieght` became a theme
|
|
1076
1168
|
parameter rather than a finding, indistinguishable from a weapon that weighs
|
|
1077
1169
|
nothing. Under `data:` the same key is reported where it was written, with the
|
|
@@ -1082,14 +1174,14 @@ assets/content/Gear/Axe.md:14:5: error: "wieght" is not a `data:` property of a
|
|
|
1082
1174
|
```
|
|
1083
1175
|
|
|
1084
1176
|
**A system block is closed too, and which blocks exist is the configuration's
|
|
1085
|
-
answer
|
|
1177
|
+
answer**. A package is held to the blocks named after the systems it
|
|
1086
1178
|
declares it ships for, read from the three places that already declare them:
|
|
1087
1179
|
`systems:`, a pack's own `system:`, and `stats.systemId` where neither is
|
|
1088
1180
|
written. So a package shipping for HM3 has its `hm3:` block checked and a
|
|
1089
|
-
package shipping for SoHL its `sohl:`.
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1181
|
+
package shipping for SoHL its `sohl:`. Held constant at `sohl`, an `hm3:` block
|
|
1182
|
+
would never be read at all: every key in it would be discarded at compile
|
|
1183
|
+
without a word, while the block that _was_ checked would be named after a system
|
|
1184
|
+
the package does not ship for.
|
|
1093
1185
|
|
|
1094
1186
|
A pack's `system:` counts because it is already authoritative at compile — a
|
|
1095
1187
|
note routed to a pack declaring one and carrying no such block fails the build —
|
|
@@ -1125,25 +1217,25 @@ values. A `weapon` declares none — SoHL distinguishes a weapon's uses by strik
|
|
|
1125
1217
|
mode rather than by kind — so `subType` on one is a finding; a `skill` declares
|
|
1126
1218
|
ten, so `subType: crafte` is a finding naming `craft`.
|
|
1127
1219
|
|
|
1128
|
-
**A `type` and a `subType` are both held to `^[
|
|
1220
|
+
**A `type` and a `subType` are both held to `^[a-z0-9]+$`** — the same
|
|
1129
1221
|
constant a `shortcode` is held to, read rather than restated. A type is a
|
|
1130
1222
|
segment of every address — the first of the short form an author writes, the
|
|
1131
1223
|
third of the canonical `package-system-type-shortcode` — so a hyphen in one is
|
|
1132
1224
|
read back as a segment boundary nobody wrote. A `subType` reaches no address
|
|
1133
|
-
|
|
1225
|
+
— sections are retired — and keeps the rule anyway: it is a vocabulary term the toolchain keys
|
|
1134
1226
|
on, and one charset that holds for every term is a rule an author can state. The
|
|
1135
1227
|
rule is checked ahead of the closed-set check, which is what makes it reach a
|
|
1136
1228
|
type whose values are declared but not yet enumerated:
|
|
1137
1229
|
|
|
1138
1230
|
```text
|
|
1139
|
-
assets/content/Beings/Folk.md:3:1: error: `subType` "common-folk" is not a well-formed subType — a subType is letters and digits only (^[
|
|
1231
|
+
assets/content/Beings/Folk.md:3:1: error: `subType` "common-folk" is not a well-formed subType — a subType is letters and digits only (^[a-z0-9]+$), the same charset a type, a shortcode and a contentPackage are held to. …
|
|
1140
1232
|
```
|
|
1141
1233
|
|
|
1142
1234
|
One declared value broke that rule: a `doc`'s `user-guide`, now **`userguide`**.
|
|
1143
1235
|
The old spelling was accepted for one transitional release, as a warning naming
|
|
1144
1236
|
the replacement, because an error would have redded every tree that took the
|
|
1145
1237
|
release before it had a chance to sweep. Every tree has swept, so the acceptance
|
|
1146
|
-
is gone
|
|
1238
|
+
is gone: `user-guide` is refused by the charset check like any other
|
|
1147
1239
|
hyphenated value, and nothing retirement-specific was left to remove.
|
|
1148
1240
|
|
|
1149
1241
|
The vocabulary lives in `engine/note-vocabulary.mjs`, one entry per note type,
|
|
@@ -1151,20 +1243,16 @@ taken from the content-format specification. It is note-format knowledge rather
|
|
|
1151
1243
|
than any system's: `data:` holds what is true of the thing, and what a system
|
|
1152
1244
|
makes of that value is declared in that system's own half.
|
|
1153
1245
|
|
|
1154
|
-
**
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
resolvers parse the hyphen qualifier themselves. The
|
|
1158
|
-
in Obsidian, so the rule cost a line of frontmatter per note for a reader that
|
|
1159
|
-
does not exist. Removing it was verified output-neutral first: across 1,735
|
|
1160
|
-
stripped notes, `package compile` produced byte-identical `build/packs-json` and
|
|
1161
|
-
the site build byte-identical `site/content`.
|
|
1246
|
+
**There is deliberately no third rule** requiring every note to repeat its own
|
|
1247
|
+
`type-shortcode` address in `aliases:`. It would serve one reader — Obsidian, so
|
|
1248
|
+
`[[type-shortcode]]` resolves in the editor — and no build reads it: both
|
|
1249
|
+
resolvers parse the hyphen qualifier themselves. The field is retired.
|
|
1162
1250
|
|
|
1163
|
-
**And the top-level field itself is now retired
|
|
1251
|
+
**And the top-level field itself is now retired.** `aliases:` fed the
|
|
1164
1252
|
alias index, which is what a bare `[[Alias]]` was looked up in. That form
|
|
1165
1253
|
resolved to nothing anywhere in the corpus, while the collision rule guarding it
|
|
1166
1254
|
folded in every note's `name.full` and so decided what a note could be named
|
|
1167
|
-
|
|
1255
|
+
. Both are gone: every wikilink is an address, written
|
|
1168
1256
|
`[[type-shortcode|Text]]`, and declaring `aliases:` is refused naming the file
|
|
1169
1257
|
and the line.
|
|
1170
1258
|
|
|
@@ -1201,7 +1289,7 @@ that harmless: three segments name a _system_ whatever the first segment could
|
|
|
1201
1289
|
also have meant, and four is the full form.
|
|
1202
1290
|
|
|
1203
1291
|
**Parsing is plain segment counting**, the same rule the canonical key follows,
|
|
1204
|
-
and it is sound because every segment is `^[
|
|
1292
|
+
and it is sound because every segment is `^[a-z0-9]+$` — so a hyphen is
|
|
1205
1293
|
purely a separator. A target with five segments is not a hyphenated shortcode;
|
|
1206
1294
|
it is a name, and not an address.
|
|
1207
1295
|
|
|
@@ -1220,12 +1308,12 @@ nothing today will read it.
|
|
|
1220
1308
|
|
|
1221
1309
|
A homepage declares a `shortcode` — conventionally `root` — and publishes at its
|
|
1222
1310
|
address, `/<package>/homepage-root/`, written by the same rule as everything
|
|
1223
|
-
else
|
|
1311
|
+
else. So `[[homepage-root|Read the introduction]]` is an ordinary
|
|
1224
1312
|
wikilink, resolving to the page the build actually writes.
|
|
1225
1313
|
|
|
1226
1314
|
It did not use to be. A page's URL derived from `name.full` while a homepage's
|
|
1227
1315
|
destination was fixed at `_index.md`, so `content-build lint` **refused** `name`
|
|
1228
|
-
and `shortcode` on one
|
|
1316
|
+
and `shortcode` on one — not out of tidiness, but because they were not
|
|
1229
1317
|
inert. A `shortcode` put the note in the address index, so
|
|
1230
1318
|
`[[homepage-<shortcode>]]` resolved _green_ to a page the site build never
|
|
1231
1319
|
wrote, and a build reporting a live link to a 404 is worse than one saying
|
|
@@ -1274,7 +1362,7 @@ is emitted into the published page, so an unrecognised key is a Hugo or theme
|
|
|
1274
1362
|
parameter this build has never heard of and has no standing to reject; a closed
|
|
1275
1363
|
list would make every new theme parameter wait on a package-build release.
|
|
1276
1364
|
`aliases` is not in the class either — it is a retired field now, refused on
|
|
1277
|
-
every note whatever its type
|
|
1365
|
+
every note whatever its type.
|
|
1278
1366
|
|
|
1279
1367
|
**Where it fires: `content-build lint` only.** Unlike a rule about the shape of
|
|
1280
1368
|
the _tree_, which the site build has its own reason to gate on, this is a
|
|
@@ -1348,7 +1436,7 @@ serves.
|
|
|
1348
1436
|
|
|
1349
1437
|
### The homepage's own links
|
|
1350
1438
|
|
|
1351
|
-
The homepage is the page a reader arrives at, and
|
|
1439
|
+
The homepage is the page a reader arrives at, and it is the one page
|
|
1352
1440
|
nothing checked. SoHL's landing pointed at `kb/creature/` and `kb/character/`
|
|
1353
1441
|
from the day those two types merged into `being` — two 404s on the package's
|
|
1354
1442
|
front page, through every build, because a landing's links went through no
|
|
@@ -1484,7 +1572,7 @@ npx content-build markdown --fix # apply the fixes markdownlint can make
|
|
|
1484
1572
|
```
|
|
1485
1573
|
|
|
1486
1574
|
Two conventions every content repository writes to, declared once here so a note
|
|
1487
|
-
formatted in one is formatted the same way in the next
|
|
1575
|
+
formatted in one is formatted the same way in the next:
|
|
1488
1576
|
|
|
1489
1577
|
- **`format`** runs Prettier. Same values SoHL has always used, so a module or a
|
|
1490
1578
|
note moving between repositories does not reformat on arrival.
|
|
@@ -1518,7 +1606,7 @@ alone: a `prettier.config.mjs` that spreads `PRETTIER_BASE` without the `**/*.md
|
|
|
1518
1606
|
override reindents every note at 4, and a partial `.prettierrc` such as
|
|
1519
1607
|
`{"tabWidth": 2}` silently discards `printWidth: 100`, `trailingComma` and the
|
|
1520
1608
|
rest. So every `format` run first names, as warnings, each shared convention this
|
|
1521
|
-
repository resolves differently
|
|
1609
|
+
repository resolves differently:
|
|
1522
1610
|
|
|
1523
1611
|
```text
|
|
1524
1612
|
prettier.config.mjs: warning: markdown `tabWidth` is 4 here; the shared configuration says 2
|
|
@@ -1722,20 +1810,20 @@ names a _document_; a URL names a _page_. So a consumer deriving a page address
|
|
|
1722
1810
|
from a manifest key drops the package **and** the system, not the package
|
|
1723
1811
|
alone.
|
|
1724
1812
|
|
|
1725
|
-
It
|
|
1813
|
+
It does not come from `name.full`. That would make a display name load-bearing three
|
|
1726
1814
|
ways at once — a rename silently 404'd every inbound link, two notes in one
|
|
1727
1815
|
section could derive the same URL so a uniqueness gate had to run, and long names
|
|
1728
1816
|
had to be shortened through a table of 200 abbreviations. The header of the
|
|
1729
1817
|
module doing it justified the cost by promising redirects "every change appends
|
|
1730
1818
|
to the legacy-URL map" — and no such map was ever written, here or in any
|
|
1731
|
-
consumer. All of it is gone
|
|
1819
|
+
consumer. All of it is gone.
|
|
1732
1820
|
|
|
1733
1821
|
The `type-` half earns its place: it keeps every content address clear of the
|
|
1734
1822
|
package's fixed mounts (`/<package>/` for the landing page, `/<package>/api/` for
|
|
1735
1823
|
generated API docs), neither of which contains a hyphen or names a type. So the
|
|
1736
1824
|
namespace is provably disjoint rather than conventionally so.
|
|
1737
1825
|
|
|
1738
|
-
**A page is written flat, named by its address
|
|
1826
|
+
**A page is written flat, named by its address**, not filed
|
|
1739
1827
|
into `<section>/`, because Hugo derives a page's section from where the file is
|
|
1740
1828
|
written and a section gave it a landing page, `.CurrentSection` and a per-section
|
|
1741
1829
|
layout lookup. But a section appears in no address, so the note format was
|
|
@@ -1745,7 +1833,7 @@ order to satisfy a rendering engine's directory semantics. The file is now
|
|
|
1745
1833
|
the package root, one level above.
|
|
1746
1834
|
|
|
1747
1835
|
**A page states its address without the package base; everything pointing _at_
|
|
1748
|
-
it composes one
|
|
1836
|
+
it composes one**. They read as one quantity and are two:
|
|
1749
1837
|
|
|
1750
1838
|
| Written | Form | Because |
|
|
1751
1839
|
| ------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------- |
|
|
@@ -1754,7 +1842,7 @@ it composes one** (#217). They read as one quantity and are two:
|
|
|
1754
1842
|
| A link-manifest `path` | `<type>-<shortcode>/` | Measured against `site.base` and stripped; a consumer prefixes its own |
|
|
1755
1843
|
|
|
1756
1844
|
`site.base` is the second and third of those and reaches the first not at all.
|
|
1757
|
-
|
|
1845
|
+
Writing it into the `url:` as well makes every consumer's Hugo prefix
|
|
1758
1846
|
its own base to a value that already carried one and published every content
|
|
1759
1847
|
page a segment too deep — `/sohl/sohl/doc-rulesintro/`, 404 at the address the
|
|
1760
1848
|
manifest, the sitemap and every inbound link named.
|
|
@@ -1763,7 +1851,7 @@ manifest, the sitemap and every inbound link named.
|
|
|
1763
1851
|
addressed the section itself; that is retired with the section. A page that
|
|
1764
1852
|
introduces the notes of a type is an ordinary note addressed `doc-<type>`, with
|
|
1765
1853
|
no build path of its own — exactly as the package's own front page is
|
|
1766
|
-
`homepage-root
|
|
1854
|
+
`homepage-root`.
|
|
1767
1855
|
|
|
1768
1856
|
**Sections stay, as configuration.** `site.sections` still writes an `_index.md`
|
|
1769
1857
|
per section, and that is now the _only_ thing that makes one exist — see
|
|
@@ -1794,13 +1882,12 @@ publish:
|
|
|
1794
1882
|
never recorded here.
|
|
1795
1883
|
|
|
1796
1884
|
`prefix` is the whole scheme. The `collection` subtype and the top-level
|
|
1797
|
-
`section:` key are refused by name
|
|
1885
|
+
`section:` key are refused by name.
|
|
1798
1886
|
|
|
1799
1887
|
A note's `subType` is checked against the values its type declares, and only
|
|
1800
1888
|
those. It briefly had a second reading — a `README` landing's `subType` was the
|
|
1801
|
-
_address_ it landed at, so the closed genre list could not answer for it
|
|
1802
|
-
|
|
1803
|
-
vocabulary again.
|
|
1889
|
+
_address_ it landed at, so the closed genre list could not answer for it. The
|
|
1890
|
+
cause is removed rather than the vocabulary widened again.
|
|
1804
1891
|
|
|
1805
1892
|
The only note the scheme yields no address for is one carrying no `shortcode`.
|
|
1806
1893
|
It is **reported and omitted**, never guessed: the command prints one located
|
|
@@ -2046,7 +2133,7 @@ That is the whole envelope. A homepage **compiles into no compendium
|
|
|
2046
2133
|
document**, and so appears in no pack and in no link manifest — which is why it
|
|
2047
2134
|
still refuses `id`. Everything else about its address is ordinary: it declares a
|
|
2048
2135
|
`shortcode`, publishes at `/<contentPackage>/homepage-root/`, and is cited as
|
|
2049
|
-
`[[homepage-root|Text]]` (
|
|
2136
|
+
`[[homepage-root|Text]]` (see
|
|
2050
2137
|
[The homepage is addressed like every other note](#the-homepage-is-addressed-like-every-other-note)).
|
|
2051
2138
|
`/<contentPackage>/` itself is a redirect the package authors, not a page this
|
|
2052
2139
|
build writes. It is dispatched on `type` like every other note, not on a
|
|
@@ -2068,8 +2155,8 @@ note-format knowledge against game-system knowledge, and a homepage carries no
|
|
|
2068
2155
|
| `content` | The homepage plus every page the content tree compiles to, and its extra trees. |
|
|
2069
2156
|
|
|
2070
2157
|
There is no value meaning "no web presence": every package publishes its
|
|
2071
|
-
homepage.
|
|
2072
|
-
|
|
2158
|
+
homepage. A boolean is refused, with a message naming the mode to write
|
|
2159
|
+
instead.
|
|
2073
2160
|
|
|
2074
2161
|
**Homepage-only is a first-class mode, not an accommodation.**
|
|
2075
2162
|
`sohl-kethira-basic` (unofficial Hârn fan material under Keléstia Productions'
|
|
@@ -2094,7 +2181,7 @@ site root, one level above the content mount, which is where
|
|
|
2094
2181
|
`publish.address.prefix` puts everything else — under the name its address gives
|
|
2095
2182
|
it, `homepage-root.md`. As with every other page, the front matter's `url`
|
|
2096
2183
|
decides where it publishes, and states it relative to the site root — `site.base`
|
|
2097
|
-
does not reach it
|
|
2184
|
+
does not reach it.
|
|
2098
2185
|
|
|
2099
2186
|
**What it does not do is decide addresses.** Those come from `publish.address`,
|
|
2100
2187
|
the same setting the content index reads, so a page and its index record cannot
|
|
@@ -2140,7 +2227,7 @@ site:
|
|
|
2140
2227
|
|
|
2141
2228
|
### What a section may declare
|
|
2142
2229
|
|
|
2143
|
-
**`sections` is what a section _is_ now
|
|
2230
|
+
**`sections` is what a section _is_ now**. A content page is addressed
|
|
2144
2231
|
`(type, shortcode)` and written flat under the mount, so no page creates a
|
|
2145
2232
|
directory and nothing else makes `/<package>/<prefix><section>/` answer at all.
|
|
2146
2233
|
A site that wants that address says so here, and this build writes the
|
|
@@ -2209,7 +2296,7 @@ Three things about the spelling, each of them load-bearing:
|
|
|
2209
2296
|
site chose; a type and a subType are addresses. They need not agree, and on
|
|
2210
2297
|
`sohl` they do not: the section is `user-guide`, because that is a published
|
|
2211
2298
|
URL, while the subType is `userguide` because an address segment is
|
|
2212
|
-
alphanumeric
|
|
2299
|
+
lowercase alphanumeric. Both values are checked against that charset here, so
|
|
2213
2300
|
copying the section's name in is refused rather than quietly matching nothing.
|
|
2214
2301
|
- **`listSubType` needs a `listType`.** A subType only tells pages apart within
|
|
2215
2302
|
a type — `rules`, `userguide` and `reference` are all `doc` — so alone it
|
|
@@ -2263,8 +2350,8 @@ to a code span.
|
|
|
2263
2350
|
Leaving it unset is the legitimate empty case — every `{@link}` degrades, and
|
|
2264
2351
|
nothing is reported. Setting it to a path that cannot be read, cannot be parsed,
|
|
2265
2352
|
or does not hold a name → page object **fails the build**, naming the file and
|
|
2266
|
-
the reason
|
|
2267
|
-
site
|
|
2353
|
+
the reason. Left unreported they are indistinguishable from "no symbols", and
|
|
2354
|
+
a site publishes dead `{@link}` tags at exit 0. A map that is read reports
|
|
2268
2355
|
its symbol count at info level, which is the only way to tell a map that loaded
|
|
2269
2356
|
from one that loaded empty without reading the emitted HTML.
|
|
2270
2357
|
|
|
@@ -2297,6 +2384,131 @@ None of them exits the process from inside the library; the command decides. Tha
|
|
|
2297
2384
|
is what makes them testable, which the consumer scripts' inline `process.exit`
|
|
2298
2385
|
calls were not.
|
|
2299
2386
|
|
|
2387
|
+
## Publishing a book
|
|
2388
|
+
|
|
2389
|
+
The third surface the content tree publishes, beside the compendium packs and
|
|
2390
|
+
the website: one PDF, built by `content-build pdf` and by `package-build release`.
|
|
2391
|
+
|
|
2392
|
+
```bash
|
|
2393
|
+
npx content-build pdf # build it where `pdf.out` says
|
|
2394
|
+
npx content-build pdf --out build/book # somewhere else
|
|
2395
|
+
npx content-build pdf --no-compile # emit the Typst source and stop
|
|
2396
|
+
npx content-build pdf --version 1.4.0 # stamp a version on the title page
|
|
2397
|
+
```
|
|
2398
|
+
|
|
2399
|
+
### A book is a selection, not a rendering of everything
|
|
2400
|
+
|
|
2401
|
+
The packs and the website publish the _whole_ tree: every note becomes a
|
|
2402
|
+
document and a page, and the three surfaces agreeing about what the content is
|
|
2403
|
+
is the point. **A book is not that.** It is a declared structure whose leaves
|
|
2404
|
+
pick notes out of the corpus, interleaved with prose that need not live in the
|
|
2405
|
+
content tree at all.
|
|
2406
|
+
|
|
2407
|
+
`pdf.document` names that structure — a YAML file of nested sections, each
|
|
2408
|
+
holding some mixture of prose files and `filter:` clauses:
|
|
2409
|
+
|
|
2410
|
+
```yaml
|
|
2411
|
+
contents:
|
|
2412
|
+
- sectionName: Gear
|
|
2413
|
+
contents:
|
|
2414
|
+
- file: prose/gear-preamble.md
|
|
2415
|
+
- filter: "type = 'weapongear'"
|
|
2416
|
+
- sectionName: Armour
|
|
2417
|
+
contents:
|
|
2418
|
+
- filter: "type = 'armorgear'"
|
|
2419
|
+
- sectionName: The Cast
|
|
2420
|
+
contents:
|
|
2421
|
+
- filter: "type = 'being' AND subType = 'npc'"
|
|
2422
|
+
```
|
|
2423
|
+
|
|
2424
|
+
Three consequences follow, and each is behaviour rather than oversight:
|
|
2425
|
+
|
|
2426
|
+
- **A note no clause selects is not in the book.** A project decides what its
|
|
2427
|
+
own volume carries, so an omission is an editorial act and nothing reports it.
|
|
2428
|
+
- **A note several clauses select appears several times**, each occurrence its
|
|
2429
|
+
own page, outline node and anchor. Inbound wikilinks are pointed at the
|
|
2430
|
+
first, so `[[weapongear-dagger]]` reaches one place however often it prints.
|
|
2431
|
+
- **A filter that selects nothing _is_ reported.** A note nobody asked for is
|
|
2432
|
+
expected; a clause matching nothing is either wrong or left over from a
|
|
2433
|
+
structure that has moved on.
|
|
2434
|
+
|
|
2435
|
+
The `WHERE` clause runs against the same content index the SQL tables read, and
|
|
2436
|
+
the build owns the `SELECT … FROM notes` — so a filter cannot name a table, and
|
|
2437
|
+
cannot reach another package's notes.
|
|
2438
|
+
|
|
2439
|
+
### What it is fenced by
|
|
2440
|
+
|
|
2441
|
+
**`publish.site` decides whether a book is built, and it is the only switch.**
|
|
2442
|
+
`content` builds one; `homepage` does not — the same fence that stops the tree
|
|
2443
|
+
being walked for pages stops it being walked for a book. A PDF of the content
|
|
2444
|
+
tree is a content surface by any reading, arguably the most portable one there
|
|
2445
|
+
is, so a package that publishes only a homepage publishes no book however its
|
|
2446
|
+
`pdf:` block is written. The command says so and exits 0.
|
|
2447
|
+
|
|
2448
|
+
That is also the opt-out: a package with no `pdf:` block, no content tree, or
|
|
2449
|
+
`publish.site: homepage` builds nothing and fails nothing.
|
|
2450
|
+
|
|
2451
|
+
### Configuration
|
|
2452
|
+
|
|
2453
|
+
```yaml
|
|
2454
|
+
pdf:
|
|
2455
|
+
title: The Hârn Ensemble # required
|
|
2456
|
+
subtitle: a roster of the ready-made
|
|
2457
|
+
document: book.yaml # required — the document tree above
|
|
2458
|
+
out: build/dist # default
|
|
2459
|
+
front: # prose before the contents
|
|
2460
|
+
- prose/colophon.md
|
|
2461
|
+
fonts:
|
|
2462
|
+
serif: Libertinus Serif
|
|
2463
|
+
sans: Libertinus Sans
|
|
2464
|
+
mono: DejaVu Sans Mono
|
|
2465
|
+
path: assets/fonts # where to find them, beyond the system's
|
|
2466
|
+
iconFonts: # icon family → the font carrying its glyphs
|
|
2467
|
+
fontawesome: assets/fonts/fa-solid-900.ttf
|
|
2468
|
+
binary: typst # when it is not simply `typst` on PATH
|
|
2469
|
+
```
|
|
2470
|
+
|
|
2471
|
+
Nothing here is an address or a brand: the title, the front matter and the faces
|
|
2472
|
+
are the publishing repository's to choose, which is why they are configuration.
|
|
2473
|
+
|
|
2474
|
+
### Typst is a binary, not a dependency
|
|
2475
|
+
|
|
2476
|
+
The compiler is an external program, found on `PATH` or named by `pdf.binary`.
|
|
2477
|
+
Bundling a native compiler would put a platform-specific artefact into the
|
|
2478
|
+
dependency tree of three repositories, only one of which is mostly a book, and
|
|
2479
|
+
it would have to resolve on every consumer's CI runner before any of them could
|
|
2480
|
+
install the toolchain at all.
|
|
2481
|
+
|
|
2482
|
+
A missing binary is a **finding**, not a failure: the `.typ` source is written
|
|
2483
|
+
anyway, which is both the diagnostic and the thing a consumer can compile by
|
|
2484
|
+
hand.
|
|
2485
|
+
|
|
2486
|
+
### What the book gets right, and why each matters
|
|
2487
|
+
|
|
2488
|
+
| Property | How |
|
|
2489
|
+
| ------------------------ | ----------------------------------------------------------------------------- |
|
|
2490
|
+
| Searchable | Real text, not page images — a roster nobody can search for a name is no use. |
|
|
2491
|
+
| Bookmark outline | Every section and entry is a heading, so a viewer's sidebar is the way in. |
|
|
2492
|
+
| Page-numbered contents | `#outline()`, shallower than the bookmarks — 2,500 entries would be 40 pages. |
|
|
2493
|
+
| Repeating table headers | `table.header`, so a property table spilling a page keeps its column names. |
|
|
2494
|
+
| Internal cross-reference | A wikilink between two notes of the book becomes an internal destination. |
|
|
2495
|
+
| External cross-reference | A cross-package link, and a note the book did not select, stay URLs. |
|
|
2496
|
+
|
|
2497
|
+
`{#anchor}` on a heading becomes an internal destination namespaced by its
|
|
2498
|
+
entry, so `[[being-jaslyne#appearance]]` reaches the section and two notes may
|
|
2499
|
+
both declare `{#appearance}`. A reference to a destination the book does not
|
|
2500
|
+
carry falls back to the entry that would have held it, or to plain text, and is
|
|
2501
|
+
reported — because Typst treats a dangling reference as fatal, and one mistyped
|
|
2502
|
+
anchor should not take a thousand-page book down at the last step.
|
|
2503
|
+
|
|
2504
|
+
### The charset is what makes a face choosable
|
|
2505
|
+
|
|
2506
|
+
A PDF embeds the faces it sets, so every character in the corpus is a claim on
|
|
2507
|
+
the book's typeface — and Typst does not warn about a missing glyph, it falls
|
|
2508
|
+
back and exits 0. That is why `content-build lint` holds content to a charset,
|
|
2509
|
+
and why `:icon-…:` names an icon rather than pasting one. Both exist for this
|
|
2510
|
+
surface. See _Prose: formatting and markdown_.
|
|
2511
|
+
|
|
2300
2512
|
## Diffing published addresses
|
|
2301
2513
|
|
|
2302
2514
|
```bash
|
|
@@ -2442,7 +2654,7 @@ affiliation standings. Each has its own entry point —
|
|
|
2442
2654
|
`.../engine/region-events`, `.../sohl/affiliation-standings` — so a client bundle
|
|
2443
2655
|
reaches the constant without importing a barrel that grows to hold compilers
|
|
2444
2656
|
reading the filesystem. Keeping one copy of each is the point: the build-time and
|
|
2445
|
-
runtime values cannot disagree
|
|
2657
|
+
runtime values cannot disagree.
|
|
2446
2658
|
|
|
2447
2659
|
`@heroiclands/package-build/content-config` exposes the configuration contract's own
|
|
2448
2660
|
module, so a consumer can name its types (`ContentBuildConfig`, `PackSpec`) from
|
|
@@ -2479,7 +2691,7 @@ was the system repository, because the package was vendored inside it (#1).
|
|
|
2479
2691
|
shipping side. This package spent its first six changes as a workspace inside
|
|
2480
2692
|
the Song of Heroic Lands repository, where npm hoisted the root's
|
|
2481
2693
|
`devDependencies` into the workspace root: an import this package never declared
|
|
2482
|
-
still resolved, and failed nowhere but a consumer's install
|
|
2694
|
+
still resolved, and failed nowhere but a consumer's install. The test
|
|
2483
2695
|
walks every module named by the `files` field and holds each bare specifier to
|
|
2484
2696
|
one of three cases — a Node builtin, this package addressing itself, or a
|
|
2485
2697
|
declared `dependency` — and checks the converse: nothing shipped may import a
|
|
@@ -2497,7 +2709,7 @@ the copy before the rest run. A module that hoisted a configured value to import
|
|
|
2497
2709
|
time fails there, and only there.
|
|
2498
2710
|
|
|
2499
2711
|
`tests/config-from-working-directory.test.ts` describes the resolution order
|
|
2500
|
-
itself
|
|
2712
|
+
itself. It builds the shape no unit test can fake — a repository with the
|
|
2501
2713
|
toolchain installed under it, and a second checkout nested inside that
|
|
2502
2714
|
repository with its own configuration and no `node_modules` — and asserts which
|
|
2503
2715
|
configuration a build run in each place reads. The nested case is the one that
|
|
@@ -2522,7 +2734,7 @@ bump and the rewritten `CHANGELOG.md`. That pull request _is_ the pending
|
|
|
2522
2734
|
release: as long as something is merged but unpublished, there is an open pull
|
|
2523
2735
|
request saying so. This is the whole point of the pipeline — the previous,
|
|
2524
2736
|
hand-driven process failed by leaving _nothing_ behind when the final step was
|
|
2525
|
-
forgotten, and on 2026-08-21 it did exactly that for two versions
|
|
2737
|
+
forgotten, and on 2026-08-21 it did exactly that for two versions.
|
|
2526
2738
|
|
|
2527
2739
|
**Merging that publishes.** `changeset publish` puts the version on npm through
|
|
2528
2740
|
Trusted Publishing (OIDC — there is no `NPM_TOKEN`), tags the commit `v<version>`
|