@descent-vtt/spec-graph 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +275 -0
- package/README.md +169 -3
- package/dist/baseline.d.ts +18 -4
- package/dist/baseline.js +14 -2
- package/dist/baseline.js.map +1 -1
- package/dist/cli.d.ts +13 -4
- package/dist/cli.js +126 -27
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +14 -2
- package/dist/config.js +28 -1
- package/dist/config.js.map +1 -1
- package/dist/extract.d.ts +39 -2
- package/dist/extract.js +137 -8
- package/dist/extract.js.map +1 -1
- package/dist/identity.d.ts +10 -0
- package/dist/identity.js +51 -5
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +16 -2
- package/dist/markdown.js +89 -22
- package/dist/markdown.js.map +1 -1
- package/dist/project-rules.d.ts +92 -0
- package/dist/project-rules.js +201 -0
- package/dist/project-rules.js.map +1 -0
- package/dist/report.d.ts +33 -8
- package/dist/report.js +181 -7
- package/dist/report.js.map +1 -1
- package/dist/resolve.d.ts +9 -1
- package/dist/resolve.js +181 -26
- package/dist/resolve.js.map +1 -1
- package/dist/rules.d.ts +8 -5
- package/dist/rules.js +96 -5
- package/dist/rules.js.map +1 -1
- package/dist/runner.d.ts +5 -2
- package/dist/runner.js +1 -0
- package/dist/runner.js.map +1 -1
- package/dist/select.d.ts +11 -0
- package/dist/select.js +31 -0
- package/dist/select.js.map +1 -1
- package/dist/types.d.ts +59 -4
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/extract.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* broken reference to specification 256.
|
|
16
16
|
*/
|
|
17
17
|
import { attr, attrList, directiveFor, parseDirectives } from './directives.js';
|
|
18
|
-
import { identify, isExternal, normaliseRef, ID_KEYS } from './identity.js';
|
|
18
|
+
import { identify, isDocumentTarget, isExternal, looksLikePath, normaliseRef, parsePrefixedRef, withinOneEdit, ID_KEYS, } from './identity.js';
|
|
19
19
|
import { isStatusHeading, phaseFromPath, phaseOf, STATUS_KEYS, supersessionTargetsIn } from './lifecycle.js';
|
|
20
20
|
import { scanMarkdown, slugify } from './markdown.js';
|
|
21
21
|
import { findSpecificationRegions, regionAt } from './sections.js';
|
|
@@ -25,34 +25,139 @@ import { parseFrontMatter, toRecord, valuesOf } from './yaml.js';
|
|
|
25
25
|
/* -------------------------------------------------------------------------- */
|
|
26
26
|
/* Vocabulary */
|
|
27
27
|
/* -------------------------------------------------------------------------- */
|
|
28
|
-
/**
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Front-matter keys that declare a relation, and what they mean.
|
|
30
|
+
*
|
|
31
|
+
* Every directional kind is spelled in **both** directions. A register that
|
|
32
|
+
* records `depends-on` but not `depended-on-by` is not a smaller vocabulary, it
|
|
33
|
+
* is a trap: the author writes the inverse, the key means nothing, and the edge
|
|
34
|
+
* they declared is missing from a graph that reports itself as consistent. Which
|
|
35
|
+
* half of a pair a repository writes is a filing convention, and a filing
|
|
36
|
+
* convention is not something a linter gets to have an opinion about.
|
|
37
|
+
*
|
|
38
|
+
* `relates-to` is the exception, and it is not one: the relation is symmetric,
|
|
39
|
+
* so it has no other direction to spell. `contains` is structural and never
|
|
40
|
+
* written by hand. `tests/parsing.test.ts` holds both facts to the fire.
|
|
41
|
+
*/
|
|
42
|
+
export const RELATION_KEYS = {
|
|
30
43
|
supersedes: { kind: 'supersedes', inverted: false },
|
|
31
44
|
supercedes: { kind: 'supersedes', inverted: false },
|
|
32
45
|
replaces: { kind: 'supersedes', inverted: false },
|
|
33
46
|
obsoletes: { kind: 'supersedes', inverted: false },
|
|
47
|
+
deprecates: { kind: 'supersedes', inverted: false },
|
|
34
48
|
'superseded-by': { kind: 'supersedes', inverted: true },
|
|
35
49
|
'superceded-by': { kind: 'supersedes', inverted: true },
|
|
36
50
|
'replaced-by': { kind: 'supersedes', inverted: true },
|
|
37
51
|
'obsoleted-by': { kind: 'supersedes', inverted: true },
|
|
52
|
+
'deprecated-by': { kind: 'supersedes', inverted: true },
|
|
53
|
+
'rolled-into': { kind: 'supersedes', inverted: true },
|
|
38
54
|
amends: { kind: 'amends', inverted: false },
|
|
39
55
|
extends: { kind: 'amends', inverted: false },
|
|
56
|
+
refines: { kind: 'amends', inverted: false },
|
|
57
|
+
clarifies: { kind: 'amends', inverted: false },
|
|
58
|
+
revises: { kind: 'amends', inverted: false },
|
|
40
59
|
'amended-by': { kind: 'amends', inverted: true },
|
|
60
|
+
'extended-by': { kind: 'amends', inverted: true },
|
|
61
|
+
'refined-by': { kind: 'amends', inverted: true },
|
|
62
|
+
'clarified-by': { kind: 'amends', inverted: true },
|
|
63
|
+
'revised-by': { kind: 'amends', inverted: true },
|
|
41
64
|
'depends-on': { kind: 'depends-on', inverted: false },
|
|
65
|
+
'dependent-on': { kind: 'depends-on', inverted: false },
|
|
42
66
|
dependencies: { kind: 'depends-on', inverted: false },
|
|
43
67
|
requires: { kind: 'depends-on', inverted: false },
|
|
68
|
+
'builds-on': { kind: 'depends-on', inverted: false },
|
|
69
|
+
'relies-on': { kind: 'depends-on', inverted: false },
|
|
70
|
+
'depended-on-by': { kind: 'depends-on', inverted: true },
|
|
71
|
+
'required-by': { kind: 'depends-on', inverted: true },
|
|
72
|
+
dependents: { kind: 'depends-on', inverted: true },
|
|
44
73
|
assumes: { kind: 'assumes', inverted: false },
|
|
74
|
+
'assumed-by': { kind: 'assumes', inverted: true },
|
|
45
75
|
'blocked-by': { kind: 'blocked-by', inverted: false },
|
|
76
|
+
'blocked-on': { kind: 'blocked-by', inverted: false },
|
|
77
|
+
'waiting-on': { kind: 'blocked-by', inverted: false },
|
|
78
|
+
'waiting-for': { kind: 'blocked-by', inverted: false },
|
|
79
|
+
'gated-on': { kind: 'blocked-by', inverted: false },
|
|
80
|
+
'gated-by': { kind: 'blocked-by', inverted: false },
|
|
46
81
|
blocks: { kind: 'blocked-by', inverted: true },
|
|
47
82
|
'delegates-to': { kind: 'delegates-to', inverted: false },
|
|
48
83
|
'delegated-to': { kind: 'delegates-to', inverted: false },
|
|
84
|
+
'deferred-to': { kind: 'delegates-to', inverted: false },
|
|
49
85
|
'tracked-in': { kind: 'delegates-to', inverted: false },
|
|
86
|
+
'tracked-by': { kind: 'delegates-to', inverted: false },
|
|
87
|
+
'continued-in': { kind: 'delegates-to', inverted: false },
|
|
88
|
+
'delegated-from': { kind: 'delegates-to', inverted: true },
|
|
50
89
|
related: { kind: 'relates-to', inverted: false },
|
|
51
90
|
'relates-to': { kind: 'relates-to', inverted: false },
|
|
91
|
+
'related-to': { kind: 'relates-to', inverted: false },
|
|
52
92
|
'see-also': { kind: 'relates-to', inverted: false },
|
|
53
93
|
references: { kind: 'references', inverted: false },
|
|
94
|
+
reference: { kind: 'references', inverted: false },
|
|
54
95
|
refs: { kind: 'references', inverted: false },
|
|
96
|
+
'referenced-by': { kind: 'references', inverted: true },
|
|
55
97
|
};
|
|
98
|
+
/**
|
|
99
|
+
* True when a front-matter value is shaped like a citation.
|
|
100
|
+
*
|
|
101
|
+
* The second half of the gate on {@link misreadRelationKeys}, and the half that
|
|
102
|
+
* does the work. `sidebar_position: 4` and `ref: main` are a hair away from a
|
|
103
|
+
* relation key by spelling alone; neither carries anything that could name a
|
|
104
|
+
* document, and reporting them would make the rule an irritation rather than a
|
|
105
|
+
* catch (ADR-0006).
|
|
106
|
+
*/
|
|
107
|
+
function looksLikeCitation(value) {
|
|
108
|
+
const trimmed = value.trim();
|
|
109
|
+
if (trimmed.length === 0)
|
|
110
|
+
return false;
|
|
111
|
+
if (parsePrefixedRef(trimmed) !== null)
|
|
112
|
+
return true;
|
|
113
|
+
return looksLikePath(trimmed) && isDocumentTarget(trimmed) && !isExternal(trimmed);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Front-matter keys that read as relations and declare none.
|
|
117
|
+
*
|
|
118
|
+
* Front matter is an open vocabulary and most of what lives there is nobody
|
|
119
|
+
* else's business, so the bar is deliberately high: the key has to be one edit
|
|
120
|
+
* from a relation key *and* carry something that could name a document. Both
|
|
121
|
+
* conditions together are what separates `dependson: ADR-0001`, which is an edge
|
|
122
|
+
* the author believes exists, from `description: ...`, which is a title.
|
|
123
|
+
*/
|
|
124
|
+
function misreadRelationKeys(byKey, from, at) {
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const [key, entry] of byKey) {
|
|
127
|
+
const folded = foldRelationKey(key);
|
|
128
|
+
if (folded.length === 0 || RELATION_INDEX.has(folded))
|
|
129
|
+
continue;
|
|
130
|
+
if (!valuesOf(entry).some(looksLikeCitation))
|
|
131
|
+
continue;
|
|
132
|
+
const near = [...RELATION_INDEX.values()]
|
|
133
|
+
.filter((relation) => withinOneEdit(folded, foldRelationKey(relation.canonical)))
|
|
134
|
+
.map((relation) => relation.canonical);
|
|
135
|
+
if (near.length === 0)
|
|
136
|
+
continue;
|
|
137
|
+
out.push({ key, suggestion: near.join(', '), from, at: at(entry.start, entry.end) });
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Folds a front-matter key to its lookup form.
|
|
143
|
+
*
|
|
144
|
+
* `depends-on`, `depends_on`, `dependsOn` and `Depends On` are one key written
|
|
145
|
+
* four ways, and which one a repository uses is decided by whatever wrote the
|
|
146
|
+
* front matter first. Dropping every separator collapses them - the YAML reader
|
|
147
|
+
* has already lower-cased the key, which is what makes camel case fold too.
|
|
148
|
+
*
|
|
149
|
+
* Which case this folds to is arbitrary and no test pins it: both sides of every
|
|
150
|
+
* comparison come through here, so upper would work exactly as well. Asserting
|
|
151
|
+
* on it would be asserting on the implementation.
|
|
152
|
+
*/
|
|
153
|
+
export function foldRelationKey(key) {
|
|
154
|
+
return key.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
155
|
+
}
|
|
156
|
+
/** The vocabulary above, indexed by folded key. */
|
|
157
|
+
const RELATION_INDEX = new Map(Object.entries(RELATION_KEYS).map(([canonical, relation]) => [
|
|
158
|
+
foldRelationKey(canonical),
|
|
159
|
+
{ ...relation, canonical },
|
|
160
|
+
]));
|
|
56
161
|
/**
|
|
57
162
|
* Phrases that give a link its meaning, longest-matching-nearest wins.
|
|
58
163
|
*
|
|
@@ -315,6 +420,8 @@ export function extractSpecifications(input) {
|
|
|
315
420
|
return first === null ? [] : [first, ...first.subSpecifications];
|
|
316
421
|
}
|
|
317
422
|
/** Reads a file's own specification. Returns `null` when it opts out. */
|
|
423
|
+
/** The one character that makes a text file read as binary to other tools. */
|
|
424
|
+
const NUL = '\u0000';
|
|
318
425
|
export function extractDocument(input) {
|
|
319
426
|
const scanned = scanMarkdown(input.text);
|
|
320
427
|
const directives = parseDirectives(scanned.comments);
|
|
@@ -324,6 +431,22 @@ export function extractDocument(input) {
|
|
|
324
431
|
const file = input.path;
|
|
325
432
|
const problems = [];
|
|
326
433
|
const at = (start, end) => refOf(file, index, start, end);
|
|
434
|
+
const nul = input.text.indexOf(NUL);
|
|
435
|
+
if (nul !== -1) {
|
|
436
|
+
// Not a finding. Nothing about the graph is wrong, and the text parsed - a
|
|
437
|
+
// NUL is whitespace to every rule here. It is a problem with the *input*,
|
|
438
|
+
// which is what a ParseProblem is for, and it is worth one line because
|
|
439
|
+
// spec-graph is likely the only tool that got this far: grep, diff and
|
|
440
|
+
// every review interface read the file as binary and show nothing at all.
|
|
441
|
+
//
|
|
442
|
+
// The cause is almost always an encoding, not a keystroke. UTF-16 read as
|
|
443
|
+
// UTF-8 puts a NUL between every character, and the document that produces
|
|
444
|
+
// looks plausible in some editors and is mojibake everywhere else.
|
|
445
|
+
problems.push({
|
|
446
|
+
message: `contains a NUL byte, so grep, diff and review tooling read this file as binary - check whether it was saved as UTF-16`,
|
|
447
|
+
at: at(nul, nul + 1),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
327
450
|
for (const directive of directives) {
|
|
328
451
|
for (const unknown of directive.unknownAttributes) {
|
|
329
452
|
problems.push({
|
|
@@ -432,6 +555,7 @@ export function extractDocument(input) {
|
|
|
432
555
|
// Hand each region the items and references that fall inside it.
|
|
433
556
|
const ownItems = allItems.filter((item) => item.document === identity.id);
|
|
434
557
|
const ownReferences = allReferences.filter((reference) => belongsTo(reference, identity.id, allItems));
|
|
558
|
+
const misreadKeys = misreadRelationKeys(byKey, identity.id, at);
|
|
435
559
|
const anchors = new Set(scanned.headings.map((h) => h.slug));
|
|
436
560
|
const filled = subSpecifications.map((spec) => ({
|
|
437
561
|
...spec.extracted,
|
|
@@ -448,6 +572,7 @@ export function extractDocument(input) {
|
|
|
448
572
|
items: ownItems,
|
|
449
573
|
references: ownReferences,
|
|
450
574
|
problems,
|
|
575
|
+
misreadKeys,
|
|
451
576
|
anchors,
|
|
452
577
|
identity,
|
|
453
578
|
scanned,
|
|
@@ -526,6 +651,8 @@ function buildRegion(context) {
|
|
|
526
651
|
items: [],
|
|
527
652
|
references,
|
|
528
653
|
problems: [],
|
|
654
|
+
// A region has no front matter of its own to misread.
|
|
655
|
+
misreadKeys: [],
|
|
529
656
|
anchors,
|
|
530
657
|
identity,
|
|
531
658
|
scanned,
|
|
@@ -717,10 +844,12 @@ function extractReferences(context) {
|
|
|
717
844
|
const { scanned, directives, document, documentAt, items, byKey, status, claimed, file, index } = context;
|
|
718
845
|
const out = [];
|
|
719
846
|
const at = (start, end) => refOf(file, index, start, end);
|
|
720
|
-
// Front matter
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
847
|
+
// Front matter, walked in the order it was written rather than in the order
|
|
848
|
+
// the vocabulary happens to be listed: a key the table does not know still has
|
|
849
|
+
// to be looked at, and only the document knows which those are.
|
|
850
|
+
for (const [key, entry] of byKey) {
|
|
851
|
+
const relation = RELATION_INDEX.get(foldRelationKey(key));
|
|
852
|
+
if (!relation)
|
|
724
853
|
continue;
|
|
725
854
|
for (const value of valuesOf(entry)) {
|
|
726
855
|
const target = cleanTarget(value);
|
|
@@ -1081,7 +1210,7 @@ function renderLink(link) {
|
|
|
1081
1210
|
return `<${link.target}>`;
|
|
1082
1211
|
case 'reference':
|
|
1083
1212
|
case 'shortcut':
|
|
1084
|
-
return `[${link.text}][${link.
|
|
1213
|
+
return `[${link.text}][${link.label ?? ''}]`;
|
|
1085
1214
|
default:
|
|
1086
1215
|
return `[${link.text}](${link.target})`;
|
|
1087
1216
|
}
|