@timiagama/ecr 0.0.1 → 0.1.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/LICENSE +202 -0
- package/NOTICE +16 -0
- package/README.md +411 -7
- package/dist/bin.d.ts +20 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +26 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli/corpus-loader.d.ts +198 -0
- package/dist/cli/corpus-loader.d.ts.map +1 -0
- package/dist/cli/corpus-loader.js +351 -0
- package/dist/cli/corpus-loader.js.map +1 -0
- package/dist/cli/corpus-statistics.d.ts +113 -0
- package/dist/cli/corpus-statistics.d.ts.map +1 -0
- package/dist/cli/corpus-statistics.js +119 -0
- package/dist/cli/corpus-statistics.js.map +1 -0
- package/dist/cli/diagnostic-reporter.d.ts +103 -0
- package/dist/cli/diagnostic-reporter.d.ts.map +1 -0
- package/dist/cli/diagnostic-reporter.js +254 -0
- package/dist/cli/diagnostic-reporter.js.map +1 -0
- package/dist/cli/project-ignore.d.ts +113 -0
- package/dist/cli/project-ignore.d.ts.map +1 -0
- package/dist/cli/project-ignore.js +199 -0
- package/dist/cli/project-ignore.js.map +1 -0
- package/dist/cli/safe-text.d.ts +25 -0
- package/dist/cli/safe-text.d.ts.map +1 -0
- package/dist/cli/safe-text.js +34 -0
- package/dist/cli/safe-text.js.map +1 -0
- package/dist/cli/supervisor.d.ts +108 -0
- package/dist/cli/supervisor.d.ts.map +1 -0
- package/dist/cli/supervisor.js +139 -0
- package/dist/cli/supervisor.js.map +1 -0
- package/dist/cli.d.ts +247 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +591 -0
- package/dist/cli.js.map +1 -0
- package/dist/corpus-validator.d.ts +183 -0
- package/dist/corpus-validator.d.ts.map +1 -0
- package/dist/corpus-validator.js +479 -0
- package/dist/corpus-validator.js.map +1 -0
- package/dist/document-identity-rule.d.ts +198 -0
- package/dist/document-identity-rule.d.ts.map +1 -0
- package/dist/document-identity-rule.js +244 -0
- package/dist/document-identity-rule.js.map +1 -0
- package/dist/ecr.d.ts +103 -0
- package/dist/ecr.d.ts.map +1 -0
- package/dist/ecr.js +216 -0
- package/dist/ecr.js.map +1 -0
- package/dist/glob-pattern.d.ts +94 -0
- package/dist/glob-pattern.d.ts.map +1 -0
- package/dist/glob-pattern.js +188 -0
- package/dist/glob-pattern.js.map +1 -0
- package/dist/heading-source-form.d.ts +77 -0
- package/dist/heading-source-form.d.ts.map +1 -0
- package/dist/heading-source-form.js +129 -0
- package/dist/heading-source-form.js.map +1 -0
- package/dist/identifier-grammar.d.ts +212 -0
- package/dist/identifier-grammar.d.ts.map +1 -0
- package/dist/identifier-grammar.js +273 -0
- package/dist/identifier-grammar.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/inline-reference-rule.d.ts +557 -0
- package/dist/inline-reference-rule.d.ts.map +1 -0
- package/dist/inline-reference-rule.js +802 -0
- package/dist/inline-reference-rule.js.map +1 -0
- package/dist/meta-documents.d.ts +91 -0
- package/dist/meta-documents.d.ts.map +1 -0
- package/dist/meta-documents.js +133 -0
- package/dist/meta-documents.js.map +1 -0
- package/dist/per-document-visitor.d.ts +510 -0
- package/dist/per-document-visitor.d.ts.map +1 -0
- package/dist/per-document-visitor.js +1032 -0
- package/dist/per-document-visitor.js.map +1 -0
- package/dist/references-section-rule.d.ts +482 -0
- package/dist/references-section-rule.d.ts.map +1 -0
- package/dist/references-section-rule.js +777 -0
- package/dist/references-section-rule.js.map +1 -0
- package/dist/section-hierarchy-rule.d.ts +299 -0
- package/dist/section-hierarchy-rule.d.ts.map +1 -0
- package/dist/section-hierarchy-rule.js +482 -0
- package/dist/section-hierarchy-rule.js.map +1 -0
- package/dist/source-alignment.d.ts +41 -0
- package/dist/source-alignment.d.ts.map +1 -0
- package/dist/source-alignment.js +166 -0
- package/dist/source-alignment.js.map +1 -0
- package/dist/source-lines.d.ts +69 -0
- package/dist/source-lines.d.ts.map +1 -0
- package/dist/source-lines.js +104 -0
- package/dist/source-lines.js.map +1 -0
- package/dist/spec-version.d.ts +19 -0
- package/dist/spec-version.d.ts.map +1 -0
- package/dist/spec-version.js +19 -0
- package/dist/spec-version.js.map +1 -0
- package/dist/types.d.ts +219 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/examples/docs/0. Orientation/0.0 - System Overview.md +34 -0
- package/examples/docs/0. Orientation/0.0.1 - Glossary.md +21 -0
- package/examples/docs/0. Orientation/0.0.2 - Quality Gates.md +22 -0
- package/examples/docs/3. Ingestion/3.1 - Ingestion - Validation Rules.md +33 -0
- package/examples/docs/4. Payments/4.10 - Settlement.md +22 -0
- package/examples/docs/4. Payments/4.2 - Payment Processing Contract.md +33 -0
- package/examples/docs/8. Orchestration/8.1 - Workflow Orchestration Contract.md +38 -0
- package/examples/docs/8. Orchestration/8.1.3 - Retry Policy.md +17 -0
- package/examples/docs/8. Orchestration/8.10 - Dead Letter Queue.md +20 -0
- package/examples/docs/README.md +7 -0
- package/package.json +65 -7
- package/protocol/navigation-protocol.md +223 -0
- package/spec/v2/1 - ECR - Structural Specification.md +1467 -0
- package/spec/v2/2 - ECR - User Guide.md +845 -0
- package/spec/v2/3 - Design Rationale - The Section Separator.md +243 -0
- package/src/bin.ts +30 -0
- package/src/cli/corpus-loader.ts +482 -0
- package/src/cli/corpus-statistics.ts +196 -0
- package/src/cli/diagnostic-reporter.ts +344 -0
- package/src/cli/project-ignore.ts +238 -0
- package/src/cli/safe-text.ts +39 -0
- package/src/cli/supervisor.ts +204 -0
- package/src/cli.ts +836 -0
- package/src/corpus-validator.ts +642 -0
- package/src/document-identity-rule.ts +422 -0
- package/src/ecr.ts +298 -0
- package/src/glob-pattern.ts +242 -0
- package/src/heading-source-form.ts +173 -0
- package/src/identifier-grammar.ts +380 -0
- package/src/index.ts +78 -0
- package/src/inline-reference-rule.ts +1235 -0
- package/src/meta-documents.ts +159 -0
- package/src/per-document-visitor.ts +1528 -0
- package/src/references-section-rule.ts +1182 -0
- package/src/section-hierarchy-rule.ts +705 -0
- package/src/source-alignment.ts +209 -0
- package/src/source-lines.ts +123 -0
- package/src/spec-version.ts +19 -0
- package/src/types.ts +267 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source Alignment
|
|
3
|
+
*
|
|
4
|
+
* Maps a parsed text node back to the raw Markdown it came from, character by
|
|
5
|
+
* character. The source-form checks of the navigation guarantee (1#9.11) need
|
|
6
|
+
* to know where a parsed character sits in the source, because a search reads
|
|
7
|
+
* the source: an identifier that parses correctly may be escaped, encoded or
|
|
8
|
+
* split across a line there, and no recipe would find it.
|
|
9
|
+
*
|
|
10
|
+
* Used by ECR104 for citations and by ECR103 for the relationship label of a
|
|
11
|
+
* References entry. Moved here unchanged from ECR104, where it was developed.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { decodeNamedCharacterReference } from 'decode-named-character-reference';
|
|
15
|
+
import { decodeNumericCharacterReference } from 'micromark-util-decode-numeric-character-reference';
|
|
16
|
+
|
|
17
|
+
/** A decoded character reference: its text, and how much source it occupied. */
|
|
18
|
+
interface Expansion {
|
|
19
|
+
/** The characters the reference produces. */
|
|
20
|
+
readonly text: string;
|
|
21
|
+
/** How many source characters the reference itself spans. */
|
|
22
|
+
readonly length: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A line ending as Markdown recognises it. */
|
|
26
|
+
const LINE_ENDING: RegExp = /^(?:\r\n|\n|\r)/;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The prefix a container repeats on its continuation lines.
|
|
30
|
+
*
|
|
31
|
+
* A paragraph inside a blockquote or a list item carries `> ` or indentation
|
|
32
|
+
* on every line after the first. The parser strips it; the source keeps it.
|
|
33
|
+
*/
|
|
34
|
+
const CONTINUATION_PREFIX: RegExp = /^[ \t>]*/;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whitespace the parser drops from the end of a line.
|
|
38
|
+
*
|
|
39
|
+
* Markdown discards spaces and tabs before a soft line break, so the source
|
|
40
|
+
* carries them and the parsed text does not.
|
|
41
|
+
*/
|
|
42
|
+
const TRAILING_WHITESPACE: RegExp = /^[ \t]*/;
|
|
43
|
+
|
|
44
|
+
/** A named character reference, such as `&` or `fj`. */
|
|
45
|
+
const NAMED_REFERENCE: RegExp = /^&[a-zA-Z][a-zA-Z0-9]{1,31};/;
|
|
46
|
+
|
|
47
|
+
/** A numeric character reference, decimal or hexadecimal. */
|
|
48
|
+
const NUMERIC_REFERENCE: RegExp = /^&#(?:([0-9]{1,7})|[xX]([0-9a-fA-F]{1,6}));/;
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Maps each offset of a node's parsed text to its offset in the source.
|
|
53
|
+
*
|
|
54
|
+
* A text node's source differs from its parsed value in several ways, and
|
|
55
|
+
* the ones undone here are: backslash escapes, character references, the
|
|
56
|
+
* whitespace Markdown drops before a soft line break, and the continuation
|
|
57
|
+
* prefix a blockquote or list item repeats on every line after the first.
|
|
58
|
+
* Line endings are carried through unchanged by the parser, so both sides
|
|
59
|
+
* consume their own.
|
|
60
|
+
*
|
|
61
|
+
* This list is not claimed to be exhaustive. Each construct that was missed
|
|
62
|
+
* rejected some piece of ordinary prose, so the cases covered here are the
|
|
63
|
+
* ones with fixtures, and a failure to align is reported rather than
|
|
64
|
+
* guessed at.
|
|
65
|
+
*
|
|
66
|
+
* Character references are decoded with the same packages the Markdown
|
|
67
|
+
* parser itself uses, rather than guessed at. Earlier versions inferred an
|
|
68
|
+
* expansion's length from surrounding text and were wrong in every direction:
|
|
69
|
+
* `fj` produces two characters, an unknown name produces none, and
|
|
70
|
+
* ` ` produces a line ending. Guessing is what made each round of this
|
|
71
|
+
* work reject a different piece of ordinary prose.
|
|
72
|
+
*
|
|
73
|
+
* @param source - The node's raw source
|
|
74
|
+
* @param parsed - The node's parsed text
|
|
75
|
+
* @returns Source offset for each parsed offset, or `undefined` if the two
|
|
76
|
+
* could not be aligned
|
|
77
|
+
*/
|
|
78
|
+
export function alignParsedToSource(
|
|
79
|
+
source: string,
|
|
80
|
+
parsed: string,
|
|
81
|
+
): readonly number[] | undefined {
|
|
82
|
+
const map: number[] = [];
|
|
83
|
+
let sourceIndex: number = 0;
|
|
84
|
+
let parsedIndex: number = 0;
|
|
85
|
+
|
|
86
|
+
while (parsedIndex < parsed.length) {
|
|
87
|
+
if (sourceIndex >= source.length) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const remaining: string = source.slice(sourceIndex);
|
|
92
|
+
const parsedCharacter: string = parsed.charAt(parsedIndex);
|
|
93
|
+
|
|
94
|
+
// A character reference, decoded exactly. Tested first because `&` also
|
|
95
|
+
// begins its own expansion (`&` parses to `&`), and because a
|
|
96
|
+
// reference can produce a line ending: ` ` decodes to a line feed
|
|
97
|
+
// while the source holds no line break at all.
|
|
98
|
+
const expansion: Expansion | undefined =
|
|
99
|
+
readCharacterReference(remaining);
|
|
100
|
+
|
|
101
|
+
if (expansion !== undefined) {
|
|
102
|
+
if (!parsed.startsWith(expansion.text, parsedIndex)) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (let unit = 0; unit < expansion.text.length; unit += 1) {
|
|
107
|
+
map[parsedIndex + unit] = sourceIndex;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
sourceIndex += expansion.length;
|
|
111
|
+
parsedIndex += expansion.text.length;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// A soft line break, and any container prefix on the line that follows.
|
|
116
|
+
//
|
|
117
|
+
// The parser does not normalise line endings: a CRLF document yields a
|
|
118
|
+
// text node whose value still contains CRLF. The source ending therefore
|
|
119
|
+
// decides how much is consumed, on both sides. Taking the parsed text's
|
|
120
|
+
// own ending instead was wrong twice over -- consuming one character for
|
|
121
|
+
// a two-character ending desynchronised every multi-line paragraph in a
|
|
122
|
+
// CRLF checkout, and consuming two for a one-character ending swallowed
|
|
123
|
+
// the line feed that a following ` ` had produced.
|
|
124
|
+
const parsedEnding: string | undefined = LINE_ENDING.exec(parsed.slice(parsedIndex))?.[0];
|
|
125
|
+
|
|
126
|
+
if (parsedEnding !== undefined) {
|
|
127
|
+
// Spaces and tabs before the break exist only in the source.
|
|
128
|
+
const dropped: string = TRAILING_WHITESPACE.exec(remaining)?.[0] ?? '';
|
|
129
|
+
const sourceEnding: string | undefined =
|
|
130
|
+
LINE_ENDING.exec(remaining.slice(dropped.length))?.[0];
|
|
131
|
+
|
|
132
|
+
if (sourceEnding === undefined) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Consume exactly what the source spent, not what the parsed text
|
|
137
|
+
// could match. The two differ when a literal CR is followed by an
|
|
138
|
+
// encoded line feed: the parsed value then holds `CR LF`, of which
|
|
139
|
+
// only the CR came from this line ending. Taking both would swallow
|
|
140
|
+
// the entity's own character and strand the reference that produced it.
|
|
141
|
+
if (!parsed.startsWith(sourceEnding, parsedIndex)) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (let unit = 0; unit < sourceEnding.length; unit += 1) {
|
|
146
|
+
map[parsedIndex + unit] = sourceIndex;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
sourceIndex += dropped.length + sourceEnding.length;
|
|
150
|
+
sourceIndex +=
|
|
151
|
+
CONTINUATION_PREFIX.exec(source.slice(sourceIndex))?.[0].length ?? 0;
|
|
152
|
+
parsedIndex += sourceEnding.length;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// A backslash escape: two source characters produce one.
|
|
157
|
+
if (remaining.startsWith(`\\${parsedCharacter}`)) {
|
|
158
|
+
map[parsedIndex] = sourceIndex;
|
|
159
|
+
sourceIndex += 2;
|
|
160
|
+
parsedIndex += 1;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (remaining.startsWith(parsedCharacter)) {
|
|
165
|
+
map[parsedIndex] = sourceIndex;
|
|
166
|
+
sourceIndex += 1;
|
|
167
|
+
parsedIndex += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return map;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Decodes a character reference at the start of `remaining`, if there is one.
|
|
179
|
+
*
|
|
180
|
+
* An unrecognised name is not a reference at all -- `&NotAnEntity;` stays
|
|
181
|
+
* literal -- and is reported as such by returning `undefined`, so the
|
|
182
|
+
* ordinary literal comparison handles it.
|
|
183
|
+
*
|
|
184
|
+
* @param remaining - Source text beginning at the candidate reference
|
|
185
|
+
* @returns The decoded text and the reference's source length, or `undefined`
|
|
186
|
+
*/
|
|
187
|
+
function readCharacterReference(remaining: string): Expansion | undefined {
|
|
188
|
+
const numeric: RegExpExecArray | null = NUMERIC_REFERENCE.exec(remaining);
|
|
189
|
+
|
|
190
|
+
if (numeric !== null) {
|
|
191
|
+
const decimal: string | undefined = numeric[1];
|
|
192
|
+
const value: string = decimal ?? numeric[2] ?? '';
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
text: decodeNumericCharacterReference(value, decimal === undefined ? 16 : 10),
|
|
196
|
+
length: numeric[0].length,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const named: RegExpExecArray | null = NAMED_REFERENCE.exec(remaining);
|
|
201
|
+
|
|
202
|
+
if (named === null) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const decoded: string | false = decodeNamedCharacterReference(named[0].slice(1, -1));
|
|
207
|
+
|
|
208
|
+
return decoded === false ? undefined : { text: decoded, length: named[0].length };
|
|
209
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source Lines
|
|
3
|
+
*
|
|
4
|
+
* Reads a document's raw Markdown by line, for the source-form checks of the
|
|
5
|
+
* navigation guarantee (1#9.11). The published recipes search raw text line
|
|
6
|
+
* by line, so the rules that promise a recipe will find something have to
|
|
7
|
+
* look at the same lines the recipe does.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Position, PositionRange } from './types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Records where each line of a source begins.
|
|
14
|
+
*
|
|
15
|
+
* Markdown treats LF, CRLF and a lone CR as line endings. Indexing only LF
|
|
16
|
+
* left a CR-only document with a single line start, so every offset beyond
|
|
17
|
+
* the first line failed to resolve and source validation was silently
|
|
18
|
+
* skipped -- exactly the documents that most need it.
|
|
19
|
+
*
|
|
20
|
+
* @param sourceText - The document's raw Markdown
|
|
21
|
+
* @returns Absolute offset of the first character of each line
|
|
22
|
+
*/
|
|
23
|
+
export function indexLineStarts(sourceText: string): readonly number[] {
|
|
24
|
+
const starts: number[] = [0];
|
|
25
|
+
|
|
26
|
+
for (let index = 0; index < sourceText.length; index += 1) {
|
|
27
|
+
const character: string = sourceText.charAt(index);
|
|
28
|
+
|
|
29
|
+
if (character === '\r') {
|
|
30
|
+
const isCarriageReturnLineFeed: boolean = sourceText.charAt(index + 1) === '\n';
|
|
31
|
+
|
|
32
|
+
starts.push(index + (isCarriageReturnLineFeed ? 2 : 1));
|
|
33
|
+
|
|
34
|
+
if (isCarriageReturnLineFeed) {
|
|
35
|
+
index += 1;
|
|
36
|
+
}
|
|
37
|
+
} else if (character === '\n') {
|
|
38
|
+
starts.push(index + 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return starts;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A document's raw source, addressable by line.
|
|
47
|
+
*/
|
|
48
|
+
export class SourceLines {
|
|
49
|
+
/** The document's raw Markdown. */
|
|
50
|
+
private readonly sourceText: string;
|
|
51
|
+
|
|
52
|
+
/** Absolute offset at which each line begins. */
|
|
53
|
+
private readonly lineStarts: readonly number[];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Indexes a source for line access.
|
|
57
|
+
*
|
|
58
|
+
* @param sourceText - The document's raw Markdown
|
|
59
|
+
*/
|
|
60
|
+
public constructor(sourceText: string) {
|
|
61
|
+
this.sourceText = sourceText;
|
|
62
|
+
this.lineStarts = indexLineStarts(sourceText);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns one line of the source, without its line ending.
|
|
67
|
+
*
|
|
68
|
+
* @param line - Zero-based line number
|
|
69
|
+
* @returns The line's text, or `undefined` when the line does not exist
|
|
70
|
+
*/
|
|
71
|
+
public readLine(line: number): string | undefined {
|
|
72
|
+
const start: number | undefined = this.lineStarts[line];
|
|
73
|
+
|
|
74
|
+
if (start === undefined) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const next: number = this.lineStarts[line + 1] ?? this.sourceText.length;
|
|
79
|
+
|
|
80
|
+
return this.sourceText.slice(start, next).replace(/(?:\r\n|\n|\r)$/, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Reports whether a Markdown line also begins a line for a search engine.
|
|
85
|
+
*
|
|
86
|
+
* Markdown ends a line at LF, CRLF or a lone CR; ripgrep and grep end one
|
|
87
|
+
* only at LF. So a line that follows a lone CR is, to a search, the middle
|
|
88
|
+
* of the line before it, and no recipe anchored with `^` can match it.
|
|
89
|
+
*
|
|
90
|
+
* @param line - Zero-based Markdown line number
|
|
91
|
+
* @returns `true` when the line starts the file or follows an LF
|
|
92
|
+
*/
|
|
93
|
+
public tellStartsSearchLine(line: number): boolean {
|
|
94
|
+
const start: number | undefined = this.lineStarts[line];
|
|
95
|
+
|
|
96
|
+
return start === 0 || (start !== undefined && this.sourceText.charAt(start - 1) === '\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Returns the source a range covers.
|
|
101
|
+
*
|
|
102
|
+
* @param range - A range within the document
|
|
103
|
+
* @returns Its raw source, or `undefined` when it lies outside the document
|
|
104
|
+
*/
|
|
105
|
+
public slice(range: PositionRange): string | undefined {
|
|
106
|
+
const start: number | undefined = this.offsetOf(range.start);
|
|
107
|
+
const end: number | undefined = this.offsetOf(range.end);
|
|
108
|
+
|
|
109
|
+
return start === undefined || end === undefined ? undefined : this.sourceText.slice(start, end);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Converts a line/character position into an absolute source offset.
|
|
114
|
+
*
|
|
115
|
+
* @param position - A position within the document
|
|
116
|
+
* @returns The offset, or `undefined` when the line is out of range
|
|
117
|
+
*/
|
|
118
|
+
public offsetOf(position: Position): number | undefined {
|
|
119
|
+
const lineStart: number | undefined = this.lineStarts[position.line];
|
|
120
|
+
|
|
121
|
+
return lineStart === undefined ? undefined : lineStart + position.character;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specification Version
|
|
3
|
+
*
|
|
4
|
+
* The version of the ECR specification this package implements.
|
|
5
|
+
*
|
|
6
|
+
* The specification and the package are versioned independently: a linter bug
|
|
7
|
+
* fix is a package release that leaves the grammar untouched, and a
|
|
8
|
+
* specification clarification may need no package release at all. Tying the
|
|
9
|
+
* two numbers together would force one to move whenever the other did.
|
|
10
|
+
*
|
|
11
|
+
* This constant is the single source of truth for the implemented version. A
|
|
12
|
+
* test asserts that the specification document and the README state the same
|
|
13
|
+
* number, so the three cannot drift apart.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The ECR specification version implemented by this package.
|
|
18
|
+
*/
|
|
19
|
+
export const ECR_SPEC_VERSION: string = '2.0.0';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECR Output Schema Types
|
|
3
|
+
*
|
|
4
|
+
* Derived from section 1#10 of the ECR Structural Specification (doc 1).
|
|
5
|
+
*
|
|
6
|
+
* These types define the normative output schema produced by the ECR linter.
|
|
7
|
+
* The schema is host-agnostic and designed for consumption by graph engines,
|
|
8
|
+
* Language Servers, Obsidian plugins, and other host environments.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Identity Type Aliases (per 1#9.2)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Stable document identity.
|
|
17
|
+
*
|
|
18
|
+
* Grammar: `Digit+ ("." Digit+)*`
|
|
19
|
+
*
|
|
20
|
+
* @example "3.1"
|
|
21
|
+
*/
|
|
22
|
+
export type DocID = string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Stable section identity.
|
|
26
|
+
*
|
|
27
|
+
* Grammar: `DocID "#" SectionPath`, where `SectionPath ::= Digit+ ("." Digit+)*`
|
|
28
|
+
*
|
|
29
|
+
* The text before the `#` is the owning document's DocID.
|
|
30
|
+
*
|
|
31
|
+
* @example "3.1#2"
|
|
32
|
+
*/
|
|
33
|
+
export type SectionID = string;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Diagnostic Types (per 1#10.3)
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Severity level for a diagnostic emitted by the ECR linter.
|
|
41
|
+
*/
|
|
42
|
+
export type DiagnosticSeverity = "error" | "warning" | "info";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A zero-based line/character position within a document.
|
|
46
|
+
*/
|
|
47
|
+
export interface Position {
|
|
48
|
+
/** Zero-based line number. */
|
|
49
|
+
readonly line: number;
|
|
50
|
+
/** Zero-based character offset within the line. */
|
|
51
|
+
readonly character: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A contiguous range within a document defined by a start and end position.
|
|
56
|
+
*/
|
|
57
|
+
export interface PositionRange {
|
|
58
|
+
/** Inclusive start of the range. */
|
|
59
|
+
readonly start: Position;
|
|
60
|
+
/** Exclusive end of the range. */
|
|
61
|
+
readonly end: Position;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A host-agnostic diagnostic produced by the ECR linter.
|
|
66
|
+
*
|
|
67
|
+
* Diagnostics can be mapped into LSP diagnostics or rendered in other UIs.
|
|
68
|
+
*/
|
|
69
|
+
export interface Diagnostic {
|
|
70
|
+
/** Severity of this diagnostic. */
|
|
71
|
+
readonly severity: DiagnosticSeverity;
|
|
72
|
+
/** Identifier of the validation rule that produced this diagnostic. */
|
|
73
|
+
readonly ruleId: string;
|
|
74
|
+
/** Human-readable description of the issue. */
|
|
75
|
+
readonly message: string;
|
|
76
|
+
/** Opaque, host-provided URI of the source document. */
|
|
77
|
+
readonly uri: string;
|
|
78
|
+
/**
|
|
79
|
+
* Positional range within the source document.
|
|
80
|
+
* Optional; depends on whether the Markdown parser provides positional metadata.
|
|
81
|
+
*/
|
|
82
|
+
readonly range?: PositionRange;
|
|
83
|
+
/** DocID of the document, when determinable. */
|
|
84
|
+
readonly docId?: DocID;
|
|
85
|
+
/** SectionID providing structural context, when determinable. */
|
|
86
|
+
readonly sectionId?: SectionID;
|
|
87
|
+
/** Arbitrary structured data associated with this diagnostic. */
|
|
88
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Reference Direction Types (per 1#9.7, 1#10.6)
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Semantic direction label for a document-level reference edge.
|
|
97
|
+
*
|
|
98
|
+
* Direction describes the relationship meaning; it does not alter edge orientation.
|
|
99
|
+
*/
|
|
100
|
+
export type ReferenceDirection = "authority" | "dependency" | "constraint" | "contract";
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Inline Reference Kind (per 1#10.7)
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The keyword form of an inline reference.
|
|
108
|
+
*/
|
|
109
|
+
export type InlineReferenceKind = "see" | "per";
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Extracted Structural Artefacts (per 1#10.4 - 1#10.7)
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A single section extracted from a validated ECR document.
|
|
117
|
+
*
|
|
118
|
+
* The H1 heading is represented as the root section node. All numbered
|
|
119
|
+
* headings of depth \>= 2 are child section nodes.
|
|
120
|
+
*/
|
|
121
|
+
export interface SectionNode {
|
|
122
|
+
/** DocID (for the root H1 node) or SectionID (for sub-headings). */
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents -- Semantically distinct: DocID for H1, SectionID for depth >= 2
|
|
124
|
+
readonly id: DocID | SectionID;
|
|
125
|
+
/**
|
|
126
|
+
* Human-readable heading text.
|
|
127
|
+
* Extracted for display and diagnostics only; not part of ECR structural identity.
|
|
128
|
+
*/
|
|
129
|
+
readonly title: string;
|
|
130
|
+
/** Markdown heading depth (1 for H1, 2 for H2, etc.). */
|
|
131
|
+
readonly headingDepth: number;
|
|
132
|
+
/**
|
|
133
|
+
* Identifier of the nearest preceding heading whose depth is exactly
|
|
134
|
+
* `headingDepth - 1`. Absent for the root H1 node.
|
|
135
|
+
*/
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents -- Semantically distinct: parent may be DocID (H1) or SectionID
|
|
137
|
+
readonly parentId?: DocID | SectionID;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* A document-level typed edge extracted from a `## References` list item.
|
|
142
|
+
*
|
|
143
|
+
* Edges are directed from the current document to the referenced document.
|
|
144
|
+
*/
|
|
145
|
+
export interface ReferenceEdge {
|
|
146
|
+
/** DocID of the document containing the reference. */
|
|
147
|
+
readonly fromDocId: DocID;
|
|
148
|
+
/** Target DocID declared in the References entry. */
|
|
149
|
+
readonly toDocId: DocID;
|
|
150
|
+
/** Semantic direction label for this edge. */
|
|
151
|
+
readonly direction: ReferenceDirection;
|
|
152
|
+
/** Non-empty explanation text extracted from the References entry. */
|
|
153
|
+
readonly explanation: string;
|
|
154
|
+
/**
|
|
155
|
+
* Non-empty title text extracted from the References entry.
|
|
156
|
+
* For display and diagnostics only; not part of ECR structural identity.
|
|
157
|
+
*/
|
|
158
|
+
readonly title: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A section-attributed edge extracted from an inline `see` or `per` reference.
|
|
163
|
+
*/
|
|
164
|
+
export interface InlineReferenceEdge {
|
|
165
|
+
/** Identifier of the section context where the inline reference occurs. */
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents -- Semantically distinct: source may be DocID or SectionID
|
|
167
|
+
readonly fromId: DocID | SectionID;
|
|
168
|
+
/** Target identifier referenced by the inline form. */
|
|
169
|
+
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents -- Semantically distinct: target may be DocID or SectionID
|
|
170
|
+
readonly toId: DocID | SectionID;
|
|
171
|
+
/** The keyword form (`see` or `per`) that produced this edge. */
|
|
172
|
+
readonly kind: InlineReferenceKind;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Canonical structural representation of a single validated ECR document.
|
|
177
|
+
*/
|
|
178
|
+
export interface ExtractedDocument {
|
|
179
|
+
/** DocID extracted from the H1 heading. */
|
|
180
|
+
readonly docId: DocID;
|
|
181
|
+
/**
|
|
182
|
+
* Document title extracted from the H1 heading.
|
|
183
|
+
* For display and diagnostics only; not part of ECR structural identity.
|
|
184
|
+
*/
|
|
185
|
+
readonly title: string;
|
|
186
|
+
/** All section nodes extracted from numbered headings, including the root H1. */
|
|
187
|
+
readonly sections: readonly SectionNode[];
|
|
188
|
+
/** Document-level reference edges extracted from the `## References` section. */
|
|
189
|
+
readonly references: readonly ReferenceEdge[];
|
|
190
|
+
/** Section-level inline reference edges extracted from valid `see`/`per` forms. */
|
|
191
|
+
readonly inlineReferences: readonly InlineReferenceEdge[];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Lint Result Types (per 1#10.2)
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Input descriptor for a single-document lint operation.
|
|
200
|
+
*/
|
|
201
|
+
export interface LintInput {
|
|
202
|
+
/** Opaque, host-provided URI identifying the document instance. */
|
|
203
|
+
readonly uri: string;
|
|
204
|
+
/** Optional version tag for the document instance. */
|
|
205
|
+
readonly version?: number;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Result of a lint operation over a single Markdown document.
|
|
210
|
+
*/
|
|
211
|
+
export interface LintResult {
|
|
212
|
+
/** Input descriptor identifying the document that was validated. */
|
|
213
|
+
readonly input: LintInput;
|
|
214
|
+
/** Whether the document satisfies per-document ECR structural invariants. */
|
|
215
|
+
readonly ok: boolean;
|
|
216
|
+
/** Diagnostics produced during validation. */
|
|
217
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
218
|
+
/**
|
|
219
|
+
* Extracted structural artefacts from the document.
|
|
220
|
+
* Present when a valid DocID is recovered and structural extraction succeeds.
|
|
221
|
+
*/
|
|
222
|
+
readonly extracted?: ExtractedDocument;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Corpus Result Types (per 1#10.8)
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A single document entry within a corpus validation result.
|
|
231
|
+
*/
|
|
232
|
+
export interface CorpusDocumentEntry {
|
|
233
|
+
/** Opaque, host-provided URI identifying the document instance. */
|
|
234
|
+
readonly uri: string;
|
|
235
|
+
/** Per-document lint result. */
|
|
236
|
+
readonly result: LintResult;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Global identifier index built from a validated corpus.
|
|
241
|
+
*
|
|
242
|
+
* Maps each unique identifier to the URI of the document that defines it.
|
|
243
|
+
*/
|
|
244
|
+
export interface CorpusIndex {
|
|
245
|
+
/** Mapping from each DocID to the URI of its defining document. */
|
|
246
|
+
readonly docIds: Readonly<Record<DocID, string>>;
|
|
247
|
+
/** Mapping from each SectionID to the URI of its defining document. */
|
|
248
|
+
readonly sectionIds: Readonly<Record<SectionID, string>>;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Result of validating a corpus of ECR documents.
|
|
253
|
+
*
|
|
254
|
+
* Includes per-document results, corpus-wide diagnostics, and an optional
|
|
255
|
+
* global identifier index suitable for graph construction.
|
|
256
|
+
*/
|
|
257
|
+
export interface CorpusResult {
|
|
258
|
+
/** Per-document lint results for every document in the corpus. */
|
|
259
|
+
readonly documents: readonly CorpusDocumentEntry[];
|
|
260
|
+
/**
|
|
261
|
+
* Global identifier index.
|
|
262
|
+
* Present when corpus-wide indexing succeeds.
|
|
263
|
+
*/
|
|
264
|
+
readonly index?: CorpusIndex;
|
|
265
|
+
/** Corpus-wide diagnostics (e.g. duplicate DocIDs, unresolved references). */
|
|
266
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
267
|
+
}
|