@descent-vtt/spec-guard 0.10.3 → 0.12.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 +410 -0
- package/README.md +385 -27
- package/dist/cites.d.ts +192 -0
- package/dist/cites.d.ts.map +1 -0
- package/dist/cites.js +599 -0
- package/dist/cites.js.map +1 -0
- package/dist/cli.d.ts +14 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +315 -22
- package/dist/cli.js.map +1 -1
- package/dist/comments.d.ts +51 -2
- package/dist/comments.d.ts.map +1 -1
- package/dist/comments.js +353 -57
- package/dist/comments.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +46 -5
- package/dist/config.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +38 -14
- package/dist/engine.js.map +1 -1
- package/dist/glob.d.ts +128 -24
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +304 -61
- package/dist/glob.js.map +1 -1
- package/dist/impact.d.ts +179 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +360 -0
- package/dist/impact.js.map +1 -0
- package/dist/imports.d.ts.map +1 -1
- package/dist/imports.js +5 -58
- package/dist/imports.js.map +1 -1
- package/dist/index.d.ts +13 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +16 -75
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +198 -370
- package/dist/mcp.js.map +1 -1
- package/dist/overlay.d.ts +39 -0
- package/dist/overlay.d.ts.map +1 -0
- package/dist/overlay.js +133 -0
- package/dist/overlay.js.map +1 -0
- package/dist/parser.d.ts +41 -24
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +217 -160
- package/dist/parser.js.map +1 -1
- package/dist/prove.d.ts +46 -0
- package/dist/prove.d.ts.map +1 -0
- package/dist/prove.js +549 -0
- package/dist/prove.js.map +1 -0
- package/dist/query.d.ts +11 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +9 -3
- package/dist/query.js.map +1 -1
- package/dist/reporter.d.ts +139 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +593 -21
- package/dist/reporter.js.map +1 -1
- package/dist/rules.js +3 -3
- package/dist/rules.js.map +1 -1
- package/dist/runner.d.ts +24 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +84 -21
- package/dist/runner.js.map +1 -1
- package/dist/specs.d.ts +5 -1
- package/dist/specs.d.ts.map +1 -1
- package/dist/specs.js +5 -1
- package/dist/specs.js.map +1 -1
- package/dist/structure.d.ts.map +1 -1
- package/dist/structure.js +7 -6
- package/dist/structure.js.map +1 -1
- package/dist/types.d.ts +193 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vendor/spec-core/jsonrpc/index.d.ts +3 -0
- package/dist/vendor/spec-core/jsonrpc/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/index.js +2 -0
- package/dist/vendor/spec-core/jsonrpc/index.js.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.d.ts +158 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.d.ts.map +1 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js +353 -0
- package/dist/vendor/spec-core/jsonrpc/mcp.js.map +1 -0
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts +104 -0
- package/dist/vendor/spec-core/markdown/frontmatter.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
- package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
- package/dist/vendor/spec-core/markdown/headings.d.ts +46 -0
- package/dist/vendor/spec-core/markdown/headings.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/headings.js +199 -0
- package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
- package/dist/vendor/spec-core/markdown/index.d.ts +6 -0
- package/dist/vendor/spec-core/markdown/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/index.js +4 -0
- package/dist/vendor/spec-core/markdown/index.js.map +1 -0
- package/dist/vendor/spec-core/markdown/layout.d.ts +40 -0
- package/dist/vendor/spec-core/markdown/layout.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/layout.js +28 -0
- package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
- package/dist/vendor/spec-core/markdown/links.d.ts +23 -0
- package/dist/vendor/spec-core/markdown/links.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/links.js +364 -0
- package/dist/vendor/spec-core/markdown/links.js.map +1 -0
- package/dist/vendor/spec-core/markdown/lists.d.ts +23 -0
- package/dist/vendor/spec-core/markdown/lists.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/lists.js +127 -0
- package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
- package/dist/vendor/spec-core/markdown/scan.d.ts +37 -0
- package/dist/vendor/spec-core/markdown/scan.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/scan.js +439 -0
- package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
- package/dist/vendor/spec-core/markdown/syntax.d.ts +76 -0
- package/dist/vendor/spec-core/markdown/syntax.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/syntax.js +181 -0
- package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
- package/dist/vendor/spec-core/markdown/tables.d.ts +19 -0
- package/dist/vendor/spec-core/markdown/tables.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/tables.js +86 -0
- package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
- package/dist/vendor/spec-core/markdown/types.d.ts +288 -0
- package/dist/vendor/spec-core/markdown/types.d.ts.map +1 -0
- package/dist/vendor/spec-core/markdown/types.js +9 -0
- package/dist/vendor/spec-core/markdown/types.js.map +1 -0
- package/dist/vendor/spec-core/path/index.d.ts +2 -0
- package/dist/vendor/spec-core/path/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/path/index.js +2 -0
- package/dist/vendor/spec-core/path/index.js.map +1 -0
- package/dist/vendor/spec-core/path/posix.d.ts +84 -0
- package/dist/vendor/spec-core/path/posix.d.ts.map +1 -0
- package/dist/vendor/spec-core/path/posix.js +150 -0
- package/dist/vendor/spec-core/path/posix.js.map +1 -0
- package/dist/vendor/spec-core/pattern/automaton.d.ts +130 -0
- package/dist/vendor/spec-core/pattern/automaton.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/automaton.js +340 -0
- package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
- package/dist/vendor/spec-core/pattern/charset.d.ts +59 -0
- package/dist/vendor/spec-core/pattern/charset.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/charset.js +116 -0
- package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
- package/dist/vendor/spec-core/pattern/glob.d.ts +122 -0
- package/dist/vendor/spec-core/pattern/glob.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/glob.js +481 -0
- package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
- package/dist/vendor/spec-core/pattern/index.d.ts +7 -0
- package/dist/vendor/spec-core/pattern/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/index.js +4 -0
- package/dist/vendor/spec-core/pattern/index.js.map +1 -0
- package/dist/vendor/spec-core/pattern/regex.d.ts +68 -0
- package/dist/vendor/spec-core/pattern/regex.d.ts.map +1 -0
- package/dist/vendor/spec-core/pattern/regex.js +813 -0
- package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
- package/dist/vendor/spec-core/text/index.d.ts +3 -0
- package/dist/vendor/spec-core/text/index.d.ts.map +1 -0
- package/dist/vendor/spec-core/text/index.js +2 -0
- package/dist/vendor/spec-core/text/index.js.map +1 -0
- package/dist/vendor/spec-core/text/lines.d.ts +73 -0
- package/dist/vendor/spec-core/text/lines.d.ts.map +1 -0
- package/dist/vendor/spec-core/text/lines.js +156 -0
- package/dist/vendor/spec-core/text/lines.js.map +1 -0
- package/package.json +8 -1
- package/src/vendor/spec-core/LICENSE +21 -0
- package/src/vendor/spec-core/README.md +8 -0
package/dist/cli.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { Readable } from 'node:stream';
|
|
|
10
10
|
import { type ConfigKey, type ProjectConfig } from './config.js';
|
|
11
11
|
import { watchTree } from './io.js';
|
|
12
12
|
import type { EnginePreference } from './engine.js';
|
|
13
|
-
import type { ConfigUse } from './types.js';
|
|
13
|
+
import type { CiteFamily, ConfigUse } from './types.js';
|
|
14
14
|
export declare const EXIT_OK = 0;
|
|
15
15
|
export declare const EXIT_FAILED = 1;
|
|
16
16
|
export declare const EXIT_ERROR = 2;
|
|
@@ -32,15 +32,23 @@ export interface CliIO {
|
|
|
32
32
|
/** Registers what Ctrl+C and SIGTERM do, and returns how to unregister it. */
|
|
33
33
|
onInterrupt?: (handler: () => void) => () => void;
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* What the command line asked for: a run, a query, a server, a proof that the
|
|
37
|
+
* rules can fail, the citations of specs in code comments, or who depends on a
|
|
38
|
+
* path.
|
|
39
|
+
*/
|
|
40
|
+
export type Command = 'check' | 'query' | 'mcp' | 'prove' | 'cites' | 'impact';
|
|
37
41
|
/** How a finished run is written out. */
|
|
38
|
-
export type OutputFormat = 'human' | 'json' | 'sarif';
|
|
42
|
+
export type OutputFormat = 'human' | 'json' | 'sarif' | 'github' | 'gitlab';
|
|
39
43
|
export interface CliOptions {
|
|
40
44
|
command: Command;
|
|
41
45
|
patterns: string[];
|
|
42
|
-
/** The paths `spec-guard query`
|
|
46
|
+
/** The paths `spec-guard query` and `spec-guard impact` ask about, and the ones `spec-guard cites` reads. */
|
|
43
47
|
paths: string[];
|
|
48
|
+
/** How many imports away `spec-guard impact` follows dependents; all of them when absent. */
|
|
49
|
+
depth?: number;
|
|
50
|
+
/** The documents comments cite, from a configuration; derived from the specs when absent. */
|
|
51
|
+
cites?: CiteFamily[];
|
|
44
52
|
root: string;
|
|
45
53
|
verbose: boolean;
|
|
46
54
|
failFast: boolean;
|
|
@@ -84,7 +92,7 @@ export declare class UsageError extends Error {
|
|
|
84
92
|
* fallback nobody knows still works.
|
|
85
93
|
*/
|
|
86
94
|
export declare function version(manifest?: string): string;
|
|
87
|
-
export declare const HELP = "spec-guard - Executable architecture assertions for Markdown specs & ADRs\n\nUsage\n spec-guard [patterns...] [options] execute the directives in the specs\n spec-guard query <paths...> [options] list the rules in force for files or directories\n spec-guard mcp [options] serve the rules to an AI agent over MCP on stdio\n\nPatterns\n Globs or paths to the Markdown specs to execute. A directory expands to the\n Markdown files inside it. Defaults to \"docs/**/*.md\" when omitted. query
|
|
95
|
+
export declare const HELP = "spec-guard - Executable architecture assertions for Markdown specs & ADRs\n\nUsage\n spec-guard [patterns...] [options] execute the directives in the specs\n spec-guard query <paths...> [options] list the rules in force for files or directories\n spec-guard mcp [options] serve the rules to an AI agent over MCP on stdio\n spec-guard prove [patterns...] [options]\n show each rule a violation of itself, in memory, and report any that pass\n spec-guard cites [paths...] [options] check that each spec a code comment cites exists and is in force\n spec-guard impact <paths...> [options] the files that depend on each path, and the rules in play there\n\nPatterns\n Globs or paths to the Markdown specs to execute. A directory expands to the\n Markdown files inside it. Defaults to \"docs/**/*.md\" when omitted. query,\n mcp, cites and impact take theirs from --spec.\n\nOptions\n -r, --root <path> Codebase root that assertions are resolved against (default: cwd)\n --spec <pattern> A spec glob or path; repeatable (default: \"docs/**/*.md\")\n -v, --verbose Print passing assertions too\n --watch Run again as the tree changes, until Ctrl+C (human output only)\n --fail-fast Stop at the first failing assertion\n --json Emit a machine-readable JSON report (same as --format json)\n --format <name> human | json | sarif | github | gitlab (sarif uploads to code scanning,\n github annotates a pull request from the log, gitlab is Code Quality)\n --engine <name> auto | rg | js (default: auto - scanner for small trees, ripgrep for big ones)\n --strict Treat analysis that could not be completed as a failure\n --allow-missing-targets\n Tolerate target paths that do not exist (they fail by default)\n --allow-empty-scope Tolerate assertions whose scope holds no files (they fail by default)\n --print-baseline Print the baseline=\"...\" that would exempt today's violations, and exit\n --no-default-skips Search .git, .hg, .svn and node_modules too\n --exclude <globs> Paths no assertion looks at, beside each directive's exclude; repeatable\n --ignore-status Execute directives in documents not in force too (see Directives)\n --include-specs Also count matches inside the spec files themselves\n --max-snippets <n> Failure snippets per assertion (default: 5)\n --concurrency <n> Assertions executed in parallel (default: 8)\n --depth <n> impact: follow dependents at most n imports away (default: all)\n --allow-empty Exit 0 when no spec files matched (a run, prove or impact; not an assertion)\n --color/--no-color Force colour on or off (NO_COLOR is honoured)\n -h, --help Show this help\n --version Print the version\n\nConfiguration\n package.json in the root can hold specs, exclude, engine, strict,\n allowMissingTargets, allowEmptyScope, ignoreStatus, includeSpecs, defaultSkips,\n maxSnippets and concurrency under \"specGuard\"; a root with no package.json can\n keep them in .spec-guard.json instead, but not in both. A flag wins over the\n file, every on/off option there also takes its opposite (--no-strict,\n --default-skips, ...), and --exclude= with nothing clears exclude.\n \"cites\": [{ \"id\": \"ADR-{n}\", \"files\": \"docs/adr/{n}-*.md\" }] names the\n documents code comments cite; without it, cites reads them off the titles of\n numbered specs.\n\nDirectives\n <!-- @assert-absence target=\"src/\" symbol=\"LegacyGateway\" exclude=\"src/legacy/**\" -->\n <!-- @assert-count target=\"src/\" symbol=\"SessionManager\" expected=\"1\" -->\n <!-- @assert-present file=\"SECURITY.md\" -->\n <!-- @assert-import-cycle target=\"src/\" types=\"ignore\" -->\n <!-- @assert-layers target=\"src/\" order=\"domain, application, infrastructure\" -->\n <!-- @assert-structure target=\"src/\" exclude=\"*.test.ts\" partner=\"[name].test.ts\" -->\n\n Matches inside comments do not count; add comments=\"include\" to count them.\n An assertion whose scope holds no files fails; add allow-empty=\"true\" to allow it.\n A document whose status is draft, proposed, rejected, deprecated, superseded\n or archived is reported and not executed; --ignore-status runs it anyway.\n\nprove adds, changes or removes files in memory only - never on disk - to make\nthe violation each rule forbids, and runs the rule over that tree. A rule that\nstill passes survived, and exits 1; one no violation could be made for is\nunprovable, and exits 1 under --strict.\n\ncites reads comment text only - not strings, not Markdown - for ids such as\nADR-0007, ADR-7 and ADR-007, which are one document. A ghost citation names no\ndocument, and exits 1; a stale one names a document superseded, deprecated,\nrejected or archived, and exits 1 under --strict. A family whose files match no\ndocument exits 2.\n\nimpact follows the import graph backwards: relative JavaScript, TypeScript and\nPython imports that name a file. Go, Rust, C# and absolute Python imports name\nmodules, and are counted rather than followed; an import that names no file is\nlisted, never guessed at.\n\nquery answers from the specs alone, without reading the codebase, so it works\nfor a file that does not exist yet. mcp offers the same answer, a check and\nimpact's, as the tools get_architectural_rules, check_architecture and\nget_dependents.\n\nExit codes\n 0 all assertions passed 1 an assertion failed 2 spec-guard could not run\n 130 a --watch session was stopped";
|
|
88
96
|
/** Minimal, dependency-free argv parser. Supports `--flag value` and `--flag=value`. */
|
|
89
97
|
export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
|
|
90
98
|
/**
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -8,17 +8,40 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { createRequire } from 'node:module';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { CitesError, findCitations } from './cites.js';
|
|
11
12
|
import { CONFIG_KEYS, ConfigError, engineNamed, findConfig } from './config.js';
|
|
12
|
-
import {
|
|
13
|
+
import { formatImpact, formatImpactJson, impactOf } from './impact.js';
|
|
14
|
+
import { excludeListError, patternListError, specPatternError } from './glob.js';
|
|
13
15
|
import { nodeIo, readText, watchTree } from './io.js';
|
|
14
16
|
import { createMcpHandler, serveStdio } from './mcp.js';
|
|
15
|
-
import { formatQuery, formatQueryJson, queryRules } from './query.js';
|
|
16
|
-
import {
|
|
17
|
+
import { formatQuery, formatQueryJson, queryRules, resolveQueryPath } from './query.js';
|
|
18
|
+
import { proveSpecGuard } from './prove.js';
|
|
19
|
+
import { citesAnnotations, formatBaselines, formatCites, formatCitesJson, formatCitesSarif, formatConfigUse, formatGithub, formatGitlab, formatJson, formatProve, formatProveJson, formatProveSarif, formatReport, formatSarif, proveAnnotations, runAnnotations, shouldUseAscii, shouldUseColor, } from './reporter.js';
|
|
17
20
|
import { DEFAULT_CONCURRENCY, DEFAULT_MAX_SNIPPETS, runSpecGuard, splitList } from './runner.js';
|
|
18
21
|
import { createSession, runWatch } from './watch.js';
|
|
19
22
|
export const EXIT_OK = 0;
|
|
20
23
|
export const EXIT_FAILED = 1;
|
|
21
24
|
export const EXIT_ERROR = 2;
|
|
25
|
+
/** The words that name a command when they come first. */
|
|
26
|
+
const COMMANDS = new Set(['query', 'mcp', 'prove', 'cites', 'impact']);
|
|
27
|
+
/**
|
|
28
|
+
* The formats each command writes. A command that lists rules rather than
|
|
29
|
+
* findings has nothing to put on a line of code, so it has no SARIF, GitHub or
|
|
30
|
+
* GitLab form; `mcp` writes the protocol, and refuses `--format` altogether.
|
|
31
|
+
*/
|
|
32
|
+
const FORMATS = {
|
|
33
|
+
check: ['human', 'json', 'sarif', 'github', 'gitlab'],
|
|
34
|
+
prove: ['human', 'json', 'sarif', 'github', 'gitlab'],
|
|
35
|
+
cites: ['human', 'json', 'sarif', 'github', 'gitlab'],
|
|
36
|
+
query: ['human', 'json'],
|
|
37
|
+
impact: ['human', 'json'],
|
|
38
|
+
};
|
|
39
|
+
/** What a command without line-level formats lists instead of results, for the message refusing one. */
|
|
40
|
+
const LISTS = { query: 'rules', impact: 'files and rules' };
|
|
41
|
+
/** `a or b`, `a, b or c`: every command that takes a format has at least two. */
|
|
42
|
+
function either(words) {
|
|
43
|
+
return `${words.slice(0, -1).join(', ')} or ${words[words.length - 1]}`;
|
|
44
|
+
}
|
|
22
45
|
export class UsageError extends Error {
|
|
23
46
|
}
|
|
24
47
|
/**
|
|
@@ -44,11 +67,15 @@ Usage
|
|
|
44
67
|
spec-guard [patterns...] [options] execute the directives in the specs
|
|
45
68
|
spec-guard query <paths...> [options] list the rules in force for files or directories
|
|
46
69
|
spec-guard mcp [options] serve the rules to an AI agent over MCP on stdio
|
|
70
|
+
spec-guard prove [patterns...] [options]
|
|
71
|
+
show each rule a violation of itself, in memory, and report any that pass
|
|
72
|
+
spec-guard cites [paths...] [options] check that each spec a code comment cites exists and is in force
|
|
73
|
+
spec-guard impact <paths...> [options] the files that depend on each path, and the rules in play there
|
|
47
74
|
|
|
48
75
|
Patterns
|
|
49
76
|
Globs or paths to the Markdown specs to execute. A directory expands to the
|
|
50
|
-
Markdown files inside it. Defaults to "docs/**/*.md" when omitted. query
|
|
51
|
-
mcp take theirs from --spec.
|
|
77
|
+
Markdown files inside it. Defaults to "docs/**/*.md" when omitted. query,
|
|
78
|
+
mcp, cites and impact take theirs from --spec.
|
|
52
79
|
|
|
53
80
|
Options
|
|
54
81
|
-r, --root <path> Codebase root that assertions are resolved against (default: cwd)
|
|
@@ -57,7 +84,8 @@ Options
|
|
|
57
84
|
--watch Run again as the tree changes, until Ctrl+C (human output only)
|
|
58
85
|
--fail-fast Stop at the first failing assertion
|
|
59
86
|
--json Emit a machine-readable JSON report (same as --format json)
|
|
60
|
-
--format <name> human | json | sarif (sarif uploads to
|
|
87
|
+
--format <name> human | json | sarif | github | gitlab (sarif uploads to code scanning,
|
|
88
|
+
github annotates a pull request from the log, gitlab is Code Quality)
|
|
61
89
|
--engine <name> auto | rg | js (default: auto - scanner for small trees, ripgrep for big ones)
|
|
62
90
|
--strict Treat analysis that could not be completed as a failure
|
|
63
91
|
--allow-missing-targets
|
|
@@ -66,11 +94,12 @@ Options
|
|
|
66
94
|
--print-baseline Print the baseline="..." that would exempt today's violations, and exit
|
|
67
95
|
--no-default-skips Search .git, .hg, .svn and node_modules too
|
|
68
96
|
--exclude <globs> Paths no assertion looks at, beside each directive's exclude; repeatable
|
|
69
|
-
--ignore-status Execute directives in
|
|
97
|
+
--ignore-status Execute directives in documents not in force too (see Directives)
|
|
70
98
|
--include-specs Also count matches inside the spec files themselves
|
|
71
99
|
--max-snippets <n> Failure snippets per assertion (default: ${DEFAULT_MAX_SNIPPETS})
|
|
72
100
|
--concurrency <n> Assertions executed in parallel (default: ${DEFAULT_CONCURRENCY})
|
|
73
|
-
--
|
|
101
|
+
--depth <n> impact: follow dependents at most n imports away (default: all)
|
|
102
|
+
--allow-empty Exit 0 when no spec files matched (a run, prove or impact; not an assertion)
|
|
74
103
|
--color/--no-color Force colour on or off (NO_COLOR is honoured)
|
|
75
104
|
-h, --help Show this help
|
|
76
105
|
--version Print the version
|
|
@@ -82,6 +111,9 @@ Configuration
|
|
|
82
111
|
keep them in .spec-guard.json instead, but not in both. A flag wins over the
|
|
83
112
|
file, every on/off option there also takes its opposite (--no-strict,
|
|
84
113
|
--default-skips, ...), and --exclude= with nothing clears exclude.
|
|
114
|
+
"cites": [{ "id": "ADR-{n}", "files": "docs/adr/{n}-*.md" }] names the
|
|
115
|
+
documents code comments cite; without it, cites reads them off the titles of
|
|
116
|
+
numbered specs.
|
|
85
117
|
|
|
86
118
|
Directives
|
|
87
119
|
<!-- @assert-absence target="src/" symbol="LegacyGateway" exclude="src/legacy/**" -->
|
|
@@ -93,12 +125,29 @@ Directives
|
|
|
93
125
|
|
|
94
126
|
Matches inside comments do not count; add comments="include" to count them.
|
|
95
127
|
An assertion whose scope holds no files fails; add allow-empty="true" to allow it.
|
|
96
|
-
A document whose status is draft, proposed, rejected, deprecated
|
|
97
|
-
is reported and not executed; --ignore-status runs it anyway.
|
|
128
|
+
A document whose status is draft, proposed, rejected, deprecated, superseded
|
|
129
|
+
or archived is reported and not executed; --ignore-status runs it anyway.
|
|
130
|
+
|
|
131
|
+
prove adds, changes or removes files in memory only - never on disk - to make
|
|
132
|
+
the violation each rule forbids, and runs the rule over that tree. A rule that
|
|
133
|
+
still passes survived, and exits 1; one no violation could be made for is
|
|
134
|
+
unprovable, and exits 1 under --strict.
|
|
135
|
+
|
|
136
|
+
cites reads comment text only - not strings, not Markdown - for ids such as
|
|
137
|
+
ADR-0007, ADR-7 and ADR-007, which are one document. A ghost citation names no
|
|
138
|
+
document, and exits 1; a stale one names a document superseded, deprecated,
|
|
139
|
+
rejected or archived, and exits 1 under --strict. A family whose files match no
|
|
140
|
+
document exits 2.
|
|
141
|
+
|
|
142
|
+
impact follows the import graph backwards: relative JavaScript, TypeScript and
|
|
143
|
+
Python imports that name a file. Go, Rust, C# and absolute Python imports name
|
|
144
|
+
modules, and are counted rather than followed; an import that names no file is
|
|
145
|
+
listed, never guessed at.
|
|
98
146
|
|
|
99
147
|
query answers from the specs alone, without reading the codebase, so it works
|
|
100
|
-
for a file that does not exist yet. mcp offers the same answer,
|
|
101
|
-
as the tools get_architectural_rules and
|
|
148
|
+
for a file that does not exist yet. mcp offers the same answer, a check and
|
|
149
|
+
impact's, as the tools get_architectural_rules, check_architecture and
|
|
150
|
+
get_dependents.
|
|
102
151
|
|
|
103
152
|
Exit codes
|
|
104
153
|
0 all assertions passed 1 an assertion failed 2 spec-guard could not run
|
|
@@ -150,7 +199,50 @@ const NOT_FOR = {
|
|
|
150
199
|
'--concurrency',
|
|
151
200
|
'--color',
|
|
152
201
|
]),
|
|
202
|
+
// Who depends on a path is answered from the graph, and the rules from the
|
|
203
|
+
// specs, as a query answers them: nothing is run, so nothing about running
|
|
204
|
+
// applies. `--allow-empty` does: the dependents are an answer without specs.
|
|
205
|
+
impact: new Set([
|
|
206
|
+
'--verbose',
|
|
207
|
+
'--watch',
|
|
208
|
+
'--fail-fast',
|
|
209
|
+
'--engine',
|
|
210
|
+
'--strict',
|
|
211
|
+
'--no-strict',
|
|
212
|
+
'--allow-missing-targets',
|
|
213
|
+
'--no-allow-missing-targets',
|
|
214
|
+
'--allow-empty-scope',
|
|
215
|
+
'--no-allow-empty-scope',
|
|
216
|
+
'--print-baseline',
|
|
217
|
+
'--max-snippets',
|
|
218
|
+
'--concurrency',
|
|
219
|
+
'--color',
|
|
220
|
+
]),
|
|
153
221
|
mcp: new Set(['--verbose', '--watch', '--fail-fast', '--json', '--format', '--print-baseline', '--allow-empty', '--color', '--no-color']),
|
|
222
|
+
// A proof runs every rule, one at a time, with the scanner reading through a
|
|
223
|
+
// door ripgrep cannot see through, and prints no snippets.
|
|
224
|
+
prove: new Set(['--watch', '--fail-fast', '--print-baseline', '--engine', '--concurrency', '--max-snippets']),
|
|
225
|
+
// Citations are read from comments, not from rules: nothing is executed, so
|
|
226
|
+
// nothing about how rules run applies, and a document's status is the
|
|
227
|
+
// answer rather than something to ignore.
|
|
228
|
+
cites: new Set([
|
|
229
|
+
'--verbose',
|
|
230
|
+
'--watch',
|
|
231
|
+
'--fail-fast',
|
|
232
|
+
'--engine',
|
|
233
|
+
'--allow-missing-targets',
|
|
234
|
+
'--no-allow-missing-targets',
|
|
235
|
+
'--allow-empty-scope',
|
|
236
|
+
'--no-allow-empty-scope',
|
|
237
|
+
'--print-baseline',
|
|
238
|
+
'--ignore-status',
|
|
239
|
+
'--no-ignore-status',
|
|
240
|
+
'--include-specs',
|
|
241
|
+
'--no-include-specs',
|
|
242
|
+
'--max-snippets',
|
|
243
|
+
'--concurrency',
|
|
244
|
+
'--allow-empty',
|
|
245
|
+
]),
|
|
154
246
|
};
|
|
155
247
|
/**
|
|
156
248
|
* Options a watch session refuses, and why.
|
|
@@ -171,7 +263,7 @@ const NOT_WITH_WATCH = [
|
|
|
171
263
|
const LONG_NAMES = { '-v': '--verbose' };
|
|
172
264
|
/** Minimal, dependency-free argv parser. Supports `--flag value` and `--flag=value`. */
|
|
173
265
|
export function parseArgs(argv, cwd) {
|
|
174
|
-
const command = argv[0]
|
|
266
|
+
const command = COMMANDS.has(argv[0]) ? argv[0] : 'check';
|
|
175
267
|
const specs = [];
|
|
176
268
|
const options = {
|
|
177
269
|
command,
|
|
@@ -208,7 +300,7 @@ export function parseArgs(argv, cwd) {
|
|
|
208
300
|
if (onlyPositional || !argument.startsWith('-') || argument === '-') {
|
|
209
301
|
if (command === 'mcp')
|
|
210
302
|
throw new UsageError(`spec-guard mcp takes no arguments, got "${argument}". Name specs with --spec.`);
|
|
211
|
-
(command === 'query' ? options.paths : options.patterns).push(argument);
|
|
303
|
+
(command === 'query' || command === 'cites' || command === 'impact' ? options.paths : options.patterns).push(argument);
|
|
212
304
|
continue;
|
|
213
305
|
}
|
|
214
306
|
if (argument === '--') {
|
|
@@ -252,11 +344,14 @@ export function parseArgs(argv, cwd) {
|
|
|
252
344
|
break;
|
|
253
345
|
case '--format': {
|
|
254
346
|
const value = nextValue().toLowerCase();
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
347
|
+
// mcp refused --format above, so every command that gets here writes one.
|
|
348
|
+
const formats = FORMATS[command];
|
|
349
|
+
if (!formats.includes(value)) {
|
|
350
|
+
// Only a command that lists leaves out one of check's formats, so
|
|
351
|
+
// one refused here is refused by a command LISTS names.
|
|
352
|
+
throw new UsageError(FORMATS.check.includes(value)
|
|
353
|
+
? `spec-guard ${command} has no ${value} format: it lists ${LISTS[command]}, not results. Expected ${either(formats)}.`
|
|
354
|
+
: `Unknown format "${value}". Expected ${either(formats)}.`);
|
|
260
355
|
}
|
|
261
356
|
options.format = value;
|
|
262
357
|
options.json = value === 'json';
|
|
@@ -341,6 +436,15 @@ export function parseArgs(argv, cwd) {
|
|
|
341
436
|
options.concurrency = Math.max(1, positiveInteger(name, nextValue()));
|
|
342
437
|
set.add('concurrency');
|
|
343
438
|
break;
|
|
439
|
+
case '--depth': {
|
|
440
|
+
if (command !== 'impact')
|
|
441
|
+
throw new UsageError('Option --depth applies only to spec-guard impact.');
|
|
442
|
+
const depth = positiveInteger(name, nextValue());
|
|
443
|
+
if (depth === 0)
|
|
444
|
+
throw new UsageError('Option --depth expects 1 or more: a depth of 0 would follow no import.');
|
|
445
|
+
options.depth = depth;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
344
448
|
default:
|
|
345
449
|
throw new UsageError(`Unknown option "${name}". Run spec-guard --help.`);
|
|
346
450
|
}
|
|
@@ -360,10 +464,26 @@ export function parseArgs(argv, cwd) {
|
|
|
360
464
|
if (command === 'query' && options.paths.length === 0 && !options.help && !options.version) {
|
|
361
465
|
throw new UsageError('spec-guard query needs a path to ask about, e.g. spec-guard query src/domain/user.ts.');
|
|
362
466
|
}
|
|
467
|
+
if (command === 'impact' && options.paths.length === 0 && !options.help && !options.version) {
|
|
468
|
+
throw new UsageError('spec-guard impact needs a path to ask about, e.g. spec-guard impact src/db/client.ts.');
|
|
469
|
+
}
|
|
363
470
|
return options;
|
|
364
471
|
}
|
|
365
472
|
/** The configurable options a query reads; the rest are about running rules. */
|
|
366
473
|
const QUERY_KEYS = new Set(['specs', 'exclude', 'ignoreStatus', 'includeSpecs', 'defaultSkips']);
|
|
474
|
+
/** The configurable options a citation check reads: which specs, what to leave out, and how strictly. */
|
|
475
|
+
const CITES_KEYS = new Set(['specs', 'exclude', 'strict', 'defaultSkips', 'cites']);
|
|
476
|
+
/** The configurable options a proof reads: what decides whether a rule passes, and not how a run is scheduled. */
|
|
477
|
+
const PROVE_KEYS = new Set([
|
|
478
|
+
'specs',
|
|
479
|
+
'exclude',
|
|
480
|
+
'strict',
|
|
481
|
+
'allowMissingTargets',
|
|
482
|
+
'allowEmptyScope',
|
|
483
|
+
'ignoreStatus',
|
|
484
|
+
'includeSpecs',
|
|
485
|
+
'defaultSkips',
|
|
486
|
+
]);
|
|
367
487
|
/** Where each key of a configuration lands on the command line's options. */
|
|
368
488
|
const SETTERS = {
|
|
369
489
|
specs: (options, value) => {
|
|
@@ -399,6 +519,9 @@ const SETTERS = {
|
|
|
399
519
|
concurrency: (options, value) => {
|
|
400
520
|
options.concurrency = value;
|
|
401
521
|
},
|
|
522
|
+
cites: (options, value) => {
|
|
523
|
+
options.cites = value.map((family) => ({ ...family }));
|
|
524
|
+
},
|
|
402
525
|
};
|
|
403
526
|
/**
|
|
404
527
|
* Fills in what the command line left unset from a configuration, and says what
|
|
@@ -417,7 +540,14 @@ export function applyConfig(options, config, file = 'package.json') {
|
|
|
417
540
|
const value = config[key];
|
|
418
541
|
if (value === undefined)
|
|
419
542
|
continue;
|
|
420
|
-
|
|
543
|
+
const reads = options.command === 'query' || options.command === 'impact'
|
|
544
|
+
? QUERY_KEYS.has(key)
|
|
545
|
+
: options.command === 'prove'
|
|
546
|
+
? PROVE_KEYS.has(key)
|
|
547
|
+
: options.command === 'cites'
|
|
548
|
+
? CITES_KEYS.has(key)
|
|
549
|
+
: key !== 'cites' && !(options.watch && key === 'engine');
|
|
550
|
+
if (!reads)
|
|
421
551
|
continue;
|
|
422
552
|
if (options.fromCommandLine.has(key)) {
|
|
423
553
|
overridden.push(key);
|
|
@@ -592,6 +722,144 @@ async function runMcp(options, commandLine, io, use) {
|
|
|
592
722
|
await serveStdio(io.stdin, io.stdout, handler);
|
|
593
723
|
return EXIT_OK;
|
|
594
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* `spec-guard prove`: each rule shown a violation of itself, in memory. ADR-0016.
|
|
727
|
+
*
|
|
728
|
+
* Exit 1 when a rule survived, or a directive could not be read; under
|
|
729
|
+
* `--strict`, when a rule is unprovable too, since a rule nobody can show
|
|
730
|
+
* failing is analysis that could not be completed.
|
|
731
|
+
*/
|
|
732
|
+
async function runProve(options, io, use) {
|
|
733
|
+
let report;
|
|
734
|
+
try {
|
|
735
|
+
report = {
|
|
736
|
+
...(await proveSpecGuard({
|
|
737
|
+
patterns: options.patterns,
|
|
738
|
+
root: options.root,
|
|
739
|
+
allowMissingTargets: options.allowMissingTargets,
|
|
740
|
+
strictTargets: options.strictTargets,
|
|
741
|
+
allowEmptyScope: options.allowEmptyScope,
|
|
742
|
+
includeSpecs: options.includeSpecs,
|
|
743
|
+
defaultSkips: options.defaultSkips,
|
|
744
|
+
exclude: options.exclude,
|
|
745
|
+
ignoreStatus: options.ignoreStatus,
|
|
746
|
+
})),
|
|
747
|
+
config: use,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
io.stderr(`spec-guard: ${error instanceof Error ? error.message : String(error)}`);
|
|
752
|
+
return EXIT_ERROR;
|
|
753
|
+
}
|
|
754
|
+
if (report.summary.specs === 0 && (options.format === 'human' || options.format === 'github')) {
|
|
755
|
+
io.stderr(`spec-guard: no spec files matched ${options.patterns.map((p) => `"${p}"`).join(', ')}`);
|
|
756
|
+
return options.allowEmpty ? EXIT_OK : EXIT_ERROR;
|
|
757
|
+
}
|
|
758
|
+
const written = options.format === 'sarif'
|
|
759
|
+
? formatProveSarif(report, { version: version() })
|
|
760
|
+
: options.format === 'json'
|
|
761
|
+
? formatProveJson(report)
|
|
762
|
+
: options.format === 'gitlab'
|
|
763
|
+
? formatGitlab(proveAnnotations(report))
|
|
764
|
+
: options.format === 'github'
|
|
765
|
+
? formatGithub(proveAnnotations(report))
|
|
766
|
+
: formatProve(report, {
|
|
767
|
+
color: shouldUseColor({ isTTY: io.isTTY }, options.color, io.env),
|
|
768
|
+
verbose: options.verbose,
|
|
769
|
+
ascii: shouldUseAscii(io.env),
|
|
770
|
+
});
|
|
771
|
+
// GitHub's commands are one per finding, and a proof with none has no line to write.
|
|
772
|
+
if (written !== '')
|
|
773
|
+
io.stdout(written);
|
|
774
|
+
if (report.summary.specs === 0)
|
|
775
|
+
return options.allowEmpty ? EXIT_OK : EXIT_ERROR;
|
|
776
|
+
return report.ok && !(options.strictTargets && report.summary.unprovable > 0) ? EXIT_OK : EXIT_FAILED;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* `spec-guard impact`: the files that depend on each path, and the rules in
|
|
780
|
+
* play there. ADR-0018. Exit 2 for a path outside the root or not there, and
|
|
781
|
+
* when no spec matched, as a query does: the dependents are written, but "no
|
|
782
|
+
* rules" over no specs is nothing measured, which the family contract says is
|
|
783
|
+
* not clean. `--allow-empty` asks for the dependents alone.
|
|
784
|
+
*/
|
|
785
|
+
async function runImpact(options, io, use) {
|
|
786
|
+
let report;
|
|
787
|
+
try {
|
|
788
|
+
report = await impactOf({
|
|
789
|
+
patterns: options.patterns,
|
|
790
|
+
root: options.root,
|
|
791
|
+
paths: options.paths,
|
|
792
|
+
// Absent and undefined are one answer to impactOf: every dependent.
|
|
793
|
+
depth: options.depth,
|
|
794
|
+
includeInactive: options.ignoreStatus,
|
|
795
|
+
includeSpecs: options.includeSpecs,
|
|
796
|
+
defaultSkips: options.defaultSkips,
|
|
797
|
+
exclude: options.exclude,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
catch (error) {
|
|
801
|
+
io.stderr(`spec-guard: ${error instanceof Error ? error.message : String(error)}`);
|
|
802
|
+
return EXIT_ERROR;
|
|
803
|
+
}
|
|
804
|
+
const answered = { ...report, config: use };
|
|
805
|
+
io.stdout(options.format === 'json' ? formatImpactJson(answered) : formatImpact(answered));
|
|
806
|
+
if (report.specFiles.length > 0)
|
|
807
|
+
return EXIT_OK;
|
|
808
|
+
if (options.format !== 'json')
|
|
809
|
+
io.stderr(`spec-guard: no spec files matched ${options.patterns.map((p) => `"${p}"`).join(', ')}`);
|
|
810
|
+
return options.allowEmpty ? EXIT_OK : EXIT_ERROR;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* `spec-guard cites`: the specs code comments cite, and whether each is still
|
|
814
|
+
* there and in force. ADR-0017.
|
|
815
|
+
*
|
|
816
|
+
* Exit 1 for a ghost, and under `--strict` for a stale citation or a file
|
|
817
|
+
* whose comments could not all be read; 2 when the answer could not be
|
|
818
|
+
* trusted - a family whose files match no document, a path outside the root.
|
|
819
|
+
*/
|
|
820
|
+
async function runCites(options, io, use) {
|
|
821
|
+
let report;
|
|
822
|
+
try {
|
|
823
|
+
const paths = await Promise.all(options.paths.map((entry) => resolveQueryPath(entry, options.root)));
|
|
824
|
+
for (const entry of paths) {
|
|
825
|
+
if (!entry.exists)
|
|
826
|
+
throw new CitesError(`"${entry.path}" does not exist, so there are no comments in it to read`);
|
|
827
|
+
}
|
|
828
|
+
report = {
|
|
829
|
+
...(await findCitations({
|
|
830
|
+
root: options.root,
|
|
831
|
+
patterns: options.patterns,
|
|
832
|
+
// Undefined, as absent, derives the families from the specs.
|
|
833
|
+
families: options.cites,
|
|
834
|
+
...(paths.length === 0 ? {} : { paths: paths.map((entry) => entry.path) }),
|
|
835
|
+
exclude: options.exclude,
|
|
836
|
+
defaultSkips: options.defaultSkips,
|
|
837
|
+
strict: options.strictTargets,
|
|
838
|
+
})),
|
|
839
|
+
config: use,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
catch (error) {
|
|
843
|
+
io.stderr(`spec-guard: ${error instanceof Error ? error.message : String(error)}`);
|
|
844
|
+
return EXIT_ERROR;
|
|
845
|
+
}
|
|
846
|
+
const written = options.format === 'json'
|
|
847
|
+
? formatCitesJson(report)
|
|
848
|
+
: options.format === 'sarif'
|
|
849
|
+
? formatCitesSarif(report, { version: version() })
|
|
850
|
+
: options.format === 'gitlab'
|
|
851
|
+
? formatGitlab(citesAnnotations(report))
|
|
852
|
+
: options.format === 'github'
|
|
853
|
+
? formatGithub(citesAnnotations(report))
|
|
854
|
+
: formatCites(report, {
|
|
855
|
+
color: shouldUseColor({ isTTY: io.isTTY }, options.color, io.env),
|
|
856
|
+
verbose: false,
|
|
857
|
+
ascii: shouldUseAscii(io.env),
|
|
858
|
+
});
|
|
859
|
+
if (written !== '')
|
|
860
|
+
io.stdout(written);
|
|
861
|
+
return report.ok ? EXIT_OK : EXIT_FAILED;
|
|
862
|
+
}
|
|
595
863
|
/** Runs the CLI and resolves to the process exit code. */
|
|
596
864
|
export async function main(argv = process.argv.slice(2), io = defaultIO()) {
|
|
597
865
|
let options;
|
|
@@ -612,6 +880,14 @@ export async function main(argv = process.argv.slice(2), io = defaultIO()) {
|
|
|
612
880
|
io.stdout(version());
|
|
613
881
|
return EXIT_OK;
|
|
614
882
|
}
|
|
883
|
+
// Before any command starts, so a server, a watch session and a proof refuse
|
|
884
|
+
// a pattern as a run does, rather than each answering with fewer documents.
|
|
885
|
+
// The configuration's are refused where it is read, naming the file.
|
|
886
|
+
const patternError = patternListError(options.patterns, specPatternError);
|
|
887
|
+
if (patternError !== null) {
|
|
888
|
+
io.stderr(`spec-guard: ${patternError}`);
|
|
889
|
+
return EXIT_ERROR;
|
|
890
|
+
}
|
|
615
891
|
// Read after --help and --version, which must work in a project whose
|
|
616
892
|
// package.json is broken, and before anything that runs a rule.
|
|
617
893
|
const commandLine = { ...options, patterns: [...options.patterns] };
|
|
@@ -621,8 +897,14 @@ export async function main(argv = process.argv.slice(2), io = defaultIO()) {
|
|
|
621
897
|
const { use } = configured;
|
|
622
898
|
if (options.command === 'query')
|
|
623
899
|
return runQuery(options, io, use);
|
|
900
|
+
if (options.command === 'cites')
|
|
901
|
+
return runCites(options, io, use);
|
|
902
|
+
if (options.command === 'impact')
|
|
903
|
+
return runImpact(options, io, use);
|
|
624
904
|
if (options.command === 'mcp')
|
|
625
905
|
return runMcp(options, commandLine, io, use);
|
|
906
|
+
if (options.command === 'prove')
|
|
907
|
+
return runProve(options, io, use);
|
|
626
908
|
if (options.watch)
|
|
627
909
|
return runWatchSession(commandLine, io);
|
|
628
910
|
let report;
|
|
@@ -639,8 +921,11 @@ export async function main(argv = process.argv.slice(2), io = defaultIO()) {
|
|
|
639
921
|
return EXIT_ERROR;
|
|
640
922
|
}
|
|
641
923
|
if (report.summary.specs === 0) {
|
|
642
|
-
|
|
643
|
-
|
|
924
|
+
// A document a script reads is still written, empty, for the script to
|
|
925
|
+
// read. GitHub's commands have nothing to annotate, and the person reading
|
|
926
|
+
// the job's log is told why, as a person at a terminal is.
|
|
927
|
+
if (options.format === 'sarif' || options.format === 'json' || options.format === 'gitlab') {
|
|
928
|
+
io.stdout(options.format === 'sarif' ? formatSarif(report, { version: version() }) : options.format === 'gitlab' ? formatGitlab([]) : formatJson(report));
|
|
644
929
|
}
|
|
645
930
|
else {
|
|
646
931
|
io.stderr(`spec-guard: no spec files matched ${options.patterns.map((p) => `"${p}"`).join(', ')}`);
|
|
@@ -659,6 +944,14 @@ export async function main(argv = process.argv.slice(2), io = defaultIO()) {
|
|
|
659
944
|
else if (options.format === 'json') {
|
|
660
945
|
io.stdout(formatJson(report));
|
|
661
946
|
}
|
|
947
|
+
else if (options.format === 'gitlab') {
|
|
948
|
+
io.stdout(formatGitlab(runAnnotations(report)));
|
|
949
|
+
}
|
|
950
|
+
else if (options.format === 'github') {
|
|
951
|
+
const annotations = formatGithub(runAnnotations(report));
|
|
952
|
+
if (annotations !== '')
|
|
953
|
+
io.stdout(annotations);
|
|
954
|
+
}
|
|
662
955
|
else {
|
|
663
956
|
io.stdout(formatReport(report, {
|
|
664
957
|
color: shouldUseColor({ isTTY: io.isTTY }, options.color, io.env),
|