@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/engine/content-lint.mjs
CHANGED
|
@@ -15,43 +15,43 @@
|
|
|
15
15
|
* Linting a content tree's **addresses** — the rules every package's notes are
|
|
16
16
|
* authored against, wherever those notes live.
|
|
17
17
|
*
|
|
18
|
-
* These rules
|
|
19
|
-
* consequences and no upside
|
|
18
|
+
* These rules do not live in a consumer's `utils/`, which has two
|
|
19
|
+
* consequences and no upside. `thalorna` and `kethira` notes were checked
|
|
20
20
|
* by nothing at all, so the packages most likely to carry authoring mistakes
|
|
21
21
|
* were the ones nothing inspected. And one rule with two implementations can
|
|
22
22
|
* disagree without anything detecting it, which the canonical-separator
|
|
23
23
|
* handling already did once on each side.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
25
|
+
* Five rules, all about a note's identity:
|
|
26
26
|
*
|
|
27
|
-
* 1. **Shape** — a `shortcode` is strictly ASCII-alphanumeric. It is
|
|
28
|
-
* identity key referenced from saved world data, and it is half of the
|
|
27
|
+
* 1. **Shape** — a `shortcode` is strictly lowercase ASCII-alphanumeric. It is
|
|
28
|
+
* the identity key referenced from saved world data, and it is half of the
|
|
29
29
|
* `type-shortcode` address, whose parse depends on the separating hyphen
|
|
30
30
|
* being the only hyphen in the string.
|
|
31
31
|
* 2. **Uniqueness** — `(type, shortcode)` names one note.
|
|
32
32
|
* 3. **The package's own address** — exactly one note claims `/<package>/`,
|
|
33
|
-
* which is {@link checkHomepageCount}
|
|
33
|
+
* which is {@link checkHomepageCount}. It belongs here for the same
|
|
34
34
|
* reason the other two do: it is a statement about which note holds which
|
|
35
35
|
* address, it needs no `site:` configuration to decide, and a package with
|
|
36
36
|
* no front page is misconfigured whether or not anyone runs a site build.
|
|
37
37
|
* 4. **Vacated addresses** — a `renamedFrom:` entry names an address this note
|
|
38
|
-
*
|
|
38
|
+
* once held and nothing holds now. It is the same statement as
|
|
39
39
|
* rule 2 read backwards, and it needs the same whole-tree view: an entry can
|
|
40
40
|
* only be checked against every *other* note's address, and two notes
|
|
41
41
|
* claiming one predecessor is the uniqueness rule applied to the past.
|
|
42
|
+
* 5. **The vocabulary a package's kind leaves it** — a package compiling no
|
|
43
|
+
* Foundry documents publishes `doc` and `homepage` notes and nothing else.
|
|
44
|
+
* It is here rather than with the claim check in `note-claims.mjs` because
|
|
45
|
+
* that check runs at compile, and this is the case where no compile runs.
|
|
42
46
|
*
|
|
43
47
|
* **Nothing here writes.** A check reports and an author fixes.
|
|
44
48
|
*
|
|
45
|
-
* **
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* qualifier themselves. The
|
|
50
|
-
*
|
|
51
|
-
* field itself is retired now (#180), refused from `retired-fields.mjs`. Removing it was
|
|
52
|
-
* verified output-neutral beforehand: across 1,735 stripped notes,
|
|
53
|
-
* `package compile` produced byte-identical `build/packs-json` and the site
|
|
54
|
-
* build byte-identical `site/content`.
|
|
49
|
+
* **There is deliberately no third rule** requiring every note to repeat its
|
|
50
|
+
* own `type-shortcode` address in a top-level `aliases:` list. It would serve
|
|
51
|
+
* exactly one reader — **Obsidian**, so `[[type-shortcode]]` resolves in the
|
|
52
|
+
* editor — and nothing in the build reads it: both resolvers parse the hyphen
|
|
53
|
+
* qualifier themselves. The field is retired, refused from
|
|
54
|
+
* `retired-fields.mjs`.
|
|
55
55
|
*
|
|
56
56
|
* **What is deliberately absent.** Corpus reachability — "every Rules document
|
|
57
57
|
* is reachable from the book's root" — is a statement about what one package
|
|
@@ -65,25 +65,30 @@ import fs from "node:fs";
|
|
|
65
65
|
import path from "node:path";
|
|
66
66
|
|
|
67
67
|
import { ADDRESS_SEGMENT_PATTERN } from "./address-charset.mjs";
|
|
68
|
+
// The kind that compiles nothing, and the vocabulary that leaves a tree with.
|
|
69
|
+
import { DOCUMENTATION_KIND, compilesFoundryDocuments } from "../content-config.mjs";
|
|
70
|
+
import { DOCUMENTATION_NOTE_TYPES } from "./note-claims.mjs";
|
|
68
71
|
import { positionInFrontmatter } from "./diagnostics.mjs";
|
|
69
72
|
import { assertStatedScope } from "./helpers.mjs";
|
|
70
|
-
// The corpus, read from the one pass that derives it
|
|
73
|
+
// The corpus, read from the one pass that derives it.
|
|
71
74
|
import { authoredFrontmatter, indexRecordsFor, isNoteRecord, noteFile } from "./content-index.mjs";
|
|
72
75
|
import { checkHomepageCount, isHomepage } from "./homepage.mjs";
|
|
73
76
|
import { declaresRenamedFrom, renamedFrom, renamedFromEntries } from "./note-renames.mjs";
|
|
74
77
|
|
|
75
78
|
/**
|
|
76
|
-
* The shape every `shortcode` must match: ASCII letters and digits
|
|
79
|
+
* The shape every `shortcode` must match: lowercase ASCII letters and digits
|
|
80
|
+
* only.
|
|
77
81
|
*
|
|
78
82
|
* This is {@link ADDRESS_SEGMENT_PATTERN}, not a second copy of it. A shortcode
|
|
79
83
|
* is the last segment of a canonical address, and the rule it is held to is the
|
|
80
84
|
* rule *every* segment is held to — so the two are one constant rather than two
|
|
81
|
-
* free to drift apart
|
|
85
|
+
* free to drift apart. The name survives because this is where the rule
|
|
82
86
|
* is applied to a note.
|
|
83
87
|
*
|
|
84
|
-
* Case is
|
|
85
|
-
*
|
|
86
|
-
*
|
|
88
|
+
* Case is held to that rule with no exception: two shortcodes differing only
|
|
89
|
+
* in case are two names nobody can tell apart, and `canonicalKey` lowercases
|
|
90
|
+
* every address it builds regardless, so a mixed-case shortcode addresses the
|
|
91
|
+
* same document as its lowercase spelling.
|
|
87
92
|
*
|
|
88
93
|
* A consuming system's *runtime* keeps its own copy of this pattern — it cannot
|
|
89
94
|
* import a build-time dependency into shipped code — and is expected to pin the
|
|
@@ -112,7 +117,7 @@ export function isValidShortcode(value) {
|
|
|
112
117
|
* `Templates/`, a `README`, a repository's own `CLAUDE.md` — has no type, is
|
|
113
118
|
* neither addressed nor addressable, and would fail rules it can never satisfy.
|
|
114
119
|
*
|
|
115
|
-
* **Read from the index, not from a walk of this pass's own
|
|
120
|
+
* **Read from the index, not from a walk of this pass's own**. The
|
|
116
121
|
* `lint` command already derives the index — its link check and its `sql`
|
|
117
122
|
* tables are built from it — and then walked the tree a second time to get
|
|
118
123
|
* here, so one command held two answers to "which files are the corpus?" and
|
|
@@ -162,7 +167,7 @@ function collectNotes(contentBase, { skipDirectories, config, records, problems
|
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
/**
|
|
165
|
-
* What one note's `renamedFrom:` says, checked against itself
|
|
170
|
+
* What one note's `renamedFrom:` says, checked against itself.
|
|
166
171
|
*
|
|
167
172
|
* The entries a note can be wrong about on its own: a value that is not a
|
|
168
173
|
* shortcode, one naming the address the note holds *now*, one written twice.
|
|
@@ -229,8 +234,9 @@ function checkRenamedFrom({ fm, file }, raw) {
|
|
|
229
234
|
...at(value),
|
|
230
235
|
severity: "error",
|
|
231
236
|
message:
|
|
232
|
-
`\`renamedFrom: ${value}\` is not strictly alphanumeric
|
|
233
|
-
`
|
|
237
|
+
`\`renamedFrom: ${value}\` is not strictly alphanumeric — ` +
|
|
238
|
+
`lowercase letters and digits only (${ADDRESS_SEGMENT_PATTERN.source}) — ` +
|
|
239
|
+
`so it is not an address this package ever published — a ` +
|
|
234
240
|
`shortcode is held to one charset whether it is current or past`,
|
|
235
241
|
});
|
|
236
242
|
continue;
|
|
@@ -276,7 +282,7 @@ function checkRenamedFrom({ fm, file }, raw) {
|
|
|
276
282
|
* @param {object} [opts.config] - The resolved build configuration, which the
|
|
277
283
|
* corpus is derived against.
|
|
278
284
|
* @param {readonly object[]} [opts.records] - Index records the caller already
|
|
279
|
-
* derived, so a command reads one corpus
|
|
285
|
+
* derived, so a command reads one corpus.
|
|
280
286
|
* @param {object[]} [opts.problems] - Collects the notes the index cannot
|
|
281
287
|
* record, instead of letting one of them silence the lint.
|
|
282
288
|
* @returns {{findings: Array<{file: string, line?: number, column?: number,
|
|
@@ -289,6 +295,12 @@ export function lintContentTree(
|
|
|
289
295
|
) {
|
|
290
296
|
const findings = [];
|
|
291
297
|
const notes = collectNotes(contentBase, { skipDirectories, config, records, problems });
|
|
298
|
+
// Whether this package's note vocabulary is the narrowed one. Asked of the
|
|
299
|
+
// configuration once rather than per note, and defaulted to the wide
|
|
300
|
+
// vocabulary when a caller supplies none — an unconfigured lint holds a
|
|
301
|
+
// tree to the rules every package shares.
|
|
302
|
+
const narrowed =
|
|
303
|
+
Boolean(config) && !compilesFoundryDocuments(/** @type {{packageKind: string}} */ (config));
|
|
292
304
|
|
|
293
305
|
/** @type {Map<string, Array<{file: string, absPath: string}>>} */
|
|
294
306
|
const byKey = new Map();
|
|
@@ -303,6 +315,23 @@ export function lintContentTree(
|
|
|
303
315
|
// tree costs one pass rather than two.
|
|
304
316
|
const raw = () => fs.readFileSync(absPath, "utf8");
|
|
305
317
|
|
|
318
|
+
// Rule 5, and it is the whole of the check for a package that compiles
|
|
319
|
+
// nothing: no pass downstream would report the note, because the pass
|
|
320
|
+
// that reports an unclaimed type is a compile pass and none runs.
|
|
321
|
+
const type = typeof fm.type === "string" ? fm.type.trim() : "";
|
|
322
|
+
if (narrowed && type && !DOCUMENTATION_NOTE_TYPES.has(type)) {
|
|
323
|
+
findings.push({
|
|
324
|
+
file,
|
|
325
|
+
...positionInFrontmatter(raw(), "type", type),
|
|
326
|
+
severity: "error",
|
|
327
|
+
message:
|
|
328
|
+
`\`type: ${type}\` compiles to a Foundry document, and a ` +
|
|
329
|
+
`\`${DOCUMENTATION_KIND}\` package compiles none — so the ` +
|
|
330
|
+
`note has no destination. Its vocabulary is ` +
|
|
331
|
+
`${[...DOCUMENTATION_NOTE_TYPES].map((t) => `\`${t}\``).join(" and ")}`,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
306
335
|
// Before the keyless `continue` below, because a note declaring a
|
|
307
336
|
// rename while carrying no address of its own is exactly one of the
|
|
308
337
|
// things this reports — and reaching it after the skip would mean it
|
|
@@ -336,7 +365,8 @@ export function lintContentTree(
|
|
|
336
365
|
...positionInFrontmatter(raw(), "shortcode", String(shortcode)),
|
|
337
366
|
severity: "error",
|
|
338
367
|
message:
|
|
339
|
-
`shortcode "${shortcode}" is not strictly alphanumeric
|
|
368
|
+
`shortcode "${shortcode}" is not strictly alphanumeric — ` +
|
|
369
|
+
`lowercase letters and digits only (${ADDRESS_SEGMENT_PATTERN.source}); it ` +
|
|
340
370
|
`is the identity key and half of the ` +
|
|
341
371
|
`"${fm.type}-${shortcode}" address, whose parse needs the ` +
|
|
342
372
|
`separator to be the only hyphen`,
|
|
@@ -348,17 +378,17 @@ export function lintContentTree(
|
|
|
348
378
|
// what a tree that failed to check out produces — so the lint would go
|
|
349
379
|
// green on the one state it most needs to catch.
|
|
350
380
|
//
|
|
351
|
-
// The state that catches is an **empty walk**, not an empty key set
|
|
381
|
+
// The state that catches is an **empty walk**, not an empty key set.
|
|
352
382
|
// Notes may be keyless: a folder document carries no `shortcode`, and a
|
|
353
383
|
// tree of them is populated, correct, and unkeyed. Reporting that as a
|
|
354
384
|
// missing checkout trains its author to stop reading the output — the one
|
|
355
385
|
// thing this guard needs them to do. A tree holding notes is therefore a
|
|
356
386
|
// tree; only a tree holding none is the absent one.
|
|
357
387
|
//
|
|
358
|
-
// The homepage
|
|
388
|
+
// The homepage is not the headline example, though it was once addressed
|
|
359
389
|
// by the package rather than by a slug — so a `publish.site: homepage`
|
|
360
390
|
// package had a tree with exactly one note and no key at all. It carries an
|
|
361
|
-
// address like every other note now
|
|
391
|
+
// address like every other note now; the guard is unchanged, because
|
|
362
392
|
// what it reads was never the key count.
|
|
363
393
|
if (notes.length === 0) {
|
|
364
394
|
findings.push({
|
|
@@ -401,8 +431,8 @@ export function lintContentTree(
|
|
|
401
431
|
}
|
|
402
432
|
}
|
|
403
433
|
|
|
404
|
-
// The two questions about a declared rename that need the whole tree
|
|
405
|
-
//
|
|
434
|
+
// The two questions about a declared rename that need the whole tree.
|
|
435
|
+
// Both are the uniqueness rule above, applied to the past: an
|
|
406
436
|
// address has one holder, so it has one successor and it cannot be both
|
|
407
437
|
// vacated and occupied.
|
|
408
438
|
for (const [claim, claimants] of claimedPredecessors) {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* ships them — read from the repository's `package-build.config.yaml`.
|
|
17
17
|
*
|
|
18
18
|
* Both values are **derived**, not declared: `config.mjs` is the single place
|
|
19
|
-
* the configuration is resolved
|
|
19
|
+
* the configuration is resolved. This module survives as the import
|
|
20
20
|
* path the link resolver and the compilers have always used, so that the values
|
|
21
21
|
* can still be mocked in one place and so no consumer has to learn a new
|
|
22
22
|
* spelling for them.
|
|
@@ -35,13 +35,11 @@ import { loadPackConfig } from "./pack-config.mjs";
|
|
|
35
35
|
* repository's identity in the address space, not a switch — and never dead
|
|
36
36
|
* configuration, whatever else changes.
|
|
37
37
|
*
|
|
38
|
-
* It
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* retired and declaring it now fails the build; this value stays, here, where
|
|
44
|
-
* it is declared once.
|
|
38
|
+
* It is not a **selector**. Every content tree is single-package — each is
|
|
39
|
+
* single-sourced in the repository that ships it — so a `package:` field on a
|
|
40
|
+
* note would restate this constant once per note, and a value matching nothing
|
|
41
|
+
* would filter the whole tree out in silence. That field is refused, and this
|
|
42
|
+
* value stays here, declared once.
|
|
45
43
|
*
|
|
46
44
|
* Stable across compilation targets. If this content were ever compiled for a
|
|
47
45
|
* second game system, it would still be published as `sohl` — only the Foundry
|
|
@@ -65,7 +63,7 @@ export function contentPackage() {
|
|
|
65
63
|
* here: a note is published under `sohl` and its documents are addressed as
|
|
66
64
|
* `Compendium.sohl.<pack>.<Type>.<id>`. In `sohl-thalorna` the two differ
|
|
67
65
|
* (`thalorna` vs `sohl-thalorna`), which is why they are separate values rather
|
|
68
|
-
* than one — treating them as interchangeable is
|
|
66
|
+
* than one — treating them as interchangeable is the mistake.
|
|
69
67
|
*
|
|
70
68
|
* Configured rather than read from the manifest so the link resolver stays
|
|
71
69
|
* filesystem-free and unit-testable. `assertPackageIdMatchesManifestFile` in
|
package/engine/content-slug.mjs
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* {@link slugify} reduces a piece of prose — a heading, a document name — to a
|
|
18
18
|
* URL-safe token. It is **not** how a page is addressed: a note's URL is its
|
|
19
19
|
* address, `type-shortcode`, derived in `engine/content-address.mjs` and
|
|
20
|
-
* touching no display string at all
|
|
20
|
+
* touching no display string at all.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
22
|
+
* It is not the other way round: a `contentSlug` here that
|
|
23
23
|
* derived a page's URL from `name.full`, abbreviating it through a table of 200
|
|
24
24
|
* words so the result stayed short, and a `findSlugCollisions` to catch the two
|
|
25
25
|
* notes that would then claim one URL. Its own header justified the readability
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
|
|
60
60
|
// The fence syntax is stated once, in `code-fences.mjs`, so the scanner that
|
|
61
61
|
// *skips* code and the one that expands a `dataview` block cannot disagree
|
|
62
|
-
// about where a block begins and ends
|
|
62
|
+
// about where a block begins and ends.
|
|
63
63
|
import { FENCE_LINE } from "./code-fences.mjs";
|
|
64
64
|
|
|
65
65
|
/** What a cell shows when its expression resolves to nothing. */
|
|
@@ -1059,17 +1059,32 @@ export function renderContentTable(spec, rows, linkable, self) {
|
|
|
1059
1059
|
/* ------------------------------------------------------------------------ */
|
|
1060
1060
|
|
|
1061
1061
|
/**
|
|
1062
|
-
*
|
|
1062
|
+
* The `WHERE` clause of a query, as authored, for a message that has to name
|
|
1063
|
+
* what matched nothing.
|
|
1064
|
+
*
|
|
1065
|
+
* The parsed spec holds an expression tree, and rendering that back to text
|
|
1066
|
+
* would be a second dialect of the query language to keep in step. The authored
|
|
1067
|
+
* line is what the author wrote and what they will edit, so it is what the
|
|
1068
|
+
* finding quotes.
|
|
1069
|
+
*
|
|
1070
|
+
* @param {string} query - The fence's contents.
|
|
1071
|
+
* @returns {string} The clause, or "" when the query has none.
|
|
1072
|
+
*/
|
|
1073
|
+
function whereText(query) {
|
|
1074
|
+
const match = /^\s*WHERE\s+(.+?)\s*$/im.exec(String(query ?? ""));
|
|
1075
|
+
return match ? `\`${match[1]}\`` : "";
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Expand every fenced `dataview` and `sql` block in a markdown body.
|
|
1063
1080
|
*
|
|
1064
1081
|
* A block that cannot be honoured — malformed or unsupported — is left in the
|
|
1065
1082
|
* body verbatim and reported in `errors`, so the failure is visible in the
|
|
1066
1083
|
* output as well as on the console. Every other code fence, and every code
|
|
1067
1084
|
* span, is left alone (that is how the syntax is documented).
|
|
1068
1085
|
*
|
|
1069
|
-
* A query that
|
|
1070
|
-
*
|
|
1071
|
-
* a category with no content yet is a normal state of the corpus rather than a
|
|
1072
|
-
* broken build.
|
|
1086
|
+
* A query that selects **no** note is an error unless the fence says
|
|
1087
|
+
* `allow-empty`, which states that an empty table is the intended result.
|
|
1073
1088
|
*
|
|
1074
1089
|
* @param {string} markdown - The note body, frontmatter already stripped.
|
|
1075
1090
|
* @param {object} ctx
|
|
@@ -1080,30 +1095,20 @@ export function renderContentTable(spec, rows, linkable, self) {
|
|
|
1080
1095
|
* @param {string} [ctx.source] - The note being expanded, for error reports.
|
|
1081
1096
|
* @param {ContentTableDoc} [ctx.self] - The note being expanded, as a searchable
|
|
1082
1097
|
* doc: what a query's `this` reads.
|
|
1098
|
+
* @param {object[]} [ctx.sqlTables] - This note's prepared `sql` results, in
|
|
1099
|
+
* document order, from
|
|
1100
|
+
* {@link module:engine/sql-tables.prepareSqlTables}. An `sql` directive with
|
|
1101
|
+
* no prepared result is an error: nothing here runs a query.
|
|
1083
1102
|
* @returns {{markdown: string, errors: Array<{source: string, directive: string,
|
|
1084
|
-
* reason: string, line: number
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1103
|
+
* reason: string, line: number, column?: number}>,
|
|
1104
|
+
* warnings: Array<{source: string, line: number, column: number,
|
|
1105
|
+
* reason: string}>, lineMap: Array<{line: number, generated: boolean}>}}
|
|
1106
|
+
* `lineMap` is parallel to the emitted lines and says which authored line
|
|
1107
|
+
* each came from, so a diagnostic about the expanded body can name an
|
|
1108
|
+
* authored position. An `errors` entry carries the 0-based line of the
|
|
1109
|
+
* directive that failed, for the same reason. `warnings` holds one entry per
|
|
1110
|
+
* `dataview` directive the body still authors.
|
|
1089
1111
|
*/
|
|
1090
|
-
/**
|
|
1091
|
-
* The `WHERE` clause of a query, as authored, for a message that has to name
|
|
1092
|
-
* what matched nothing.
|
|
1093
|
-
*
|
|
1094
|
-
* The parsed spec holds an expression tree, and rendering that back to text
|
|
1095
|
-
* would be a second dialect of the query language to keep in step. The authored
|
|
1096
|
-
* line is what the author wrote and what they will edit, so it is what the
|
|
1097
|
-
* finding quotes.
|
|
1098
|
-
*
|
|
1099
|
-
* @param {string} query - The fence's contents.
|
|
1100
|
-
* @returns {string} The clause, or "" when the query has none.
|
|
1101
|
-
*/
|
|
1102
|
-
function whereText(query) {
|
|
1103
|
-
const match = /^\s*WHERE\s+(.+?)\s*$/im.exec(String(query ?? ""));
|
|
1104
|
-
return match ? `\`${match[1]}\`` : "";
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
1112
|
export function expandContentTables(
|
|
1108
1113
|
markdown,
|
|
1109
1114
|
{
|
|
@@ -1121,12 +1126,12 @@ export function expandContentTables(
|
|
|
1121
1126
|
let sqlOrdinal = 0;
|
|
1122
1127
|
// One entry per `dataview` directive still authored, so a caller can say how
|
|
1123
1128
|
// much of the corpus is still on the retiring language without this pass
|
|
1124
|
-
// failing a build over it
|
|
1129
|
+
// failing a build over it.
|
|
1125
1130
|
const warnings = [];
|
|
1126
1131
|
const lines = String(markdown ?? "").split("\n");
|
|
1127
1132
|
const out = [];
|
|
1128
1133
|
// Which authored line each emitted line came from, so a later pass can
|
|
1129
|
-
// report an authored position for text this one moved
|
|
1134
|
+
// report an authored position for text this one moved. A generated
|
|
1130
1135
|
// row has no authored line of its own, so it is blamed on the directive
|
|
1131
1136
|
// that produced it — the thing an author can actually edit — and marked,
|
|
1132
1137
|
// because its columns mean nothing.
|
|
@@ -1203,8 +1208,7 @@ export function expandContentTables(
|
|
|
1203
1208
|
line: i,
|
|
1204
1209
|
column: indent.length + 1,
|
|
1205
1210
|
reason:
|
|
1206
|
-
"`dataview` content tables are being replaced by `sql` over the " +
|
|
1207
|
-
"content index (#246)",
|
|
1211
|
+
"`dataview` content tables are being replaced by `sql` over the " + "content index",
|
|
1208
1212
|
});
|
|
1209
1213
|
// `dataview allow-empty` says a table selecting nothing is the intended
|
|
1210
1214
|
// state. Spelled on the fence rather than in the query, because it is a
|
|
@@ -1219,7 +1223,7 @@ export function expandContentTables(
|
|
|
1219
1223
|
// a header with nothing under it makes that indistinguishable from
|
|
1220
1224
|
// a category that is legitimately empty. Eight tables in `sohl`'s
|
|
1221
1225
|
// `Rules/Bestiary.md` published that way for months after the
|
|
1222
|
-
// `creature` → `being` rename, and no build said a word
|
|
1226
|
+
// `creature` → `being` rename, and no build said a word.
|
|
1223
1227
|
if (rows.length === 0 && !allowEmpty) {
|
|
1224
1228
|
errors.push({
|
|
1225
1229
|
source,
|
|
@@ -1239,7 +1243,7 @@ export function expandContentTables(
|
|
|
1239
1243
|
directive: block.join("\n"),
|
|
1240
1244
|
reason: err.message,
|
|
1241
1245
|
// The fence's own line, so a table error is as locatable as
|
|
1242
|
-
// everything else the build reports
|
|
1246
|
+
// everything else the build reports.
|
|
1243
1247
|
line: i,
|
|
1244
1248
|
});
|
|
1245
1249
|
block.forEach((text, k) => emit(text, i + k));
|
package/engine/content-tree.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* notices — the LevelDB packs are written, the deploy mirrors them, and the
|
|
21
21
|
* defect surfaces as "my compendiums are blank" long after the build went green.
|
|
22
22
|
*
|
|
23
|
-
* `assets/content/` is this repository's own source
|
|
23
|
+
* `assets/content/` is this repository's own source, but an empty or
|
|
24
24
|
* partial tree is still possible — a bad checkout, or a sparse clone. This
|
|
25
25
|
* counts what is actually there so the build can refuse.
|
|
26
26
|
*/
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `system` keys a note may **never** author, because the *compiler* writes them
|
|
16
|
+
* from the note itself.
|
|
17
|
+
*
|
|
18
|
+
* The third of three refusals that read alike and state different facts:
|
|
19
|
+
*
|
|
20
|
+
* | module | refuses a field | because |
|
|
21
|
+
* | --- | --- | --- |
|
|
22
|
+
* | `retired-fields.mjs` | a note may no longer declare | it has been withdrawn |
|
|
23
|
+
* | `runtime-only-fields.mjs` | a note never could | the *document* writes it in play |
|
|
24
|
+
* | this one | a note never could | the *compiler* writes it, from the note |
|
|
25
|
+
*
|
|
26
|
+
* The distinction between the last two is where the value comes from, and it
|
|
27
|
+
* decides what an author is told. A runtime-only field has no compile-time
|
|
28
|
+
* value at all, so the fix is to delete the key. A derived one **does** have a
|
|
29
|
+
* compile-time value — it is already being written, from somewhere else in the
|
|
30
|
+
* same note — so the fix is to move the content to wherever the compiler reads
|
|
31
|
+
* it from, and the message has to say where that is.
|
|
32
|
+
*
|
|
33
|
+
* ## Why an authored value cannot be the source of anything
|
|
34
|
+
*
|
|
35
|
+
* The compiler writes the key unconditionally. An authored one is therefore at
|
|
36
|
+
* best redundant, and at worst the wrong type in a shipped document: SoHL's
|
|
37
|
+
* `docHtml` holds a `@UUID` pointing at the JournalEntry a note's prose
|
|
38
|
+
* compiled into, so a note writing prose there ships a string where every
|
|
39
|
+
* reader expects a pointer, with nothing to report it.
|
|
40
|
+
*
|
|
41
|
+
* Either way the text is not reaching a reader the way its author intended. It
|
|
42
|
+
* was written as a description, and the field it was written into is not where
|
|
43
|
+
* descriptions live — the note's own body is.
|
|
44
|
+
*
|
|
45
|
+
* ## Declared by the compiler, not named here
|
|
46
|
+
*
|
|
47
|
+
* A derived key is a fact about a *pass*, not about a schema: it exists because
|
|
48
|
+
* that compiler chose to write it. So the list is a static on the compiler and
|
|
49
|
+
* this module knows no key names, which is what lets a second system declare
|
|
50
|
+
* its own — HM3 derives `description` from an anchored prose section exactly as
|
|
51
|
+
* SoHL derives `docHtml` from the body.
|
|
52
|
+
*
|
|
53
|
+
* @module
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import { getFrontmatter } from "./frontmatter.mjs";
|
|
57
|
+
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
58
|
+
import { SYSTEM_DATA_KEY, systemData } from "./system-block.mjs";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What a note authoring a derived key is told, in one place.
|
|
62
|
+
*
|
|
63
|
+
* Shared by every caller that can meet one, so an author meets the same
|
|
64
|
+
* sentence whichever runs first. It names the correction rather than only the
|
|
65
|
+
* fault, because "delete this" is not the fix here: the text is wanted, and
|
|
66
|
+
* there is somewhere it belongs.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} key - The **whole key the note wrote**, from the region it
|
|
69
|
+
* sits in down to the field: `sohl.system.docHtml`. Composed by the caller,
|
|
70
|
+
* because only it knows where it found the value.
|
|
71
|
+
* @param {string} from - Where the compiler reads the value from, completing
|
|
72
|
+
* "the compiler writes it from …". The declaration carries it.
|
|
73
|
+
* @param {string} [file] - The note's path, named in the message. Omit it where
|
|
74
|
+
* the caller emits through a diagnostic, whose locator already starts the
|
|
75
|
+
* line — repeating it prints the path twice.
|
|
76
|
+
* @returns {string} The message, unpunctuated at the end as a finding is.
|
|
77
|
+
*/
|
|
78
|
+
export function derivedMessage(key, from, file) {
|
|
79
|
+
return (
|
|
80
|
+
`\`${key}:\` is written by the compiler, not authored` +
|
|
81
|
+
(file ? ` — ${file}` : "") +
|
|
82
|
+
`. It is derived from ${from}, and the compiler writes it ` +
|
|
83
|
+
`unconditionally, so an authored value is overwritten or ships the ` +
|
|
84
|
+
`wrong type. Move the text to ${from} and delete the key`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The derived keys a note actually writes, in declaration order.
|
|
90
|
+
*
|
|
91
|
+
* **Presence is the whole test**, as it is for a retired or a runtime-only
|
|
92
|
+
* field: an authored empty string is as much a claim on the key as prose is,
|
|
93
|
+
* and it is the same belief the message exists to correct. So the question is
|
|
94
|
+
* whether the path resolves to anything at all, never whether the value is a
|
|
95
|
+
* usable one.
|
|
96
|
+
*
|
|
97
|
+
* @param {object|null|undefined} fm - Parsed frontmatter.
|
|
98
|
+
* @param {readonly {key: string, from: string}[]} [keys] - What the compiler
|
|
99
|
+
* derives. A pass that derives nothing passes.
|
|
100
|
+
* @param {object} options - Options.
|
|
101
|
+
* @param {string} options.block - The system block to look in.
|
|
102
|
+
* @returns {{key: string, from: string}[]} The offending declarations.
|
|
103
|
+
*/
|
|
104
|
+
export function authoredDerivedKeys(fm, keys, { block } = {}) {
|
|
105
|
+
if (!fm || typeof fm !== "object" || !block) return [];
|
|
106
|
+
return derivedIn(systemData(fm, block), keys);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The same question asked of a `system` block directly.
|
|
111
|
+
*
|
|
112
|
+
* A note's own block is reached through {@link authoredDerivedKeys}, but it is
|
|
113
|
+
* not the only place an author writes one: an actor note's `items:` entries
|
|
114
|
+
* carry a `system:` overlay that is deep-merged onto the template verbatim,
|
|
115
|
+
* with no field declaration in the path at all. That overlay is a `system`
|
|
116
|
+
* block by every meaning except where it sits, and a `docHtml` written there
|
|
117
|
+
* ships exactly as one written on the item's own note.
|
|
118
|
+
*
|
|
119
|
+
* @param {Record<string, unknown>|null|undefined} data - The authored `system`
|
|
120
|
+
* data.
|
|
121
|
+
* @param {readonly {key: string, from: string}[]} [keys] - What the compiler
|
|
122
|
+
* derives.
|
|
123
|
+
* @returns {{key: string, from: string}[]} The offending declarations.
|
|
124
|
+
*/
|
|
125
|
+
export function derivedIn(data, keys) {
|
|
126
|
+
if (!data || typeof data !== "object") return [];
|
|
127
|
+
return (keys ?? []).filter(
|
|
128
|
+
(entry) =>
|
|
129
|
+
typeof entry?.key === "string" &&
|
|
130
|
+
entry.key !== "" &&
|
|
131
|
+
getFrontmatter(data, entry.key, undefined) !== undefined,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Refuse a note that authors any key its compiler derives.
|
|
137
|
+
*
|
|
138
|
+
* Refused rather than reported, for the reason its two siblings are: the note
|
|
139
|
+
* is not compiled, so nothing it would have emitted reaches a pack, and each
|
|
140
|
+
* caller counts the refused note and emits a located diagnostic — a refusal is
|
|
141
|
+
* never a silent skip.
|
|
142
|
+
*
|
|
143
|
+
* The **first** offending key is thrown on. A note authoring two of them is one
|
|
144
|
+
* mistake with one fix, and the build stops on this note either way.
|
|
145
|
+
*
|
|
146
|
+
* @param {object|null|undefined} fm - Parsed frontmatter, or nothing when it
|
|
147
|
+
* could not be parsed.
|
|
148
|
+
* @param {readonly {key: string, from: string}[]} [keys] - What the compiler
|
|
149
|
+
* derives.
|
|
150
|
+
* @param {object} options - Options.
|
|
151
|
+
* @param {string} options.block - The system block to look in.
|
|
152
|
+
* @param {string} [options.file] - The note's path, named in the message. Omit
|
|
153
|
+
* it where the caller emits through a diagnostic, which puts the locator at
|
|
154
|
+
* the start of the line already.
|
|
155
|
+
* @param {string} [options.absPath] - The note's file on disk, read only on the
|
|
156
|
+
* failing path to locate the offending line and column. The position rides on
|
|
157
|
+
* the thrown error as `position`, for a caller that emits a diagnostic.
|
|
158
|
+
* @returns {void}
|
|
159
|
+
* @throws {Error} When the note authors one.
|
|
160
|
+
*/
|
|
161
|
+
export function assertNoDerivedFields(fm, keys, { block, file, absPath } = {}) {
|
|
162
|
+
const [entry] = authoredDerivedKeys(fm, keys, { block });
|
|
163
|
+
if (!entry) return;
|
|
164
|
+
|
|
165
|
+
const wrote = `${block}.${SYSTEM_DATA_KEY}.${entry.key}`;
|
|
166
|
+
const err = new Error(`${derivedMessage(wrote, entry.from, file)}.`);
|
|
167
|
+
// The **leaf** of the key, which is how the note writes it: a locator
|
|
168
|
+
// handed a dotted path would find nothing. Deliberately not anchored at
|
|
169
|
+
// column 1 — the key lives two levels in, under `<block>.system`.
|
|
170
|
+
const leaf = entry.key.split(".").pop();
|
|
171
|
+
const position = locateFrontmatterKey(absPath, /** @type {string} */ (leaf));
|
|
172
|
+
if (position) err.position = position;
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
package/engine/diagnostics.mjs
CHANGED
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
* in "The Capital Nome"` — has told the author what is wrong and not where it
|
|
19
19
|
* is. A name is not an address: finding it means searching the tree for a note
|
|
20
20
|
* whose `name.full` matches and then searching that file for the link, and
|
|
21
|
-
* four identical warnings on one note are indistinguishable from one another
|
|
22
|
-
* (#17).
|
|
21
|
+
* four identical warnings on one note are indistinguishable from one another.
|
|
23
22
|
*
|
|
24
23
|
* So every diagnostic about a note is emitted in the form every C-family
|
|
25
24
|
* compiler, `tsc` and ESLint already use:
|
|
@@ -185,7 +184,7 @@ export function positionInBody(body, offset, { bodyLine = 1, bodyColumn = 1, lin
|
|
|
185
184
|
* same name answers when no top-level one is present — which is usually what a
|
|
186
185
|
* reader wants, the key being unique in nearly every note. Pass `topLevel` where
|
|
187
186
|
* it is not: `aliases` is both a retired top-level field and a **permitted**
|
|
188
|
-
* `name.aliases
|
|
187
|
+
* `name.aliases`, and a finding about the first must never open on the
|
|
189
188
|
* second, which would tell an author to delete a field they are allowed to
|
|
190
189
|
* write.
|
|
191
190
|
*
|
|
@@ -387,7 +386,7 @@ export function yamlKeyPath(field) {
|
|
|
387
386
|
* the same name is legal in two regions — `data.weight` and a top-level
|
|
388
387
|
* `weight` are different keys, and a search finds whichever comes first.
|
|
389
388
|
*
|
|
390
|
-
* So this is the located form for the closed regions
|
|
389
|
+
* So this is the located form for the closed regions. It reuses
|
|
391
390
|
* {@link positionOfYamlPath} rather than repeating its parse, which is what
|
|
392
391
|
* keeps a finding about `data.weight` pointing at `data.weight`.
|
|
393
392
|
*
|