repo-contract 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +46 -0
  3. package/dist/.dts/config/define-repo-contract.d.ts +36 -0
  4. package/dist/.dts/config/define-repo-contract.d.ts.map +1 -0
  5. package/dist/.dts/config/tokenize-command.d.ts +33 -0
  6. package/dist/.dts/config/tokenize-command.d.ts.map +1 -0
  7. package/dist/.dts/config/validate-config.d.ts +32 -0
  8. package/dist/.dts/config/validate-config.d.ts.map +1 -0
  9. package/dist/.dts/errors.d.ts +172 -0
  10. package/dist/.dts/errors.d.ts.map +1 -0
  11. package/dist/.dts/evidence/build-evidence.d.ts +26 -0
  12. package/dist/.dts/evidence/build-evidence.d.ts.map +1 -0
  13. package/dist/.dts/execution/abort-signals.d.ts +29 -0
  14. package/dist/.dts/execution/abort-signals.d.ts.map +1 -0
  15. package/dist/.dts/execution/concurrency-pool.d.ts +14 -0
  16. package/dist/.dts/execution/concurrency-pool.d.ts.map +1 -0
  17. package/dist/.dts/execution/dependency-scheduler.d.ts +30 -0
  18. package/dist/.dts/execution/dependency-scheduler.d.ts.map +1 -0
  19. package/dist/.dts/execution/process-tree.d.ts +59 -0
  20. package/dist/.dts/execution/process-tree.d.ts.map +1 -0
  21. package/dist/.dts/execution/run-checks.d.ts +31 -0
  22. package/dist/.dts/execution/run-checks.d.ts.map +1 -0
  23. package/dist/.dts/execution/spawn-check.d.ts +49 -0
  24. package/dist/.dts/execution/spawn-check.d.ts.map +1 -0
  25. package/dist/.dts/index.d.ts +14 -0
  26. package/dist/.dts/index.d.ts.map +1 -0
  27. package/dist/.dts/parsing/format-schema-issues.d.ts +11 -0
  28. package/dist/.dts/parsing/format-schema-issues.d.ts.map +1 -0
  29. package/dist/.dts/parsing/parse-json.d.ts +8 -0
  30. package/dist/.dts/parsing/parse-json.d.ts.map +1 -0
  31. package/dist/.dts/parsing/parse-output.d.ts +25 -0
  32. package/dist/.dts/parsing/parse-output.d.ts.map +1 -0
  33. package/dist/.dts/parsing/parse-text.d.ts +8 -0
  34. package/dist/.dts/parsing/parse-text.d.ts.map +1 -0
  35. package/dist/.dts/parsing/parse-yaml.d.ts +21 -0
  36. package/dist/.dts/parsing/parse-yaml.d.ts.map +1 -0
  37. package/dist/.dts/policy/run-policies.d.ts +38 -0
  38. package/dist/.dts/policy/run-policies.d.ts.map +1 -0
  39. package/dist/.dts/presets/arethetypeswrong.d.ts +39 -0
  40. package/dist/.dts/presets/arethetypeswrong.d.ts.map +1 -0
  41. package/dist/.dts/presets/broken-links.d.ts +16 -0
  42. package/dist/.dts/presets/broken-links.d.ts.map +1 -0
  43. package/dist/.dts/presets/commitlint.d.ts +22 -0
  44. package/dist/.dts/presets/commitlint.d.ts.map +1 -0
  45. package/dist/.dts/presets/dead-code.d.ts +23 -0
  46. package/dist/.dts/presets/dead-code.d.ts.map +1 -0
  47. package/dist/.dts/presets/duplication.d.ts +14 -0
  48. package/dist/.dts/presets/duplication.d.ts.map +1 -0
  49. package/dist/.dts/presets/e2e.d.ts +4 -0
  50. package/dist/.dts/presets/e2e.d.ts.map +1 -0
  51. package/dist/.dts/presets/format.d.ts +4 -0
  52. package/dist/.dts/presets/format.d.ts.map +1 -0
  53. package/dist/.dts/presets/index.d.ts +31 -0
  54. package/dist/.dts/presets/index.d.ts.map +1 -0
  55. package/dist/.dts/presets/license.d.ts +4 -0
  56. package/dist/.dts/presets/license.d.ts.map +1 -0
  57. package/dist/.dts/presets/lint.d.ts +20 -0
  58. package/dist/.dts/presets/lint.d.ts.map +1 -0
  59. package/dist/.dts/presets/markdownlint.d.ts +23 -0
  60. package/dist/.dts/presets/markdownlint.d.ts.map +1 -0
  61. package/dist/.dts/presets/publint.d.ts +13 -0
  62. package/dist/.dts/presets/publint.d.ts.map +1 -0
  63. package/dist/.dts/presets/security-deps.d.ts +4 -0
  64. package/dist/.dts/presets/security-deps.d.ts.map +1 -0
  65. package/dist/.dts/presets/security-secrets.d.ts +4 -0
  66. package/dist/.dts/presets/security-secrets.d.ts.map +1 -0
  67. package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts +13 -0
  68. package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts.map +1 -0
  69. package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts +23 -0
  70. package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts.map +1 -0
  71. package/dist/.dts/presets/shared/missing-dependency.d.ts +19 -0
  72. package/dist/.dts/presets/shared/missing-dependency.d.ts.map +1 -0
  73. package/dist/.dts/presets/shared/read-json-report.d.ts +33 -0
  74. package/dist/.dts/presets/shared/read-json-report.d.ts.map +1 -0
  75. package/dist/.dts/presets/shared/terminal-status.d.ts +25 -0
  76. package/dist/.dts/presets/shared/terminal-status.d.ts.map +1 -0
  77. package/dist/.dts/presets/shared/vitest-json-policy.d.ts +20 -0
  78. package/dist/.dts/presets/shared/vitest-json-policy.d.ts.map +1 -0
  79. package/dist/.dts/presets/stylelint.d.ts +17 -0
  80. package/dist/.dts/presets/stylelint.d.ts.map +1 -0
  81. package/dist/.dts/presets/test.d.ts +4 -0
  82. package/dist/.dts/presets/test.d.ts.map +1 -0
  83. package/dist/.dts/presets/typecheck.d.ts +4 -0
  84. package/dist/.dts/presets/typecheck.d.ts.map +1 -0
  85. package/dist/.dts/run-repo-contract.d.ts +38 -0
  86. package/dist/.dts/run-repo-contract.d.ts.map +1 -0
  87. package/dist/.dts/standard-schema/types.d.ts +76 -0
  88. package/dist/.dts/standard-schema/types.d.ts.map +1 -0
  89. package/dist/.dts/types.d.ts +424 -0
  90. package/dist/.dts/types.d.ts.map +1 -0
  91. package/dist/index.cjs +99 -4
  92. package/dist/index.cjs.map +1 -1
  93. package/dist/index.d.cts +1 -0
  94. package/dist/index.d.ts +1 -0
  95. package/dist/index.js +99 -5
  96. package/dist/index.js.map +1 -1
  97. package/dist/presets.d.cts +1 -0
  98. package/dist/presets.d.ts +1 -0
  99. package/package.json +1 -1
  100. package/schemas/evidence.schema.json +2 -2
  101. package/src/config/validate-config.ts +55 -1
  102. package/src/errors.ts +26 -0
  103. package/src/evidence/build-evidence.ts +6 -1
  104. package/src/index.ts +3 -0
  105. package/src/parsing/format-schema-issues.ts +56 -0
  106. package/src/parsing/parse-output.ts +50 -3
  107. package/src/standard-schema/types.ts +93 -0
  108. package/src/types.ts +48 -12
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Public type surface for repo-contract. Evidence describes what happened;
3
+ * Verdict describes whether it was acceptable. They are deliberately
4
+ * separate concepts (see specs/architecture.md) even though
5
+ * `runRepoContract` returns both together.
6
+ */
7
+ import type { ChildProcess, SpawnOptions, SpawnSyncOptions, SpawnSyncReturns } from "node:child_process";
8
+ import type { StandardSchemaV1 } from "./standard-schema/types.js";
9
+ /** Output interpretation a check can explicitly request. No format requested means no parsing -- the consumer gets raw stdout/stderr only. */
10
+ export type OutputFormat = "json" | "yaml" | "text";
11
+ /**
12
+ * Why a check's process ended up in its terminal state. `"completed"` means
13
+ * the process ran to exit on its own -- the exit code may still be
14
+ * non-zero, and that is for the check's policy to interpret, never this
15
+ * package. The other five values all mean the process did not exit on its
16
+ * own; repo-contract terminated it, or it was terminated for a reason
17
+ * repo-contract can observe but did not cause.
18
+ *
19
+ * `"signaled"` specifically means a signal repo-contract did *not* itself request -- an
20
+ * externally-caused termination. A check killed because the *host* process running repo-contract
21
+ * received its own SIGINT/SIGTERM (see `run-checks.ts`'s termination-handler cleanup) is instead
22
+ * `"host_terminated"`: repo-contract did request that signal, just not via `options.signal` or
23
+ * `timeoutMs` (see `"aborted"`/`"timed_out"`), so it must not be conflated with an externally-caused
24
+ * `"signaled"`.
25
+ */
26
+ export type CheckStatus = "completed" | "timed_out" | "signaled" | "host_terminated" | "spawn_error" | "aborted";
27
+ /** A requested parse of a check's stdout succeeded. */
28
+ export interface ParsedOutputSuccess<T> {
29
+ /** The format that was requested and successfully parsed. */
30
+ readonly format: OutputFormat;
31
+ /** Always `true`. */
32
+ readonly success: true;
33
+ /** The parsed value. */
34
+ readonly value: T;
35
+ }
36
+ /**
37
+ * A requested parse of a check's stdout failed. The raw stdout on the
38
+ * parent `CheckEvidence` is preserved unchanged -- a parse failure is never
39
+ * silently reinterpreted or discarded.
40
+ */
41
+ export interface ParsedOutputFailure {
42
+ /** The format that was requested (and failed to parse). */
43
+ readonly format: OutputFormat;
44
+ /** Always `false`. */
45
+ readonly success: false;
46
+ /** The parse error's message. */
47
+ readonly error: string;
48
+ }
49
+ /** The result of a check's requested output-format parse: either a successful `ParsedOutputSuccess`, or a `ParsedOutputFailure`. */
50
+ export type ParsedOutput<T> = ParsedOutputSuccess<T> | ParsedOutputFailure;
51
+ /**
52
+ * What actually happened when one configured check ran. `output` is present
53
+ * only if that check's config requested a format, and is otherwise
54
+ * `undefined` -- a policy narrows with `ctx.result.output?.success` (or an
55
+ * `if (ctx.result.output) { ... }` guard) before reading `.value`/`.error`.
56
+ *
57
+ * `output.value` is the value produced by the configured `format` parser, optionally validated
58
+ * and (if a schema transforms/coerces it) replaced by `output.schema` (see
59
+ * `CheckDefinitionConfig.output.schema`) -- so it is not necessarily the raw parser result once a
60
+ * check's config supplies a schema. It is typed `unknown` for every format regardless, including
61
+ * `"text"` (even though `parseText` always produces a `string` at runtime) and regardless of
62
+ * whether a schema was supplied -- neither repo-contract nor TypeScript's generic inference can
63
+ * reliably carry a specific check's own literal `output.format` (or a specific `schema`'s own
64
+ * inferred output type) through to that same check's `policy` parameter once several checks with
65
+ * heterogeneous formats live together in one `checks` record (a real TypeScript inference
66
+ * limitation hit and confirmed during implementation, not a hypothetical -- see specs/decisions/
67
+ * for the isolated repro). This is a deliberate, acknowledged tradeoff, not a gap this package
68
+ * is attempting to close: a schema still gives its own author real compile-time input/output
69
+ * typing via `StandardSchemaV1<Input, Output>` for their own code, just not threaded through this
70
+ * shared `CheckEvidence` shape. A policy author narrows or casts `.value` themselves, exactly as
71
+ * they already must for `"json"`/`"yaml"` with no schema supplied.
72
+ */
73
+ export interface CheckEvidence {
74
+ /** The executable that was actually spawned (after tokenization, if `run` was a string). */
75
+ readonly command: string;
76
+ /** The arguments passed to `command`, exactly as spawned. */
77
+ readonly args: readonly string[];
78
+ /** ISO 8601 timestamp of when the process was spawned. */
79
+ readonly startedAt: string;
80
+ /** ISO 8601 timestamp of when the process reached its terminal state. */
81
+ readonly completedAt: string;
82
+ /** Wall-clock time from spawn to termination, in milliseconds. */
83
+ readonly durationMs: number;
84
+ /** `null` when the process never exited normally -- see `signal` and `status`. */
85
+ readonly exitCode: number | null;
86
+ /** The signal that terminated the process, if any. `null` for a normal exit or a spawn failure. */
87
+ readonly signal: NodeJS.Signals | null;
88
+ /** The process's raw standard output, captured verbatim up to an internal size cap (10 MiB); content beyond the cap is replaced with a truncation marker. */
89
+ readonly stdout: string;
90
+ /** The process's raw standard error, captured verbatim up to an internal size cap (10 MiB); content beyond the cap is replaced with a truncation marker. */
91
+ readonly stderr: string;
92
+ /** Why the process reached its terminal state; see `CheckStatus`. */
93
+ readonly status: CheckStatus;
94
+ /** Populated only for `status === "spawn_error"` -- the underlying Node error message (e.g. "spawn foo ENOENT"). Never populated for any other status. */
95
+ readonly spawnError?: string;
96
+ /** Populated only for `status === "spawn_error"` -- the underlying Node `ErrnoException`'s structured `.code` (e.g. `"ENOENT"`, `"EACCES"`), when Node provides one. Never populated for any other status. Distinguishes "the executable does not exist" from other spawn failures (permission denied, invalid executable format, etc.) without parsing `spawnError`'s free-text message. */
97
+ readonly spawnErrorCode?: string;
98
+ /**
99
+ * The parsed interpretation of `stdout`, present only if this check's config requested a
100
+ * `format`. If that config also supplied `output.schema`, a successful parse is additionally
101
+ * validated (and possibly transformed) by that schema before landing here -- see this
102
+ * interface's own doc comment above and `CheckDefinitionConfig.output.schema`.
103
+ */
104
+ readonly output?: ParsedOutput<unknown>;
105
+ }
106
+ /**
107
+ * Versioned, immutable record of one complete `runRepoContract` execution --
108
+ * every configured check's evidence, plus timing for the run as a whole.
109
+ * Says nothing about whether any of it was acceptable; see `Verdict`.
110
+ * Additive fields are a compatible change; changing or removing an existing
111
+ * field requires bumping this version number (see VERSIONING.md).
112
+ */
113
+ export interface Evidence<TChecks extends CheckSchema = CheckSchema> {
114
+ /** Schema version of this shape; see VERSIONING.md. */
115
+ readonly version: 1;
116
+ /** ISO 8601 timestamp of when the run began. */
117
+ readonly startedAt: string;
118
+ /** ISO 8601 timestamp of when the last check finished. */
119
+ readonly completedAt: string;
120
+ /** Wall-clock time for the run as a whole, in milliseconds. */
121
+ readonly durationMs: number;
122
+ /** Each configured check's own evidence, keyed by check id. */
123
+ readonly checks: {
124
+ readonly [K in keyof TChecks]: CheckEvidence;
125
+ };
126
+ }
127
+ /**
128
+ * What a check's `policy` function is called with. `result` is that check's
129
+ * own evidence; `evidence` is the complete run's evidence, including every
130
+ * sibling check -- present so a policy can make cross-check decisions (e.g.
131
+ * "only enforce the mutation-score threshold if the tests check passed").
132
+ * By the time any policy runs, every check has already finished executing
133
+ * and every check's evidence has already been assembled -- no policy ever
134
+ * observes a partially-populated `evidence` (see specs/architecture.md).
135
+ */
136
+ export interface PolicyContext<TChecks extends CheckSchema = CheckSchema> {
137
+ /** This check's own evidence. */
138
+ readonly result: CheckEvidence;
139
+ /** The complete run's evidence, including every sibling check. */
140
+ readonly evidence: Evidence<TChecks>;
141
+ /**
142
+ * This check's own declared `dependsOn` dependencies' evidence, keyed by
143
+ * check id -- a convenience view, fully derivable from `evidence.checks`
144
+ * plus this check's own `dependsOn`, provided so no policy has to do that
145
+ * lookup itself. `{}` for a check with no `dependsOn` -- always an
146
+ * object, never `undefined`, matching how `evidence.checks` is already
147
+ * used today (a policy narrows a specific key's presence, never the
148
+ * field itself). Evidence only, not policy outcomes -- a dependency's
149
+ * policy result stays visible only via the top-level `Verdict`, exactly
150
+ * as today; this keeps policy evaluation itself fully parallel and
151
+ * unaffected by `dependsOn`.
152
+ */
153
+ readonly dependencies: Readonly<Record<string, CheckEvidence>>;
154
+ }
155
+ /**
156
+ * `"pass"`: the repository-owned policy evaluated the captured evidence as
157
+ * satisfying its configured requirements. `"fail"`: the evidence did not
158
+ * satisfy them. `"warn"`: the evidence does not violate the policy's
159
+ * blocking requirements, but the policy has intentionally decided the
160
+ * condition is materially relevant and wants it surfaced -- not a synonym
161
+ * for "minor failure"; a `warn` never fails `Verdict.passed` (see
162
+ * `runPolicies` in `src/policy/run-policies.ts`).
163
+ */
164
+ export type PolicyOutcome = "pass" | "fail" | "warn";
165
+ /**
166
+ * A repository-owned policy's interpretation of one check's captured
167
+ * evidence -- fully JSON-serializable (a plain object of primitives only,
168
+ * never an `Error`, class instance, function, or tool-specific object) so it
169
+ * can be persisted, transmitted, aggregated across parallel checks, and
170
+ * consumed directly by a human or an AI without rerunning anything.
171
+ *
172
+ * `rationale` is mandatory and must contain enough actionable detail --
173
+ * specific file/line locations, rule ids, test names, counts -- for a
174
+ * consumer to understand *why* the policy reached its outcome from this
175
+ * value alone. A rationale like "see output above" or "check the report for
176
+ * details" defeats the purpose: it forces the consumer back to raw,
177
+ * unstructured command output, exactly what this type exists to avoid. See
178
+ * specs/architecture.md for the evidence/rationale/judgment distinction this
179
+ * type is built around: evidence answers "what happened?", `rationale`
180
+ * answers "what does the repository's policy conclude about what
181
+ * happened?", and a policy's `outcome` is not the final word -- a human or
182
+ * AI consumer still makes the final judgment call using both.
183
+ */
184
+ export interface PolicyResult {
185
+ /** The policy's pass/fail/warn decision. */
186
+ readonly outcome: PolicyOutcome;
187
+ /** Why the policy reached `outcome`, in enough detail to act on without rerunning anything. */
188
+ readonly rationale: string;
189
+ }
190
+ /**
191
+ * A repository-owned decision about whether one check's evidence is
192
+ * acceptable. Returns a `PolicyResult` -- never a bare boolean or string --
193
+ * so a policy can communicate more than pass/fail even when the run is
194
+ * otherwise acceptable (see `PolicyResult`/`PolicyOutcome`). May be
195
+ * synchronous or return a `Promise`. repo-contract does not interpret
196
+ * `rationale` beyond storing and surfacing it verbatim; the package has no
197
+ * opinion about what makes a check pass, fail, or warrant a `warn`.
198
+ */
199
+ export type Policy<TChecks extends CheckSchema = CheckSchema> = (ctx: PolicyContext<TChecks>) => PolicyResult | Promise<PolicyResult>;
200
+ /** One partially configured check: how to run it, how (if at all) to interpret its output, and the policy that decides whether its evidence is acceptable. */
201
+ export interface CheckDefinitionConfig {
202
+ /**
203
+ * The command to run. A `string` is tokenized into executable + arguments
204
+ * without invoking a shell -- shell operators (`;`, `&`, `|`, backticks,
205
+ * `$(`, `<`, `>`, newlines) are rejected with a configuration error rather
206
+ * than silently passed through as literal arguments, since a string
207
+ * containing one almost always reflects a mistaken assumption that shell
208
+ * interpretation is happening. Glob characters (`*`, `?`, `~`, `[`, `]`,
209
+ * `{`, `}`) are NOT rejected -- they are common, legitimate literal argv
210
+ * content (e.g. `eslint "src/**\/*.ts"`) that many CLI tools glob-expand
211
+ * internally, and carry no shell-injection risk when no shell is invoked.
212
+ * An array bypasses tokenization entirely and is used as argv verbatim.
213
+ */
214
+ readonly run: string | readonly string[];
215
+ /**
216
+ * Opt into real shell execution instead of the safe argv-only default.
217
+ * When `true` (or left unset with `RepoContractConfig.shell: true` as the
218
+ * run-wide default -- see that field), `run` must be a `string` and is
219
+ * passed to the platform shell as-is (via the supplied `Spawner`'s own
220
+ * `shell` option) -- shell metacharacter rejection does not apply. See
221
+ * SECURITY.md before enabling this.
222
+ */
223
+ readonly shell?: boolean;
224
+ /** Working directory for the spawned process. Defaults to the current process's `cwd`. */
225
+ readonly cwd?: string;
226
+ /** Additional environment variables for the spawned process, applied on top of (or, if `inheritEnv` is `false`, instead of) the inherited environment. */
227
+ readonly env?: Readonly<Record<string, string>>;
228
+ /** Whether the spawned process inherits `process.env`. Defaults to `true` -- most check commands (npm scripts, locally-installed CLIs) need `PATH` and similar to resolve at all. Set to `false` for a minimal environment containing only `env` (plus whatever the OS itself always provides). */
229
+ readonly inheritEnv?: boolean;
230
+ /** Maximum time to let this check's process run before it is terminated and recorded with `status: "timed_out"`. No timeout by default. */
231
+ readonly timeoutMs?: number;
232
+ /** Request that stdout be parsed as this format. Omit for no parsing -- the consumer gets raw stdout/stderr only. */
233
+ readonly output?: {
234
+ readonly format: OutputFormat;
235
+ /**
236
+ * An optional Standard Schema-compliant validator (Zod, Valibot, ArkType, or any other
237
+ * implementation of https://standardschema.dev), run once the requested `format` parse
238
+ * itself succeeds. repo-contract never imports a schema library itself --
239
+ * `StandardSchemaV1` is hand-vendored (see `src/standard-schema/types.ts`) purely as a
240
+ * type-level contract, so accepting any consumer's own schema object costs zero new
241
+ * runtime or dev dependencies (see
242
+ * specs/decisions/0012-hand-vendored-standard-schema-support-for-optional-output-validation.md). A successful
243
+ * `~standard.validate()` result *replaces* the parsed value on `CheckEvidence.output.value`
244
+ * (letting a schema transform, not just check, its input); a failing result becomes an
245
+ * ordinary `ParsedOutputFailure`, indistinguishable in shape from a malformed-JSON/YAML
246
+ * parse failure -- both are "this check's output isn't what was expected," reported as
247
+ * data, never a throw (see `parseOutput`). `validate()` itself throwing or rejecting is a
248
+ * different case -- a bug in the supplied schema, not malformed output -- and surfaces as
249
+ * `StandardSchemaValidateThrewError` instead (see that error's own doc comment). Like every
250
+ * other format's `value`, `schema`'s inferred output type is *not* threaded to
251
+ * `CheckEvidence.output.value`'s declared type -- it stays `unknown`, for the same
252
+ * heterogeneous-checks-in-one-record TypeScript inference limitation already documented on
253
+ * `CheckEvidence` above; a policy still narrows or casts `.value` itself, now
254
+ * shaped/transformed by `schema` at runtime even though the type alone doesn't say so.
255
+ */
256
+ readonly schema?: StandardSchemaV1;
257
+ };
258
+ /**
259
+ * A full scheduling barrier at this check's own position in the `checks` object: it does not
260
+ * spawn until every check declared *earlier* has reached a terminal status (nothing "currently in
261
+ * flight" when its turn comes can be anything other than an earlier-declared check, since nothing
262
+ * declared later has even been reached yet), and every check declared *after* it waits for it in
263
+ * turn -- so nothing overlaps it in either direction. Purely a scheduling primitive for a check
264
+ * whose own tooling spawns concurrent workers that would otherwise contend with the rest of the
265
+ * run for machine resources (e.g. Stryker's own worker pool); it expresses no need for any other
266
+ * check's evidence, only for the machine to itself, and never appears in a policy's
267
+ * `ctx.dependencies` or in a partial `options.checks` run's transitive closure on its own (an
268
+ * explicit `dependsOn` alongside it still works exactly as it would on any other check). Defaults
269
+ * to `false` -- unaffected, fully-parallel scheduling in declaration order, unchanged. Two
270
+ * isolated checks are always sequential relative to each other (whichever is declared second
271
+ * waits for the first, as one of the "every check declared earlier" it's barred behind). See
272
+ * specs/decisions/0002-dependson-and-isolated-are-two-scheduling-primitives.md.
273
+ */
274
+ readonly isolated?: boolean;
275
+ /** Decides whether this check's evidence is acceptable, once its process has finished running. */
276
+ readonly policy: Policy;
277
+ }
278
+ /** One fully configured check: how to run it, how (if at all) to interpret its output, and the policy that decides whether its evidence is acceptable. */
279
+ export interface CheckDefinition extends CheckDefinitionConfig {
280
+ /**
281
+ * Other check ids (from this same `checks` record) that must reach a
282
+ * terminal status -- not necessarily a passing policy -- before this
283
+ * check's own process is spawned. Execution ordering only: whether a
284
+ * dependency's *policy* passed is never consulted by repo-contract to
285
+ * decide whether this check's process spawns -- that is this check's
286
+ * own command or policy's decision (see `ctx.dependencies` on
287
+ * `PolicyContext`). Omit, or an empty array, for no dependencies -- the
288
+ * default, fully-parallel behavior, unchanged. Validated before any
289
+ * process spawns: every named id must exist in `checks`, a check cannot
290
+ * depend on itself, and -- since declaration order in the `checks` object
291
+ * doubles as the required topological order -- every named id must be
292
+ * declared *earlier* than the check declaring `dependsOn` on it (a
293
+ * forward reference throws `DependencyDeclaredLaterError`; a cycle is
294
+ * consequently impossible, since no edge can ever point forward). See
295
+ * specs/decisions/0002-dependson-and-isolated-are-two-scheduling-primitives.md.
296
+ */
297
+ readonly dependsOn?: readonly string[];
298
+ }
299
+ /** The full set of checks in a `RepoContractConfig`, keyed by check id. */
300
+ export type CheckSchema = Record<string, CheckDefinition>;
301
+ /**
302
+ * Same shape as a check schema `T`, except each check's own `dependsOn` is
303
+ * narrowed from `readonly string[]` to only the *other* keys of that same
304
+ * `T` -- so a typo'd or self-referencing id fails to compile instead of
305
+ * only failing at runtime (`validate-config.ts` still enforces this at
306
+ * runtime too, for any config that reaches `runRepoContract` without having
307
+ * gone through `defineRepoContract`'s static checking first, e.g. one
308
+ * assembled dynamically from untyped data).
309
+ *
310
+ * Used as an additional constraint on `defineRepoContract`'s parameter,
311
+ * intersected with `RepoContractConfig<TChecks>` rather than substituted
312
+ * for it -- inferring `TChecks` from the *unwrapped* `RepoContractConfig<TChecks>`
313
+ * position first is what keeps every check's own `policy` callback
314
+ * contextually typed (a real TypeScript inference limitation: inferring
315
+ * `TChecks` directly from a mapped/conditional type over itself, as this
316
+ * type is, loses that contextual typing -- confirmed during implementation,
317
+ * not a hypothetical).
318
+ */
319
+ export type ValidatedCheckSchema<T> = {
320
+ readonly [K in keyof T]: T[K] extends CheckDefinitionConfig ? Omit<T[K], "dependsOn"> & {
321
+ readonly dependsOn?: readonly (Exclude<keyof T, K> & string)[];
322
+ } : never;
323
+ };
324
+ /**
325
+ * Spawns a child process, given a resolved command, argv, and options --
326
+ * modeled directly on `node:child_process`'s own `spawn(command, args,
327
+ * options)` signature so both `node:child_process.spawn` and cross-spawn's
328
+ * exported `spawn` are valid, drop-in values with no adapter code required
329
+ * (see specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md).
330
+ * repo-contract treats whatever function is supplied as a trusted
331
+ * capability: it calls it with a resolved command/argv/options and does not
332
+ * inspect, wrap, or sanitize it -- the security properties of the spawned
333
+ * process are entirely the supplied function's own.
334
+ */
335
+ export type Spawner = (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
336
+ /**
337
+ * Synchronously spawns a child process and waits for it to exit -- modeled directly on
338
+ * `node:child_process`'s own `spawnSync(command, args, options)` signature, the same
339
+ * drop-in-compatibility approach as `Spawner`. Used only for `RepoContractConfig.killProcessTree`
340
+ * (Windows process-tree cleanup via `taskkill`, which fundamentally needs to run synchronously from
341
+ * a signal-handling context that cannot `await` anything else -- see
342
+ * specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md).
343
+ * `node:child_process.spawnSync` and cross-spawn's exported `sync` are both valid, drop-in values.
344
+ */
345
+ export type SyncSpawner = (command: string, args: readonly string[], options: SpawnSyncOptions) => SpawnSyncReturns<Buffer | string>;
346
+ /** Top-level configuration passed to `defineRepoContract`/`runRepoContract`. */
347
+ export interface RepoContractConfig<TChecks extends CheckSchema = CheckSchema> {
348
+ /** Every check to run, keyed by check id. */
349
+ readonly checks: TChecks;
350
+ /** Maximum number of checks to execute concurrently. Defaults to `os.availableParallelism()`. Must be a positive integer. */
351
+ readonly concurrency?: number;
352
+ /**
353
+ * The trusted capability repo-contract calls to spawn every check's
354
+ * process -- e.g. `child_process.spawn` (from `"node:child_process"`) or
355
+ * cross-spawn's exported `spawn`. Required: repo-contract never imports a
356
+ * process-spawning implementation itself (see ADR 0011 above `Spawner`).
357
+ * `child_process.spawn` alone does not resolve Windows `.cmd`/`.bat`
358
+ * shims (most npm-installed CLI tools on Windows) without `shell: true`;
359
+ * pass cross-spawn instead for that correctness without enabling shell
360
+ * metacharacter interpretation -- cross-spawn is a spawn implementation
361
+ * choice, not a `shell: true` equivalent. See `shell` below.
362
+ */
363
+ readonly spawn: Spawner;
364
+ /**
365
+ * The ambient environment repo-contract treats as inheritable by each
366
+ * check whose `inheritEnv` is not `false` (the default) -- typically
367
+ * `process.env`, passed straight through by reference (never copied
368
+ * internally) so a consumer that mutates `process.env` mid-run still sees
369
+ * that reflected in later-spawned checks, exactly as if repo-contract had
370
+ * read `process.env` itself. Required: repo-contract never reads
371
+ * `process.env` internally (see ADR 0011 above `Spawner`). Typed as
372
+ * `NodeJS.ProcessEnv` so `env: process.env` needs no casting.
373
+ */
374
+ readonly env: NodeJS.ProcessEnv;
375
+ /**
376
+ * Global default for every check's own `shell` when that check doesn't
377
+ * set one itself (`check.shell ?? shell ?? false`). Defaults to `false`,
378
+ * the safe argv-only mode -- unrelated to which `spawn` is supplied; see
379
+ * `spawn`'s own doc comment for that distinction.
380
+ */
381
+ readonly shell?: boolean;
382
+ /**
383
+ * The trusted capability repo-contract calls, on Windows only, to forcibly terminate a check's
384
+ * entire process tree (not just its immediate process) on a timeout, an aborted run, or a
385
+ * host-process SIGINT/SIGTERM -- e.g. `child_process.spawnSync` (from `"node:child_process"`) or
386
+ * cross-spawn's exported `sync`. Optional, unlike `spawn`/`env`: when omitted, Windows cleanup
387
+ * falls back to terminating only the check's own immediate process (not any subprocess it spawned
388
+ * internally) -- correct for the common case, but a check that spawns its own descendants (e.g.
389
+ * `npm test` spawning the real test runner) may leave them running. POSIX cleanup never needs
390
+ * this at all (`process.kill(-pid, signal)` reaches the whole process group directly, no spawning
391
+ * required) -- see specs/decisions/0011-process-spawning-and-ambient-environment-access-are-consumer-supplied-capabilities-not-package-owned.md.
392
+ */
393
+ readonly killProcessTree?: SyncSpawner;
394
+ }
395
+ /** Optional per-run controls for `runRepoContract`. */
396
+ export interface RunRepoContractOptions {
397
+ /** Abort the entire run. Checks already in flight are terminated; checks not yet started never spawn. Every configured check still receives a well-formed evidence entry (`status: "aborted"`) and still has its policy invoked. */
398
+ readonly signal?: AbortSignal;
399
+ /** Restrict this run to only these check ids (and whatever they `dependsOn`, transitively). Every configured check runs when omitted. */
400
+ readonly checks?: string[];
401
+ }
402
+ /**
403
+ * Versioned, immutable aggregate result of evaluating every configured
404
+ * check's policy against its evidence -- each check's own `PolicyResult`,
405
+ * verbatim, keyed by check id. `passed` is `true` only if every check's
406
+ * `outcome` is `"pass"` or `"warn"` -- `"fail"` is the only outcome that
407
+ * fails the run; one failing check never collapses into a single generic
408
+ * message, every check remains individually inspectable under `checks`.
409
+ * Versioned independently of `Evidence` (see VERSIONING.md's
410
+ * schema-versioning policy) -- `version: 2` reflects `checks[id]` changing
411
+ * shape from `{ passed, reason? }` to a full `PolicyResult`
412
+ * (`{ outcome, rationale }`); see ADR 0001.
413
+ */
414
+ export interface Verdict<TChecks extends CheckSchema = CheckSchema> {
415
+ /** Schema version of this shape; see VERSIONING.md. */
416
+ readonly version: 2;
417
+ /** `true` only if every check's `outcome` is `"pass"` or `"warn"`. */
418
+ readonly passed: boolean;
419
+ /** Each configured check's own `PolicyResult`, verbatim, keyed by check id. */
420
+ readonly checks: {
421
+ readonly [K in keyof TChecks]: PolicyResult;
422
+ };
423
+ }
424
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -96,6 +96,20 @@ var ParserDependencyMissingError = class extends RepoContractError {
96
96
  this.format = format;
97
97
  }
98
98
  };
