@heroiclands/package-build 22.17.0 → 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 +23 -0
- package/bin/content-commands.mjs +114 -37
- package/config.mjs +54 -3
- package/docs/api.md +4 -14
- package/docs/commands.md +89 -52
- package/docs/configuration.md +49 -22
- 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/readability-lint.mjs +196 -18
- package/engine/subtype-registry.mjs +2 -6
- package/engine/systems.mjs +11 -13
- package/package.json +5 -1
- package/types/config.d.mts +14 -2
- 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/readability-lint.d.mts +11 -5
- 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
|
@@ -8,11 +8,18 @@ import path from "node:path";
|
|
|
8
8
|
import { unified } from "unified";
|
|
9
9
|
import remarkParse from "remark-parse";
|
|
10
10
|
import remarkFrontmatter from "remark-frontmatter";
|
|
11
|
+
import remarkGfm from "remark-gfm";
|
|
11
12
|
import remarkRetext from "remark-retext";
|
|
12
13
|
import retextEnglish from "retext-english";
|
|
13
14
|
import retextReadability from "retext-readability";
|
|
14
15
|
import retextSimplify from "retext-simplify";
|
|
15
16
|
import retextStringify from "retext-stringify";
|
|
17
|
+
import { flesch } from "flesch";
|
|
18
|
+
import { syllable } from "syllable";
|
|
19
|
+
import { daleChall } from "dale-chall";
|
|
20
|
+
|
|
21
|
+
const FAMILIAR_WORDS = new Set(daleChall);
|
|
22
|
+
const NOMINALIZATION = /(?:tion|sion|ment|ness|ity|ance|ence|ism)$/i;
|
|
16
23
|
|
|
17
24
|
/** Files in a content tree, excluding configured directory names. */
|
|
18
25
|
export function proseFiles(root, skipDirectories = []) {
|
|
@@ -31,28 +38,84 @@ export function proseFiles(root, skipDirectories = []) {
|
|
|
31
38
|
|
|
32
39
|
/** Keep source offsets while excluding HeroicLands inline addresses and expressions. */
|
|
33
40
|
function maskMarkup(source) {
|
|
34
|
-
return source
|
|
35
|
-
match.replace(/[^\n]/g, " ")
|
|
36
|
-
|
|
41
|
+
return source
|
|
42
|
+
.replace(/!?\[\[[^\]\n]+\]\]|\{\{[^}\n]+\}\}/g, (match) => match.replace(/[^\n]/g, " "))
|
|
43
|
+
.replace(/^:::sql[^\n]*\n[\s\S]*?^:::[ \t]*$/gm, (match) => match.replace(/[^\n]/g, " "))
|
|
44
|
+
.replace(/^:::[^\n]*$/gm, (match) => match.replace(/[^\n]/g, " "));
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
47
|
+
/** Select complete paragraphs while keeping their original source positions. */
|
|
48
|
+
function proseTree(source) {
|
|
49
|
+
const processor = unified().use(remarkParse).use(remarkFrontmatter).use(remarkGfm);
|
|
50
|
+
const tree = processor.parse(maskMarkup(source));
|
|
51
|
+
const keepInline = (node) => {
|
|
52
|
+
if (
|
|
53
|
+
["inlineCode", "image", "imageReference", "html", "footnoteReference"].includes(
|
|
54
|
+
node.type,
|
|
55
|
+
)
|
|
56
|
+
) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (node.children) node.children = node.children.filter(keepInline);
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
const paragraphs = [];
|
|
63
|
+
const collect = (node) => {
|
|
64
|
+
if (node.type === "paragraph") paragraphs.push(node);
|
|
65
|
+
else if (node.type === "blockquote")
|
|
66
|
+
for (const child of node.children ?? []) collect(child);
|
|
67
|
+
};
|
|
68
|
+
for (const node of tree.children) collect(node);
|
|
69
|
+
tree.children = paragraphs;
|
|
70
|
+
for (const paragraph of tree.children)
|
|
71
|
+
paragraph.children = paragraph.children.filter(keepInline);
|
|
72
|
+
return tree;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Count readable body words, including headings and lists, for score coverage. */
|
|
76
|
+
function bodyWordCount(source) {
|
|
77
|
+
const tree = unified()
|
|
44
78
|
.use(remarkParse)
|
|
45
79
|
.use(remarkFrontmatter)
|
|
46
|
-
.use(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
80
|
+
.use(remarkGfm)
|
|
81
|
+
.parse(maskMarkup(source));
|
|
82
|
+
const visit = (node) => {
|
|
83
|
+
if (
|
|
84
|
+
["yaml", "code", "table", "html", "inlineCode", "image", "imageReference"].includes(
|
|
85
|
+
node.type,
|
|
86
|
+
)
|
|
87
|
+
) {
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
if (node.type === "text")
|
|
91
|
+
return (node.value.match(/[\p{L}\p{N}]+(?:['’-][\p{L}\p{N}]+)*/gu) ?? []).length;
|
|
92
|
+
return (node.children ?? []).reduce((sum, child) => sum + visit(child), 0);
|
|
93
|
+
};
|
|
94
|
+
return visit(tree);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Bridge selected Markdown paragraphs to retext for lint and scoring. */
|
|
98
|
+
async function retextProse(file, source, options = {}, inspect) {
|
|
99
|
+
const virtualFile = { value: maskMarkup(source), path: file, messages: [] };
|
|
100
|
+
const analysis = unified().use(retextEnglish);
|
|
101
|
+
if (options.rules !== "simplify" && options.rules !== "none") {
|
|
102
|
+
analysis.use(retextReadability, {
|
|
103
|
+
age: options.age ?? 21,
|
|
104
|
+
threshold: (options.threshold ?? 5) / 7,
|
|
105
|
+
minWords: options.minWords ?? 8,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (options.rules === "simplify" || options.rules === "all") analysis.use(retextSimplify);
|
|
109
|
+
if (inspect) analysis.use(() => (tree) => inspect(tree));
|
|
110
|
+
analysis.use(retextStringify);
|
|
111
|
+
await unified().use(remarkRetext, analysis).run(proseTree(source), virtualFile);
|
|
112
|
+
return virtualFile.messages;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Source-aligned findings for one Markdown note. */
|
|
116
|
+
export async function analyzeProse(file, source, options = {}) {
|
|
117
|
+
const messages = await retextProse(file, source, options);
|
|
118
|
+
return messages.map((message) => {
|
|
56
119
|
const position = message.place?.start;
|
|
57
120
|
const sentence = message.ancestors?.find((node) => node.type === "SentenceNode");
|
|
58
121
|
const span = sentence?.position;
|
|
@@ -84,3 +147,118 @@ export async function lintProse(root, options, skipDirectories = []) {
|
|
|
84
147
|
}
|
|
85
148
|
return findings;
|
|
86
149
|
}
|
|
150
|
+
|
|
151
|
+
/** Concatenate the text leaves in a retext word. */
|
|
152
|
+
function wordText(node) {
|
|
153
|
+
return node.value ?? (node.children ?? []).map(wordText).join("");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Count the words and syllables in one retext sentence. */
|
|
157
|
+
function sentenceCounts(sentence) {
|
|
158
|
+
const words = [];
|
|
159
|
+
const visit = (node) => {
|
|
160
|
+
if (node.type === "WordNode") words.push(wordText(node));
|
|
161
|
+
else for (const child of node.children ?? []) visit(child);
|
|
162
|
+
};
|
|
163
|
+
visit(sentence);
|
|
164
|
+
return {
|
|
165
|
+
words: words.length,
|
|
166
|
+
syllables: words.reduce((sum, word) => sum + syllable(word), 0),
|
|
167
|
+
unfamiliar: words.filter((word) => !FAMILIAR_WORDS.has(word.toLowerCase())).length,
|
|
168
|
+
nominalizations: words.filter((word) => NOMINALIZATION.test(word)).length,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Score the complete prose paragraphs in one note. */
|
|
173
|
+
export async function scoreProse(file, source, { minWords = 80, bands = {} } = {}) {
|
|
174
|
+
const sentences = [];
|
|
175
|
+
await retextProse(file, source, { rules: "none" }, (tree) => {
|
|
176
|
+
const visit = (node) => {
|
|
177
|
+
if (node.type === "SentenceNode") sentences.push(sentenceCounts(node));
|
|
178
|
+
else for (const child of node.children ?? []) visit(child);
|
|
179
|
+
};
|
|
180
|
+
visit(tree);
|
|
181
|
+
});
|
|
182
|
+
const totals = sentences.reduce(
|
|
183
|
+
(sum, sentence) => ({
|
|
184
|
+
words: sum.words + sentence.words,
|
|
185
|
+
syllables: sum.syllables + sentence.syllables,
|
|
186
|
+
unfamiliar: sum.unfamiliar + sentence.unfamiliar,
|
|
187
|
+
nominalizations: sum.nominalizations + sentence.nominalizations,
|
|
188
|
+
}),
|
|
189
|
+
{ words: 0, syllables: 0, unfamiliar: 0, nominalizations: 0 },
|
|
190
|
+
);
|
|
191
|
+
const longestSentenceWords = Math.max(0, ...sentences.map((sentence) => sentence.words));
|
|
192
|
+
const count = sentences.filter((sentence) => sentence.words > 0).length;
|
|
193
|
+
const metrics =
|
|
194
|
+
count && totals.words ?
|
|
195
|
+
{
|
|
196
|
+
flesch: flesch({ sentence: count, word: totals.words, syllable: totals.syllables }),
|
|
197
|
+
syllablesPerWord: totals.syllables / totals.words,
|
|
198
|
+
meanSentenceWords: totals.words / count,
|
|
199
|
+
longestSentenceWords,
|
|
200
|
+
unfamiliarWordPercent: (100 * totals.unfamiliar) / totals.words,
|
|
201
|
+
nominalizationsPer1000Words: (1000 * totals.nominalizations) / totals.words,
|
|
202
|
+
}
|
|
203
|
+
: null;
|
|
204
|
+
const insufficient = totals.words < minWords;
|
|
205
|
+
const violations = [];
|
|
206
|
+
if (!insufficient) {
|
|
207
|
+
for (const [metric, band] of Object.entries(bands)) {
|
|
208
|
+
if (band.min !== undefined && metrics[metric] < band.min) {
|
|
209
|
+
violations.push(`${metric} below ${band.min}`);
|
|
210
|
+
}
|
|
211
|
+
if (band.max !== undefined && metrics[metric] > band.max) {
|
|
212
|
+
violations.push(`${metric} above ${band.max}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const bodyWords = bodyWordCount(source);
|
|
217
|
+
return {
|
|
218
|
+
file,
|
|
219
|
+
...totals,
|
|
220
|
+
bodyWords,
|
|
221
|
+
coveragePercent: bodyWords ? (100 * totals.words) / bodyWords : 0,
|
|
222
|
+
sentences: count,
|
|
223
|
+
longestSentenceWords,
|
|
224
|
+
metrics,
|
|
225
|
+
insufficient,
|
|
226
|
+
violations,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Score a note or content tree and pool counts for its aggregate. */
|
|
231
|
+
export async function scoreProseTree(root, options, skipDirectories = []) {
|
|
232
|
+
const notes = [];
|
|
233
|
+
for (const file of proseFiles(root, skipDirectories)) {
|
|
234
|
+
notes.push(await scoreProse(file, fs.readFileSync(file, "utf8"), options));
|
|
235
|
+
}
|
|
236
|
+
const scored = notes.filter((note) => !note.insufficient);
|
|
237
|
+
const totals = scored.reduce(
|
|
238
|
+
(sum, note) => ({
|
|
239
|
+
words: sum.words + note.words,
|
|
240
|
+
syllables: sum.syllables + note.syllables,
|
|
241
|
+
sentences: sum.sentences + note.sentences,
|
|
242
|
+
unfamiliar: sum.unfamiliar + note.unfamiliar,
|
|
243
|
+
nominalizations: sum.nominalizations + note.nominalizations,
|
|
244
|
+
}),
|
|
245
|
+
{ words: 0, syllables: 0, sentences: 0, unfamiliar: 0, nominalizations: 0 },
|
|
246
|
+
);
|
|
247
|
+
return {
|
|
248
|
+
notes,
|
|
249
|
+
summary: {
|
|
250
|
+
...totals,
|
|
251
|
+
scored: scored.length,
|
|
252
|
+
insufficient: notes.length - scored.length,
|
|
253
|
+
outside: scored.filter((note) => note.violations.length).length,
|
|
254
|
+
flesch:
|
|
255
|
+
totals.words && totals.sentences ?
|
|
256
|
+
flesch({
|
|
257
|
+
sentence: totals.sentences,
|
|
258
|
+
word: totals.words,
|
|
259
|
+
syllable: totals.syllables,
|
|
260
|
+
})
|
|
261
|
+
: null,
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
@@ -43,10 +43,6 @@ import { HM3_DOCUMENT_SUBTYPES } from "../hm3/document-subtypes.mjs";
|
|
|
43
43
|
/**
|
|
44
44
|
* The note-type → document-subtype maps this toolchain ships.
|
|
45
45
|
*
|
|
46
|
-
* Two, since `hm3/` landed — and it joined this list rather than the
|
|
47
|
-
* claim table growing a second copy of the same fact, which is what the list
|
|
48
|
-
* was for.
|
|
49
|
-
*
|
|
50
46
|
* The union is what makes the vocabulary wider than any one repository's
|
|
51
47
|
* configuration: `armorlocation` is a real content type because HM3 maps it,
|
|
52
48
|
* however a given repository is configured, so a tree full of them is a
|
|
@@ -90,7 +86,7 @@ export const KNOWN_DOCUMENT_SUBTYPE_MAPS = Object.freeze([
|
|
|
90
86
|
export const DEFAULT_DOCUMENT_SUBTYPES = SOHL_DOCUMENT_SUBTYPES;
|
|
91
87
|
|
|
92
88
|
/**
|
|
93
|
-
* Every system
|
|
89
|
+
* Every system identifier with declared system-specific document types.
|
|
94
90
|
*
|
|
95
91
|
* Derived from {@link KNOWN_DOCUMENT_SUBTYPE_MAPS}, so registering a map is the
|
|
96
92
|
* whole of adding a system to every reader that asks "which systems are there?"
|
|
@@ -156,7 +152,7 @@ export const ACTOR_TYPES = Object.freeze(
|
|
|
156
152
|
/**
|
|
157
153
|
* The map one system ships, by its id.
|
|
158
154
|
*
|
|
159
|
-
* @param {string|undefined} system - The system id
|
|
155
|
+
* @param {string|undefined} system - The system id.
|
|
160
156
|
* @returns {import("./document-subtypes.mjs").DocumentSubtypeMap|undefined} Its
|
|
161
157
|
* map, or `undefined` where this toolchain ships none for it.
|
|
162
158
|
*/
|
package/engine/systems.mjs
CHANGED
|
@@ -20,10 +20,8 @@
|
|
|
20
20
|
* address grammar asks on every note, and the issue's answer is that it comes
|
|
21
21
|
* from a **registry rather than from a hardcoded set**: known system ids are
|
|
22
22
|
* declared here, once, an unknown value is an error, and _adding a system is a
|
|
23
|
-
* data change_.
|
|
24
|
-
*
|
|
25
|
-
* per document-subtype map, a system-keyed table per compiler — none of which
|
|
26
|
-
* could refuse a value none of the others had heard of.
|
|
23
|
+
* data change_. The same fact is read by address parsing, configuration and
|
|
24
|
+
* linting.
|
|
27
25
|
*
|
|
28
26
|
* **`none` is a word, deliberately.** It is not a YAML null (`null`, `~`, or an
|
|
29
27
|
* empty value), because those spell "nobody filled this in", and a note that
|
|
@@ -38,8 +36,8 @@
|
|
|
38
36
|
* the configuration loader, by a compiler, and by a lint without closing a
|
|
39
37
|
* cycle around any of them. In particular the ids are *declared* here rather
|
|
40
38
|
* than derived from `KNOWN_DOCUMENT_SUBTYPE_MAPS` — deriving them would drag
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* system-specific modules behind every import of this file. `tests/systems.test.ts`
|
|
40
|
+
* holds the registry and note-key vocabulary in step, which is where
|
|
43
41
|
* the agreement between a registry and its implementations belongs.
|
|
44
42
|
*
|
|
45
43
|
* @module
|
|
@@ -66,11 +64,11 @@ export const NO_SYSTEM = "none";
|
|
|
66
64
|
export const NOTE_SYSTEM = "note";
|
|
67
65
|
|
|
68
66
|
/**
|
|
69
|
-
* Every game system this toolchain
|
|
67
|
+
* Every game system identifier this toolchain recognizes.
|
|
70
68
|
*
|
|
71
|
-
* The registry, and the only place the list is written down.
|
|
72
|
-
*
|
|
73
|
-
*
|
|
69
|
+
* The registry, and the only place the list is written down. A recognized id
|
|
70
|
+
* can have no system-specific document types; adding one of those requires a
|
|
71
|
+
* subtype map and compiler as well.
|
|
74
72
|
*
|
|
75
73
|
* `none` is **not** a member: it marks the absence of a system, and a caller
|
|
76
74
|
* asking "is this a system" and a caller asking "is this a well-formed
|
|
@@ -79,7 +77,7 @@ export const NOTE_SYSTEM = "note";
|
|
|
79
77
|
*
|
|
80
78
|
* @type {ReadonlySet<string>}
|
|
81
79
|
*/
|
|
82
|
-
export const SYSTEM_IDS = Object.freeze(new Set(["sohl", "hm3"]));
|
|
80
|
+
export const SYSTEM_IDS = Object.freeze(new Set(["sohl", "hm3", "dnd5e"]));
|
|
83
81
|
|
|
84
82
|
/**
|
|
85
83
|
* Everything the `<system>` segment of an address may say.
|
|
@@ -127,8 +125,8 @@ export function isSystemSegment(value) {
|
|
|
127
125
|
*
|
|
128
126
|
* The nullish case is called out by name because it is the likely mistake and
|
|
129
127
|
* the least legible failure: a YAML `null`, a `~`, or a key written with no
|
|
130
|
-
* value at all reaches here as `undefined`, and
|
|
131
|
-
*
|
|
128
|
+
* value at all reaches here as `undefined`, and a list of accepted values
|
|
129
|
+
* would leave a reader hunting for the value they cannot see.
|
|
132
130
|
*
|
|
133
131
|
* @param {unknown} value - The offending value.
|
|
134
132
|
* @param {string} [where] - What carried it, for the message — a note path, a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.17.
|
|
3
|
+
"version": "22.17.2",
|
|
4
4
|
"description": "Shared toolchain for building and shipping a HeroicLands Foundry VTT package — content compilation, manifest, localization, staging, bundle, release and deployment.",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -137,10 +137,12 @@
|
|
|
137
137
|
"acorn": "^8.18.0",
|
|
138
138
|
"archiver": "^8.0.0",
|
|
139
139
|
"classic-level": "^3.0.0",
|
|
140
|
+
"dale-chall": "^2.0.1",
|
|
140
141
|
"dotenv": "^18.0.0",
|
|
141
142
|
"eslint": "^10.10.0",
|
|
142
143
|
"eslint-plugin-yml": "^3.8.1",
|
|
143
144
|
"fflate": "^0.8.3",
|
|
145
|
+
"flesch": "^2.0.1",
|
|
144
146
|
"glob": "^13.0.6",
|
|
145
147
|
"gray-matter": "^4.0.3",
|
|
146
148
|
"handlebars": "^4.7.9",
|
|
@@ -153,6 +155,7 @@
|
|
|
153
155
|
"pagefind": "^1.5.2",
|
|
154
156
|
"prettier": "^3.9.6",
|
|
155
157
|
"remark-frontmatter": "^5.0.0",
|
|
158
|
+
"remark-gfm": "^4.0.1",
|
|
156
159
|
"remark-parse": "^11.0.0",
|
|
157
160
|
"remark-retext": "^6.0.1",
|
|
158
161
|
"retext-english": "^5.0.0",
|
|
@@ -162,6 +165,7 @@
|
|
|
162
165
|
"sharp": "^0.35.4",
|
|
163
166
|
"smol-toml": "^1.8.0",
|
|
164
167
|
"ssh2-sftp-client": "^12.1.1",
|
|
168
|
+
"syllable": "^5.0.1",
|
|
165
169
|
"typescript": "^6.0.3",
|
|
166
170
|
"unidecode": "^1.1.0",
|
|
167
171
|
"unified": "^11.0.5",
|
package/types/config.d.mts
CHANGED
|
@@ -94,7 +94,8 @@ export const DERIVED_MANIFEST_KEYS: Readonly<Record<string, string>>;
|
|
|
94
94
|
*
|
|
95
95
|
* @typedef {object} PackageBuildConfig
|
|
96
96
|
* @property {string} rootDir The repository root, from package-build.
|
|
97
|
-
* @property {{age: number, threshold: number, minWords: number}} proseLint Optional prose
|
|
97
|
+
* @property {{age: number, threshold: number, minWords: number, rules: string}} proseLint Optional prose lint settings.
|
|
98
|
+
* @property {{minWords: number, bands: Record<string, {min?: number, max?: number}>}} proseScore Optional note score settings.
|
|
98
99
|
* @property {string} packageKind `systems` or `modules`.
|
|
99
100
|
* @property {string} packageId The Foundry package id.
|
|
100
101
|
* @property {string} artifact Derived: `system` or `module`.
|
|
@@ -198,12 +199,23 @@ export type PackageBuildConfig = {
|
|
|
198
199
|
*/
|
|
199
200
|
rootDir: string;
|
|
200
201
|
/**
|
|
201
|
-
* Optional prose
|
|
202
|
+
* Optional prose lint settings.
|
|
202
203
|
*/
|
|
203
204
|
proseLint: {
|
|
204
205
|
age: number;
|
|
205
206
|
threshold: number;
|
|
206
207
|
minWords: number;
|
|
208
|
+
rules: string;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Optional note score settings.
|
|
212
|
+
*/
|
|
213
|
+
proseScore: {
|
|
214
|
+
minWords: number;
|
|
215
|
+
bands: Record<string, {
|
|
216
|
+
min?: number;
|
|
217
|
+
max?: number;
|
|
218
|
+
}>;
|
|
207
219
|
};
|
|
208
220
|
/**
|
|
209
221
|
* `systems` or `modules`.
|
|
@@ -230,11 +230,11 @@ export class SystemActorCompiler extends BasePackCompiler {
|
|
|
230
230
|
* The actor's two pieces of art, resolved, with the being default beneath
|
|
231
231
|
* them.
|
|
232
232
|
*
|
|
233
|
-
* **The default
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
233
|
+
* **The default follows the note's `subType`**, with tags as a compatibility
|
|
234
|
+
* fallback for content trees that have not migrated. Both addresses belong
|
|
235
|
+
* to the package that ships them. A tree whose index cannot answer an
|
|
236
|
+
* address — a dependency not yet fetched — falls through to the document
|
|
237
|
+
* subtype's own default, so the document is never left with no art.
|
|
238
238
|
*
|
|
239
239
|
* `tokenIcon` unset follows `icon`, and the fallback is applied after
|
|
240
240
|
* resolution rather than before: a note naming an icon and no token icon
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one spelling of an address in this space: `type:shortcode`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* shortcode is used **verbatim**, not lowercased: `readItemAddresses` reads it
|
|
8
|
-
* off the compiled document, where `Tabri` is stored as authored, and folding
|
|
9
|
-
* case here would join two addresses the packs keep apart.
|
|
4
|
+
* Used for addresses read from compiled documents. The shortcode is used
|
|
5
|
+
* **verbatim**, not lowercased: folding case would join two addresses the packs
|
|
6
|
+
* keep apart.
|
|
10
7
|
*
|
|
11
8
|
* @param {string} type - The Foundry document subtype, not the note type.
|
|
12
9
|
* @param {string} shortcode - The address's `system.shortcode`.
|
|
@@ -39,89 +36,24 @@ export function readItemAddresses(dirs: readonly string[]): Map<string, {
|
|
|
39
36
|
shortcode: string;
|
|
40
37
|
file: string;
|
|
41
38
|
}>;
|
|
42
|
-
/**
|
|
43
|
-
* Every rename the tree's notes **declare**, as old address → where it went.
|
|
44
|
-
*
|
|
45
|
-
* Read from the content tree rather than from compiled output, because a
|
|
46
|
-
* declaration is authored and the compiled document does not carry it: nothing
|
|
47
|
-
* downstream consumes `renamedFrom:`, so emitting it into every pack to let one
|
|
48
|
-
* diagnostic read it back would put a build-time note in shipped data forever.
|
|
49
|
-
* The tree is already read by this module for the same reason
|
|
50
|
-
* ({@link noteFilesById}) — to place a finding where its author can fix it.
|
|
51
|
-
*
|
|
52
|
-
* **A declaration is keyed by document subtype, not by note type.** The address
|
|
53
|
-
* space is the one consumers resolve against, and it is spelled in compiled
|
|
54
|
-
* documents: `hm3` compiles a `projectile` note into a `missilegear` item,
|
|
55
|
-
* so that is the address a rename of it moves. {@link referencedSubtype} is the
|
|
56
|
-
* function that already answers this for a being's embedded `(type, shortcode)`
|
|
57
|
-
* references, so both sides read the same rule rather than a second copy of it.
|
|
58
|
-
*
|
|
59
|
-
* **An entry is emitted for every system that maps the type**, whether or not
|
|
60
|
-
* the note declares that system's block. Over-emitting is inert — the diff uses
|
|
61
|
-
* an entry only when the baseline published the old address *and* this build
|
|
62
|
-
* publishes the new one, and a system the note does not compile for satisfies
|
|
63
|
-
* neither — while asking which blocks a note declares would put a second,
|
|
64
|
-
* subtly different answer to that question in a third place.
|
|
65
|
-
*
|
|
66
|
-
* **First claim wins on a collision.** Two notes naming one predecessor is a
|
|
67
|
-
* contradiction — an address has one successor — and it is reported as an error
|
|
68
|
-
* by `engine/content-lint.mjs`, where both notes are in hand and can both be
|
|
69
|
-
* named. Picking one here keeps this a map; it is not a resolution, and nothing
|
|
70
|
-
* rests on which one it picked.
|
|
71
|
-
*
|
|
72
|
-
* @param {string} contentBase - Root of the content tree.
|
|
73
|
-
* @param {object} opts
|
|
74
|
-
* @param {readonly string[]} [opts.skipDirectories] - The corpus scope. Stated
|
|
75
|
-
* by the caller, never defaulted — see {@link addressCorpus}.
|
|
76
|
-
* @param {readonly object[]} [opts.maps] - The document-subtype maps.
|
|
77
|
-
* @param {object} [opts.config] - The resolved build configuration.
|
|
78
|
-
* @param {readonly object[]} [opts.records] - Index records the caller already
|
|
79
|
-
* derived, shared with {@link noteFilesById} so one command reads one corpus.
|
|
80
|
-
* @param {object[]} [opts.problems] - Collects the notes the index cannot
|
|
81
|
-
* record, so one of them does not abort the diff before it reports.
|
|
82
|
-
* @returns {Map<string, {to: string, file: string, shortcode: string}>} Old
|
|
83
|
-
* address → the address the declaring note publishes at now, and that note.
|
|
84
|
-
*/
|
|
85
|
-
export function declaredPredecessors(contentBase: string, { skipDirectories, maps, config, records, problems }?: {
|
|
86
|
-
skipDirectories?: readonly string[] | undefined;
|
|
87
|
-
maps?: readonly object[] | undefined;
|
|
88
|
-
config?: object | undefined;
|
|
89
|
-
records?: readonly object[] | undefined;
|
|
90
|
-
problems?: object[] | undefined;
|
|
91
|
-
}): Map<string, {
|
|
92
|
-
to: string;
|
|
93
|
-
file: string;
|
|
94
|
-
shortcode: string;
|
|
95
|
-
}>;
|
|
96
39
|
/**
|
|
97
40
|
* Every address the baseline published that this build does not.
|
|
98
41
|
*
|
|
99
|
-
* An address that
|
|
100
|
-
*
|
|
101
|
-
*
|
|
42
|
+
* An address that arrived is not a finding. A departed address is a rename
|
|
43
|
+
* only when its document id appears at another current address; otherwise it
|
|
44
|
+
* is a withdrawal.
|
|
102
45
|
*
|
|
103
46
|
* @param {Map<string, object>} baseline - The released address space.
|
|
104
47
|
* @param {Map<string, object>} current - This build's address space.
|
|
105
48
|
* @param {object} opts
|
|
106
|
-
* @param {string} opts.baseline - What the baseline is, for the message
|
|
107
|
-
*
|
|
108
|
-
* @param {Map<string, {to: string, file: string}>} [opts.predecessors] - The
|
|
109
|
-
* declared renames, from {@link declaredPredecessors}. Omitted, the diff
|
|
110
|
-
* falls back to the id join alone and reports an unpinned rename as a
|
|
111
|
-
* withdrawal, which is what it did before.
|
|
112
|
-
* @returns {Array<object>} One finding per departed address, in address order
|
|
113
|
-
* so two runs read the same. `kind` is `"renamed"` (with `to`, and `declared`
|
|
114
|
-
* when it was the note's word rather than an id match) or `"withdrawn"`.
|
|
49
|
+
* @param {string} opts.baseline - What the baseline is, for the message.
|
|
50
|
+
* @returns {Array<object>} One finding per departed address, in address order.
|
|
115
51
|
*/
|
|
116
|
-
export function diffItemAddresses(baseline: Map<string, object>, current: Map<string, object>, { baseline: label
|
|
52
|
+
export function diffItemAddresses(baseline: Map<string, object>, current: Map<string, object>, { baseline: label }: {
|
|
117
53
|
baseline: string;
|
|
118
|
-
predecessors?: Map<string, {
|
|
119
|
-
to: string;
|
|
120
|
-
file: string;
|
|
121
|
-
}> | undefined;
|
|
122
54
|
}): Array<object>;
|
|
123
55
|
/**
|
|
124
|
-
* Every content note in a tree, indexed by the document id it
|
|
56
|
+
* Every content note in a tree, indexed by the document id it compiles under.
|
|
125
57
|
*
|
|
126
58
|
* The address space is read from compiled output because that is what actually
|
|
127
59
|
* ships; the tree is read only to place a finding somewhere a reader can open
|
|
@@ -135,7 +67,7 @@ export function diffItemAddresses(baseline: Map<string, object>, current: Map<st
|
|
|
135
67
|
* @param {object} [opts.config] - The resolved build configuration, which the
|
|
136
68
|
* id is derived against. See {@link addressCorpus} for why that matters.
|
|
137
69
|
* @param {readonly object[]} [opts.records] - Index records the caller already
|
|
138
|
-
* derived
|
|
70
|
+
* derived by the caller.
|
|
139
71
|
* @param {object[]} [opts.problems] - Collects the notes the index cannot
|
|
140
72
|
* record, so one of them does not abort the diff before it reports.
|
|
141
73
|
* @returns {Map<string, string>} Document id → the note's absolute path.
|
|
@@ -149,22 +81,13 @@ export function noteFilesById(contentBase: string, { skipDirectories, config, re
|
|
|
149
81
|
/**
|
|
150
82
|
* Where to send the reader for one finding.
|
|
151
83
|
*
|
|
152
|
-
* A
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* the baseline document, which is the only artefact left that records the
|
|
156
|
-
* address existing. When neither is readable the position is **dropped**, never
|
|
157
|
-
* defaulted to `1:1`.
|
|
158
|
-
*
|
|
159
|
-
* A **declared** rename knows its note without any lookup — the declaration is
|
|
160
|
-
* how it was found — and is reported at the `renamedFrom:` line rather than the
|
|
161
|
-
* `shortcode:` line, because that is the line the finding is about and the one
|
|
162
|
-
* the author deletes once the declaration has done its work.
|
|
84
|
+
* A matched document is located at its current `shortcode:` line. A withdrawal
|
|
85
|
+
* is located in the baseline artifact, the remaining evidence for its address.
|
|
86
|
+
* When neither source has a usable position, the location is dropped.
|
|
163
87
|
*
|
|
164
88
|
* @param {object} finding - One finding from {@link diffItemAddresses}.
|
|
165
89
|
* @param {Map<string, string>} noteFiles - From {@link noteFilesById}.
|
|
166
|
-
* @returns {{file?: string, line?: number, column?: number}}
|
|
167
|
-
* position fields for {@link formatDiagnostic}.
|
|
90
|
+
* @returns {{file?: string, line?: number, column?: number}} Position fields.
|
|
168
91
|
*/
|
|
169
92
|
export function locateAddressFinding(finding: object, noteFiles: Map<string, string>): {
|
|
170
93
|
file?: string;
|
|
@@ -174,21 +97,9 @@ export function locateAddressFinding(finding: object, noteFiles: Map<string, str
|
|
|
174
97
|
/**
|
|
175
98
|
* What one finding says, without a locator or a severity.
|
|
176
99
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* known — and says so, rather than leaving the reader to wonder whether one was
|
|
181
|
-
* looked for.
|
|
182
|
-
*
|
|
183
|
-
* **A declared rename says it is declared**. The two claims are not
|
|
184
|
-
* equally checkable: an id match is a fact in the artefacts, while a
|
|
185
|
-
* declaration is an author's word, and a reader deciding whether to trust the
|
|
186
|
-
* successor needs to know which one they have. Saying "the same document" of a
|
|
187
|
-
* declared rename would spend the id match's credibility on it.
|
|
188
|
-
*
|
|
189
|
-
* The consequence sentence is the same for both, because it is the same
|
|
190
|
-
* consequence: the old address stops resolving either way, and that is what
|
|
191
|
-
* the reader has to act on.
|
|
100
|
+
* A matched document names the address where it now appears. A withdrawal
|
|
101
|
+
* names no successor because the current build publishes no document with
|
|
102
|
+
* that identity.
|
|
192
103
|
*
|
|
193
104
|
* @param {object} finding - One finding from {@link diffItemAddresses}.
|
|
194
105
|
* @returns {string} The message.
|
|
@@ -142,14 +142,13 @@ export function artPathname(index: object, value: unknown, defaultType: string,
|
|
|
142
142
|
type?: string;
|
|
143
143
|
};
|
|
144
144
|
/**
|
|
145
|
-
* The default art address a being's
|
|
145
|
+
* The default art address a being's subtype chooses, or `null`.
|
|
146
146
|
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* lookup rather than a precedence rule.
|
|
147
|
+
* An NPC and a character use person art; a creature uses creature art. The
|
|
148
|
+
* tag lookup preserves defaults for package trees that have not migrated.
|
|
150
149
|
*
|
|
151
150
|
* @param {object} fm - The note's frontmatter.
|
|
152
|
-
* @returns {string|null} The address, or `null`
|
|
151
|
+
* @returns {string|null} The address, or `null` when no subtype or legacy tag applies.
|
|
153
152
|
*/
|
|
154
153
|
export function beingDefaultArt(fm: object): string | null;
|
|
155
154
|
/**
|
|
@@ -12,6 +12,20 @@
|
|
|
12
12
|
* @returns {boolean} Whether it is acceptable.
|
|
13
13
|
*/
|
|
14
14
|
export function matchesKind(value: unknown, kind: string, context?: object): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Resolve system-block addresses declared by the system's own field schema.
|
|
17
|
+
* @param {object} note - A note from the link index.
|
|
18
|
+
* @param {object} opts
|
|
19
|
+
* @param {object} opts.index - The address index.
|
|
20
|
+
* @param {Record<string, readonly object[]>} [opts.schemas] - Note schemas.
|
|
21
|
+
* @param {Readonly<Record<string, object>>} [opts.systems] - Declared system blocks.
|
|
22
|
+
* @returns {object[]} Located findings for unresolved addresses.
|
|
23
|
+
*/
|
|
24
|
+
export function systemAddressFindings(note: object, { index, schemas, systems }?: {
|
|
25
|
+
index: object;
|
|
26
|
+
schemas?: Record<string, readonly object[]> | undefined;
|
|
27
|
+
systems?: Readonly<Record<string, object>> | undefined;
|
|
28
|
+
}): object[];
|
|
15
29
|
/**
|
|
16
30
|
* Check one note against its type's schema.
|
|
17
31
|
*
|