@heroiclands/package-build 20.4.0 → 20.6.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 +288 -0
- package/CONTENT.md +213 -20
- package/README.md +19 -1
- package/bin/content-build.mjs +135 -32
- package/bin/package-build.mjs +46 -13
- package/content-config.mjs +345 -101
- package/docs/api.md +1352 -0
- package/docs/commands.md +1609 -0
- package/docs/configuration.md +1432 -0
- package/docs/content-format.md +16 -6
- package/docs/diagnostics.md +356 -0
- package/docs/getting-started.md +813 -0
- package/docs/project-setup.md +469 -0
- package/engine/actor-compiler.mjs +30 -27
- package/engine/address-diff.mjs +45 -41
- package/engine/base-compiler.mjs +6 -0
- package/engine/bundles.mjs +9 -0
- package/engine/content-address.mjs +9 -9
- package/engine/content-index.mjs +44 -23
- package/engine/content-links.mjs +44 -11
- package/engine/content-lint.mjs +44 -10
- package/engine/content-tables.mjs +32 -27
- package/engine/folder-notes.mjs +4 -2
- package/engine/frontmatter-lint.mjs +35 -38
- package/engine/generate.mjs +5 -0
- package/engine/helpers.mjs +86 -32
- package/engine/index.mjs +12 -2
- package/engine/journals.mjs +9 -0
- package/engine/note-claims.mjs +18 -10
- package/engine/note-schemas.mjs +0 -5
- package/engine/note-vocabulary.mjs +32 -31
- package/engine/pack-config.mjs +26 -12
- 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 +876 -0
- package/engine/pdf-toc.mjs +525 -0
- package/engine/scenes.mjs +14 -5
- package/engine/schema-check.mjs +1 -1
- package/engine/site-build.mjs +21 -3
- package/engine/web-wikilinks.mjs +6 -3
- package/engine/wikilinks.mjs +2 -4
- package/hm3/actors.mjs +8 -0
- package/hm3/items.mjs +8 -0
- package/package.json +1 -1
- package/release.mjs +63 -3
- package/sohl/actors.mjs +8 -0
- package/sohl/items.mjs +8 -0
- package/sohl/note-schemas.mjs +5 -5
- package/types/content-config.d.mts +66 -15
- package/types/engine/actor-compiler.d.mts +34 -30
- package/types/engine/address-diff.d.mts +57 -3
- package/types/engine/base-compiler.d.mts +10 -2
- package/types/engine/bundles.d.mts +9 -0
- package/types/engine/content-address.d.mts +9 -9
- package/types/engine/content-index.d.mts +57 -13
- package/types/engine/content-lint.d.mts +6 -4
- package/types/engine/content-tables.d.mts +49 -18
- package/types/engine/frontmatter-lint.d.mts +3 -2
- package/types/engine/helpers.d.mts +105 -31
- package/types/engine/index.d.mts +4 -0
- package/types/engine/journals.d.mts +9 -0
- package/types/engine/note-claims.d.mts +17 -10
- package/types/engine/note-vocabulary.d.mts +23 -196
- package/types/engine/pack-config.d.mts +4 -4
- 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 +156 -0
- package/types/engine/pdf-toc.d.mts +114 -0
- package/types/engine/scenes.d.mts +10 -1
- package/types/engine/schema-check.d.mts +2 -2
- package/types/engine/site-build.d.mts +34 -6
- package/types/engine/wikilinks.d.mts +2 -3
- package/types/hm3/actors.d.mts +8 -0
- package/types/hm3/items.d.mts +8 -0
- package/types/release.d.mts +15 -4
- package/types/sohl/actors.d.mts +10 -2
- package/types/sohl/items.d.mts +8 -0
package/docs/content-format.md
CHANGED
|
@@ -346,7 +346,10 @@ there are exactly three answers:
|
|
|
346
346
|
|
|
347
347
|
`<assetRoot>` is `<packageKind>/<foundryPackage>/assets`, derived from the
|
|
348
348
|
configuration — `systems/sohl/assets` for the system,
|
|
349
|
-
`modules/sohl-thalorna/assets` for that module.
|
|
349
|
+
`modules/sohl-thalorna/assets` for that module. A `documentation` package has
|
|
350
|
+
no asset root at all, because Foundry installs no such package and serves no
|
|
351
|
+
files for it: there, the third row is refused, and a note names the owning
|
|
352
|
+
package (`systems/…`, `modules/…`) or a URL. So one authored
|
|
350
353
|
`icons/relic.svg` means "my own `assets/icons/relic.svg`" in whichever package
|
|
351
354
|
writes it, while an authored `systems/sohl/assets/icons/noun/shield.svg` names
|
|
352
355
|
the system's file and is left exactly as written wherever it appears. That
|
|
@@ -823,9 +826,9 @@ only in case are two names nobody can tell apart, and they collapsed onto one
|
|
|
823
826
|
address, one `_id` and one URL with nothing to report it.
|
|
824
827
|
|
|
825
828
|
**Parsing is positional counting from the right, and nothing else.** Every
|
|
826
|
-
segment is alphanumeric — shortcodes, **types** and **subTypes** are
|
|
827
|
-
`^[a-z0-9]+$`, systems come from a closed registry, and `contentPackage` is
|
|
828
|
-
|
|
829
|
+
segment is lowercase alphanumeric — shortcodes, **types** and **subTypes** are
|
|
830
|
+
all `^[a-z0-9]+$`, systems come from a closed registry, and `contentPackage` is
|
|
831
|
+
`^[a-z0-9]+$` too — so the hyphen is purely a separator. There is no longest-match
|
|
829
832
|
against a roster and no vocabulary check before splitting.
|
|
830
833
|
|
|
831
834
|
**`type` and `subType` are held to that charset, not merely expected to meet
|
|
@@ -2266,9 +2269,15 @@ Content prepared to be played — a situation with its cast, places, and possibl
|
|
|
2266
2269
|
- rules: The rules of the game, independent of medium — valid at a table with paper and dice.
|
|
2267
2270
|
- userguide: How to operate the Foundry implementation to play by the rules.
|
|
2268
2271
|
- reference: Out-of-world lookup material about the setting or system — correspondences, conversions, glossaries.
|
|
2272
|
+
- howto: A task with an outcome, written as the steps that reach it.
|
|
2273
|
+
- concept: An explanation of how something works and why it is shaped that way, read to understand rather than to follow.
|
|
2269
2274
|
|
|
2270
2275
|
A `doc` declares no properties of its own.
|
|
2271
2276
|
|
|
2277
|
+
The field is a genre and only a genre: it says what kind of page this is, never
|
|
2278
|
+
who reads it. A page written for a developer is a `howto` or a `concept` like
|
|
2279
|
+
any other, and the audience is the section it sits in.
|
|
2280
|
+
|
|
2272
2281
|
**A page that introduces a type is an ordinary note, named by convention.**
|
|
2273
2282
|
Write `type: doc`, `subType: reference`, `shortcode: <type>` — so the
|
|
2274
2283
|
affiliations introduction is `doc-affiliation`, addressed and linked like
|
|
@@ -2452,8 +2461,9 @@ data:
|
|
|
2452
2461
|
A folder is addressed `<package>-none-folder-<shortcode>` — **`none`**, because a
|
|
2453
2462
|
`Folder` is a core Foundry document like a `JournalEntry` or a `Scene`, not a
|
|
2454
2463
|
system's. Its shortcode is [an address segment](#the-canonical-address) like
|
|
2455
|
-
every other, so it is strictly alphanumeric: `possessionscooking`,
|
|
2456
|
-
`possessions-cooking`, which would read as two segments and resolve to
|
|
2464
|
+
every other, so it is strictly lowercase alphanumeric: `possessionscooking`,
|
|
2465
|
+
never `possessions-cooking`, which would read as two segments and resolve to
|
|
2466
|
+
nothing.
|
|
2457
2467
|
|
|
2458
2468
|
**`color` must be quoted**, and YAML gives no third option: `color: #7a4b2a`
|
|
2459
2469
|
parses as `null` (a `#` after a space opens a comment) and `color: 000000` parses
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# Diagnostics
|
|
2
|
+
|
|
3
|
+
Every warning or error this toolchain emits is machine-parseable and says
|
|
4
|
+
where it is. This document is for the person reading a wall of that output:
|
|
5
|
+
what the fields mean, why a field is sometimes missing, why a run's exit code
|
|
6
|
+
does not always track its warning count, and how to parse the output
|
|
7
|
+
yourself. `engine/diagnostics.mjs` is the module that owns the contract;
|
|
8
|
+
everything here is read from it.
|
|
9
|
+
|
|
10
|
+
## The located form
|
|
11
|
+
|
|
12
|
+
A finding about a file is printed as:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
file:line:column: severity: message
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
For example:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
assets/content/Regions/Capital_Nome.md:43:635: error: address [[place-kenbetpat]] resolves to no note — no package publishes it. Fix the shortcode, or declare the package that does as a dependency and run `content-build deps fetch` — in "The Capital Nome".
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That is the same shape every C-family compiler, `tsc` and ESLint already use,
|
|
25
|
+
which is deliberate: an editor, a CI annotator or a `grep` parses it with no
|
|
26
|
+
knowledge of this build. `formatDiagnostic` builds the line and
|
|
27
|
+
`formatLocator` builds the `file:line:column` part of it; `severity` is
|
|
28
|
+
either `warning` or `error`, and `message` is one sentence.
|
|
29
|
+
|
|
30
|
+
The path is relative to the working directory — during a build, the
|
|
31
|
+
consuming repository's root — because that is both shorter to read and what
|
|
32
|
+
an editor resolves a relative diagnostic against. A file outside the working
|
|
33
|
+
directory keeps its absolute path; a `../../..` locator would help nobody.
|
|
34
|
+
|
|
35
|
+
Three rules hold the form together.
|
|
36
|
+
|
|
37
|
+
### The path starts the line
|
|
38
|
+
|
|
39
|
+
A located diagnostic is never indented and never carries a prefix — not a
|
|
40
|
+
timestamp, not a severity tag, nothing before the file name. `emitDiagnostic`
|
|
41
|
+
prints every diagnostic with a bare `console.error` or `console.warn`,
|
|
42
|
+
deliberately bypassing this toolchain's own `loglevel` logger, whose
|
|
43
|
+
configured `[timestamp] [WARN]:` prefix occupies exactly the position a
|
|
44
|
+
parser reads the path from. A greedy path pattern would swallow that prefix
|
|
45
|
+
and hand a reader a filename nothing can open. Progress and summary lines are
|
|
46
|
+
not diagnostics and go through `loglevel` as ordinary prose — see
|
|
47
|
+
[Summary counts and prose are not findings](#summary-counts-and-prose-are-not-findings)
|
|
48
|
+
below.
|
|
49
|
+
|
|
50
|
+
### A field is dropped, never guessed
|
|
51
|
+
|
|
52
|
+
`formatLocator` reports the most precise position it can honestly establish
|
|
53
|
+
and nothing more:
|
|
54
|
+
|
|
55
|
+
- `file:line:column: …` when a line and a meaningful column are both known.
|
|
56
|
+
- `file:line: …` when only the line is — a column is ignored entirely once
|
|
57
|
+
there is no line to pair it with, since a column alone locates nothing.
|
|
58
|
+
- `file: …` when nothing beyond the file itself could be established.
|
|
59
|
+
|
|
60
|
+
Nothing ever defaults to `1:1`. A missing position that silently became `1:1`
|
|
61
|
+
would send a reader to the top of the file — usually the frontmatter — for
|
|
62
|
+
every diagnostic that could not otherwise be placed, and it would look
|
|
63
|
+
exactly like a real position instead of an absent one.
|
|
64
|
+
|
|
65
|
+
### An implicit position is recovered by search
|
|
66
|
+
|
|
67
|
+
Some diagnostics know their position by construction — a compile pass
|
|
68
|
+
tracking a scanner's offset as it reads a note's body. Others know only that
|
|
69
|
+
something is wrong with a literal a file contains — a wikilink, a caption, a
|
|
70
|
+
localization key — and never carried a byte offset in the first place.
|
|
71
|
+
`engine/diagnostics.mjs` supplies one function for the first case and three
|
|
72
|
+
for the second, and all of them fall back to `{}` — an empty spreadable
|
|
73
|
+
object — when a position cannot be established, so a caller writes
|
|
74
|
+
`{ ...position }` and gets the drop-not-guess rule for free.
|
|
75
|
+
|
|
76
|
+
**`positionInBody(body, offset, opts)`** answers the constructed case: where
|
|
77
|
+
a character offset into a note's parsed **body** falls in the note's **file**.
|
|
78
|
+
Three corrections apply, each only where it is true:
|
|
79
|
+
|
|
80
|
+
1. The frontmatter's own lines are not part of the body, so an offset is not
|
|
81
|
+
a file line until they are added back (`opts.bodyLine`).
|
|
82
|
+
2. The body is trimmed before it is scanned, which can strip leading
|
|
83
|
+
whitespace from only its first line; `opts.bodyColumn` restores it, and
|
|
84
|
+
only on that line.
|
|
85
|
+
3. A body is scanned **after** its content tables expand, so an offset can
|
|
86
|
+
land in text nobody authored. `opts.lineMap` — from
|
|
87
|
+
`expandContentTables` — maps each scanned line back to the authored line
|
|
88
|
+
it came from. A line with no authored source reports the directive that
|
|
89
|
+
generated it and **no column**, because there is no authored character to
|
|
90
|
+
point a column at; `positionInBody` marks this `generated: true`.
|
|
91
|
+
|
|
92
|
+
**`positionOfLiteral(text, needle, occurrence)`** answers the plain case: a
|
|
93
|
+
string the reader can see in a file that is not a note body — a manifest, a
|
|
94
|
+
lockfile, a config file, a file being checked as raw text rather than parsed
|
|
95
|
+
frontmatter. Because one literal can appear more than once, `occurrence`
|
|
96
|
+
(1-based, default `1`) selects which match the position is for — the caller
|
|
97
|
+
counts how many times it has already seen the literal in this file and passes
|
|
98
|
+
the next number, so four identical findings on one note land on four
|
|
99
|
+
different lines or columns instead of collapsing onto the first.
|
|
100
|
+
|
|
101
|
+
**`positionInFrontmatter(raw, key, value, opts)`** locates a **top-level
|
|
102
|
+
frontmatter key** by searching the fence rather than the whole file — a bare
|
|
103
|
+
search for a key like `name` or `type` would routinely match a line of prose
|
|
104
|
+
first. Passing `value` narrows the match to the entry that carries it, which
|
|
105
|
+
is what a list-valued key (`aliases`) needs: the finding belongs on the
|
|
106
|
+
wrong entry, not on the key that introduces the list. `opts.topLevel`
|
|
107
|
+
requires the match at column 1, for a key name that is legal both at the top
|
|
108
|
+
level and nested (`aliases` is both a retired top-level field and a
|
|
109
|
+
permitted `name.aliases`) — without it, a finding about the retired field
|
|
110
|
+
could resolve to the nested one and tell an author to delete a field they are
|
|
111
|
+
allowed to write.
|
|
112
|
+
|
|
113
|
+
**`positionOfYamlPath(text, keyPath, opts)`** and
|
|
114
|
+
**`positionOfFrontmatterPath(raw, keyPath, opts)`** locate a node by
|
|
115
|
+
**path** rather than by searching for a name, which is the right tool once a
|
|
116
|
+
name can legally appear in more than one place — `data.weight` and a
|
|
117
|
+
top-level `weight` are different keys, and a name search finds whichever
|
|
118
|
+
comes first. `keyPath` is an array of map keys (strings) and sequence indices
|
|
119
|
+
(numbers); `yamlKeyPath(field)` turns a dotted path as a message would write
|
|
120
|
+
it — `packs[1].name`, `site.sections.affliction.title` — into that array,
|
|
121
|
+
refusing the whole path rather than resolving part of it if any segment does
|
|
122
|
+
not parse. `positionOfYamlPath` re-parses the document to get a node with a
|
|
123
|
+
range attached — the loader that originally read the file discards ranges
|
|
124
|
+
once the data is materialized, so carrying a parallel position tree through
|
|
125
|
+
configuration resolution would be a second representation of the same file to
|
|
126
|
+
keep in sync. `positionOfFrontmatterPath` is the same lookup scoped to a
|
|
127
|
+
note's frontmatter fence rather than a whole document.
|
|
128
|
+
|
|
129
|
+
By default the position is the node's **value** — a finding that a value is
|
|
130
|
+
wrong belongs on the value. Passing `opts.key: true` reports where the last
|
|
131
|
+
segment is **declared** instead, for a finding that names the field itself
|
|
132
|
+
(`site.sections.x is not a recognized option`) — in a flow mapping like
|
|
133
|
+
`{ title: X, banner: Y }` the key and the value are different columns on the
|
|
134
|
+
same line, and a field-naming finding should send the reader to the field,
|
|
135
|
+
not to whatever happens to be written there.
|
|
136
|
+
|
|
137
|
+
## A configuration error is located the same way
|
|
138
|
+
|
|
139
|
+
`content-config.mjs` and `config.mjs` report every configuration check
|
|
140
|
+
through one `fail()`, which names the offending key's dotted path and knows
|
|
141
|
+
nothing about where in the file it was written.
|
|
142
|
+
`locateConfigError`, in `engine/pack-config.mjs`, is where the path becomes a
|
|
143
|
+
position: it turns the dotted field into a `keyPath` with `yamlKeyPath`, asks
|
|
144
|
+
`positionOfYamlPath` for that key's own declaration (`{ key: true }`), and
|
|
145
|
+
falls back one level to the **enclosing mapping** when the exact key has no
|
|
146
|
+
node of its own — a required key that is simply absent has nothing to point
|
|
147
|
+
at but the block it belongs in. A field one level deep with nothing above it
|
|
148
|
+
but the whole document gets no position at all, which is the same
|
|
149
|
+
drop-rather-than-guess rule applied one level higher.
|
|
150
|
+
|
|
151
|
+
This only runs against a YAML configuration. An `.mjs` configuration is
|
|
152
|
+
JavaScript, not data a YAML parser can be asked for a range in — feeding it
|
|
153
|
+
to one would not fail, since arbitrary JavaScript source routinely also
|
|
154
|
+
parses as _some_ YAML document, and the resulting position would point at a
|
|
155
|
+
line that has nothing to do with the key. A wrong position is worse than
|
|
156
|
+
none, so `positionInConfig` (the function behind `locateConfigError`) checks
|
|
157
|
+
the file extension first and returns `{}` for anything that is not
|
|
158
|
+
`.yaml`/`.yml`.
|
|
159
|
+
|
|
160
|
+
A configuration finding reads the same as any other:
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
package-build.config.yaml:382:64: error: package-build config: `site.sections.being.descrption` is not a recognized option (expected one of: title, banner, description, listType, listSubType).
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Summary counts and prose are not findings
|
|
167
|
+
|
|
168
|
+
A run's progress and summary lines — `42 address(es) across 10 note(s).`,
|
|
169
|
+
`Formatting is clean (120 file(s)).`, `3 of 22 compared field pair(s)
|
|
170
|
+
disagree between the specification and the declaration that compiles them.`
|
|
171
|
+
— are not diagnostics and are never emitted through `emitDiagnostic`. They
|
|
172
|
+
carry no file, no position and nothing for a parser to act on; printing them
|
|
173
|
+
in the located form would be a lie about what is known; a machine reading the
|
|
174
|
+
findings has to skip them regardless, since they have no `file:line:column:`
|
|
175
|
+
prefix to key on. They go through this toolchain's ordinary `loglevel`
|
|
176
|
+
logger and keep whatever prefix that logger is configured with.
|
|
177
|
+
|
|
178
|
+
## Both severities go to stderr
|
|
179
|
+
|
|
180
|
+
`emitDiagnostic` writes both a `warning` and an `error` to `console.error`.
|
|
181
|
+
`console.warn` writes to `process.stderr` too — in Node it is simply another
|
|
182
|
+
name for `console.error` — so there is no separate warning stream to redirect
|
|
183
|
+
a build's findings away from its errors. Splitting warnings from errors, or
|
|
184
|
+
suppressing one severity, means reading the emitted `severity` field, not
|
|
185
|
+
picking which stream to listen on. A shell filter that assumes warnings land
|
|
186
|
+
on stdout and errors on stderr will see nothing on stdout and everything on
|
|
187
|
+
stderr, including every warning.
|
|
188
|
+
|
|
189
|
+
## Which commands exit non-zero
|
|
190
|
+
|
|
191
|
+
The rule that recurs everywhere a command reports diagnostics: **a run fails
|
|
192
|
+
when it reports at least one `error`-severity diagnostic. A `warning` alone
|
|
193
|
+
never fails a run.** `bin/report.mjs`'s `reportFindings` draws this line once
|
|
194
|
+
for the commands built from the package's pure rule functions (`content-build
|
|
195
|
+
lint`, `package-build lang check`, `package-build lang coverage`,
|
|
196
|
+
`package-build yaml`, `package-build labels check`): it emits every finding
|
|
197
|
+
and returns only the count whose `severity` is `error`, and the command sets
|
|
198
|
+
`process.exitCode = 1` exactly when that count is nonzero. Commands whose
|
|
199
|
+
findings are hand-assembled — `content-build links`, `content-build
|
|
200
|
+
reachability`, `content-build site`, `content-build package compile` — apply
|
|
201
|
+
the same rule by construction: everything they can report is a broken
|
|
202
|
+
address, a dead link or a document Foundry would silently drop a field from,
|
|
203
|
+
so every finding they emit is already `severity: "error"` and any finding at
|
|
204
|
+
all fails the run.
|
|
205
|
+
|
|
206
|
+
**A corpus problem always fails, regardless of severity or a `--strict`
|
|
207
|
+
flag.** When the content tree itself cannot be indexed — a note the walk
|
|
208
|
+
cannot record, most often a retired `package:` key — the note is absent from
|
|
209
|
+
every other answer a command gives. Reporting it and still exiting `0` would
|
|
210
|
+
call the tree clean while silently omitting a note, so every command that
|
|
211
|
+
indexes a corpus (`lint`, `links`, `reachability`, `content-format notes`,
|
|
212
|
+
`addresses diff`) fails on a corpus problem unconditionally, before it
|
|
213
|
+
evaluates anything else.
|
|
214
|
+
|
|
215
|
+
**A few commands report advisory findings that never fail the run on their
|
|
216
|
+
own**, and say so where they emit them: `content-format notes` measures a
|
|
217
|
+
content tree against the format specification as a progress report during a
|
|
218
|
+
migration, printing every finding as a `warning` and passing `--strict` to
|
|
219
|
+
promote a class to `error` only once that class has reached zero; `format`
|
|
220
|
+
reports where this repository's Prettier configuration diverges from the
|
|
221
|
+
shared convention as a `warning`, because a repository's own configuration
|
|
222
|
+
is allowed to win on purpose; `addresses diff` reports a withdrawn or renamed
|
|
223
|
+
published address as a `warning` by default and only as an `error` under
|
|
224
|
+
`--strict`, since retiring or renaming content is legitimate and the check
|
|
225
|
+
exists to make sure it was noticed, not to forbid it; `pdf` reports a
|
|
226
|
+
book-build finding but fails only when it could not build the book for a
|
|
227
|
+
reason that is not a deliberate no-op (an empty filter, an absent block); and
|
|
228
|
+
`lang coverage`'s unreferenced-key half is advisory without a way to promote
|
|
229
|
+
it — nothing can see every way a key might be reached, so failing a build
|
|
230
|
+
over one would teach people to stop reading the report.
|
|
231
|
+
|
|
232
|
+
| command | fails when |
|
|
233
|
+
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
234
|
+
| `content-build lint [root]` | an address, frontmatter, schema, charset, icon or HTML finding is `error`-severity, or the corpus cannot be indexed |
|
|
235
|
+
| `content-build content-format notes [root]` | `--strict` was passed and it reported any finding (all findings are `error`-severity only under `--strict`), or the corpus cannot be indexed |
|
|
236
|
+
| `content-build content-format schema` | it reports any finding at all |
|
|
237
|
+
| `content-build content-format fields` | it reports any finding at all |
|
|
238
|
+
| `content-build links [root]` | it reports any finding at all, an index is unusable (a stale or unaddressable foreign manifest), or the corpus cannot be indexed |
|
|
239
|
+
| `content-build reachability <dir> [file]` | it reports any orphaned document, or the corpus cannot be indexed |
|
|
240
|
+
| `content-build addresses diff --from <artifact>` | the corpus cannot be indexed (always); otherwise only with `--strict`, and only then are the address findings `error`-severity |
|
|
241
|
+
| `content-build format [paths..]` | a file it checked is not formatted (or, under `--write`, could not be brought to a fixpoint) — never on a shared-convention divergence, which is always advisory |
|
|
242
|
+
| `content-build markdown [paths..]` | it reports any markdownlint finding |
|
|
243
|
+
| `content-build site` | any gate finding (no homepage, a wikilink in frontmatter, an address that cannot be derived, an unusable or unaddressable foreign manifest, a conflicting address) or any table or wikilink error while writing pages — the run stops at the first gate that fires |
|
|
244
|
+
| `content-build pdf` | it could not build the book for a reason other than a deliberate no-op — never on a reported finding by itself |
|
|
245
|
+
| `content-build docs item-fields --check` | the generated page differs from what is committed |
|
|
246
|
+
| `content-build package compile` | pack JSON generation reported any `error`-severity finding (checked, and the compile refused, before any pack is written), or a compiled pack fails its Scene/Level integrity check |
|
|
247
|
+
| `content-build deps fetch` | the fetch itself throws — it emits no diagnostics |
|
|
248
|
+
| `package-build lang check` | any localization finding is `error`-severity |
|
|
249
|
+
| `package-build lang coverage` | a referenced key is missing (`error`-severity); an unreferenced key is always advisory and never fails the run |
|
|
250
|
+
| `package-build yaml [paths..]` | any YAML finding is `error`-severity |
|
|
251
|
+
| `package-build labels check` | the label registry and the documented table disagree on any label |
|
|
252
|
+
|
|
253
|
+
Every command in both binaries also fails on a thrown error unrelated to a
|
|
254
|
+
diagnostic — a missing configuration, an unreadable file, a network failure —
|
|
255
|
+
through the same `process.exitCode = 1` (or `process.exit(1)`), reported as
|
|
256
|
+
one line with no stack.
|
|
257
|
+
|
|
258
|
+
## A worked example
|
|
259
|
+
|
|
260
|
+
This parses a run's diagnostics the way a script or a CI step would: run the
|
|
261
|
+
command, capture stderr, turn each line into a record, and decide what to do
|
|
262
|
+
based on the `severity` field.
|
|
263
|
+
|
|
264
|
+
A command that emits a few diagnostics, using the same `emitDiagnostic` every
|
|
265
|
+
pass in this package calls:
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
// emit.mjs
|
|
269
|
+
import { emitDiagnostic } from "@heroiclands/package-build/engine/diagnostics";
|
|
270
|
+
|
|
271
|
+
emitDiagnostic({
|
|
272
|
+
file: "assets/content/Regions/Capital_Nome.md",
|
|
273
|
+
line: 43,
|
|
274
|
+
column: 635,
|
|
275
|
+
severity: "error",
|
|
276
|
+
message:
|
|
277
|
+
"address [[place-kenbetpat]] resolves to no note — no package publishes it. " +
|
|
278
|
+
"Fix the shortcode, or declare the package that does as a dependency and " +
|
|
279
|
+
'run `content-build deps fetch` — in "The Capital Nome".',
|
|
280
|
+
});
|
|
281
|
+
emitDiagnostic({
|
|
282
|
+
file: "assets/content/Regions/Capital_Nome.md",
|
|
283
|
+
line: 12,
|
|
284
|
+
severity: "warning",
|
|
285
|
+
message: "`system.sohl.unemitted` is declared but no builder ever sets it",
|
|
286
|
+
});
|
|
287
|
+
emitDiagnostic({
|
|
288
|
+
file: "assets/content/Regions/Capital_Nome.md",
|
|
289
|
+
severity: "error",
|
|
290
|
+
message: "duplicate frontmatter key `name` — the second declaration wins and the first is dead",
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// A build's own summary line: prose, not a finding.
|
|
294
|
+
console.log("3 finding(s) across 1 note(s).");
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
A parser that runs it, reads its diagnostics off stderr, and reports the
|
|
298
|
+
split:
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
// parse.mjs
|
|
302
|
+
import { spawn } from "node:child_process";
|
|
303
|
+
import { fileURLToPath } from "node:url";
|
|
304
|
+
|
|
305
|
+
// `formatDiagnostic`'s own grammar, read backwards: the path is whatever
|
|
306
|
+
// comes before an optional `:line` and `:line:column`, then
|
|
307
|
+
// `severity: message`. A field the diagnostic dropped is simply absent from
|
|
308
|
+
// its capture group — never a guessed `1:1`.
|
|
309
|
+
const LOCATED =
|
|
310
|
+
/^(?<file>[^:]+?)(?::(?<line>\d+))?(?::(?<column>\d+))?: (?<severity>warning|error): (?<message>.+)$/;
|
|
311
|
+
|
|
312
|
+
function parseDiagnostic(line) {
|
|
313
|
+
const m = LOCATED.exec(line);
|
|
314
|
+
if (!m) return null;
|
|
315
|
+
const { file, line: ln, column, severity, message } = m.groups;
|
|
316
|
+
return {
|
|
317
|
+
file,
|
|
318
|
+
...(ln ? { line: Number(ln) } : {}),
|
|
319
|
+
...(column ? { column: Number(column) } : {}),
|
|
320
|
+
severity,
|
|
321
|
+
message,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const child = spawn(process.execPath, [fileURLToPath(new URL("./emit.mjs", import.meta.url))]);
|
|
326
|
+
let stderr = "";
|
|
327
|
+
child.stderr.on("data", (chunk) => (stderr += chunk));
|
|
328
|
+
await new Promise((resolve) => child.on("close", resolve));
|
|
329
|
+
|
|
330
|
+
const findings = stderr.split("\n").filter(Boolean).map(parseDiagnostic).filter(Boolean);
|
|
331
|
+
const errors = findings.filter((f) => f.severity === "error");
|
|
332
|
+
const warnings = findings.filter((f) => f.severity === "warning");
|
|
333
|
+
|
|
334
|
+
console.log(`${errors.length} error(s), ${warnings.length} warning(s):\n`);
|
|
335
|
+
for (const f of findings) {
|
|
336
|
+
const where = f.file + (f.line ? `:${f.line}` + (f.column ? `:${f.column}` : "") : "");
|
|
337
|
+
console.log(`[${f.severity}] ${where} — ${f.message}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
process.exitCode = errors.length ? 1 : 0;
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Running `node parse.mjs` prints:
|
|
344
|
+
|
|
345
|
+
```text
|
|
346
|
+
2 error(s), 1 warning(s):
|
|
347
|
+
|
|
348
|
+
[error] assets/content/Regions/Capital_Nome.md:43:635 — address [[place-kenbetpat]] resolves to no note — no package publishes it. Fix the shortcode, or declare the package that does as a dependency and run `content-build deps fetch` — in "The Capital Nome".
|
|
349
|
+
[warning] assets/content/Regions/Capital_Nome.md:12 — `system.sohl.unemitted` is declared but no builder ever sets it
|
|
350
|
+
[error] assets/content/Regions/Capital_Nome.md — duplicate frontmatter key `name` — the second declaration wins and the first is dead
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
and exits `1`, because it saw two `error`-severity diagnostics. The summary
|
|
354
|
+
line the emitting process printed to stdout, `3 finding(s) across 1
|
|
355
|
+
note(s).`, never reaches the parser at all — it was never on stderr, and the
|
|
356
|
+
regex would not have matched it if it had been.
|