@heroiclands/package-build 22.17.1 → 22.17.2
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 +14 -0
- package/bin/content-commands.mjs +21 -23
- package/docs/api.md +4 -14
- package/docs/commands.md +19 -18
- package/docs/reference/format-details.md +27 -45
- package/docs/reference/note-types.md +1 -1
- package/engine/actor-compiler.mjs +5 -5
- package/engine/address-diff.mjs +32 -279
- package/engine/art-fields.mjs +8 -5
- package/engine/content-format.yaml +4 -4
- package/engine/content-index.mjs +28 -3
- package/engine/content-lint.mjs +3 -182
- package/engine/frontmatter-lint.mjs +112 -59
- package/engine/helpers.mjs +15 -3
- package/engine/index.mjs +0 -3
- package/engine/map-graphviz.mjs +4 -1
- package/engine/note-frontmatter.mjs +1 -0
- package/engine/note-vocabulary.mjs +1 -13
- package/engine/subtype-registry.mjs +2 -6
- package/engine/systems.mjs +11 -13
- package/package.json +1 -1
- package/types/engine/actor-compiler.d.mts +5 -5
- package/types/engine/address-diff.d.mts +18 -107
- package/types/engine/art-fields.d.mts +4 -5
- package/types/engine/frontmatter-lint.d.mts +14 -0
- package/types/engine/helpers.d.mts +12 -1
- package/types/engine/index.d.mts +0 -1
- package/types/engine/note-vocabulary.d.mts +0 -9
- package/types/engine/subtype-registry.d.mts +2 -6
- package/types/engine/systems.d.mts +6 -6
- package/engine/note-renames.mjs +0 -134
- package/types/engine/note-renames.d.mts +0 -102
package/engine/address-diff.mjs
CHANGED
|
@@ -12,75 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Compare published Item addresses between a released package and its current
|
|
16
|
+
* build. The `(type, shortcode)` pair is the address consumers resolve from
|
|
17
|
+
* package indexes.
|
|
16
18
|
*
|
|
17
|
-
* A
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* renaming a shortcode is a breaking change to something other repositories
|
|
22
|
-
* consume, and until this module there was nothing that noticed: the check that
|
|
23
|
-
* got made was a repository-local grep, which cannot see the other
|
|
24
|
-
* repositories and reports the reassuring answer.
|
|
25
|
-
*
|
|
26
|
-
* `sohl` renamed one weapon's shortcode from `Tabri` to `Taburi` two days after
|
|
27
|
-
* the `v0.8.2` tag, on the stated ground that "nothing referenced the old
|
|
28
|
-
* value, so the rename is self-contained". True of that repository. Both
|
|
29
|
-
* satellites pin `v0.8.2` and address `weapongear:Tabri` on their copy of the
|
|
30
|
-
* same character — five lookups that resolve today and fail the moment either
|
|
31
|
-
* pin moves, with an error reading like a missing item.
|
|
32
|
-
*
|
|
33
|
-
* **The comparison is release-to-release, in the repository doing the
|
|
34
|
-
* renaming.** The alternative — checking a consumer's addresses against its
|
|
35
|
-
* pinned release — already exists and already fails the build (`no predefined
|
|
36
|
-
* item for "weapongear:Taburi"`); what it lacks is an explanation, and it
|
|
37
|
-
* cannot honestly produce one, because at the point of the miss all it holds is
|
|
38
|
-
* the address string. It has no document id and no name to match a candidate
|
|
39
|
-
* against, so any successor it named would be a guess at a similar-looking
|
|
40
|
-
* string. Here both sides are whole documents, so the question is decidable.
|
|
41
|
-
*
|
|
42
|
-
* **A rename is told from a removal by the document id, and that is an identity
|
|
43
|
-
* match rather than an inference.** An address that disappeared while its
|
|
44
|
-
* document is still published elsewhere *is* a rename — not "probably" one.
|
|
45
|
-
* When the id is published under no address at all, that is all this can say:
|
|
46
|
-
* **withdrawn**, with no successor named. A split, a deletion and a merge are
|
|
47
|
-
* indistinguishable from one another at that point, and inventing a "did you
|
|
48
|
-
* mean" from string similarity would be worse than saying nothing, because a
|
|
49
|
-
* wrong one sends the reader to the wrong fix.
|
|
50
|
-
*
|
|
51
|
-
* **That match is narrow, and a declaration is what makes up the
|
|
52
|
-
* difference.** The join rested on the id being independent of the shortcode: a
|
|
53
|
-
* note authored its `_id`, so the `Tabri` → `Taburi` commit changed the
|
|
54
|
-
* shortcode alone and left the id to join the two sides. An id is
|
|
55
|
-
* *derived from the canonical address*, which carries the shortcode — so
|
|
56
|
-
* renaming a shortcode moves the id too, both sides of the join move together,
|
|
57
|
-
* and the match finds nothing. It stays exact for a note that **pins** an `id`,
|
|
58
|
-
* and pinning is still how a document keeps its identity across a rename; what
|
|
59
|
-
* it cannot do is help the author who did not pin, because pinning has to
|
|
60
|
-
* happen before the rename, by someone who does not yet know they will make
|
|
61
|
-
* one.
|
|
62
|
-
*
|
|
63
|
-
* So an author who has just renamed a shortcode **says so**, with
|
|
64
|
-
* `renamedFrom:` on the note that made the change (and see
|
|
65
|
-
* `engine/note-renames.mjs`). That is neither a guess nor an identity match but
|
|
66
|
-
* testimony from the only party that knows, and the diagnostic reports which of
|
|
67
|
-
* the two it had rather than blending them — a reader can verify a matched id
|
|
68
|
-
* in both artefacts, and can only take a declaration on its author's word.
|
|
69
|
-
*
|
|
70
|
-
* **Three joins, tried in that order of authority**: the document id, then a
|
|
71
|
-
* declaration, then nothing — which remains **withdrawn**. Nothing here
|
|
72
|
-
* infers, so a rename that is neither pinned nor declared is still reported as
|
|
73
|
-
* a withdrawal; that is the honest answer, not a gap.
|
|
74
|
-
*
|
|
75
|
-
* **Severity is decided per case.** A withdrawal is legitimate — content is
|
|
76
|
-
* allowed to be retired — so it is reported and does not fail a build. A rename
|
|
77
|
-
* is equally legitimate as a decision (the charset rule forces some), which
|
|
78
|
-
* is why it does not fail one either; what it must not do is happen in silence.
|
|
79
|
-
* A caller that wants a gate passes `error` and treats any finding as one.
|
|
80
|
-
*
|
|
81
|
-
* Item packs only, because that is the address space consumers resolve
|
|
82
|
-
* against: {@link foreignItemCatalogDirs} extracts nothing else, and a being's
|
|
83
|
-
* embedded items are the only cross-package resolution by `(type, shortcode)`.
|
|
19
|
+
* A departed address is a rename when its document id is still published under
|
|
20
|
+
* another address. Otherwise it is reported as withdrawn. Since an id derived
|
|
21
|
+
* from a note's canonical address changes with its shortcode, that kind of
|
|
22
|
+
* rename produces a withdrawal finding; the added address has no finding.
|
|
84
23
|
*
|
|
85
24
|
* @module
|
|
86
25
|
*/
|
|
@@ -95,19 +34,13 @@ import { assertStatedScope } from "./helpers.mjs";
|
|
|
95
34
|
// index's import graph reaches this module — only `bin/` imports it — so this
|
|
96
35
|
// is a plain static import, as in the link checker.
|
|
97
36
|
import { indexRecordsFor, isNoteRecord, noteFile } from "./content-index.mjs";
|
|
98
|
-
import { renamedFrom } from "./note-renames.mjs";
|
|
99
|
-
import { referencedSubtype } from "./document-subtypes.mjs";
|
|
100
|
-
import { KNOWN_DOCUMENT_SUBTYPE_MAPS } from "./subtype-registry.mjs";
|
|
101
37
|
|
|
102
38
|
/**
|
|
103
39
|
* The one spelling of an address in this space: `type:shortcode`.
|
|
104
40
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* shortcode is used **verbatim**, not lowercased: `readItemAddresses` reads it
|
|
109
|
-
* off the compiled document, where `Tabri` is stored as authored, and folding
|
|
110
|
-
* case here would join two addresses the packs keep apart.
|
|
41
|
+
* Used for addresses read from compiled documents. The shortcode is used
|
|
42
|
+
* **verbatim**, not lowercased: folding case would join two addresses the packs
|
|
43
|
+
* keep apart.
|
|
111
44
|
*
|
|
112
45
|
* @param {string} type - The Foundry document subtype, not the note type.
|
|
113
46
|
* @param {string} shortcode - The address's `system.shortcode`.
|
|
@@ -174,40 +107,14 @@ export function readItemAddresses(dirs) {
|
|
|
174
107
|
}
|
|
175
108
|
|
|
176
109
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* **One walk, not two.** `addresses diff` reads the tree twice — once for the
|
|
180
|
-
* declarations and once to place its findings — and those would otherwise be
|
|
181
|
-
* two
|
|
182
|
-
* independent walks that each parsed every note. They are now one derivation,
|
|
183
|
-
* shared: the caller derives the records and hands them to both, so the two
|
|
184
|
-
* halves of a single command cannot disagree about which files the corpus is,
|
|
185
|
-
* or about the ids in it.
|
|
186
|
-
*
|
|
187
|
-
* **The id is why it matters, and not only tidiness.** `noteFilesById` joins
|
|
188
|
-
* tree-side ids against ids read out of the *compiled packs*. An id
|
|
189
|
-
* is derived from the canonical address, whose first segment is the content
|
|
190
|
-
* package — and the tree side used to derive it through `resolveNoteId(fm)`
|
|
191
|
-
* with no package, which falls back to `contentPackage()` and so to whichever
|
|
192
|
-
* configuration the working directory answers with. The compiled side is
|
|
193
|
-
* produced by a compiler running on the configuration the *build* resolved. Let
|
|
194
|
-
* those differ — under `PACKAGE_BUILD_CONFIG`, in a worktree, in a test — and
|
|
195
|
-
* every id fails to join, so every rename degrades to a withdrawal and every
|
|
196
|
-
* finding loses the note it should have been reported against. Reading the
|
|
197
|
-
* index derives both sides from the one resolved configuration.
|
|
198
|
-
*
|
|
199
|
-
* A tree that is not there yields no records rather than throwing, which is
|
|
200
|
-
* what the walk this replaces did: an absent tree is a caller's business, and
|
|
201
|
-
* these two functions have never been the ones to report it.
|
|
110
|
+
* Read the scoped content corpus as content-index records.
|
|
202
111
|
*
|
|
203
112
|
* @param {string} contentBase - Root of the content tree.
|
|
204
113
|
* @param {object} opts - Options.
|
|
205
|
-
* @param {readonly string[]} [opts.skipDirectories] - The scope
|
|
206
|
-
*
|
|
207
|
-
* @param {object} [opts.config] - The resolved configuration.
|
|
114
|
+
* @param {readonly string[]} [opts.skipDirectories] - The stated corpus scope.
|
|
115
|
+
* @param {object} [opts.config] - The resolved build configuration.
|
|
208
116
|
* @param {readonly object[]} [opts.records] - Records the caller derived.
|
|
209
|
-
* @param {object[]} [opts.problems] - Collects
|
|
210
|
-
* record, so one of them does not abort the diff before it reports.
|
|
117
|
+
* @param {object[]} [opts.problems] - Collects notes the index cannot record.
|
|
211
118
|
* @returns {readonly object[]} The index records.
|
|
212
119
|
*/
|
|
213
120
|
function addressCorpus(contentBase, { skipDirectories, config, records, problems } = {}) {
|
|
@@ -217,118 +124,20 @@ function addressCorpus(contentBase, { skipDirectories, config, records, problems
|
|
|
217
124
|
return indexRecordsFor({ contentBase, config, skipDirectories, problems });
|
|
218
125
|
}
|
|
219
126
|
|
|
220
|
-
/**
|
|
221
|
-
* Every rename the tree's notes **declare**, as old address → where it went.
|
|
222
|
-
*
|
|
223
|
-
* Read from the content tree rather than from compiled output, because a
|
|
224
|
-
* declaration is authored and the compiled document does not carry it: nothing
|
|
225
|
-
* downstream consumes `renamedFrom:`, so emitting it into every pack to let one
|
|
226
|
-
* diagnostic read it back would put a build-time note in shipped data forever.
|
|
227
|
-
* The tree is already read by this module for the same reason
|
|
228
|
-
* ({@link noteFilesById}) — to place a finding where its author can fix it.
|
|
229
|
-
*
|
|
230
|
-
* **A declaration is keyed by document subtype, not by note type.** The address
|
|
231
|
-
* space is the one consumers resolve against, and it is spelled in compiled
|
|
232
|
-
* documents: `hm3` compiles a `projectile` note into a `missilegear` item,
|
|
233
|
-
* so that is the address a rename of it moves. {@link referencedSubtype} is the
|
|
234
|
-
* function that already answers this for a being's embedded `(type, shortcode)`
|
|
235
|
-
* references, so both sides read the same rule rather than a second copy of it.
|
|
236
|
-
*
|
|
237
|
-
* **An entry is emitted for every system that maps the type**, whether or not
|
|
238
|
-
* the note declares that system's block. Over-emitting is inert — the diff uses
|
|
239
|
-
* an entry only when the baseline published the old address *and* this build
|
|
240
|
-
* publishes the new one, and a system the note does not compile for satisfies
|
|
241
|
-
* neither — while asking which blocks a note declares would put a second,
|
|
242
|
-
* subtly different answer to that question in a third place.
|
|
243
|
-
*
|
|
244
|
-
* **First claim wins on a collision.** Two notes naming one predecessor is a
|
|
245
|
-
* contradiction — an address has one successor — and it is reported as an error
|
|
246
|
-
* by `engine/content-lint.mjs`, where both notes are in hand and can both be
|
|
247
|
-
* named. Picking one here keeps this a map; it is not a resolution, and nothing
|
|
248
|
-
* rests on which one it picked.
|
|
249
|
-
*
|
|
250
|
-
* @param {string} contentBase - Root of the content tree.
|
|
251
|
-
* @param {object} opts
|
|
252
|
-
* @param {readonly string[]} [opts.skipDirectories] - The corpus scope. Stated
|
|
253
|
-
* by the caller, never defaulted — see {@link addressCorpus}.
|
|
254
|
-
* @param {readonly object[]} [opts.maps] - The document-subtype maps.
|
|
255
|
-
* @param {object} [opts.config] - The resolved build configuration.
|
|
256
|
-
* @param {readonly object[]} [opts.records] - Index records the caller already
|
|
257
|
-
* derived, shared with {@link noteFilesById} so one command reads one corpus.
|
|
258
|
-
* @param {object[]} [opts.problems] - Collects the notes the index cannot
|
|
259
|
-
* record, so one of them does not abort the diff before it reports.
|
|
260
|
-
* @returns {Map<string, {to: string, file: string, shortcode: string}>} Old
|
|
261
|
-
* address → the address the declaring note publishes at now, and that note.
|
|
262
|
-
*/
|
|
263
|
-
export function declaredPredecessors(
|
|
264
|
-
contentBase,
|
|
265
|
-
{ skipDirectories, maps = KNOWN_DOCUMENT_SUBTYPE_MAPS, config, records, problems } = {},
|
|
266
|
-
) {
|
|
267
|
-
const byOldAddress = new Map();
|
|
268
|
-
for (const record of addressCorpus(contentBase, {
|
|
269
|
-
skipDirectories,
|
|
270
|
-
config,
|
|
271
|
-
records,
|
|
272
|
-
problems,
|
|
273
|
-
})) {
|
|
274
|
-
// A documentation journal is a document this tree emits, not a note in
|
|
275
|
-
// it: it has no file and declares nothing.
|
|
276
|
-
if (!isNoteRecord(record)) continue;
|
|
277
|
-
// The record carries the note's frontmatter, so a declaration is read
|
|
278
|
-
// off it exactly as it was read off the parse.
|
|
279
|
-
const declared = renamedFrom(record);
|
|
280
|
-
if (!declared.length) continue;
|
|
281
|
-
const shortcode = typeof record.shortcode === "string" ? record.shortcode.trim() : "";
|
|
282
|
-
// A note with no address of its own has nowhere for a predecessor to
|
|
283
|
-
// have gone, so it declares a rename to nothing. Reported by the lint;
|
|
284
|
-
// silently skipped here rather than indexed as a rename to `type:`.
|
|
285
|
-
if (!shortcode) continue;
|
|
286
|
-
const absPath = noteFile(contentBase, record);
|
|
287
|
-
for (const map of maps) {
|
|
288
|
-
const { subType } = referencedSubtype(map, record.type, "Item");
|
|
289
|
-
if (!subType) continue;
|
|
290
|
-
const to = itemAddressKey(subType, shortcode);
|
|
291
|
-
for (const old of declared) {
|
|
292
|
-
const from = itemAddressKey(subType, old);
|
|
293
|
-
// A note naming its own current address declares nothing, and
|
|
294
|
-
// indexing it would make every such address look renamed to
|
|
295
|
-
// itself. The lint reports it.
|
|
296
|
-
if (from === to) continue;
|
|
297
|
-
if (!byOldAddress.has(from)) {
|
|
298
|
-
byOldAddress.set(from, { to, file: absPath, shortcode: old });
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return byOldAddress;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
127
|
/**
|
|
307
128
|
* Every address the baseline published that this build does not.
|
|
308
129
|
*
|
|
309
|
-
* An address that
|
|
310
|
-
*
|
|
311
|
-
*
|
|
130
|
+
* An address that arrived is not a finding. A departed address is a rename
|
|
131
|
+
* only when its document id appears at another current address; otherwise it
|
|
132
|
+
* is a withdrawal.
|
|
312
133
|
*
|
|
313
134
|
* @param {Map<string, object>} baseline - The released address space.
|
|
314
135
|
* @param {Map<string, object>} current - This build's address space.
|
|
315
136
|
* @param {object} opts
|
|
316
|
-
* @param {string} opts.baseline - What the baseline is, for the message
|
|
317
|
-
*
|
|
318
|
-
* @param {Map<string, {to: string, file: string}>} [opts.predecessors] - The
|
|
319
|
-
* declared renames, from {@link declaredPredecessors}. Omitted, the diff
|
|
320
|
-
* falls back to the id join alone and reports an unpinned rename as a
|
|
321
|
-
* withdrawal, which is what it did before.
|
|
322
|
-
* @returns {Array<object>} One finding per departed address, in address order
|
|
323
|
-
* so two runs read the same. `kind` is `"renamed"` (with `to`, and `declared`
|
|
324
|
-
* when it was the note's word rather than an id match) or `"withdrawn"`.
|
|
137
|
+
* @param {string} opts.baseline - What the baseline is, for the message.
|
|
138
|
+
* @returns {Array<object>} One finding per departed address, in address order.
|
|
325
139
|
*/
|
|
326
|
-
export function diffItemAddresses(baseline, current, { baseline: label
|
|
327
|
-
// A baseline that yields no address at all cannot produce a finding, so it
|
|
328
|
-
// reports a clean result for every possible input — the one failure a check
|
|
329
|
-
// like this can never catch, and the same one `foreign-manifests.mjs` exists
|
|
330
|
-
// to stop. It is a real state, not a hypothetical: `sohl-kethira-basic@0.5.3`
|
|
331
|
-
// shipped 307 items carrying no `system.shortcode` between them.
|
|
140
|
+
export function diffItemAddresses(baseline, current, { baseline: label }) {
|
|
332
141
|
if (!baseline.size) {
|
|
333
142
|
throw new Error(
|
|
334
143
|
`${label} publishes no addressable item — no document in its Item ` +
|
|
@@ -337,8 +146,6 @@ export function diffItemAddresses(baseline, current, { baseline: label, predeces
|
|
|
337
146
|
`so it is refused rather than passed`,
|
|
338
147
|
);
|
|
339
148
|
}
|
|
340
|
-
// Where each still-published document lives now. Built once: a rename is
|
|
341
|
-
// decided by identity, so this is the whole evidence base.
|
|
342
149
|
const currentById = new Map();
|
|
343
150
|
for (const [address, entry] of current) {
|
|
344
151
|
if (!currentById.has(entry.id)) currentById.set(entry.id, address);
|
|
@@ -347,28 +154,11 @@ export function diffItemAddresses(baseline, current, { baseline: label, predeces
|
|
|
347
154
|
const findings = [];
|
|
348
155
|
for (const [address, entry] of baseline) {
|
|
349
156
|
if (current.has(address)) continue;
|
|
350
|
-
|
|
351
|
-
// The id first, because it is the strongest answer available: a match
|
|
352
|
-
// is an identity, and a reader can check it in both artefacts.
|
|
353
|
-
const matched = currentById.get(entry.id);
|
|
354
|
-
// Then the author's word, and only where it checks out — the note that
|
|
355
|
-
// claims this address as a predecessor must itself be publishing now.
|
|
356
|
-
// A declaration pointing at an address this build does not publish
|
|
357
|
-
// describes a rename that did not survive to the packs, and naming it
|
|
358
|
-
// as the successor would send the reader somewhere nothing is.
|
|
359
|
-
const claim = matched ? undefined : predecessors?.get(address);
|
|
360
|
-
const declared = claim && current.has(claim.to) ? claim : undefined;
|
|
361
|
-
const to = matched ?? declared?.to;
|
|
362
|
-
|
|
157
|
+
const to = currentById.get(entry.id);
|
|
363
158
|
findings.push({
|
|
364
159
|
kind: to ? "renamed" : "withdrawn",
|
|
365
160
|
address,
|
|
366
161
|
...(to ? { to } : {}),
|
|
367
|
-
// Set only on a declared rename, so a reader of a finding can tell
|
|
368
|
-
// testimony from an identity match without re-deriving which it was.
|
|
369
|
-
...(matched ? {}
|
|
370
|
-
: declared ? { declared: true, noteFile: declared.file }
|
|
371
|
-
: {}),
|
|
372
162
|
id: entry.id,
|
|
373
163
|
name: entry.name,
|
|
374
164
|
shortcode: entry.shortcode,
|
|
@@ -381,7 +171,7 @@ export function diffItemAddresses(baseline, current, { baseline: label, predeces
|
|
|
381
171
|
}
|
|
382
172
|
|
|
383
173
|
/**
|
|
384
|
-
* Every content note in a tree, indexed by the document id it
|
|
174
|
+
* Every content note in a tree, indexed by the document id it compiles under.
|
|
385
175
|
*
|
|
386
176
|
* The address space is read from compiled output because that is what actually
|
|
387
177
|
* ships; the tree is read only to place a finding somewhere a reader can open
|
|
@@ -395,7 +185,7 @@ export function diffItemAddresses(baseline, current, { baseline: label, predeces
|
|
|
395
185
|
* @param {object} [opts.config] - The resolved build configuration, which the
|
|
396
186
|
* id is derived against. See {@link addressCorpus} for why that matters.
|
|
397
187
|
* @param {readonly object[]} [opts.records] - Index records the caller already
|
|
398
|
-
* derived
|
|
188
|
+
* derived by the caller.
|
|
399
189
|
* @param {object[]} [opts.problems] - Collects the notes the index cannot
|
|
400
190
|
* record, so one of them does not abort the diff before it reports.
|
|
401
191
|
* @returns {Map<string, string>} Document id → the note's absolute path.
|
|
@@ -426,35 +216,15 @@ export function noteFilesById(contentBase, { skipDirectories, config, records, p
|
|
|
426
216
|
/**
|
|
427
217
|
* Where to send the reader for one finding.
|
|
428
218
|
*
|
|
429
|
-
* A
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
* the baseline document, which is the only artefact left that records the
|
|
433
|
-
* address existing. When neither is readable the position is **dropped**, never
|
|
434
|
-
* defaulted to `1:1`.
|
|
435
|
-
*
|
|
436
|
-
* A **declared** rename knows its note without any lookup — the declaration is
|
|
437
|
-
* how it was found — and is reported at the `renamedFrom:` line rather than the
|
|
438
|
-
* `shortcode:` line, because that is the line the finding is about and the one
|
|
439
|
-
* the author deletes once the declaration has done its work.
|
|
219
|
+
* A matched document is located at its current `shortcode:` line. A withdrawal
|
|
220
|
+
* is located in the baseline artifact, the remaining evidence for its address.
|
|
221
|
+
* When neither source has a usable position, the location is dropped.
|
|
440
222
|
*
|
|
441
223
|
* @param {object} finding - One finding from {@link diffItemAddresses}.
|
|
442
224
|
* @param {Map<string, string>} noteFiles - From {@link noteFilesById}.
|
|
443
|
-
* @returns {{file?: string, line?: number, column?: number}}
|
|
444
|
-
* position fields for {@link formatDiagnostic}.
|
|
225
|
+
* @returns {{file?: string, line?: number, column?: number}} Position fields.
|
|
445
226
|
*/
|
|
446
227
|
export function locateAddressFinding(finding, noteFiles) {
|
|
447
|
-
if (finding.declared && finding.noteFile) {
|
|
448
|
-
try {
|
|
449
|
-
const raw = fs.readFileSync(finding.noteFile, "utf8");
|
|
450
|
-
return {
|
|
451
|
-
file: finding.noteFile,
|
|
452
|
-
...positionInFrontmatter(raw, "renamedFrom", finding.shortcode),
|
|
453
|
-
};
|
|
454
|
-
} catch {
|
|
455
|
-
return { file: finding.noteFile };
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
228
|
const note = noteFiles?.get(finding.id);
|
|
459
229
|
if (note) {
|
|
460
230
|
try {
|
|
@@ -479,36 +249,19 @@ export function locateAddressFinding(finding, noteFiles) {
|
|
|
479
249
|
/**
|
|
480
250
|
* What one finding says, without a locator or a severity.
|
|
481
251
|
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
* known — and says so, rather than leaving the reader to wonder whether one was
|
|
486
|
-
* looked for.
|
|
487
|
-
*
|
|
488
|
-
* **A declared rename says it is declared**. The two claims are not
|
|
489
|
-
* equally checkable: an id match is a fact in the artefacts, while a
|
|
490
|
-
* declaration is an author's word, and a reader deciding whether to trust the
|
|
491
|
-
* successor needs to know which one they have. Saying "the same document" of a
|
|
492
|
-
* declared rename would spend the id match's credibility on it.
|
|
493
|
-
*
|
|
494
|
-
* The consequence sentence is the same for both, because it is the same
|
|
495
|
-
* consequence: the old address stops resolving either way, and that is what
|
|
496
|
-
* the reader has to act on.
|
|
252
|
+
* A matched document names the address where it now appears. A withdrawal
|
|
253
|
+
* names no successor because the current build publishes no document with
|
|
254
|
+
* that identity.
|
|
497
255
|
*
|
|
498
256
|
* @param {object} finding - One finding from {@link diffItemAddresses}.
|
|
499
257
|
* @returns {string} The message.
|
|
500
258
|
*/
|
|
501
259
|
export function addressFindingMessage(finding) {
|
|
502
260
|
if (finding.kind === "renamed") {
|
|
503
|
-
const how =
|
|
504
|
-
finding.declared ?
|
|
505
|
-
`the note now published as ${finding.to} declares it was ` +
|
|
506
|
-
`renamed from ${finding.shortcode}`
|
|
507
|
-
: `the same document (${finding.id}) is now published as ${finding.to}`;
|
|
508
261
|
return (
|
|
509
262
|
`since ${finding.baseline}, ${finding.address} is no longer ` +
|
|
510
|
-
`published; ${
|
|
511
|
-
`breaks when it moves past ${finding.baseline}`
|
|
263
|
+
`published; the same document (${finding.id}) is now published as ${finding.to}. ` +
|
|
264
|
+
`Every package that resolves ${finding.address} breaks when it moves past ${finding.baseline}`
|
|
512
265
|
);
|
|
513
266
|
}
|
|
514
267
|
return (
|
package/engine/art-fields.mjs
CHANGED
|
@@ -250,16 +250,19 @@ export const BEING_DEFAULT_ART = Object.freeze({
|
|
|
250
250
|
});
|
|
251
251
|
|
|
252
252
|
/**
|
|
253
|
-
* The default art address a being's
|
|
253
|
+
* The default art address a being's subtype chooses, or `null`.
|
|
254
254
|
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* lookup rather than a precedence rule.
|
|
255
|
+
* An NPC and a character use person art; a creature uses creature art. The
|
|
256
|
+
* tag lookup preserves defaults for package trees that have not migrated.
|
|
258
257
|
*
|
|
259
258
|
* @param {object} fm - The note's frontmatter.
|
|
260
|
-
* @returns {string|null} The address, or `null`
|
|
259
|
+
* @returns {string|null} The address, or `null` when no subtype or legacy tag applies.
|
|
261
260
|
*/
|
|
262
261
|
export function beingDefaultArt(fm) {
|
|
262
|
+
if (fm?.subType === "npc" || fm?.subType === "character") {
|
|
263
|
+
return BEING_DEFAULT_ART.character;
|
|
264
|
+
}
|
|
265
|
+
if (fm?.subType === "creature") return BEING_DEFAULT_ART.creature;
|
|
263
266
|
for (const [tag, address] of Object.entries(BEING_DEFAULT_ART)) {
|
|
264
267
|
if (hasTag(fm, tag)) return address;
|
|
265
268
|
}
|
|
@@ -29,7 +29,10 @@ types:
|
|
|
29
29
|
- appearance.skin_color
|
|
30
30
|
- appearance.complexion
|
|
31
31
|
- appearance.extra_features
|
|
32
|
-
subTypes:
|
|
32
|
+
subTypes:
|
|
33
|
+
- npc
|
|
34
|
+
- character
|
|
35
|
+
- creature
|
|
33
36
|
homepage:
|
|
34
37
|
data: []
|
|
35
38
|
subTypes: []
|
|
@@ -676,9 +679,6 @@ vocabularies:
|
|
|
676
679
|
- link
|
|
677
680
|
- links
|
|
678
681
|
- list
|
|
679
|
-
beingKind:
|
|
680
|
-
- character
|
|
681
|
-
- creature
|
|
682
682
|
class:
|
|
683
683
|
- .full-width
|
|
684
684
|
size:
|
package/engine/content-index.mjs
CHANGED
|
@@ -696,14 +696,39 @@ export function collectContentIndex(
|
|
|
696
696
|
// `walkMarkdownTree` is a generator — a single-use one, exhausted the
|
|
697
697
|
// moment anything else iterates it first.
|
|
698
698
|
const notes = fs.existsSync(contentBase) ? [...walkMarkdownTree(contentBase, walkOpts)] : [];
|
|
699
|
+
const parsedNotes = [];
|
|
700
|
+
for (const note of notes) {
|
|
701
|
+
if (!note.parseError) {
|
|
702
|
+
parsedNotes.push(note);
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
const problem = {
|
|
706
|
+
file: note.absPath,
|
|
707
|
+
...(note.parseError.line === undefined ? {} : { line: note.parseError.line }),
|
|
708
|
+
...(note.parseError.column === undefined ? {} : { column: note.parseError.column }),
|
|
709
|
+
severity: "error",
|
|
710
|
+
message: note.parseError.message,
|
|
711
|
+
};
|
|
712
|
+
if (!problems) {
|
|
713
|
+
const error = new Error(problem.message);
|
|
714
|
+
error.file = problem.file;
|
|
715
|
+
error.position = {
|
|
716
|
+
...(problem.line === undefined ? {} : { line: problem.line }),
|
|
717
|
+
...(problem.column === undefined ? {} : { column: problem.column }),
|
|
718
|
+
};
|
|
719
|
+
error.keyPath = [];
|
|
720
|
+
throw error;
|
|
721
|
+
}
|
|
722
|
+
problems.push(problem);
|
|
723
|
+
}
|
|
699
724
|
|
|
700
725
|
// The package's declared present, read once from whichever `place` note in
|
|
701
726
|
// this same walk states one — the whole tree is already in memory, so no
|
|
702
727
|
// second read is needed to answer a question about all of it.
|
|
703
|
-
const present = presentAmongFrontmatters(
|
|
704
|
-
const dates = reckoningContext({ notes:
|
|
728
|
+
const present = presentAmongFrontmatters(parsedNotes.map((n) => n.frontmatter));
|
|
729
|
+
const dates = reckoningContext({ notes: parsedNotes.map((n) => n.frontmatter) });
|
|
705
730
|
|
|
706
|
-
for (const { frontmatter, body, bodyLine, absPath } of
|
|
731
|
+
for (const { frontmatter, body, bodyLine, absPath } of parsedNotes) {
|
|
707
732
|
const fm = frontmatter ?? {};
|
|
708
733
|
applyComputedBeingAge(fm, present, dates);
|
|
709
734
|
const relPath = path.relative(contentBase, absPath);
|