@descent-vtt/spec-graph 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +430 -5
- package/README.md +137 -7
- 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 +12 -4
- package/dist/cli.js +267 -59
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +45 -2
- package/dist/config.js +61 -2
- package/dist/config.js.map +1 -1
- package/dist/extract.d.ts +20 -11
- package/dist/extract.js +58 -44
- package/dist/extract.js.map +1 -1
- package/dist/identity.d.ts +10 -0
- package/dist/identity.js +37 -0
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +13 -9
- package/dist/index.js +8 -6
- package/dist/index.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/regex.d.ts +64 -0
- package/dist/regex.js +708 -0
- package/dist/regex.js.map +1 -0
- package/dist/report.d.ts +34 -7
- package/dist/report.js +172 -5
- package/dist/report.js.map +1 -1
- package/dist/resolve.d.ts +8 -1
- package/dist/resolve.js +154 -30
- package/dist/resolve.js.map +1 -1
- package/dist/rules.d.ts +21 -5
- package/dist/rules.js +92 -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 +19 -0
- package/dist/select.js +64 -14
- package/dist/select.js.map +1 -1
- package/dist/types.d.ts +36 -1
- 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, isDocumentTarget, isExternal, looksLikePath, normaliseRef, parsePrefixedRef, 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';
|
|
@@ -95,43 +95,6 @@ export const RELATION_KEYS = {
|
|
|
95
95
|
refs: { kind: 'references', inverted: false },
|
|
96
96
|
'referenced-by': { kind: 'references', inverted: true },
|
|
97
97
|
};
|
|
98
|
-
/**
|
|
99
|
-
* True when two folded keys are one edit apart.
|
|
100
|
-
*
|
|
101
|
-
* One edit, not two, and a transposition counts as one: `supercedesby` for
|
|
102
|
-
* `supercededby` is a slip of the finger, `categories` for `dependencies` is a
|
|
103
|
-
* different word. Written out rather than as a distance matrix because the
|
|
104
|
-
* answer is a yes or a no and the bound is one - the general algorithm would be
|
|
105
|
-
* more code, not less.
|
|
106
|
-
*/
|
|
107
|
-
export function withinOneEdit(a, b) {
|
|
108
|
-
if (a === b)
|
|
109
|
-
return false;
|
|
110
|
-
if (a.length === b.length) {
|
|
111
|
-
let first = -1;
|
|
112
|
-
for (let i = 0; i < a.length; i += 1) {
|
|
113
|
-
if (a[i] === b[i])
|
|
114
|
-
continue;
|
|
115
|
-
if (first === -1) {
|
|
116
|
-
first = i;
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
// A second difference is allowed only if the two are a swapped pair, and
|
|
120
|
-
// only if there is no third.
|
|
121
|
-
return first === i - 1 && a[first] === b[i] && a[i] === b[first] && a.slice(i + 1) === b.slice(i + 1);
|
|
122
|
-
}
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
const [short, long] = a.length < b.length ? [a, b] : [b, a];
|
|
126
|
-
if (long.length - short.length !== 1)
|
|
127
|
-
return false;
|
|
128
|
-
for (let i = 0; i < short.length; i += 1) {
|
|
129
|
-
if (short[i] === long[i])
|
|
130
|
-
continue;
|
|
131
|
-
return short.slice(i) === long.slice(i + 1);
|
|
132
|
-
}
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
98
|
/**
|
|
136
99
|
* True when a front-matter value is shaped like a citation.
|
|
137
100
|
*
|
|
@@ -195,6 +158,35 @@ const RELATION_INDEX = new Map(Object.entries(RELATION_KEYS).map(([canonical, re
|
|
|
195
158
|
foldRelationKey(canonical),
|
|
196
159
|
{ ...relation, canonical },
|
|
197
160
|
]));
|
|
161
|
+
/**
|
|
162
|
+
* Keys a region must answer for itself, so the file's answer cannot shadow one.
|
|
163
|
+
*
|
|
164
|
+
* Identity and status and title are the three things a register exists to vary
|
|
165
|
+
* row by row. The rest of the file's front matter describes the file, and a
|
|
166
|
+
* decision inside the file is described by it too.
|
|
167
|
+
*/
|
|
168
|
+
const UNINHERITED = new Set([...ID_KEYS, ...STATUS_KEYS, 'title', 'alias', 'aliases']);
|
|
169
|
+
/**
|
|
170
|
+
* The front matter a region inherits from the file that contains it.
|
|
171
|
+
*
|
|
172
|
+
* [ADR-0009](../docs/adr/0009-a-specification-is-a-region.md) left this open,
|
|
173
|
+
* and [ADR-0016](../docs/adr/0016-a-query-needs-a-sentence.md) made leaving it
|
|
174
|
+
* open a defect. A repository can now write `document[fm.owner!=platform]` as a
|
|
175
|
+
* rule of its own, and on a register every region answered nothing - which
|
|
176
|
+
* `!=` reads as a mismatch, because an absent value is not the value. A file
|
|
177
|
+
* whose front matter said `owner: platform` was reported twice for not being
|
|
178
|
+
* owned by platform, and the message rendered `{0.fm.owner}` as the literal
|
|
179
|
+
* placeholder. Confident, wrong, and unanswerable: the ADR-0006 failure.
|
|
180
|
+
*
|
|
181
|
+
* Two kinds of key stay behind. A relation key is a claim the file made -
|
|
182
|
+
* `supersedes:` at the top of a register supersedes on behalf of the register,
|
|
183
|
+
* not on behalf of each decision in it, and the edges were built from the
|
|
184
|
+
* file's entries before this ran. And a key the region answers itself must not
|
|
185
|
+
* be shadowed, which is {@link UNINHERITED}.
|
|
186
|
+
*/
|
|
187
|
+
export function inheritableFrontMatter(entries) {
|
|
188
|
+
return toRecord(entries.filter((entry) => !UNINHERITED.has(entry.key) && !RELATION_INDEX.has(foldRelationKey(entry.key))));
|
|
189
|
+
}
|
|
198
190
|
/**
|
|
199
191
|
* Phrases that give a link its meaning, longest-matching-nearest wins.
|
|
200
192
|
*
|
|
@@ -457,6 +449,8 @@ export function extractSpecifications(input) {
|
|
|
457
449
|
return first === null ? [] : [first, ...first.subSpecifications];
|
|
458
450
|
}
|
|
459
451
|
/** Reads a file's own specification. Returns `null` when it opts out. */
|
|
452
|
+
/** The one character that makes a text file read as binary to other tools. */
|
|
453
|
+
const NUL = '\u0000';
|
|
460
454
|
export function extractDocument(input) {
|
|
461
455
|
const scanned = scanMarkdown(input.text);
|
|
462
456
|
const directives = parseDirectives(scanned.comments);
|
|
@@ -466,6 +460,22 @@ export function extractDocument(input) {
|
|
|
466
460
|
const file = input.path;
|
|
467
461
|
const problems = [];
|
|
468
462
|
const at = (start, end) => refOf(file, index, start, end);
|
|
463
|
+
const nul = input.text.indexOf(NUL);
|
|
464
|
+
if (nul !== -1) {
|
|
465
|
+
// Not a finding. Nothing about the graph is wrong, and the text parsed - a
|
|
466
|
+
// NUL is whitespace to every rule here. It is a problem with the *input*,
|
|
467
|
+
// which is what a ParseProblem is for, and it is worth one line because
|
|
468
|
+
// spec-graph is likely the only tool that got this far: grep, diff and
|
|
469
|
+
// every review interface read the file as binary and show nothing at all.
|
|
470
|
+
//
|
|
471
|
+
// The cause is almost always an encoding, not a keystroke. UTF-16 read as
|
|
472
|
+
// UTF-8 puts a NUL between every character, and the document that produces
|
|
473
|
+
// looks plausible in some editors and is mojibake everywhere else.
|
|
474
|
+
problems.push({
|
|
475
|
+
message: `contains a NUL byte, so grep, diff and review tooling read this file as binary - check whether it was saved as UTF-16`,
|
|
476
|
+
at: at(nul, nul + 1),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
469
479
|
for (const directive of directives) {
|
|
470
480
|
for (const unknown of directive.unknownAttributes) {
|
|
471
481
|
problems.push({
|
|
@@ -478,6 +488,7 @@ export function extractDocument(input) {
|
|
|
478
488
|
const byKey = new Map();
|
|
479
489
|
for (const entry of entries)
|
|
480
490
|
byKey.set(entry.key, entry);
|
|
491
|
+
const inherited = inheritableFrontMatter(entries);
|
|
481
492
|
const h1 = scanned.headings.find((h) => h.level === 1) ?? null;
|
|
482
493
|
const frontMatterId = firstValue(byKey, ID_KEYS) ?? null;
|
|
483
494
|
const identityOf = (declaredId, directive) => identify({
|
|
@@ -544,6 +555,7 @@ export function extractDocument(input) {
|
|
|
544
555
|
file,
|
|
545
556
|
pathPhase,
|
|
546
557
|
record,
|
|
558
|
+
inherited,
|
|
547
559
|
}));
|
|
548
560
|
const ownerAt = (offset) => {
|
|
549
561
|
const region = regionAt(regions, offset);
|
|
@@ -612,12 +624,13 @@ function belongsTo(reference, documentId, items) {
|
|
|
612
624
|
* It produces exactly the `DocumentNode` a whole file produces, which is the
|
|
613
625
|
* reason every rule, query and reporter works on registers without knowing they
|
|
614
626
|
* exist. The only deliberate differences: the node is anchored at its heading
|
|
615
|
-
* rather than at line one, it carries
|
|
616
|
-
* not claim the file's path as an alias - the
|
|
617
|
-
* answering to one path would make every link
|
|
627
|
+
* rather than at line one, it carries the file's front matter rather than front
|
|
628
|
+
* matter of its own, and it does not claim the file's path as an alias - the
|
|
629
|
+
* file already does, and two nodes answering to one path would make every link
|
|
630
|
+
* to that file ambiguous.
|
|
618
631
|
*/
|
|
619
632
|
function buildRegion(context) {
|
|
620
|
-
const { region, input, scanned, index, file, pathPhase, record } = context;
|
|
633
|
+
const { region, input, scanned, index, file, pathPhase, record, inherited } = context;
|
|
621
634
|
const at = (start, end) => refOf(file, index, start, end);
|
|
622
635
|
const identity = identify({
|
|
623
636
|
path: input.path,
|
|
@@ -648,7 +661,7 @@ function buildRegion(context) {
|
|
|
648
661
|
phase: record ? 'record' : declaredPhase !== 'unknown' ? declaredPhase : pathPhase,
|
|
649
662
|
rawStatus,
|
|
650
663
|
statusAt,
|
|
651
|
-
frontMatter:
|
|
664
|
+
frontMatter: inherited,
|
|
652
665
|
};
|
|
653
666
|
const anchors = new Set(scanned.headings.filter((h) => h.start >= region.start && h.start < region.end).map((h) => h.slug));
|
|
654
667
|
// Relations a table declares by column, typed by the header the author wrote.
|
|
@@ -670,7 +683,8 @@ function buildRegion(context) {
|
|
|
670
683
|
items: [],
|
|
671
684
|
references,
|
|
672
685
|
problems: [],
|
|
673
|
-
//
|
|
686
|
+
// The file has already reported anything misread at the top of it, and
|
|
687
|
+
// there is one file however many regions it holds.
|
|
674
688
|
misreadKeys: [],
|
|
675
689
|
anchors,
|
|
676
690
|
identity,
|