repo-contract 0.3.2 → 0.4.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 +37 -0
- package/README.md +123 -968
- package/bin/package-json-patch.d.mts +9 -0
- package/bin/package-json-patch.mjs +221 -0
- package/bin/preset-catalog.d.mts +39 -0
- package/bin/preset-catalog.mjs +71 -0
- package/bin/repo-contract.mjs +222 -0
- package/bin/templates.d.mts +5 -0
- package/bin/templates.mjs +74 -0
- package/dist/.dts/evidence/build-evidence.d.ts.map +1 -1
- package/dist/.dts/helpers/exception-policy.d.ts +199 -0
- package/dist/.dts/helpers/exception-policy.d.ts.map +1 -0
- package/dist/.dts/helpers/index.d.ts +34 -0
- package/dist/.dts/helpers/index.d.ts.map +1 -0
- package/dist/.dts/helpers/load-exception-registry.d.ts +41 -0
- package/dist/.dts/helpers/load-exception-registry.d.ts.map +1 -0
- package/dist/.dts/helpers/reconcile-exceptions.d.ts +91 -0
- package/dist/.dts/helpers/reconcile-exceptions.d.ts.map +1 -0
- package/dist/.dts/helpers/write-exception-registry.d.ts +45 -0
- package/dist/.dts/helpers/write-exception-registry.d.ts.map +1 -0
- package/dist/.dts/parsing/parse-output.d.ts.map +1 -1
- package/dist/helpers.cjs +306 -0
- package/dist/helpers.cjs.map +1 -0
- package/dist/helpers.d.cts +1 -0
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.js +296 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.cjs +1 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -7
- package/dist/index.js.map +1 -1
- package/helpers/package.json +5 -0
- package/package.json +25 -3
- package/src/helpers/exception-policy.ts +421 -0
- package/src/helpers/index.ts +57 -0
- package/src/helpers/load-exception-registry.ts +136 -0
- package/src/helpers/reconcile-exceptions.ts +171 -0
- package/src/helpers/write-exception-registry.ts +136 -0
package/README.md
CHANGED
|
@@ -2,1062 +2,217 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/MaverickCER/repo-contract/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/repo-contract)
|
|
5
|
+
[](https://www.npmjs.com/package/repo-contract)
|
|
5
6
|
[](LICENSE)
|
|
6
|
-
[](scripts/coverage-thresholds.mjs)
|
|
7
|
-
[](tsconfig.json)
|
|
8
|
-
[](package.json)
|
|
9
|
-
[](https://badge.socket.dev/npm/package/repo-contract/latest)
|
|
10
7
|
|
|
11
|
-
**
|
|
8
|
+
**Define your engineering standards once. Share them across repositories. Get actionable rationale for every outcome.**
|
|
12
9
|
|
|
13
|
-
repo-contract is a
|
|
10
|
+
repo-contract is a TypeScript library that turns the engineering rules scattered across CI, scripts, configuration, and documentation into a single contract you can version, review, and share. It runs the tools you already use, evaluates their results against your standards, and gives actionable information for every outcome. You replace none of your existing tools; your CI, scripts, and development workflow stay intact.
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
[See it run](#see-it-run) · [Quick Start](#quick-start) · [Across repositories](#from-one-repo-to-a-whole-org)
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
{ outcome: "pass" | "fail" | "warn", rationale: string }
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
repo-contract aggregates every policy result into one verdict.
|
|
22
|
-
|
|
23
|
-
It does not decide what "good code" means.
|
|
24
|
-
|
|
25
|
-
**Your repository does.**
|
|
26
|
-
|
|
27
|
-
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.
|
|
28
|
-
|
|
29
|
-
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.
|
|
30
|
-
|
|
31
|
-
The result is not merely "the tests passed."
|
|
32
|
-
|
|
33
|
-
It is a repository-defined engineering contract with evidence explaining why.
|
|
34
|
-
|
|
35
|
-
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.
|
|
36
|
-
|
|
37
|
-
**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.
|
|
38
|
-
|
|
39
|
-
## Why contracts?
|
|
40
|
-
|
|
41
|
-
Modern repositories accumulate engineering standards faster than they accumulate enforcement.
|
|
42
|
-
|
|
43
|
-
A team may agree that:
|
|
44
|
-
|
|
45
|
-
- tests must pass;
|
|
46
|
-
- coverage must remain above a threshold;
|
|
47
|
-
- mutation scores must remain above a threshold;
|
|
48
|
-
- dependencies must have no known high-severity vulnerabilities;
|
|
49
|
-
- generated files must remain synchronized;
|
|
50
|
-
- public APIs must remain compatible;
|
|
51
|
-
- documentation must accompany changes;
|
|
52
|
-
- new code must satisfy architectural boundaries.
|
|
53
|
-
|
|
54
|
-
Those standards are often scattered across CI YAML, package scripts, documentation, code review conventions, and institutional knowledge.
|
|
55
|
-
|
|
56
|
-
repo-contract gives those standards a single executable boundary.
|
|
57
|
-
|
|
58
|
-
```text
|
|
59
|
-
repository standards
|
|
60
|
-
|
|
|
61
|
-
v
|
|
62
|
-
repo-contract
|
|
63
|
-
|
|
|
64
|
-
+--> execute checks
|
|
65
|
-
|
|
|
66
|
-
+--> collect evidence
|
|
67
|
-
|
|
|
68
|
-
+--> interpret evidence with repository-owned policies
|
|
69
|
-
|
|
|
70
|
-
v
|
|
71
|
-
enforceable verdict
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
The important distinction is that repo-contract does not own the standards.
|
|
75
|
-
|
|
76
|
-
Your repository does.
|
|
77
|
-
|
|
78
|
-
## Installation
|
|
79
|
-
|
|
80
|
-
```sh
|
|
81
|
-
npm install --save-dev repo-contract
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Requires Node.js `>=20.0.0`.
|
|
85
|
-
|
|
86
|
-
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.
|
|
87
|
-
|
|
88
|
-
`yaml` is an optional peer dependency, needed only if a check requests `output: { format: "yaml" }`:
|
|
89
|
-
|
|
90
|
-
```sh
|
|
91
|
-
npm install --save-dev yaml
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### Runtime support matrix
|
|
95
|
-
|
|
96
|
-
repo-contract's checks run as spawned processes and read the ambient environment — but repo-contract never imports a process-spawning implementation or reads `process.env` itself (see [Supplying `spawn`/`env`](#supplying-spawnenv) below); it is server/CLI-only by design regardless, not an isomorphic/browser package, since the capability a consumer supplies is itself always a real Node-only spawning mechanism.
|
|
97
|
-
|
|
98
|
-
| Environment | Supported |
|
|
99
|
-
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
100
|
-
| Node.js `>=20.0.0` (macOS, Linux, Windows) | Yes |
|
|
101
|
-
| Bun (latest release) | Yes — tested in CI against the real published package shape. No non-default permissions needed. |
|
|
102
|
-
| 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)). |
|
|
103
|
-
| Browser | No — this package executes local processes |
|
|
104
|
-
|
|
105
|
-
See [ADR 0003](specs/decisions/0003-cross-platform-command-execution-and-process-cleanup.md) for what "tested" covers here and why.
|
|
106
|
-
|
|
107
|
-
### Accessibility
|
|
108
|
-
|
|
109
|
-
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.
|
|
110
|
-
|
|
111
|
-
## Quick start
|
|
112
|
-
|
|
113
|
-
Define your repository's standards as checks:
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
// repo-contract.config.ts
|
|
117
|
-
import { spawn } from "node:child_process"
|
|
118
|
-
import { defineRepoContract } from "repo-contract"
|
|
119
|
-
|
|
120
|
-
export default defineRepoContract({
|
|
121
|
-
// repo-contract never spawns a process or reads process.env itself -- see
|
|
122
|
-
// "Supplying spawn/env" below for why, and what to pass on Windows.
|
|
123
|
-
spawn,
|
|
124
|
-
env: process.env,
|
|
125
|
-
checks: {
|
|
126
|
-
tests: {
|
|
127
|
-
run: "npm test",
|
|
128
|
-
|
|
129
|
-
policy: ({ result }) =>
|
|
130
|
-
result.exitCode === 0
|
|
131
|
-
? {
|
|
132
|
-
outcome: "pass",
|
|
133
|
-
rationale: "Tests exited 0.",
|
|
134
|
-
}
|
|
135
|
-
: {
|
|
136
|
-
outcome: "fail",
|
|
137
|
-
rationale: "Tests must pass.",
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
|
|
141
|
-
mutation: {
|
|
142
|
-
run: "npm run mutation",
|
|
143
|
-
output: { format: "json" },
|
|
144
|
-
|
|
145
|
-
policy: ({ result }) => {
|
|
146
|
-
if (!result.output?.success) {
|
|
147
|
-
return {
|
|
148
|
-
outcome: "fail",
|
|
149
|
-
rationale: "Mutation report was not valid JSON.",
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const score = (result.output.value as { mutationScore: number }).mutationScore
|
|
154
|
-
|
|
155
|
-
return score >= 90
|
|
156
|
-
? {
|
|
157
|
-
outcome: "pass",
|
|
158
|
-
rationale: `Mutation score was ${score}%.`,
|
|
159
|
-
}
|
|
160
|
-
: {
|
|
161
|
-
outcome: "fail",
|
|
162
|
-
rationale: `Mutation score must be at least 90% (got ${score}%).`,
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
})
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
Execute the contract:
|
|
171
|
-
|
|
172
|
-
```ts
|
|
173
|
-
import { runRepoContract } from "repo-contract"
|
|
174
|
-
import config from "./repo-contract.config.js"
|
|
175
|
-
|
|
176
|
-
const { evidence, verdict } = await runRepoContract(config)
|
|
177
|
-
|
|
178
|
-
console.log(verdict.passed)
|
|
179
|
-
console.log(verdict.checks.mutation)
|
|
180
|
-
|
|
181
|
-
process.exitCode = verdict.passed ? 0 : 1
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
`runRepoContract()` never calls `process.exit()` itself. Your integration decides what to do with the result.
|
|
185
|
-
|
|
186
|
-
There is no CLI, no config-file discovery magic, and no hidden state.
|
|
187
|
-
|
|
188
|
-
### Supplying `spawn`/`env`
|
|
189
|
-
|
|
190
|
-
`spawn` and `env` are required fields on the config — repo-contract never imports a process-spawning implementation or reads `process.env` internally (see [ADR 0011](specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md)). You supply both, as trusted capabilities repo-contract calls with a resolved command/argv/options — it does not inspect, wrap, or sanitize them.
|
|
191
|
-
|
|
192
|
-
**macOS/Linux** — plain `node:child_process` is enough:
|
|
193
|
-
|
|
194
|
-
```ts
|
|
195
|
-
import { spawn } from "node:child_process"
|
|
196
|
-
|
|
197
|
-
export default defineRepoContract({
|
|
198
|
-
spawn,
|
|
199
|
-
env: process.env,
|
|
200
|
-
checks: {/* ... */},
|
|
201
|
-
})
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
**Windows** — most npm-installed CLI tools (`eslint`, `prettier`, `tsc`, …) resolve to `.cmd` shims, and plain `node:child_process.spawn` refuses to run those at all without `shell: true` (Node's own CVE-2024-27980 mitigation). Install [`cross-spawn`](https://www.npmjs.com/package/cross-spawn) and pass it instead — it resolves `.cmd`/`.bat` shims and quotes arguments safely for `cmd.exe`, **without** turning on shell metacharacter interpretation:
|
|
205
|
-
|
|
206
|
-
```ts
|
|
207
|
-
import crossSpawn, { sync as crossSpawnSync } from "cross-spawn"
|
|
208
|
-
|
|
209
|
-
export default defineRepoContract({
|
|
210
|
-
spawn: crossSpawn,
|
|
211
|
-
env: process.env,
|
|
212
|
-
// Optional: lets a timed-out/aborted/Ctrl+C-killed check's full process
|
|
213
|
-
// tree (not just its immediate process) get cleaned up on Windows too.
|
|
214
|
-
killProcessTree: crossSpawnSync,
|
|
215
|
-
checks: {/* ... */},
|
|
216
|
-
})
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
**`cross-spawn` does not mean "shell execution."** These are two independent choices:
|
|
220
|
-
|
|
221
|
-
| `Spawner` choice | `shell` option | Result |
|
|
222
|
-
| ---------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
223
|
-
| native `spawn` | `false` (default) | argv-only, no shell interpretation |
|
|
224
|
-
| `cross-spawn` | `false` (default) | Windows `.cmd`/`.bat` resolution + safe `cmd.exe` quoting, still argv-only |
|
|
225
|
-
| either | `true` | shell metacharacters (`&&`, `\|`, …) interpreted — a per-check or global opt-in, see [`shell`](docs/api-report/repo-contract.api.md) |
|
|
226
|
-
|
|
227
|
-
Passing `cross-spawn` fixes Windows command resolution; it does not by itself enable shell metacharacter interpretation. `check.shell` (or the config-level `shell` default) is the separate, explicit opt-in for that — see `SECURITY.md` before enabling it.
|
|
228
|
-
|
|
229
|
-
`repo-contract` doesn't ship a ready-made spawner of its own, on purpose (see ADR 0011's Alternatives) — the two snippets above are the whole integration.
|
|
230
|
-
|
|
231
|
-
## The model
|
|
232
|
-
|
|
233
|
-
```text
|
|
234
|
-
check configuration
|
|
235
|
-
|
|
|
236
|
-
v
|
|
237
|
-
execute checks
|
|
238
|
-
|
|
|
239
|
-
v
|
|
240
|
-
collect evidence
|
|
241
|
-
|
|
|
242
|
-
v
|
|
243
|
-
optionally parse explicitly requested output
|
|
244
|
-
|
|
|
245
|
-
v
|
|
246
|
-
execute per-check policy
|
|
247
|
-
|
|
|
248
|
-
v
|
|
249
|
-
PolicyResult {
|
|
250
|
-
outcome: "pass" | "fail" | "warn",
|
|
251
|
-
rationale: string
|
|
252
|
-
}
|
|
253
|
-
|
|
|
254
|
-
v
|
|
255
|
-
aggregate verdict
|
|
256
|
-
|
|
|
257
|
-
v
|
|
258
|
-
{ evidence, verdict }
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
**Evidence** describes what happened: the command, its exit code, signal, timing, captured stdout/stderr, and, if requested, parsed output.
|
|
262
|
-
|
|
263
|
-
Evidence never decides whether the result was acceptable.
|
|
264
|
-
|
|
265
|
-
**Verdict** describes whether the result was acceptable according to your policies.
|
|
266
|
-
|
|
267
|
-
**Policies are repository-owned.** A policy returns:
|
|
268
|
-
|
|
269
|
-
```ts
|
|
270
|
-
{
|
|
271
|
-
outcome: "pass" | "fail" | "warn"
|
|
272
|
-
rationale: string
|
|
273
|
-
}
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
`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.
|
|
277
|
-
|
|
278
|
-
`"warn"` is non-blocking. It means the policy's requirements were satisfied but the evidence is worth surfacing for review.
|
|
279
|
-
|
|
280
|
-
## AI guardrails
|
|
281
|
-
|
|
282
|
-
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.
|
|
283
|
-
|
|
284
|
-
The problem is whether the code consistently satisfies the engineering standards of the repository.
|
|
285
|
-
|
|
286
|
-
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.
|
|
287
|
-
|
|
288
|
-
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**.
|
|
289
|
-
|
|
290
|
-
repo-contract lets your repository define those guardrails.
|
|
291
|
-
|
|
292
|
-
For example:
|
|
293
|
-
|
|
294
|
-
```text
|
|
295
|
-
AI writes code
|
|
296
|
-
|
|
|
297
|
-
v
|
|
298
|
-
repo-contract
|
|
299
|
-
|
|
|
300
|
-
+--> tests
|
|
301
|
-
+--> typecheck
|
|
302
|
-
+--> lint
|
|
303
|
-
+--> coverage
|
|
304
|
-
+--> mutation testing
|
|
305
|
-
+--> architecture
|
|
306
|
-
+--> security
|
|
307
|
-
+--> API compatibility
|
|
308
|
-
+--> repository-specific standards
|
|
309
|
-
|
|
|
310
|
-
v
|
|
311
|
-
actionable verdict
|
|
312
|
-
|
|
|
313
|
-
v
|
|
314
|
-
AI fixes what failed
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
The AI does not need to memorize every rule.
|
|
318
|
-
|
|
319
|
-
It needs to satisfy the repository's contract.
|
|
320
|
-
|
|
321
|
-
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.
|
|
322
|
-
|
|
323
|
-
The result is a feedback loop:
|
|
324
|
-
|
|
325
|
-
```text
|
|
326
|
-
generate
|
|
327
|
-
|
|
|
328
|
-
v
|
|
329
|
-
verify
|
|
330
|
-
|
|
|
331
|
-
v
|
|
332
|
-
explain failure
|
|
333
|
-
|
|
|
334
|
-
v
|
|
335
|
-
fix
|
|
336
|
-
|
|
|
337
|
-
v
|
|
338
|
-
verify again
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
The policy rationale is important here. A result such as:
|
|
342
|
-
|
|
343
|
-
```text
|
|
344
|
-
FAIL: mutation score must be at least 90% (got 82%)
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
is useful to a human.
|
|
348
|
-
|
|
349
|
-
A result such as:
|
|
350
|
-
|
|
351
|
-
```text
|
|
352
|
-
FAIL: 3 mutants survived in src/auth/session.ts;
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
is useful to both a human and an AI agent.
|
|
356
|
-
|
|
357
|
-
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.
|
|
358
|
-
|
|
359
|
-
**The repository supplies the guardrails. The AI supplies the implementation.**
|
|
360
|
-
|
|
361
|
-
## Open source contribution guardrails
|
|
362
|
-
|
|
363
|
-
Open source projects face a different version of the same problem.
|
|
364
|
-
|
|
365
|
-
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.
|
|
366
|
-
|
|
367
|
-
The result can be repeated review cycles:
|
|
368
|
-
|
|
369
|
-
```text
|
|
370
|
-
contributor submits PR
|
|
371
|
-
|
|
|
372
|
-
v
|
|
373
|
-
maintainer finds issue
|
|
374
|
-
|
|
|
375
|
-
v
|
|
376
|
-
contributor fixes issue
|
|
377
|
-
|
|
|
378
|
-
v
|
|
379
|
-
another issue is discovered
|
|
380
|
-
|
|
|
381
|
-
v
|
|
382
|
-
repeat
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
repo-contract can move those expectations into an executable contract.
|
|
386
|
-
|
|
387
|
-
A project can enforce standards for:
|
|
388
|
-
|
|
389
|
-
- tests and test coverage;
|
|
390
|
-
- mutation scores;
|
|
391
|
-
- formatting and linting;
|
|
392
|
-
- type safety;
|
|
393
|
-
- architecture;
|
|
394
|
-
- documentation;
|
|
395
|
-
- dependency security;
|
|
396
|
-
- licenses;
|
|
397
|
-
- public API compatibility;
|
|
398
|
-
- generated artifacts;
|
|
399
|
-
- package publishing;
|
|
400
|
-
- repository-specific conventions.
|
|
401
|
-
|
|
402
|
-
More importantly, policies can interpret the evidence and provide actionable guidance.
|
|
403
|
-
|
|
404
|
-
Instead of:
|
|
405
|
-
|
|
406
|
-
```text
|
|
407
|
-
CI failed.
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
a contributor can receive:
|
|
411
|
-
|
|
412
|
-
```text
|
|
413
|
-
FAIL: public API compatibility check failed.
|
|
414
|
-
|
|
415
|
-
2 breaking changes were detected:
|
|
416
|
-
|
|
417
|
-
- Removed export: ContractResult
|
|
418
|
-
- Changed parameter type: runRepoContract(config)
|
|
419
|
-
|
|
420
|
-
Restore the export or document the breaking change according to
|
|
421
|
-
the repository's versioning policy.
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
This reduces the amount of repository knowledge a contributor must acquire before making a successful contribution.
|
|
425
|
-
|
|
426
|
-
It also gives maintainers a consistent enforcement mechanism that does not depend on a particular maintainer remembering every rule during review.
|
|
427
|
-
|
|
428
|
-
The goal is not to eliminate human review.
|
|
429
|
-
|
|
430
|
-
The goal is to make human review focus on the things that require human judgment rather than repeatedly identifying mechanical violations.
|
|
431
|
-
|
|
432
|
-
## Defining checks
|
|
433
|
-
|
|
434
|
-
Each check owns its identifier, command, output interpretation, and policy:
|
|
435
|
-
|
|
436
|
-
```ts
|
|
437
|
-
checks: {
|
|
438
|
-
lint: {
|
|
439
|
-
run: ["eslint", ".", "--format", "json"],
|
|
440
|
-
output: { format: "json" },
|
|
441
|
-
|
|
442
|
-
policy: ({ result }) => {
|
|
443
|
-
if (!result.output?.success) {
|
|
444
|
-
return {
|
|
445
|
-
outcome: "fail",
|
|
446
|
-
rationale: "ESLint output was not valid JSON.",
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const files = result.output.value as { errorCount: number }[]
|
|
451
|
-
const errors = files.reduce((sum, file) => sum + file.errorCount, 0)
|
|
452
|
-
|
|
453
|
-
return errors === 0
|
|
454
|
-
? {
|
|
455
|
-
outcome: "pass",
|
|
456
|
-
rationale: "ESLint reported 0 errors.",
|
|
457
|
-
}
|
|
458
|
-
: {
|
|
459
|
-
outcome: "fail",
|
|
460
|
-
rationale: `${errors} lint error(s).`,
|
|
461
|
-
}
|
|
462
|
-
},
|
|
463
|
-
},
|
|
464
|
-
}
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
### `run`
|
|
468
|
-
|
|
469
|
-
A `string` is tokenized into an executable and its arguments **without invoking a shell**.
|
|
470
|
-
|
|
471
|
-
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.
|
|
472
|
-
|
|
473
|
-
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.)
|
|
474
|
-
|
|
475
|
-
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.
|
|
476
|
-
|
|
477
|
-
```ts
|
|
478
|
-
run: "eslint . --max-warnings 0"
|
|
479
|
-
run: ["eslint", ".", "--max-warnings", "0"]
|
|
480
|
-
run: "npm run build && npm test" // throws: run string contains an unquoted "&" -- use array form or "shell: true"
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
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.
|
|
484
|
-
|
|
485
|
-
See [Security model](#security-model) before enabling this.
|
|
486
|
-
|
|
487
|
-
### `output`
|
|
488
|
-
|
|
489
|
-
By default, check output is not parsed. `result.stdout` and `result.stderr` contain the captured raw text.
|
|
14
|
+
## See it run
|
|
490
15
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
```ts
|
|
494
|
-
output: {
|
|
495
|
-
format: "json"
|
|
496
|
-
}
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
Uses `JSON.parse`. Malformed output produces:
|
|
500
|
-
|
|
501
|
-
```ts
|
|
502
|
-
{
|
|
503
|
-
success: false,
|
|
504
|
-
error: string
|
|
505
|
-
}
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
and never throws.
|
|
509
|
-
|
|
510
|
-
```ts
|
|
511
|
-
output: {
|
|
512
|
-
format: "yaml"
|
|
513
|
-
}
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
Requires the optional `yaml` peer dependency.
|
|
517
|
-
|
|
518
|
-
```ts
|
|
519
|
-
output: {
|
|
520
|
-
format: "text"
|
|
521
|
-
}
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
Provides trimmed text passthrough and always succeeds.
|
|
525
|
-
|
|
526
|
-
`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.
|
|
527
|
-
|
|
528
|
-
`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.
|
|
529
|
-
|
|
530
|
-
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.
|
|
531
|
-
|
|
532
|
-
#### Validating parsed output with a schema
|
|
533
|
-
|
|
534
|
-
`output.schema` accepts any object implementing [Standard Schema](https://standardschema.dev) --
|
|
535
|
-
Zod, Valibot, ArkType, and others already do. repo-contract does not install or depend on any of
|
|
536
|
-
them; it hand-vendors the (pure-type, zero-runtime-code) `StandardSchemaV1` interface itself (see
|
|
537
|
-
[ADR 0012](specs/decisions/0012-hand-vendored-standard-schema-support-for-optional-output-validation.md)). Bring whichever schema
|
|
538
|
-
library your own repo already uses -- `npm install zod` (or `valibot`, or `arktype`) is your call,
|
|
539
|
-
not repo-contract's. The smallest possible example, using a hand-written object satisfying the
|
|
540
|
-
interface directly rather than any particular library:
|
|
541
|
-
|
|
542
|
-
```ts
|
|
543
|
-
output: {
|
|
544
|
-
format: "json",
|
|
545
|
-
schema: {
|
|
546
|
-
"~standard": {
|
|
547
|
-
version: 1,
|
|
548
|
-
vendor: "example",
|
|
549
|
-
validate: (value) => {
|
|
550
|
-
const errorCount =
|
|
551
|
-
value !== null && typeof value === "object"
|
|
552
|
-
? (value as { errorCount?: unknown }).errorCount
|
|
553
|
-
: undefined
|
|
554
|
-
return typeof errorCount === "number"
|
|
555
|
-
? { value: { errorCount } }
|
|
556
|
-
: { issues: [{ message: "errorCount must be a number", path: ["errorCount"] }] }
|
|
557
|
-
},
|
|
558
|
-
},
|
|
559
|
-
},
|
|
560
|
-
}
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
A successful validation _replaces_ `result.output.value` with the schema's own (possibly
|
|
564
|
-
transformed or coerced) output -- one of the most useful aspects of Standard Schema, not just a
|
|
565
|
-
boolean check: a schema can normalize, default, or reshape its input, and the policy receives that
|
|
566
|
-
result, not the raw parsed value. A failing validation becomes a normal `ParsedOutputFailure` --
|
|
567
|
-
indistinguishable in shape from a malformed-JSON parse failure, joining every issue's own path and
|
|
568
|
-
message into `result.output.error` (e.g. `Schema validation failed: errorCount: errorCount must be
|
|
569
|
-
a number`). `result.output.value`'s declared TypeScript type stays `unknown` regardless -- narrow
|
|
570
|
-
or cast it yourself, same as without a schema.
|
|
571
|
-
|
|
572
|
-
`schema["~standard"].validate()` itself throwing or rejecting (rather than returning a `Result`) is
|
|
573
|
-
different from a failed validation -- it means the schema is broken, not the check's output, so it
|
|
574
|
-
rejects `runRepoContract()` with `StandardSchemaValidateThrewError` instead of becoming evidence.
|
|
575
|
-
|
|
576
|
-
### `dependsOn`
|
|
577
|
-
|
|
578
|
-
By default, every check runs independently and in parallel.
|
|
579
|
-
|
|
580
|
-
Name other check IDs to establish explicit execution ordering:
|
|
16
|
+
**Actionable rationale for every outcome.** Here's the real output of the tiny contract in [`examples/demo/`](examples/demo/README.md) — three published presets against one deliberately imperfect file:
|
|
581
17
|
|
|
582
18
|
```ts
|
|
583
19
|
checks: {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
policy: ({ result }) =>
|
|
588
|
-
result.exitCode === 0
|
|
589
|
-
? {
|
|
590
|
-
outcome: "pass",
|
|
591
|
-
rationale: "Build succeeded.",
|
|
592
|
-
}
|
|
593
|
-
: {
|
|
594
|
-
outcome: "fail",
|
|
595
|
-
rationale: "Build failed.",
|
|
596
|
-
},
|
|
597
|
-
},
|
|
598
|
-
|
|
599
|
-
integration: {
|
|
600
|
-
run: "npm run test:integration",
|
|
601
|
-
dependsOn: ["build"],
|
|
602
|
-
|
|
603
|
-
policy: ({ result }) =>
|
|
604
|
-
result.exitCode === 0
|
|
605
|
-
? {
|
|
606
|
-
outcome: "pass",
|
|
607
|
-
rationale: "Integration tests passed.",
|
|
608
|
-
}
|
|
609
|
-
: {
|
|
610
|
-
outcome: "fail",
|
|
611
|
-
rationale: "Integration tests failed.",
|
|
612
|
-
},
|
|
613
|
-
},
|
|
614
|
-
}
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
Independent checks run concurrently. `dependsOn` allows checks to express ordering only when ordering is actually required.
|
|
618
|
-
|
|
619
|
-
`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.
|
|
620
|
-
|
|
621
|
-
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.
|
|
622
|
-
|
|
623
|
-
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.
|
|
624
|
-
|
|
625
|
-
### `policy`
|
|
626
|
-
|
|
627
|
-
```ts
|
|
628
|
-
interface PolicyResult {
|
|
629
|
-
outcome: "pass" | "fail" | "warn"
|
|
630
|
-
rationale: string
|
|
20
|
+
typecheck,
|
|
21
|
+
format: { ...format, run: ["prettier", "--check", "."] },
|
|
22
|
+
lint: lint(),
|
|
631
23
|
}
|
|
632
|
-
|
|
633
|
-
policy: (ctx) => PolicyResult | Promise<PolicyResult>
|
|
634
24
|
```
|
|
635
25
|
|
|
636
|
-
`outcome` is the policy's own judgment:
|
|
637
|
-
|
|
638
|
-
- `"pass"` — the evidence satisfies the repository's requirements.
|
|
639
|
-
- `"fail"` — it does not.
|
|
640
|
-
- `"warn"` — the requirements are satisfied, but something worth reviewing should be surfaced.
|
|
641
|
-
|
|
642
|
-
`"warn"` never fails `verdict.passed`.
|
|
643
|
-
|
|
644
|
-
`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.
|
|
645
|
-
|
|
646
|
-
Prefer:
|
|
647
|
-
|
|
648
26
|
```text
|
|
649
|
-
|
|
650
|
-
- src/foo.ts:12:4 [no-explicit-any]: Unexpected any.
|
|
651
|
-
- src/bar.ts:8:7 [no-unused-vars]: 'value' is defined but never used.
|
|
652
|
-
```
|
|
27
|
+
$ npm run demo
|
|
653
28
|
|
|
654
|
-
|
|
29
|
+
[PASS] typecheck
|
|
30
|
+
tsc reported no type errors.
|
|
655
31
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
`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:
|
|
663
|
-
|
|
664
|
-
```ts
|
|
665
|
-
policy: ({ result, evidence }) => {
|
|
666
|
-
const testsPassed = evidence.checks.tests?.exitCode === 0
|
|
32
|
+
[FAIL] format
|
|
33
|
+
Prettier reported formatting failures:
|
|
34
|
+
Checking formatting...
|
|
35
|
+
[warn] src/greet.ts
|
|
36
|
+
[warn] Code style issues found in the above file. Run Prettier with --write to fix.
|
|
667
37
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
rationale: "Mutation policy requires the test suite to pass.",
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
const score = Number(result.stdout)
|
|
676
|
-
|
|
677
|
-
return score >= 90
|
|
678
|
-
? {
|
|
679
|
-
outcome: "pass",
|
|
680
|
-
rationale: `Mutation score was ${score}%.`,
|
|
681
|
-
}
|
|
682
|
-
: {
|
|
683
|
-
outcome: "fail",
|
|
684
|
-
rationale: `Mutation score must be at least 90% (got ${score}%).`,
|
|
685
|
-
}
|
|
686
|
-
}
|
|
38
|
+
[WARN] lint
|
|
39
|
+
ESLint reported 0 errors but 1 warning(s):
|
|
40
|
+
- src/greet.ts:15:3 [no-console]: Unexpected console statement.
|
|
687
41
|
```
|
|
688
42
|
|
|
689
|
-
|
|
43
|
+
Real output, not a mockup — run it yourself with `npm run demo` from [`examples/demo/`](examples/demo/README.md). Not "CI failed": each line says what happened, where, why the repository treats it that way, and — when there's something to fix — how. `warn` doesn't block the run; `fail` does.
|
|
690
44
|
|
|
691
|
-
|
|
692
|
-
policy: (ctx) => {
|
|
693
|
-
const buildOutput = ctx.dependencies.build?.stdout
|
|
45
|
+
## Why it exists
|
|
694
46
|
|
|
695
|
-
|
|
696
|
-
? {
|
|
697
|
-
outcome: "pass",
|
|
698
|
-
rationale: "Build output confirmed compilation succeeded.",
|
|
699
|
-
}
|
|
700
|
-
: {
|
|
701
|
-
outcome: "fail",
|
|
702
|
-
rationale: "Build output did not confirm successful compilation.",
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
```
|
|
47
|
+
Green CI does not mean your standard held.
|
|
706
48
|
|
|
707
|
-
|
|
49
|
+
- Coverage stays above 85%, but the file you just added has almost none — and whether that's acceptable isn't written down anywhere executable.
|
|
50
|
+
- The mutation score moved because the test suite moved — and nothing reconciles the two.
|
|
51
|
+
- A contributor gets "CI failed" and no idea what the repository expects them to do.
|
|
708
52
|
|
|
709
|
-
|
|
53
|
+
Individual tools answer individual questions. Your engineering standard is the answer to all of them together — and today it lives scattered across CI YAML, `package.json` scripts, configuration, docs, and review habits. repo-contract makes that standard one executable thing.
|
|
710
54
|
|
|
711
|
-
repo-contract
|
|
55
|
+
Tools run checks. Quality gates aggregate their exit codes. repo-contract sits a layer under both: it turns each tool's output into structured evidence, then lets your own policies decide what that evidence means — including reading one check's evidence to judge another's.
|
|
712
56
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
Other execution options include:
|
|
716
|
-
|
|
717
|
-
- `cwd`
|
|
718
|
-
- `env`
|
|
719
|
-
- `inheritEnv`
|
|
720
|
-
- `timeoutMs`
|
|
721
|
-
|
|
722
|
-
`inheritEnv` defaults to `true`. Set it to `false` when a check requires a minimal environment.
|
|
57
|
+
## Quick start
|
|
723
58
|
|
|
724
|
-
|
|
59
|
+
**Your standards, defined once as typed code.**
|
|
725
60
|
|
|
726
|
-
|
|
61
|
+
```sh
|
|
62
|
+
npm install --save-dev repo-contract tsx typescript vitest eslint
|
|
63
|
+
npx repo-contract init
|
|
64
|
+
npm run contract
|
|
65
|
+
```
|
|
727
66
|
|
|
728
|
-
`
|
|
67
|
+
`init` reads your `package.json`'s existing devDependencies (`typescript`, `vitest`, and `eslint`
|
|
68
|
+
above — install whichever of the [presets](GUIDE.md#presets) apply to you, `init` only wires up
|
|
69
|
+
what it finds), writes the same two files shown below
|
|
70
|
+
from them, and adds (or, if one's already there, leaves untouched) that same `"contract"` entry in
|
|
71
|
+
`package.json`'s own `scripts` — nothing it generates is required to run a contract; it's the same
|
|
72
|
+
scaffold you'd otherwise type by hand, once, so it stops being the first thing between you and
|
|
73
|
+
seeing this work. Point your pre-commit hook and your CI job at that same `npm run contract`.
|
|
74
|
+
`init` is Experimental (see [VERSIONING.md](VERSIONING.md)) — everything it writes is yours from
|
|
75
|
+
the moment it lands, so that classification is about the generator, never about what it produces.
|
|
729
76
|
|
|
730
|
-
|
|
77
|
+
### Already know how you want it configured? Create it by hand
|
|
731
78
|
|
|
732
|
-
|
|
79
|
+
A hand-authored contract is the same two files and the same `package.json` entry `init` writes,
|
|
80
|
+
with nothing hidden.
|
|
733
81
|
|
|
734
|
-
|
|
82
|
+
`tsx` runs the TypeScript config and runner. Each check invokes its own tool, so install those too — `typescript`, `vitest`, and `eslint` for the three below. repo-contract bundles none of them.
|
|
735
83
|
|
|
736
84
|
```ts
|
|
85
|
+
// repo-contract.config.mts — your standard, as typed code
|
|
737
86
|
import { spawn } from "node:child_process"
|
|
738
87
|
import { defineRepoContract } from "repo-contract"
|
|
739
|
-
import {
|
|
88
|
+
import { lint, test, typecheck } from "repo-contract/presets"
|
|
740
89
|
|
|
741
90
|
export default defineRepoContract({
|
|
742
91
|
spawn,
|
|
743
92
|
env: process.env,
|
|
744
|
-
checks: {
|
|
745
|
-
format,
|
|
746
|
-
typecheck: {
|
|
747
|
-
...typecheck,
|
|
748
|
-
timeoutMs: 60_000,
|
|
749
|
-
},
|
|
750
|
-
license: {
|
|
751
|
-
...license,
|
|
752
|
-
policy: myStricterLicensePolicy,
|
|
753
|
-
},
|
|
754
|
-
},
|
|
93
|
+
checks: { typecheck, test, lint: lint() },
|
|
755
94
|
})
|
|
756
95
|
```
|
|
757
96
|
|
|
758
|
-
|
|
97
|
+
`.mts`, not `.ts`: an `.mts` file is always ESM to Node, regardless of whether your own `package.json` has `"type": "module"` set (`npm init`'s default output doesn't). A plain `.ts` config would compile to CommonJS in that case while the `.mjs` runner below — ESM by its own extension — imports it, and Node's CJS/ESM default-export interop would silently hand `runRepoContract` the wrong shape.
|
|
759
98
|
|
|
760
99
|
```ts
|
|
761
|
-
|
|
762
|
-
import { defineRepoContract } from "repo-contract"
|
|
763
|
-
import { lint, deadCode } from "repo-contract/presets"
|
|
764
|
-
|
|
765
|
-
export default defineRepoContract({
|
|
766
|
-
spawn,
|
|
767
|
-
env: process.env,
|
|
768
|
-
checks: {
|
|
769
|
-
lint: lint({ path: "src" }),
|
|
770
|
-
deadCode: deadCode({
|
|
771
|
-
exemptUnusedDevDependencies: ["some-cli-only-tool"],
|
|
772
|
-
}),
|
|
773
|
-
},
|
|
774
|
-
})
|
|
775
|
-
```
|
|
776
|
-
|
|
777
|
-
**Preset options are the preferred way to change what a preset executes.** A direct `run` override is an escape hatch.
|
|
778
|
-
|
|
779
|
-
Using options keeps your configuration decoupled from a preset's exact command representation, which can evolve independently.
|
|
780
|
-
|
|
781
|
-
Every execution-affecting preset option is threaded through the actual command line so evidence records the exact options used in `evidence.checks.<id>.args`.
|
|
782
|
-
|
|
783
|
-
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.
|
|
784
|
-
|
|
785
|
-
Each preset assumes its CLI is already a devDependency of your repository:
|
|
786
|
-
|
|
787
|
-
```sh
|
|
788
|
-
npm install --save-dev prettier
|
|
789
|
-
```
|
|
790
|
-
|
|
791
|
-
or:
|
|
792
|
-
|
|
793
|
-
```sh
|
|
794
|
-
pnpm add -D prettier
|
|
795
|
-
yarn add -D prettier
|
|
796
|
-
bun add -d prettier
|
|
797
|
-
```
|
|
798
|
-
|
|
799
|
-
| Category | Preset | Wraps |
|
|
800
|
-
| ------------------- | ------------------------ | ------------------------------------------------------------------------------ |
|
|
801
|
-
| Testing | `test` | `vitest run --reporter=json` |
|
|
802
|
-
| Testing | `e2e` | `playwright test --reporter=json` |
|
|
803
|
-
| Code quality | `lint(options?)` | `eslint <path> --format json` |
|
|
804
|
-
| Code quality | `format` | `prettier --write .` |
|
|
805
|
-
| Code quality | `typecheck` | `tsc --noEmit -p tsconfig.json` |
|
|
806
|
-
| Code quality | `deadCode(options?)` | `knip --reporter json` |
|
|
807
|
-
| Code quality | `duplication(options?)` | `jscpd <path> --reporters json --output reports/jscpd --silent` |
|
|
808
|
-
| Code quality | `stylelint(options?)` | `stylelint <glob> --formatter json` |
|
|
809
|
-
| Docs | `markdownlint(options?)` | `markdownlint-cli2 <glob>` — requires repository configuration for JSON output |
|
|
810
|
-
| Docs | `brokenLinks(options?)` | `linkinator <start> --recurse --format json --skip node_modules` |
|
|
811
|
-
| Security/governance | `securityDeps` | `npm audit --omit=dev --json` |
|
|
812
|
-
| Security/governance | `securitySecrets` | `secretlint --format json --output reports/secretlint.json **/*` |
|
|
813
|
-
| Security/governance | `license` | `licensee --production --osi --errors-only --ndjson` |
|
|
814
|
-
| Security/governance | `commitlint(options?)` | `commitlint --from <from> --to <to>` |
|
|
815
|
-
| Publishing | `publint` | `publint run` |
|
|
816
|
-
| Publishing | `arethetypeswrong` | `attw --pack . --format json` |
|
|
817
|
-
|
|
818
|
-
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.
|
|
819
|
-
|
|
820
|
-
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.
|
|
821
|
-
|
|
822
|
-
## Evidence
|
|
823
|
-
|
|
824
|
-
See the generated [API report](docs/api-report/repo-contract.api.md) for the full field-by-field reference on `Evidence` and `CheckEvidence`.
|
|
825
|
-
|
|
826
|
-
```ts
|
|
827
|
-
interface Evidence {
|
|
828
|
-
version: 1
|
|
829
|
-
startedAt: string
|
|
830
|
-
completedAt: string
|
|
831
|
-
durationMs: number
|
|
832
|
-
|
|
833
|
-
checks: Record<
|
|
834
|
-
string,
|
|
835
|
-
{
|
|
836
|
-
command: string
|
|
837
|
-
args: readonly string[]
|
|
838
|
-
startedAt: string
|
|
839
|
-
completedAt: string
|
|
840
|
-
durationMs: number
|
|
841
|
-
exitCode: number | null
|
|
842
|
-
signal: NodeJS.Signals | null
|
|
843
|
-
stdout: string
|
|
844
|
-
stderr: string
|
|
845
|
-
status: "completed" | "timed_out" | "signaled" | "host_terminated" | "spawn_error" | "aborted"
|
|
846
|
-
spawnError?: string
|
|
847
|
-
output?:
|
|
848
|
-
| {
|
|
849
|
-
format: string
|
|
850
|
-
success: true
|
|
851
|
-
value: unknown
|
|
852
|
-
}
|
|
853
|
-
| {
|
|
854
|
-
format: string
|
|
855
|
-
success: false
|
|
856
|
-
error: string
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
>
|
|
860
|
-
}
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
`status` distinguishes **why** a process ended in its terminal state, independently of whether the policy considered that result acceptable.
|
|
864
|
-
|
|
865
|
-
A non-zero exit code is `status: "completed"`, not an execution error.
|
|
866
|
-
|
|
867
|
-
## Verdict
|
|
868
|
-
|
|
869
|
-
See the generated [API report](docs/api-report/repo-contract.api.md) for the full field-by-field reference on `Verdict`.
|
|
870
|
-
|
|
871
|
-
```ts
|
|
872
|
-
interface Verdict {
|
|
873
|
-
version: 2
|
|
874
|
-
passed: boolean
|
|
875
|
-
|
|
876
|
-
checks: Record<
|
|
877
|
-
string,
|
|
878
|
-
{
|
|
879
|
-
outcome: "pass" | "fail" | "warn"
|
|
880
|
-
rationale: string
|
|
881
|
-
}
|
|
882
|
-
>
|
|
883
|
-
}
|
|
884
|
-
```
|
|
885
|
-
|
|
886
|
-
`checks[id]` is that check's own `PolicyResult`, exactly as returned by its policy.
|
|
887
|
-
|
|
888
|
-
`passed` is `true` only when every check's outcome is `"pass"` or `"warn"`.
|
|
889
|
-
|
|
890
|
-
`"fail"` is the only outcome that fails the run.
|
|
891
|
-
|
|
892
|
-
`Verdict` is returned alongside `Evidence`, never merged into it. Consumers can inspect:
|
|
893
|
-
|
|
894
|
-
```ts
|
|
895
|
-
evidence.checks[id]
|
|
896
|
-
```
|
|
897
|
-
|
|
898
|
-
to understand what happened and:
|
|
899
|
-
|
|
900
|
-
```ts
|
|
901
|
-
verdict.checks[id]
|
|
902
|
-
```
|
|
903
|
-
|
|
904
|
-
to understand what the repository concluded about it.
|
|
905
|
-
|
|
906
|
-
See [Evidence, policy rationale, and consumer judgment](specs/architecture.md#evidence-policy-rationale-and-consumer-judgment) for why these responsibilities remain separate.
|
|
907
|
-
|
|
908
|
-
## Regression detection
|
|
909
|
-
|
|
910
|
-
repo-contract has no built-in baseline system and no persistence layer.
|
|
911
|
-
|
|
912
|
-
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.
|
|
913
|
-
|
|
914
|
-
If your repository wants regression detection, persist the evidence or relevant measurements yourself and compare them in your policy:
|
|
915
|
-
|
|
916
|
-
```ts
|
|
917
|
-
import baseline from "./baseline.json"
|
|
918
|
-
|
|
919
|
-
policy: ({ result }) => {
|
|
920
|
-
const current = (result.output?.value as { score: number }).score
|
|
921
|
-
|
|
922
|
-
return current >= baseline.mutation.score
|
|
923
|
-
? {
|
|
924
|
-
outcome: "pass",
|
|
925
|
-
rationale: `Mutation score was ${current}.`,
|
|
926
|
-
}
|
|
927
|
-
: {
|
|
928
|
-
outcome: "fail",
|
|
929
|
-
rationale: `Mutation score regressed from ${baseline.mutation.score} to ${current}.`,
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
```
|
|
933
|
-
|
|
934
|
-
This keeps persistence and baseline semantics under repository control.
|
|
935
|
-
|
|
936
|
-
## CI integration
|
|
937
|
-
|
|
938
|
-
repo-contract has no CLI and no config-discovery magic.
|
|
939
|
-
|
|
940
|
-
Call `runRepoContract()` from a small script and map the result to a process exit code yourself:
|
|
941
|
-
|
|
942
|
-
```ts
|
|
943
|
-
// scripts/run-contract.mjs
|
|
100
|
+
// scripts/contract.mjs — the entry point; this is the whole thing
|
|
944
101
|
import { runRepoContract } from "repo-contract"
|
|
945
|
-
import config from "../repo-contract.config.
|
|
102
|
+
import config from "../repo-contract.config.mjs"
|
|
946
103
|
|
|
947
104
|
const { verdict } = await runRepoContract(config)
|
|
948
|
-
|
|
949
105
|
for (const [id, result] of Object.entries(verdict.checks)) {
|
|
950
106
|
console.log(`[${result.outcome.toUpperCase()}] ${id}: ${result.rationale}`)
|
|
951
107
|
}
|
|
952
|
-
|
|
953
108
|
process.exitCode = verdict.passed ? 0 : 1
|
|
954
109
|
```
|
|
955
110
|
|
|
956
111
|
```json
|
|
957
|
-
{
|
|
958
|
-
"scripts": {
|
|
959
|
-
"contract": "tsx scripts/run-contract.mjs"
|
|
960
|
-
}
|
|
961
|
-
}
|
|
112
|
+
{ "scripts": { "contract": "tsx scripts/contract.mjs" } }
|
|
962
113
|
```
|
|
963
114
|
|
|
964
|
-
The same contract can then be used by local development and CI:
|
|
965
|
-
|
|
966
115
|
```sh
|
|
967
116
|
npm run contract
|
|
968
117
|
```
|
|
969
118
|
|
|
970
|
-
Point
|
|
971
|
-
|
|
972
|
-
This repository uses its own `repo-contract.config.ts` to validate itself. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
973
|
-
|
|
974
|
-
## Example: layered organizational governance
|
|
119
|
+
Point your pre-commit hook and your CI job at that same `npm run contract`. The [Guide](GUIDE.md#the-runner-and-ci-integration) covers the runner, the `spawn`/`env` capability model, and Windows. Node.js `>=20` (Bun and Deno are tested too).
|
|
975
120
|
|
|
976
|
-
|
|
121
|
+
Two patterns worth knowing about early, not just once you're rolling this out across an org: the
|
|
122
|
+
[**ratchet**](examples/day-one-walkthrough/README.md) (a new requirement lands as a dated `warn` →
|
|
123
|
+
`fail`, never an overnight red build) and [**governed exceptions**](examples/exceptions-walkthrough/README.md)
|
|
124
|
+
(_Experimental_ — a justified, reviewed waiver for one specific finding, without weakening the
|
|
125
|
+
check for everything else).
|
|
977
126
|
|
|
978
|
-
|
|
127
|
+
## What a policy can express
|
|
979
128
|
|
|
980
|
-
|
|
129
|
+
**Checks produce evidence. Policies decide whether that evidence meets your standard** — and a policy can read any other check's result to do it:
|
|
981
130
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
## Security model
|
|
997
|
-
|
|
998
|
-
Command execution is the core of this package and therefore a security-sensitive boundary.
|
|
999
|
-
|
|
1000
|
-
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.
|
|
1001
|
-
|
|
1002
|
-
No untrusted value is interpolated into a command line in a way that lets it inject a second command, a redirect, or a pipeline.
|
|
1003
|
-
|
|
1004
|
-
`shell: true` is an explicit opt-in exception with different security properties. Whatever you put in `run` is handed to the platform shell verbatim.
|
|
131
|
+
```ts
|
|
132
|
+
checks: {
|
|
133
|
+
tests: test,
|
|
134
|
+
|
|
135
|
+
mutation: {
|
|
136
|
+
run: ["npm", "run", "mutation"],
|
|
137
|
+
dependsOn: ["tests"], // run after tests; hand this policy their result
|
|
138
|
+
policy: ({ dependencies }) =>
|
|
139
|
+
dependencies.tests?.exitCode !== 0
|
|
140
|
+
? { outcome: "fail", rationale: "Tests must pass before a mutation score means anything." }
|
|
141
|
+
: { outcome: "pass", rationale: "Tests passed; mutation score is meaningful." },
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
```
|
|
1005
145
|
|
|
1006
|
-
|
|
146
|
+
That relationship — "don't evaluate X until Y passed" — is easy to state in the contract and awkward to maintain as independent CI steps. The [Guide](GUIDE.md#a-fuller-example-cross-check-policy) has the full version: coverage floors, per-file warnings, surviving-mutant rationales.
|
|
1007
147
|
|
|
1008
|
-
|
|
148
|
+
Presets exist for the common tools — TypeScript, ESLint, Vitest, Prettier, security auditing, dependency and dead-code analysis, package validation, and more. Each assumes its CLI is already a devDependency; repo-contract never installs anything. [Full catalog →](GUIDE.md#presets)
|
|
1009
149
|
|
|
1010
|
-
##
|
|
150
|
+
## Why not just add more CI steps?
|
|
1011
151
|
|
|
1012
|
-
|
|
152
|
+
**repo-contract distributes the whole engineering standard, not just individual checks.** One versioned package defines what your repositories consider acceptable — typed code, reviewed like any other code, and runnable on a developer's laptop as well as in CI.
|
|
1013
153
|
|
|
1014
|
-
|
|
154
|
+
You can express any individual check in CI. The problem is that the standard governing those checks ends up scattered across CI YAML, shared configs, and reusable workflows — infrastructure that distributes **how checks run**, rather than **what your organization considers acceptable**.
|
|
1015
155
|
|
|
1016
|
-
|
|
156
|
+
Shared ESLint configs, reusable workflows, and template repos distribute individual checks. repo-contract distributes the **whole standard**: the checks, the policies that relate them, and the rationale for their outcomes.
|
|
1017
157
|
|
|
1018
|
-
|
|
158
|
+
```text
|
|
159
|
+
GitHub Actions repo-contract
|
|
160
|
+
lint ✓ one policy reads another check's result
|
|
161
|
+
test ✓ the same contract runs in a pre-commit hook and in CI
|
|
162
|
+
coverage ✓ it's typed code in your repo, reviewed like any code
|
|
163
|
+
mutation ✓ every outcome carries a rationale you can act on
|
|
164
|
+
── but does that meet
|
|
165
|
+
our standard? ──
|
|
166
|
+
```
|
|
1019
167
|
|
|
1020
|
-
|
|
168
|
+
## From one repo to a whole org
|
|
1021
169
|
|
|
1022
|
-
```
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
170
|
+
```text
|
|
171
|
+
typecheck + lint + test
|
|
172
|
+
| add policy
|
|
173
|
+
coverage + mutation + security
|
|
174
|
+
| share it
|
|
175
|
+
one contract package, inherited by every repository
|
|
176
|
+
| roll out safely
|
|
177
|
+
new requirements land as `warn`, become `fail` on a date
|
|
1027
178
|
```
|
|
1028
179
|
|
|
1029
|
-
|
|
180
|
+
**One definition, shared across every repository.** An organization expresses its engineering standard for a project type **once** — an internal package that wraps repo-contract and owns the executors — and every project of that type extends it instead of redefining it. Change the shared standard, and consuming repositories receive it through their normal dependency updates, including ones created from an older boilerplate.
|
|
1030
181
|
|
|
1031
|
-
|
|
182
|
+
- [`examples/`](examples/README.md) — a minimal, runnable end-to-end wiring of that model.
|
|
183
|
+
- [`examples/day-one-walkthrough/`](examples/day-one-walkthrough/README.md) — the **ratchet** pattern: rolling out a new shared requirement as a dated `warn` → `fail`, not an overnight red build.
|
|
184
|
+
- [`examples/exceptions-walkthrough/`](examples/exceptions-walkthrough/README.md) — _Experimental:_ a governed, justified waiver for one finding, without weakening the check.
|
|
185
|
+
- [ADR 0010](specs/decisions/0010-review-driven-contracts-and-shared-internal-system-contracts.md) — the reasoning.
|
|
1032
186
|
|
|
1033
|
-
|
|
1034
|
-
{
|
|
1035
|
-
outcome: "fail",
|
|
1036
|
-
rationale: string
|
|
1037
|
-
}
|
|
1038
|
-
```
|
|
187
|
+
## You probably don't need it when
|
|
1039
188
|
|
|
1040
|
-
|
|
189
|
+
- `npm test` plus a linter is the whole story;
|
|
190
|
+
- each CI check is already independent and that's fine;
|
|
191
|
+
- you don't need any policy logic on top of exit codes.
|
|
1041
192
|
|
|
1042
|
-
|
|
193
|
+
## Works with automation
|
|
1043
194
|
|
|
1044
|
-
|
|
195
|
+
AI coding agents, CI bots, and release automation consume the same contract as human contributors. An actionable rationale gives automation a concrete thing to fix rather than a bare "a check failed" — making regenerate-until-green workflows much easier to converge.
|
|
1045
196
|
|
|
1046
|
-
|
|
197
|
+
## Status
|
|
1047
198
|
|
|
1048
|
-
|
|
199
|
+
Pre-1.0. Per [VERSIONING.md](VERSIONING.md), a `0.x` minor may carry a breaking change to the Stable tier before 1.0 — pin accordingly and read the [CHANGELOG](CHANGELOG.md) on every minor upgrade.
|
|
1049
200
|
|
|
1050
|
-
##
|
|
201
|
+
## Learn more
|
|
1051
202
|
|
|
1052
|
-
|
|
203
|
+
- **[Guide](GUIDE.md)** — how to integrate it, define checks, parse output, write cross-check policies, use presets, and handle errors.
|
|
204
|
+
- **[Security](SECURITY.md)** — how commands run, which capabilities the consumer supplies, and the enforced no-network guarantee.
|
|
205
|
+
- **[API reference](https://maverickcer.github.io/repo-contract/api/)** — every exported type, field, and error code, generated from source so it cannot drift.
|
|
206
|
+
- **[Architecture](specs/architecture.md)** and the **[ADRs](https://github.com/MaverickCER/repo-contract/tree/main/specs/decisions)** — why it is built this way.
|
|
207
|
+
- **[examples/](examples/README.md)** — runnable end-to-end wiring, plus the two deep-dive walkthroughs.
|
|
1053
208
|
|
|
1054
|
-
|
|
209
|
+
## If this is useful
|
|
1055
210
|
|
|
1056
|
-
|
|
211
|
+
If this model matches how you think about repository standards, try the demo and examples. If it doesn't fit your repo, [open an issue](https://github.com/MaverickCER/repo-contract/issues) and say why.
|
|
1057
212
|
|
|
1058
213
|
## Contributing
|
|
1059
214
|
|
|
1060
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and how this repository
|
|
215
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and how this repository uses its own `repo-contract.config.ts` to validate itself, and [RELEASING.md](RELEASING.md) for the release process.
|
|
1061
216
|
|
|
1062
217
|
## License
|
|
1063
218
|
|