@heroiclands/package-build 9.0.0 → 10.0.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 +721 -0
- package/CONTENT.md +273 -13
- package/bin/content-build.mjs +437 -7
- package/content-config.mjs +259 -28
- package/docs/content-format.md +1418 -0
- package/engine/address-charset.mjs +62 -0
- package/engine/alias-index.mjs +153 -0
- package/engine/base-compiler.mjs +194 -4
- package/engine/content-address.mjs +4 -4
- package/engine/content-format-check.mjs +570 -0
- package/engine/content-format.mjs +253 -0
- package/engine/content-links.mjs +132 -56
- package/engine/content-lint.mjs +8 -1
- package/engine/diagnostics.mjs +33 -0
- package/engine/document-subtypes.mjs +440 -0
- package/engine/field-spec.mjs +49 -43
- package/engine/frontmatter-lint.mjs +351 -28
- package/engine/generate.mjs +32 -4
- package/engine/helpers.mjs +41 -29
- package/engine/ids.mjs +19 -1
- package/engine/index.mjs +15 -0
- package/engine/item-registry.mjs +72 -5
- package/engine/kb-manifest.mjs +36 -7
- package/engine/map-notes.mjs +34 -18
- package/engine/note-claims.mjs +383 -0
- package/engine/note-vocabulary.mjs +678 -0
- package/engine/pack-config.mjs +39 -22
- package/engine/pack-router.mjs +17 -6
- package/engine/prose-lint.mjs +55 -3
- package/engine/retired-fields.mjs +117 -3
- package/engine/scenes.mjs +19 -1
- package/engine/schema-check.mjs +347 -3
- package/engine/site-build.mjs +1 -1
- package/engine/site-index.mjs +38 -21
- package/engine/system-block.mjs +513 -0
- package/engine/web-wikilinks.mjs +112 -80
- package/engine/wikilink-syntax.mjs +30 -0
- package/engine/wikilinks.mjs +67 -51
- package/package.json +6 -2
- package/sohl/actors.mjs +249 -36
- package/sohl/document-subtypes.mjs +82 -0
- package/sohl/index.mjs +3 -0
- package/sohl/items.mjs +110 -14
- package/sohl/note-schemas.mjs +11 -7
- package/types/content-config.d.mts +48 -4
- package/types/engine/address-charset.d.mts +45 -0
- package/types/engine/alias-index.d.mts +122 -0
- package/types/engine/base-compiler.d.mts +132 -4
- package/types/engine/content-address.d.mts +2 -2
- package/types/engine/content-format-check.d.mts +163 -0
- package/types/engine/content-format.d.mts +101 -0
- package/types/engine/content-links.d.mts +16 -1
- package/types/engine/content-lint.d.mts +6 -0
- package/types/engine/diagnostics.d.mts +29 -0
- package/types/engine/document-subtypes.d.mts +233 -0
- package/types/engine/field-spec.d.mts +76 -23
- package/types/engine/frontmatter-lint.d.mts +47 -2
- package/types/engine/generate.d.mts +14 -1
- package/types/engine/helpers.d.mts +21 -13
- package/types/engine/ids.d.mts +10 -0
- package/types/engine/index.d.mts +5 -0
- package/types/engine/item-registry.d.mts +21 -2
- package/types/engine/kb-manifest.d.mts +35 -8
- package/types/engine/map-notes.d.mts +21 -11
- package/types/engine/note-claims.d.mts +113 -0
- package/types/engine/note-vocabulary.d.mts +251 -0
- package/types/engine/pack-config.d.mts +4 -3
- package/types/engine/pack-router.d.mts +4 -4
- package/types/engine/prose-lint.d.mts +6 -2
- package/types/engine/retired-fields.d.mts +73 -2
- package/types/engine/schema-check.d.mts +182 -0
- package/types/engine/system-block.d.mts +281 -0
- package/types/engine/web-wikilinks.d.mts +23 -12
- package/types/engine/wikilink-syntax.d.mts +29 -0
- package/types/sohl/actors.d.mts +62 -6
- package/types/sohl/document-subtypes.d.mts +14 -0
- package/types/sohl/index.d.mts +1 -0
- package/types/sohl/items.d.mts +21 -0
package/bin/content-build.mjs
CHANGED
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
* npx content-build package clean [pack] [entry]
|
|
40
40
|
* npx content-build docs item-fields [--out <path>] [--title <title>]
|
|
41
41
|
* npx content-build lint [root] [--no-references]
|
|
42
|
+
* npx content-build content-format schema --schema <system>=<path>
|
|
43
|
+
* npx content-build content-format fields [--fields <system>]
|
|
44
|
+
* npx content-build content-format notes [root] [--strict]
|
|
42
45
|
* npx content-build links [root] [--manifests <dir>]
|
|
43
46
|
* npx content-build format [paths..] [--write]
|
|
44
47
|
* npx content-build markdown [paths..] [--fix]
|
|
@@ -69,6 +72,12 @@ import {
|
|
|
69
72
|
import { renderItemFieldReference } from "../engine/field-reference.mjs";
|
|
70
73
|
import { lintContentTree } from "../engine/content-lint.mjs";
|
|
71
74
|
import { lintFrontmatter } from "../engine/frontmatter-lint.mjs";
|
|
75
|
+
import { loadContentFormat } from "../engine/content-format.mjs";
|
|
76
|
+
import {
|
|
77
|
+
checkDeclaredFields,
|
|
78
|
+
checkSchemaTargets,
|
|
79
|
+
measureCorpus,
|
|
80
|
+
} from "../engine/content-format-check.mjs";
|
|
72
81
|
import {
|
|
73
82
|
compareFields,
|
|
74
83
|
resolveSchemaArtifact,
|
|
@@ -79,9 +88,13 @@ import {
|
|
|
79
88
|
// set — an adventure module ships skills, beings and magic swords — so no
|
|
80
89
|
// consumer gets a subset (#19, #20).
|
|
81
90
|
import { NOTE_SCHEMAS } from "../sohl/note-schemas.mjs";
|
|
91
|
+
// The shipped declarations, so this repository can check its own specification
|
|
92
|
+
// against them without standing up a consumer's configuration (#136).
|
|
93
|
+
import { ITEM_FIELDS } from "../sohl/item-fields.mjs";
|
|
82
94
|
// The engine's own types, merged under the registry's so the vocabulary stands
|
|
83
95
|
// in a package that configures no `itemBuilders` at all (#51).
|
|
84
96
|
import { ENGINE_NOTE_SCHEMAS } from "../engine/note-schemas.mjs";
|
|
97
|
+
import { NOTE_VOCABULARY } from "../engine/note-vocabulary.mjs";
|
|
85
98
|
import { checkFormatting, lintMarkdown } from "../engine/prose-lint.mjs";
|
|
86
99
|
import { emitLinkManifest } from "../engine/manifest-emit.mjs";
|
|
87
100
|
import {
|
|
@@ -90,7 +103,12 @@ import {
|
|
|
90
103
|
formatUnaddressableFinding as formatUnaddressable,
|
|
91
104
|
} from "../engine/site-build.mjs";
|
|
92
105
|
import { auditLinks, buildLinkIndex, walkReachability } from "../engine/content-links.mjs";
|
|
93
|
-
import {
|
|
106
|
+
import {
|
|
107
|
+
emitDiagnostic,
|
|
108
|
+
positionInFrontmatter,
|
|
109
|
+
positionOfLiteral,
|
|
110
|
+
} from "../engine/diagnostics.mjs";
|
|
111
|
+
import { reportFindings } from "./report.mjs";
|
|
94
112
|
import {
|
|
95
113
|
readItemAddresses,
|
|
96
114
|
diffItemAddresses,
|
|
@@ -99,6 +117,7 @@ import {
|
|
|
99
117
|
addressFindingMessage,
|
|
100
118
|
} from "../engine/address-diff.mjs";
|
|
101
119
|
import { itemPackJsonDirs } from "../engine/generate.mjs";
|
|
120
|
+
import { walkMarkdownTree } from "../engine/helpers.mjs";
|
|
102
121
|
import {
|
|
103
122
|
formatUnaddressableFinding,
|
|
104
123
|
unaddressableForeignPackages,
|
|
@@ -170,11 +189,19 @@ function reportFailure(err) {
|
|
|
170
189
|
else log.error(message);
|
|
171
190
|
}
|
|
172
191
|
|
|
192
|
+
/**
|
|
193
|
+
* The declaration sets this package ships, addressable by system id.
|
|
194
|
+
*
|
|
195
|
+
* @type {Record<string, Record<string, readonly object[]>>}
|
|
196
|
+
*/
|
|
197
|
+
const SHIPPED_ITEM_FIELDS = { sohl: ITEM_FIELDS };
|
|
198
|
+
|
|
173
199
|
const argv = yargs(hideBin(process.argv))
|
|
174
200
|
.command(packageCommand())
|
|
175
201
|
.command(depsCommand())
|
|
176
202
|
.command(docsCommand())
|
|
177
203
|
.command(lintCommand())
|
|
204
|
+
.command(contentFormatCommand())
|
|
178
205
|
.command(linksCommand())
|
|
179
206
|
.command(formatCommand())
|
|
180
207
|
.command(markdownCommand())
|
|
@@ -315,6 +342,319 @@ function docsCommand() {
|
|
|
315
342
|
};
|
|
316
343
|
}
|
|
317
344
|
|
|
345
|
+
/**
|
|
346
|
+
* `content-build content-format` — check the format specification itself (#130).
|
|
347
|
+
*
|
|
348
|
+
* Two checks, because the specification makes claims about two different
|
|
349
|
+
* worlds, and they fail for different reasons and at different times:
|
|
350
|
+
*
|
|
351
|
+
* - `schema` compares every `system.*` target the document names against the
|
|
352
|
+
* naming system's published `schema.json`. A failure means the specification
|
|
353
|
+
* and the system disagree, which is a defect in one of the two.
|
|
354
|
+
* - `fields` compares the per-type tables against the field declarations that
|
|
355
|
+
* compile them, so the hand-written half cannot drift from the generated one
|
|
356
|
+
* (#136).
|
|
357
|
+
* - `notes` measures a content tree against the vocabulary the document
|
|
358
|
+
* declares per type. During #127 it is the migration's progress bar rather
|
|
359
|
+
* than a gate, so it **reports** by default and `--strict` makes it fatal —
|
|
360
|
+
* turned on one class at a time as each slice lands.
|
|
361
|
+
*
|
|
362
|
+
* Both read the committed document rather than a transcription of it, so
|
|
363
|
+
* editing `docs/content-format.md` changes what they assert.
|
|
364
|
+
*
|
|
365
|
+
* @returns {object} The yargs command module.
|
|
366
|
+
*/
|
|
367
|
+
// eslint-disable-next-line
|
|
368
|
+
function contentFormatCommand() {
|
|
369
|
+
return {
|
|
370
|
+
command: "content-format <action>",
|
|
371
|
+
describe: "Check the content format specification against schemas and notes",
|
|
372
|
+
builder: (yargs) =>
|
|
373
|
+
yargs
|
|
374
|
+
.command(contentFormatSchemaCommand())
|
|
375
|
+
.command(contentFormatFieldsCommand())
|
|
376
|
+
.command(contentFormatNotesCommand())
|
|
377
|
+
.demandCommand(1, "Name an action.")
|
|
378
|
+
.strict(),
|
|
379
|
+
handler: () => {},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The parsed specification a `content-format` action should read.
|
|
385
|
+
*
|
|
386
|
+
* @param {object} argv - The parsed command line.
|
|
387
|
+
* @returns {import("../engine/content-format.mjs").ContentFormat} The document.
|
|
388
|
+
*/
|
|
389
|
+
function specFrom(argv) {
|
|
390
|
+
return argv.spec ? loadContentFormat(path.resolve(argv.spec)) : loadContentFormat();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* `content-format schema` — every `system.*` target, against a published schema.
|
|
395
|
+
*
|
|
396
|
+
* The schemas are named on the command line as `<system>=<path>`, because a
|
|
397
|
+
* consumer holds one and this repository holds a committed fixture, and neither
|
|
398
|
+
* arrangement should be the one the other has to pretend to. A system the
|
|
399
|
+
* document maps onto but no schema was supplied for is reported as unchecked —
|
|
400
|
+
* HM3 publishes no artifact today, so that is the ordinary case for a fifth of
|
|
401
|
+
* the claims, and a check that quietly skipped them would read as one that
|
|
402
|
+
* passed.
|
|
403
|
+
*
|
|
404
|
+
* @returns {object} The yargs command module.
|
|
405
|
+
*/
|
|
406
|
+
function contentFormatSchemaCommand() {
|
|
407
|
+
return {
|
|
408
|
+
command: "schema",
|
|
409
|
+
describe: "Check every `system.*` target the format names against a published schema.json",
|
|
410
|
+
builder: (yargs) => {
|
|
411
|
+
yargs.option("spec", {
|
|
412
|
+
describe:
|
|
413
|
+
"The specification to read. Defaults to the docs/content-format.md this package ships.",
|
|
414
|
+
type: "string",
|
|
415
|
+
});
|
|
416
|
+
yargs.option("schema", {
|
|
417
|
+
describe:
|
|
418
|
+
"A published schema, as `<system>=<path>`. Repeatable; a system with none is reported unchecked.",
|
|
419
|
+
type: "string",
|
|
420
|
+
array: true,
|
|
421
|
+
demandOption: true,
|
|
422
|
+
});
|
|
423
|
+
},
|
|
424
|
+
handler: (argv) => {
|
|
425
|
+
try {
|
|
426
|
+
const format = specFrom(argv);
|
|
427
|
+
/** @type {Record<string, object>} */
|
|
428
|
+
const schemas = {};
|
|
429
|
+
for (const entry of argv.schema) {
|
|
430
|
+
const at = String(entry).indexOf("=");
|
|
431
|
+
if (at <= 0) {
|
|
432
|
+
log.error(`--schema takes \`<system>=<path>\`, not "${entry}".`);
|
|
433
|
+
process.exitCode = 1;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const system = entry.slice(0, at);
|
|
437
|
+
const file = path.resolve(entry.slice(at + 1));
|
|
438
|
+
schemas[system] = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const { findings, checked, unchecked } = checkSchemaTargets({ format, schemas });
|
|
442
|
+
for (const finding of findings) emitDiagnostic(finding);
|
|
443
|
+
for (const [system, count] of Object.entries(unchecked)) {
|
|
444
|
+
log.info(
|
|
445
|
+
`${count} claim(s) about ${system} are unchecked — no ` +
|
|
446
|
+
`schema was supplied for it, so nothing here confirms them.`,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
if (findings.length) {
|
|
450
|
+
log.error(
|
|
451
|
+
`${findings.length} of ${checked} checked claim(s) name a ` +
|
|
452
|
+
`field no schema declares.`,
|
|
453
|
+
);
|
|
454
|
+
process.exitCode = 1;
|
|
455
|
+
} else {
|
|
456
|
+
log.info(`${checked} mapping claim(s) confirmed against the supplied schemas.`);
|
|
457
|
+
}
|
|
458
|
+
} catch (err) {
|
|
459
|
+
reportFailure(err);
|
|
460
|
+
process.exitCode = 1;
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* The field declarations a `content-format fields` run should compare against.
|
|
468
|
+
*
|
|
469
|
+
* Either this package's own shipped registry, named on the command line, or the
|
|
470
|
+
* consuming repository's resolved configuration. Both are real arrangements and
|
|
471
|
+
* neither should have to pretend to be the other: this repository ships the
|
|
472
|
+
* specification *and* the SoHL declarations and configures no content tree,
|
|
473
|
+
* while a consumer configures `itemBuilders` and resolves the specification
|
|
474
|
+
* from its toolchain.
|
|
475
|
+
*
|
|
476
|
+
* @param {object} argv - The parsed command line.
|
|
477
|
+
* @returns {{itemFields: Record<string, readonly object[]>, system: string}}
|
|
478
|
+
* The declarations, and the system column of the mapping tables they compile.
|
|
479
|
+
*/
|
|
480
|
+
function declarationsFrom(argv) {
|
|
481
|
+
if (argv.fields) return { itemFields: SHIPPED_ITEM_FIELDS[argv.fields], system: argv.fields };
|
|
482
|
+
const config = loadPackConfig();
|
|
483
|
+
const system = config.stats?.systemId;
|
|
484
|
+
if (!system) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
"package-build: this repository's configuration names no system, so " +
|
|
487
|
+
"nothing says which column of the format's mapping tables its " +
|
|
488
|
+
"`itemBuilders` declarations compile. Name a shipped set with " +
|
|
489
|
+
"`--fields <system>` instead.",
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return { itemFields: config.itemFields ?? {}, system };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* `content-format fields` — the per-type tables, against the declarations.
|
|
497
|
+
*
|
|
498
|
+
* The specification hand-writes a `data` table under most of its type sections,
|
|
499
|
+
* covering ground {@link module:engine/field-reference} already generates from
|
|
500
|
+
* the `fields` on each `itemBuilders` entry — the duplication that module exists
|
|
501
|
+
* to prevent, one document over (#136).
|
|
502
|
+
*
|
|
503
|
+
* **Checked, not merged.** The document's vocabulary spans note types that
|
|
504
|
+
* produce Scenes, Macros and JournalEntries, which no item registry covers, so
|
|
505
|
+
* there is no wholesale generation to fall back on. What the two *can* be held
|
|
506
|
+
* to is agreement where they both speak: a mapping row saying `data.weight`
|
|
507
|
+
* reaches `system.weightBase` and a declaration writing `weight` to `weightBase`
|
|
508
|
+
* are one statement made twice, and a rename that moves only one of them is a
|
|
509
|
+
* defect. A type only one side describes is named as out of reach rather than
|
|
510
|
+
* skipped in silence.
|
|
511
|
+
*
|
|
512
|
+
* @returns {object} The yargs command module.
|
|
513
|
+
*/
|
|
514
|
+
function contentFormatFieldsCommand() {
|
|
515
|
+
return {
|
|
516
|
+
command: "fields",
|
|
517
|
+
describe: "Check the format's per-type tables against the field declarations",
|
|
518
|
+
builder: (yargs) => {
|
|
519
|
+
yargs.option("spec", {
|
|
520
|
+
describe:
|
|
521
|
+
"The specification to read. Defaults to the docs/content-format.md this package ships.",
|
|
522
|
+
type: "string",
|
|
523
|
+
});
|
|
524
|
+
yargs.option("fields", {
|
|
525
|
+
describe:
|
|
526
|
+
"A declaration set this package ships, by system id. Defaults to the consuming repository's own `itemBuilders`.",
|
|
527
|
+
type: "string",
|
|
528
|
+
choices: Object.keys(SHIPPED_ITEM_FIELDS),
|
|
529
|
+
});
|
|
530
|
+
yargs.option("coverage", {
|
|
531
|
+
describe:
|
|
532
|
+
"List, per type, the fields only one side names. They are not findings — the two vocabularies differ by design until #127 lands.",
|
|
533
|
+
type: "boolean",
|
|
534
|
+
default: false,
|
|
535
|
+
});
|
|
536
|
+
},
|
|
537
|
+
handler: (argv) => {
|
|
538
|
+
try {
|
|
539
|
+
const format = specFrom(argv);
|
|
540
|
+
const { itemFields, system } = declarationsFrom(argv);
|
|
541
|
+
const result = checkDeclaredFields({ format, itemFields, system });
|
|
542
|
+
for (const finding of result.findings) emitDiagnostic(finding);
|
|
543
|
+
|
|
544
|
+
// Named rather than left implicit: a check that silently
|
|
545
|
+
// compared nine of twenty-two types would read as one that
|
|
546
|
+
// covered them all.
|
|
547
|
+
log.info(
|
|
548
|
+
`${result.checked.length} type(s) compared against ${system}'s ` +
|
|
549
|
+
`declarations (${result.fields} field pair(s)).`,
|
|
550
|
+
);
|
|
551
|
+
log.info(
|
|
552
|
+
`${result.skipped.spec.length} type(s) the format declares are ` +
|
|
553
|
+
`out of reach — no \`itemBuilders\` entry covers them: ` +
|
|
554
|
+
`${result.skipped.spec.join(", ")}.`,
|
|
555
|
+
);
|
|
556
|
+
if (result.skipped.registry.length) {
|
|
557
|
+
log.info(
|
|
558
|
+
`${result.skipped.registry.length} declared type(s) the ` +
|
|
559
|
+
`format names no section for: ` +
|
|
560
|
+
`${result.skipped.registry.join(", ")}.`,
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
if (argv.coverage) {
|
|
564
|
+
for (const entry of result.coverage) {
|
|
565
|
+
log.info(
|
|
566
|
+
`${entry.type}: format only [${entry.specOnly.join(", ")}], ` +
|
|
567
|
+
`declaration only [${entry.registryOnly.join(", ")}]`,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (result.findings.length) {
|
|
573
|
+
log.error(
|
|
574
|
+
`${result.findings.length} of ${result.fields} compared field ` +
|
|
575
|
+
`pair(s) disagree between the specification and the ` +
|
|
576
|
+
`declaration that compiles them.`,
|
|
577
|
+
);
|
|
578
|
+
process.exitCode = 1;
|
|
579
|
+
}
|
|
580
|
+
} catch (err) {
|
|
581
|
+
reportFailure(err);
|
|
582
|
+
process.exitCode = 1;
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* `content-format notes` — a content tree, against the declared vocabulary.
|
|
590
|
+
*
|
|
591
|
+
* **A report, not a gate.** Every authored note predates the format, so a
|
|
592
|
+
* failing check would be red in every repository from the day it lands and
|
|
593
|
+
* would stay red for the length of #127 — which is a check nobody can act on.
|
|
594
|
+
* `--strict` raises the findings to errors, and #127 turns it on slice by
|
|
595
|
+
* slice as each class of finding reaches zero.
|
|
596
|
+
*
|
|
597
|
+
* @returns {object} The yargs command module.
|
|
598
|
+
*/
|
|
599
|
+
function contentFormatNotesCommand() {
|
|
600
|
+
return {
|
|
601
|
+
command: "notes [root]",
|
|
602
|
+
describe: "Measure a content tree against the vocabulary the format declares (a report)",
|
|
603
|
+
builder: (yargs) => {
|
|
604
|
+
yargs.positional("root", {
|
|
605
|
+
describe: "Content tree to measure. Defaults to the configured contentBase.",
|
|
606
|
+
type: "string",
|
|
607
|
+
});
|
|
608
|
+
yargs.option("spec", {
|
|
609
|
+
describe:
|
|
610
|
+
"The specification to read. Defaults to the docs/content-format.md this package ships.",
|
|
611
|
+
type: "string",
|
|
612
|
+
});
|
|
613
|
+
yargs.option("strict", {
|
|
614
|
+
describe:
|
|
615
|
+
"Fail on the findings instead of reporting them. Turned on per slice of #127, as each class reaches zero.",
|
|
616
|
+
type: "boolean",
|
|
617
|
+
default: false,
|
|
618
|
+
});
|
|
619
|
+
},
|
|
620
|
+
handler: (argv) => {
|
|
621
|
+
try {
|
|
622
|
+
const config = loadPackConfig();
|
|
623
|
+
const root = argv.root ?? config.paths.content;
|
|
624
|
+
const format = specFrom(argv);
|
|
625
|
+
|
|
626
|
+
const notes = [];
|
|
627
|
+
for (const { frontmatter, absPath } of walkMarkdownTree(root, {
|
|
628
|
+
skipDirectories: config.skipDirectories,
|
|
629
|
+
})) {
|
|
630
|
+
if (!frontmatter || typeof frontmatter.type !== "string") continue;
|
|
631
|
+
notes.push({
|
|
632
|
+
file: absPath,
|
|
633
|
+
fm: frontmatter,
|
|
634
|
+
raw: fs.readFileSync(absPath, "utf8"),
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const { findings, byClass } = measureCorpus(notes, format, {
|
|
639
|
+
strict: argv.strict,
|
|
640
|
+
});
|
|
641
|
+
for (const finding of findings) emitDiagnostic(finding);
|
|
642
|
+
|
|
643
|
+
const counts = Object.entries(byClass).sort(([a], [b]) => (a < b ? -1 : 1));
|
|
644
|
+
for (const [cls, count] of counts) log.info(`${cls}: ${count}`);
|
|
645
|
+
log.info(
|
|
646
|
+
`${findings.length} finding(s) across ${notes.length} note(s) ` +
|
|
647
|
+
`measured against ${format.file}.`,
|
|
648
|
+
);
|
|
649
|
+
if (argv.strict && findings.length) process.exitCode = 1;
|
|
650
|
+
} catch (err) {
|
|
651
|
+
reportFailure(err);
|
|
652
|
+
process.exitCode = 1;
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
318
658
|
/**
|
|
319
659
|
* `content-build lint` — check a content tree's addresses.
|
|
320
660
|
*
|
|
@@ -368,6 +708,11 @@ function lintCommand() {
|
|
|
368
708
|
});
|
|
369
709
|
const frontmatter = lintFrontmatter(index, {
|
|
370
710
|
schemas: { ...ENGINE_NOTE_SCHEMAS, ...NOTE_SCHEMAS },
|
|
711
|
+
// The closed frontmatter regions (#128). Passed in rather
|
|
712
|
+
// than reached for, so the linter stays a checker of
|
|
713
|
+
// whatever it is handed and this stays the one place that
|
|
714
|
+
// decides which vocabulary a tree is held to.
|
|
715
|
+
vocabulary: NOTE_VOCABULARY,
|
|
371
716
|
references: argv.references,
|
|
372
717
|
});
|
|
373
718
|
|
|
@@ -445,10 +790,23 @@ function lintCommand() {
|
|
|
445
790
|
...frontmatter.findings,
|
|
446
791
|
...schemaFindings,
|
|
447
792
|
];
|
|
448
|
-
|
|
449
|
-
|
|
793
|
+
// Only an **error** fails the run. Every finding was an error
|
|
794
|
+
// until #142, so this changed nothing on the day it landed —
|
|
795
|
+
// but a field retired in favour of another is reported while
|
|
796
|
+
// both spellings still compile, and failing a build over a note
|
|
797
|
+
// that produces the correct document would red a tree that has
|
|
798
|
+
// done nothing wrong. `reportFindings` already draws exactly
|
|
799
|
+
// that line, so the rule is the shared one rather than a second
|
|
800
|
+
// copy here. Each finding names its own file.
|
|
801
|
+
const errors = reportFindings(findings, {});
|
|
802
|
+
if (errors) {
|
|
450
803
|
log.error(`${findings.length} finding(s) across ${addresses.notes} note(s).`);
|
|
451
804
|
process.exitCode = 1;
|
|
805
|
+
} else if (findings.length) {
|
|
806
|
+
log.warn(
|
|
807
|
+
`${findings.length} advisory finding(s) across ` +
|
|
808
|
+
`${addresses.notes} note(s); none fails the build.`,
|
|
809
|
+
);
|
|
452
810
|
} else {
|
|
453
811
|
log.info(
|
|
454
812
|
`Addresses and frontmatter are well-formed ` +
|
|
@@ -521,6 +879,17 @@ function formatCommand() {
|
|
|
521
879
|
`Formatted ${written.length} of ${checked} file(s).`
|
|
522
880
|
: `Already formatted (${checked} file(s)).`,
|
|
523
881
|
);
|
|
882
|
+
// `--write` collects findings too — a file Prettier cannot
|
|
883
|
+
// parse, or one that will not format to a fixpoint — and
|
|
884
|
+
// used to discard them, so a run that had left files
|
|
885
|
+
// unformatted still reported success and exited 0 (#125).
|
|
886
|
+
for (const finding of findings) emitDiagnostic(finding);
|
|
887
|
+
if (findings.length) {
|
|
888
|
+
log.error(
|
|
889
|
+
`${findings.length} of ${checked} file(s) could not be formatted.`,
|
|
890
|
+
);
|
|
891
|
+
process.exitCode = 1;
|
|
892
|
+
}
|
|
524
893
|
return;
|
|
525
894
|
}
|
|
526
895
|
for (const finding of findings) emitDiagnostic(finding);
|
|
@@ -653,6 +1022,8 @@ function linksCommand() {
|
|
|
653
1022
|
const {
|
|
654
1023
|
deadAnchors,
|
|
655
1024
|
deadAddresses,
|
|
1025
|
+
deadAliases,
|
|
1026
|
+
aliasCollisions,
|
|
656
1027
|
frontmatterLinks,
|
|
657
1028
|
homepageLinks,
|
|
658
1029
|
usedManifest,
|
|
@@ -668,14 +1039,62 @@ function linksCommand() {
|
|
|
668
1039
|
`heading in ${d.dest.rel} declares`,
|
|
669
1040
|
});
|
|
670
1041
|
}
|
|
1042
|
+
// The pipe says the author meant an address (#131), so all
|
|
1043
|
+
// three of these are errors — but they read differently
|
|
1044
|
+
// because the corrections differ.
|
|
671
1045
|
for (const d of deadAddresses) {
|
|
672
1046
|
emitDiagnostic({
|
|
673
1047
|
file: d.note.file,
|
|
674
1048
|
...positionOfLiteral(d.note.raw, d.text, d.occurrence),
|
|
675
1049
|
severity: "error",
|
|
676
|
-
message:
|
|
1050
|
+
message:
|
|
1051
|
+
d.reason === "not-an-address" ?
|
|
1052
|
+
`"${d.target}" is written as an address — the ` +
|
|
1053
|
+
`"|" says so — but it is not one; write ` +
|
|
1054
|
+
`[[type-shortcode|Text]], or drop the "|" to ` +
|
|
1055
|
+
`name it as an alias within this note's type`
|
|
1056
|
+
: d.reason === "unknown-type" ?
|
|
1057
|
+
`address [[${d.target}]] names no known ` + `content type`
|
|
1058
|
+
: `dead address [[${d.target}]] — no document ` + `has that identity`,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
// An alias that names nothing may be a worldbuilding
|
|
1062
|
+
// placeholder — a long-standing convention in the setting
|
|
1063
|
+
// trees — so it is reported and does not fail the build. The
|
|
1064
|
+
// ambiguous case is the collision below, reported at its
|
|
1065
|
+
// claimants rather than here.
|
|
1066
|
+
for (const d of deadAliases) {
|
|
1067
|
+
emitDiagnostic({
|
|
1068
|
+
file: d.note.file,
|
|
1069
|
+
...positionOfLiteral(d.note.raw, d.text, d.occurrence),
|
|
1070
|
+
severity: "warning",
|
|
1071
|
+
message:
|
|
1072
|
+
d.ambiguous ?
|
|
1073
|
+
`alias [[${d.target}]] is claimed by ` +
|
|
1074
|
+
`${d.claimants.length} ${d.note.type} notes, ` +
|
|
1075
|
+
`so it names none of them; address the ` +
|
|
1076
|
+
`intended one as [[type-shortcode|Text]]`
|
|
1077
|
+
: `unresolved alias [[${d.target}]] — no ` +
|
|
1078
|
+
`${d.note.type} note claims that name`,
|
|
677
1079
|
});
|
|
678
1080
|
}
|
|
1081
|
+
// Reported once per claimant, at the claimant, because the
|
|
1082
|
+
// note that merely cites an ambiguous alias is innocent (#13).
|
|
1083
|
+
for (const c of aliasCollisions) {
|
|
1084
|
+
const others = c.claimants.map((n) => n.rel).join(", ");
|
|
1085
|
+
for (const claimant of c.claimants) {
|
|
1086
|
+
emitDiagnostic({
|
|
1087
|
+
file: claimant.file,
|
|
1088
|
+
...positionInFrontmatter(claimant.raw, "aliases", c.alias),
|
|
1089
|
+
severity: "error",
|
|
1090
|
+
message:
|
|
1091
|
+
`alias "${c.alias}" is claimed by ` +
|
|
1092
|
+
`${c.claimants.length} ${c.type} notes ` +
|
|
1093
|
+
`(${others}), so [[${c.alias}]] names none of ` +
|
|
1094
|
+
`them; rename all but one`,
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
679
1098
|
for (const f of frontmatterLinks) {
|
|
680
1099
|
emitDiagnostic({
|
|
681
1100
|
file: f.note.file,
|
|
@@ -700,9 +1119,12 @@ function linksCommand() {
|
|
|
700
1119
|
});
|
|
701
1120
|
}
|
|
702
1121
|
|
|
1122
|
+
// Warnings are reported and do not fail: a dead alias may be a
|
|
1123
|
+
// note not yet written, which is the placeholder convention.
|
|
703
1124
|
const failures =
|
|
704
1125
|
deadAnchors.length +
|
|
705
1126
|
deadAddresses.length +
|
|
1127
|
+
aliasCollisions.reduce((n, c) => n + c.claimants.length, 0) +
|
|
706
1128
|
frontmatterLinks.length +
|
|
707
1129
|
homepageLinks.length;
|
|
708
1130
|
if (failures) {
|
|
@@ -711,10 +1133,18 @@ function linksCommand() {
|
|
|
711
1133
|
} else {
|
|
712
1134
|
log.info(
|
|
713
1135
|
`${index.notes.length} notes: every anchor link lands ` +
|
|
714
|
-
`and every
|
|
1136
|
+
`and every address resolves ` +
|
|
715
1137
|
`(${usedManifest.size} cross-package reference(s) ` +
|
|
716
|
-
`via manifest), no
|
|
717
|
-
`every homepage address
|
|
1138
|
+
`via manifest), no alias claimed twice, no ` +
|
|
1139
|
+
`wikilink in frontmatter, every homepage address ` +
|
|
1140
|
+
`resolvable.`,
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
if (deadAliases.length) {
|
|
1144
|
+
log.warn(
|
|
1145
|
+
`${deadAliases.length} unresolved alias(es) — a bare ` +
|
|
1146
|
+
`[[Name]] naming no note may be a placeholder, so ` +
|
|
1147
|
+
`these are reported rather than failed.`,
|
|
718
1148
|
);
|
|
719
1149
|
}
|
|
720
1150
|
} catch (err) {
|