@descent-vtt/spec-graph 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +430 -5
- package/README.md +137 -7
- package/dist/baseline.d.ts +18 -4
- package/dist/baseline.js +14 -2
- package/dist/baseline.js.map +1 -1
- package/dist/cli.d.ts +12 -4
- package/dist/cli.js +267 -59
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +45 -2
- package/dist/config.js +61 -2
- package/dist/config.js.map +1 -1
- package/dist/extract.d.ts +20 -11
- package/dist/extract.js +58 -44
- package/dist/extract.js.map +1 -1
- package/dist/identity.d.ts +10 -0
- package/dist/identity.js +37 -0
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +13 -9
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/project-rules.d.ts +92 -0
- package/dist/project-rules.js +201 -0
- package/dist/project-rules.js.map +1 -0
- package/dist/regex.d.ts +64 -0
- package/dist/regex.js +708 -0
- package/dist/regex.js.map +1 -0
- package/dist/report.d.ts +34 -7
- package/dist/report.js +172 -5
- package/dist/report.js.map +1 -1
- package/dist/resolve.d.ts +8 -1
- package/dist/resolve.js +154 -30
- package/dist/resolve.js.map +1 -1
- package/dist/rules.d.ts +21 -5
- package/dist/rules.js +92 -5
- package/dist/rules.js.map +1 -1
- package/dist/runner.d.ts +5 -2
- package/dist/runner.js +1 -0
- package/dist/runner.js.map +1 -1
- package/dist/select.d.ts +19 -0
- package/dist/select.js +64 -14
- package/dist/select.js.map +1 -1
- package/dist/types.d.ts +36 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/baseline.d.ts
CHANGED
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
* the same reason extraction never touches a disk.
|
|
15
15
|
*/
|
|
16
16
|
import type { SpecGraph } from './graph.js';
|
|
17
|
-
import type
|
|
17
|
+
import { type AnyRuleId, type Diagnostic } from './types.js';
|
|
18
18
|
/** The current file format. Bumped only for a change old files cannot survive. */
|
|
19
19
|
export declare const BASELINE_VERSION = 1;
|
|
20
20
|
/** One class of accepted finding, and how many of it there were. */
|
|
21
21
|
export interface BaselineEntry {
|
|
22
|
-
readonly rule:
|
|
22
|
+
readonly rule: AnyRuleId;
|
|
23
23
|
/** The specification the finding is about, not the file it was found in. */
|
|
24
24
|
readonly document: string;
|
|
25
25
|
/** What within it: a citation target, or the document at the other end. */
|
|
@@ -70,18 +70,32 @@ export interface ParsedBaseline {
|
|
|
70
70
|
}
|
|
71
71
|
/** Parses a baseline file. Never throws; an unusable file yields an empty one. */
|
|
72
72
|
export declare function parseBaseline(raw: string, source: string): ParsedBaseline;
|
|
73
|
+
/**
|
|
74
|
+
* An entry the run did not spend, and why.
|
|
75
|
+
*
|
|
76
|
+
* The distinction is the whole value of the label. `paid` is the ratchet
|
|
77
|
+
* working: the defect was fixed and the exemption can be struck. `gone` is not
|
|
78
|
+
* an achievement at all - the document the entry is about was not in this
|
|
79
|
+
* corpus, so nothing whatever is known about the defect. Both make the file
|
|
80
|
+
* stale and both need it re-recorded, but only one of them is good news, and a
|
|
81
|
+
* report that says "paid" over a narrowed `patterns` glob is congratulating a
|
|
82
|
+
* team for losing sight of the problem.
|
|
83
|
+
*/
|
|
84
|
+
export interface StaleEntry extends BaselineEntry {
|
|
85
|
+
readonly reason: 'paid' | 'gone';
|
|
86
|
+
}
|
|
73
87
|
export interface BaselineOutcome {
|
|
74
88
|
/** Findings the baseline does not account for. These are the report. */
|
|
75
89
|
readonly kept: readonly Diagnostic[];
|
|
76
90
|
/** How many were accounted for. Reported as a number, not as a list. */
|
|
77
91
|
readonly suppressed: number;
|
|
78
92
|
/**
|
|
79
|
-
* Entries whose debt
|
|
93
|
+
* Entries whose debt was not spent, wholly or partly.
|
|
80
94
|
*
|
|
81
95
|
* The ratchet: once a repository is clean of something, saying so in the file
|
|
82
96
|
* stops it coming back unnoticed.
|
|
83
97
|
*/
|
|
84
|
-
readonly stale: readonly
|
|
98
|
+
readonly stale: readonly StaleEntry[];
|
|
85
99
|
}
|
|
86
100
|
/**
|
|
87
101
|
* Removes the findings a baseline accounts for.
|
package/dist/baseline.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* the same reason extraction never touches a disk.
|
|
15
15
|
*/
|
|
16
16
|
import { RULE_IDS } from './rules.js';
|
|
17
|
+
import { isProjectRule } from './types.js';
|
|
17
18
|
/** The current file format. Bumped only for a change old files cannot survive. */
|
|
18
19
|
export const BASELINE_VERSION = 1;
|
|
19
20
|
export const EMPTY_BASELINE = Object.freeze({ version: BASELINE_VERSION, findings: [] });
|
|
@@ -128,7 +129,12 @@ function readEntry(row, where, problems) {
|
|
|
128
129
|
}
|
|
129
130
|
const fields = row;
|
|
130
131
|
const rule = fields['rule'];
|
|
131
|
-
|
|
132
|
+
// A project rule is accepted on its namespace alone, because the baseline is
|
|
133
|
+
// read before the configuration that defines it has any say - and an entry
|
|
134
|
+
// rejected here would silently un-accept debt a team had already signed off.
|
|
135
|
+
// A name nothing defines suppresses nothing and is reported as stale, which
|
|
136
|
+
// is the same answer by a route that keeps the file readable.
|
|
137
|
+
if (typeof rule !== 'string' || !(RULE_IDS.includes(rule) || isProjectRule(rule))) {
|
|
132
138
|
problems.push(`${where}: unknown rule ${JSON.stringify(rule)}`);
|
|
133
139
|
return null;
|
|
134
140
|
}
|
|
@@ -177,7 +183,13 @@ export function applyBaseline(graph, diagnostics, baseline) {
|
|
|
177
183
|
const unspent = budget.get(key) ?? 0;
|
|
178
184
|
if (unspent <= 0)
|
|
179
185
|
continue;
|
|
180
|
-
|
|
186
|
+
// Asked of the graph rather than of the filesystem, because the graph is
|
|
187
|
+
// what this run looked at. A document that still exists on disk and fell
|
|
188
|
+
// outside the include patterns is gone for the purposes of the answer, and
|
|
189
|
+
// that is the case worth telling apart - it is the one a team reaches by
|
|
190
|
+
// editing a glob rather than by fixing anything.
|
|
191
|
+
const reason = graph.document(entry.document) === undefined ? 'gone' : 'paid';
|
|
192
|
+
stale.push({ ...entry, count: Math.min(unspent, entry.count), reason });
|
|
181
193
|
// An entry counted once; a duplicate key in a hand-edited file must not
|
|
182
194
|
// claim the same surplus twice.
|
|
183
195
|
budget.set(key, 0);
|
package/dist/baseline.js.map
CHANGED
|
@@ -1 +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"]}
|
|
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 { isProjectRule, type AnyRuleId, type Diagnostic, type 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: AnyRuleId;\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: AnyRuleId, 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 // A project rule is accepted on its namespace alone, because the baseline is\n // read before the configuration that defines it has any say - and an entry\n // rejected here would silently un-accept debt a team had already signed off.\n // A name nothing defines suppresses nothing and is reported as stale, which\n // is the same answer by a route that keeps the file readable.\n if (typeof rule !== 'string' || !(RULE_IDS.includes(rule as RuleId) || isProjectRule(rule as AnyRuleId))) {\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 AnyRuleId, document, subject, count };\n}\n\n/**\n * An entry the run did not spend, and why.\n *\n * The distinction is the whole value of the label. `paid` is the ratchet\n * working: the defect was fixed and the exemption can be struck. `gone` is not\n * an achievement at all - the document the entry is about was not in this\n * corpus, so nothing whatever is known about the defect. Both make the file\n * stale and both need it re-recorded, but only one of them is good news, and a\n * report that says \"paid\" over a narrowed `patterns` glob is congratulating a\n * team for losing sight of the problem.\n */\nexport interface StaleEntry extends BaselineEntry {\n readonly reason: 'paid' | 'gone';\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 was not spent, 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 StaleEntry[];\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: StaleEntry[] = [];\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 // Asked of the graph rather than of the filesystem, because the graph is\n // what this run looked at. A document that still exists on disk and fell\n // outside the include patterns is gone for the purposes of the answer, and\n // that is the case worth telling apart - it is the one a team reaches by\n // editing a glob rather than by fixing anything.\n const reason = graph.document(entry.document) === undefined ? 'gone' : 'paid';\n stale.push({ ...entry, count: Math.min(unspent, entry.count), reason });\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
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* itself could not run. A usage mistake never masquerades as a passing build.
|
|
10
10
|
*/
|
|
11
11
|
import { type GraphFormat } from './report.js';
|
|
12
|
-
import type
|
|
12
|
+
import { type AnyRuleId, type Severity } from './types.js';
|
|
13
13
|
export declare const EXIT_OK = 0;
|
|
14
14
|
export declare const EXIT_FAILED = 1;
|
|
15
15
|
export declare const EXIT_ERROR = 2;
|
|
@@ -21,6 +21,14 @@ export interface CliOptions {
|
|
|
21
21
|
readonly command: Command;
|
|
22
22
|
readonly patterns: readonly string[];
|
|
23
23
|
readonly root: string;
|
|
24
|
+
/**
|
|
25
|
+
* Whether `--root` was given.
|
|
26
|
+
*
|
|
27
|
+
* When it was, the caller has named the root and configuration is read from
|
|
28
|
+
* exactly there. When it was not, the root is discovered - which is a
|
|
29
|
+
* different question from what the root currently is. See ADR-0018.
|
|
30
|
+
*/
|
|
31
|
+
readonly rootExplicit: boolean;
|
|
24
32
|
readonly ignore: readonly string[];
|
|
25
33
|
/** Reference targets to leave unreported when they do not resolve. */
|
|
26
34
|
readonly ignoreReferences: readonly string[];
|
|
@@ -45,9 +53,9 @@ export interface CliOptions {
|
|
|
45
53
|
readonly ratchet: boolean;
|
|
46
54
|
/** Skip the repository configuration file entirely. */
|
|
47
55
|
readonly noConfig: boolean;
|
|
48
|
-
readonly format: 'human' | 'json' | 'sarif';
|
|
56
|
+
readonly format: 'human' | 'json' | 'sarif' | 'markdown';
|
|
49
57
|
readonly graphFormat: GraphFormat;
|
|
50
|
-
readonly severities: Partial<Record<
|
|
58
|
+
readonly severities: Partial<Record<AnyRuleId, Severity>>;
|
|
51
59
|
readonly color: boolean | null;
|
|
52
60
|
readonly ascii: boolean | null;
|
|
53
61
|
readonly verbose: boolean;
|
|
@@ -68,7 +76,7 @@ export interface CliIO {
|
|
|
68
76
|
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
69
77
|
readonly isTTY?: boolean | undefined;
|
|
70
78
|
}
|
|
71
|
-
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
|
|
79
|
+
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|project:rule> [patterns...] [options]\n spec-graph graph [patterns...] [--graph-format dot|mermaid|json]\n spec-graph rules [rule-id] [--explain]\n\nCOMMANDS\n check Validate the specification graph. The default.\n query Run a selector - or a registered project rule, by its id - and\n print the matching paths.\n graph Export the graph for Graphviz, Mermaid, or another tool.\n rules List the diagnostics that will run, built in and project. Name one\n to see only that one; --explain adds its selector and the ADR that\n decided it.\n\nOPTIONS\n --root <dir> Directory the patterns resolve against. Without it,\n .spec-graph.json is looked for in the working\n directory and then upward as far as the repository,\n and the directory holding it becomes the root - so a\n run from a subdirectory reports what a run from the\n top reports. Paths typed on the command line stay\n relative to where they were typed.\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 --ratchet Also fail when a baseline entry no longer occurs, so\n a paid-off exemption cannot outlive the defect.\n --no-config Ignore .spec-graph.json and the package.json key.\n --format <fmt> human, json, sarif or markdown. sarif is the\n interchange format GitHub code scanning and editors\n already read; markdown is a table for a pull-request\n comment or $GITHUB_STEP_SUMMARY (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. A project\n rule is named in full: project:no-draft-dependency.\n 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, per-file detail, and every\n reference --ignore-ref or --ignore-family silenced\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 ~= is a regular expression, matched by an automaton that cannot\n backtrack: linear in the subject, whatever the pattern. It reads\n the usual syntax minus backreferences and lookaround, which are\n not regular - both are refused when the selector is read, with\n the character pointed at\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\nPROJECT RULES\n A convention spec-graph never anticipated is a selector plus a sentence, and\n belongs in the same file. Every rule declared here runs beside the built-ins\n and is reported, baselined, escalated by --strict and silenced by --rule in\n exactly the same way.\n\n { \"rules\": {\n \"no-draft-dependency\": {\n \"query\": \"document[phase=active] -depends-on-> document[phase=draft]\",\n \"message\": \"{0} depends on {1}, which is still a draft\",\n \"hint\": \"wait for {1} to be accepted, or drop it from {0.path}\",\n \"severity\": \"error\" } } }\n\n query One selector, or a list of them read as a union.\n message The headline. {0} is the first node on the path, {1} the next;\n {1.phase} and {0.fm.owner} read any selector attribute.\n hint The next action. Optional, and templated the same way.\n severity error, warn, info or off. Defaults to warn - a rule a team has\n just written has not yet earned the right to fail their build.\n\n The id is the name with project: in front, which is why it can never collide\n with a built-in. A selector that does not parse, or a {2} the query can never\n reach, is reported when the file is read rather than found missing later.\n\n To see what one matches without copying its selector out of the file:\n\n spec-graph query project:no-draft-dependency --verbose\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 spec-graph rules --explain # including this repository's own\n spec-graph rules ghost-handover --explain # and why it exists\n spec-graph check --rule project:no-draft-dependency=off\n spec-graph query project:no-draft-dependency # what does that rule match?\n spec-graph check --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n";
|
|
72
80
|
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
73
81
|
export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
|
|
74
82
|
/** Runs the CLI and returns the process exit code. Never throws. */
|