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
package/README.md
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
# repo-contract
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
[](https://github.com/maverickcer/repo-contract/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/repo-contract)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[]()
|
|
8
|
+
[]()
|
|
9
|
+
[]()
|
|
10
|
+
[]()
|
|
11
|
+
TODO(readme-badges): uncomment once the first version is published to npm and CI is live.
|
|
12
|
+
-->
|
|
13
|
+
|
|
14
|
+
**Turn your repository's engineering standards into enforceable contracts.**
|
|
15
|
+
|
|
16
|
+
repo-contract is a tool-agnostic contract execution and evidence layer. You define the engineering standards your repository cares about — tests, coverage, mutation testing, linting, security scanning, documentation, dependency health, or anything else that runs as a command — and repo-contract turns those standards into enforceable, machine-readable contracts.
|
|
17
|
+
|
|
18
|
+
Each check executes a command, captures what actually happened as evidence, and hands that evidence to a policy function **you write**. The policy decides whether the evidence satisfies your repository's standard:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
{ outcome: "pass" | "fail" | "warn", rationale: string }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
repo-contract aggregates every policy result into one verdict.
|
|
25
|
+
|
|
26
|
+
It does not decide what "good code" means.
|
|
27
|
+
|
|
28
|
+
**Your repository does.**
|
|
29
|
+
|
|
30
|
+
This makes repo-contract different from a test runner, linter, CI provider, or quality analyzer. It does not replace ESLint, Vitest, Stryker, npm audit, or similar tools. It executes them, captures their results, and gives your repository a programmable enforcement layer across all of them.
|
|
31
|
+
|
|
32
|
+
Because it is a plain function call with no CLI and no hidden state, the same contract can run locally and in CI. Wire it into a `precommit`, `prepublishOnly`, CI job, or whatever workflow your repository already uses.
|
|
33
|
+
|
|
34
|
+
The result is not merely "the tests passed."
|
|
35
|
+
|
|
36
|
+
It is a repository-defined engineering contract with evidence explaining why.
|
|
37
|
+
|
|
38
|
+
This README is a narrative walkthrough. For the precise reference — every exported type, field, and error code — see the generated [API report](docs/api-report/repo-contract.api.md) (and its [presets counterpart](docs/api-report/repo-contract-presets.api.md) for `repo-contract/presets`), produced straight from source by [API Extractor](https://api-extractor.com/) so it can never drift from what the package actually exports.
|
|
39
|
+
|
|
40
|
+
**When this isn't worth adopting:** if your CI already runs each tool as its own separate step with its own separate pass/fail gate, and you do not need unified evidence or policies that reason across checks, plain shell scripts in your CI configuration may be all you need. repo-contract earns its keep when you want a single programmatic contract across multiple engineering standards, evidence you can persist or diff over time, or policies that reason about more than one check's output.
|
|
41
|
+
|
|
42
|
+
## Why contracts?
|
|
43
|
+
|
|
44
|
+
Modern repositories accumulate engineering standards faster than they accumulate enforcement.
|
|
45
|
+
|
|
46
|
+
A team may agree that:
|
|
47
|
+
|
|
48
|
+
- tests must pass;
|
|
49
|
+
- coverage must remain above a threshold;
|
|
50
|
+
- mutation scores must remain above a threshold;
|
|
51
|
+
- dependencies must have no known high-severity vulnerabilities;
|
|
52
|
+
- generated files must remain synchronized;
|
|
53
|
+
- public APIs must remain compatible;
|
|
54
|
+
- documentation must accompany changes;
|
|
55
|
+
- new code must satisfy architectural boundaries.
|
|
56
|
+
|
|
57
|
+
Those standards are often scattered across CI YAML, package scripts, documentation, code review conventions, and institutional knowledge.
|
|
58
|
+
|
|
59
|
+
repo-contract gives those standards a single executable boundary.
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
repository standards
|
|
63
|
+
|
|
|
64
|
+
v
|
|
65
|
+
repo-contract
|
|
66
|
+
|
|
|
67
|
+
+--> execute checks
|
|
68
|
+
|
|
|
69
|
+
+--> collect evidence
|
|
70
|
+
|
|
|
71
|
+
+--> interpret evidence with repository-owned policies
|
|
72
|
+
|
|
|
73
|
+
v
|
|
74
|
+
enforceable verdict
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The important distinction is that repo-contract does not own the standards.
|
|
78
|
+
|
|
79
|
+
Your repository does.
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
npm install --save-dev repo-contract
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Requires Node.js `>=20.0.0`.
|
|
88
|
+
|
|
89
|
+
While repo-contract is pre-1.0, pin a tilde range (`"repo-contract": "~0.1.0"`): a `0.x` **minor** bump can carry a breaking change to the Stable tier (see [VERSIONING.md](VERSIONING.md)), so read the [CHANGELOG](CHANGELOG.md)'s breaking-changes notes on every minor upgrade, not just majors.
|
|
90
|
+
|
|
91
|
+
`yaml` is an optional peer dependency, needed only if a check requests `output: { format: "yaml" }`:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
npm install --save-dev yaml
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Runtime support matrix
|
|
98
|
+
|
|
99
|
+
repo-contract spawns processes and reads `process.env` — it is server/CLI-only by design, not an isomorphic/browser package.
|
|
100
|
+
|
|
101
|
+
| Environment | Supported |
|
|
102
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
103
|
+
| Node.js `>=20.0.0` (macOS, Linux, Windows) | Yes |
|
|
104
|
+
| Bun (latest release) | Yes — tested in CI against the real published package shape. No non-default permissions needed. |
|
|
105
|
+
| Deno (latest release) | Yes — tested in CI against the real published package shape. Requires `--allow-read --allow-run --allow-env` (see [ADR 0003](specs/decisions/0003-cross-platform-command-execution-and-process-cleanup.md)). |
|
|
106
|
+
| Browser | No — this package executes local processes |
|
|
107
|
+
|
|
108
|
+
See [ADR 0003](specs/decisions/0003-cross-platform-command-execution-and-process-cleanup.md) for what "tested" covers here and why.
|
|
109
|
+
|
|
110
|
+
### Accessibility
|
|
111
|
+
|
|
112
|
+
repo-contract has no user interface. It produces machine-readable `Evidence`/`Verdict` objects and typed errors; any rendering — a terminal summary, a CI annotation, a dashboard — is the consumer's surface, and WCAG / accessibility conformance applies there, not here.
|
|
113
|
+
|
|
114
|
+
## Quick start
|
|
115
|
+
|
|
116
|
+
Define your repository's standards as checks:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// repo-contract.config.ts
|
|
120
|
+
import { defineRepoContract } from "repo-contract"
|
|
121
|
+
|
|
122
|
+
export default defineRepoContract({
|
|
123
|
+
checks: {
|
|
124
|
+
tests: {
|
|
125
|
+
run: "npm test",
|
|
126
|
+
|
|
127
|
+
policy: ({ result }) =>
|
|
128
|
+
result.exitCode === 0
|
|
129
|
+
? {
|
|
130
|
+
outcome: "pass",
|
|
131
|
+
rationale: "Tests exited 0.",
|
|
132
|
+
}
|
|
133
|
+
: {
|
|
134
|
+
outcome: "fail",
|
|
135
|
+
rationale: "Tests must pass.",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
mutation: {
|
|
140
|
+
run: "npm run mutation",
|
|
141
|
+
output: { format: "json" },
|
|
142
|
+
|
|
143
|
+
policy: ({ result }) => {
|
|
144
|
+
if (!result.output?.success) {
|
|
145
|
+
return {
|
|
146
|
+
outcome: "fail",
|
|
147
|
+
rationale: "Mutation report was not valid JSON.",
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const score = (result.output.value as { mutationScore: number }).mutationScore
|
|
152
|
+
|
|
153
|
+
return score >= 90
|
|
154
|
+
? {
|
|
155
|
+
outcome: "pass",
|
|
156
|
+
rationale: `Mutation score was ${score}%.`,
|
|
157
|
+
}
|
|
158
|
+
: {
|
|
159
|
+
outcome: "fail",
|
|
160
|
+
rationale: `Mutation score must be at least 90% (got ${score}%).`,
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Execute the contract:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { runRepoContract } from "repo-contract"
|
|
172
|
+
import config from "./repo-contract.config.js"
|
|
173
|
+
|
|
174
|
+
const { evidence, verdict } = await runRepoContract(config)
|
|
175
|
+
|
|
176
|
+
console.log(verdict.passed)
|
|
177
|
+
console.log(verdict.checks.mutation)
|
|
178
|
+
|
|
179
|
+
process.exitCode = verdict.passed ? 0 : 1
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`runRepoContract()` never calls `process.exit()` itself. Your integration decides what to do with the result.
|
|
183
|
+
|
|
184
|
+
There is no CLI, no config-file discovery magic, and no hidden state.
|
|
185
|
+
|
|
186
|
+
## The model
|
|
187
|
+
|
|
188
|
+
```text
|
|
189
|
+
check configuration
|
|
190
|
+
|
|
|
191
|
+
v
|
|
192
|
+
execute checks
|
|
193
|
+
|
|
|
194
|
+
v
|
|
195
|
+
collect evidence
|
|
196
|
+
|
|
|
197
|
+
v
|
|
198
|
+
optionally parse explicitly requested output
|
|
199
|
+
|
|
|
200
|
+
v
|
|
201
|
+
execute per-check policy
|
|
202
|
+
|
|
|
203
|
+
v
|
|
204
|
+
PolicyResult {
|
|
205
|
+
outcome: "pass" | "fail" | "warn",
|
|
206
|
+
rationale: string
|
|
207
|
+
}
|
|
208
|
+
|
|
|
209
|
+
v
|
|
210
|
+
aggregate verdict
|
|
211
|
+
|
|
|
212
|
+
v
|
|
213
|
+
{ evidence, verdict }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Evidence** describes what happened: the command, its exit code, signal, timing, captured stdout/stderr, and, if requested, parsed output.
|
|
217
|
+
|
|
218
|
+
Evidence never decides whether the result was acceptable.
|
|
219
|
+
|
|
220
|
+
**Verdict** describes whether the result was acceptable according to your policies.
|
|
221
|
+
|
|
222
|
+
**Policies are repository-owned.** A policy returns:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
{
|
|
226
|
+
outcome: "pass" | "fail" | "warn"
|
|
227
|
+
rationale: string
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`rationale` is mandatory for every outcome, including `"pass"`. It should contain enough actionable detail — file/line locations, rule IDs, test names, counts, or other relevant information — that a human, CI system, or AI agent can understand the result without rerunning the check.
|
|
232
|
+
|
|
233
|
+
`"warn"` is non-blocking. It means the policy's requirements were satisfied but the evidence is worth surfacing for review.
|
|
234
|
+
|
|
235
|
+
## AI guardrails
|
|
236
|
+
|
|
237
|
+
AI coding systems have become capable of producing substantial amounts of working software. The problem is no longer simply whether an AI can write code.
|
|
238
|
+
|
|
239
|
+
The problem is whether the code consistently satisfies the engineering standards of the repository.
|
|
240
|
+
|
|
241
|
+
Robert C. Martin ("Uncle Bob") has discussed this problem publicly, describing AI-generated code that can appear productive while still leaving significant amounts of poor-quality code behind. His experience highlighted an important distinction: giving an AI more and more software-development guidance is not necessarily the best way to improve the result.
|
|
242
|
+
|
|
243
|
+
Instead of attempting to put every engineering principle, convention, and quality rule into an AI's context window, put the important rules around the AI as **enforceable guardrails**.
|
|
244
|
+
|
|
245
|
+
repo-contract lets your repository define those guardrails.
|
|
246
|
+
|
|
247
|
+
For example:
|
|
248
|
+
|
|
249
|
+
```text
|
|
250
|
+
AI writes code
|
|
251
|
+
|
|
|
252
|
+
v
|
|
253
|
+
repo-contract
|
|
254
|
+
|
|
|
255
|
+
+--> tests
|
|
256
|
+
+--> typecheck
|
|
257
|
+
+--> lint
|
|
258
|
+
+--> coverage
|
|
259
|
+
+--> mutation testing
|
|
260
|
+
+--> architecture
|
|
261
|
+
+--> security
|
|
262
|
+
+--> API compatibility
|
|
263
|
+
+--> repository-specific standards
|
|
264
|
+
|
|
|
265
|
+
v
|
|
266
|
+
actionable verdict
|
|
267
|
+
|
|
|
268
|
+
v
|
|
269
|
+
AI fixes what failed
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The AI does not need to memorize every rule.
|
|
273
|
+
|
|
274
|
+
It needs to satisfy the repository's contract.
|
|
275
|
+
|
|
276
|
+
This is particularly useful for quality checks that are difficult to express through instructions alone. Mutation testing can test whether a test suite actually detects meaningful code changes. CRAP reports can expose code that combines complexity with insufficient test coverage. Your policy can simply require a low CRAP score or it can require both a low score and a maximum complexity. Security checks can enforce dependency and secret-management standards. Architecture checks can enforce dependency boundaries.
|
|
277
|
+
|
|
278
|
+
The result is a feedback loop:
|
|
279
|
+
|
|
280
|
+
```text
|
|
281
|
+
generate
|
|
282
|
+
|
|
|
283
|
+
v
|
|
284
|
+
verify
|
|
285
|
+
|
|
|
286
|
+
v
|
|
287
|
+
explain failure
|
|
288
|
+
|
|
|
289
|
+
v
|
|
290
|
+
fix
|
|
291
|
+
|
|
|
292
|
+
v
|
|
293
|
+
verify again
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The policy rationale is important here. A result such as:
|
|
297
|
+
|
|
298
|
+
```text
|
|
299
|
+
FAIL: mutation score must be at least 90% (got 82%)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
is useful to a human.
|
|
303
|
+
|
|
304
|
+
A result such as:
|
|
305
|
+
|
|
306
|
+
```text
|
|
307
|
+
FAIL: 3 mutants survived in src/auth/session.ts;
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
is useful to both a human and an AI agent.
|
|
311
|
+
|
|
312
|
+
repo-contract does not attempt to become an AI coding agent. It provides the executable boundary against which an agent's work can be evaluated.
|
|
313
|
+
|
|
314
|
+
**The repository supplies the guardrails. The AI supplies the implementation.**
|
|
315
|
+
|
|
316
|
+
## Open source contribution guardrails
|
|
317
|
+
|
|
318
|
+
Open source projects face a different version of the same problem.
|
|
319
|
+
|
|
320
|
+
A maintainer may know exactly what a contribution needs to satisfy, while a first-time contributor has no way to know all of those expectations.
|
|
321
|
+
|
|
322
|
+
The result can be repeated review cycles:
|
|
323
|
+
|
|
324
|
+
```text
|
|
325
|
+
contributor submits PR
|
|
326
|
+
|
|
|
327
|
+
v
|
|
328
|
+
maintainer finds issue
|
|
329
|
+
|
|
|
330
|
+
v
|
|
331
|
+
contributor fixes issue
|
|
332
|
+
|
|
|
333
|
+
v
|
|
334
|
+
another issue is discovered
|
|
335
|
+
|
|
|
336
|
+
v
|
|
337
|
+
repeat
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
repo-contract can move those expectations into an executable contract.
|
|
341
|
+
|
|
342
|
+
A project can enforce standards for:
|
|
343
|
+
|
|
344
|
+
- tests and test coverage;
|
|
345
|
+
- mutation scores;
|
|
346
|
+
- formatting and linting;
|
|
347
|
+
- type safety;
|
|
348
|
+
- architecture;
|
|
349
|
+
- documentation;
|
|
350
|
+
- dependency security;
|
|
351
|
+
- licenses;
|
|
352
|
+
- public API compatibility;
|
|
353
|
+
- generated artifacts;
|
|
354
|
+
- package publishing;
|
|
355
|
+
- repository-specific conventions.
|
|
356
|
+
|
|
357
|
+
More importantly, policies can interpret the evidence and provide actionable guidance.
|
|
358
|
+
|
|
359
|
+
Instead of:
|
|
360
|
+
|
|
361
|
+
```text
|
|
362
|
+
CI failed.
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
a contributor can receive:
|
|
366
|
+
|
|
367
|
+
```text
|
|
368
|
+
FAIL: public API compatibility check failed.
|
|
369
|
+
|
|
370
|
+
2 breaking changes were detected:
|
|
371
|
+
|
|
372
|
+
- Removed export: ContractResult
|
|
373
|
+
- Changed parameter type: runRepoContract(config)
|
|
374
|
+
|
|
375
|
+
Restore the export or document the breaking change according to
|
|
376
|
+
the repository's versioning policy.
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
This reduces the amount of repository knowledge a contributor must acquire before making a successful contribution.
|
|
380
|
+
|
|
381
|
+
It also gives maintainers a consistent enforcement mechanism that does not depend on a particular maintainer remembering every rule during review.
|
|
382
|
+
|
|
383
|
+
The goal is not to eliminate human review.
|
|
384
|
+
|
|
385
|
+
The goal is to make human review focus on the things that require human judgment rather than repeatedly identifying mechanical violations.
|
|
386
|
+
|
|
387
|
+
## Defining checks
|
|
388
|
+
|
|
389
|
+
Each check owns its identifier, command, output interpretation, and policy:
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
checks: {
|
|
393
|
+
lint: {
|
|
394
|
+
run: ["eslint", ".", "--format", "json"],
|
|
395
|
+
output: { format: "json" },
|
|
396
|
+
|
|
397
|
+
policy: ({ result }) => {
|
|
398
|
+
if (!result.output?.success) {
|
|
399
|
+
return {
|
|
400
|
+
outcome: "fail",
|
|
401
|
+
rationale: "ESLint output was not valid JSON.",
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const files = result.output.value as { errorCount: number }[]
|
|
406
|
+
const errors = files.reduce((sum, file) => sum + file.errorCount, 0)
|
|
407
|
+
|
|
408
|
+
return errors === 0
|
|
409
|
+
? {
|
|
410
|
+
outcome: "pass",
|
|
411
|
+
rationale: "ESLint reported 0 errors.",
|
|
412
|
+
}
|
|
413
|
+
: {
|
|
414
|
+
outcome: "fail",
|
|
415
|
+
rationale: `${errors} lint error(s).`,
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### `run`
|
|
423
|
+
|
|
424
|
+
A `string` is tokenized into an executable and its arguments **without invoking a shell**.
|
|
425
|
+
|
|
426
|
+
Shell operators such as `;`, `&`, `|`, backticks, `$(...)`, `<`, `>`, and newlines are not interpreted. A string containing one is rejected with a configuration error because it indicates an assumption that shell interpretation is occurring.
|
|
427
|
+
|
|
428
|
+
Glob characters such as `*`, `?`, `~`, `[`, `]`, `{`, and `}` are not rejected. Many CLI tools expand their own arguments internally, and each argument is passed through as its own, separately-escaped element rather than concatenated into a command line, so those characters do not create shell injection behavior. (On Windows, resolving a `.cmd`/`.bat` shim unavoidably routes through `cmd.exe` — see [Security model](#security-model) and [SECURITY.md](SECURITY.md) for that platform-specific nuance.)
|
|
429
|
+
|
|
430
|
+
A `readonly string[]` bypasses tokenization entirely and is used as argv verbatim. This is the recommended form for arguments containing characters that should never be interpreted.
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
run: "eslint . --max-warnings 0"
|
|
434
|
+
run: ["eslint", ".", "--max-warnings", "0"]
|
|
435
|
+
run: "npm run build && npm test" // throws: run string contains an unquoted "&" -- use array form or "shell: true"
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
To opt into real shell execution, including pipes, redirects, and `&&`, set `shell: true`. In that mode `run` must be a string and is passed to the platform shell as-is.
|
|
439
|
+
|
|
440
|
+
See [Security model](#security-model) before enabling this.
|
|
441
|
+
|
|
442
|
+
### `output`
|
|
443
|
+
|
|
444
|
+
By default, check output is not parsed. `result.stdout` and `result.stderr` contain the captured raw text.
|
|
445
|
+
|
|
446
|
+
Request parsing explicitly:
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
output: {
|
|
450
|
+
format: "json"
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Uses `JSON.parse`. Malformed output produces:
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
{
|
|
458
|
+
success: false,
|
|
459
|
+
error: string
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
and never throws.
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
output: {
|
|
467
|
+
format: "yaml"
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
Requires the optional `yaml` peer dependency.
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
output: {
|
|
475
|
+
format: "text"
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Provides trimmed text passthrough and always succeeds.
|
|
480
|
+
|
|
481
|
+
`result.output.value` is `unknown` for every format. repo-contract has no schema knowledge of what an external tool prints, so your policy narrows or casts it according to the tool's actual output.
|
|
482
|
+
|
|
483
|
+
`result.output` is `undefined` when a check does not request a format. If a policy reads `result.output.value` (or `.success`/`.error`/`.format`) without narrowing first, and that check never configured `output`, `runRepoContract()` rejects with [`PolicyReadUnrequestedOutputError`](docs/api-report/repo-contract.api.md), which names the check and tells you to add `output: { format: "json" }` (or `"yaml"`/`"text"`) -- rather than the generic `PolicyThrewError` you'd otherwise have to debug from a bare "Cannot read properties of undefined" stack trace.
|
|
484
|
+
|
|
485
|
+
The sibling mistake -- reading `result.output.value` when the format _was_ requested but the parse itself failed (`result.output.success === false`, so `result.output` has `error`, not `value`) -- similarly rejects with [`PolicyReadFailedParseValueError`](docs/api-report/repo-contract.api.md) instead of a generic `PolicyThrewError`. Check `result.output.success` before reading `.value` to handle a parse failure explicitly rather than hitting either error.
|
|
486
|
+
|
|
487
|
+
### `dependsOn`
|
|
488
|
+
|
|
489
|
+
By default, every check runs independently and in parallel.
|
|
490
|
+
|
|
491
|
+
Name other check IDs to establish explicit execution ordering:
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
checks: {
|
|
495
|
+
build: {
|
|
496
|
+
run: "npm run build",
|
|
497
|
+
|
|
498
|
+
policy: ({ result }) =>
|
|
499
|
+
result.exitCode === 0
|
|
500
|
+
? {
|
|
501
|
+
outcome: "pass",
|
|
502
|
+
rationale: "Build succeeded.",
|
|
503
|
+
}
|
|
504
|
+
: {
|
|
505
|
+
outcome: "fail",
|
|
506
|
+
rationale: "Build failed.",
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
integration: {
|
|
511
|
+
run: "npm run test:integration",
|
|
512
|
+
dependsOn: ["build"],
|
|
513
|
+
|
|
514
|
+
policy: ({ result }) =>
|
|
515
|
+
result.exitCode === 0
|
|
516
|
+
? {
|
|
517
|
+
outcome: "pass",
|
|
518
|
+
rationale: "Integration tests passed.",
|
|
519
|
+
}
|
|
520
|
+
: {
|
|
521
|
+
outcome: "fail",
|
|
522
|
+
rationale: "Integration tests failed.",
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Independent checks run concurrently. `dependsOn` allows checks to express ordering only when ordering is actually required.
|
|
529
|
+
|
|
530
|
+
`dependsOn` does not cause repo-contract to decide whether the dependent check should run based on the dependency's policy result. That decision belongs to the dependent check's command or policy.
|
|
531
|
+
|
|
532
|
+
Artifacts needed by another check should flow through the filesystem or another system designed for data transfer. `dependsOn` controls execution ordering; it does not move data between checks.
|
|
533
|
+
|
|
534
|
+
Every named dependency must exist in `checks`, a check cannot depend on itself, and the entire dependency graph must be acyclic. These conditions are validated synchronously before anything is spawned.
|
|
535
|
+
|
|
536
|
+
### `policy`
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
interface PolicyResult {
|
|
540
|
+
outcome: "pass" | "fail" | "warn"
|
|
541
|
+
rationale: string
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
policy: (ctx) => PolicyResult | Promise<PolicyResult>
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
`outcome` is the policy's own judgment:
|
|
548
|
+
|
|
549
|
+
- `"pass"` — the evidence satisfies the repository's requirements.
|
|
550
|
+
- `"fail"` — it does not.
|
|
551
|
+
- `"warn"` — the requirements are satisfied, but something worth reviewing should be surfaced.
|
|
552
|
+
|
|
553
|
+
`"warn"` never fails `verdict.passed`.
|
|
554
|
+
|
|
555
|
+
`rationale` is required for every outcome. It should contain enough actionable information that the consumer can understand the result without rerunning the command or re-parsing its output.
|
|
556
|
+
|
|
557
|
+
Prefer:
|
|
558
|
+
|
|
559
|
+
```text
|
|
560
|
+
ESLint reported 2 errors:
|
|
561
|
+
- src/foo.ts:12:4 [no-explicit-any]: Unexpected any.
|
|
562
|
+
- src/bar.ts:8:7 [no-unused-vars]: 'value' is defined but never used.
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
over:
|
|
566
|
+
|
|
567
|
+
```text
|
|
568
|
+
See output above.
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
`ctx.result` is the current check's evidence.
|
|
572
|
+
|
|
573
|
+
`ctx.evidence` is the entire run's evidence, including every sibling check. Every configured check completes execution and has its evidence fully assembled before policies run, allowing policies to reason across checks:
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
policy: ({ result, evidence }) => {
|
|
577
|
+
const testsPassed = evidence.checks.tests?.exitCode === 0
|
|
578
|
+
|
|
579
|
+
if (!testsPassed) {
|
|
580
|
+
return {
|
|
581
|
+
outcome: "fail",
|
|
582
|
+
rationale: "Mutation policy requires the test suite to pass.",
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const score = Number(result.stdout)
|
|
587
|
+
|
|
588
|
+
return score >= 90
|
|
589
|
+
? {
|
|
590
|
+
outcome: "pass",
|
|
591
|
+
rationale: `Mutation score was ${score}%.`,
|
|
592
|
+
}
|
|
593
|
+
: {
|
|
594
|
+
outcome: "fail",
|
|
595
|
+
rationale: `Mutation score must be at least 90% (got ${score}%).`,
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
`ctx.dependencies` contains this check's declared `dependsOn` evidence, keyed by ID. It is `{}` for a check without dependencies and is never `undefined`.
|
|
601
|
+
|
|
602
|
+
```ts
|
|
603
|
+
policy: (ctx) => {
|
|
604
|
+
const buildOutput = ctx.dependencies.build?.stdout
|
|
605
|
+
|
|
606
|
+
return buildOutput?.includes("Compiled successfully")
|
|
607
|
+
? {
|
|
608
|
+
outcome: "pass",
|
|
609
|
+
rationale: "Build output confirmed compilation succeeded.",
|
|
610
|
+
}
|
|
611
|
+
: {
|
|
612
|
+
outcome: "fail",
|
|
613
|
+
rationale: "Build output did not confirm successful compilation.",
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
Dependency policy results are not included in `ctx.dependencies`. They remain available at the top-level `Verdict`.
|
|
619
|
+
|
|
620
|
+
A policy is always invoked for every check, regardless of how that check's process ended: completed, timed out, killed by a signal, or failed to spawn.
|
|
621
|
+
|
|
622
|
+
repo-contract does not decide what those execution outcomes mean. Your policy does.
|
|
623
|
+
|
|
624
|
+
A policy that throws is different. A thrown or rejected policy represents a bug in policy code, not a failed engineering check. `runRepoContract()` rejects with `PolicyThrewError`, or an `AggregateError` when multiple policies throw.
|
|
625
|
+
|
|
626
|
+
Other execution options include:
|
|
627
|
+
|
|
628
|
+
- `cwd`
|
|
629
|
+
- `env`
|
|
630
|
+
- `inheritEnv`
|
|
631
|
+
- `timeoutMs`
|
|
632
|
+
|
|
633
|
+
`inheritEnv` defaults to `true`. Set it to `false` when a check requires a minimal environment.
|
|
634
|
+
|
|
635
|
+
## Preset checks
|
|
636
|
+
|
|
637
|
+
You do not have to hand-write every common check.
|
|
638
|
+
|
|
639
|
+
`repo-contract/presets` ships a curated, growing catalog of ready-made `CheckDefinitionConfig`s for tools commonly used by TypeScript and JavaScript repositories.
|
|
640
|
+
|
|
641
|
+
A preset encodes how to execute and interpret a common tool.
|
|
642
|
+
|
|
643
|
+
It does **not** encode your repository's definition of quality.
|
|
644
|
+
|
|
645
|
+
Import a preset, spread it into your own `checks` record, and override whatever you need — most often `policy`:
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
import { defineRepoContract } from "repo-contract"
|
|
649
|
+
import { format, typecheck, license } from "repo-contract/presets"
|
|
650
|
+
|
|
651
|
+
export default defineRepoContract({
|
|
652
|
+
checks: {
|
|
653
|
+
format,
|
|
654
|
+
typecheck: {
|
|
655
|
+
...typecheck,
|
|
656
|
+
timeoutMs: 60_000,
|
|
657
|
+
},
|
|
658
|
+
license: {
|
|
659
|
+
...license,
|
|
660
|
+
policy: myStricterLicensePolicy,
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
})
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Some presets are factories because they expose options that change what gets executed:
|
|
667
|
+
|
|
668
|
+
```ts
|
|
669
|
+
import { defineRepoContract } from "repo-contract"
|
|
670
|
+
import { lint, deadCode } from "repo-contract/presets"
|
|
671
|
+
|
|
672
|
+
export default defineRepoContract({
|
|
673
|
+
checks: {
|
|
674
|
+
lint: lint({ path: "src" }),
|
|
675
|
+
deadCode: deadCode({
|
|
676
|
+
exemptUnusedDevDependencies: ["some-cli-only-tool"],
|
|
677
|
+
}),
|
|
678
|
+
},
|
|
679
|
+
})
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
**Preset options are the preferred way to change what a preset executes.** A direct `run` override is an escape hatch.
|
|
683
|
+
|
|
684
|
+
Using options keeps your configuration decoupled from a preset's exact command representation, which can evolve independently.
|
|
685
|
+
|
|
686
|
+
Every execution-affecting preset option is threaded through the actual command line so evidence records the exact options used in `evidence.checks.<id>.args`.
|
|
687
|
+
|
|
688
|
+
Every preset's policy fails with an actionable message if its underlying tool is not installed, with one deliberate exception: `securityDeps` shells out to `npm` itself, which cannot be "missing" in any environment capable of running `npm run <script>` at all. repo-contract never installs, bundles, or implicitly depends on these tools.
|
|
689
|
+
|
|
690
|
+
Each preset assumes its CLI is already a devDependency of your repository:
|
|
691
|
+
|
|
692
|
+
```sh
|
|
693
|
+
npm install --save-dev prettier
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
or:
|
|
697
|
+
|
|
698
|
+
```sh
|
|
699
|
+
pnpm add -D prettier
|
|
700
|
+
yarn add -D prettier
|
|
701
|
+
bun add -d prettier
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
| Category | Preset | Wraps |
|
|
705
|
+
| ------------------- | ------------------------ | ------------------------------------------------------------------------------ |
|
|
706
|
+
| Testing | `test` | `vitest run --reporter=json` |
|
|
707
|
+
| Testing | `e2e` | `playwright test --reporter=json` |
|
|
708
|
+
| Code quality | `lint(options?)` | `eslint <path> --format json` |
|
|
709
|
+
| Code quality | `format` | `prettier --write .` |
|
|
710
|
+
| Code quality | `typecheck` | `tsc --noEmit -p tsconfig.json` |
|
|
711
|
+
| Code quality | `deadCode(options?)` | `knip --reporter json` |
|
|
712
|
+
| Code quality | `duplication(options?)` | `jscpd <path> --reporters json --output reports/jscpd --silent` |
|
|
713
|
+
| Code quality | `stylelint(options?)` | `stylelint <glob> --formatter json` |
|
|
714
|
+
| Docs | `markdownlint(options?)` | `markdownlint-cli2 <glob>` — requires repository configuration for JSON output |
|
|
715
|
+
| Docs | `brokenLinks(options?)` | `linkinator <start> --recurse --format json --skip node_modules` |
|
|
716
|
+
| Security/governance | `securityDeps` | `npm audit --omit=dev --json` |
|
|
717
|
+
| Security/governance | `securitySecrets` | `secretlint --format json --output reports/secretlint.json **/*` |
|
|
718
|
+
| Security/governance | `license` | `licensee --production --osi --errors-only --ndjson` |
|
|
719
|
+
| Security/governance | `commitlint(options?)` | `commitlint --from <from> --to <to>` |
|
|
720
|
+
| Publishing | `publint` | `publint run` |
|
|
721
|
+
| Publishing | `arethetypeswrong` | `attw --pack . --format json` |
|
|
722
|
+
|
|
723
|
+
Not every test runner has a preset. Jest, Cypress, and Mocha have different reporter formats and may require bespoke presets. The underlying pattern remains the same: execute the tool, capture evidence, interpret its output, and apply your repository's policy.
|
|
724
|
+
|
|
725
|
+
Unlike its neighbors, `format` auto-fixes (`--write`) and therefore cannot itself fail on unformatted input — `prettier --write` reports success once it finishes rewriting files. If you want a hard gate on formatting (in CI, for example), run `prettier --check .` directly instead of this preset.
|
|
726
|
+
|
|
727
|
+
## Evidence
|
|
728
|
+
|
|
729
|
+
See the generated [API report](docs/api-report/repo-contract.api.md) for the full field-by-field reference on `Evidence` and `CheckEvidence`.
|
|
730
|
+
|
|
731
|
+
```ts
|
|
732
|
+
interface Evidence {
|
|
733
|
+
version: 1
|
|
734
|
+
startedAt: string
|
|
735
|
+
completedAt: string
|
|
736
|
+
durationMs: number
|
|
737
|
+
|
|
738
|
+
checks: Record<
|
|
739
|
+
string,
|
|
740
|
+
{
|
|
741
|
+
command: string
|
|
742
|
+
args: readonly string[]
|
|
743
|
+
startedAt: string
|
|
744
|
+
completedAt: string
|
|
745
|
+
durationMs: number
|
|
746
|
+
exitCode: number | null
|
|
747
|
+
signal: NodeJS.Signals | null
|
|
748
|
+
stdout: string
|
|
749
|
+
stderr: string
|
|
750
|
+
status: "completed" | "timed_out" | "signaled" | "host_terminated" | "spawn_error" | "aborted"
|
|
751
|
+
spawnError?: string
|
|
752
|
+
output?:
|
|
753
|
+
| {
|
|
754
|
+
format: string
|
|
755
|
+
success: true
|
|
756
|
+
value: unknown
|
|
757
|
+
}
|
|
758
|
+
| {
|
|
759
|
+
format: string
|
|
760
|
+
success: false
|
|
761
|
+
error: string
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
>
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
`status` distinguishes **why** a process ended in its terminal state, independently of whether the policy considered that result acceptable.
|
|
769
|
+
|
|
770
|
+
A non-zero exit code is `status: "completed"`, not an execution error.
|
|
771
|
+
|
|
772
|
+
## Verdict
|
|
773
|
+
|
|
774
|
+
See the generated [API report](docs/api-report/repo-contract.api.md) for the full field-by-field reference on `Verdict`.
|
|
775
|
+
|
|
776
|
+
```ts
|
|
777
|
+
interface Verdict {
|
|
778
|
+
version: 2
|
|
779
|
+
passed: boolean
|
|
780
|
+
|
|
781
|
+
checks: Record<
|
|
782
|
+
string,
|
|
783
|
+
{
|
|
784
|
+
outcome: "pass" | "fail" | "warn"
|
|
785
|
+
rationale: string
|
|
786
|
+
}
|
|
787
|
+
>
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
`checks[id]` is that check's own `PolicyResult`, exactly as returned by its policy.
|
|
792
|
+
|
|
793
|
+
`passed` is `true` only when every check's outcome is `"pass"` or `"warn"`.
|
|
794
|
+
|
|
795
|
+
`"fail"` is the only outcome that fails the run.
|
|
796
|
+
|
|
797
|
+
`Verdict` is returned alongside `Evidence`, never merged into it. Consumers can inspect:
|
|
798
|
+
|
|
799
|
+
```ts
|
|
800
|
+
evidence.checks[id]
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
to understand what happened and:
|
|
804
|
+
|
|
805
|
+
```ts
|
|
806
|
+
verdict.checks[id]
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
to understand what the repository concluded about it.
|
|
810
|
+
|
|
811
|
+
See [Evidence, policy rationale, and consumer judgment](specs/architecture.md#evidence-policy-rationale-and-consumer-judgment) for why these responsibilities remain separate.
|
|
812
|
+
|
|
813
|
+
## Regression detection
|
|
814
|
+
|
|
815
|
+
repo-contract has no built-in baseline system and no persistence layer.
|
|
816
|
+
|
|
817
|
+
The core engine (validation, execution, evidence, policy) does not read or write files on its own initiative beyond spawning the commands you configure and, where you set `output: { format: "json" | "yaml" }`, parsing that command's own captured stdout. A handful of published presets (`securitySecrets`, `duplication`, `markdownlint`) additionally read back a fixed report file their own `run` command was told to write, as part of interpreting that tool's JSON output — always that preset's own single, hardcoded, tool-specific path, never a scan or discovery of arbitrary files.
|
|
818
|
+
|
|
819
|
+
If your repository wants regression detection, persist the evidence or relevant measurements yourself and compare them in your policy:
|
|
820
|
+
|
|
821
|
+
```ts
|
|
822
|
+
import baseline from "./baseline.json"
|
|
823
|
+
|
|
824
|
+
policy: ({ result }) => {
|
|
825
|
+
const current = (result.output?.value as { score: number }).score
|
|
826
|
+
|
|
827
|
+
return current >= baseline.mutation.score
|
|
828
|
+
? {
|
|
829
|
+
outcome: "pass",
|
|
830
|
+
rationale: `Mutation score was ${current}.`,
|
|
831
|
+
}
|
|
832
|
+
: {
|
|
833
|
+
outcome: "fail",
|
|
834
|
+
rationale: `Mutation score regressed from ${baseline.mutation.score} to ${current}.`,
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
This keeps persistence and baseline semantics under repository control.
|
|
840
|
+
|
|
841
|
+
## CI integration
|
|
842
|
+
|
|
843
|
+
repo-contract has no CLI and no config-discovery magic.
|
|
844
|
+
|
|
845
|
+
Call `runRepoContract()` from a small script and map the result to a process exit code yourself:
|
|
846
|
+
|
|
847
|
+
```ts
|
|
848
|
+
// scripts/run-contract.mjs
|
|
849
|
+
import { runRepoContract } from "repo-contract"
|
|
850
|
+
import config from "../repo-contract.config.js"
|
|
851
|
+
|
|
852
|
+
const { verdict } = await runRepoContract(config)
|
|
853
|
+
|
|
854
|
+
for (const [id, result] of Object.entries(verdict.checks)) {
|
|
855
|
+
console.log(`[${result.outcome.toUpperCase()}] ${id}: ${result.rationale}`)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
process.exitCode = verdict.passed ? 0 : 1
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
```json
|
|
862
|
+
{
|
|
863
|
+
"scripts": {
|
|
864
|
+
"contract": "tsx scripts/run-contract.mjs"
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
The same contract can then be used by local development and CI:
|
|
870
|
+
|
|
871
|
+
```sh
|
|
872
|
+
npm run contract
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
Point a `precommit`, `prepublishOnly`, or CI job at the same command to enforce the same engineering standards in each environment.
|
|
876
|
+
|
|
877
|
+
This repository uses its own `repo-contract.config.ts` to validate itself. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
878
|
+
|
|
879
|
+
## Example: layered organizational governance
|
|
880
|
+
|
|
881
|
+
repo-contract is the mechanism, not the policy owner. An organization can express its engineering standard for a project type **once** -- as an internal contract package that wraps repo-contract and owns the executors -- and start every project of that type from a matching boilerplate that extends the shared configuration rather than redefining it.
|
|
882
|
+
|
|
883
|
+
[`examples/`](examples/README.md) is a minimal, runnable end-to-end wiring of that model: an `internal-boilerplate-contract` package (three read-only checks, exported ESLint/Prettier/TypeScript baselines, a reusable CI workflow) and a trivial `boilerplate` that consumes it. See the [walkthrough](examples/README.md) and [ADR 0010](specs/decisions/0010-review-driven-contracts-and-shared-internal-system-contracts.md) for the architecture.
|
|
884
|
+
|
|
885
|
+
## Enterprise / locked-down environments
|
|
886
|
+
|
|
887
|
+
The package's entire shipped surface (`src/**` -- the programmatic API and every published preset) has:
|
|
888
|
+
|
|
889
|
+
- no CLI;
|
|
890
|
+
- no network calls;
|
|
891
|
+
- no telemetry;
|
|
892
|
+
- no automatic package installation;
|
|
893
|
+
- no hidden configuration.
|
|
894
|
+
|
|
895
|
+
It only does what your configuration tells it to do: execute the commands you define with the environment and options you specify. The core engine never discovers or reads a file you didn't ask it to; a small number of presets (`securitySecrets`, `duplication`, `markdownlint`) read back their own tool's fixed, hardcoded report path after running it — see [Regression detection](#regression-detection) above — which is deterministic per preset, not discovery of arbitrary filesystem state.
|
|
896
|
+
|
|
897
|
+
This makes it suitable for locked-down enterprise environments where tools such as `npx` or network access may be unavailable.
|
|
898
|
+
|
|
899
|
+
The "no network calls" guarantee is mechanically enforced, not merely documented: an ESLint rule and an independent, ESLint-free repository check both reject network-capable imports, globals, and unreviewed spawned commands anywhere in the shipped surface. See [SECURITY.md](SECURITY.md) for the full threat model and [ADR 0007](specs/decisions/0007-no-network-surface.md) for what's covered, what's deliberately excluded, and why.
|
|
900
|
+
|
|
901
|
+
## Security model
|
|
902
|
+
|
|
903
|
+
Command execution is the core of this package and therefore a security-sensitive boundary.
|
|
904
|
+
|
|
905
|
+
The default `run` behavior — whether a string or array — never explicitly invokes a shell. On POSIX this means no shell is invoked at all; on Windows, resolving a `.cmd`/`.bat` shim (how most npm-installed CLIs are actually invoked there) unavoidably routes through `cmd.exe`, with argument escaping — not shell absence — providing the same safety property. See [SECURITY.md](SECURITY.md) for the full platform-specific detail.
|
|
906
|
+
|
|
907
|
+
No untrusted value is interpolated into a command line in a way that lets it inject a second command, a redirect, or a pipeline.
|
|
908
|
+
|
|
909
|
+
`shell: true` is an explicit opt-in exception with different security properties. Whatever you put in `run` is handed to the platform shell verbatim.
|
|
910
|
+
|
|
911
|
+
Never construct a `run` string by concatenating untrusted input, such as content originating from a pull request.
|
|
912
|
+
|
|
913
|
+
See [SECURITY.md](SECURITY.md) for the complete threat model, including environment variables, command execution, captured output, and shell execution.
|
|
914
|
+
|
|
915
|
+
## Errors
|
|
916
|
+
|
|
917
|
+
See the generated [API report](docs/api-report/repo-contract.api.md) for each error class's exact shape and `code` string.
|
|
918
|
+
|
|
919
|
+
repo-contract distinguishes several failure categories and does not conflate them.
|
|
920
|
+
|
|
921
|
+
**Configuration errors** (`InvalidRepoContractConfigError`, `InvalidCheckConfigError`) indicate structurally invalid configuration. They are thrown synchronously before anything spawns.
|
|
922
|
+
|
|
923
|
+
**Execution outcomes** include missing binaries, timeouts, non-zero exits, signals, and aborted processes. These are recorded as evidence rather than thrown so that repository policies can decide what they mean.
|
|
924
|
+
|
|
925
|
+
**Parser errors** occur when requested output cannot be parsed. They are recorded as:
|
|
926
|
+
|
|
927
|
+
```ts
|
|
928
|
+
{
|
|
929
|
+
success: false,
|
|
930
|
+
error: string
|
|
931
|
+
}
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
on `result.output`, while raw stdout remains available.
|
|
935
|
+
|
|
936
|
+
**Policy failures** occur when your policy returns:
|
|
937
|
+
|
|
938
|
+
```ts
|
|
939
|
+
{
|
|
940
|
+
outcome: "fail",
|
|
941
|
+
rationale: string
|
|
942
|
+
}
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
This is not an error in repo-contract.
|
|
946
|
+
|
|
947
|
+
It is the contract working correctly.
|
|
948
|
+
|
|
949
|
+
A **policy throwing** is different. A synchronous throw or rejected promise from policy code indicates a bug in the policy itself and causes `runRepoContract()` to reject with `PolicyThrewError`, or an `AggregateError` when multiple policies throw.
|
|
950
|
+
|
|
951
|
+
Two specific mistakes get their own error instead of a plain `PolicyThrewError`, both naming the check: reading `result.output.value` (or `.success`/`.error`/`.format`) on a check that never configured `output` throws `PolicyReadUnrequestedOutputError`, telling you to add `output: { format: "json" }` (or `"yaml"`/`"text"`); reading `result.output.value` on a check whose requested parse actually failed throws `PolicyReadFailedParseValueError`, telling you to check `result.output.success` first -- see [`output`](#output).
|
|
952
|
+
|
|
953
|
+
## Status and versioning
|
|
954
|
+
|
|
955
|
+
repo-contract is pre-1.0.
|
|
956
|
+
|
|
957
|
+
Per [VERSIONING.md](VERSIONING.md), minor versions may include breaking changes to the Stable tier before 1.0.
|
|
958
|
+
|
|
959
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
960
|
+
|
|
961
|
+
## Contributing
|
|
962
|
+
|
|
963
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and how this repository's own `repo-contract.config.ts` uses the package to validate itself, and [RELEASING.md](RELEASING.md) for the release process.
|
|
964
|
+
|
|
965
|
+
## License
|
|
966
|
+
|
|
967
|
+
MIT — see [LICENSE](LICENSE).
|