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.
Files changed (145) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +967 -0
  4. package/dist/.dts/config/define-repo-contract.d.ts +36 -0
  5. package/dist/.dts/config/define-repo-contract.d.ts.map +1 -0
  6. package/dist/.dts/config/tokenize-command.d.ts +33 -0
  7. package/dist/.dts/config/tokenize-command.d.ts.map +1 -0
  8. package/dist/.dts/config/validate-config.d.ts +32 -0
  9. package/dist/.dts/config/validate-config.d.ts.map +1 -0
  10. package/dist/.dts/errors.d.ts +155 -0
  11. package/dist/.dts/errors.d.ts.map +1 -0
  12. package/dist/.dts/evidence/build-evidence.d.ts +26 -0
  13. package/dist/.dts/evidence/build-evidence.d.ts.map +1 -0
  14. package/dist/.dts/execution/abort-signals.d.ts +29 -0
  15. package/dist/.dts/execution/abort-signals.d.ts.map +1 -0
  16. package/dist/.dts/execution/concurrency-pool.d.ts +14 -0
  17. package/dist/.dts/execution/concurrency-pool.d.ts.map +1 -0
  18. package/dist/.dts/execution/dependency-scheduler.d.ts +30 -0
  19. package/dist/.dts/execution/dependency-scheduler.d.ts.map +1 -0
  20. package/dist/.dts/execution/process-tree.d.ts +48 -0
  21. package/dist/.dts/execution/process-tree.d.ts.map +1 -0
  22. package/dist/.dts/execution/run-checks.d.ts +29 -0
  23. package/dist/.dts/execution/run-checks.d.ts.map +1 -0
  24. package/dist/.dts/execution/spawn-check.d.ts +30 -0
  25. package/dist/.dts/execution/spawn-check.d.ts.map +1 -0
  26. package/dist/.dts/index.d.ts +13 -0
  27. package/dist/.dts/index.d.ts.map +1 -0
  28. package/dist/.dts/parsing/parse-json.d.ts +8 -0
  29. package/dist/.dts/parsing/parse-json.d.ts.map +1 -0
  30. package/dist/.dts/parsing/parse-output.d.ts +10 -0
  31. package/dist/.dts/parsing/parse-output.d.ts.map +1 -0
  32. package/dist/.dts/parsing/parse-text.d.ts +8 -0
  33. package/dist/.dts/parsing/parse-text.d.ts.map +1 -0
  34. package/dist/.dts/parsing/parse-yaml.d.ts +21 -0
  35. package/dist/.dts/parsing/parse-yaml.d.ts.map +1 -0
  36. package/dist/.dts/policy/run-policies.d.ts +38 -0
  37. package/dist/.dts/policy/run-policies.d.ts.map +1 -0
  38. package/dist/.dts/presets/arethetypeswrong.d.ts +39 -0
  39. package/dist/.dts/presets/arethetypeswrong.d.ts.map +1 -0
  40. package/dist/.dts/presets/broken-links.d.ts +16 -0
  41. package/dist/.dts/presets/broken-links.d.ts.map +1 -0
  42. package/dist/.dts/presets/commitlint.d.ts +22 -0
  43. package/dist/.dts/presets/commitlint.d.ts.map +1 -0
  44. package/dist/.dts/presets/dead-code.d.ts +23 -0
  45. package/dist/.dts/presets/dead-code.d.ts.map +1 -0
  46. package/dist/.dts/presets/duplication.d.ts +14 -0
  47. package/dist/.dts/presets/duplication.d.ts.map +1 -0
  48. package/dist/.dts/presets/e2e.d.ts +4 -0
  49. package/dist/.dts/presets/e2e.d.ts.map +1 -0
  50. package/dist/.dts/presets/format.d.ts +4 -0
  51. package/dist/.dts/presets/format.d.ts.map +1 -0
  52. package/dist/.dts/presets/index.d.ts +31 -0
  53. package/dist/.dts/presets/index.d.ts.map +1 -0
  54. package/dist/.dts/presets/license.d.ts +4 -0
  55. package/dist/.dts/presets/license.d.ts.map +1 -0
  56. package/dist/.dts/presets/lint.d.ts +20 -0
  57. package/dist/.dts/presets/lint.d.ts.map +1 -0
  58. package/dist/.dts/presets/markdownlint.d.ts +23 -0
  59. package/dist/.dts/presets/markdownlint.d.ts.map +1 -0
  60. package/dist/.dts/presets/publint.d.ts +13 -0
  61. package/dist/.dts/presets/publint.d.ts.map +1 -0
  62. package/dist/.dts/presets/security-deps.d.ts +4 -0
  63. package/dist/.dts/presets/security-deps.d.ts.map +1 -0
  64. package/dist/.dts/presets/security-secrets.d.ts +4 -0
  65. package/dist/.dts/presets/security-secrets.d.ts.map +1 -0
  66. package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts +13 -0
  67. package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts.map +1 -0
  68. package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts +23 -0
  69. package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts.map +1 -0
  70. package/dist/.dts/presets/shared/missing-dependency.d.ts +19 -0
  71. package/dist/.dts/presets/shared/missing-dependency.d.ts.map +1 -0
  72. package/dist/.dts/presets/shared/read-json-report.d.ts +33 -0
  73. package/dist/.dts/presets/shared/read-json-report.d.ts.map +1 -0
  74. package/dist/.dts/presets/shared/terminal-status.d.ts +25 -0
  75. package/dist/.dts/presets/shared/terminal-status.d.ts.map +1 -0
  76. package/dist/.dts/presets/shared/vitest-json-policy.d.ts +20 -0
  77. package/dist/.dts/presets/shared/vitest-json-policy.d.ts.map +1 -0
  78. package/dist/.dts/presets/stylelint.d.ts +17 -0
  79. package/dist/.dts/presets/stylelint.d.ts.map +1 -0
  80. package/dist/.dts/presets/test.d.ts +4 -0
  81. package/dist/.dts/presets/test.d.ts.map +1 -0
  82. package/dist/.dts/presets/typecheck.d.ts +4 -0
  83. package/dist/.dts/presets/typecheck.d.ts.map +1 -0
  84. package/dist/.dts/run-repo-contract.d.ts +38 -0
  85. package/dist/.dts/run-repo-contract.d.ts.map +1 -0
  86. package/dist/.dts/types.d.ts +324 -0
  87. package/dist/.dts/types.d.ts.map +1 -0
  88. package/dist/index.cjs +46 -0
  89. package/dist/index.cjs.map +1 -0
  90. package/dist/index.d.cts +1 -0
  91. package/dist/index.d.ts +1 -0
  92. package/dist/index.js +11 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/presets.cjs +30 -0
  95. package/dist/presets.cjs.map +1 -0
  96. package/dist/presets.d.cts +1 -0
  97. package/dist/presets.d.ts +1 -0
  98. package/dist/presets.js +13 -0
  99. package/dist/presets.js.map +1 -0
  100. package/package.json +192 -0
  101. package/presets/package.json +5 -0
  102. package/schemas/evidence.schema.json +253 -0
  103. package/schemas/verdict.schema.json +66 -0
  104. package/src/config/define-repo-contract.ts +38 -0
  105. package/src/config/tokenize-command.ts +214 -0
  106. package/src/config/validate-config.ts +368 -0
  107. package/src/errors.ts +229 -0
  108. package/src/evidence/build-evidence.ts +91 -0
  109. package/src/execution/abort-signals.ts +56 -0
  110. package/src/execution/concurrency-pool.ts +64 -0
  111. package/src/execution/dependency-scheduler.ts +216 -0
  112. package/src/execution/process-tree.ts +107 -0
  113. package/src/execution/run-checks.ts +348 -0
  114. package/src/execution/spawn-check.ts +494 -0
  115. package/src/index.ts +44 -0
  116. package/src/parsing/parse-json.ts +18 -0
  117. package/src/parsing/parse-output.ts +26 -0
  118. package/src/parsing/parse-text.ts +10 -0
  119. package/src/parsing/parse-yaml.ts +40 -0
  120. package/src/policy/run-policies.ts +261 -0
  121. package/src/presets/arethetypeswrong.ts +116 -0
  122. package/src/presets/broken-links.ts +95 -0
  123. package/src/presets/commitlint.ts +77 -0
  124. package/src/presets/dead-code.ts +223 -0
  125. package/src/presets/duplication.ts +137 -0
  126. package/src/presets/e2e.ts +144 -0
  127. package/src/presets/format.ts +25 -0
  128. package/src/presets/index.ts +30 -0
  129. package/src/presets/license.ts +90 -0
  130. package/src/presets/lint.ts +116 -0
  131. package/src/presets/markdownlint.ts +105 -0
  132. package/src/presets/publint.ts +38 -0
  133. package/src/presets/security-deps.ts +142 -0
  134. package/src/presets/security-secrets.ts +93 -0
  135. package/src/presets/shared/error-warning-pass-policy.ts +39 -0
  136. package/src/presets/shared/exit-code-fail-rationale.ts +34 -0
  137. package/src/presets/shared/missing-dependency.ts +31 -0
  138. package/src/presets/shared/read-json-report.ts +46 -0
  139. package/src/presets/shared/terminal-status.ts +70 -0
  140. package/src/presets/shared/vitest-json-policy.ts +95 -0
  141. package/src/presets/stylelint.ts +101 -0
  142. package/src/presets/test.ts +19 -0
  143. package/src/presets/typecheck.ts +25 -0
  144. package/src/run-repo-contract.ts +80 -0
  145. 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
+ }