@heroiclands/package-build 22.17.0 → 22.17.1
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 +9 -0
- package/bin/content-commands.mjs +93 -14
- package/config.mjs +54 -3
- package/docs/commands.md +70 -34
- package/docs/configuration.md +49 -22
- package/engine/readability-lint.mjs +196 -18
- package/package.json +5 -1
- package/types/config.d.mts +14 -2
- package/types/engine/readability-lint.d.mts +11 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# @heroiclands/package-build
|
|
2
2
|
|
|
3
|
+
## 22.17.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
**Prose review**
|
|
8
|
+
|
|
9
|
+
- Prose lint focuses on difficult sentences by default and skips tables and code; simpler-word suggestions remain available on request.
|
|
10
|
+
- Prose score reports each note's readability and lets a content package define an advisory range for its writing.
|
|
11
|
+
|
|
3
12
|
## 22.17.0
|
|
4
13
|
|
|
5
14
|
### Minor Changes
|
package/bin/content-commands.mjs
CHANGED
|
@@ -97,7 +97,7 @@ import { ENGINE_NOTE_SCHEMAS } from "../engine/note-schemas.mjs";
|
|
|
97
97
|
import { schemaSubtypeOf } from "../engine/subtype-registry.mjs";
|
|
98
98
|
import { NOTE_VOCABULARY } from "../engine/note-vocabulary.mjs";
|
|
99
99
|
import { checkFormatting, checkPrettierConventions, lintMarkdown } from "../engine/prose-lint.mjs";
|
|
100
|
-
import { lintProse } from "../engine/readability-lint.mjs";
|
|
100
|
+
import { lintProse, scoreProseTree } from "../engine/readability-lint.mjs";
|
|
101
101
|
import { loadPackageBuildConfig } from "../config.mjs";
|
|
102
102
|
import {
|
|
103
103
|
authoredFrontmatter,
|
|
@@ -240,14 +240,14 @@ export function registerContentCommands(cli) {
|
|
|
240
240
|
.command(addressesCommand());
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
/**
|
|
243
|
+
/** Optional sentence findings and note-level readability reports. */
|
|
244
244
|
function proseCommand() {
|
|
245
245
|
return {
|
|
246
246
|
command: "prose <action> [path]",
|
|
247
247
|
describe: "Analyze note prose on demand",
|
|
248
248
|
builder: (yargs) =>
|
|
249
249
|
yargs
|
|
250
|
-
.positional("action", { choices: ["lint"], describe: "Analyze prose" })
|
|
250
|
+
.positional("action", { choices: ["lint", "score"], describe: "Analyze prose" })
|
|
251
251
|
.positional("path", {
|
|
252
252
|
type: "string",
|
|
253
253
|
describe: "One Markdown file or a content tree",
|
|
@@ -257,17 +257,97 @@ function proseCommand() {
|
|
|
257
257
|
type: "number",
|
|
258
258
|
describe: "Readability algorithms required (1–7)",
|
|
259
259
|
})
|
|
260
|
-
.option("min-words", {
|
|
260
|
+
.option("min-words", {
|
|
261
|
+
type: "number",
|
|
262
|
+
describe: "Minimum words per sentence or note",
|
|
263
|
+
})
|
|
264
|
+
.option("rules", {
|
|
265
|
+
choices: ["readability", "simplify", "all"],
|
|
266
|
+
describe: "Lint rule set",
|
|
267
|
+
})
|
|
268
|
+
.option("fail-on-warning", {
|
|
269
|
+
type: "boolean",
|
|
270
|
+
default: false,
|
|
271
|
+
describe: "Exit unsuccessfully when lint reports suggestions",
|
|
272
|
+
})
|
|
273
|
+
.option("fail-outside", {
|
|
274
|
+
type: "boolean",
|
|
275
|
+
default: false,
|
|
276
|
+
describe: "Exit unsuccessfully when a scored note is outside a configured band",
|
|
277
|
+
}),
|
|
261
278
|
handler: async (argv) => {
|
|
262
279
|
try {
|
|
263
280
|
const config = loadPackConfig();
|
|
264
|
-
const defaults = loadPackageBuildConfig()
|
|
281
|
+
const defaults = loadPackageBuildConfig();
|
|
282
|
+
const root = argv.path ?? config.paths.content;
|
|
283
|
+
const displayFile = (file) => path.relative(process.cwd(), path.resolve(file));
|
|
284
|
+
if (argv.action === "score") {
|
|
285
|
+
if (
|
|
286
|
+
argv.age !== undefined ||
|
|
287
|
+
argv.threshold !== undefined ||
|
|
288
|
+
argv.rules !== undefined ||
|
|
289
|
+
argv.failOnWarning
|
|
290
|
+
) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
"--age, --threshold, --rules, and --fail-on-warning apply only to prose lint",
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const minWords = argv.minWords ?? defaults.proseScore.minWords;
|
|
296
|
+
if (!Number.isInteger(minWords) || minWords < 1) {
|
|
297
|
+
throw new Error("minWords must be a positive integer");
|
|
298
|
+
}
|
|
299
|
+
const result = await scoreProseTree(
|
|
300
|
+
root,
|
|
301
|
+
{ ...defaults.proseScore, minWords },
|
|
302
|
+
config.skipDirectories,
|
|
303
|
+
);
|
|
304
|
+
const fixed = (number, places = 1) => number.toFixed(places);
|
|
305
|
+
for (const note of result.notes) {
|
|
306
|
+
if (note.insufficient) {
|
|
307
|
+
console.log(
|
|
308
|
+
`${displayFile(note.file)}: insufficient prose (${note.words}/${minWords} words; coverage=${fixed(note.coveragePercent)}%)`,
|
|
309
|
+
);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const metrics = note.metrics;
|
|
313
|
+
const detail =
|
|
314
|
+
`flesch=${fixed(metrics.flesch)} syl/word=${fixed(metrics.syllablesPerWord, 2)} ` +
|
|
315
|
+
`mean=${fixed(metrics.meanSentenceWords)}w longest=${note.longestSentenceWords}w ` +
|
|
316
|
+
`unfamiliar=${fixed(metrics.unfamiliarWordPercent)}% ` +
|
|
317
|
+
`nominalizations/1k=${fixed(metrics.nominalizationsPer1000Words)} ` +
|
|
318
|
+
`words=${note.words} coverage=${fixed(note.coveragePercent)}%`;
|
|
319
|
+
if (note.violations.length) {
|
|
320
|
+
emitDiagnostic({
|
|
321
|
+
file: displayFile(note.file),
|
|
322
|
+
severity: "warning",
|
|
323
|
+
message: `prose-score/band: ${detail}; ${note.violations.join(", ")}`,
|
|
324
|
+
});
|
|
325
|
+
} else {
|
|
326
|
+
console.log(`${displayFile(note.file)}: ${detail}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const aggregate = result.summary;
|
|
330
|
+
console.log(
|
|
331
|
+
`Total: ${aggregate.scored} scored, ${aggregate.insufficient} insufficient, ` +
|
|
332
|
+
`${aggregate.outside} outside band; ${aggregate.words} words; ` +
|
|
333
|
+
`pooled flesch=${aggregate.flesch === null ? "n/a" : fixed(aggregate.flesch)} ` +
|
|
334
|
+
`syl/word=${aggregate.words ? fixed(aggregate.syllables / aggregate.words, 2) : "n/a"} ` +
|
|
335
|
+
`mean=${aggregate.sentences ? fixed(aggregate.words / aggregate.sentences) : "n/a"}w ` +
|
|
336
|
+
`unfamiliar=${aggregate.words ? fixed((100 * aggregate.unfamiliar) / aggregate.words) : "n/a"}% ` +
|
|
337
|
+
`nominalizations/1k=${aggregate.words ? fixed((1000 * aggregate.nominalizations) / aggregate.words) : "n/a"}`,
|
|
338
|
+
);
|
|
339
|
+
if (argv.failOutside && aggregate.outside) process.exitCode = 1;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (argv.failOutside) throw new Error("--fail-outside applies only to prose score");
|
|
265
343
|
const options = {
|
|
266
|
-
age: argv.age ?? defaults.age,
|
|
267
|
-
threshold: argv.threshold ?? defaults.threshold,
|
|
268
|
-
minWords: argv.minWords ?? defaults.minWords,
|
|
344
|
+
age: argv.age ?? defaults.proseLint.age,
|
|
345
|
+
threshold: argv.threshold ?? defaults.proseLint.threshold,
|
|
346
|
+
minWords: argv.minWords ?? defaults.proseLint.minWords,
|
|
347
|
+
rules: argv.rules ?? defaults.proseLint.rules,
|
|
269
348
|
};
|
|
270
349
|
for (const [key, value] of Object.entries(options)) {
|
|
350
|
+
if (key === "rules") continue;
|
|
271
351
|
if (
|
|
272
352
|
!Number.isInteger(value) ||
|
|
273
353
|
value < 1 ||
|
|
@@ -278,13 +358,12 @@ function proseCommand() {
|
|
|
278
358
|
);
|
|
279
359
|
}
|
|
280
360
|
}
|
|
281
|
-
const findings = await lintProse(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
);
|
|
286
|
-
for (const finding of findings) emitDiagnostic(finding);
|
|
361
|
+
const findings = await lintProse(root, options, config.skipDirectories);
|
|
362
|
+
for (const finding of findings) {
|
|
363
|
+
emitDiagnostic({ ...finding, file: displayFile(finding.file) });
|
|
364
|
+
}
|
|
287
365
|
log.info(`${findings.length} prose suggestion(s).`);
|
|
366
|
+
if (argv.failOnWarning && findings.length) process.exitCode = 1;
|
|
288
367
|
} catch (err) {
|
|
289
368
|
reportFailure(err);
|
|
290
369
|
process.exitCode = 1;
|
package/config.mjs
CHANGED
|
@@ -76,6 +76,7 @@ const SECTION_KEYS = [
|
|
|
76
76
|
"container",
|
|
77
77
|
"e2e",
|
|
78
78
|
"proseLint",
|
|
79
|
+
"proseScore",
|
|
79
80
|
];
|
|
80
81
|
|
|
81
82
|
/**
|
|
@@ -528,7 +529,8 @@ function normalizeExceptions(value, field, where) {
|
|
|
528
529
|
*
|
|
529
530
|
* @typedef {object} PackageBuildConfig
|
|
530
531
|
* @property {string} rootDir The repository root, from package-build.
|
|
531
|
-
* @property {{age: number, threshold: number, minWords: number}} proseLint Optional prose
|
|
532
|
+
* @property {{age: number, threshold: number, minWords: number, rules: string}} proseLint Optional prose lint settings.
|
|
533
|
+
* @property {{minWords: number, bands: Record<string, {min?: number, max?: number}>}} proseScore Optional note score settings.
|
|
532
534
|
* @property {string} packageKind `systems` or `modules`.
|
|
533
535
|
* @property {string} packageId The Foundry package id.
|
|
534
536
|
* @property {string} artifact Derived: `system` or `module`.
|
|
@@ -693,9 +695,20 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
693
695
|
|
|
694
696
|
const proseLint = section.proseLint ?? {};
|
|
695
697
|
if (!isMapping(proseLint)) fail("packageBuild.proseLint", "must be a mapping");
|
|
696
|
-
rejectUnknownKeys(
|
|
697
|
-
|
|
698
|
+
rejectUnknownKeys(
|
|
699
|
+
proseLint,
|
|
700
|
+
["age", "threshold", "minWords", "rules"],
|
|
701
|
+
"packageBuild.proseLint.",
|
|
702
|
+
);
|
|
703
|
+
const proseOptions = { age: 21, threshold: 5, minWords: 8, rules: "readability" };
|
|
698
704
|
for (const [key, value] of Object.entries(proseLint)) {
|
|
705
|
+
if (key === "rules") {
|
|
706
|
+
if (!["readability", "simplify", "all"].includes(value)) {
|
|
707
|
+
fail("packageBuild.proseLint.rules", "must be readability, simplify, or all");
|
|
708
|
+
}
|
|
709
|
+
proseOptions.rules = value;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
699
712
|
const upper = key === "threshold" ? 7 : Infinity;
|
|
700
713
|
if (!Number.isInteger(value) || value < 1 || value > upper) {
|
|
701
714
|
fail(
|
|
@@ -708,6 +721,43 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
708
721
|
proseOptions[key] = value;
|
|
709
722
|
}
|
|
710
723
|
|
|
724
|
+
const proseScore = section.proseScore ?? {};
|
|
725
|
+
if (!isMapping(proseScore)) fail("packageBuild.proseScore", "must be a mapping");
|
|
726
|
+
rejectUnknownKeys(proseScore, ["minWords", "bands"], "packageBuild.proseScore.");
|
|
727
|
+
const scoreMinWords = proseScore.minWords ?? 80;
|
|
728
|
+
if (!Number.isInteger(scoreMinWords) || scoreMinWords < 1) {
|
|
729
|
+
fail("packageBuild.proseScore.minWords", "must be a positive integer");
|
|
730
|
+
}
|
|
731
|
+
const bands = proseScore.bands ?? {};
|
|
732
|
+
if (!isMapping(bands)) fail("packageBuild.proseScore.bands", "must be a mapping");
|
|
733
|
+
const scoreMetrics = [
|
|
734
|
+
"flesch",
|
|
735
|
+
"syllablesPerWord",
|
|
736
|
+
"meanSentenceWords",
|
|
737
|
+
"longestSentenceWords",
|
|
738
|
+
"unfamiliarWordPercent",
|
|
739
|
+
"nominalizationsPer1000Words",
|
|
740
|
+
];
|
|
741
|
+
rejectUnknownKeys(bands, scoreMetrics, "packageBuild.proseScore.bands.");
|
|
742
|
+
const scoreBands = {};
|
|
743
|
+
for (const [metric, band] of Object.entries(bands)) {
|
|
744
|
+
const where = `packageBuild.proseScore.bands.${metric}`;
|
|
745
|
+
if (!isMapping(band)) fail(where, "must be a mapping");
|
|
746
|
+
rejectUnknownKeys(band, ["min", "max"], `${where}.`);
|
|
747
|
+
if (band.min === undefined && band.max === undefined) {
|
|
748
|
+
fail(where, "must specify min, max, or both");
|
|
749
|
+
}
|
|
750
|
+
for (const [limit, value] of Object.entries(band)) {
|
|
751
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
752
|
+
fail(`${where}.${limit}`, "must be a finite number");
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (band.min !== undefined && band.max !== undefined && band.min > band.max) {
|
|
756
|
+
fail(where, "min must not exceed max");
|
|
757
|
+
}
|
|
758
|
+
scoreBands[metric] = Object.freeze({ ...band });
|
|
759
|
+
}
|
|
760
|
+
|
|
711
761
|
if (section.assets !== undefined && !Array.isArray(section.assets)) {
|
|
712
762
|
fail("packageBuild.assets", "must be a list");
|
|
713
763
|
}
|
|
@@ -809,6 +859,7 @@ export function resolvePackageBuildConfig(shared) {
|
|
|
809
859
|
return Object.freeze({
|
|
810
860
|
rootDir: shared.rootDir,
|
|
811
861
|
proseLint: Object.freeze(proseOptions),
|
|
862
|
+
proseScore: Object.freeze({ minWords: scoreMinWords, bands: Object.freeze(scoreBands) }),
|
|
812
863
|
// Where the package is assembled before it is zipped or deployed. Every
|
|
813
864
|
// asset destination is relative to it, so a repository's table says
|
|
814
865
|
// `lang`, not `build/stage/lang` — the latter is what each consumer's
|
package/docs/commands.md
CHANGED
|
@@ -1712,82 +1712,118 @@ $ package-build docs item-fields --out docs/item-fields.md --title "Demo Item Fi
|
|
|
1712
1712
|
|
|
1713
1713
|
[Configuration](configuration.md).
|
|
1714
1714
|
|
|
1715
|
-
### `package-build prose lint [path]`
|
|
1715
|
+
### `package-build prose lint [path]` and `prose score [path]`
|
|
1716
1716
|
|
|
1717
1717
|
**NAME**
|
|
1718
1718
|
|
|
1719
|
-
|
|
1719
|
+
Review sentence readability or score each note against a package's prose bands.
|
|
1720
1720
|
|
|
1721
1721
|
**SYNOPSIS**
|
|
1722
1722
|
|
|
1723
1723
|
```text
|
|
1724
|
-
package-build prose lint [path] [--age <years>] [--threshold <count>] [--min-words <count>]
|
|
1724
|
+
package-build prose lint [path] [--age <years>] [--threshold <count>] [--min-words <count>] [--rules readability|simplify|all] [--fail-on-warning]
|
|
1725
|
+
package-build prose score [path] [--min-words <count>] [--fail-outside]
|
|
1725
1726
|
```
|
|
1726
1727
|
|
|
1727
1728
|
**DESCRIPTION**
|
|
1728
1729
|
|
|
1729
|
-
Run
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
`package-build lint` and the normal lint scripts do not run it.
|
|
1730
|
+
Run the `lint` or `score` action from the package root. Pass a Markdown note while editing;
|
|
1731
|
+
omit the path to read the configured `paths.content` tree. A directory path
|
|
1732
|
+
scans Markdown files recursively and honors `skipDirectories`. A file path
|
|
1733
|
+
analyzes that file alone, even in an excluded directory. Both actions read
|
|
1734
|
+
source files and write nothing. They are optional editorial checks and are
|
|
1735
|
+
outside `package-build lint` and the normal build.
|
|
1736
1736
|
|
|
1737
1737
|
Set defaults in `package-build.config.yaml` under `packageBuild.proseLint`:
|
|
1738
1738
|
|
|
1739
1739
|
```yaml
|
|
1740
1740
|
packageBuild:
|
|
1741
|
-
proseLint: { age: 21, threshold: 5, minWords: 8 }
|
|
1741
|
+
proseLint: { age: 21, threshold: 5, minWords: 8, rules: readability }
|
|
1742
|
+
proseScore:
|
|
1743
|
+
minWords: 80
|
|
1744
|
+
bands:
|
|
1745
|
+
flesch: { min: 45, max: 80 }
|
|
1746
|
+
syllablesPerWord: { max: 1.65 }
|
|
1742
1747
|
```
|
|
1743
1748
|
|
|
1744
|
-
No configuration is required
|
|
1749
|
+
No configuration is required. `proseLint` uses the defaults shown. The
|
|
1750
|
+
`proseScore` default is `minWords: 80` with no bands; the example bands express
|
|
1751
|
+
one package's editorial preference, not a toolchain standard. `age` is the
|
|
1745
1752
|
intended reader's age in years. `threshold` is the number of seven readability
|
|
1746
1753
|
algorithms that must classify a sentence as difficult before it is reported.
|
|
1747
1754
|
For example, a threshold of 5 reports a sentence if five or more algorithms
|
|
1748
|
-
flag it; a threshold of 7 reports only unanimous findings. `minWords`
|
|
1749
|
-
shorter sentences
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
expressions are excluded.
|
|
1755
|
+
flag it; a threshold of 7 reports only unanimous findings. Lint's `minWords`
|
|
1756
|
+
excludes shorter sentences. `rules: readability` reports difficult sentences;
|
|
1757
|
+
`simplify` reports wording suggestions; `all` reports both. The CLI options
|
|
1758
|
+
override individual settings. Lint numeric settings are positive integers;
|
|
1759
|
+
`threshold` is at most 7.
|
|
1760
|
+
|
|
1761
|
+
Lint analyzes Markdown paragraphs, including those in blockquotes, with `retext-english` and the selected
|
|
1762
|
+
readability or simplify rules; `retext-stringify` completes the text pipeline.
|
|
1763
|
+
Frontmatter, headings, lists, GFM tables, code blocks and inline code, image
|
|
1764
|
+
embeds, wikilinks, and inline expressions are excluded. The body of an ordinary
|
|
1765
|
+
directive such as `:::info` remains prose; SQL directive bodies are excluded.
|
|
1766
|
+
Each suggestion is a warning with a file, line,
|
|
1758
1767
|
column, rule ID, confidence, offending sentence, and `expected` replacement
|
|
1759
1768
|
list. The source location marks the difficult sentence or the specific phrase
|
|
1760
1769
|
that could be simplified. Readability confidence is the number of algorithms
|
|
1761
1770
|
that found the sentence difficult, out of seven. Simplify suggestions have no
|
|
1762
1771
|
numeric confidence score, so they say `unscored`. `expected` lists suggested
|
|
1763
1772
|
replacements; `[]` means the rule offers no replacement and may recommend
|
|
1764
|
-
deleting a word or rewriting a sentence. Suggestions are advisory
|
|
1765
|
-
applied automatically
|
|
1766
|
-
|
|
1773
|
+
deleting a word or rewriting a sentence. Suggestions are advisory and are
|
|
1774
|
+
never applied automatically. `--fail-on-warning` makes a selected rule's
|
|
1775
|
+
suggestions an explicit exit-code gate.
|
|
1776
|
+
|
|
1777
|
+
Score uses the same paragraph selection. It reports Flesch reading ease,
|
|
1778
|
+
syllables per word, mean and longest sentence length, unfamiliar-word percentage
|
|
1779
|
+
against the Dale–Chall list, and nominalizations per thousand words. A
|
|
1780
|
+
nominalization is counted by its word ending (`-tion`, `-sion`, `-ment`,
|
|
1781
|
+
`-ness`, `-ity`, `-ance`, `-ence`, or `-ism`); this is a mechanical count, not a
|
|
1782
|
+
grammar judgment. Coverage is the percentage of readable body words included
|
|
1783
|
+
in the score: headings and lists count toward the body but are not scored.
|
|
1784
|
+
Review low-coverage notes in context. A note with fewer than score's `minWords`
|
|
1785
|
+
is reported as insufficient and is not compared with bands.
|
|
1786
|
+
|
|
1787
|
+
Under `packageBuild.proseScore.bands`, any of `flesch`, `syllablesPerWord`,
|
|
1788
|
+
`meanSentenceWords`, `longestSentenceWords`, `unfamiliarWordPercent`, and
|
|
1789
|
+
`nominalizationsPer1000Words` may set `min`, `max`, or both. Limits are finite
|
|
1790
|
+
numbers and a minimum cannot exceed a maximum. Violations are warnings. The
|
|
1791
|
+
directory summary pools sentence, word, and syllable counts across sufficiently
|
|
1792
|
+
long notes before computing Flesch; it is not an average of note scores.
|
|
1793
|
+
`--fail-outside` makes band violations an explicit gate. No band is enforced
|
|
1794
|
+
until the package declares it.
|
|
1767
1795
|
|
|
1768
1796
|
**OPTIONS**
|
|
1769
1797
|
|
|
1770
|
-
| Input
|
|
1771
|
-
|
|
|
1772
|
-
| `path`
|
|
1773
|
-
| `--age`
|
|
1774
|
-
| `--threshold`
|
|
1775
|
-
| `--
|
|
1798
|
+
| Input | Default | Meaning |
|
|
1799
|
+
| ------------------- | -------------------------- | -------------------------------------------- |
|
|
1800
|
+
| `path` | configured `paths.content` | Markdown file or content tree |
|
|
1801
|
+
| `--age` | 21 | Lint reader age in years |
|
|
1802
|
+
| `--threshold` | 5 | Lint readability algorithms required (1–7) |
|
|
1803
|
+
| `--rules` | `readability` | Lint rules: `readability`, `simplify`, `all` |
|
|
1804
|
+
| `--min-words` | 8 lint; 80 score | Minimum words per sentence or note |
|
|
1805
|
+
| `--fail-on-warning` | false | Fail lint on selected rule findings |
|
|
1806
|
+
| `--fail-outside` | false | Fail score when a note crosses a band |
|
|
1776
1807
|
|
|
1777
1808
|
**EXIT STATUS**
|
|
1778
1809
|
|
|
1779
|
-
0 after
|
|
1810
|
+
0 after an advisory report; 1 for invalid options, unreadable paths, or an
|
|
1811
|
+
explicit failure condition.
|
|
1780
1812
|
|
|
1781
1813
|
**EXAMPLES**
|
|
1782
1814
|
|
|
1783
1815
|
```bash
|
|
1784
1816
|
package-build prose lint assets/content/Lore/Harbor.md
|
|
1817
|
+
package-build prose lint assets/content/Lore/Harbor.md --rules simplify
|
|
1818
|
+
package-build prose lint assets/content/Lore/Harbor.md --fail-on-warning
|
|
1785
1819
|
package-build prose lint
|
|
1786
1820
|
package-build prose lint assets/content --age 18 --threshold 6 --min-words 10
|
|
1821
|
+
package-build prose score assets/content/Lore/Harbor.md
|
|
1822
|
+
package-build prose score assets/content/Lore --fail-outside
|
|
1787
1823
|
```
|
|
1788
1824
|
|
|
1789
|
-
For a note whose fifth line says `The utilization is very high.`,
|
|
1790
|
-
can report:
|
|
1825
|
+
For a note whose fifth line says `The utilization is very high.`, lint with
|
|
1826
|
+
`--rules simplify` can report:
|
|
1791
1827
|
|
|
1792
1828
|
```text
|
|
1793
1829
|
assets/content/Lore/Harbor.md:5:5: warning: retext-simplify/utilization: confidence=unscored; sentence="The utilization is very high."; expected=["use"]; Unexpected `utilization`, use `use` instead
|
package/docs/configuration.md
CHANGED
|
@@ -1457,38 +1457,65 @@ packageBuild:
|
|
|
1457
1457
|
envPrefix: SOHL
|
|
1458
1458
|
```
|
|
1459
1459
|
|
|
1460
|
-
| Key | Type | Required | Default
|
|
1461
|
-
| ------------------------------------------------------------------------------------------ | ------------------------------------------- | -------- |
|
|
1462
|
-
| [`packageBuild.stageDir`](#packagebuildstagedir-and-packagebuildassets) | string | no | `build/stage`
|
|
1463
|
-
| [`packageBuild.assets`](#packagebuildstagedir-and-packagebuildassets) | array | no | `[]`
|
|
1464
|
-
| [`packageBuild.assetTransform`](#packagebuildassettransform-and-packagebuildmanifestflags) | string (path to a module) | no | `null`
|
|
1465
|
-
| [`packageBuild.manifest`](#packagebuildmanifest) | object, pass-through | no | `{}`
|
|
1466
|
-
| [`packageBuild.manifestFlags`](#packagebuildassettransform-and-packagebuildmanifestflags) | string (path to a module) | no | `null`
|
|
1467
|
-
| [`packageBuild.schema`](#packagebuildschema) | object (`{documentType: {from, registry}}`) | no | `[]`
|
|
1468
|
-
| [`packageBuild.clean`](#packagebuildclean) | object | no | `{extra: []}`
|
|
1469
|
-
| [`packageBuild.lang`](#packagebuildlang) | object | no | see below
|
|
1470
|
-
| [`packageBuild.deploy`](#packagebuilddeploy) | object | no | `{envPrefix: "SOHL"}`
|
|
1471
|
-
| [`packageBuild.release`](#packagebuildrelease) | object | no | `{artifact: <from packageKind>}`
|
|
1472
|
-
| [`packageBuild.bundle`](#packagebuildbundle) | object | no | `{entry: "<foundryPackage>.mjs"}`
|
|
1473
|
-
| [`packageBuild.container`](#packagebuildcontainer) | object | no | see below
|
|
1474
|
-
| [`packageBuild.e2e`](#packagebuilde2e) | object | no | see below
|
|
1475
|
-
| [`packageBuild.proseLint`](#packagebuildproselint) | object | no | `{age: 21, threshold: 5, minWords: 8}` |
|
|
1476
|
-
|
|
1477
|
-
|
|
1460
|
+
| Key | Type | Required | Default |
|
|
1461
|
+
| ------------------------------------------------------------------------------------------ | ------------------------------------------- | -------- | ---------------------------------------------------------- |
|
|
1462
|
+
| [`packageBuild.stageDir`](#packagebuildstagedir-and-packagebuildassets) | string | no | `build/stage` |
|
|
1463
|
+
| [`packageBuild.assets`](#packagebuildstagedir-and-packagebuildassets) | array | no | `[]` |
|
|
1464
|
+
| [`packageBuild.assetTransform`](#packagebuildassettransform-and-packagebuildmanifestflags) | string (path to a module) | no | `null` |
|
|
1465
|
+
| [`packageBuild.manifest`](#packagebuildmanifest) | object, pass-through | no | `{}` |
|
|
1466
|
+
| [`packageBuild.manifestFlags`](#packagebuildassettransform-and-packagebuildmanifestflags) | string (path to a module) | no | `null` |
|
|
1467
|
+
| [`packageBuild.schema`](#packagebuildschema) | object (`{documentType: {from, registry}}`) | no | `[]` |
|
|
1468
|
+
| [`packageBuild.clean`](#packagebuildclean) | object | no | `{extra: []}` |
|
|
1469
|
+
| [`packageBuild.lang`](#packagebuildlang) | object | no | see below |
|
|
1470
|
+
| [`packageBuild.deploy`](#packagebuilddeploy) | object | no | `{envPrefix: "SOHL"}` |
|
|
1471
|
+
| [`packageBuild.release`](#packagebuildrelease) | object | no | `{artifact: <from packageKind>}` |
|
|
1472
|
+
| [`packageBuild.bundle`](#packagebuildbundle) | object | no | `{entry: "<foundryPackage>.mjs"}` |
|
|
1473
|
+
| [`packageBuild.container`](#packagebuildcontainer) | object | no | see below |
|
|
1474
|
+
| [`packageBuild.e2e`](#packagebuilde2e) | object | no | see below |
|
|
1475
|
+
| [`packageBuild.proseLint`](#packagebuildproselint) | object | no | `{age: 21, threshold: 5, minWords: 8, rules: readability}` |
|
|
1476
|
+
| [`packageBuild.proseScore`](#packagebuildprosescore) | object | no | `{minWords: 80, bands: {}}` |
|
|
1477
|
+
|
|
1478
|
+
> ``package-build config: `packageBuild.<key>` is not a recognised key (expected one of: stageDir, assets, assetTransform, manifest, manifestFlags, schema, clean, lang, deploy, release, bundle, container, e2e, proseLint, proseScore).``
|
|
1478
1479
|
|
|
1479
1480
|
### `packageBuild.proseLint`
|
|
1480
1481
|
|
|
1481
|
-
Optional defaults for [`package-build prose lint`](commands.md#package-build-prose-lint-path).
|
|
1482
|
+
Optional defaults for [`package-build prose lint`](commands.md#package-build-prose-lint-path-and-prose-score-path).
|
|
1482
1483
|
`age` is the reader age, `threshold` is the number of readability algorithms
|
|
1483
1484
|
required to flag a sentence (1–7), and `minWords` is the minimum sentence
|
|
1484
|
-
length.
|
|
1485
|
-
them for one analysis.
|
|
1485
|
+
length. `rules` is `readability`, `simplify`, or `all`. Each numeric setting is
|
|
1486
|
+
a positive integer; command-line options can override them for one analysis.
|
|
1486
1487
|
|
|
1487
1488
|
```yaml
|
|
1488
1489
|
packageBuild:
|
|
1489
|
-
proseLint: { age: 21, threshold: 5, minWords: 8 }
|
|
1490
|
+
proseLint: { age: 21, threshold: 5, minWords: 8, rules: readability }
|
|
1490
1491
|
```
|
|
1491
1492
|
|
|
1493
|
+
### `packageBuild.proseScore`
|
|
1494
|
+
|
|
1495
|
+
Optional defaults for [`package-build prose score`](commands.md#package-build-prose-lint-path-and-prose-score-path).
|
|
1496
|
+
`minWords` is the minimum number of analyzed words for a note to receive a
|
|
1497
|
+
score. `bands` states the package's acceptable range for any reported metric;
|
|
1498
|
+
there are no default bands. Each band has `min`, `max`, or both, as finite
|
|
1499
|
+
numbers. An omitted bound is open. The score report stays advisory unless
|
|
1500
|
+
`--fail-outside` is requested.
|
|
1501
|
+
|
|
1502
|
+
```yaml
|
|
1503
|
+
packageBuild:
|
|
1504
|
+
proseScore:
|
|
1505
|
+
minWords: 80
|
|
1506
|
+
bands:
|
|
1507
|
+
flesch: { min: 45, max: 80 }
|
|
1508
|
+
syllablesPerWord: { max: 1.65 }
|
|
1509
|
+
meanSentenceWords: { max: 28 }
|
|
1510
|
+
longestSentenceWords: { max: 55 }
|
|
1511
|
+
unfamiliarWordPercent: { max: 35 }
|
|
1512
|
+
nominalizationsPer1000Words: { max: 30 }
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
Flesch is a reading-ease score, so a value below its minimum is harder prose
|
|
1516
|
+
and one above its maximum is simpler prose. These sample bounds are editorial
|
|
1517
|
+
choices for illustration. `--min-words` overrides `minWords` for one score run.
|
|
1518
|
+
|
|
1492
1519
|
### `packageBuild.stageDir` and `packageBuild.assets`
|
|
1493
1520
|
|
|
1494
1521
|
`stageDir` is where the package is assembled before it is zipped or
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heroiclands/package-build",
|
|
3
|
-
"version": "22.17.
|
|
3
|
+
"version": "22.17.1",
|
|
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`.
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
/** Files in a content tree, excluding configured directory names. */
|
|
2
2
|
export function proseFiles(root: any, skipDirectories?: any[]): any[];
|
|
3
3
|
/** Source-aligned findings for one Markdown note. */
|
|
4
|
-
export function analyzeProse(file: any, source: any, {
|
|
5
|
-
age?: number | undefined;
|
|
6
|
-
threshold?: number | undefined;
|
|
7
|
-
minWords?: number | undefined;
|
|
8
|
-
}): Promise<{
|
|
4
|
+
export function analyzeProse(file: any, source: any, options?: {}): Promise<{
|
|
9
5
|
file: any;
|
|
10
6
|
line: any;
|
|
11
7
|
column: any;
|
|
@@ -20,3 +16,13 @@ export function lintProse(root: any, options: any, skipDirectories?: any[]): Pro
|
|
|
20
16
|
severity: string;
|
|
21
17
|
message: string;
|
|
22
18
|
}[]>;
|
|
19
|
+
/** Score the complete prose paragraphs in one note. */
|
|
20
|
+
export function scoreProse(file: any, source: any, { minWords, bands }?: {
|
|
21
|
+
minWords?: number | undefined;
|
|
22
|
+
bands?: {} | undefined;
|
|
23
|
+
}): Promise<any>;
|
|
24
|
+
/** Score a note or content tree and pool counts for its aggregate. */
|
|
25
|
+
export function scoreProseTree(root: any, options: any, skipDirectories?: any[]): Promise<{
|
|
26
|
+
notes: any[];
|
|
27
|
+
summary: any;
|
|
28
|
+
}>;
|