repo-contract 0.1.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 +7 -0
- package/LICENSE +21 -0
- package/README.md +967 -0
- package/dist/.dts/config/define-repo-contract.d.ts +36 -0
- package/dist/.dts/config/define-repo-contract.d.ts.map +1 -0
- package/dist/.dts/config/tokenize-command.d.ts +33 -0
- package/dist/.dts/config/tokenize-command.d.ts.map +1 -0
- package/dist/.dts/config/validate-config.d.ts +32 -0
- package/dist/.dts/config/validate-config.d.ts.map +1 -0
- package/dist/.dts/errors.d.ts +155 -0
- package/dist/.dts/errors.d.ts.map +1 -0
- package/dist/.dts/evidence/build-evidence.d.ts +26 -0
- package/dist/.dts/evidence/build-evidence.d.ts.map +1 -0
- package/dist/.dts/execution/abort-signals.d.ts +29 -0
- package/dist/.dts/execution/abort-signals.d.ts.map +1 -0
- package/dist/.dts/execution/concurrency-pool.d.ts +14 -0
- package/dist/.dts/execution/concurrency-pool.d.ts.map +1 -0
- package/dist/.dts/execution/dependency-scheduler.d.ts +30 -0
- package/dist/.dts/execution/dependency-scheduler.d.ts.map +1 -0
- package/dist/.dts/execution/process-tree.d.ts +48 -0
- package/dist/.dts/execution/process-tree.d.ts.map +1 -0
- package/dist/.dts/execution/run-checks.d.ts +29 -0
- package/dist/.dts/execution/run-checks.d.ts.map +1 -0
- package/dist/.dts/execution/spawn-check.d.ts +30 -0
- package/dist/.dts/execution/spawn-check.d.ts.map +1 -0
- package/dist/.dts/index.d.ts +13 -0
- package/dist/.dts/index.d.ts.map +1 -0
- package/dist/.dts/parsing/parse-json.d.ts +8 -0
- package/dist/.dts/parsing/parse-json.d.ts.map +1 -0
- package/dist/.dts/parsing/parse-output.d.ts +10 -0
- package/dist/.dts/parsing/parse-output.d.ts.map +1 -0
- package/dist/.dts/parsing/parse-text.d.ts +8 -0
- package/dist/.dts/parsing/parse-text.d.ts.map +1 -0
- package/dist/.dts/parsing/parse-yaml.d.ts +21 -0
- package/dist/.dts/parsing/parse-yaml.d.ts.map +1 -0
- package/dist/.dts/policy/run-policies.d.ts +38 -0
- package/dist/.dts/policy/run-policies.d.ts.map +1 -0
- package/dist/.dts/presets/arethetypeswrong.d.ts +39 -0
- package/dist/.dts/presets/arethetypeswrong.d.ts.map +1 -0
- package/dist/.dts/presets/broken-links.d.ts +16 -0
- package/dist/.dts/presets/broken-links.d.ts.map +1 -0
- package/dist/.dts/presets/commitlint.d.ts +22 -0
- package/dist/.dts/presets/commitlint.d.ts.map +1 -0
- package/dist/.dts/presets/dead-code.d.ts +23 -0
- package/dist/.dts/presets/dead-code.d.ts.map +1 -0
- package/dist/.dts/presets/duplication.d.ts +14 -0
- package/dist/.dts/presets/duplication.d.ts.map +1 -0
- package/dist/.dts/presets/e2e.d.ts +4 -0
- package/dist/.dts/presets/e2e.d.ts.map +1 -0
- package/dist/.dts/presets/format.d.ts +4 -0
- package/dist/.dts/presets/format.d.ts.map +1 -0
- package/dist/.dts/presets/index.d.ts +31 -0
- package/dist/.dts/presets/index.d.ts.map +1 -0
- package/dist/.dts/presets/license.d.ts +4 -0
- package/dist/.dts/presets/license.d.ts.map +1 -0
- package/dist/.dts/presets/lint.d.ts +20 -0
- package/dist/.dts/presets/lint.d.ts.map +1 -0
- package/dist/.dts/presets/markdownlint.d.ts +23 -0
- package/dist/.dts/presets/markdownlint.d.ts.map +1 -0
- package/dist/.dts/presets/publint.d.ts +13 -0
- package/dist/.dts/presets/publint.d.ts.map +1 -0
- package/dist/.dts/presets/security-deps.d.ts +4 -0
- package/dist/.dts/presets/security-deps.d.ts.map +1 -0
- package/dist/.dts/presets/security-secrets.d.ts +4 -0
- package/dist/.dts/presets/security-secrets.d.ts.map +1 -0
- package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts +13 -0
- package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts.map +1 -0
- package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts +23 -0
- package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts.map +1 -0
- package/dist/.dts/presets/shared/missing-dependency.d.ts +19 -0
- package/dist/.dts/presets/shared/missing-dependency.d.ts.map +1 -0
- package/dist/.dts/presets/shared/read-json-report.d.ts +33 -0
- package/dist/.dts/presets/shared/read-json-report.d.ts.map +1 -0
- package/dist/.dts/presets/shared/terminal-status.d.ts +25 -0
- package/dist/.dts/presets/shared/terminal-status.d.ts.map +1 -0
- package/dist/.dts/presets/shared/vitest-json-policy.d.ts +20 -0
- package/dist/.dts/presets/shared/vitest-json-policy.d.ts.map +1 -0
- package/dist/.dts/presets/stylelint.d.ts +17 -0
- package/dist/.dts/presets/stylelint.d.ts.map +1 -0
- package/dist/.dts/presets/test.d.ts +4 -0
- package/dist/.dts/presets/test.d.ts.map +1 -0
- package/dist/.dts/presets/typecheck.d.ts +4 -0
- package/dist/.dts/presets/typecheck.d.ts.map +1 -0
- package/dist/.dts/run-repo-contract.d.ts +38 -0
- package/dist/.dts/run-repo-contract.d.ts.map +1 -0
- package/dist/.dts/types.d.ts +324 -0
- package/dist/.dts/types.d.ts.map +1 -0
- package/dist/index.cjs +46 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/presets.cjs +30 -0
- package/dist/presets.cjs.map +1 -0
- package/dist/presets.d.cts +1 -0
- package/dist/presets.d.ts +1 -0
- package/dist/presets.js +13 -0
- package/dist/presets.js.map +1 -0
- package/package.json +192 -0
- package/presets/package.json +5 -0
- package/schemas/evidence.schema.json +253 -0
- package/schemas/verdict.schema.json +66 -0
- package/src/config/define-repo-contract.ts +38 -0
- package/src/config/tokenize-command.ts +214 -0
- package/src/config/validate-config.ts +368 -0
- package/src/errors.ts +229 -0
- package/src/evidence/build-evidence.ts +91 -0
- package/src/execution/abort-signals.ts +56 -0
- package/src/execution/concurrency-pool.ts +64 -0
- package/src/execution/dependency-scheduler.ts +216 -0
- package/src/execution/process-tree.ts +107 -0
- package/src/execution/run-checks.ts +348 -0
- package/src/execution/spawn-check.ts +494 -0
- package/src/index.ts +44 -0
- package/src/parsing/parse-json.ts +18 -0
- package/src/parsing/parse-output.ts +26 -0
- package/src/parsing/parse-text.ts +10 -0
- package/src/parsing/parse-yaml.ts +40 -0
- package/src/policy/run-policies.ts +261 -0
- package/src/presets/arethetypeswrong.ts +116 -0
- package/src/presets/broken-links.ts +95 -0
- package/src/presets/commitlint.ts +77 -0
- package/src/presets/dead-code.ts +223 -0
- package/src/presets/duplication.ts +137 -0
- package/src/presets/e2e.ts +144 -0
- package/src/presets/format.ts +25 -0
- package/src/presets/index.ts +30 -0
- package/src/presets/license.ts +90 -0
- package/src/presets/lint.ts +116 -0
- package/src/presets/markdownlint.ts +105 -0
- package/src/presets/publint.ts +38 -0
- package/src/presets/security-deps.ts +142 -0
- package/src/presets/security-secrets.ts +93 -0
- package/src/presets/shared/error-warning-pass-policy.ts +39 -0
- package/src/presets/shared/exit-code-fail-rationale.ts +34 -0
- package/src/presets/shared/missing-dependency.ts +31 -0
- package/src/presets/shared/read-json-report.ts +46 -0
- package/src/presets/shared/terminal-status.ts +70 -0
- package/src/presets/shared/vitest-json-policy.ts +95 -0
- package/src/presets/stylelint.ts +101 -0
- package/src/presets/test.ts +19 -0
- package/src/presets/typecheck.ts +25 -0
- package/src/run-repo-contract.ts +80 -0
- package/src/types.ts +340 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { CheckEvidence, PolicyResult } from "../../types.js"
|
|
2
|
+
import { combinedOutput } from "./exit-code-fail-rationale.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The guard every preset needs immediately after `checkDependencyInstalled`
|
|
6
|
+
* and before its own pass/fail logic: a check whose process did not run to a
|
|
7
|
+
* normal exit on its own -- it timed out, was aborted via `options.signal`,
|
|
8
|
+
* was killed by a host `SIGINT`/`SIGTERM`, received an external signal, or
|
|
9
|
+
* failed to spawn for a reason other than a missing executable -- has a
|
|
10
|
+
* `null` `exitCode` and only whatever partial output the tool managed before
|
|
11
|
+
* termination. Interpreting that with an exit-code comparison or a JSON
|
|
12
|
+
* parse would present an operational failure to the consumer as a fabricated
|
|
13
|
+
* tool-specific verdict ("TypeScript reported type errors (exit code null)",
|
|
14
|
+
* "ESLint output could not be parsed as JSON"). This reports the real
|
|
15
|
+
* terminal cause instead.
|
|
16
|
+
*
|
|
17
|
+
* `"completed"` returns `undefined` -- the process reached its own exit and
|
|
18
|
+
* the preset interprets the (possibly non-zero) exit code itself. The
|
|
19
|
+
* ENOENT `"spawn_error"` case is already handled by `checkDependencyInstalled`;
|
|
20
|
+
* this catches the remaining spawn failures (`EACCES`, `ENOEXEC`) that
|
|
21
|
+
* `missing-dependency.ts` documents as falling through.
|
|
22
|
+
* @param result - the check's raw execution evidence to inspect.
|
|
23
|
+
* @param toolName - the tool's own display name for the rationale (e.g. "ESLint", "TypeScript").
|
|
24
|
+
* @returns a fail `PolicyResult` describing the abnormal termination, or `undefined` if the process ran to its own exit and the caller should proceed.
|
|
25
|
+
*/
|
|
26
|
+
export function checkTerminatedAbnormally(
|
|
27
|
+
result: CheckEvidence,
|
|
28
|
+
toolName: string,
|
|
29
|
+
): PolicyResult | undefined {
|
|
30
|
+
if (result.status === "completed") {
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const detail = combinedOutput(result)
|
|
35
|
+
const suffix = detail.length > 0 ? `\n${detail}` : ""
|
|
36
|
+
|
|
37
|
+
switch (result.status) {
|
|
38
|
+
case "timed_out":
|
|
39
|
+
return {
|
|
40
|
+
outcome: "fail",
|
|
41
|
+
rationale: `${toolName} did not finish: its process exceeded the configured timeout and was terminated.${suffix}`,
|
|
42
|
+
}
|
|
43
|
+
case "aborted":
|
|
44
|
+
return {
|
|
45
|
+
outcome: "fail",
|
|
46
|
+
rationale: `${toolName} did not finish: the run was aborted before its process completed.${suffix}`,
|
|
47
|
+
}
|
|
48
|
+
case "host_terminated":
|
|
49
|
+
return {
|
|
50
|
+
outcome: "fail",
|
|
51
|
+
rationale: `${toolName} did not finish: the host process received a termination signal and killed it.${suffix}`,
|
|
52
|
+
}
|
|
53
|
+
case "signaled":
|
|
54
|
+
return {
|
|
55
|
+
outcome: "fail",
|
|
56
|
+
rationale: `${toolName} was terminated by signal ${result.signal ?? "unknown"} before it completed.${suffix}`,
|
|
57
|
+
}
|
|
58
|
+
case "spawn_error":
|
|
59
|
+
return {
|
|
60
|
+
outcome: "fail",
|
|
61
|
+
rationale: `${toolName} could not be started: ${result.spawnError ?? "the process failed to spawn"}.`,
|
|
62
|
+
}
|
|
63
|
+
default: {
|
|
64
|
+
// A newly added `CheckStatus` must be classified here deliberately, not
|
|
65
|
+
// silently fall through the switch and be read by callers as "proceed".
|
|
66
|
+
const unhandled: never = result.status
|
|
67
|
+
throw new Error(`checkTerminatedAbnormally: unhandled check status ${String(unhandled)}`)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { JsonAssertionResult, JsonTestResult, JsonTestResults } from "vitest/reporters"
|
|
2
|
+
import type { ParsedOutput, PolicyResult } from "../../types.js"
|
|
3
|
+
|
|
4
|
+
type VitestJsonReport = JsonTestResults
|
|
5
|
+
type VitestJsonTestSuite = JsonTestResult
|
|
6
|
+
type VitestJsonAssertion = JsonAssertionResult
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Interprets Vitest's own `--reporter=json` output shape -- and nothing
|
|
10
|
+
* else. Shared between the `test` preset (a single, un-categorized
|
|
11
|
+
* `vitest run --reporter=json`) and this repository's own
|
|
12
|
+
* `test-unit`/`test-integration`/`test-property`/`test-e2e` checks, whose
|
|
13
|
+
* `run` splits tests into four mutually exclusive categories via
|
|
14
|
+
* scripts/run-test-category.mjs -- a repository-specific concern this
|
|
15
|
+
* function knows nothing about. Each of those four checks owns its own
|
|
16
|
+
* category-specific semantics on top of this evaluator; this function must
|
|
17
|
+
* stay narrowly scoped to "parse Vitest's JSON reporter shape," never grow
|
|
18
|
+
* into a general "testing policy" abstraction. A category's own
|
|
19
|
+
* `output.success === false` (vitest crashed / produced unparseable output)
|
|
20
|
+
* is kept distinct from a substantive test failure, exactly as every other
|
|
21
|
+
* preset in this package already does via `ParsedOutput.success`.
|
|
22
|
+
* @param output - the vitest check's parsed `--reporter=json` output to evaluate.
|
|
23
|
+
* @returns the pass/fail outcome and its rationale.
|
|
24
|
+
*/
|
|
25
|
+
export function evaluateVitestJsonPolicy(output: ParsedOutput<unknown> | undefined): PolicyResult {
|
|
26
|
+
if (!output?.success) {
|
|
27
|
+
return { outcome: "fail", rationale: "Vitest output could not be parsed as JSON." }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// `output.value` is `unknown` -- valid JSON that isn't Vitest's reporter
|
|
31
|
+
// shape (`null`, a primitive, a wrapper that printed `{}`, a config dump, a
|
|
32
|
+
// future schema change) must produce a fail verdict, not a TypeError out of
|
|
33
|
+
// the policy that `runPolicies` rethrows as `PolicyThrewError` and rejects the
|
|
34
|
+
// whole `runRepoContract()` promise with. `numFailedTests`/
|
|
35
|
+
// `numFailedTestSuites` being `undefined` (not `0`) already falls through the
|
|
36
|
+
// pass branch below, so `testResults` must be guarded before it is walked.
|
|
37
|
+
const value: unknown = output.value
|
|
38
|
+
if (typeof value !== "object" || value === null) {
|
|
39
|
+
return { outcome: "fail", rationale: "Vitest produced invalid JSON report data." }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const report = value as VitestJsonReport
|
|
43
|
+
|
|
44
|
+
if (!Array.isArray(report.testResults)) {
|
|
45
|
+
return { outcome: "fail", rationale: "Vitest produced invalid JSON report data." }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (report.numFailedTests === 0 && report.numFailedTestSuites === 0) {
|
|
49
|
+
return {
|
|
50
|
+
outcome: "pass",
|
|
51
|
+
rationale: `Vitest completed ${String(report.numTotalTests)} test(s) with 0 failures across ${String(report.numTotalTestSuites)} suite(s).`,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// `Partial<>` because the elements are untrusted parsed JSON, not a guaranteed
|
|
56
|
+
// `JsonTestResult` -- a suite missing `assertionResults` entirely (a partial or
|
|
57
|
+
// older reporter shape) must not throw out of the policy, just contribute no
|
|
58
|
+
// failure detail lines.
|
|
59
|
+
const failures = report.testResults.flatMap((suite: Partial<VitestJsonTestSuite>): string[] =>
|
|
60
|
+
(suite.assertionResults ?? [])
|
|
61
|
+
.filter((test: VitestJsonAssertion) => test.status === "failed")
|
|
62
|
+
.map((test: VitestJsonAssertion) => {
|
|
63
|
+
const location = test.location
|
|
64
|
+
? `:${String(test.location.line)}:${String(test.location.column)}`
|
|
65
|
+
: ""
|
|
66
|
+
|
|
67
|
+
const messages = (test.failureMessages ?? [])
|
|
68
|
+
.map((message: string) => message.trim())
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join(" | ")
|
|
71
|
+
|
|
72
|
+
// This mutant (dropping .filter(Boolean).join(" — "), returning the bare array instead)
|
|
73
|
+
// is proven killed under direct, unmocked `vitest run` of
|
|
74
|
+
// test/unit/presets/shared/vitest-json-policy.test.ts -- applying this exact replacement
|
|
75
|
+
// by hand and re-running fails 4 of that file's tests, including two that assert the
|
|
76
|
+
// complete rationale string via `toBe` specifically to make this mutation observable --
|
|
77
|
+
// but it survives every `npx stryker run` against this file (scoped or full, reproduced
|
|
78
|
+
// three times, once with concurrency forced to 1): this mutant's own `coveredBy`/
|
|
79
|
+
// `testsCompleted` show only 6 of the file's 11 tests ever ran against it, never the two
|
|
80
|
+
// `toBe` tests that would kill it. See stryker.config.mjs's own comment for the identical
|
|
81
|
+
// "coverage-attribution quirk" already found and worked around for `perTest` mode -- this
|
|
82
|
+
// is the same class of bug surfacing under `"all"` mode too, for this one line.
|
|
83
|
+
// Stryker disable next-line MethodExpression -- proven killed under direct vitest execution but Stryker's own coverage attribution never runs the two tests that would kill it; see the comment above.
|
|
84
|
+
return [`${suite.name}${location}`, test.fullName, messages].filter(Boolean).join(" — ")
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
outcome: "fail",
|
|
90
|
+
rationale: [
|
|
91
|
+
`Vitest reported ${String(report.numFailedTests)} failing test(s) across ${String(report.numFailedTestSuites)} failing suite(s):`,
|
|
92
|
+
...failures.map((failure: string) => `- ${failure}`),
|
|
93
|
+
].join("\n"),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { CheckDefinitionConfig } from "../types.js"
|
|
2
|
+
import { combinedOutput } from "./shared/exit-code-fail-rationale.js"
|
|
3
|
+
import { errorWarningPassPolicy } from "./shared/error-warning-pass-policy.js"
|
|
4
|
+
import { checkDependencyInstalled } from "./shared/missing-dependency.js"
|
|
5
|
+
import { checkTerminatedAbnormally } from "./shared/terminal-status.js"
|
|
6
|
+
|
|
7
|
+
/** Stylelint's own `--formatter json` contract -- not published as a TypeScript type by the tool. */
|
|
8
|
+
interface StylelintWarning {
|
|
9
|
+
readonly line: number
|
|
10
|
+
readonly column: number
|
|
11
|
+
readonly rule: string
|
|
12
|
+
readonly severity: "error" | "warning"
|
|
13
|
+
readonly text: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface StylelintResult {
|
|
17
|
+
readonly source?: string
|
|
18
|
+
readonly errored: boolean
|
|
19
|
+
readonly warnings: readonly StylelintWarning[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Options accepted by {@link stylelint}. */
|
|
23
|
+
interface StylelintOptions {
|
|
24
|
+
/** Glob passed straight through to stylelint as its positional target. Defaults to `"**\/*.{css,scss}"` -- adjust it for your own stylesheet file extensions (e.g. `.less`, `.vue`, `.svelte`). */
|
|
25
|
+
readonly glob?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* CSS/SCSS lint via stylelint, using whatever stylelint config the
|
|
30
|
+
* consumer's own repository already has. `severity: "error"` blocks;
|
|
31
|
+
* `severity: "warning"` is reported but never blocks, matching how the
|
|
32
|
+
* `lint` preset treats ESLint's own severities.
|
|
33
|
+
* @param options - configuration for this check; see {@link StylelintOptions}.
|
|
34
|
+
* @returns the configured check.
|
|
35
|
+
*/
|
|
36
|
+
export function stylelint(options: StylelintOptions = {}): CheckDefinitionConfig {
|
|
37
|
+
const { glob = "**/*.{css,scss}" } = options
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
run: ["stylelint", glob, "--formatter", "json"],
|
|
41
|
+
output: { format: "json" },
|
|
42
|
+
policy: ({ result }) => {
|
|
43
|
+
const missing = checkDependencyInstalled(result, "stylelint")
|
|
44
|
+
if (missing) return missing
|
|
45
|
+
|
|
46
|
+
const terminated = checkTerminatedAbnormally(result, "stylelint")
|
|
47
|
+
if (terminated) return terminated
|
|
48
|
+
|
|
49
|
+
if (!result.output?.success) {
|
|
50
|
+
// stylelint exits non-zero with no JSON on stdout when its glob
|
|
51
|
+
// matched no stylesheets or its config failed to load. Surface what
|
|
52
|
+
// it printed so a repo that legitimately has no CSS sees the real
|
|
53
|
+
// cause rather than an opaque parse failure.
|
|
54
|
+
const printed = combinedOutput(result)
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
outcome: "fail",
|
|
58
|
+
rationale:
|
|
59
|
+
printed.length > 0
|
|
60
|
+
? `stylelint output could not be parsed as JSON. stylelint printed:\n${printed}`
|
|
61
|
+
: "stylelint output could not be parsed as JSON.",
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Valid JSON of an unexpected shape (a stylelint formatter change, a
|
|
66
|
+
// primitive, an entry without a `warnings` array) must fail cleanly, not
|
|
67
|
+
// throw a TypeError out of the `flatMap` below.
|
|
68
|
+
const value: unknown = result.output.value
|
|
69
|
+
if (
|
|
70
|
+
!Array.isArray(value) ||
|
|
71
|
+
!value.every(
|
|
72
|
+
(entry): entry is StylelintResult =>
|
|
73
|
+
typeof entry === "object" &&
|
|
74
|
+
entry !== null &&
|
|
75
|
+
Array.isArray((entry as { warnings?: unknown }).warnings),
|
|
76
|
+
)
|
|
77
|
+
) {
|
|
78
|
+
return { outcome: "fail", rationale: "stylelint output could not be parsed as JSON." }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const results: readonly StylelintResult[] = value
|
|
82
|
+
|
|
83
|
+
const render = (file: StylelintResult, warning: StylelintWarning): string =>
|
|
84
|
+
`${file.source ?? "<unknown file>"}:${String(warning.line)}:${String(warning.column)} [${warning.rule}]: ${warning.text}`
|
|
85
|
+
|
|
86
|
+
const errorDetails = results.flatMap((file: StylelintResult): string[] =>
|
|
87
|
+
file.warnings
|
|
88
|
+
.filter((warning: StylelintWarning) => warning.severity === "error")
|
|
89
|
+
.map((warning: StylelintWarning) => render(file, warning)),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const warningDetails = results.flatMap((file: StylelintResult): string[] =>
|
|
93
|
+
file.warnings
|
|
94
|
+
.filter((warning: StylelintWarning) => warning.severity === "warning")
|
|
95
|
+
.map((warning: StylelintWarning) => render(file, warning)),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return errorWarningPassPolicy("stylelint", errorDetails, warningDetails)
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CheckDefinitionConfig } from "../types.js"
|
|
2
|
+
import { checkDependencyInstalled } from "./shared/missing-dependency.js"
|
|
3
|
+
import { checkTerminatedAbnormally } from "./shared/terminal-status.js"
|
|
4
|
+
import { evaluateVitestJsonPolicy } from "./shared/vitest-json-policy.js"
|
|
5
|
+
|
|
6
|
+
/** Unit/integration test execution via Vitest, reading its JSON reporter output. */
|
|
7
|
+
export const test: CheckDefinitionConfig = {
|
|
8
|
+
run: ["vitest", "run", "--reporter=json"],
|
|
9
|
+
output: { format: "json" },
|
|
10
|
+
policy: ({ result }) => {
|
|
11
|
+
const missing = checkDependencyInstalled(result, "vitest")
|
|
12
|
+
if (missing) return missing
|
|
13
|
+
|
|
14
|
+
const terminated = checkTerminatedAbnormally(result, "Vitest")
|
|
15
|
+
if (terminated) return terminated
|
|
16
|
+
|
|
17
|
+
return evaluateVitestJsonPolicy(result.output)
|
|
18
|
+
},
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CheckDefinitionConfig } from "../types.js"
|
|
2
|
+
import { checkDependencyInstalled } from "./shared/missing-dependency.js"
|
|
3
|
+
import { checkTerminatedAbnormally } from "./shared/terminal-status.js"
|
|
4
|
+
import { exitCodeFailRationale } from "./shared/exit-code-fail-rationale.js"
|
|
5
|
+
|
|
6
|
+
/** Type checking via `tsc --noEmit`. */
|
|
7
|
+
export const typecheck: CheckDefinitionConfig = {
|
|
8
|
+
run: ["tsc", "--noEmit", "-p", "tsconfig.json"],
|
|
9
|
+
policy: ({ result }) => {
|
|
10
|
+
const missing = checkDependencyInstalled(result, "typescript")
|
|
11
|
+
if (missing) return missing
|
|
12
|
+
|
|
13
|
+
const terminated = checkTerminatedAbnormally(result, "TypeScript")
|
|
14
|
+
if (terminated) return terminated
|
|
15
|
+
|
|
16
|
+
if (result.exitCode === 0) {
|
|
17
|
+
return { outcome: "pass", rationale: "tsc reported no type errors." }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
outcome: "fail",
|
|
22
|
+
rationale: exitCodeFailRationale(result, "TypeScript reported type errors"),
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as os from "node:os"
|
|
2
|
+
import { validateRepoContractConfig } from "./config/validate-config.js"
|
|
3
|
+
import { buildEvidence } from "./evidence/build-evidence.js"
|
|
4
|
+
import { runChecks } from "./execution/run-checks.js"
|
|
5
|
+
import { runPolicies } from "./policy/run-policies.js"
|
|
6
|
+
import type {
|
|
7
|
+
CheckSchema,
|
|
8
|
+
Evidence,
|
|
9
|
+
RepoContractConfig,
|
|
10
|
+
RunRepoContractOptions,
|
|
11
|
+
Verdict,
|
|
12
|
+
} from "./types.js"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Executes every configured check, collects its evidence, optionally parses
|
|
16
|
+
* its output, evaluates every check's repository-owned policy against the
|
|
17
|
+
* complete evidence, and aggregates a verdict. Never calls `process.exit()`
|
|
18
|
+
* -- returns data; the caller decides what to do with `verdict.passed`.
|
|
19
|
+
*
|
|
20
|
+
* Structural configuration problems throw synchronously before any process
|
|
21
|
+
* spawns. Anything only discoverable by attempting execution (a missing
|
|
22
|
+
* binary, a bad `cwd`) becomes evidence on that check (`status:
|
|
23
|
+
* "spawn_error"`), never a throw. A policy function throwing or rejecting
|
|
24
|
+
* rejects this function's own returned promise (`PolicyThrewError`, or an
|
|
25
|
+
* `AggregateError` of them if more than one policy failed this way) --
|
|
26
|
+
* distinct from a policy that ran fine and simply returned a failure
|
|
27
|
+
* string. See `src/errors.ts` for the full distinction.
|
|
28
|
+
*
|
|
29
|
+
* Execution and policy evaluation are strictly phased: every check finishes
|
|
30
|
+
* running and every check's evidence is fully assembled before any policy
|
|
31
|
+
* is invoked (see specs/architecture.md) -- a policy can safely read
|
|
32
|
+
* `ctx.evidence` for any sibling check's result.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately not declared `async`: validation runs and can throw before this function returns
|
|
35
|
+
* anything at all, so a config problem is a genuine synchronous exception to the caller (as
|
|
36
|
+
* documented above and on `InvalidRepoContractConfigError`/`InvalidCheckConfigError`/
|
|
37
|
+
* `DependencyDeclaredLaterError`) -- an `async function`'s body runs to its first `await` still
|
|
38
|
+
* inside the caller's own call stack, but any throw before that point is nonetheless converted by
|
|
39
|
+
* the language into a rejected `Promise`, never surfaced as a synchronous exception. Splitting
|
|
40
|
+
* validation out into this synchronous wrapper, with the rest of the work in the `async` function
|
|
41
|
+
* below, keeps the documented synchronous-throw guarantee actually true rather than aspirational.
|
|
42
|
+
* @param config - the repo-contract configuration to run: its checks, concurrency, and their policies
|
|
43
|
+
* @param options - run options; `options.checks` restricts execution to specific check ids, `options.signal` allows cancelling the run
|
|
44
|
+
* @returns the assembled `evidence` for every check together with the aggregated `verdict`
|
|
45
|
+
*/
|
|
46
|
+
export function runRepoContract<const TChecks extends CheckSchema>(
|
|
47
|
+
config: RepoContractConfig<TChecks>,
|
|
48
|
+
options?: RunRepoContractOptions,
|
|
49
|
+
): Promise<{ evidence: Evidence<TChecks>; verdict: Verdict<TChecks> }> {
|
|
50
|
+
validateRepoContractConfig(config)
|
|
51
|
+
return runRepoContractAfterValidation(config, options)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The rest of `runRepoContract`'s work, once its config has already been validated -- see that
|
|
56
|
+
* function's own doc comment for why validation itself lives in a separate, non-`async` wrapper.
|
|
57
|
+
* @param config - the already-validated repo-contract configuration to run.
|
|
58
|
+
* @param options - run options; see `runRepoContract`.
|
|
59
|
+
* @returns the assembled `evidence` for every check together with the aggregated `verdict`
|
|
60
|
+
*/
|
|
61
|
+
async function runRepoContractAfterValidation<const TChecks extends CheckSchema>(
|
|
62
|
+
config: RepoContractConfig<TChecks>,
|
|
63
|
+
options: RunRepoContractOptions | undefined,
|
|
64
|
+
): Promise<{ evidence: Evidence<TChecks>; verdict: Verdict<TChecks> }> {
|
|
65
|
+
// validateRepoContractConfig above already rejects any config.concurrency that is not a
|
|
66
|
+
// positive integer (>= 1, see validate-config.ts), so by the time this line runs
|
|
67
|
+
// config.concurrency is always either undefined or already truthy -- `??` and `&&` therefore
|
|
68
|
+
// select the identical branch for every value this parameter can actually hold here.
|
|
69
|
+
// Stryker disable next-line LogicalOperator -- validateRepoContractConfig above already rejects any config.concurrency that is not a positive integer (>= 1), so by the time this line runs config.concurrency is always either undefined or already truthy; `??` and `&&` therefore select the identical branch for every value this parameter can actually hold here, making them equivalent at this exact call site.
|
|
70
|
+
const concurrency = config.concurrency ?? os.availableParallelism()
|
|
71
|
+
const startedAt = new Date()
|
|
72
|
+
|
|
73
|
+
const results = await runChecks(config.checks, concurrency, options)
|
|
74
|
+
const completedAt = new Date()
|
|
75
|
+
|
|
76
|
+
const { evidence, entries } = await buildEvidence(results, startedAt, completedAt)
|
|
77
|
+
const verdict = await runPolicies(entries, evidence)
|
|
78
|
+
|
|
79
|
+
return { evidence, verdict } as { evidence: Evidence<TChecks>; verdict: Verdict<TChecks> }
|
|
80
|
+
}
|