@descent-vtt/spec-graph 0.9.10 → 0.11.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 +86 -2
- package/README.md +468 -313
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +112 -67
- package/dist/cli.js.map +1 -1
- package/dist/extract.js +66 -6
- package/dist/extract.js.map +1 -1
- package/dist/glob.d.ts +11 -0
- package/dist/glob.js +17 -1
- package/dist/glob.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.js +5 -2
- package/dist/lifecycle.js.map +1 -1
- package/dist/report.d.ts +22 -0
- package/dist/report.js +50 -3
- package/dist/report.js.map +1 -1
- package/dist/sections.d.ts +7 -1
- package/dist/sections.js +9 -1
- package/dist/sections.js.map +1 -1
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +11 -1
- package/dist/vendor/spec-core/markdown/frontmatter.js +20 -3
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
- package/dist/vendor/spec-core/pattern/glob.d.ts +85 -0
- package/dist/vendor/spec-core/pattern/glob.js +207 -17
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -1
- package/dist/vendor/spec-core/pattern/index.d.ts +2 -2
- package/dist/vendor/spec-core/pattern/index.js +1 -1
- package/dist/vendor/spec-core/pattern/index.js.map +1 -1
- package/dist/vendor/spec-core/text/index.d.ts +1 -0
- package/dist/vendor/spec-core/text/index.js +1 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -1
- package/dist/vendor/spec-core/text/width.d.ts +30 -0
- package/dist/vendor/spec-core/text/width.js +113 -0
- package/dist/vendor/spec-core/text/width.js.map +1 -0
- package/dist/yaml.js +6 -2
- package/dist/yaml.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -54,7 +54,7 @@ export interface CliOptions {
|
|
|
54
54
|
readonly ratchet: boolean;
|
|
55
55
|
/** Skip the repository configuration file entirely. */
|
|
56
56
|
readonly noConfig: boolean;
|
|
57
|
-
readonly format: 'human' | 'json' | 'sarif' | 'markdown' | 'gitlab';
|
|
57
|
+
readonly format: 'human' | 'json' | 'sarif' | 'markdown' | 'gitlab' | 'github';
|
|
58
58
|
readonly graphFormat: GraphFormat;
|
|
59
59
|
readonly severities: Partial<Record<AnyRuleId, Severity>>;
|
|
60
60
|
readonly color: boolean | null;
|
|
@@ -77,7 +77,7 @@ export interface CliIO {
|
|
|
77
77
|
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
78
78
|
readonly isTTY?: boolean | undefined;
|
|
79
79
|
}
|
|
80
|
-
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 spec-graph diff <before.json> <after.json> [--format human|json|markdown]\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 diff Compare two graph exports: documents added, removed, moved or\n accepted; relations added or removed; and obligations resolved or\n reopened where they can be told apart. Make both exports with the\n same spec-graph. Exits 0 whether anything changed or not.\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. The file has to be there: a\n path that reads nothing accepts nothing and says so\n nowhere. A \"baseline\" in the configuration may name a\n file not recorded yet; a path typed here may not.\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, gitlab or markdown. sarif is the\n interchange format GitHub code scanning and editors\n already read, and gitlab the Code Quality report a\n GitLab merge request reads, both for check; markdown\n is a table for a pull-request comment or\n $GITHUB_STEP_SUMMARY, for check or diff\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 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 \"maxRelated\": 3 }\n\n A flag always wins over the file, and list flags add to it rather than\n replacing it. maxRelated has no flag: it caps how many related locations one\n finding lists, 8 unless set, and 0 lists none.\n\n A problem in that file stops the run with exit 2, named on stderr: invalid\n JSON, an unknown key, a rule that does not compile. A configuration that did\n not load checks a different repository than the one configured, and would\n report that one as consistent. --no-config runs on defaults instead.\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 spec-graph check --format gitlab > gl-code-quality-report.json\n spec-graph diff base.json head.json --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n";
|
|
80
|
+
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 spec-graph diff <before.json> <after.json> [--format human|json|markdown]\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 diff Compare two graph exports: documents added, removed, moved or\n accepted; relations added or removed; and obligations resolved or\n reopened where they can be told apart. Make both exports with the\n same spec-graph. Exits 0 whether anything changed or not.\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. The file has to be there: a\n path that reads nothing accepts nothing and says so\n nowhere. A \"baseline\" in the configuration may name a\n file not recorded yet; a path typed here may not.\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, github, gitlab or markdown. sarif\n is the interchange format GitHub code scanning and\n editors already read, github the workflow commands a\n GitHub Actions log turns into annotations, and gitlab\n the Code Quality report a GitLab merge request reads,\n all three for check; markdown is a table for a\n pull-request comment or $GITHUB_STEP_SUMMARY, for\n check or diff (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 \"maxRelated\": 3 }\n\n A flag always wins over the file, and list flags add to it rather than\n replacing it. maxRelated has no flag: it caps how many related locations one\n finding lists, 8 unless set, and 0 lists none.\n\n A problem in that file stops the run with exit 2, named on stderr: invalid\n JSON, an unknown key, a rule that does not compile. A configuration that did\n not load checks a different repository than the one configured, and would\n report that one as consistent. --no-config runs on defaults instead.\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 spec-graph check --format github # annotations from an Actions log\n spec-graph check --format gitlab > gl-code-quality-report.json\n spec-graph diff base.json head.json --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n";
|
|
81
81
|
/** Parses argv into options. Throws {@link UsageError} on anything malformed. */
|
|
82
82
|
export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
|
|
83
83
|
/** Runs the CLI and returns the process exit code. Never throws. */
|
package/dist/cli.js
CHANGED
|
@@ -12,14 +12,15 @@
|
|
|
12
12
|
import { applyBaseline, EMPTY_BASELINE, formatBaseline, parseBaseline, } from './baseline.js';
|
|
13
13
|
import { discoverConfig, loadConfig } from './config.js';
|
|
14
14
|
import { DiffInputError, diffExports, formatDiffJson, formatDiffMarkdown, formatDiffText, parseGraphExport, } from './diff.js';
|
|
15
|
-
import { isGlob, underRoot } from './glob.js';
|
|
15
|
+
import { isGlob, rebasePattern, underRoot } from './glob.js';
|
|
16
16
|
import { analyse, DEFAULT_PATTERNS, withDiagnostics } from './runner.js';
|
|
17
|
-
import { formatGitlab, formatGraph, formatJson, formatMarkdown, formatReport, formatSarif, shouldUseAscii, shouldUseColor, } from './report.js';
|
|
17
|
+
import { formatGithub, formatGitlab, formatGraph, formatJson, formatMarkdown, formatReport, formatSarif, shouldUseAscii, shouldUseColor, } from './report.js';
|
|
18
18
|
import { DEFAULT_SEVERITIES, resolveStrict, RULE_DECISIONS, decisionUrl, RULE_DESCRIPTIONS, RULE_IDS, RULE_QUERIES, } from './rules.js';
|
|
19
|
-
import { isAbsolutePath, toPosix } from './paths.js';
|
|
19
|
+
import { isAbsolutePath, joinPosix, toPosix } from './paths.js';
|
|
20
20
|
import { formatRef } from './source.js';
|
|
21
21
|
import { execute, parseQuery, QueryError, renderMatch } from './select.js';
|
|
22
22
|
import { isProjectRule } from './types.js';
|
|
23
|
+
import { displayWidth } from './vendor/spec-core/text/index.js';
|
|
23
24
|
export const EXIT_OK = 0;
|
|
24
25
|
export const EXIT_FAILED = 1;
|
|
25
26
|
export const EXIT_ERROR = 2;
|
|
@@ -81,13 +82,14 @@ OPTIONS
|
|
|
81
82
|
--ratchet Also fail when a baseline entry no longer occurs, so
|
|
82
83
|
a paid-off exemption cannot outlive the defect.
|
|
83
84
|
--no-config Ignore .spec-graph.json and the package.json key.
|
|
84
|
-
--format <fmt> human, json, sarif, gitlab or markdown. sarif
|
|
85
|
-
interchange format GitHub code scanning and
|
|
86
|
-
already read,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
--format <fmt> human, json, sarif, github, gitlab or markdown. sarif
|
|
86
|
+
is the interchange format GitHub code scanning and
|
|
87
|
+
editors already read, github the workflow commands a
|
|
88
|
+
GitHub Actions log turns into annotations, and gitlab
|
|
89
|
+
the Code Quality report a GitLab merge request reads,
|
|
90
|
+
all three for check; markdown is a table for a
|
|
91
|
+
pull-request comment or $GITHUB_STEP_SUMMARY, for
|
|
92
|
+
check or diff (default: human)
|
|
91
93
|
--graph-format <fmt> dot, mermaid or json (default: dot)
|
|
92
94
|
--documents-only Leave items out of the exported graph
|
|
93
95
|
--rule <id>=<severity> Override one rule: error, warn, info or off. A project
|
|
@@ -205,6 +207,7 @@ EXAMPLES
|
|
|
205
207
|
spec-graph check --rule project:no-draft-dependency=off
|
|
206
208
|
spec-graph query project:no-draft-dependency # what does that rule match?
|
|
207
209
|
spec-graph check --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
210
|
+
spec-graph check --format github # annotations from an Actions log
|
|
208
211
|
spec-graph check --format gitlab > gl-code-quality-report.json
|
|
209
212
|
spec-graph diff base.json head.json --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
210
213
|
`;
|
|
@@ -333,8 +336,13 @@ export function parseArgs(argv, cwd) {
|
|
|
333
336
|
break;
|
|
334
337
|
case '--format': {
|
|
335
338
|
const value = next(arg, i);
|
|
336
|
-
if (value !== 'human' &&
|
|
337
|
-
|
|
339
|
+
if (value !== 'human' &&
|
|
340
|
+
value !== 'json' &&
|
|
341
|
+
value !== 'sarif' &&
|
|
342
|
+
value !== 'markdown' &&
|
|
343
|
+
value !== 'gitlab' &&
|
|
344
|
+
value !== 'github') {
|
|
345
|
+
throw new UsageError(`--format must be human, json, sarif, markdown, gitlab or github, got "${value}"`);
|
|
338
346
|
}
|
|
339
347
|
format = value;
|
|
340
348
|
i += 1;
|
|
@@ -384,12 +392,12 @@ export function parseArgs(argv, cwd) {
|
|
|
384
392
|
throw new UsageError(`unknown option "${arg}"\n run "spec-graph --help" to see the available options`);
|
|
385
393
|
}
|
|
386
394
|
}
|
|
387
|
-
// SARIF and GitLab's Code Quality are reports
|
|
388
|
-
// `check` produces those. Falling back to JSON would
|
|
389
|
-
// its uploader rejects with a message about a
|
|
390
|
-
// command that was run. Markdown is for a pull
|
|
391
|
-
// or a diff.
|
|
392
|
-
if ((format === 'sarif' || format === 'gitlab') && command !== 'check' && !help && !version) {
|
|
395
|
+
// SARIF, GitHub's workflow commands and GitLab's Code Quality are reports
|
|
396
|
+
// about findings, and only `check` produces those. Falling back to JSON would
|
|
397
|
+
// hand a pipeline something its uploader rejects with a message about a
|
|
398
|
+
// schema rather than about the command that was run. Markdown is for a pull
|
|
399
|
+
// request, which reads a check or a diff.
|
|
400
|
+
if ((format === 'sarif' || format === 'gitlab' || format === 'github') && command !== 'check' && !help && !version) {
|
|
393
401
|
throw new UsageError(`--format ${format} reports findings, so it belongs to check, not to ${command}`);
|
|
394
402
|
}
|
|
395
403
|
if (format === 'markdown' && command !== 'check' && command !== 'diff' && !help && !version) {
|
|
@@ -554,41 +562,53 @@ export async function main(io = {}) {
|
|
|
554
562
|
out(renderRules(options.verbose, await readVersion(), projectRules, wanted ?? null, loaded.source));
|
|
555
563
|
return EXIT_OK;
|
|
556
564
|
}
|
|
557
|
-
|
|
565
|
+
// Named the way the reader would have to type it, because a discovered
|
|
566
|
+
// configuration is often not the one in front of them.
|
|
567
|
+
//
|
|
568
|
+
// On stderr, with everything else this run says about itself. stdout is the
|
|
569
|
+
// report, and in three of the four formats it is a document: a line above it
|
|
570
|
+
// made `--verbose --format json` unparseable and handed `--format sarif` to a
|
|
571
|
+
// code-scanning uploader that rejects it over a schema.
|
|
572
|
+
if (options.verbose && loaded.source !== null)
|
|
573
|
+
err(`configuration: ${fromHere(here, loaded.source)}\n`);
|
|
574
|
+
// A bare `--ignore` name prunes a directory of that name at any depth, the
|
|
575
|
+
// way a .gitignore line does, so it means the same thing wherever it was
|
|
576
|
+
// typed. Every other pattern is matched against the repository-relative
|
|
577
|
+
// path, a bare `--history` name included, and leaving one alone would make
|
|
578
|
+
// it silently match something else or nothing. A pattern that cannot be
|
|
579
|
+
// rebased onto the root stops the run as one the dialect refuses does,
|
|
580
|
+
// named as it was typed.
|
|
581
|
+
let typed;
|
|
582
|
+
try {
|
|
583
|
+
typed = {
|
|
584
|
+
patterns: options.patterns.map((pattern) => anchor(here, pattern)),
|
|
585
|
+
ignore: options.ignore.map((pattern) => anchorPath(here, pattern)),
|
|
586
|
+
history: options.historyPatterns.map((pattern) => anchor(here, pattern)),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
err(`spec-graph: ${error.message}\n`);
|
|
591
|
+
return EXIT_ERROR;
|
|
592
|
+
}
|
|
593
|
+
const patterns = typed.patterns.length > 0 ? typed.patterns : (file.patterns ?? DEFAULT_PATTERNS);
|
|
558
594
|
const severityOverrides = { ...(file.severities ?? {}), ...options.severities };
|
|
559
595
|
const { severities, escalated } = resolveStrict(severityOverrides, options.strict || (file.strict ?? false), projectRules);
|
|
560
596
|
const analyseOptions = {
|
|
561
597
|
root,
|
|
562
598
|
patterns,
|
|
563
|
-
// A bare name prunes a directory of that name at any depth, the way a
|
|
564
|
-
// .gitignore line does, so it means the same thing wherever it was typed.
|
|
565
|
-
// A path or a glob is matched against the repository-relative path, and
|
|
566
|
-
// leaving that one alone would make it silently match nothing.
|
|
567
|
-
//
|
|
568
599
|
// A stray entry in one of the empty fallbacks below changes a run only if
|
|
569
600
|
// it matches a path, a target or a family some corpus has, so the mutants
|
|
570
601
|
// that plant one survive every corpus but one built for them - except in
|
|
571
602
|
// `families`, where any entry is an allowlist.
|
|
572
|
-
ignore: [...(file.ignore ?? []), ...
|
|
603
|
+
ignore: [...(file.ignore ?? []), ...typed.ignore],
|
|
573
604
|
ignoreReferences: [...(file.ignoreReferences ?? []), ...options.ignoreReferences],
|
|
574
605
|
families: [...(file.families ?? []), ...options.families],
|
|
575
606
|
ignoreFamilies: [...(file.ignoreFamilies ?? []), ...options.ignoreFamilies],
|
|
576
|
-
historyPatterns: [...(file.historyPatterns ?? []), ...
|
|
607
|
+
historyPatterns: [...(file.historyPatterns ?? []), ...typed.history],
|
|
577
608
|
severities,
|
|
578
609
|
projectRules,
|
|
579
610
|
maxRelated: file.maxRelated,
|
|
580
611
|
};
|
|
581
|
-
// Named the way the reader would have to type it, because a discovered
|
|
582
|
-
// configuration is often not the one in front of them.
|
|
583
|
-
//
|
|
584
|
-
// On stderr, with everything else this run says about itself. stdout is the
|
|
585
|
-
// report, and in three of the four formats it is a document: a line above it
|
|
586
|
-
// made `--verbose --format json` unparseable and handed `--format sarif` to a
|
|
587
|
-
// code-scanning uploader that rejects it over a schema.
|
|
588
|
-
if (options.verbose && loaded.source !== null) {
|
|
589
|
-
const depth = here === '' ? 0 : here.split('/').length;
|
|
590
|
-
err(`configuration: ${'../'.repeat(depth)}${loaded.source}\n`);
|
|
591
|
-
}
|
|
592
612
|
let result;
|
|
593
613
|
try {
|
|
594
614
|
result = await analyse(analyseOptions);
|
|
@@ -658,7 +678,7 @@ export async function main(io = {}) {
|
|
|
658
678
|
const { writeFile } = await import('node:fs/promises');
|
|
659
679
|
// Node writes a string as UTF-8 when the encoding is empty too, so
|
|
660
680
|
// emptying this literal writes the same bytes.
|
|
661
|
-
await writeFile(underRoot(root,
|
|
681
|
+
await writeFile(underRoot(root, anchorFile(here, options.recordBaseline)), text, 'utf8');
|
|
662
682
|
}
|
|
663
683
|
catch (error) {
|
|
664
684
|
err(`spec-graph: cannot write ${options.recordBaseline}: ${error.message}\n`);
|
|
@@ -668,10 +688,11 @@ export async function main(io = {}) {
|
|
|
668
688
|
out(`recorded ${count(result.diagnostics.length, 'finding')} as ${count(entries, 'entry', 'entries')} in ${options.recordBaseline}\n`);
|
|
669
689
|
return EXIT_OK;
|
|
670
690
|
}
|
|
671
|
-
const source = options.baseline === null ? (file.baseline ?? null) :
|
|
691
|
+
const source = options.baseline === null ? (file.baseline ?? null) : anchorFile(here, options.baseline);
|
|
672
692
|
const ratchet = options.ratchet || file.ratchet === true;
|
|
673
693
|
let reported = result;
|
|
674
694
|
let note;
|
|
695
|
+
let recordAs;
|
|
675
696
|
if (source !== null) {
|
|
676
697
|
const held = await readBaseline(underRoot(root, source), source);
|
|
677
698
|
// A path typed on the command line says the file is there for this run.
|
|
@@ -701,6 +722,10 @@ export async function main(io = {}) {
|
|
|
701
722
|
ratchet,
|
|
702
723
|
entries: outcome.stale,
|
|
703
724
|
};
|
|
725
|
+
// The command that re-records it is typed where this run was: a path
|
|
726
|
+
// typed on this command line is relative to there already, and the
|
|
727
|
+
// configuration's is from the root.
|
|
728
|
+
recordAs = options.baseline ?? fromHere(here, source);
|
|
704
729
|
// Listed rather than counted when the run turns on them: a number is
|
|
705
730
|
// enough to know the file has slack, and not enough to strike it. On
|
|
706
731
|
// stdout only for a human - the structured formats carry the same rows
|
|
@@ -715,16 +740,18 @@ export async function main(io = {}) {
|
|
|
715
740
|
}
|
|
716
741
|
}
|
|
717
742
|
const looseBaseline = note !== undefined && note.ratchet && note.stale > 0;
|
|
718
|
-
const reporterOptions = { verbose: options.verbose, max: options.max, escalated, baseline: note };
|
|
743
|
+
const reporterOptions = { verbose: options.verbose, max: options.max, escalated, baseline: note, recordAs };
|
|
719
744
|
out(options.format === 'sarif'
|
|
720
745
|
? formatSarif(reported, reported.graph, { version: await readVersion(), escalated, projectRules })
|
|
721
746
|
: options.format === 'gitlab'
|
|
722
747
|
? formatGitlab(reported, { escalated })
|
|
723
|
-
: options.format === '
|
|
724
|
-
?
|
|
725
|
-
: options.format === '
|
|
726
|
-
?
|
|
727
|
-
:
|
|
748
|
+
: options.format === 'github'
|
|
749
|
+
? formatGithub(reported, { escalated })
|
|
750
|
+
: options.format === 'json'
|
|
751
|
+
? formatJson(reported, { escalated, baseline: note })
|
|
752
|
+
: options.format === 'markdown'
|
|
753
|
+
? formatMarkdown(reported, reporterOptions)
|
|
754
|
+
: `${formatReport(reported, { color, ascii, ...reporterOptions })}\n`);
|
|
728
755
|
if (!reported.ok || looseBaseline)
|
|
729
756
|
return EXIT_FAILED;
|
|
730
757
|
if (options.maxWarnings >= 0 && reported.summary.warnings > options.maxWarnings)
|
|
@@ -751,33 +778,46 @@ function below(root, from) {
|
|
|
751
778
|
return start.startsWith(`${root}/`) ? start.slice(root.length + 1) : '';
|
|
752
779
|
}
|
|
753
780
|
/**
|
|
754
|
-
*
|
|
781
|
+
* A path from the root, as it is typed in `here`: one `../` for each directory
|
|
782
|
+
* `here` is below the root. An absolute path reads the same from anywhere.
|
|
783
|
+
*/
|
|
784
|
+
function fromHere(here, path) {
|
|
785
|
+
return here === '' || isAbsolutePath(path) ? path : `${'../'.repeat(here.split('/').length)}${path}`;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Re-anchors a pattern typed in `prefix` so it reads from the root, brace
|
|
789
|
+
* alternative by brace alternative, or throws naming it as typed.
|
|
790
|
+
*
|
|
791
|
+
* `../docs` typed one directory down is the root's `docs`. That is arithmetic
|
|
792
|
+
* on two relative paths, and this is the one place both are known; left in the
|
|
793
|
+
* pattern, it reads as a glob climbing out of the root, which is refused.
|
|
755
794
|
*
|
|
756
795
|
* An absolute path is left alone: it was not relative to anywhere, so moving
|
|
757
|
-
* the root cannot change what it means.
|
|
796
|
+
* the root cannot change what it means. spec-core roots a leading `/` and
|
|
797
|
+
* knows no drive letter, so `C:/docs` is told apart here.
|
|
758
798
|
*/
|
|
759
|
-
function anchor(prefix,
|
|
760
|
-
|
|
761
|
-
return value;
|
|
762
|
-
const negated = value.startsWith('!');
|
|
763
|
-
const directories = prefix.split('/');
|
|
764
|
-
let rest = toPosix(negated ? value.slice(1) : value);
|
|
765
|
-
// `../docs` typed one directory down is the root's `docs`. That is arithmetic
|
|
766
|
-
// on two relative paths, and this is the one place both are known; left in
|
|
767
|
-
// the pattern, it reads as a glob climbing out of the root, which is refused.
|
|
768
|
-
// A bare `..` typed in `docs/deep` leaves `docs/`: the directory's contents,
|
|
769
|
-
// which is what it named.
|
|
770
|
-
while (directories.length > 0 && (rest === '..' || rest.startsWith('../'))) {
|
|
771
|
-
directories.pop();
|
|
772
|
-
rest = rest.slice(3);
|
|
773
|
-
}
|
|
774
|
-
const joined = [...directories, rest].join('/');
|
|
775
|
-
return negated ? `!${joined}` : joined;
|
|
799
|
+
function anchor(prefix, pattern) {
|
|
800
|
+
return isAbsolutePath(pattern) ? pattern : rebasePattern(pattern, prefix);
|
|
776
801
|
}
|
|
777
|
-
/**
|
|
802
|
+
/**
|
|
803
|
+
* The same, for an `--ignore`, where a bare name is a directory at any depth.
|
|
804
|
+
* Only the walk reads a name that way; a `--history` pattern is matched as a
|
|
805
|
+
* whole path, and goes to {@link anchor}.
|
|
806
|
+
*/
|
|
778
807
|
function anchorPath(prefix, pattern) {
|
|
779
808
|
return isGlob(pattern) || pattern.includes('/') ? anchor(prefix, pattern) : pattern;
|
|
780
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Re-anchors a file typed in `prefix`, a baseline, so it reads from the root.
|
|
812
|
+
*
|
|
813
|
+
* A file name is not a pattern: a brace or a `!` in it is a character, which
|
|
814
|
+
* {@link anchor} would read as syntax, and a baseline outside the repository
|
|
815
|
+
* is as good as one inside, so a `..` past the root is a path like any other.
|
|
816
|
+
* At the root the file is named as it was typed.
|
|
817
|
+
*/
|
|
818
|
+
function anchorFile(prefix, file) {
|
|
819
|
+
return prefix === '' || isAbsolutePath(file) ? file : joinPosix(prefix, toPosix(file));
|
|
820
|
+
}
|
|
781
821
|
function knownProjectRules(projectRules) {
|
|
782
822
|
const known = projectRules.map((rule) => rule.id);
|
|
783
823
|
return known.length > 0 ? `project rules here: ${known.join(', ')}` : 'this repository defines no project rules';
|
|
@@ -884,9 +924,14 @@ function nodeSummary(node) {
|
|
|
884
924
|
...(node.kind === 'document' ? { phase: node.phase } : { state: node.disposition, openness: node.openness }),
|
|
885
925
|
};
|
|
886
926
|
}
|
|
887
|
-
/**
|
|
927
|
+
/**
|
|
928
|
+
* Points at the exact character of a selector that failed to parse, in the
|
|
929
|
+
* columns a terminal draws: a Han character before it takes two, so an offset
|
|
930
|
+
* counted in UTF-16 units put the caret short of it. The offset is a position
|
|
931
|
+
* the parser reached, never negative, so it needs no floor.
|
|
932
|
+
*/
|
|
888
933
|
function renderQueryError(selector, error) {
|
|
889
|
-
const caret = `${' '.repeat(
|
|
934
|
+
const caret = `${' '.repeat(displayWidth(selector.slice(0, error.offset)))}^`;
|
|
890
935
|
return `spec-graph: ${error.message}\n ${selector}\n ${caret}\n`;
|
|
891
936
|
}
|
|
892
937
|
/**
|