@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/cli.js
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command line.
|
|
3
|
+
*
|
|
4
|
+
* Four verbs, because there are four questions worth asking of a specification
|
|
5
|
+
* graph: is it consistent (`check`), what does it contain (`query`), what does
|
|
6
|
+
* it look like (`graph`), and what will you check for me (`rules`).
|
|
7
|
+
*
|
|
8
|
+
* Exit codes are the contract with CI: `0` clean, `1` findings, `2` the tool
|
|
9
|
+
* itself could not run. A usage mistake never masquerades as a passing build.
|
|
10
|
+
*/
|
|
11
|
+
import { applyBaseline, EMPTY_BASELINE, formatBaseline, parseBaseline } from './baseline.js';
|
|
12
|
+
import { loadConfig } from './config.js';
|
|
13
|
+
import { underRoot } from './glob.js';
|
|
14
|
+
import { analyse, DEFAULT_PATTERNS, withDiagnostics } from './runner.js';
|
|
15
|
+
import { formatGraph, formatJson, formatReport, shouldUseAscii, shouldUseColor } from './report.js';
|
|
16
|
+
import { DEFAULT_SEVERITIES, resolveStrict, RULE_DESCRIPTIONS, RULE_IDS, RULE_QUERIES } from './rules.js';
|
|
17
|
+
import { formatRef } from './source.js';
|
|
18
|
+
import { execute, parseQuery, QueryError, renderMatch } from './select.js';
|
|
19
|
+
export const EXIT_OK = 0;
|
|
20
|
+
export const EXIT_FAILED = 1;
|
|
21
|
+
export const EXIT_ERROR = 2;
|
|
22
|
+
export class UsageError extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'UsageError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export const HELP = `spec-graph - turn Markdown specifications into a verifiable graph
|
|
29
|
+
|
|
30
|
+
USAGE
|
|
31
|
+
spec-graph [check] [patterns...] [options]
|
|
32
|
+
spec-graph query <selector> [patterns...] [options]
|
|
33
|
+
spec-graph graph [patterns...] [--graph-format dot|mermaid|json]
|
|
34
|
+
spec-graph rules [--explain]
|
|
35
|
+
|
|
36
|
+
COMMANDS
|
|
37
|
+
check Validate the specification graph. The default.
|
|
38
|
+
query Run a selector and print the matching paths.
|
|
39
|
+
graph Export the graph for Graphviz, Mermaid, or another tool.
|
|
40
|
+
rules List the built-in diagnostics.
|
|
41
|
+
|
|
42
|
+
OPTIONS
|
|
43
|
+
--root <dir> Directory the patterns resolve against (default: cwd)
|
|
44
|
+
--ignore <glob> Skip paths. Repeatable.
|
|
45
|
+
--ignore-ref <glob> Do not report these reference targets when they fail
|
|
46
|
+
to resolve, for repositories where [[...]] tags a
|
|
47
|
+
concept rather than naming a file. Repeatable.
|
|
48
|
+
Suppresses findings only, never edges.
|
|
49
|
+
--family <name> Families a bare identifier in prose may name. When
|
|
50
|
+
given, everything else stays prose. Repeatable.
|
|
51
|
+
--ignore-family <name> Families that are never citations - RFC when the repo
|
|
52
|
+
cites RFC 2119 and keeps its own RFCs. Repeatable.
|
|
53
|
+
--history <glob> Files that log what was decided rather than deciding
|
|
54
|
+
it - journals, changelogs, minutes. Their links are
|
|
55
|
+
still checked; their obligations are not. Repeatable.
|
|
56
|
+
--baseline <file> Accept the findings recorded in this file and report
|
|
57
|
+
only what is new since. Missing file = accept nothing.
|
|
58
|
+
--record-baseline <f> Write today's findings to this file as accepted debt,
|
|
59
|
+
and exit 0 without judging them.
|
|
60
|
+
--no-config Ignore .spec-graph.json and the package.json key.
|
|
61
|
+
--format human|json Report format (default: human)
|
|
62
|
+
--graph-format <fmt> dot, mermaid or json (default: dot)
|
|
63
|
+
--documents-only Leave items out of the exported graph
|
|
64
|
+
--rule <id>=<severity> Override one rule: error, warn, info or off. Repeatable.
|
|
65
|
+
--max <n> Show at most n findings (0 = no limit)
|
|
66
|
+
--max-warnings <n> Fail when warnings exceed n (default: no limit)
|
|
67
|
+
--strict Raise every warning to an error. An explicit --rule
|
|
68
|
+
still wins, so --strict --rule x=warn exempts x.
|
|
69
|
+
--color / --no-color Force colour on or off
|
|
70
|
+
--ascii Use ASCII glyphs only
|
|
71
|
+
--verbose Include parse problems and per-file detail
|
|
72
|
+
-h, --help Show this help
|
|
73
|
+
-v, --version Show the version
|
|
74
|
+
|
|
75
|
+
SELECTORS
|
|
76
|
+
A selector is a path through the graph.
|
|
77
|
+
|
|
78
|
+
item[openness=open] -delegates-to-> document[phase=retired]
|
|
79
|
+
document[phase=active] -assumes-> document[phase=retired]
|
|
80
|
+
document =supersedes=> document
|
|
81
|
+
*[path^=docs/adr] -contains-> item[state=accepted-debt]
|
|
82
|
+
|
|
83
|
+
Node types: document, item, * (any)
|
|
84
|
+
Attributes: id, kind, title, path, file, line, phase, status, receptivity,
|
|
85
|
+
alias, document, state (or disposition), openness, section,
|
|
86
|
+
text, body, evidence, conflicted, fm.<front-matter-key>
|
|
87
|
+
Operators: = != ^= $= *= ~= and [attr] for "is present"
|
|
88
|
+
Relations: -kind-> <-kind- =kind=> <=kind= (= forms are transitive)
|
|
89
|
+
|
|
90
|
+
EXIT CODES
|
|
91
|
+
0 clean
|
|
92
|
+
1 findings at error severity (or over --max-warnings)
|
|
93
|
+
2 the tool could not run
|
|
94
|
+
|
|
95
|
+
CONFIGURATION
|
|
96
|
+
Anything repeated on every run belongs in the repository rather than in the
|
|
97
|
+
command. spec-graph reads the first of these that exists:
|
|
98
|
+
|
|
99
|
+
.spec-graph.json, spec-graph.config.json, or a "spec-graph" key in
|
|
100
|
+
package.json
|
|
101
|
+
|
|
102
|
+
{ "patterns": ["docs/**/*.md"],
|
|
103
|
+
"ignoreReferences": ["trap *"],
|
|
104
|
+
"ignoreFamilies": ["RFC"],
|
|
105
|
+
"historyPatterns": ["**/JOURNAL_*.md", "archive/**"],
|
|
106
|
+
"baseline": ".spec-graph-baseline.json",
|
|
107
|
+
"severities": { "self-reference": "off" },
|
|
108
|
+
"strict": true }
|
|
109
|
+
|
|
110
|
+
A flag always wins over the file, and list flags add to it rather than
|
|
111
|
+
replacing it.
|
|
112
|
+
|
|
113
|
+
ADOPTING THIS ON AN OLD REPOSITORY
|
|
114
|
+
Record what is already wrong, then report only what happens next:
|
|
115
|
+
|
|
116
|
+
spec-graph check --record-baseline .spec-graph-baseline.json
|
|
117
|
+
git add .spec-graph-baseline.json
|
|
118
|
+
|
|
119
|
+
The file is keyed by specification and citation, not by line number, so it
|
|
120
|
+
survives edits, moves and renames. Findings that stop occurring are reported
|
|
121
|
+
so the file can be tightened; nothing new gets in.
|
|
122
|
+
|
|
123
|
+
EXAMPLES
|
|
124
|
+
spec-graph "docs/**/*.md"
|
|
125
|
+
spec-graph check --rule self-reference=off --format json
|
|
126
|
+
spec-graph query 'item[openness=open] -delegates-to-> document[phase=retired]'
|
|
127
|
+
spec-graph check --ignore-ref "trap *" # [[trap 55]] tags a concept, not a file
|
|
128
|
+
spec-graph graph --documents-only --graph-format mermaid > graph.mmd
|
|
129
|
+
spec-graph check --history "**/JOURNAL_*.md" # a log is not a specification
|
|
130
|
+
spec-graph check --baseline .spec-graph-baseline.json
|
|
131
|
+
`;
|
|
132
|
+
const SEVERITIES = ['error', 'warn', 'info', 'off'];
|
|
133
|
+
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
134
|
+
export function parseArgs(argv, cwd) {
|
|
135
|
+
const args = [...argv];
|
|
136
|
+
let command = 'check';
|
|
137
|
+
if (args.length > 0 && !(args[0].startsWith('-'))) {
|
|
138
|
+
const first = args[0];
|
|
139
|
+
if (first === 'check' || first === 'query' || first === 'graph' || first === 'rules') {
|
|
140
|
+
command = first;
|
|
141
|
+
args.shift();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const patterns = [];
|
|
145
|
+
const ignore = [];
|
|
146
|
+
const ignoreReferences = [];
|
|
147
|
+
const families = [];
|
|
148
|
+
const ignoreFamilies = [];
|
|
149
|
+
const historyPatterns = [];
|
|
150
|
+
let noConfig = false;
|
|
151
|
+
const severities = {};
|
|
152
|
+
let root = cwd;
|
|
153
|
+
let format = 'human';
|
|
154
|
+
let graphFormat = 'dot';
|
|
155
|
+
let color = null;
|
|
156
|
+
let ascii = null;
|
|
157
|
+
let verbose = false;
|
|
158
|
+
let documentsOnly = false;
|
|
159
|
+
let max = 0;
|
|
160
|
+
let maxWarnings = -1;
|
|
161
|
+
let strict = false;
|
|
162
|
+
let baseline = null;
|
|
163
|
+
let recordBaseline = null;
|
|
164
|
+
let selector = null;
|
|
165
|
+
let help = false;
|
|
166
|
+
let version = false;
|
|
167
|
+
const next = (flag, index) => {
|
|
168
|
+
const value = args[index + 1];
|
|
169
|
+
if (value === undefined)
|
|
170
|
+
throw new UsageError(`${flag} needs a value`);
|
|
171
|
+
return value;
|
|
172
|
+
};
|
|
173
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
174
|
+
const arg = args[i];
|
|
175
|
+
if (arg === '--') {
|
|
176
|
+
patterns.push(...args.slice(i + 1));
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (!arg.startsWith('-')) {
|
|
180
|
+
if (command === 'query' && selector === null)
|
|
181
|
+
selector = arg;
|
|
182
|
+
else
|
|
183
|
+
patterns.push(arg);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
switch (arg) {
|
|
187
|
+
case '-h':
|
|
188
|
+
case '--help':
|
|
189
|
+
help = true;
|
|
190
|
+
break;
|
|
191
|
+
case '-v':
|
|
192
|
+
case '--version':
|
|
193
|
+
version = true;
|
|
194
|
+
break;
|
|
195
|
+
case '--verbose':
|
|
196
|
+
verbose = true;
|
|
197
|
+
break;
|
|
198
|
+
case '--color':
|
|
199
|
+
color = true;
|
|
200
|
+
break;
|
|
201
|
+
case '--no-color':
|
|
202
|
+
color = false;
|
|
203
|
+
break;
|
|
204
|
+
case '--ascii':
|
|
205
|
+
ascii = true;
|
|
206
|
+
break;
|
|
207
|
+
case '--documents-only':
|
|
208
|
+
documentsOnly = true;
|
|
209
|
+
break;
|
|
210
|
+
case '--strict':
|
|
211
|
+
strict = true;
|
|
212
|
+
break;
|
|
213
|
+
case '--explain':
|
|
214
|
+
verbose = true;
|
|
215
|
+
break;
|
|
216
|
+
case '--root':
|
|
217
|
+
root = next(arg, i);
|
|
218
|
+
i += 1;
|
|
219
|
+
break;
|
|
220
|
+
case '--ignore':
|
|
221
|
+
ignore.push(next(arg, i));
|
|
222
|
+
i += 1;
|
|
223
|
+
break;
|
|
224
|
+
case '--ignore-ref':
|
|
225
|
+
ignoreReferences.push(next(arg, i));
|
|
226
|
+
i += 1;
|
|
227
|
+
break;
|
|
228
|
+
case '--family':
|
|
229
|
+
families.push(next(arg, i));
|
|
230
|
+
i += 1;
|
|
231
|
+
break;
|
|
232
|
+
case '--ignore-family':
|
|
233
|
+
ignoreFamilies.push(next(arg, i));
|
|
234
|
+
i += 1;
|
|
235
|
+
break;
|
|
236
|
+
case '--history':
|
|
237
|
+
historyPatterns.push(next(arg, i));
|
|
238
|
+
i += 1;
|
|
239
|
+
break;
|
|
240
|
+
case '--baseline':
|
|
241
|
+
baseline = next(arg, i);
|
|
242
|
+
i += 1;
|
|
243
|
+
break;
|
|
244
|
+
case '--record-baseline':
|
|
245
|
+
recordBaseline = next(arg, i);
|
|
246
|
+
i += 1;
|
|
247
|
+
break;
|
|
248
|
+
case '--no-config':
|
|
249
|
+
noConfig = true;
|
|
250
|
+
break;
|
|
251
|
+
case '--format': {
|
|
252
|
+
const value = next(arg, i);
|
|
253
|
+
if (value !== 'human' && value !== 'json') {
|
|
254
|
+
throw new UsageError(`--format must be human or json, got "${value}"`);
|
|
255
|
+
}
|
|
256
|
+
format = value;
|
|
257
|
+
i += 1;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case '--graph-format': {
|
|
261
|
+
const value = next(arg, i);
|
|
262
|
+
if (value !== 'dot' && value !== 'mermaid' && value !== 'json') {
|
|
263
|
+
throw new UsageError(`--graph-format must be dot, mermaid or json, got "${value}"`);
|
|
264
|
+
}
|
|
265
|
+
graphFormat = value;
|
|
266
|
+
i += 1;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case '--rule': {
|
|
270
|
+
const value = next(arg, i);
|
|
271
|
+
const equals = value.indexOf('=');
|
|
272
|
+
if (equals === -1)
|
|
273
|
+
throw new UsageError(`--rule expects <id>=<severity>, got "${value}"`);
|
|
274
|
+
const id = value.slice(0, equals);
|
|
275
|
+
const level = value.slice(equals + 1);
|
|
276
|
+
if (!RULE_IDS.includes(id)) {
|
|
277
|
+
throw new UsageError(`unknown rule "${id}"\n known rules: ${RULE_IDS.join(', ')}`);
|
|
278
|
+
}
|
|
279
|
+
if (!SEVERITIES.includes(level)) {
|
|
280
|
+
throw new UsageError(`unknown severity "${level}", expected one of: ${SEVERITIES.join(', ')}`);
|
|
281
|
+
}
|
|
282
|
+
severities[id] = level;
|
|
283
|
+
i += 1;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case '--max': {
|
|
287
|
+
max = parseCount(arg, next(arg, i));
|
|
288
|
+
i += 1;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case '--max-warnings': {
|
|
292
|
+
maxWarnings = parseCount(arg, next(arg, i));
|
|
293
|
+
i += 1;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
default:
|
|
297
|
+
throw new UsageError(`unknown option "${arg}"\n run "spec-graph --help" to see the available options`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (command === 'query' && selector === null && !help && !version) {
|
|
301
|
+
throw new UsageError('query needs a selector, for example:\n spec-graph query \'item[openness=open]\'');
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
command,
|
|
305
|
+
patterns,
|
|
306
|
+
root,
|
|
307
|
+
ignore,
|
|
308
|
+
ignoreReferences,
|
|
309
|
+
families,
|
|
310
|
+
ignoreFamilies,
|
|
311
|
+
historyPatterns,
|
|
312
|
+
baseline,
|
|
313
|
+
recordBaseline,
|
|
314
|
+
noConfig,
|
|
315
|
+
format,
|
|
316
|
+
graphFormat,
|
|
317
|
+
severities,
|
|
318
|
+
color,
|
|
319
|
+
ascii,
|
|
320
|
+
verbose,
|
|
321
|
+
documentsOnly,
|
|
322
|
+
max,
|
|
323
|
+
maxWarnings,
|
|
324
|
+
strict,
|
|
325
|
+
selector,
|
|
326
|
+
help,
|
|
327
|
+
version,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function count(n, word, many) {
|
|
331
|
+
return `${n} ${n === 1 ? word : (many ?? `${word}s`)}`;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Reads a baseline file.
|
|
335
|
+
*
|
|
336
|
+
* A missing file is not an error: `--baseline` against a repository that has
|
|
337
|
+
* not recorded one yet should report everything, which is exactly what an empty
|
|
338
|
+
* baseline does.
|
|
339
|
+
*/
|
|
340
|
+
async function readBaseline(path, source) {
|
|
341
|
+
let raw;
|
|
342
|
+
try {
|
|
343
|
+
const { readFile } = await import('node:fs/promises');
|
|
344
|
+
raw = await readFile(path, 'utf8');
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return { baseline: EMPTY_BASELINE, problems: [] };
|
|
348
|
+
}
|
|
349
|
+
return parseBaseline(raw, source);
|
|
350
|
+
}
|
|
351
|
+
function parseCount(flag, value) {
|
|
352
|
+
const parsed = Number.parseInt(value, 10);
|
|
353
|
+
if (!Number.isFinite(parsed) || parsed < 0 || String(parsed) !== value.trim()) {
|
|
354
|
+
throw new UsageError(`${flag} expects a non-negative whole number, got "${value}"`);
|
|
355
|
+
}
|
|
356
|
+
return parsed;
|
|
357
|
+
}
|
|
358
|
+
/* -------------------------------------------------------------------------- */
|
|
359
|
+
/* Entry point */
|
|
360
|
+
/* -------------------------------------------------------------------------- */
|
|
361
|
+
/** Runs the CLI and returns the process exit code. Never throws. */
|
|
362
|
+
export async function main(io = {}) {
|
|
363
|
+
const argv = io.argv ?? process.argv.slice(2);
|
|
364
|
+
const cwd = io.cwd ?? process.cwd();
|
|
365
|
+
const out = io.stdout ?? ((text) => process.stdout.write(text));
|
|
366
|
+
const err = io.stderr ?? ((text) => process.stderr.write(text));
|
|
367
|
+
const env = io.env ?? process.env;
|
|
368
|
+
let options;
|
|
369
|
+
try {
|
|
370
|
+
options = parseArgs(argv, cwd);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
if (error instanceof UsageError) {
|
|
374
|
+
err(`spec-graph: ${error.message}\n`);
|
|
375
|
+
return EXIT_ERROR;
|
|
376
|
+
}
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
if (options.help) {
|
|
380
|
+
out(HELP);
|
|
381
|
+
return EXIT_OK;
|
|
382
|
+
}
|
|
383
|
+
if (options.version) {
|
|
384
|
+
out(`${await readVersion()}\n`);
|
|
385
|
+
return EXIT_OK;
|
|
386
|
+
}
|
|
387
|
+
const color = options.color ?? shouldUseColor({ isTTY: io.isTTY, env });
|
|
388
|
+
const ascii = options.ascii ?? shouldUseAscii({ env });
|
|
389
|
+
if (options.command === 'rules') {
|
|
390
|
+
out(renderRules(options.verbose));
|
|
391
|
+
return EXIT_OK;
|
|
392
|
+
}
|
|
393
|
+
// Configuration is what is true of the repository; a flag is somebody
|
|
394
|
+
// overriding it for one run. So a flag always wins, and lists add rather than
|
|
395
|
+
// replace - a `--ignore-ref` on the command line is one more exclusion, not a
|
|
396
|
+
// decision to throw away the ones the repository already declared.
|
|
397
|
+
const loaded = options.noConfig ? { config: {}, source: null, problems: [] } : loadConfig(options.root);
|
|
398
|
+
for (const problem of loaded.problems)
|
|
399
|
+
err(`spec-graph: ${problem}\n`);
|
|
400
|
+
const file = loaded.config;
|
|
401
|
+
const patterns = options.patterns.length > 0 ? options.patterns : (file.patterns ?? DEFAULT_PATTERNS);
|
|
402
|
+
const severityOverrides = { ...(file.severities ?? {}), ...options.severities };
|
|
403
|
+
const { severities, escalated } = resolveStrict(severityOverrides, options.strict || (file.strict ?? false));
|
|
404
|
+
const analyseOptions = {
|
|
405
|
+
root: options.root,
|
|
406
|
+
patterns,
|
|
407
|
+
ignore: [...(file.ignore ?? []), ...options.ignore],
|
|
408
|
+
ignoreReferences: [...(file.ignoreReferences ?? []), ...options.ignoreReferences],
|
|
409
|
+
families: [...(file.families ?? []), ...options.families],
|
|
410
|
+
ignoreFamilies: [...(file.ignoreFamilies ?? []), ...options.ignoreFamilies],
|
|
411
|
+
historyPatterns: [...(file.historyPatterns ?? []), ...options.historyPatterns],
|
|
412
|
+
severities,
|
|
413
|
+
...(file.maxRelated !== undefined ? { maxRelated: file.maxRelated } : {}),
|
|
414
|
+
};
|
|
415
|
+
if (options.verbose && loaded.source !== null)
|
|
416
|
+
out(`configuration: ${loaded.source}\n`);
|
|
417
|
+
let result;
|
|
418
|
+
try {
|
|
419
|
+
result = await analyse(analyseOptions);
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
err(`spec-graph: ${error.message}\n`);
|
|
423
|
+
return EXIT_ERROR;
|
|
424
|
+
}
|
|
425
|
+
if (result.files.length === 0) {
|
|
426
|
+
err(`spec-graph: no specifications matched ${options.patterns.length > 0 ? options.patterns.map((p) => `"${p}"`).join(', ') : 'the default patterns'}\n looked under ${options.root}\n`);
|
|
427
|
+
return EXIT_ERROR;
|
|
428
|
+
}
|
|
429
|
+
switch (options.command) {
|
|
430
|
+
case 'graph':
|
|
431
|
+
out(formatGraph(result.graph, options.graphFormat, { documentsOnly: options.documentsOnly }));
|
|
432
|
+
return EXIT_OK;
|
|
433
|
+
case 'query': {
|
|
434
|
+
try {
|
|
435
|
+
const matches = execute(result.graph, parseQuery(options.selector));
|
|
436
|
+
out(renderMatches(matches, options.format, result.graph.nodes.size));
|
|
437
|
+
return matches.length > 0 ? EXIT_OK : EXIT_FAILED;
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
if (error instanceof QueryError) {
|
|
441
|
+
err(renderQueryError(options.selector, error));
|
|
442
|
+
return EXIT_ERROR;
|
|
443
|
+
}
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
default: {
|
|
448
|
+
// Recording is not checking. It writes down what is wrong today so that
|
|
449
|
+
// tomorrow can be compared against it, and says nothing about whether
|
|
450
|
+
// today is acceptable - so it reports what it wrote and exits clean.
|
|
451
|
+
if (options.recordBaseline !== null) {
|
|
452
|
+
const text = formatBaseline(result.graph, result.diagnostics);
|
|
453
|
+
try {
|
|
454
|
+
const { writeFile } = await import('node:fs/promises');
|
|
455
|
+
await writeFile(underRoot(options.root, options.recordBaseline), text, 'utf8');
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
err(`spec-graph: cannot write ${options.recordBaseline}: ${error.message}\n`);
|
|
459
|
+
return EXIT_ERROR;
|
|
460
|
+
}
|
|
461
|
+
const entries = JSON.parse(text).findings.length;
|
|
462
|
+
out(`recorded ${count(result.diagnostics.length, 'finding')} as ${count(entries, 'entry', 'entries')} in ${options.recordBaseline}\n`);
|
|
463
|
+
return EXIT_OK;
|
|
464
|
+
}
|
|
465
|
+
const source = options.baseline ?? file.baseline ?? null;
|
|
466
|
+
let reported = result;
|
|
467
|
+
let note;
|
|
468
|
+
if (source !== null) {
|
|
469
|
+
const held = await readBaseline(underRoot(options.root, source), source);
|
|
470
|
+
for (const problem of held.problems)
|
|
471
|
+
err(`spec-graph: ${problem}\n`);
|
|
472
|
+
const outcome = applyBaseline(result.graph, result.diagnostics, held.baseline);
|
|
473
|
+
reported = withDiagnostics(result, outcome.kept);
|
|
474
|
+
note = { source, suppressed: outcome.suppressed, stale: outcome.stale.length };
|
|
475
|
+
if (options.verbose) {
|
|
476
|
+
for (const entry of outcome.stale) {
|
|
477
|
+
out(` paid: ${entry.rule} ${entry.document}${entry.subject === '' ? '' : ` "${entry.subject}"`}\n`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const baselineNote = note === undefined ? {} : { baseline: note };
|
|
482
|
+
out(options.format === 'json'
|
|
483
|
+
? formatJson(reported, { escalated, ...baselineNote })
|
|
484
|
+
: `${formatReport(reported, { color, ascii, verbose: options.verbose, max: options.max, escalated, ...baselineNote })}\n`);
|
|
485
|
+
if (!reported.ok)
|
|
486
|
+
return EXIT_FAILED;
|
|
487
|
+
if (options.maxWarnings >= 0 && reported.summary.warnings > options.maxWarnings)
|
|
488
|
+
return EXIT_FAILED;
|
|
489
|
+
return EXIT_OK;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/* -------------------------------------------------------------------------- */
|
|
494
|
+
/* Rendering */
|
|
495
|
+
/* -------------------------------------------------------------------------- */
|
|
496
|
+
function renderRules(explain) {
|
|
497
|
+
const width = Math.max(...RULE_IDS.map((id) => id.length));
|
|
498
|
+
const lines = [];
|
|
499
|
+
for (const id of RULE_IDS) {
|
|
500
|
+
lines.push(`${id.padEnd(width)} ${DEFAULT_SEVERITIES[id].padEnd(5)} ${RULE_DESCRIPTIONS[id]}`);
|
|
501
|
+
const query = RULE_QUERIES[id];
|
|
502
|
+
if (explain && query)
|
|
503
|
+
lines.push(`${' '.repeat(width)} ${query}`);
|
|
504
|
+
}
|
|
505
|
+
return `${lines.join('\n')}\n`;
|
|
506
|
+
}
|
|
507
|
+
function renderMatches(matches, format, total) {
|
|
508
|
+
if (format === 'json') {
|
|
509
|
+
return `${JSON.stringify({
|
|
510
|
+
version: 1,
|
|
511
|
+
count: matches.length,
|
|
512
|
+
matches: matches.map((match) => ({
|
|
513
|
+
nodes: match.nodes.map((node) => nodeSummary(node)),
|
|
514
|
+
edges: match.edges.map((edge) => ({
|
|
515
|
+
kind: edge.kind,
|
|
516
|
+
from: edge.from,
|
|
517
|
+
to: edge.to,
|
|
518
|
+
file: edge.declaredAt.file,
|
|
519
|
+
line: edge.declaredAt.span.start.line,
|
|
520
|
+
})),
|
|
521
|
+
})),
|
|
522
|
+
}, null, 2)}\n`;
|
|
523
|
+
}
|
|
524
|
+
if (matches.length === 0)
|
|
525
|
+
return `no matches (searched ${total} nodes)\n`;
|
|
526
|
+
const lines = [];
|
|
527
|
+
for (const match of matches) {
|
|
528
|
+
const head = match.nodes[0];
|
|
529
|
+
const tail = match.nodes[match.nodes.length - 1];
|
|
530
|
+
lines.push(renderMatch(match));
|
|
531
|
+
lines.push(` ${formatRef(head.at)}${match.nodes.length > 1 ? ` ${formatRef(tail.at)}` : ''}`);
|
|
532
|
+
}
|
|
533
|
+
lines.push(`${matches.length} ${matches.length === 1 ? 'match' : 'matches'}`);
|
|
534
|
+
return `${lines.join('\n')}\n`;
|
|
535
|
+
}
|
|
536
|
+
function nodeSummary(node) {
|
|
537
|
+
return {
|
|
538
|
+
id: node.id,
|
|
539
|
+
kind: node.kind,
|
|
540
|
+
title: node.title,
|
|
541
|
+
file: node.at.file,
|
|
542
|
+
line: node.at.span.start.line,
|
|
543
|
+
...(node.kind === 'document' ? { phase: node.phase } : { state: node.disposition, openness: node.openness }),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/** Points at the exact character of a selector that failed to parse. */
|
|
547
|
+
function renderQueryError(selector, error) {
|
|
548
|
+
const caret = `${' '.repeat(Math.max(0, error.offset))}^`;
|
|
549
|
+
return `spec-graph: ${error.message}\n ${selector}\n ${caret}\n`;
|
|
550
|
+
}
|
|
551
|
+
async function readVersion() {
|
|
552
|
+
try {
|
|
553
|
+
const { readFile } = await import('node:fs/promises');
|
|
554
|
+
const url = new URL('../package.json', import.meta.url);
|
|
555
|
+
const parsed = JSON.parse(await readFile(url, 'utf8'));
|
|
556
|
+
if (typeof parsed === 'object' && parsed !== null && 'version' in parsed) {
|
|
557
|
+
return String(parsed.version);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
// Fall through to the placeholder rather than failing `--version`.
|
|
562
|
+
}
|
|
563
|
+
return '0.0.0';
|
|
564
|
+
}
|
|
565
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The command line.\n *\n * Four verbs, because there are four questions worth asking of a specification\n * graph: is it consistent (`check`), what does it contain (`query`), what does\n * it look like (`graph`), and what will you check for me (`rules`).\n *\n * Exit codes are the contract with CI: `0` clean, `1` findings, `2` the tool\n * itself could not run. A usage mistake never masquerades as a passing build.\n */\n\nimport { applyBaseline, EMPTY_BASELINE, formatBaseline, parseBaseline, type Baseline } from './baseline.js';\nimport { loadConfig, type SpecGraphConfig } from './config.js';\nimport { underRoot } from './glob.js';\nimport { analyse, DEFAULT_PATTERNS, withDiagnostics, type AnalyseOptions } from './runner.js';\nimport { formatGraph, formatJson, formatReport, shouldUseAscii, shouldUseColor, type GraphFormat } from './report.js';\nimport { DEFAULT_SEVERITIES, resolveStrict, RULE_DESCRIPTIONS, RULE_IDS, RULE_QUERIES } from './rules.js';\nimport { formatRef } from './source.js';\nimport { execute, parseQuery, QueryError, renderMatch, type Match } from './select.js';\nimport type { RuleId, Severity, SpecNode } from './types.js';\n\nexport const EXIT_OK = 0;\nexport const EXIT_FAILED = 1;\nexport const EXIT_ERROR = 2;\n\nexport class UsageError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'UsageError';\n }\n}\n\nexport type Command = 'check' | 'query' | 'graph' | 'rules';\n\nexport interface CliOptions {\n readonly command: Command;\n readonly patterns: readonly string[];\n readonly root: string;\n readonly ignore: readonly string[];\n /** Reference targets to leave unreported when they do not resolve. */\n readonly ignoreReferences: readonly string[];\n /** Families a bare identifier may name. Empty means any family in the corpus. */\n readonly families: readonly string[];\n /** Families that are never citations. */\n readonly ignoreFamilies: readonly string[];\n /** Files that log what was decided rather than deciding it. */\n readonly historyPatterns: readonly string[];\n /** Accepted-debt file to read. `null` means none was asked for. */\n readonly baseline: string | null;\n /** Where to write the current findings as accepted debt. */\n readonly recordBaseline: string | null;\n /** Skip the repository configuration file entirely. */\n readonly noConfig: boolean;\n readonly format: 'human' | 'json';\n readonly graphFormat: GraphFormat;\n readonly severities: Partial<Record<RuleId, Severity>>;\n readonly color: boolean | null;\n readonly ascii: boolean | null;\n readonly verbose: boolean;\n readonly documentsOnly: boolean;\n readonly max: number;\n readonly maxWarnings: number;\n /** Raise every warning to an error. Explicit `--rule` overrides still win. */\n readonly strict: boolean;\n readonly selector: string | null;\n readonly help: boolean;\n readonly version: boolean;\n}\n\nexport interface CliIO {\n readonly argv?: readonly string[] | undefined;\n readonly cwd?: string | undefined;\n readonly stdout?: ((text: string) => void) | undefined;\n readonly stderr?: ((text: string) => void) | undefined;\n readonly env?: Readonly<Record<string, string | undefined>> | undefined;\n readonly isTTY?: boolean | undefined;\n}\n\nexport const HELP = `spec-graph - turn Markdown specifications into a verifiable graph\n\nUSAGE\n spec-graph [check] [patterns...] [options]\n spec-graph query <selector> [patterns...] [options]\n spec-graph graph [patterns...] [--graph-format dot|mermaid|json]\n spec-graph rules [--explain]\n\nCOMMANDS\n check Validate the specification graph. The default.\n query Run a selector and print the matching paths.\n graph Export the graph for Graphviz, Mermaid, or another tool.\n rules List the built-in diagnostics.\n\nOPTIONS\n --root <dir> Directory the patterns resolve against (default: cwd)\n --ignore <glob> Skip paths. Repeatable.\n --ignore-ref <glob> Do not report these reference targets when they fail\n to resolve, for repositories where [[...]] tags a\n concept rather than naming a file. Repeatable.\n Suppresses findings only, never edges.\n --family <name> Families a bare identifier in prose may name. When\n given, everything else stays prose. Repeatable.\n --ignore-family <name> Families that are never citations - RFC when the repo\n cites RFC 2119 and keeps its own RFCs. Repeatable.\n --history <glob> Files that log what was decided rather than deciding\n it - journals, changelogs, minutes. Their links are\n still checked; their obligations are not. Repeatable.\n --baseline <file> Accept the findings recorded in this file and report\n only what is new since. Missing file = accept nothing.\n --record-baseline <f> Write today's findings to this file as accepted debt,\n and exit 0 without judging them.\n --no-config Ignore .spec-graph.json and the package.json key.\n --format human|json Report format (default: human)\n --graph-format <fmt> dot, mermaid or json (default: dot)\n --documents-only Leave items out of the exported graph\n --rule <id>=<severity> Override one rule: error, warn, info or off. Repeatable.\n --max <n> Show at most n findings (0 = no limit)\n --max-warnings <n> Fail when warnings exceed n (default: no limit)\n --strict Raise every warning to an error. An explicit --rule\n still wins, so --strict --rule x=warn exempts x.\n --color / --no-color Force colour on or off\n --ascii Use ASCII glyphs only\n --verbose Include parse problems and per-file detail\n -h, --help Show this help\n -v, --version Show the version\n\nSELECTORS\n A selector is a path through the graph.\n\n item[openness=open] -delegates-to-> document[phase=retired]\n document[phase=active] -assumes-> document[phase=retired]\n document =supersedes=> document\n *[path^=docs/adr] -contains-> item[state=accepted-debt]\n\n Node types: document, item, * (any)\n Attributes: id, kind, title, path, file, line, phase, status, receptivity,\n alias, document, state (or disposition), openness, section,\n text, body, evidence, conflicted, fm.<front-matter-key>\n Operators: = != ^= $= *= ~= and [attr] for \"is present\"\n Relations: -kind-> <-kind- =kind=> <=kind= (= forms are transitive)\n\nEXIT CODES\n 0 clean\n 1 findings at error severity (or over --max-warnings)\n 2 the tool could not run\n\nCONFIGURATION\n Anything repeated on every run belongs in the repository rather than in the\n command. spec-graph reads the first of these that exists:\n\n .spec-graph.json, spec-graph.config.json, or a \"spec-graph\" key in\n package.json\n\n { \"patterns\": [\"docs/**/*.md\"],\n \"ignoreReferences\": [\"trap *\"],\n \"ignoreFamilies\": [\"RFC\"],\n \"historyPatterns\": [\"**/JOURNAL_*.md\", \"archive/**\"],\n \"baseline\": \".spec-graph-baseline.json\",\n \"severities\": { \"self-reference\": \"off\" },\n \"strict\": true }\n\n A flag always wins over the file, and list flags add to it rather than\n replacing it.\n\nADOPTING THIS ON AN OLD REPOSITORY\n Record what is already wrong, then report only what happens next:\n\n spec-graph check --record-baseline .spec-graph-baseline.json\n git add .spec-graph-baseline.json\n\n The file is keyed by specification and citation, not by line number, so it\n survives edits, moves and renames. Findings that stop occurring are reported\n so the file can be tightened; nothing new gets in.\n\nEXAMPLES\n spec-graph \"docs/**/*.md\"\n spec-graph check --rule self-reference=off --format json\n spec-graph query 'item[openness=open] -delegates-to-> document[phase=retired]'\n spec-graph check --ignore-ref \"trap *\" # [[trap 55]] tags a concept, not a file\n spec-graph graph --documents-only --graph-format mermaid > graph.mmd\n spec-graph check --history \"**/JOURNAL_*.md\" # a log is not a specification\n spec-graph check --baseline .spec-graph-baseline.json\n`;\n\nconst SEVERITIES: readonly Severity[] = ['error', 'warn', 'info', 'off'];\n\n/** Parses argv into options. Throws {@link UsageError} on anything malformed. */\nexport function parseArgs(argv: readonly string[], cwd: string): CliOptions {\n const args = [...argv];\n let command: Command = 'check';\n\n if (args.length > 0 && !((args[0] as string).startsWith('-'))) {\n const first = args[0] as string;\n if (first === 'check' || first === 'query' || first === 'graph' || first === 'rules') {\n command = first;\n args.shift();\n }\n }\n\n const patterns: string[] = [];\n const ignore: string[] = [];\n const ignoreReferences: string[] = [];\n const families: string[] = [];\n const ignoreFamilies: string[] = [];\n const historyPatterns: string[] = [];\n let noConfig = false;\n const severities: Partial<Record<RuleId, Severity>> = {};\n let root = cwd;\n let format: 'human' | 'json' = 'human';\n let graphFormat: GraphFormat = 'dot';\n let color: boolean | null = null;\n let ascii: boolean | null = null;\n let verbose = false;\n let documentsOnly = false;\n let max = 0;\n let maxWarnings = -1;\n let strict = false;\n let baseline: string | null = null;\n let recordBaseline: string | null = null;\n let selector: string | null = null;\n let help = false;\n let version = false;\n\n const next = (flag: string, index: number): string => {\n const value = args[index + 1];\n if (value === undefined) throw new UsageError(`${flag} needs a value`);\n return value;\n };\n\n for (let i = 0; i < args.length; i += 1) {\n const arg = args[i] as string;\n\n if (arg === '--') {\n patterns.push(...args.slice(i + 1));\n break;\n }\n\n if (!arg.startsWith('-')) {\n if (command === 'query' && selector === null) selector = arg;\n else patterns.push(arg);\n continue;\n }\n\n switch (arg) {\n case '-h':\n case '--help':\n help = true;\n break;\n case '-v':\n case '--version':\n version = true;\n break;\n case '--verbose':\n verbose = true;\n break;\n case '--color':\n color = true;\n break;\n case '--no-color':\n color = false;\n break;\n case '--ascii':\n ascii = true;\n break;\n case '--documents-only':\n documentsOnly = true;\n break;\n case '--strict':\n strict = true;\n break;\n case '--explain':\n verbose = true;\n break;\n case '--root':\n root = next(arg, i);\n i += 1;\n break;\n case '--ignore':\n ignore.push(next(arg, i));\n i += 1;\n break;\n case '--ignore-ref':\n ignoreReferences.push(next(arg, i));\n i += 1;\n break;\n case '--family':\n families.push(next(arg, i));\n i += 1;\n break;\n case '--ignore-family':\n ignoreFamilies.push(next(arg, i));\n i += 1;\n break;\n case '--history':\n historyPatterns.push(next(arg, i));\n i += 1;\n break;\n case '--baseline':\n baseline = next(arg, i);\n i += 1;\n break;\n case '--record-baseline':\n recordBaseline = next(arg, i);\n i += 1;\n break;\n case '--no-config':\n noConfig = true;\n break;\n case '--format': {\n const value = next(arg, i);\n if (value !== 'human' && value !== 'json') {\n throw new UsageError(`--format must be human or json, got \"${value}\"`);\n }\n format = value;\n i += 1;\n break;\n }\n case '--graph-format': {\n const value = next(arg, i);\n if (value !== 'dot' && value !== 'mermaid' && value !== 'json') {\n throw new UsageError(`--graph-format must be dot, mermaid or json, got \"${value}\"`);\n }\n graphFormat = value;\n i += 1;\n break;\n }\n case '--rule': {\n const value = next(arg, i);\n const equals = value.indexOf('=');\n if (equals === -1) throw new UsageError(`--rule expects <id>=<severity>, got \"${value}\"`);\n const id = value.slice(0, equals) as RuleId;\n const level = value.slice(equals + 1) as Severity;\n if (!RULE_IDS.includes(id)) {\n throw new UsageError(`unknown rule \"${id}\"\\n known rules: ${RULE_IDS.join(', ')}`);\n }\n if (!SEVERITIES.includes(level)) {\n throw new UsageError(`unknown severity \"${level}\", expected one of: ${SEVERITIES.join(', ')}`);\n }\n severities[id] = level;\n i += 1;\n break;\n }\n case '--max': {\n max = parseCount(arg, next(arg, i));\n i += 1;\n break;\n }\n case '--max-warnings': {\n maxWarnings = parseCount(arg, next(arg, i));\n i += 1;\n break;\n }\n default:\n throw new UsageError(`unknown option \"${arg}\"\\n run \"spec-graph --help\" to see the available options`);\n }\n }\n\n if (command === 'query' && selector === null && !help && !version) {\n throw new UsageError('query needs a selector, for example:\\n spec-graph query \\'item[openness=open]\\'');\n }\n\n return {\n command,\n patterns,\n root,\n ignore,\n ignoreReferences,\n families,\n ignoreFamilies,\n historyPatterns,\n baseline,\n recordBaseline,\n noConfig,\n format,\n graphFormat,\n severities,\n color,\n ascii,\n verbose,\n documentsOnly,\n max,\n maxWarnings,\n strict,\n selector,\n help,\n version,\n };\n}\n\nfunction count(n: number, word: string, many?: string): string {\n return `${n} ${n === 1 ? word : (many ?? `${word}s`)}`;\n}\n\n/**\n * Reads a baseline file.\n *\n * A missing file is not an error: `--baseline` against a repository that has\n * not recorded one yet should report everything, which is exactly what an empty\n * baseline does.\n */\nasync function readBaseline(path: string, source: string): Promise<ReturnType<typeof parseBaseline>> {\n let raw: string;\n try {\n const { readFile } = await import('node:fs/promises');\n raw = await readFile(path, 'utf8');\n } catch {\n return { baseline: EMPTY_BASELINE, problems: [] };\n }\n return parseBaseline(raw, source);\n}\n\nfunction parseCount(flag: string, value: string): number {\n const parsed = Number.parseInt(value, 10);\n if (!Number.isFinite(parsed) || parsed < 0 || String(parsed) !== value.trim()) {\n throw new UsageError(`${flag} expects a non-negative whole number, got \"${value}\"`);\n }\n return parsed;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Entry point */\n/* -------------------------------------------------------------------------- */\n\n/** Runs the CLI and returns the process exit code. Never throws. */\nexport async function main(io: CliIO = {}): Promise<number> {\n const argv = io.argv ?? process.argv.slice(2);\n const cwd = io.cwd ?? process.cwd();\n const out = io.stdout ?? ((text) => process.stdout.write(text));\n const err = io.stderr ?? ((text) => process.stderr.write(text));\n const env = io.env ?? process.env;\n\n let options: CliOptions;\n try {\n options = parseArgs(argv, cwd);\n } catch (error) {\n if (error instanceof UsageError) {\n err(`spec-graph: ${error.message}\\n`);\n return EXIT_ERROR;\n }\n throw error;\n }\n\n if (options.help) {\n out(HELP);\n return EXIT_OK;\n }\n if (options.version) {\n out(`${await readVersion()}\\n`);\n return EXIT_OK;\n }\n\n const color = options.color ?? shouldUseColor({ isTTY: io.isTTY, env });\n const ascii = options.ascii ?? shouldUseAscii({ env });\n\n if (options.command === 'rules') {\n out(renderRules(options.verbose));\n return EXIT_OK;\n }\n\n // Configuration is what is true of the repository; a flag is somebody\n // overriding it for one run. So a flag always wins, and lists add rather than\n // replace - a `--ignore-ref` on the command line is one more exclusion, not a\n // decision to throw away the ones the repository already declared.\n const loaded = options.noConfig ? { config: {} as SpecGraphConfig, source: null, problems: [] } : loadConfig(options.root);\n for (const problem of loaded.problems) err(`spec-graph: ${problem}\\n`);\n const file = loaded.config;\n\n const patterns =\n options.patterns.length > 0 ? options.patterns : (file.patterns ?? DEFAULT_PATTERNS);\n const severityOverrides = { ...(file.severities ?? {}), ...options.severities };\n const { severities, escalated } = resolveStrict(severityOverrides, options.strict || (file.strict ?? false));\n\n const analyseOptions: AnalyseOptions = {\n root: options.root,\n patterns,\n ignore: [...(file.ignore ?? []), ...options.ignore],\n ignoreReferences: [...(file.ignoreReferences ?? []), ...options.ignoreReferences],\n families: [...(file.families ?? []), ...options.families],\n ignoreFamilies: [...(file.ignoreFamilies ?? []), ...options.ignoreFamilies],\n historyPatterns: [...(file.historyPatterns ?? []), ...options.historyPatterns],\n severities,\n ...(file.maxRelated !== undefined ? { maxRelated: file.maxRelated } : {}),\n };\n\n if (options.verbose && loaded.source !== null) out(`configuration: ${loaded.source}\\n`);\n\n let result;\n try {\n result = await analyse(analyseOptions);\n } catch (error) {\n err(`spec-graph: ${(error as Error).message}\\n`);\n return EXIT_ERROR;\n }\n\n if (result.files.length === 0) {\n err(\n `spec-graph: no specifications matched ${\n options.patterns.length > 0 ? options.patterns.map((p) => `\"${p}\"`).join(', ') : 'the default patterns'\n }\\n looked under ${options.root}\\n`,\n );\n return EXIT_ERROR;\n }\n\n switch (options.command) {\n case 'graph':\n out(formatGraph(result.graph, options.graphFormat, { documentsOnly: options.documentsOnly }));\n return EXIT_OK;\n\n case 'query': {\n try {\n const matches = execute(result.graph, parseQuery(options.selector as string));\n out(renderMatches(matches, options.format, result.graph.nodes.size));\n return matches.length > 0 ? EXIT_OK : EXIT_FAILED;\n } catch (error) {\n if (error instanceof QueryError) {\n err(renderQueryError(options.selector as string, error));\n return EXIT_ERROR;\n }\n throw error;\n }\n }\n\n default: {\n // Recording is not checking. It writes down what is wrong today so that\n // tomorrow can be compared against it, and says nothing about whether\n // today is acceptable - so it reports what it wrote and exits clean.\n if (options.recordBaseline !== null) {\n const text = formatBaseline(result.graph, result.diagnostics);\n try {\n const { writeFile } = await import('node:fs/promises');\n await writeFile(underRoot(options.root, options.recordBaseline), text, 'utf8');\n } catch (error) {\n err(`spec-graph: cannot write ${options.recordBaseline}: ${(error as Error).message}\\n`);\n return EXIT_ERROR;\n }\n const entries = (JSON.parse(text) as Baseline).findings.length;\n out(\n `recorded ${count(result.diagnostics.length, 'finding')} as ${count(entries, 'entry', 'entries')} in ${options.recordBaseline}\\n`,\n );\n return EXIT_OK;\n }\n\n const source = options.baseline ?? file.baseline ?? null;\n let reported = result;\n let note: { source: string; suppressed: number; stale: number } | undefined;\n if (source !== null) {\n const held = await readBaseline(underRoot(options.root, source), source);\n for (const problem of held.problems) err(`spec-graph: ${problem}\\n`);\n const outcome = applyBaseline(result.graph, result.diagnostics, held.baseline);\n reported = withDiagnostics(result, outcome.kept);\n note = { source, suppressed: outcome.suppressed, stale: outcome.stale.length };\n if (options.verbose) {\n for (const entry of outcome.stale) {\n out(` paid: ${entry.rule} ${entry.document}${entry.subject === '' ? '' : ` \"${entry.subject}\"`}\\n`);\n }\n }\n }\n\n const baselineNote = note === undefined ? {} : { baseline: note };\n out(\n options.format === 'json'\n ? formatJson(reported, { escalated, ...baselineNote })\n : `${formatReport(reported, { color, ascii, verbose: options.verbose, max: options.max, escalated, ...baselineNote })}\\n`,\n );\n if (!reported.ok) return EXIT_FAILED;\n if (options.maxWarnings >= 0 && reported.summary.warnings > options.maxWarnings) return EXIT_FAILED;\n return EXIT_OK;\n }\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* Rendering */\n/* -------------------------------------------------------------------------- */\n\nfunction renderRules(explain: boolean): string {\n const width = Math.max(...RULE_IDS.map((id) => id.length));\n const lines: string[] = [];\n for (const id of RULE_IDS) {\n lines.push(`${id.padEnd(width)} ${DEFAULT_SEVERITIES[id].padEnd(5)} ${RULE_DESCRIPTIONS[id]}`);\n const query = RULE_QUERIES[id];\n if (explain && query) lines.push(`${' '.repeat(width)} ${query}`);\n }\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction renderMatches(matches: readonly Match[], format: 'human' | 'json', total: number): string {\n if (format === 'json') {\n return `${JSON.stringify(\n {\n version: 1,\n count: matches.length,\n matches: matches.map((match) => ({\n nodes: match.nodes.map((node) => nodeSummary(node)),\n edges: match.edges.map((edge) => ({\n kind: edge.kind,\n from: edge.from,\n to: edge.to,\n file: edge.declaredAt.file,\n line: edge.declaredAt.span.start.line,\n })),\n })),\n },\n null,\n 2,\n )}\\n`;\n }\n\n if (matches.length === 0) return `no matches (searched ${total} nodes)\\n`;\n\n const lines: string[] = [];\n for (const match of matches) {\n const head = match.nodes[0] as SpecNode;\n const tail = match.nodes[match.nodes.length - 1] as SpecNode;\n lines.push(renderMatch(match));\n lines.push(` ${formatRef(head.at)}${match.nodes.length > 1 ? ` ${formatRef(tail.at)}` : ''}`);\n }\n lines.push(`${matches.length} ${matches.length === 1 ? 'match' : 'matches'}`);\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction nodeSummary(node: SpecNode): Record<string, unknown> {\n return {\n id: node.id,\n kind: node.kind,\n title: node.title,\n file: node.at.file,\n line: node.at.span.start.line,\n ...(node.kind === 'document' ? { phase: node.phase } : { state: node.disposition, openness: node.openness }),\n };\n}\n\n/** Points at the exact character of a selector that failed to parse. */\nfunction renderQueryError(selector: string, error: QueryError): string {\n const caret = `${' '.repeat(Math.max(0, error.offset))}^`;\n return `spec-graph: ${error.message}\\n ${selector}\\n ${caret}\\n`;\n}\n\nasync function readVersion(): Promise<string> {\n try {\n const { readFile } = await import('node:fs/promises');\n const url = new URL('../package.json', import.meta.url);\n const parsed: unknown = JSON.parse(await readFile(url, 'utf8'));\n if (typeof parsed === 'object' && parsed !== null && 'version' in parsed) {\n return String((parsed as { version: unknown }).version);\n }\n } catch {\n // Fall through to the placeholder rather than failing `--version`.\n }\n return '0.0.0';\n}\n"]}
|