99
+ var StandardSchemaValidateThrewError = class extends RepoContractError {
100
+ /** Always `"REPO_CONTRACT_STANDARD_SCHEMA_VALIDATE_THREW"`. */
101
+ code = "REPO_CONTRACT_STANDARD_SCHEMA_VALIDATE_THREW";
102
+ /** The id of the check whose `output.schema` threw during validation. */
103
+ checkId;
104
+ constructor(checkId, cause) {
105
+ super(
106
+ `Check "${checkId}"'s output.schema["~standard"].validate() threw instead of returning a Result`,
107
+ { cause }
108
+ );
109
+ this.name = "StandardSchemaValidateThrewError";
110
+ this.checkId = checkId;
111
+ }
112
+ };
99
113
  var PolicyThrewError = class extends RepoContractError {
100
114
  /** Always `"REPO_CONTRACT_POLICY_THREW"`. */
101
115
  code = "REPO_CONTRACT_POLICY_THREW";
@@ -380,13 +394,46 @@ function validateOutput(checkId, output) {
380
394
  if (output === null || typeof output !== "object") {
381
395
  throw new InvalidCheckConfigError(checkId, "output must be an object when provided.");
382
396
  }
383
- const { format } = output;
397
+ const { format, schema } = output;
384
398
  if (typeof format !== "string" || !OUTPUT_FORMATS.includes(format)) {
385
399
  throw new InvalidCheckConfigError(
386
400
  checkId,
387
401
  `output.format must be one of ${OUTPUT_FORMATS.map((f) => `"${f}"`).join(", ")}.`
388
402
  );
389
403
  }
404
+ validateOutputSchema(checkId, schema);
405
+ }
406
+ function validateOutputSchema(checkId, schema) {
407
+ if (schema === void 0) return;
408
+ if (schema === null || typeof schema !== "object" && typeof schema !== "function") {
409
+ throw new InvalidCheckConfigError(
410
+ checkId,
411
+ "output.schema must be a Standard Schema-compliant object or function when provided (see https://standardschema.dev)."
412
+ );
413
+ }
414
+ const standard = schema["~standard"];
415
+ if (standard === null || typeof standard !== "object") {
416
+ throw new InvalidCheckConfigError(
417
+ checkId,
418
+ 'output.schema must have a "~standard" property (see https://standardschema.dev).'
419
+ );
420
+ }
421
+ const { version, vendor, validate } = standard;
422
+ if (version !== 1) {
423
+ throw new InvalidCheckConfigError(checkId, 'output.schema["~standard"].version must be 1.');
424
+ }
425
+ if (typeof vendor !== "string") {
426
+ throw new InvalidCheckConfigError(
427
+ checkId,
428
+ 'output.schema["~standard"].vendor must be a string.'
429
+ );
430
+ }
431
+ if (typeof validate !== "function") {
432
+ throw new InvalidCheckConfigError(
433
+ checkId,
434
+ 'output.schema["~standard"].validate must be a function.'
435
+ );
436
+ }
390
437
  }
391
438
  function validatePolicy(checkId, policy) {
392
439
  if (typeof policy !== "function") {
@@ -427,6 +474,28 @@ function validateDependencyGraph(checks) {
427
474
  }
428
475
  }
429
476
 
477
+ // src/parsing/format-schema-issues.ts
478
+ function formatSchemaIssues(issues) {
479
+ return `Schema validation failed: ${issues.map(formatIssue).join("; ")}`;
480
+ }
481
+ function formatIssue(issue) {
482
+ const path = formatPath(issue.path);
483
+ return path === "" ? issue.message : `${path}: ${issue.message}`;
484
+ }
485
+ function formatPath(path) {
486
+ if (path === void 0 || path.length === 0) return "";
487
+ return path.map(
488
+ (segment) => (
489
+ // eslint-disable-next-line secure-coding/no-improper-type-validation -- `segment` is declared `PropertyKey | StandardSchemaV1.PathSegment` (never `null` or an array), so `typeof segment === "object"` can only match the `PathSegment` object form here; the null/array-safe rewrite this rule suggests is flagged as an unreachable condition by @typescript-eslint/no-unnecessary-condition given that exact type, so satisfying both rules at once is impossible -- the type itself is the guarantee this check would otherwise add at runtime.
490
+ typeof segment === "object" ? segment.key : segment
491
+ )
492
+ ).map((key, index) => {
493
+ if (typeof key === "number") return `[${String(key)}]`;
494
+ const rendered = String(key);
495
+ return index === 0 ? rendered : `.${rendered}`;
496
+ }).join("");
497
+ }
498
+
430
499
  // src/parsing/parse-json.ts
431
500
  function parseJson(stdout) {
432
501
  try {
@@ -465,7 +534,24 @@ async function parseYaml(stdout, checkId) {
465
534
  }
466
535
 
467
536
  // src/parsing/parse-output.ts
468
- async function parseOutput(format, stdout, checkId) {
537
+ async function parseOutput(format, stdout, checkId, schema) {
538
+ const parsed = await dispatch(format, stdout, checkId);
539
+ if (!parsed.success || schema === void 0) return parsed;
540
+ let result;
541
+ try {
542
+ result = await schema["~standard"].validate(parsed.value);
543
+ if (typeof result !== "object" || result === null) {
544
+ throw new TypeError(`validate() returned ${String(result)} instead of a Result object`);
545
+ }
546
+ } catch (error) {
547
+ throw new StandardSchemaValidateThrewError(checkId, error);
548
+ }
549
+ if (result.issues === void 0) {
550
+ return { format, success: true, value: result.value };
551
+ }
552
+ return { format, success: false, error: formatSchemaIssues(result.issues) };
553
+ }
554
+ async function dispatch(format, stdout, checkId) {
469
555
  switch (format) {
470
556
  case "json":
471
557
  return parseJson(stdout);
@@ -483,7 +569,12 @@ async function buildEvidence(results, startedAt, completedAt) {
483
569
  results.map(async ([checkId, check, raw]) => {
484
570
  if (check.output === void 0) return [checkId, check, raw];
485
571
  try {
486
- const output = await parseOutput(check.output.format, raw.stdout, checkId);
572
+ const output = await parseOutput(
573
+ check.output.format,
574
+ raw.stdout,
575
+ checkId,
576
+ check.output.schema
577
+ );
487
578
  return [checkId, check, { ...raw, output }];
488
579
  } catch (error) {
489
580
  thrown.push(error);
@@ -496,7 +587,10 @@ async function buildEvidence(results, startedAt, completedAt) {
496
587
  throw only;
497
588
  }
498
589
  if (thrown.length > 1) {
499
- throw new AggregateError(thrown, `${String(thrown.length)} check output(s) failed to parse.`);
590
+ throw new AggregateError(
591
+ thrown,
592
+ `${String(thrown.length)} check output(s) failed during output processing.`
593
+ );
500
594
  }
501
595
  const evidence = {
502
596
  version: 1,
@@ -1096,6 +1190,7 @@ exports.PolicyReadFailedParseValueError = PolicyReadFailedParseValueError;
1096
1190
  exports.PolicyReadUnrequestedOutputError = PolicyReadUnrequestedOutputError;
1097
1191
  exports.PolicyThrewError = PolicyThrewError;
1098
1192
  exports.RepoContractError = RepoContractError;
1193
+ exports.StandardSchemaValidateThrewError = StandardSchemaValidateThrewError;
1099
1194
  exports.UnknownCheckIdError = UnknownCheckIdError;
1100
1195
  exports.defineRepoContract = defineRepoContract;
1101
1196
  exports.runRepoContract = runRepoContract;