@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/README.md +468 -313
  3. package/dist/cli.d.ts +2 -2
  4. package/dist/cli.js +112 -67
  5. package/dist/cli.js.map +1 -1
  6. package/dist/extract.js +66 -6
  7. package/dist/extract.js.map +1 -1
  8. package/dist/glob.d.ts +11 -0
  9. package/dist/glob.js +17 -1
  10. package/dist/glob.js.map +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/lifecycle.js +5 -2
  15. package/dist/lifecycle.js.map +1 -1
  16. package/dist/report.d.ts +22 -0
  17. package/dist/report.js +50 -3
  18. package/dist/report.js.map +1 -1
  19. package/dist/sections.d.ts +7 -1
  20. package/dist/sections.js +9 -1
  21. package/dist/sections.js.map +1 -1
  22. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +11 -1
  23. package/dist/vendor/spec-core/markdown/frontmatter.js +20 -3
  24. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -1
  25. package/dist/vendor/spec-core/pattern/glob.d.ts +85 -0
  26. package/dist/vendor/spec-core/pattern/glob.js +207 -17
  27. package/dist/vendor/spec-core/pattern/glob.js.map +1 -1
  28. package/dist/vendor/spec-core/pattern/index.d.ts +2 -2
  29. package/dist/vendor/spec-core/pattern/index.js +1 -1
  30. package/dist/vendor/spec-core/pattern/index.js.map +1 -1
  31. package/dist/vendor/spec-core/text/index.d.ts +1 -0
  32. package/dist/vendor/spec-core/text/index.js +1 -0
  33. package/dist/vendor/spec-core/text/index.js.map +1 -1
  34. package/dist/vendor/spec-core/text/width.d.ts +30 -0
  35. package/dist/vendor/spec-core/text/width.js +113 -0
  36. package/dist/vendor/spec-core/text/width.js.map +1 -0
  37. package/dist/yaml.js +6 -2
  38. package/dist/yaml.js.map +1 -1
  39. 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 is the
85
- interchange format GitHub code scanning and editors
86
- already read, and gitlab the Code Quality report a
87
- GitLab merge request reads, both for check; markdown
88
- is a table for a pull-request comment or
89
- $GITHUB_STEP_SUMMARY, for check or diff
90
- (default: human)
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' && value !== 'json' && value !== 'sarif' && value !== 'markdown' && value !== 'gitlab') {
337
- throw new UsageError(`--format must be human, json, sarif, markdown or gitlab, got "${value}"`);
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 about findings, and only
388
- // `check` produces those. Falling back to JSON would hand a pipeline something
389
- // its uploader rejects with a message about a schema rather than about the
390
- // command that was run. Markdown is for a pull request, which reads a check
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
- const patterns = options.patterns.length > 0 ? options.patterns.map((pattern) => anchor(here, pattern)) : (file.patterns ?? DEFAULT_PATTERNS);
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 ?? []), ...options.ignore.map((pattern) => anchorPath(here, pattern))],
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 ?? []), ...options.historyPatterns.map((pattern) => anchorPath(here, pattern))],
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, anchor(here, options.recordBaseline)), text, 'utf8');
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) : anchor(here, options.baseline);
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 === 'json'
724
- ? formatJson(reported, { escalated, baseline: note })
725
- : options.format === 'markdown'
726
- ? formatMarkdown(reported, reporterOptions)
727
- : `${formatReport(reported, { color, ascii, ...reporterOptions })}\n`);
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
- * Re-anchors a path typed in `prefix` so it reads from the root.
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, value) {
760
- if (prefix === '' || isAbsolutePath(value))
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
- /** The same, for an ignore, where a bare name is a directory at any depth. */
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
- /** Points at the exact character of a selector that failed to parse. */
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(Math.max(0, error.offset))}^`;
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
  /**