@descent-vtt/spec-graph 0.2.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 +21 -0
- package/README.md +627 -0
- package/bin/spec-graph.js +24 -0
- package/dist/baseline.d.ts +93 -0
- package/dist/baseline.js +187 -0
- package/dist/baseline.js.map +1 -0
- package/dist/cli.d.ts +66 -0
- package/dist/cli.js +565 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +66 -0
- package/dist/config.js +183 -0
- package/dist/config.js.map +1 -0
- package/dist/directives.d.ts +56 -0
- package/dist/directives.js +125 -0
- package/dist/directives.js.map +1 -0
- package/dist/extract.d.ts +116 -0
- package/dist/extract.js +1043 -0
- package/dist/extract.js.map +1 -0
- package/dist/glob.d.ts +73 -0
- package/dist/glob.js +284 -0
- package/dist/glob.js.map +1 -0
- package/dist/graph.d.ts +55 -0
- package/dist/graph.js +201 -0
- package/dist/graph.js.map +1 -0
- package/dist/identity.d.ts +87 -0
- package/dist/identity.js +294 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +57 -0
- package/dist/lifecycle.js +258 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/markdown.d.ts +152 -0
- package/dist/markdown.js +820 -0
- package/dist/markdown.js.map +1 -0
- package/dist/paths.d.ts +22 -0
- package/dist/paths.js +58 -0
- package/dist/paths.js.map +1 -0
- package/dist/report.d.ts +89 -0
- package/dist/report.js +326 -0
- package/dist/report.js.map +1 -0
- package/dist/resolve.d.ts +61 -0
- package/dist/resolve.js +340 -0
- package/dist/resolve.js.map +1 -0
- package/dist/rules.d.ts +61 -0
- package/dist/rules.js +557 -0
- package/dist/rules.js.map +1 -0
- package/dist/runner.d.ts +108 -0
- package/dist/runner.js +186 -0
- package/dist/runner.js.map +1 -0
- package/dist/sections.d.ts +94 -0
- package/dist/sections.js +372 -0
- package/dist/sections.js.map +1 -0
- package/dist/select.d.ts +96 -0
- package/dist/select.js +413 -0
- package/dist/select.js.map +1 -0
- package/dist/source.d.ts +43 -0
- package/dist/source.js +109 -0
- package/dist/source.js.map +1 -0
- package/dist/state.d.ts +45 -0
- package/dist/state.js +330 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +294 -0
- package/dist/types.js +52 -0
- package/dist/types.js.map +1 -0
- package/dist/yaml.d.ts +37 -0
- package/dist/yaml.js +240 -0
- package/dist/yaml.js.map +1 -0
- package/package.json +71 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The spec-graph data model.
|
|
3
|
+
*
|
|
4
|
+
* Three ideas carry the whole design:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Documents have a lifecycle phase**, normalised from whatever vocabulary
|
|
7
|
+
* the repository already uses (MADR `accepted`, KEP `implementable`, RFC
|
|
8
|
+
* `final`, plain English `archived`). The phase answers one question that
|
|
9
|
+
* matters more than any other: *can this document still absorb new work?*
|
|
10
|
+
*
|
|
11
|
+
* 2. **Items are obligations with nuanced state.** A checkbox is not a boolean.
|
|
12
|
+
* An item can be open, narrowed to a smaller scope, satisfied, consciously
|
|
13
|
+
* accepted as debt, or obviated because the premise it rested on evaporated.
|
|
14
|
+
* Collapsing those five into `[ ]` / `[x]` is what makes ad-hoc greps lie.
|
|
15
|
+
*
|
|
16
|
+
* 3. **Edges are typed and carry provenance.** Every relation records where it
|
|
17
|
+
* was *declared*, which is frequently not the node it points at. That is what
|
|
18
|
+
* lets a diagnostic say "ADR-0009 line 12 claims to supersede ADR-0003"
|
|
19
|
+
* instead of "something, somewhere, is inconsistent".
|
|
20
|
+
*/
|
|
21
|
+
/** A point in a source file. `offset` is a UTF-16 code-unit index. */
|
|
22
|
+
export interface Position {
|
|
23
|
+
readonly offset: number;
|
|
24
|
+
/** 1-based. */
|
|
25
|
+
readonly line: number;
|
|
26
|
+
/** 1-based, counted in UTF-16 code units. */
|
|
27
|
+
readonly column: number;
|
|
28
|
+
}
|
|
29
|
+
/** A half-open `[start, end)` range of a source file. */
|
|
30
|
+
export interface Span {
|
|
31
|
+
readonly start: Position;
|
|
32
|
+
readonly end: Position;
|
|
33
|
+
}
|
|
34
|
+
/** A span plus the file it belongs to. Everything user-visible carries one. */
|
|
35
|
+
export interface SourceRef {
|
|
36
|
+
/** Repository-relative POSIX path. */
|
|
37
|
+
readonly file: string;
|
|
38
|
+
readonly span: Span;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The normalised document lifecycle, as a four-state lattice.
|
|
42
|
+
*
|
|
43
|
+
* Ecosystem vocabularies are large and mutually incompatible; the *consequences*
|
|
44
|
+
* of a status are not. These four phases are chosen so that every question
|
|
45
|
+
* spec-graph asks can be answered from the phase alone.
|
|
46
|
+
*
|
|
47
|
+
* - `draft` - not yet binding. Mutable. Absorbs new work.
|
|
48
|
+
* - `active` - binding and mutable. Absorbs new work.
|
|
49
|
+
* - `frozen` - binding and immutable. Changes require a new document.
|
|
50
|
+
* - `retired` - no longer binding: superseded, rejected, withdrawn, archived.
|
|
51
|
+
*
|
|
52
|
+
* `unknown` is a fifth, deliberately non-lattice value for documents that never
|
|
53
|
+
* declared a status. Rules treat it permissively rather than guessing.
|
|
54
|
+
*/
|
|
55
|
+
export type Phase = 'draft' | 'active' | 'frozen' | 'retired' | 'record' | 'unknown';
|
|
56
|
+
/**
|
|
57
|
+
* Can a document in this phase take on a *new* open obligation?
|
|
58
|
+
*
|
|
59
|
+
* This is the single predicate behind ghost-handover detection. Delegating an
|
|
60
|
+
* open question into a `frozen` or `retired` document is a write to a closed
|
|
61
|
+
* ledger: nobody will ever read it again.
|
|
62
|
+
*/
|
|
63
|
+
export type Receptivity = 'receptive' | 'sealed' | 'unknown';
|
|
64
|
+
/**
|
|
65
|
+
* How much of an obligation is left.
|
|
66
|
+
*
|
|
67
|
+
* Kept separate from {@link Disposition} because the two answer different
|
|
68
|
+
* questions: `openness` is "does someone still owe work?", `disposition` is
|
|
69
|
+
* "what happened to it?". Rules almost always want the former; humans reading a
|
|
70
|
+
* report always want the latter.
|
|
71
|
+
*/
|
|
72
|
+
export type Openness = 'open' | 'partial' | 'closed';
|
|
73
|
+
/** Why an item is in the state it is in. */
|
|
74
|
+
export type Disposition =
|
|
75
|
+
/** Nothing has happened yet. */
|
|
76
|
+
'unresolved'
|
|
77
|
+
/** Scope was cut down, but a remainder is still owed. */
|
|
78
|
+
| 'narrowed'
|
|
79
|
+
/** Ownership moved elsewhere. Still owed - by somebody else. */
|
|
80
|
+
| 'delegated'
|
|
81
|
+
/** Done, answered, decided. */
|
|
82
|
+
| 'satisfied'
|
|
83
|
+
/** Knowingly not done. A closed item with a scar. */
|
|
84
|
+
| 'accepted-debt'
|
|
85
|
+
/** Decided against. */
|
|
86
|
+
| 'rejected'
|
|
87
|
+
/**
|
|
88
|
+
* The premise evaporated, so the question stopped being a question.
|
|
89
|
+
* Load-bearing citations to an obviated item are stale premises.
|
|
90
|
+
*/
|
|
91
|
+
| 'obviated';
|
|
92
|
+
export declare const DISPOSITIONS: readonly Disposition[];
|
|
93
|
+
/** The openness implied by each disposition. */
|
|
94
|
+
export declare const OPENNESS_OF: Readonly<Record<Disposition, Openness>>;
|
|
95
|
+
/**
|
|
96
|
+
* Relation kinds.
|
|
97
|
+
*
|
|
98
|
+
* The set is small on purpose. Two orthogonal properties - declared once in
|
|
99
|
+
* {@link EDGE_TRAITS} - do the actual work, so adding a kind never means
|
|
100
|
+
* touching a rule.
|
|
101
|
+
*/
|
|
102
|
+
export type EdgeKind =
|
|
103
|
+
/** Structural: a document contains an item. Never inferred from prose. */
|
|
104
|
+
'contains'
|
|
105
|
+
/** A neutral citation. */
|
|
106
|
+
| 'references'
|
|
107
|
+
/** Explicitly *not* load-bearing: "See also", "History", "Prior art". */
|
|
108
|
+
| 'relates-to'
|
|
109
|
+
/** The source design depends on the target content. */
|
|
110
|
+
| 'depends-on'
|
|
111
|
+
/** The source rests on a claim made by the target. */
|
|
112
|
+
| 'assumes'
|
|
113
|
+
/** The source hands an obligation to the target. */
|
|
114
|
+
| 'delegates-to'
|
|
115
|
+
/** The source cannot proceed until the target does. */
|
|
116
|
+
| 'blocked-by'
|
|
117
|
+
/** The source replaces the target. */
|
|
118
|
+
| 'supersedes'
|
|
119
|
+
/** The source modifies the target without replacing it. */
|
|
120
|
+
| 'amends';
|
|
121
|
+
export interface EdgeTraits {
|
|
122
|
+
/**
|
|
123
|
+
* Does the source continued validity depend on what the target says?
|
|
124
|
+
*
|
|
125
|
+
* Load-bearing edges into retired documents are stale premises. Non
|
|
126
|
+
* load-bearing ones ("see also") are just history, and reporting them would
|
|
127
|
+
* bury the real findings - which is exactly how ad-hoc linters lose trust.
|
|
128
|
+
*/
|
|
129
|
+
readonly loadBearing: boolean;
|
|
130
|
+
/** Does the edge move an obligation from source to target? */
|
|
131
|
+
readonly transfersObligation: boolean;
|
|
132
|
+
/** Does the edge assert something about the target lifecycle? */
|
|
133
|
+
readonly lifecycle: boolean;
|
|
134
|
+
/** Human-readable phrase used in diagnostics: "ADR-1 <phrase> ADR-2". */
|
|
135
|
+
readonly phrase: string;
|
|
136
|
+
}
|
|
137
|
+
export declare const EDGE_TRAITS: Readonly<Record<EdgeKind, EdgeTraits>>;
|
|
138
|
+
export declare const EDGE_KINDS: readonly EdgeKind[];
|
|
139
|
+
/** How an edge came to exist. Reported verbatim so findings are auditable. */
|
|
140
|
+
export type EdgeOrigin =
|
|
141
|
+
/** A `<!-- @spec-edge -->` directive. */
|
|
142
|
+
'directive'
|
|
143
|
+
/** A front-matter field such as `supersedes:` or `superseded-by:`. */
|
|
144
|
+
| 'front-matter'
|
|
145
|
+
/** A Markdown link, classified by the section it sits in. */
|
|
146
|
+
| 'link'
|
|
147
|
+
/** A bare textual identifier such as `ADR-0007` in prose. */
|
|
148
|
+
| 'text'
|
|
149
|
+
/** Synthesised by the extractor (document -> item). */
|
|
150
|
+
| 'structural';
|
|
151
|
+
export interface Edge {
|
|
152
|
+
readonly kind: EdgeKind;
|
|
153
|
+
/** Node id. */
|
|
154
|
+
readonly from: string;
|
|
155
|
+
/** Node id. */
|
|
156
|
+
readonly to: string;
|
|
157
|
+
readonly origin: EdgeOrigin;
|
|
158
|
+
/**
|
|
159
|
+
* Where the relation was written down. Not necessarily inside `from`: a
|
|
160
|
+
* `superseded-by: ADR-9` field in ADR-3 declares `ADR-9 supersedes ADR-3`,
|
|
161
|
+
* and the only place a human can go fix it is ADR-3.
|
|
162
|
+
*/
|
|
163
|
+
readonly declaredAt: SourceRef;
|
|
164
|
+
/** The raw text that produced the edge, for diagnostics. */
|
|
165
|
+
readonly raw: string;
|
|
166
|
+
/**
|
|
167
|
+
* Every file that declares this relation.
|
|
168
|
+
*
|
|
169
|
+
* A supersession stated only in the superseding document leaves the
|
|
170
|
+
* superseded one silently presenting itself as current, which is a finding in
|
|
171
|
+
* its own right - so the set of declaration sites has to survive de-duplication.
|
|
172
|
+
*/
|
|
173
|
+
readonly declaredIn: readonly string[];
|
|
174
|
+
/** True when `from` and `to` are the same node. Kept, never traversed. */
|
|
175
|
+
readonly reflexive: boolean;
|
|
176
|
+
}
|
|
177
|
+
export type NodeKind = 'document' | 'item';
|
|
178
|
+
interface NodeBase {
|
|
179
|
+
/** Canonical, graph-unique. Documents: `ADR-0007`. Items: `ADR-0007#q.1`. */
|
|
180
|
+
readonly id: string;
|
|
181
|
+
readonly kind: NodeKind;
|
|
182
|
+
/** Short human label used in reports. */
|
|
183
|
+
readonly title: string;
|
|
184
|
+
readonly at: SourceRef;
|
|
185
|
+
}
|
|
186
|
+
export interface DocumentNode extends NodeBase {
|
|
187
|
+
readonly kind: 'document';
|
|
188
|
+
/** Repository-relative POSIX path. */
|
|
189
|
+
readonly path: string;
|
|
190
|
+
/** Every spelling that resolves to this document, lowercased. */
|
|
191
|
+
readonly aliases: readonly string[];
|
|
192
|
+
readonly phase: Phase;
|
|
193
|
+
/** The status string as written, before normalisation. `null` if absent. */
|
|
194
|
+
readonly rawStatus: string | null;
|
|
195
|
+
/** Where the status was declared, when it was declared anywhere. */
|
|
196
|
+
readonly statusAt: SourceRef | null;
|
|
197
|
+
/** Front-matter fields, flattened to strings and string arrays. */
|
|
198
|
+
readonly frontMatter: Readonly<Record<string, string | readonly string[]>>;
|
|
199
|
+
}
|
|
200
|
+
export interface ItemNode extends NodeBase {
|
|
201
|
+
readonly kind: 'item';
|
|
202
|
+
/** Owning document id. */
|
|
203
|
+
readonly document: string;
|
|
204
|
+
/** Heading path the item sits under, outermost first. */
|
|
205
|
+
readonly section: readonly string[];
|
|
206
|
+
/** First line of the item, normalised to one line for reports. */
|
|
207
|
+
readonly text: string;
|
|
208
|
+
/** The full multi-line body, continuations included. */
|
|
209
|
+
readonly body: string;
|
|
210
|
+
readonly disposition: Disposition;
|
|
211
|
+
readonly openness: Openness;
|
|
212
|
+
/** Which signal decided the disposition. */
|
|
213
|
+
readonly evidence: StateSignal;
|
|
214
|
+
/** Conflicting signals found while resolving the state. */
|
|
215
|
+
readonly conflicts: readonly StateSignal[];
|
|
216
|
+
}
|
|
217
|
+
export type SpecNode = DocumentNode | ItemNode;
|
|
218
|
+
/** One observation about an item state. */
|
|
219
|
+
export interface StateSignal {
|
|
220
|
+
/** Higher wins. See `SIGNAL_PRIORITY` in `state.ts`. */
|
|
221
|
+
readonly source: 'directive' | 'marker' | 'strikethrough' | 'checkbox' | 'section' | 'default';
|
|
222
|
+
readonly disposition: Disposition;
|
|
223
|
+
/** The literal text that produced the signal. */
|
|
224
|
+
readonly raw: string;
|
|
225
|
+
readonly at: SourceRef;
|
|
226
|
+
}
|
|
227
|
+
export type DanglingReason =
|
|
228
|
+
/** Nothing in the corpus answers to this identifier. */
|
|
229
|
+
'unknown-target'
|
|
230
|
+
/** More than one document answers to it. */
|
|
231
|
+
| 'ambiguous'
|
|
232
|
+
/** The path exists on disk but is not a parsed specification. */
|
|
233
|
+
| 'not-a-spec'
|
|
234
|
+
/** The document resolved, but the `#anchor` did not. */
|
|
235
|
+
| 'unknown-anchor';
|
|
236
|
+
/** A citation that failed foreign-key validation. */
|
|
237
|
+
export interface DanglingRef {
|
|
238
|
+
readonly kind: EdgeKind;
|
|
239
|
+
readonly from: string;
|
|
240
|
+
/** The identifier as written. */
|
|
241
|
+
readonly target: string;
|
|
242
|
+
readonly reason: DanglingReason;
|
|
243
|
+
readonly origin: EdgeOrigin;
|
|
244
|
+
readonly declaredAt: SourceRef;
|
|
245
|
+
readonly raw: string;
|
|
246
|
+
/** Candidate ids when `reason` is `ambiguous`, or near-misses otherwise. */
|
|
247
|
+
readonly candidates: readonly string[];
|
|
248
|
+
}
|
|
249
|
+
export type Severity = 'error' | 'warn' | 'info' | 'off';
|
|
250
|
+
export type RuleId = 'ghost-handover' | 'stale-premise' | 'broken-reference' | 'reference-outside-corpus' | 'ambiguous-reference' | 'circular-delegation' | 'orphaned-obligation' | 'unreciprocated-supersession' | 'live-supersession' | 'state-conflict' | 'self-reference';
|
|
251
|
+
export interface RelatedLocation {
|
|
252
|
+
readonly at: SourceRef;
|
|
253
|
+
readonly note: string;
|
|
254
|
+
}
|
|
255
|
+
export interface Diagnostic {
|
|
256
|
+
readonly rule: RuleId;
|
|
257
|
+
readonly severity: Exclude<Severity, 'off'>;
|
|
258
|
+
/** One line, no trailing period. The headline of the finding. */
|
|
259
|
+
readonly message: string;
|
|
260
|
+
/** Where a human should go to fix it. */
|
|
261
|
+
readonly at: SourceRef;
|
|
262
|
+
/** Node ids involved, most relevant first. */
|
|
263
|
+
readonly nodes: readonly string[];
|
|
264
|
+
/**
|
|
265
|
+
* The citation this finding is about, for the rules that are about one.
|
|
266
|
+
*
|
|
267
|
+
* `null` everywhere else, where `nodes` already names the subject. It exists
|
|
268
|
+
* because a broken reference names something that is not a node - that is
|
|
269
|
+
* what makes it broken - and a baseline still has to be able to tell one
|
|
270
|
+
* broken reference from another. See ADR-0012.
|
|
271
|
+
*/
|
|
272
|
+
readonly target: string | null;
|
|
273
|
+
/** Supporting locations: the other end of an edge, a cycle members. */
|
|
274
|
+
readonly related: readonly RelatedLocation[];
|
|
275
|
+
/** Concrete next action. */
|
|
276
|
+
readonly hint: string;
|
|
277
|
+
}
|
|
278
|
+
/** A problem with the input itself, as opposed to a finding about the graph. */
|
|
279
|
+
export interface ParseProblem {
|
|
280
|
+
readonly message: string;
|
|
281
|
+
readonly at: SourceRef;
|
|
282
|
+
}
|
|
283
|
+
export interface AnalysisSummary {
|
|
284
|
+
readonly documents: number;
|
|
285
|
+
readonly items: number;
|
|
286
|
+
readonly edges: number;
|
|
287
|
+
readonly openObligations: number;
|
|
288
|
+
readonly errors: number;
|
|
289
|
+
readonly warnings: number;
|
|
290
|
+
readonly infos: number;
|
|
291
|
+
/** Milliseconds, wall clock. */
|
|
292
|
+
readonly durationMs: number;
|
|
293
|
+
}
|
|
294
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The spec-graph data model.
|
|
3
|
+
*
|
|
4
|
+
* Three ideas carry the whole design:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Documents have a lifecycle phase**, normalised from whatever vocabulary
|
|
7
|
+
* the repository already uses (MADR `accepted`, KEP `implementable`, RFC
|
|
8
|
+
* `final`, plain English `archived`). The phase answers one question that
|
|
9
|
+
* matters more than any other: *can this document still absorb new work?*
|
|
10
|
+
*
|
|
11
|
+
* 2. **Items are obligations with nuanced state.** A checkbox is not a boolean.
|
|
12
|
+
* An item can be open, narrowed to a smaller scope, satisfied, consciously
|
|
13
|
+
* accepted as debt, or obviated because the premise it rested on evaporated.
|
|
14
|
+
* Collapsing those five into `[ ]` / `[x]` is what makes ad-hoc greps lie.
|
|
15
|
+
*
|
|
16
|
+
* 3. **Edges are typed and carry provenance.** Every relation records where it
|
|
17
|
+
* was *declared*, which is frequently not the node it points at. That is what
|
|
18
|
+
* lets a diagnostic say "ADR-0009 line 12 claims to supersede ADR-0003"
|
|
19
|
+
* instead of "something, somewhere, is inconsistent".
|
|
20
|
+
*/
|
|
21
|
+
export const DISPOSITIONS = Object.freeze([
|
|
22
|
+
'unresolved',
|
|
23
|
+
'narrowed',
|
|
24
|
+
'delegated',
|
|
25
|
+
'satisfied',
|
|
26
|
+
'accepted-debt',
|
|
27
|
+
'rejected',
|
|
28
|
+
'obviated',
|
|
29
|
+
]);
|
|
30
|
+
/** The openness implied by each disposition. */
|
|
31
|
+
export const OPENNESS_OF = Object.freeze({
|
|
32
|
+
unresolved: 'open',
|
|
33
|
+
narrowed: 'partial',
|
|
34
|
+
delegated: 'open',
|
|
35
|
+
satisfied: 'closed',
|
|
36
|
+
'accepted-debt': 'closed',
|
|
37
|
+
rejected: 'closed',
|
|
38
|
+
obviated: 'closed',
|
|
39
|
+
});
|
|
40
|
+
export const EDGE_TRAITS = Object.freeze({
|
|
41
|
+
contains: { loadBearing: false, transfersObligation: false, lifecycle: false, phrase: 'contains' },
|
|
42
|
+
references: { loadBearing: false, transfersObligation: false, lifecycle: false, phrase: 'references' },
|
|
43
|
+
'relates-to': { loadBearing: false, transfersObligation: false, lifecycle: false, phrase: 'relates to' },
|
|
44
|
+
'depends-on': { loadBearing: true, transfersObligation: false, lifecycle: false, phrase: 'depends on' },
|
|
45
|
+
assumes: { loadBearing: true, transfersObligation: false, lifecycle: false, phrase: 'assumes' },
|
|
46
|
+
'delegates-to': { loadBearing: false, transfersObligation: true, lifecycle: false, phrase: 'delegates to' },
|
|
47
|
+
'blocked-by': { loadBearing: true, transfersObligation: true, lifecycle: false, phrase: 'is blocked by' },
|
|
48
|
+
supersedes: { loadBearing: false, transfersObligation: false, lifecycle: true, phrase: 'supersedes' },
|
|
49
|
+
amends: { loadBearing: true, transfersObligation: false, lifecycle: true, phrase: 'amends' },
|
|
50
|
+
});
|
|
51
|
+
export const EDGE_KINDS = Object.keys(EDGE_TRAITS);
|
|
52
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AA4FH,MAAM,CAAC,MAAM,YAAY,GAA2B,MAAM,CAAC,MAAM,CAAC;IAChE,YAAY;IACZ,UAAU;IACV,WAAW;IACX,WAAW;IACX,eAAe;IACf,UAAU;IACV,UAAU;CACX,CAAC,CAAC;AAEH,gDAAgD;AAChD,MAAM,CAAC,MAAM,WAAW,GAA4C,MAAM,CAAC,MAAM,CAAC;IAChF,UAAU,EAAE,MAAM;IAClB,QAAQ,EAAE,SAAS;IACnB,SAAS,EAAE,MAAM;IACjB,SAAS,EAAE,QAAQ;IACnB,eAAe,EAAE,QAAQ;IACzB,QAAQ,EAAE,QAAQ;IAClB,QAAQ,EAAE,QAAQ;CACnB,CAAC,CAAC;AAkDH,MAAM,CAAC,MAAM,WAAW,GAA2C,MAAM,CAAC,MAAM,CAAC;IAC/E,QAAQ,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE;IAClG,UAAU,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE;IACtG,YAAY,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE;IACxG,YAAY,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE;IACvG,OAAO,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE;IAC/F,cAAc,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,mBAAmB,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE;IAC3G,YAAY,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE;IACzG,UAAU,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE;IACrG,MAAM,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE;CAC7F,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAwB,CAAC","sourcesContent":["/**\n * The spec-graph data model.\n *\n * Three ideas carry the whole design:\n *\n * 1. **Documents have a lifecycle phase**, normalised from whatever vocabulary\n * the repository already uses (MADR `accepted`, KEP `implementable`, RFC\n * `final`, plain English `archived`). The phase answers one question that\n * matters more than any other: *can this document still absorb new work?*\n *\n * 2. **Items are obligations with nuanced state.** A checkbox is not a boolean.\n * An item can be open, narrowed to a smaller scope, satisfied, consciously\n * accepted as debt, or obviated because the premise it rested on evaporated.\n * Collapsing those five into `[ ]` / `[x]` is what makes ad-hoc greps lie.\n *\n * 3. **Edges are typed and carry provenance.** Every relation records where it\n * was *declared*, which is frequently not the node it points at. That is what\n * lets a diagnostic say \"ADR-0009 line 12 claims to supersede ADR-0003\"\n * instead of \"something, somewhere, is inconsistent\".\n */\n\n/* -------------------------------------------------------------------------- */\n/* Source positions */\n/* -------------------------------------------------------------------------- */\n\n/** A point in a source file. `offset` is a UTF-16 code-unit index. */\nexport interface Position {\n readonly offset: number;\n /** 1-based. */\n readonly line: number;\n /** 1-based, counted in UTF-16 code units. */\n readonly column: number;\n}\n\n/** A half-open `[start, end)` range of a source file. */\nexport interface Span {\n readonly start: Position;\n readonly end: Position;\n}\n\n/** A span plus the file it belongs to. Everything user-visible carries one. */\nexport interface SourceRef {\n /** Repository-relative POSIX path. */\n readonly file: string;\n readonly span: Span;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Lifecycle */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The normalised document lifecycle, as a four-state lattice.\n *\n * Ecosystem vocabularies are large and mutually incompatible; the *consequences*\n * of a status are not. These four phases are chosen so that every question\n * spec-graph asks can be answered from the phase alone.\n *\n * - `draft` - not yet binding. Mutable. Absorbs new work.\n * - `active` - binding and mutable. Absorbs new work.\n * - `frozen` - binding and immutable. Changes require a new document.\n * - `retired` - no longer binding: superseded, rejected, withdrawn, archived.\n *\n * `unknown` is a fifth, deliberately non-lattice value for documents that never\n * declared a status. Rules treat it permissively rather than guessing.\n */\nexport type Phase = 'draft' | 'active' | 'frozen' | 'retired' | 'record' | 'unknown';\n\n/**\n * Can a document in this phase take on a *new* open obligation?\n *\n * This is the single predicate behind ghost-handover detection. Delegating an\n * open question into a `frozen` or `retired` document is a write to a closed\n * ledger: nobody will ever read it again.\n */\nexport type Receptivity = 'receptive' | 'sealed' | 'unknown';\n\n/* -------------------------------------------------------------------------- */\n/* Item state */\n/* -------------------------------------------------------------------------- */\n\n/**\n * How much of an obligation is left.\n *\n * Kept separate from {@link Disposition} because the two answer different\n * questions: `openness` is \"does someone still owe work?\", `disposition` is\n * \"what happened to it?\". Rules almost always want the former; humans reading a\n * report always want the latter.\n */\nexport type Openness = 'open' | 'partial' | 'closed';\n\n/** Why an item is in the state it is in. */\nexport type Disposition =\n /** Nothing has happened yet. */\n | 'unresolved'\n /** Scope was cut down, but a remainder is still owed. */\n | 'narrowed'\n /** Ownership moved elsewhere. Still owed - by somebody else. */\n | 'delegated'\n /** Done, answered, decided. */\n | 'satisfied'\n /** Knowingly not done. A closed item with a scar. */\n | 'accepted-debt'\n /** Decided against. */\n | 'rejected'\n /**\n * The premise evaporated, so the question stopped being a question.\n * Load-bearing citations to an obviated item are stale premises.\n */\n | 'obviated';\n\nexport const DISPOSITIONS: readonly Disposition[] = Object.freeze([\n 'unresolved',\n 'narrowed',\n 'delegated',\n 'satisfied',\n 'accepted-debt',\n 'rejected',\n 'obviated',\n]);\n\n/** The openness implied by each disposition. */\nexport const OPENNESS_OF: Readonly<Record<Disposition, Openness>> = Object.freeze({\n unresolved: 'open',\n narrowed: 'partial',\n delegated: 'open',\n satisfied: 'closed',\n 'accepted-debt': 'closed',\n rejected: 'closed',\n obviated: 'closed',\n});\n\n/* -------------------------------------------------------------------------- */\n/* Edges */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Relation kinds.\n *\n * The set is small on purpose. Two orthogonal properties - declared once in\n * {@link EDGE_TRAITS} - do the actual work, so adding a kind never means\n * touching a rule.\n */\nexport type EdgeKind =\n /** Structural: a document contains an item. Never inferred from prose. */\n | 'contains'\n /** A neutral citation. */\n | 'references'\n /** Explicitly *not* load-bearing: \"See also\", \"History\", \"Prior art\". */\n | 'relates-to'\n /** The source design depends on the target content. */\n | 'depends-on'\n /** The source rests on a claim made by the target. */\n | 'assumes'\n /** The source hands an obligation to the target. */\n | 'delegates-to'\n /** The source cannot proceed until the target does. */\n | 'blocked-by'\n /** The source replaces the target. */\n | 'supersedes'\n /** The source modifies the target without replacing it. */\n | 'amends';\n\nexport interface EdgeTraits {\n /**\n * Does the source continued validity depend on what the target says?\n *\n * Load-bearing edges into retired documents are stale premises. Non\n * load-bearing ones (\"see also\") are just history, and reporting them would\n * bury the real findings - which is exactly how ad-hoc linters lose trust.\n */\n readonly loadBearing: boolean;\n /** Does the edge move an obligation from source to target? */\n readonly transfersObligation: boolean;\n /** Does the edge assert something about the target lifecycle? */\n readonly lifecycle: boolean;\n /** Human-readable phrase used in diagnostics: \"ADR-1 <phrase> ADR-2\". */\n readonly phrase: string;\n}\n\nexport const EDGE_TRAITS: Readonly<Record<EdgeKind, EdgeTraits>> = Object.freeze({\n contains: { loadBearing: false, transfersObligation: false, lifecycle: false, phrase: 'contains' },\n references: { loadBearing: false, transfersObligation: false, lifecycle: false, phrase: 'references' },\n 'relates-to': { loadBearing: false, transfersObligation: false, lifecycle: false, phrase: 'relates to' },\n 'depends-on': { loadBearing: true, transfersObligation: false, lifecycle: false, phrase: 'depends on' },\n assumes: { loadBearing: true, transfersObligation: false, lifecycle: false, phrase: 'assumes' },\n 'delegates-to': { loadBearing: false, transfersObligation: true, lifecycle: false, phrase: 'delegates to' },\n 'blocked-by': { loadBearing: true, transfersObligation: true, lifecycle: false, phrase: 'is blocked by' },\n supersedes: { loadBearing: false, transfersObligation: false, lifecycle: true, phrase: 'supersedes' },\n amends: { loadBearing: true, transfersObligation: false, lifecycle: true, phrase: 'amends' },\n});\n\nexport const EDGE_KINDS = Object.keys(EDGE_TRAITS) as readonly EdgeKind[];\n\n/** How an edge came to exist. Reported verbatim so findings are auditable. */\nexport type EdgeOrigin =\n /** A `<!-- @spec-edge -->` directive. */\n | 'directive'\n /** A front-matter field such as `supersedes:` or `superseded-by:`. */\n | 'front-matter'\n /** A Markdown link, classified by the section it sits in. */\n | 'link'\n /** A bare textual identifier such as `ADR-0007` in prose. */\n | 'text'\n /** Synthesised by the extractor (document -> item). */\n | 'structural';\n\nexport interface Edge {\n readonly kind: EdgeKind;\n /** Node id. */\n readonly from: string;\n /** Node id. */\n readonly to: string;\n readonly origin: EdgeOrigin;\n /**\n * Where the relation was written down. Not necessarily inside `from`: a\n * `superseded-by: ADR-9` field in ADR-3 declares `ADR-9 supersedes ADR-3`,\n * and the only place a human can go fix it is ADR-3.\n */\n readonly declaredAt: SourceRef;\n /** The raw text that produced the edge, for diagnostics. */\n readonly raw: string;\n /**\n * Every file that declares this relation.\n *\n * A supersession stated only in the superseding document leaves the\n * superseded one silently presenting itself as current, which is a finding in\n * its own right - so the set of declaration sites has to survive de-duplication.\n */\n readonly declaredIn: readonly string[];\n /** True when `from` and `to` are the same node. Kept, never traversed. */\n readonly reflexive: boolean;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Nodes */\n/* -------------------------------------------------------------------------- */\n\nexport type NodeKind = 'document' | 'item';\n\ninterface NodeBase {\n /** Canonical, graph-unique. Documents: `ADR-0007`. Items: `ADR-0007#q.1`. */\n readonly id: string;\n readonly kind: NodeKind;\n /** Short human label used in reports. */\n readonly title: string;\n readonly at: SourceRef;\n}\n\nexport interface DocumentNode extends NodeBase {\n readonly kind: 'document';\n /** Repository-relative POSIX path. */\n readonly path: string;\n /** Every spelling that resolves to this document, lowercased. */\n readonly aliases: readonly string[];\n readonly phase: Phase;\n /** The status string as written, before normalisation. `null` if absent. */\n readonly rawStatus: string | null;\n /** Where the status was declared, when it was declared anywhere. */\n readonly statusAt: SourceRef | null;\n /** Front-matter fields, flattened to strings and string arrays. */\n readonly frontMatter: Readonly<Record<string, string | readonly string[]>>;\n}\n\nexport interface ItemNode extends NodeBase {\n readonly kind: 'item';\n /** Owning document id. */\n readonly document: string;\n /** Heading path the item sits under, outermost first. */\n readonly section: readonly string[];\n /** First line of the item, normalised to one line for reports. */\n readonly text: string;\n /** The full multi-line body, continuations included. */\n readonly body: string;\n readonly disposition: Disposition;\n readonly openness: Openness;\n /** Which signal decided the disposition. */\n readonly evidence: StateSignal;\n /** Conflicting signals found while resolving the state. */\n readonly conflicts: readonly StateSignal[];\n}\n\nexport type SpecNode = DocumentNode | ItemNode;\n\n/** One observation about an item state. */\nexport interface StateSignal {\n /** Higher wins. See `SIGNAL_PRIORITY` in `state.ts`. */\n readonly source: 'directive' | 'marker' | 'strikethrough' | 'checkbox' | 'section' | 'default';\n readonly disposition: Disposition;\n /** The literal text that produced the signal. */\n readonly raw: string;\n readonly at: SourceRef;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Unresolved references */\n/* -------------------------------------------------------------------------- */\n\nexport type DanglingReason =\n /** Nothing in the corpus answers to this identifier. */\n | 'unknown-target'\n /** More than one document answers to it. */\n | 'ambiguous'\n /** The path exists on disk but is not a parsed specification. */\n | 'not-a-spec'\n /** The document resolved, but the `#anchor` did not. */\n | 'unknown-anchor';\n\n/** A citation that failed foreign-key validation. */\nexport interface DanglingRef {\n readonly kind: EdgeKind;\n readonly from: string;\n /** The identifier as written. */\n readonly target: string;\n readonly reason: DanglingReason;\n readonly origin: EdgeOrigin;\n readonly declaredAt: SourceRef;\n readonly raw: string;\n /** Candidate ids when `reason` is `ambiguous`, or near-misses otherwise. */\n readonly candidates: readonly string[];\n}\n\n/* -------------------------------------------------------------------------- */\n/* Diagnostics */\n/* -------------------------------------------------------------------------- */\n\nexport type Severity = 'error' | 'warn' | 'info' | 'off';\n\nexport type RuleId =\n | 'ghost-handover'\n | 'stale-premise'\n | 'broken-reference'\n | 'reference-outside-corpus'\n | 'ambiguous-reference'\n | 'circular-delegation'\n | 'orphaned-obligation'\n | 'unreciprocated-supersession'\n | 'live-supersession'\n | 'state-conflict'\n | 'self-reference';\n\nexport interface RelatedLocation {\n readonly at: SourceRef;\n readonly note: string;\n}\n\nexport interface Diagnostic {\n readonly rule: RuleId;\n readonly severity: Exclude<Severity, 'off'>;\n /** One line, no trailing period. The headline of the finding. */\n readonly message: string;\n /** Where a human should go to fix it. */\n readonly at: SourceRef;\n /** Node ids involved, most relevant first. */\n readonly nodes: readonly string[];\n /**\n * The citation this finding is about, for the rules that are about one.\n *\n * `null` everywhere else, where `nodes` already names the subject. It exists\n * because a broken reference names something that is not a node - that is\n * what makes it broken - and a baseline still has to be able to tell one\n * broken reference from another. See ADR-0012.\n */\n readonly target: string | null;\n /** Supporting locations: the other end of an edge, a cycle members. */\n readonly related: readonly RelatedLocation[];\n /** Concrete next action. */\n readonly hint: string;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Analysis output */\n/* -------------------------------------------------------------------------- */\n\n/** A problem with the input itself, as opposed to a finding about the graph. */\nexport interface ParseProblem {\n readonly message: string;\n readonly at: SourceRef;\n}\n\nexport interface AnalysisSummary {\n readonly documents: number;\n readonly items: number;\n readonly edges: number;\n readonly openObligations: number;\n readonly errors: number;\n readonly warnings: number;\n readonly infos: number;\n /** Milliseconds, wall clock. */\n readonly durationMs: number;\n}\n"]}
|
package/dist/yaml.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A front-matter reader for the subset of YAML that specifications actually use.
|
|
3
|
+
*
|
|
4
|
+
* Front matter in an ADR is a flat bag of scalars and lists. Pulling in a full
|
|
5
|
+
* YAML implementation to read `status: accepted` would add a parser with its own
|
|
6
|
+
* CVE history, and would still need wrapping to recover the byte offset of the
|
|
7
|
+
* value - which is the whole reason we parse it ourselves: when a status is
|
|
8
|
+
* wrong, the report has to point at the line that declares it.
|
|
9
|
+
*
|
|
10
|
+
* Supported: scalars, quoted scalars, inline `[a, b]` sequences, block `- item`
|
|
11
|
+
* sequences, one level of nested mapping (flattened to `parent.child`), and `#`
|
|
12
|
+
* comments. Anything richer is preserved as the raw scalar text rather than
|
|
13
|
+
* being silently mangled.
|
|
14
|
+
*/
|
|
15
|
+
export interface YamlEntry {
|
|
16
|
+
/** Lower-cased key. Nested keys are flattened with a dot. */
|
|
17
|
+
readonly key: string;
|
|
18
|
+
readonly value: string | readonly string[];
|
|
19
|
+
/** Offset of the key. */
|
|
20
|
+
readonly start: number;
|
|
21
|
+
/** Offset just past the value. */
|
|
22
|
+
readonly end: number;
|
|
23
|
+
/** Offset of the first character of the value, for precise diagnostics. */
|
|
24
|
+
readonly valueStart: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parses front-matter text.
|
|
28
|
+
*
|
|
29
|
+
* @param raw The text between the delimiters.
|
|
30
|
+
* @param baseOffset Offset of `raw` within the containing file, so the returned
|
|
31
|
+
* spans address the file rather than the fragment.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseFrontMatter(raw: string, baseOffset?: number): YamlEntry[];
|
|
34
|
+
/** Collapses parsed entries into the flat record stored on a document node. */
|
|
35
|
+
export declare function toRecord(entries: readonly YamlEntry[]): Record<string, string | readonly string[]>;
|
|
36
|
+
/** Every value of a key as a flat list, whether it was a scalar or a sequence. */
|
|
37
|
+
export declare function valuesOf(entry: YamlEntry | undefined): string[];
|
package/dist/yaml.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A front-matter reader for the subset of YAML that specifications actually use.
|
|
3
|
+
*
|
|
4
|
+
* Front matter in an ADR is a flat bag of scalars and lists. Pulling in a full
|
|
5
|
+
* YAML implementation to read `status: accepted` would add a parser with its own
|
|
6
|
+
* CVE history, and would still need wrapping to recover the byte offset of the
|
|
7
|
+
* value - which is the whole reason we parse it ourselves: when a status is
|
|
8
|
+
* wrong, the report has to point at the line that declares it.
|
|
9
|
+
*
|
|
10
|
+
* Supported: scalars, quoted scalars, inline `[a, b]` sequences, block `- item`
|
|
11
|
+
* sequences, one level of nested mapping (flattened to `parent.child`), and `#`
|
|
12
|
+
* comments. Anything richer is preserved as the raw scalar text rather than
|
|
13
|
+
* being silently mangled.
|
|
14
|
+
*/
|
|
15
|
+
const KEY_LINE = /^(\s*)([A-Za-z0-9_.$-]+)\s*:(.*)$/;
|
|
16
|
+
const SEQUENCE_ITEM = /^(\s*)-\s*(.*)$/;
|
|
17
|
+
/**
|
|
18
|
+
* Parses front-matter text.
|
|
19
|
+
*
|
|
20
|
+
* @param raw The text between the delimiters.
|
|
21
|
+
* @param baseOffset Offset of `raw` within the containing file, so the returned
|
|
22
|
+
* spans address the file rather than the fragment.
|
|
23
|
+
*/
|
|
24
|
+
export function parseFrontMatter(raw, baseOffset = 0) {
|
|
25
|
+
const entries = [];
|
|
26
|
+
const lines = splitLines(raw);
|
|
27
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
28
|
+
const line = lines[i];
|
|
29
|
+
const match = KEY_LINE.exec(line.text);
|
|
30
|
+
if (!match)
|
|
31
|
+
continue;
|
|
32
|
+
const indent = match[1].length;
|
|
33
|
+
const key = match[2].toLowerCase();
|
|
34
|
+
const rest = match[3];
|
|
35
|
+
const restStart = line.start + indent + match[2].length + 1;
|
|
36
|
+
const trimmed = stripComment(rest).trim();
|
|
37
|
+
if (trimmed.length > 0) {
|
|
38
|
+
const valueStart = restStart + rest.indexOf(trimmed);
|
|
39
|
+
entries.push({
|
|
40
|
+
key,
|
|
41
|
+
value: parseScalarOrFlow(trimmed),
|
|
42
|
+
start: baseOffset + line.start + indent,
|
|
43
|
+
end: baseOffset + valueStart + trimmed.length,
|
|
44
|
+
valueStart: baseOffset + valueStart,
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// An empty value opens either a block sequence or a nested mapping.
|
|
49
|
+
const block = readBlock(lines, i + 1, indent);
|
|
50
|
+
if (block.kind === 'sequence') {
|
|
51
|
+
entries.push({
|
|
52
|
+
key,
|
|
53
|
+
value: block.items,
|
|
54
|
+
start: baseOffset + line.start + indent,
|
|
55
|
+
end: baseOffset + block.end,
|
|
56
|
+
valueStart: baseOffset + block.valueStart,
|
|
57
|
+
});
|
|
58
|
+
i = block.lastIndex;
|
|
59
|
+
}
|
|
60
|
+
else if (block.kind === 'mapping') {
|
|
61
|
+
for (const child of block.entries) {
|
|
62
|
+
entries.push({
|
|
63
|
+
key: `${key}.${child.key}`,
|
|
64
|
+
value: child.value,
|
|
65
|
+
start: baseOffset + child.start,
|
|
66
|
+
end: baseOffset + child.end,
|
|
67
|
+
valueStart: baseOffset + child.valueStart,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
i = block.lastIndex;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
entries.push({
|
|
74
|
+
key,
|
|
75
|
+
value: '',
|
|
76
|
+
start: baseOffset + line.start + indent,
|
|
77
|
+
end: baseOffset + line.end,
|
|
78
|
+
valueStart: baseOffset + line.end,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return entries;
|
|
83
|
+
}
|
|
84
|
+
/** Collapses parsed entries into the flat record stored on a document node. */
|
|
85
|
+
export function toRecord(entries) {
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const entry of entries)
|
|
88
|
+
out[entry.key] = entry.value;
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/** Every value of a key as a flat list, whether it was a scalar or a sequence. */
|
|
92
|
+
export function valuesOf(entry) {
|
|
93
|
+
if (!entry)
|
|
94
|
+
return [];
|
|
95
|
+
return typeof entry.value === 'string'
|
|
96
|
+
? entry.value.length > 0
|
|
97
|
+
? [entry.value]
|
|
98
|
+
: []
|
|
99
|
+
: [...entry.value];
|
|
100
|
+
}
|
|
101
|
+
function splitLines(raw) {
|
|
102
|
+
const out = [];
|
|
103
|
+
let start = 0;
|
|
104
|
+
for (let i = 0; i <= raw.length; i += 1) {
|
|
105
|
+
if (i === raw.length || raw[i] === '\n') {
|
|
106
|
+
let end = i;
|
|
107
|
+
if (end > start && raw[end - 1] === '\r')
|
|
108
|
+
end -= 1;
|
|
109
|
+
out.push({ text: raw.slice(start, end), start, end });
|
|
110
|
+
start = i + 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (out.length > 0 && out[out.length - 1].text === '' && raw.endsWith('\n'))
|
|
114
|
+
out.pop();
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
function readBlock(lines, from, indent) {
|
|
118
|
+
const items = [];
|
|
119
|
+
let valueStart = -1;
|
|
120
|
+
let end = -1;
|
|
121
|
+
let last = from - 1;
|
|
122
|
+
for (let i = from; i < lines.length; i += 1) {
|
|
123
|
+
const line = lines[i];
|
|
124
|
+
if (line.text.trim().length === 0) {
|
|
125
|
+
last = i;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const lineIndent = line.text.length - line.text.trimStart().length;
|
|
129
|
+
if (lineIndent <= indent)
|
|
130
|
+
break;
|
|
131
|
+
const seq = SEQUENCE_ITEM.exec(line.text);
|
|
132
|
+
if (!seq) {
|
|
133
|
+
if (items.length > 0)
|
|
134
|
+
break;
|
|
135
|
+
// A nested mapping: re-parse the indented region as its own document.
|
|
136
|
+
const region = collectRegion(lines, from, indent);
|
|
137
|
+
if (region.text.trim().length === 0)
|
|
138
|
+
return { kind: 'empty', lastIndex: from - 1 };
|
|
139
|
+
const nested = parseFrontMatter(region.text, region.start);
|
|
140
|
+
return { kind: 'mapping', entries: nested, lastIndex: region.lastIndex };
|
|
141
|
+
}
|
|
142
|
+
const value = stripComment(seq[2]).trim();
|
|
143
|
+
const offset = line.start + line.text.indexOf(value, seq[1].length + 1);
|
|
144
|
+
if (valueStart === -1)
|
|
145
|
+
valueStart = value.length > 0 ? offset : line.start;
|
|
146
|
+
items.push(unquote(value));
|
|
147
|
+
end = line.start + line.text.length;
|
|
148
|
+
last = i;
|
|
149
|
+
}
|
|
150
|
+
if (items.length === 0)
|
|
151
|
+
return { kind: 'empty', lastIndex: from - 1 };
|
|
152
|
+
return { kind: 'sequence', items, end, valueStart, lastIndex: last };
|
|
153
|
+
}
|
|
154
|
+
function collectRegion(lines, from, indent) {
|
|
155
|
+
let last = from - 1;
|
|
156
|
+
for (let i = from; i < lines.length; i += 1) {
|
|
157
|
+
const line = lines[i];
|
|
158
|
+
if (line.text.trim().length === 0) {
|
|
159
|
+
last = i;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const lineIndent = line.text.length - line.text.trimStart().length;
|
|
163
|
+
if (lineIndent <= indent)
|
|
164
|
+
break;
|
|
165
|
+
last = i;
|
|
166
|
+
}
|
|
167
|
+
if (last < from)
|
|
168
|
+
return { text: '', start: 0, lastIndex: from - 1 };
|
|
169
|
+
const start = lines[from].start;
|
|
170
|
+
const stop = lines[last].end;
|
|
171
|
+
const parts = [];
|
|
172
|
+
for (let i = from; i <= last; i += 1)
|
|
173
|
+
parts.push(lines[i].text);
|
|
174
|
+
return { text: parts.join('\n'), start, lastIndex: last };
|
|
175
|
+
void stop;
|
|
176
|
+
}
|
|
177
|
+
/** Drops a trailing `#` comment, respecting quotes. */
|
|
178
|
+
function stripComment(value) {
|
|
179
|
+
let quote = null;
|
|
180
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
181
|
+
const ch = value[i];
|
|
182
|
+
if (quote) {
|
|
183
|
+
if (ch === '\\')
|
|
184
|
+
i += 1;
|
|
185
|
+
else if (ch === quote)
|
|
186
|
+
quote = null;
|
|
187
|
+
}
|
|
188
|
+
else if (ch === '"' || ch === "'") {
|
|
189
|
+
quote = ch;
|
|
190
|
+
}
|
|
191
|
+
else if (ch === '#' && (i === 0 || value[i - 1] === ' ' || value[i - 1] === '\t')) {
|
|
192
|
+
return value.slice(0, i);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
function parseScalarOrFlow(value) {
|
|
198
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
199
|
+
const inner = value.slice(1, -1).trim();
|
|
200
|
+
if (inner.length === 0)
|
|
201
|
+
return [];
|
|
202
|
+
return splitFlow(inner).map(unquote);
|
|
203
|
+
}
|
|
204
|
+
return unquote(value);
|
|
205
|
+
}
|
|
206
|
+
/** Splits `a, "b, c", d` on commas that are not inside quotes. */
|
|
207
|
+
function splitFlow(value) {
|
|
208
|
+
const out = [];
|
|
209
|
+
let quote = null;
|
|
210
|
+
let start = 0;
|
|
211
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
212
|
+
const ch = value[i];
|
|
213
|
+
if (quote) {
|
|
214
|
+
if (ch === '\\')
|
|
215
|
+
i += 1;
|
|
216
|
+
else if (ch === quote)
|
|
217
|
+
quote = null;
|
|
218
|
+
}
|
|
219
|
+
else if (ch === '"' || ch === "'") {
|
|
220
|
+
quote = ch;
|
|
221
|
+
}
|
|
222
|
+
else if (ch === ',') {
|
|
223
|
+
out.push(value.slice(start, i).trim());
|
|
224
|
+
start = i + 1;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
out.push(value.slice(start).trim());
|
|
228
|
+
return out.filter((part) => part.length > 0);
|
|
229
|
+
}
|
|
230
|
+
function unquote(value) {
|
|
231
|
+
if (value.length >= 2) {
|
|
232
|
+
const first = value[0];
|
|
233
|
+
const last = value[value.length - 1];
|
|
234
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
235
|
+
return value.slice(1, -1).replace(/\\(.)/g, '$1');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=yaml.js.map
|