@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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accepted debt, and the ratchet that keeps it from growing.
|
|
3
|
+
*
|
|
4
|
+
* A linter introduced to a repository that predates it reports everything at
|
|
5
|
+
* once. Fifty findings on day one is not a report, it is a decision to be
|
|
6
|
+
* ignored: the team cannot stop to fix them, so they turn the tool off, or they
|
|
7
|
+
* turn the rules off and the next specification rots unwatched.
|
|
8
|
+
*
|
|
9
|
+
* A baseline is the third answer. It records what was already wrong on the day
|
|
10
|
+
* the tool arrived, and reports only what happened since. Debt can be paid down
|
|
11
|
+
* whenever there is room, and in the meantime nothing new gets in.
|
|
12
|
+
*
|
|
13
|
+
* The whole module is pure. Reading and writing the file is the CLI's job, for
|
|
14
|
+
* the same reason extraction never touches a disk.
|
|
15
|
+
*/
|
|
16
|
+
import type { SpecGraph } from './graph.js';
|
|
17
|
+
import type { Diagnostic, RuleId } from './types.js';
|
|
18
|
+
/** The current file format. Bumped only for a change old files cannot survive. */
|
|
19
|
+
export declare const BASELINE_VERSION = 1;
|
|
20
|
+
/** One class of accepted finding, and how many of it there were. */
|
|
21
|
+
export interface BaselineEntry {
|
|
22
|
+
readonly rule: RuleId;
|
|
23
|
+
/** The specification the finding is about, not the file it was found in. */
|
|
24
|
+
readonly document: string;
|
|
25
|
+
/** What within it: a citation target, or the document at the other end. */
|
|
26
|
+
readonly subject: string;
|
|
27
|
+
readonly count: number;
|
|
28
|
+
}
|
|
29
|
+
export interface Baseline {
|
|
30
|
+
readonly version: number;
|
|
31
|
+
readonly findings: readonly BaselineEntry[];
|
|
32
|
+
}
|
|
33
|
+
export declare const EMPTY_BASELINE: Baseline;
|
|
34
|
+
/**
|
|
35
|
+
* What makes two findings the same finding.
|
|
36
|
+
*
|
|
37
|
+
* Not the line. A baseline keyed on line numbers is invalidated by any edit
|
|
38
|
+
* above it, which turns every unrelated pull request into a wall of findings
|
|
39
|
+
* nobody introduced. Not the message either: rewording a diagnostic would
|
|
40
|
+
* silently expire every baseline in the world.
|
|
41
|
+
*
|
|
42
|
+
* What survives is the pair the finding is actually about - which specification,
|
|
43
|
+
* and what within it. Both are ids that outlive a file being renamed, a section
|
|
44
|
+
* being moved into its own file, or an obligation being reordered, because that
|
|
45
|
+
* is exactly what identifiers were made stable for (ADR-0009).
|
|
46
|
+
*
|
|
47
|
+
* The cost is deliberate coarseness: two broken links from one document to the
|
|
48
|
+
* same target are one entry with a count of two. Paying that buys a baseline
|
|
49
|
+
* that does not lie the first time somebody reformats a paragraph.
|
|
50
|
+
*/
|
|
51
|
+
export declare function fingerprintOf(graph: SpecGraph, diagnostic: Diagnostic): {
|
|
52
|
+
document: string;
|
|
53
|
+
subject: string;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Renders a baseline for the findings given.
|
|
57
|
+
*
|
|
58
|
+
* Sorted by rule, then document, then subject, and written with two-space
|
|
59
|
+
* indentation: the file lands in a repository and is read in diffs, so its byte
|
|
60
|
+
* order has to be a function of its content and nothing else. No timestamp, for
|
|
61
|
+
* the same reason - a generated date makes every re-record a change even when
|
|
62
|
+
* nothing changed.
|
|
63
|
+
*/
|
|
64
|
+
export declare function formatBaseline(graph: SpecGraph, diagnostics: readonly Diagnostic[]): string;
|
|
65
|
+
export interface ParsedBaseline {
|
|
66
|
+
readonly baseline: Baseline;
|
|
67
|
+
/** Reported, never silently dropped. A baseline nobody can read is worse
|
|
68
|
+
* than none: it suppresses findings the reader cannot account for. */
|
|
69
|
+
readonly problems: readonly string[];
|
|
70
|
+
}
|
|
71
|
+
/** Parses a baseline file. Never throws; an unusable file yields an empty one. */
|
|
72
|
+
export declare function parseBaseline(raw: string, source: string): ParsedBaseline;
|
|
73
|
+
export interface BaselineOutcome {
|
|
74
|
+
/** Findings the baseline does not account for. These are the report. */
|
|
75
|
+
readonly kept: readonly Diagnostic[];
|
|
76
|
+
/** How many were accounted for. Reported as a number, not as a list. */
|
|
77
|
+
readonly suppressed: number;
|
|
78
|
+
/**
|
|
79
|
+
* Entries whose debt has been paid, wholly or partly.
|
|
80
|
+
*
|
|
81
|
+
* The ratchet: once a repository is clean of something, saying so in the file
|
|
82
|
+
* stops it coming back unnoticed.
|
|
83
|
+
*/
|
|
84
|
+
readonly stale: readonly BaselineEntry[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Removes the findings a baseline accounts for.
|
|
88
|
+
*
|
|
89
|
+
* Where a baseline allows two of something and three exist, one is reported.
|
|
90
|
+
* Which one is not a meaningful question - they share a fingerprint - so the
|
|
91
|
+
* findings are taken in report order and the surplus is what is left.
|
|
92
|
+
*/
|
|
93
|
+
export declare function applyBaseline(graph: SpecGraph, diagnostics: readonly Diagnostic[], baseline: Baseline): BaselineOutcome;
|
package/dist/baseline.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accepted debt, and the ratchet that keeps it from growing.
|
|
3
|
+
*
|
|
4
|
+
* A linter introduced to a repository that predates it reports everything at
|
|
5
|
+
* once. Fifty findings on day one is not a report, it is a decision to be
|
|
6
|
+
* ignored: the team cannot stop to fix them, so they turn the tool off, or they
|
|
7
|
+
* turn the rules off and the next specification rots unwatched.
|
|
8
|
+
*
|
|
9
|
+
* A baseline is the third answer. It records what was already wrong on the day
|
|
10
|
+
* the tool arrived, and reports only what happened since. Debt can be paid down
|
|
11
|
+
* whenever there is room, and in the meantime nothing new gets in.
|
|
12
|
+
*
|
|
13
|
+
* The whole module is pure. Reading and writing the file is the CLI's job, for
|
|
14
|
+
* the same reason extraction never touches a disk.
|
|
15
|
+
*/
|
|
16
|
+
import { RULE_IDS } from './rules.js';
|
|
17
|
+
/** The current file format. Bumped only for a change old files cannot survive. */
|
|
18
|
+
export const BASELINE_VERSION = 1;
|
|
19
|
+
export const EMPTY_BASELINE = Object.freeze({ version: BASELINE_VERSION, findings: [] });
|
|
20
|
+
/**
|
|
21
|
+
* What makes two findings the same finding.
|
|
22
|
+
*
|
|
23
|
+
* Not the line. A baseline keyed on line numbers is invalidated by any edit
|
|
24
|
+
* above it, which turns every unrelated pull request into a wall of findings
|
|
25
|
+
* nobody introduced. Not the message either: rewording a diagnostic would
|
|
26
|
+
* silently expire every baseline in the world.
|
|
27
|
+
*
|
|
28
|
+
* What survives is the pair the finding is actually about - which specification,
|
|
29
|
+
* and what within it. Both are ids that outlive a file being renamed, a section
|
|
30
|
+
* being moved into its own file, or an obligation being reordered, because that
|
|
31
|
+
* is exactly what identifiers were made stable for (ADR-0009).
|
|
32
|
+
*
|
|
33
|
+
* The cost is deliberate coarseness: two broken links from one document to the
|
|
34
|
+
* same target are one entry with a count of two. Paying that buys a baseline
|
|
35
|
+
* that does not lie the first time somebody reformats a paragraph.
|
|
36
|
+
*/
|
|
37
|
+
export function fingerprintOf(graph, diagnostic) {
|
|
38
|
+
const first = diagnostic.nodes[0] ?? '';
|
|
39
|
+
const document = graph.owningDocument(first)?.id ?? first;
|
|
40
|
+
if (diagnostic.target !== null)
|
|
41
|
+
return { document, subject: diagnostic.target };
|
|
42
|
+
const second = diagnostic.nodes[1];
|
|
43
|
+
const subject = second === undefined ? '' : (graph.owningDocument(second)?.id ?? second);
|
|
44
|
+
// A document-level rule lists its own items as the rest of `nodes`, so the
|
|
45
|
+
// other end resolves back to where it started. Saying so twice in the file
|
|
46
|
+
// would only invite a reader to look for a distinction that is not there.
|
|
47
|
+
return { document, subject: subject === document ? '' : subject };
|
|
48
|
+
}
|
|
49
|
+
function keyOf(rule, document, subject) {
|
|
50
|
+
// Tab-joined: the three parts are ids and targets, and a tab is the one
|
|
51
|
+
// character none of them can contain.
|
|
52
|
+
return `${rule}\t${document}\t${subject}`;
|
|
53
|
+
}
|
|
54
|
+
/** Counts findings by fingerprint. */
|
|
55
|
+
function tally(graph, diagnostics) {
|
|
56
|
+
const out = new Map();
|
|
57
|
+
for (const diagnostic of diagnostics) {
|
|
58
|
+
const { document, subject } = fingerprintOf(graph, diagnostic);
|
|
59
|
+
const key = keyOf(diagnostic.rule, document, subject);
|
|
60
|
+
const seen = out.get(key);
|
|
61
|
+
out.set(key, {
|
|
62
|
+
rule: diagnostic.rule,
|
|
63
|
+
document,
|
|
64
|
+
subject,
|
|
65
|
+
count: (seen?.count ?? 0) + 1,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Renders a baseline for the findings given.
|
|
72
|
+
*
|
|
73
|
+
* Sorted by rule, then document, then subject, and written with two-space
|
|
74
|
+
* indentation: the file lands in a repository and is read in diffs, so its byte
|
|
75
|
+
* order has to be a function of its content and nothing else. No timestamp, for
|
|
76
|
+
* the same reason - a generated date makes every re-record a change even when
|
|
77
|
+
* nothing changed.
|
|
78
|
+
*/
|
|
79
|
+
export function formatBaseline(graph, diagnostics) {
|
|
80
|
+
const findings = [...tally(graph, diagnostics).values()].sort(compareEntries);
|
|
81
|
+
return `${JSON.stringify({ version: BASELINE_VERSION, findings }, null, 2)}\n`;
|
|
82
|
+
}
|
|
83
|
+
function compareEntries(a, b) {
|
|
84
|
+
if (a.rule !== b.rule)
|
|
85
|
+
return a.rule < b.rule ? -1 : 1;
|
|
86
|
+
if (a.document !== b.document)
|
|
87
|
+
return a.document < b.document ? -1 : 1;
|
|
88
|
+
if (a.subject !== b.subject)
|
|
89
|
+
return a.subject < b.subject ? -1 : 1;
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
/** Parses a baseline file. Never throws; an unusable file yields an empty one. */
|
|
93
|
+
export function parseBaseline(raw, source) {
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
return { baseline: EMPTY_BASELINE, problems: [`${source} is not valid JSON: ${error.message}`] };
|
|
100
|
+
}
|
|
101
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
102
|
+
return { baseline: EMPTY_BASELINE, problems: [`${source} must contain a JSON object`] };
|
|
103
|
+
}
|
|
104
|
+
const record = parsed;
|
|
105
|
+
const problems = [];
|
|
106
|
+
if (record['version'] !== BASELINE_VERSION) {
|
|
107
|
+
return {
|
|
108
|
+
baseline: EMPTY_BASELINE,
|
|
109
|
+
problems: [`${source} is version ${String(record['version'])}, and this build reads version ${BASELINE_VERSION}`],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const rows = record['findings'];
|
|
113
|
+
if (!Array.isArray(rows)) {
|
|
114
|
+
return { baseline: EMPTY_BASELINE, problems: [`${source}: "findings" must be an array`] };
|
|
115
|
+
}
|
|
116
|
+
const findings = [];
|
|
117
|
+
for (const [index, row] of rows.entries()) {
|
|
118
|
+
const entry = readEntry(row, `${source}: findings[${index}]`, problems);
|
|
119
|
+
if (entry !== null)
|
|
120
|
+
findings.push(entry);
|
|
121
|
+
}
|
|
122
|
+
return { baseline: { version: BASELINE_VERSION, findings }, problems };
|
|
123
|
+
}
|
|
124
|
+
function readEntry(row, where, problems) {
|
|
125
|
+
if (typeof row !== 'object' || row === null || Array.isArray(row)) {
|
|
126
|
+
problems.push(`${where} must be an object`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const fields = row;
|
|
130
|
+
const rule = fields['rule'];
|
|
131
|
+
if (typeof rule !== 'string' || !RULE_IDS.includes(rule)) {
|
|
132
|
+
problems.push(`${where}: unknown rule ${JSON.stringify(rule)}`);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const document = fields['document'];
|
|
136
|
+
const subject = fields['subject'] ?? '';
|
|
137
|
+
const count = fields['count'] ?? 1;
|
|
138
|
+
if (typeof document !== 'string' || typeof subject !== 'string') {
|
|
139
|
+
problems.push(`${where}: "document" and "subject" must be strings`);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (typeof count !== 'number' || !Number.isInteger(count) || count < 1) {
|
|
143
|
+
problems.push(`${where}: "count" must be a whole number of at least 1`);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
return { rule: rule, document, subject, count };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Removes the findings a baseline accounts for.
|
|
150
|
+
*
|
|
151
|
+
* Where a baseline allows two of something and three exist, one is reported.
|
|
152
|
+
* Which one is not a meaningful question - they share a fingerprint - so the
|
|
153
|
+
* findings are taken in report order and the surplus is what is left.
|
|
154
|
+
*/
|
|
155
|
+
export function applyBaseline(graph, diagnostics, baseline) {
|
|
156
|
+
const budget = new Map();
|
|
157
|
+
for (const entry of baseline.findings) {
|
|
158
|
+
const key = keyOf(entry.rule, entry.document, entry.subject);
|
|
159
|
+
budget.set(key, (budget.get(key) ?? 0) + entry.count);
|
|
160
|
+
}
|
|
161
|
+
const kept = [];
|
|
162
|
+
let suppressed = 0;
|
|
163
|
+
for (const diagnostic of diagnostics) {
|
|
164
|
+
const { document, subject } = fingerprintOf(graph, diagnostic);
|
|
165
|
+
const key = keyOf(diagnostic.rule, document, subject);
|
|
166
|
+
const left = budget.get(key) ?? 0;
|
|
167
|
+
if (left <= 0) {
|
|
168
|
+
kept.push(diagnostic);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
budget.set(key, left - 1);
|
|
172
|
+
suppressed += 1;
|
|
173
|
+
}
|
|
174
|
+
const stale = [];
|
|
175
|
+
for (const entry of baseline.findings) {
|
|
176
|
+
const key = keyOf(entry.rule, entry.document, entry.subject);
|
|
177
|
+
const unspent = budget.get(key) ?? 0;
|
|
178
|
+
if (unspent <= 0)
|
|
179
|
+
continue;
|
|
180
|
+
stale.push({ ...entry, count: Math.min(unspent, entry.count) });
|
|
181
|
+
// An entry counted once; a duplicate key in a hand-edited file must not
|
|
182
|
+
// claim the same surplus twice.
|
|
183
|
+
budget.set(key, 0);
|
|
184
|
+
}
|
|
185
|
+
return { kept, suppressed, stale: stale.sort(compareEntries) };
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=baseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Accepted debt, and the ratchet that keeps it from growing.\n *\n * A linter introduced to a repository that predates it reports everything at\n * once. Fifty findings on day one is not a report, it is a decision to be\n * ignored: the team cannot stop to fix them, so they turn the tool off, or they\n * turn the rules off and the next specification rots unwatched.\n *\n * A baseline is the third answer. It records what was already wrong on the day\n * the tool arrived, and reports only what happened since. Debt can be paid down\n * whenever there is room, and in the meantime nothing new gets in.\n *\n * The whole module is pure. Reading and writing the file is the CLI's job, for\n * the same reason extraction never touches a disk.\n */\n\nimport type { SpecGraph } from './graph.js';\nimport { RULE_IDS } from './rules.js';\nimport type { Diagnostic, RuleId } from './types.js';\n\n/** The current file format. Bumped only for a change old files cannot survive. */\nexport const BASELINE_VERSION = 1;\n\n/** One class of accepted finding, and how many of it there were. */\nexport interface BaselineEntry {\n readonly rule: RuleId;\n /** The specification the finding is about, not the file it was found in. */\n readonly document: string;\n /** What within it: a citation target, or the document at the other end. */\n readonly subject: string;\n readonly count: number;\n}\n\nexport interface Baseline {\n readonly version: number;\n readonly findings: readonly BaselineEntry[];\n}\n\nexport const EMPTY_BASELINE: Baseline = Object.freeze({ version: BASELINE_VERSION, findings: [] });\n\n/**\n * What makes two findings the same finding.\n *\n * Not the line. A baseline keyed on line numbers is invalidated by any edit\n * above it, which turns every unrelated pull request into a wall of findings\n * nobody introduced. Not the message either: rewording a diagnostic would\n * silently expire every baseline in the world.\n *\n * What survives is the pair the finding is actually about - which specification,\n * and what within it. Both are ids that outlive a file being renamed, a section\n * being moved into its own file, or an obligation being reordered, because that\n * is exactly what identifiers were made stable for (ADR-0009).\n *\n * The cost is deliberate coarseness: two broken links from one document to the\n * same target are one entry with a count of two. Paying that buys a baseline\n * that does not lie the first time somebody reformats a paragraph.\n */\nexport function fingerprintOf(graph: SpecGraph, diagnostic: Diagnostic): { document: string; subject: string } {\n const first = diagnostic.nodes[0] ?? '';\n const document = graph.owningDocument(first)?.id ?? first;\n if (diagnostic.target !== null) return { document, subject: diagnostic.target };\n\n const second = diagnostic.nodes[1];\n const subject = second === undefined ? '' : (graph.owningDocument(second)?.id ?? second);\n // A document-level rule lists its own items as the rest of `nodes`, so the\n // other end resolves back to where it started. Saying so twice in the file\n // would only invite a reader to look for a distinction that is not there.\n return { document, subject: subject === document ? '' : subject };\n}\n\nfunction keyOf(rule: RuleId, document: string, subject: string): string {\n // Tab-joined: the three parts are ids and targets, and a tab is the one\n // character none of them can contain.\n return `${rule}\\t${document}\\t${subject}`;\n}\n\n/** Counts findings by fingerprint. */\nfunction tally(graph: SpecGraph, diagnostics: readonly Diagnostic[]): Map<string, BaselineEntry> {\n const out = new Map<string, BaselineEntry>();\n for (const diagnostic of diagnostics) {\n const { document, subject } = fingerprintOf(graph, diagnostic);\n const key = keyOf(diagnostic.rule, document, subject);\n const seen = out.get(key);\n out.set(key, {\n rule: diagnostic.rule,\n document,\n subject,\n count: (seen?.count ?? 0) + 1,\n });\n }\n return out;\n}\n\n/**\n * Renders a baseline for the findings given.\n *\n * Sorted by rule, then document, then subject, and written with two-space\n * indentation: the file lands in a repository and is read in diffs, so its byte\n * order has to be a function of its content and nothing else. No timestamp, for\n * the same reason - a generated date makes every re-record a change even when\n * nothing changed.\n */\nexport function formatBaseline(graph: SpecGraph, diagnostics: readonly Diagnostic[]): string {\n const findings = [...tally(graph, diagnostics).values()].sort(compareEntries);\n return `${JSON.stringify({ version: BASELINE_VERSION, findings }, null, 2)}\\n`;\n}\n\nfunction compareEntries(a: BaselineEntry, b: BaselineEntry): number {\n if (a.rule !== b.rule) return a.rule < b.rule ? -1 : 1;\n if (a.document !== b.document) return a.document < b.document ? -1 : 1;\n if (a.subject !== b.subject) return a.subject < b.subject ? -1 : 1;\n return 0;\n}\n\nexport interface ParsedBaseline {\n readonly baseline: Baseline;\n /** Reported, never silently dropped. A baseline nobody can read is worse\n * than none: it suppresses findings the reader cannot account for. */\n readonly problems: readonly string[];\n}\n\n/** Parses a baseline file. Never throws; an unusable file yields an empty one. */\nexport function parseBaseline(raw: string, source: string): ParsedBaseline {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw);\n } catch (error) {\n return { baseline: EMPTY_BASELINE, problems: [`${source} is not valid JSON: ${(error as Error).message}`] };\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { baseline: EMPTY_BASELINE, problems: [`${source} must contain a JSON object`] };\n }\n\n const record = parsed as Record<string, unknown>;\n const problems: string[] = [];\n if (record['version'] !== BASELINE_VERSION) {\n return {\n baseline: EMPTY_BASELINE,\n problems: [`${source} is version ${String(record['version'])}, and this build reads version ${BASELINE_VERSION}`],\n };\n }\n\n const rows = record['findings'];\n if (!Array.isArray(rows)) {\n return { baseline: EMPTY_BASELINE, problems: [`${source}: \"findings\" must be an array`] };\n }\n\n const findings: BaselineEntry[] = [];\n for (const [index, row] of rows.entries()) {\n const entry = readEntry(row, `${source}: findings[${index}]`, problems);\n if (entry !== null) findings.push(entry);\n }\n return { baseline: { version: BASELINE_VERSION, findings }, problems };\n}\n\nfunction readEntry(row: unknown, where: string, problems: string[]): BaselineEntry | null {\n if (typeof row !== 'object' || row === null || Array.isArray(row)) {\n problems.push(`${where} must be an object`);\n return null;\n }\n const fields = row as Record<string, unknown>;\n const rule = fields['rule'];\n if (typeof rule !== 'string' || !RULE_IDS.includes(rule as RuleId)) {\n problems.push(`${where}: unknown rule ${JSON.stringify(rule)}`);\n return null;\n }\n const document = fields['document'];\n const subject = fields['subject'] ?? '';\n const count = fields['count'] ?? 1;\n if (typeof document !== 'string' || typeof subject !== 'string') {\n problems.push(`${where}: \"document\" and \"subject\" must be strings`);\n return null;\n }\n if (typeof count !== 'number' || !Number.isInteger(count) || count < 1) {\n problems.push(`${where}: \"count\" must be a whole number of at least 1`);\n return null;\n }\n return { rule: rule as RuleId, document, subject, count };\n}\n\nexport interface BaselineOutcome {\n /** Findings the baseline does not account for. These are the report. */\n readonly kept: readonly Diagnostic[];\n /** How many were accounted for. Reported as a number, not as a list. */\n readonly suppressed: number;\n /**\n * Entries whose debt has been paid, wholly or partly.\n *\n * The ratchet: once a repository is clean of something, saying so in the file\n * stops it coming back unnoticed.\n */\n readonly stale: readonly BaselineEntry[];\n}\n\n/**\n * Removes the findings a baseline accounts for.\n *\n * Where a baseline allows two of something and three exist, one is reported.\n * Which one is not a meaningful question - they share a fingerprint - so the\n * findings are taken in report order and the surplus is what is left.\n */\nexport function applyBaseline(\n graph: SpecGraph,\n diagnostics: readonly Diagnostic[],\n baseline: Baseline,\n): BaselineOutcome {\n const budget = new Map<string, number>();\n for (const entry of baseline.findings) {\n const key = keyOf(entry.rule, entry.document, entry.subject);\n budget.set(key, (budget.get(key) ?? 0) + entry.count);\n }\n\n const kept: Diagnostic[] = [];\n let suppressed = 0;\n for (const diagnostic of diagnostics) {\n const { document, subject } = fingerprintOf(graph, diagnostic);\n const key = keyOf(diagnostic.rule, document, subject);\n const left = budget.get(key) ?? 0;\n if (left <= 0) {\n kept.push(diagnostic);\n continue;\n }\n budget.set(key, left - 1);\n suppressed += 1;\n }\n\n const stale: BaselineEntry[] = [];\n for (const entry of baseline.findings) {\n const key = keyOf(entry.rule, entry.document, entry.subject);\n const unspent = budget.get(key) ?? 0;\n if (unspent <= 0) continue;\n stale.push({ ...entry, count: Math.min(unspent, entry.count) });\n // An entry counted once; a duplicate key in a hand-edited file must not\n // claim the same surplus twice.\n budget.set(key, 0);\n }\n\n return { kept, suppressed, stale: stale.sort(compareEntries) };\n}\n"]}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
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 { type GraphFormat } from './report.js';
|
|
12
|
+
import type { RuleId, Severity } from './types.js';
|
|
13
|
+
export declare const EXIT_OK = 0;
|
|
14
|
+
export declare const EXIT_FAILED = 1;
|
|
15
|
+
export declare const EXIT_ERROR = 2;
|
|
16
|
+
export declare class UsageError extends Error {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
export type Command = 'check' | 'query' | 'graph' | 'rules';
|
|
20
|
+
export interface CliOptions {
|
|
21
|
+
readonly command: Command;
|
|
22
|
+
readonly patterns: readonly string[];
|
|
23
|
+
readonly root: string;
|
|
24
|
+
readonly ignore: readonly string[];
|
|
25
|
+
/** Reference targets to leave unreported when they do not resolve. */
|
|
26
|
+
readonly ignoreReferences: readonly string[];
|
|
27
|
+
/** Families a bare identifier may name. Empty means any family in the corpus. */
|
|
28
|
+
readonly families: readonly string[];
|
|
29
|
+
/** Families that are never citations. */
|
|
30
|
+
readonly ignoreFamilies: readonly string[];
|
|
31
|
+
/** Files that log what was decided rather than deciding it. */
|
|
32
|
+
readonly historyPatterns: readonly string[];
|
|
33
|
+
/** Accepted-debt file to read. `null` means none was asked for. */
|
|
34
|
+
readonly baseline: string | null;
|
|
35
|
+
/** Where to write the current findings as accepted debt. */
|
|
36
|
+
readonly recordBaseline: string | null;
|
|
37
|
+
/** Skip the repository configuration file entirely. */
|
|
38
|
+
readonly noConfig: boolean;
|
|
39
|
+
readonly format: 'human' | 'json';
|
|
40
|
+
readonly graphFormat: GraphFormat;
|
|
41
|
+
readonly severities: Partial<Record<RuleId, Severity>>;
|
|
42
|
+
readonly color: boolean | null;
|
|
43
|
+
readonly ascii: boolean | null;
|
|
44
|
+
readonly verbose: boolean;
|
|
45
|
+
readonly documentsOnly: boolean;
|
|
46
|
+
readonly max: number;
|
|
47
|
+
readonly maxWarnings: number;
|
|
48
|
+
/** Raise every warning to an error. Explicit `--rule` overrides still win. */
|
|
49
|
+
readonly strict: boolean;
|
|
50
|
+
readonly selector: string | null;
|
|
51
|
+
readonly help: boolean;
|
|
52
|
+
readonly version: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface CliIO {
|
|
55
|
+
readonly argv?: readonly string[] | undefined;
|
|
56
|
+
readonly cwd?: string | undefined;
|
|
57
|
+
readonly stdout?: ((text: string) => void) | undefined;
|
|
58
|
+
readonly stderr?: ((text: string) => void) | undefined;
|
|
59
|
+
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
60
|
+
readonly isTTY?: boolean | undefined;
|
|
61
|
+
}
|
|
62
|
+
export declare 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";
|
|
63
|
+
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
64
|
+
export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
|
|
65
|
+
/** Runs the CLI and returns the process exit code. Never throws. */
|
|
66
|
+
export declare function main(io?: CliIO): Promise<number>;
|