@descent-vtt/spec-graph 0.2.3 → 0.4.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 +275 -0
- package/README.md +169 -3
- 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 +13 -4
- package/dist/cli.js +126 -27
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +14 -2
- package/dist/config.js +28 -1
- package/dist/config.js.map +1 -1
- package/dist/extract.d.ts +39 -2
- package/dist/extract.js +137 -8
- package/dist/extract.js.map +1 -1
- package/dist/identity.d.ts +10 -0
- package/dist/identity.js +51 -5
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/markdown.d.ts +16 -2
- package/dist/markdown.js +89 -22
- package/dist/markdown.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/report.d.ts +33 -8
- package/dist/report.js +181 -7
- package/dist/report.js.map +1 -1
- package/dist/resolve.d.ts +9 -1
- package/dist/resolve.js +181 -26
- package/dist/resolve.js.map +1 -1
- package/dist/rules.d.ts +8 -5
- package/dist/rules.js +96 -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 +11 -0
- package/dist/select.js +31 -0
- package/dist/select.js.map +1 -1
- package/dist/types.d.ts +59 -4
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
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;
|
|
@@ -34,11 +34,20 @@ export interface CliOptions {
|
|
|
34
34
|
readonly baseline: string | null;
|
|
35
35
|
/** Where to write the current findings as accepted debt. */
|
|
36
36
|
readonly recordBaseline: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Fail when the baseline allows something that no longer happens.
|
|
39
|
+
*
|
|
40
|
+
* The other side of the ratchet, and off by default for the reason ADR-0012
|
|
41
|
+
* gives: failing a build because somebody fixed something is a strange way to
|
|
42
|
+
* encourage them. A team that has decided its debt only goes one way asks for
|
|
43
|
+
* it explicitly, and then a dead exemption cannot outlive the defect.
|
|
44
|
+
*/
|
|
45
|
+
readonly ratchet: boolean;
|
|
37
46
|
/** Skip the repository configuration file entirely. */
|
|
38
47
|
readonly noConfig: boolean;
|
|
39
|
-
readonly format: 'human' | 'json';
|
|
48
|
+
readonly format: 'human' | 'json' | 'sarif';
|
|
40
49
|
readonly graphFormat: GraphFormat;
|
|
41
|
-
readonly severities: Partial<Record<
|
|
50
|
+
readonly severities: Partial<Record<AnyRuleId, Severity>>;
|
|
42
51
|
readonly color: boolean | null;
|
|
43
52
|
readonly ascii: boolean | null;
|
|
44
53
|
readonly verbose: boolean;
|
|
@@ -59,7 +68,7 @@ export interface CliIO {
|
|
|
59
68
|
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
60
69
|
readonly isTTY?: boolean | undefined;
|
|
61
70
|
}
|
|
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
|
|
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 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.\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 --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, or sarif - the interchange format\n GitHub code scanning and editors already read\n (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 JavaScript regular expression, run once per node; ^= $=\n and *= cover most cases and cannot backtrack\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\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 check --rule project:no-draft-dependency=off\n";
|
|
63
72
|
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
64
73
|
export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
|
|
65
74
|
/** Runs the CLI and returns the process exit code. Never throws. */
|
package/dist/cli.js
CHANGED
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
* Exit codes are the contract with CI: `0` clean, `1` findings, `2` the tool
|
|
9
9
|
* itself could not run. A usage mistake never masquerades as a passing build.
|
|
10
10
|
*/
|
|
11
|
-
import { applyBaseline, EMPTY_BASELINE, formatBaseline, parseBaseline } from './baseline.js';
|
|
11
|
+
import { applyBaseline, EMPTY_BASELINE, formatBaseline, parseBaseline, } from './baseline.js';
|
|
12
12
|
import { loadConfig } from './config.js';
|
|
13
13
|
import { underRoot } from './glob.js';
|
|
14
14
|
import { analyse, DEFAULT_PATTERNS, withDiagnostics } from './runner.js';
|
|
15
|
-
import { formatGraph, formatJson, formatReport, shouldUseAscii, shouldUseColor } from './report.js';
|
|
15
|
+
import { formatGraph, formatJson, formatReport, formatSarif, shouldUseAscii, shouldUseColor, } from './report.js';
|
|
16
16
|
import { DEFAULT_SEVERITIES, resolveStrict, RULE_DESCRIPTIONS, RULE_IDS, RULE_QUERIES } from './rules.js';
|
|
17
17
|
import { formatRef } from './source.js';
|
|
18
18
|
import { execute, parseQuery, QueryError, renderMatch } from './select.js';
|
|
19
|
+
import { isProjectRule } from './types.js';
|
|
19
20
|
export const EXIT_OK = 0;
|
|
20
21
|
export const EXIT_FAILED = 1;
|
|
21
22
|
export const EXIT_ERROR = 2;
|
|
@@ -37,7 +38,7 @@ COMMANDS
|
|
|
37
38
|
check Validate the specification graph. The default.
|
|
38
39
|
query Run a selector and print the matching paths.
|
|
39
40
|
graph Export the graph for Graphviz, Mermaid, or another tool.
|
|
40
|
-
rules List the built
|
|
41
|
+
rules List the diagnostics that will run, built in and project.
|
|
41
42
|
|
|
42
43
|
OPTIONS
|
|
43
44
|
--root <dir> Directory the patterns resolve against (default: cwd)
|
|
@@ -57,18 +58,25 @@ OPTIONS
|
|
|
57
58
|
only what is new since. Missing file = accept nothing.
|
|
58
59
|
--record-baseline <f> Write today's findings to this file as accepted debt,
|
|
59
60
|
and exit 0 without judging them.
|
|
61
|
+
--ratchet Also fail when a baseline entry no longer occurs, so
|
|
62
|
+
a paid-off exemption cannot outlive the defect.
|
|
60
63
|
--no-config Ignore .spec-graph.json and the package.json key.
|
|
61
|
-
--format human
|
|
64
|
+
--format <fmt> human, json, or sarif - the interchange format
|
|
65
|
+
GitHub code scanning and editors already read
|
|
66
|
+
(default: human)
|
|
62
67
|
--graph-format <fmt> dot, mermaid or json (default: dot)
|
|
63
68
|
--documents-only Leave items out of the exported graph
|
|
64
|
-
--rule <id>=<severity> Override one rule: error, warn, info or off.
|
|
69
|
+
--rule <id>=<severity> Override one rule: error, warn, info or off. A project
|
|
70
|
+
rule is named in full: project:no-draft-dependency.
|
|
71
|
+
Repeatable.
|
|
65
72
|
--max <n> Show at most n findings (0 = no limit)
|
|
66
73
|
--max-warnings <n> Fail when warnings exceed n (default: no limit)
|
|
67
74
|
--strict Raise every warning to an error. An explicit --rule
|
|
68
75
|
still wins, so --strict --rule x=warn exempts x.
|
|
69
76
|
--color / --no-color Force colour on or off
|
|
70
77
|
--ascii Use ASCII glyphs only
|
|
71
|
-
--verbose Include parse problems
|
|
78
|
+
--verbose Include parse problems, per-file detail, and every
|
|
79
|
+
reference --ignore-ref or --ignore-family silenced
|
|
72
80
|
-h, --help Show this help
|
|
73
81
|
-v, --version Show the version
|
|
74
82
|
|
|
@@ -85,6 +93,8 @@ SELECTORS
|
|
|
85
93
|
alias, document, state (or disposition), openness, section,
|
|
86
94
|
text, body, evidence, conflicted, fm.<front-matter-key>
|
|
87
95
|
Operators: = != ^= $= *= ~= and [attr] for "is present"
|
|
96
|
+
~= is a JavaScript regular expression, run once per node; ^= $=
|
|
97
|
+
and *= cover most cases and cannot backtrack
|
|
88
98
|
Relations: -kind-> <-kind- =kind=> <=kind= (= forms are transitive)
|
|
89
99
|
|
|
90
100
|
EXIT CODES
|
|
@@ -110,6 +120,30 @@ CONFIGURATION
|
|
|
110
120
|
A flag always wins over the file, and list flags add to it rather than
|
|
111
121
|
replacing it.
|
|
112
122
|
|
|
123
|
+
PROJECT RULES
|
|
124
|
+
A convention spec-graph never anticipated is a selector plus a sentence, and
|
|
125
|
+
belongs in the same file. Every rule declared here runs beside the built-ins
|
|
126
|
+
and is reported, baselined, escalated by --strict and silenced by --rule in
|
|
127
|
+
exactly the same way.
|
|
128
|
+
|
|
129
|
+
{ "rules": {
|
|
130
|
+
"no-draft-dependency": {
|
|
131
|
+
"query": "document[phase=active] -depends-on-> document[phase=draft]",
|
|
132
|
+
"message": "{0} depends on {1}, which is still a draft",
|
|
133
|
+
"hint": "wait for {1} to be accepted, or drop it from {0.path}",
|
|
134
|
+
"severity": "error" } } }
|
|
135
|
+
|
|
136
|
+
query One selector, or a list of them read as a union.
|
|
137
|
+
message The headline. {0} is the first node on the path, {1} the next;
|
|
138
|
+
{1.phase} and {0.fm.owner} read any selector attribute.
|
|
139
|
+
hint The next action. Optional, and templated the same way.
|
|
140
|
+
severity error, warn, info or off. Defaults to warn - a rule a team has
|
|
141
|
+
just written has not yet earned the right to fail their build.
|
|
142
|
+
|
|
143
|
+
The id is the name with project: in front, which is why it can never collide
|
|
144
|
+
with a built-in. A selector that does not parse, or a {2} the query can never
|
|
145
|
+
reach, is reported when the file is read rather than found missing later.
|
|
146
|
+
|
|
113
147
|
ADOPTING THIS ON AN OLD REPOSITORY
|
|
114
148
|
Record what is already wrong, then report only what happens next:
|
|
115
149
|
|
|
@@ -128,6 +162,8 @@ EXAMPLES
|
|
|
128
162
|
spec-graph graph --documents-only --graph-format mermaid > graph.mmd
|
|
129
163
|
spec-graph check --history "**/JOURNAL_*.md" # a log is not a specification
|
|
130
164
|
spec-graph check --baseline .spec-graph-baseline.json
|
|
165
|
+
spec-graph rules --explain # including this repository's own
|
|
166
|
+
spec-graph check --rule project:no-draft-dependency=off
|
|
131
167
|
`;
|
|
132
168
|
const SEVERITIES = ['error', 'warn', 'info', 'off'];
|
|
133
169
|
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
@@ -160,6 +196,7 @@ export function parseArgs(argv, cwd) {
|
|
|
160
196
|
let maxWarnings = -1;
|
|
161
197
|
let strict = false;
|
|
162
198
|
let baseline = null;
|
|
199
|
+
let ratchet = false;
|
|
163
200
|
let recordBaseline = null;
|
|
164
201
|
let selector = null;
|
|
165
202
|
let help = false;
|
|
@@ -210,6 +247,9 @@ export function parseArgs(argv, cwd) {
|
|
|
210
247
|
case '--strict':
|
|
211
248
|
strict = true;
|
|
212
249
|
break;
|
|
250
|
+
case '--ratchet':
|
|
251
|
+
ratchet = true;
|
|
252
|
+
break;
|
|
213
253
|
case '--explain':
|
|
214
254
|
verbose = true;
|
|
215
255
|
break;
|
|
@@ -250,8 +290,8 @@ export function parseArgs(argv, cwd) {
|
|
|
250
290
|
break;
|
|
251
291
|
case '--format': {
|
|
252
292
|
const value = next(arg, i);
|
|
253
|
-
if (value !== 'human' && value !== 'json') {
|
|
254
|
-
throw new UsageError(`--format must be human or
|
|
293
|
+
if (value !== 'human' && value !== 'json' && value !== 'sarif') {
|
|
294
|
+
throw new UsageError(`--format must be human, json or sarif, got "${value}"`);
|
|
255
295
|
}
|
|
256
296
|
format = value;
|
|
257
297
|
i += 1;
|
|
@@ -273,7 +313,11 @@ export function parseArgs(argv, cwd) {
|
|
|
273
313
|
throw new UsageError(`--rule expects <id>=<severity>, got "${value}"`);
|
|
274
314
|
const id = value.slice(0, equals);
|
|
275
315
|
const level = value.slice(equals + 1);
|
|
276
|
-
|
|
316
|
+
// A project rule is taken on its namespace here and checked for real
|
|
317
|
+
// once the configuration defining it has been read: arguments are
|
|
318
|
+
// parsed before any file is opened, and a flag has to be usable on that
|
|
319
|
+
// first pass whatever the repository turns out to declare.
|
|
320
|
+
if (!RULE_IDS.includes(id) && !isProjectRule(id)) {
|
|
277
321
|
throw new UsageError(`unknown rule "${id}"\n known rules: ${RULE_IDS.join(', ')}`);
|
|
278
322
|
}
|
|
279
323
|
if (!SEVERITIES.includes(level)) {
|
|
@@ -297,6 +341,12 @@ export function parseArgs(argv, cwd) {
|
|
|
297
341
|
throw new UsageError(`unknown option "${arg}"\n run "spec-graph --help" to see the available options`);
|
|
298
342
|
}
|
|
299
343
|
}
|
|
344
|
+
// SARIF is a report about findings, and only `check` produces those. Falling
|
|
345
|
+
// back to JSON would hand a pipeline something its uploader rejects with a
|
|
346
|
+
// message about a schema rather than about the command that was run.
|
|
347
|
+
if (format === 'sarif' && command !== 'check' && !help && !version) {
|
|
348
|
+
throw new UsageError(`--format sarif reports findings, so it belongs to check, not to ${command}`);
|
|
349
|
+
}
|
|
300
350
|
if (command === 'query' && selector === null && !help && !version) {
|
|
301
351
|
throw new UsageError('query needs a selector, for example:\n spec-graph query \'item[openness=open]\'');
|
|
302
352
|
}
|
|
@@ -310,6 +360,7 @@ export function parseArgs(argv, cwd) {
|
|
|
310
360
|
ignoreFamilies,
|
|
311
361
|
historyPatterns,
|
|
312
362
|
baseline,
|
|
363
|
+
ratchet,
|
|
313
364
|
recordBaseline,
|
|
314
365
|
noConfig,
|
|
315
366
|
format,
|
|
@@ -386,21 +437,36 @@ export async function main(io = {}) {
|
|
|
386
437
|
}
|
|
387
438
|
const color = options.color ?? shouldUseColor({ isTTY: io.isTTY, env });
|
|
388
439
|
const ascii = options.ascii ?? shouldUseAscii({ env });
|
|
389
|
-
if (options.command === 'rules') {
|
|
390
|
-
out(renderRules(options.verbose));
|
|
391
|
-
return EXIT_OK;
|
|
392
|
-
}
|
|
393
440
|
// Configuration is what is true of the repository; a flag is somebody
|
|
394
441
|
// overriding it for one run. So a flag always wins, and lists add rather than
|
|
395
442
|
// replace - a `--ignore-ref` on the command line is one more exclusion, not a
|
|
396
443
|
// decision to throw away the ones the repository already declared.
|
|
444
|
+
//
|
|
445
|
+
// Read before `rules` prints anything, so that command lists the conventions
|
|
446
|
+
// this repository will actually check rather than the ones spec-graph ships.
|
|
397
447
|
const loaded = options.noConfig ? { config: {}, source: null, problems: [] } : loadConfig(options.root);
|
|
398
448
|
for (const problem of loaded.problems)
|
|
399
449
|
err(`spec-graph: ${problem}\n`);
|
|
400
450
|
const file = loaded.config;
|
|
451
|
+
const projectRules = file.rules ?? [];
|
|
452
|
+
// A `--rule` naming a project rule that does not exist is a flag that
|
|
453
|
+
// silently does nothing, and nothing in the output would distinguish that
|
|
454
|
+
// from a rule that ran and found none.
|
|
455
|
+
for (const id of Object.keys(options.severities)) {
|
|
456
|
+
if (!isProjectRule(id) || projectRules.some((rule) => rule.id === id))
|
|
457
|
+
continue;
|
|
458
|
+
const known = projectRules.map((rule) => rule.id);
|
|
459
|
+
const where = known.length > 0 ? `project rules here: ${known.join(', ')}` : 'this repository defines no project rules';
|
|
460
|
+
err(`spec-graph: unknown rule "${id}"\n ${where}\n`);
|
|
461
|
+
return EXIT_ERROR;
|
|
462
|
+
}
|
|
463
|
+
if (options.command === 'rules') {
|
|
464
|
+
out(renderRules(options.verbose, projectRules));
|
|
465
|
+
return EXIT_OK;
|
|
466
|
+
}
|
|
401
467
|
const patterns = options.patterns.length > 0 ? options.patterns : (file.patterns ?? DEFAULT_PATTERNS);
|
|
402
468
|
const severityOverrides = { ...(file.severities ?? {}), ...options.severities };
|
|
403
|
-
const { severities, escalated } = resolveStrict(severityOverrides, options.strict || (file.strict ?? false));
|
|
469
|
+
const { severities, escalated } = resolveStrict(severityOverrides, options.strict || (file.strict ?? false), projectRules);
|
|
404
470
|
const analyseOptions = {
|
|
405
471
|
root: options.root,
|
|
406
472
|
patterns,
|
|
@@ -410,6 +476,7 @@ export async function main(io = {}) {
|
|
|
410
476
|
ignoreFamilies: [...(file.ignoreFamilies ?? []), ...options.ignoreFamilies],
|
|
411
477
|
historyPatterns: [...(file.historyPatterns ?? []), ...options.historyPatterns],
|
|
412
478
|
severities,
|
|
479
|
+
projectRules,
|
|
413
480
|
...(file.maxRelated !== undefined ? { maxRelated: file.maxRelated } : {}),
|
|
414
481
|
};
|
|
415
482
|
if (options.verbose && loaded.source !== null)
|
|
@@ -433,7 +500,7 @@ export async function main(io = {}) {
|
|
|
433
500
|
case 'query': {
|
|
434
501
|
try {
|
|
435
502
|
const matches = execute(result.graph, parseQuery(options.selector));
|
|
436
|
-
out(renderMatches(matches, options.format, result.graph.nodes.size));
|
|
503
|
+
out(renderMatches(matches, options.format === 'json' ? 'json' : 'human', result.graph.nodes.size));
|
|
437
504
|
return matches.length > 0 ? EXIT_OK : EXIT_FAILED;
|
|
438
505
|
}
|
|
439
506
|
catch (error) {
|
|
@@ -463,6 +530,7 @@ export async function main(io = {}) {
|
|
|
463
530
|
return EXIT_OK;
|
|
464
531
|
}
|
|
465
532
|
const source = options.baseline ?? file.baseline ?? null;
|
|
533
|
+
const ratchet = options.ratchet || file.ratchet === true;
|
|
466
534
|
let reported = result;
|
|
467
535
|
let note;
|
|
468
536
|
if (source !== null) {
|
|
@@ -471,18 +539,34 @@ export async function main(io = {}) {
|
|
|
471
539
|
err(`spec-graph: ${problem}\n`);
|
|
472
540
|
const outcome = applyBaseline(result.graph, result.diagnostics, held.baseline);
|
|
473
541
|
reported = withDiagnostics(result, outcome.kept);
|
|
474
|
-
note = {
|
|
475
|
-
|
|
542
|
+
note = {
|
|
543
|
+
source,
|
|
544
|
+
suppressed: outcome.suppressed,
|
|
545
|
+
stale: outcome.stale.length,
|
|
546
|
+
ratchet,
|
|
547
|
+
entries: outcome.stale,
|
|
548
|
+
};
|
|
549
|
+
// Listed rather than counted when the run turns on them: a number is
|
|
550
|
+
// enough to know the file has slack, and not enough to strike it. On
|
|
551
|
+
// stdout only for a human - the structured formats carry the same rows
|
|
552
|
+
// inside the document, where a stray line is the difference between
|
|
553
|
+
// parsing and not.
|
|
554
|
+
if (options.format === 'human' && (options.verbose || (ratchet && outcome.stale.length > 0))) {
|
|
476
555
|
for (const entry of outcome.stale) {
|
|
477
|
-
|
|
556
|
+
const subject = entry.subject === '' ? '' : ` "${entry.subject}"`;
|
|
557
|
+
const why = entry.reason === 'gone' ? ` - ${entry.document} is not in this corpus` : '';
|
|
558
|
+
out(` ${entry.reason}: ${entry.rule} ${entry.document}${subject}${why}\n`);
|
|
478
559
|
}
|
|
479
560
|
}
|
|
480
561
|
}
|
|
562
|
+
const looseBaseline = note !== undefined && note.ratchet && note.stale > 0;
|
|
481
563
|
const baselineNote = note === undefined ? {} : { baseline: note };
|
|
482
|
-
out(options.format === '
|
|
483
|
-
?
|
|
484
|
-
:
|
|
485
|
-
|
|
564
|
+
out(options.format === 'sarif'
|
|
565
|
+
? formatSarif(reported, reported.graph, { version: await readVersion(), escalated, projectRules })
|
|
566
|
+
: options.format === 'json'
|
|
567
|
+
? formatJson(reported, { escalated, ...baselineNote })
|
|
568
|
+
: `${formatReport(reported, { color, ascii, verbose: options.verbose, max: options.max, escalated, ...baselineNote })}\n`);
|
|
569
|
+
if (!reported.ok || looseBaseline)
|
|
486
570
|
return EXIT_FAILED;
|
|
487
571
|
if (options.maxWarnings >= 0 && reported.summary.warnings > options.maxWarnings)
|
|
488
572
|
return EXIT_FAILED;
|
|
@@ -493,12 +577,27 @@ export async function main(io = {}) {
|
|
|
493
577
|
/* -------------------------------------------------------------------------- */
|
|
494
578
|
/* Rendering */
|
|
495
579
|
/* -------------------------------------------------------------------------- */
|
|
496
|
-
function renderRules(explain) {
|
|
497
|
-
|
|
580
|
+
function renderRules(explain, projectRules = []) {
|
|
581
|
+
// A project rule is described by the selector it is, because that is what it
|
|
582
|
+
// is - its message is a template, and a template is not a description.
|
|
583
|
+
const rows = [
|
|
584
|
+
...RULE_IDS.map((id) => [
|
|
585
|
+
id,
|
|
586
|
+
DEFAULT_SEVERITIES[id],
|
|
587
|
+
RULE_DESCRIPTIONS[id],
|
|
588
|
+
RULE_QUERIES[id],
|
|
589
|
+
]),
|
|
590
|
+
...projectRules.map((rule) => [
|
|
591
|
+
rule.id,
|
|
592
|
+
rule.severity,
|
|
593
|
+
rule.sources[0],
|
|
594
|
+
rule.sources.slice(1).join(' | ') || undefined,
|
|
595
|
+
]),
|
|
596
|
+
];
|
|
597
|
+
const width = Math.max(...rows.map(([id]) => id.length));
|
|
498
598
|
const lines = [];
|
|
499
|
-
for (const id of
|
|
500
|
-
lines.push(`${id.padEnd(width)} ${
|
|
501
|
-
const query = RULE_QUERIES[id];
|
|
599
|
+
for (const [id, severity, description, query] of rows) {
|
|
600
|
+
lines.push(`${id.padEnd(width)} ${severity.padEnd(5)} ${description}`);
|
|
502
601
|
if (explain && query)
|
|
503
602
|
lines.push(`${' '.repeat(width)} ${query}`);
|
|
504
603
|
}
|