repo-contract 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +967 -0
- package/dist/.dts/config/define-repo-contract.d.ts +36 -0
- package/dist/.dts/config/define-repo-contract.d.ts.map +1 -0
- package/dist/.dts/config/tokenize-command.d.ts +33 -0
- package/dist/.dts/config/tokenize-command.d.ts.map +1 -0
- package/dist/.dts/config/validate-config.d.ts +32 -0
- package/dist/.dts/config/validate-config.d.ts.map +1 -0
- package/dist/.dts/errors.d.ts +155 -0
- package/dist/.dts/errors.d.ts.map +1 -0
- package/dist/.dts/evidence/build-evidence.d.ts +26 -0
- package/dist/.dts/evidence/build-evidence.d.ts.map +1 -0
- package/dist/.dts/execution/abort-signals.d.ts +29 -0
- package/dist/.dts/execution/abort-signals.d.ts.map +1 -0
- package/dist/.dts/execution/concurrency-pool.d.ts +14 -0
- package/dist/.dts/execution/concurrency-pool.d.ts.map +1 -0
- package/dist/.dts/execution/dependency-scheduler.d.ts +30 -0
- package/dist/.dts/execution/dependency-scheduler.d.ts.map +1 -0
- package/dist/.dts/execution/process-tree.d.ts +48 -0
- package/dist/.dts/execution/process-tree.d.ts.map +1 -0
- package/dist/.dts/execution/run-checks.d.ts +29 -0
- package/dist/.dts/execution/run-checks.d.ts.map +1 -0
- package/dist/.dts/execution/spawn-check.d.ts +30 -0
- package/dist/.dts/execution/spawn-check.d.ts.map +1 -0
- package/dist/.dts/index.d.ts +13 -0
- package/dist/.dts/index.d.ts.map +1 -0
- package/dist/.dts/parsing/parse-json.d.ts +8 -0
- package/dist/.dts/parsing/parse-json.d.ts.map +1 -0
- package/dist/.dts/parsing/parse-output.d.ts +10 -0
- package/dist/.dts/parsing/parse-output.d.ts.map +1 -0
- package/dist/.dts/parsing/parse-text.d.ts +8 -0
- package/dist/.dts/parsing/parse-text.d.ts.map +1 -0
- package/dist/.dts/parsing/parse-yaml.d.ts +21 -0
- package/dist/.dts/parsing/parse-yaml.d.ts.map +1 -0
- package/dist/.dts/policy/run-policies.d.ts +38 -0
- package/dist/.dts/policy/run-policies.d.ts.map +1 -0
- package/dist/.dts/presets/arethetypeswrong.d.ts +39 -0
- package/dist/.dts/presets/arethetypeswrong.d.ts.map +1 -0
- package/dist/.dts/presets/broken-links.d.ts +16 -0
- package/dist/.dts/presets/broken-links.d.ts.map +1 -0
- package/dist/.dts/presets/commitlint.d.ts +22 -0
- package/dist/.dts/presets/commitlint.d.ts.map +1 -0
- package/dist/.dts/presets/dead-code.d.ts +23 -0
- package/dist/.dts/presets/dead-code.d.ts.map +1 -0
- package/dist/.dts/presets/duplication.d.ts +14 -0
- package/dist/.dts/presets/duplication.d.ts.map +1 -0
- package/dist/.dts/presets/e2e.d.ts +4 -0
- package/dist/.dts/presets/e2e.d.ts.map +1 -0
- package/dist/.dts/presets/format.d.ts +4 -0
- package/dist/.dts/presets/format.d.ts.map +1 -0
- package/dist/.dts/presets/index.d.ts +31 -0
- package/dist/.dts/presets/index.d.ts.map +1 -0
- package/dist/.dts/presets/license.d.ts +4 -0
- package/dist/.dts/presets/license.d.ts.map +1 -0
- package/dist/.dts/presets/lint.d.ts +20 -0
- package/dist/.dts/presets/lint.d.ts.map +1 -0
- package/dist/.dts/presets/markdownlint.d.ts +23 -0
- package/dist/.dts/presets/markdownlint.d.ts.map +1 -0
- package/dist/.dts/presets/publint.d.ts +13 -0
- package/dist/.dts/presets/publint.d.ts.map +1 -0
- package/dist/.dts/presets/security-deps.d.ts +4 -0
- package/dist/.dts/presets/security-deps.d.ts.map +1 -0
- package/dist/.dts/presets/security-secrets.d.ts +4 -0
- package/dist/.dts/presets/security-secrets.d.ts.map +1 -0
- package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts +13 -0
- package/dist/.dts/presets/shared/error-warning-pass-policy.d.ts.map +1 -0
- package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts +23 -0
- package/dist/.dts/presets/shared/exit-code-fail-rationale.d.ts.map +1 -0
- package/dist/.dts/presets/shared/missing-dependency.d.ts +19 -0
- package/dist/.dts/presets/shared/missing-dependency.d.ts.map +1 -0
- package/dist/.dts/presets/shared/read-json-report.d.ts +33 -0
- package/dist/.dts/presets/shared/read-json-report.d.ts.map +1 -0
- package/dist/.dts/presets/shared/terminal-status.d.ts +25 -0
- package/dist/.dts/presets/shared/terminal-status.d.ts.map +1 -0
- package/dist/.dts/presets/shared/vitest-json-policy.d.ts +20 -0
- package/dist/.dts/presets/shared/vitest-json-policy.d.ts.map +1 -0
- package/dist/.dts/presets/stylelint.d.ts +17 -0
- package/dist/.dts/presets/stylelint.d.ts.map +1 -0
- package/dist/.dts/presets/test.d.ts +4 -0
- package/dist/.dts/presets/test.d.ts.map +1 -0
- package/dist/.dts/presets/typecheck.d.ts +4 -0
- package/dist/.dts/presets/typecheck.d.ts.map +1 -0
- package/dist/.dts/run-repo-contract.d.ts +38 -0
- package/dist/.dts/run-repo-contract.d.ts.map +1 -0
- package/dist/.dts/types.d.ts +324 -0
- package/dist/.dts/types.d.ts.map +1 -0
- package/dist/index.cjs +46 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/presets.cjs +30 -0
- package/dist/presets.cjs.map +1 -0
- package/dist/presets.d.cts +1 -0
- package/dist/presets.d.ts +1 -0
- package/dist/presets.js +13 -0
- package/dist/presets.js.map +1 -0
- package/package.json +192 -0
- package/presets/package.json +5 -0
- package/schemas/evidence.schema.json +253 -0
- package/schemas/verdict.schema.json +66 -0
- package/src/config/define-repo-contract.ts +38 -0
- package/src/config/tokenize-command.ts +214 -0
- package/src/config/validate-config.ts +368 -0
- package/src/errors.ts +229 -0
- package/src/evidence/build-evidence.ts +91 -0
- package/src/execution/abort-signals.ts +56 -0
- package/src/execution/concurrency-pool.ts +64 -0
- package/src/execution/dependency-scheduler.ts +216 -0
- package/src/execution/process-tree.ts +107 -0
- package/src/execution/run-checks.ts +348 -0
- package/src/execution/spawn-check.ts +494 -0
- package/src/index.ts +44 -0
- package/src/parsing/parse-json.ts +18 -0
- package/src/parsing/parse-output.ts +26 -0
- package/src/parsing/parse-text.ts +10 -0
- package/src/parsing/parse-yaml.ts +40 -0
- package/src/policy/run-policies.ts +261 -0
- package/src/presets/arethetypeswrong.ts +116 -0
- package/src/presets/broken-links.ts +95 -0
- package/src/presets/commitlint.ts +77 -0
- package/src/presets/dead-code.ts +223 -0
- package/src/presets/duplication.ts +137 -0
- package/src/presets/e2e.ts +144 -0
- package/src/presets/format.ts +25 -0
- package/src/presets/index.ts +30 -0
- package/src/presets/license.ts +90 -0
- package/src/presets/lint.ts +116 -0
- package/src/presets/markdownlint.ts +105 -0
- package/src/presets/publint.ts +38 -0
- package/src/presets/security-deps.ts +142 -0
- package/src/presets/security-secrets.ts +93 -0
- package/src/presets/shared/error-warning-pass-policy.ts +39 -0
- package/src/presets/shared/exit-code-fail-rationale.ts +34 -0
- package/src/presets/shared/missing-dependency.ts +31 -0
- package/src/presets/shared/read-json-report.ts +46 -0
- package/src/presets/shared/terminal-status.ts +70 -0
- package/src/presets/shared/vitest-json-policy.ts +95 -0
- package/src/presets/stylelint.ts +101 -0
- package/src/presets/test.ts +19 -0
- package/src/presets/typecheck.ts +25 -0
- package/src/run-repo-contract.ts +80 -0
- package/src/types.ts +340 -0
|
@@ -0,0 +1,324 @@
|
|
|
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
|
+
/** Output interpretation a check can explicitly request. No format requested means no parsing -- the consumer gets raw stdout/stderr only. */
|
|
8
|
+
export type OutputFormat = "json" | "yaml" | "text";
|
|
9
|
+
/**
|
|
10
|
+
* Why a check's process ended up in its terminal state. `"completed"` means
|
|
11
|
+
* the process ran to exit on its own -- the exit code may still be
|
|
12
|
+
* non-zero, and that is for the check's policy to interpret, never this
|
|
13
|
+
* package. The other five values all mean the process did not exit on its
|
|
14
|
+
* own; repo-contract terminated it, or it was terminated for a reason
|
|
15
|
+
* repo-contract can observe but did not cause.
|
|
16
|
+
*
|
|
17
|
+
* `"signaled"` specifically means a signal repo-contract did *not* itself request -- an
|
|
18
|
+
* externally-caused termination. A check killed because the *host* process running repo-contract
|
|
19
|
+
* received its own SIGINT/SIGTERM (see `run-checks.ts`'s termination-handler cleanup) is instead
|
|
20
|
+
* `"host_terminated"`: repo-contract did request that signal, just not via `options.signal` or
|
|
21
|
+
* `timeoutMs` (see `"aborted"`/`"timed_out"`), so it must not be conflated with an externally-caused
|
|
22
|
+
* `"signaled"`.
|
|
23
|
+
*/
|
|
24
|
+
export type CheckStatus = "completed" | "timed_out" | "signaled" | "host_terminated" | "spawn_error" | "aborted";
|
|
25
|
+
/** A requested parse of a check's stdout succeeded. */
|
|
26
|
+
export interface ParsedOutputSuccess<T> {
|
|
27
|
+
/** The format that was requested and successfully parsed. */
|
|
28
|
+
readonly format: OutputFormat;
|
|
29
|
+
/** Always `true`. */
|
|
30
|
+
readonly success: true;
|
|
31
|
+
/** The parsed value. */
|
|
32
|
+
readonly value: T;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A requested parse of a check's stdout failed. The raw stdout on the
|
|
36
|
+
* parent `CheckEvidence` is preserved unchanged -- a parse failure is never
|
|
37
|
+
* silently reinterpreted or discarded.
|
|
38
|
+
*/
|
|
39
|
+
export interface ParsedOutputFailure {
|
|
40
|
+
/** The format that was requested (and failed to parse). */
|
|
41
|
+
readonly format: OutputFormat;
|
|
42
|
+
/** Always `false`. */
|
|
43
|
+
readonly success: false;
|
|
44
|
+
/** The parse error's message. */
|
|
45
|
+
readonly error: string;
|
|
46
|
+
}
|
|
47
|
+
/** The result of a check's requested output-format parse: either a successful `ParsedOutputSuccess`, or a `ParsedOutputFailure`. */
|
|
48
|
+
export type ParsedOutput<T> = ParsedOutputSuccess<T> | ParsedOutputFailure;
|
|
49
|
+
/**
|
|
50
|
+
* What actually happened when one configured check ran. `output` is present
|
|
51
|
+
* only if that check's config requested a format, and is otherwise
|
|
52
|
+
* `undefined` -- a policy narrows with `ctx.result.output?.success` (or an
|
|
53
|
+
* `if (ctx.result.output) { ... }` guard) before reading `.value`/`.error`.
|
|
54
|
+
*
|
|
55
|
+
* `output.value` is typed `unknown` for every format, including `"text"`
|
|
56
|
+
* (even though `parseText` always produces a `string` at runtime) --
|
|
57
|
+
* neither repo-contract nor TypeScript's generic inference can reliably
|
|
58
|
+
* carry a specific check's own literal `output.format` through to that
|
|
59
|
+
* same check's `policy` parameter once several checks with heterogeneous
|
|
60
|
+
* formats live together in one `checks` record (a real TypeScript
|
|
61
|
+
* inference limitation hit and confirmed during implementation, not a
|
|
62
|
+
* hypothetical -- see specs/decisions/ for the isolated repro). A policy
|
|
63
|
+
* author narrows or casts `.value` themselves, exactly as they already must
|
|
64
|
+
* for `"json"`/`"yaml"` where no schema knowledge exists either way.
|
|
65
|
+
*/
|
|
66
|
+
export interface CheckEvidence {
|
|
67
|
+
/** The executable that was actually spawned (after tokenization, if `run` was a string). */
|
|
68
|
+
readonly command: string;
|
|
69
|
+
/** The arguments passed to `command`, exactly as spawned. */
|
|
70
|
+
readonly args: readonly string[];
|
|
71
|
+
/** ISO 8601 timestamp of when the process was spawned. */
|
|
72
|
+
readonly startedAt: string;
|
|
73
|
+
/** ISO 8601 timestamp of when the process reached its terminal state. */
|
|
74
|
+
readonly completedAt: string;
|
|
75
|
+
/** Wall-clock time from spawn to termination, in milliseconds. */
|
|
76
|
+
readonly durationMs: number;
|
|
77
|
+
/** `null` when the process never exited normally -- see `signal` and `status`. */
|
|
78
|
+
readonly exitCode: number | null;
|
|
79
|
+
/** The signal that terminated the process, if any. `null` for a normal exit or a spawn failure. */
|
|
80
|
+
readonly signal: NodeJS.Signals | null;
|
|
81
|
+
/** 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. */
|
|
82
|
+
readonly stdout: string;
|
|
83
|
+
/** 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. */
|
|
84
|
+
readonly stderr: string;
|
|
85
|
+
/** Why the process reached its terminal state; see `CheckStatus`. */
|
|
86
|
+
readonly status: CheckStatus;
|
|
87
|
+
/** Populated only for `status === "spawn_error"` -- the underlying Node error message (e.g. "spawn foo ENOENT"). Never populated for any other status. */
|
|
88
|
+
readonly spawnError?: string;
|
|
89
|
+
/** 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. */
|
|
90
|
+
readonly spawnErrorCode?: string;
|
|
91
|
+
/** The parsed interpretation of `stdout`, present only if this check's config requested a `format`. */
|
|
92
|
+
readonly output?: ParsedOutput<unknown>;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Versioned, immutable record of one complete `runRepoContract` execution --
|
|
96
|
+
* every configured check's evidence, plus timing for the run as a whole.
|
|
97
|
+
* Says nothing about whether any of it was acceptable; see `Verdict`.
|
|
98
|
+
* Additive fields are a compatible change; changing or removing an existing
|
|
99
|
+
* field requires bumping this version number (see VERSIONING.md).
|
|
100
|
+
*/
|
|
101
|
+
export interface Evidence<TChecks extends CheckSchema = CheckSchema> {
|
|
102
|
+
/** Schema version of this shape; see VERSIONING.md. */
|
|
103
|
+
readonly version: 1;
|
|
104
|
+
/** ISO 8601 timestamp of when the run began. */
|
|
105
|
+
readonly startedAt: string;
|
|
106
|
+
/** ISO 8601 timestamp of when the last check finished. */
|
|
107
|
+
readonly completedAt: string;
|
|
108
|
+
/** Wall-clock time for the run as a whole, in milliseconds. */
|
|
109
|
+
readonly durationMs: number;
|
|
110
|
+
/** Each configured check's own evidence, keyed by check id. */
|
|
111
|
+
readonly checks: {
|
|
112
|
+
readonly [K in keyof TChecks]: CheckEvidence;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* What a check's `policy` function is called with. `result` is that check's
|
|
117
|
+
* own evidence; `evidence` is the complete run's evidence, including every
|
|
118
|
+
* sibling check -- present so a policy can make cross-check decisions (e.g.
|
|
119
|
+
* "only enforce the mutation-score threshold if the tests check passed").
|
|
120
|
+
* By the time any policy runs, every check has already finished executing
|
|
121
|
+
* and every check's evidence has already been assembled -- no policy ever
|
|
122
|
+
* observes a partially-populated `evidence` (see specs/architecture.md).
|
|
123
|
+
*/
|
|
124
|
+
export interface PolicyContext<TChecks extends CheckSchema = CheckSchema> {
|
|
125
|
+
/** This check's own evidence. */
|
|
126
|
+
readonly result: CheckEvidence;
|
|
127
|
+
/** The complete run's evidence, including every sibling check. */
|
|
128
|
+
readonly evidence: Evidence<TChecks>;
|
|
129
|
+
/**
|
|
130
|
+
* This check's own declared `dependsOn` dependencies' evidence, keyed by
|
|
131
|
+
* check id -- a convenience view, fully derivable from `evidence.checks`
|
|
132
|
+
* plus this check's own `dependsOn`, provided so no policy has to do that
|
|
133
|
+
* lookup itself. `{}` for a check with no `dependsOn` -- always an
|
|
134
|
+
* object, never `undefined`, matching how `evidence.checks` is already
|
|
135
|
+
* used today (a policy narrows a specific key's presence, never the
|
|
136
|
+
* field itself). Evidence only, not policy outcomes -- a dependency's
|
|
137
|
+
* policy result stays visible only via the top-level `Verdict`, exactly
|
|
138
|
+
* as today; this keeps policy evaluation itself fully parallel and
|
|
139
|
+
* unaffected by `dependsOn`.
|
|
140
|
+
*/
|
|
141
|
+
readonly dependencies: Readonly<Record<string, CheckEvidence>>;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* `"pass"`: the repository-owned policy evaluated the captured evidence as
|
|
145
|
+
* satisfying its configured requirements. `"fail"`: the evidence did not
|
|
146
|
+
* satisfy them. `"warn"`: the evidence does not violate the policy's
|
|
147
|
+
* blocking requirements, but the policy has intentionally decided the
|
|
148
|
+
* condition is materially relevant and wants it surfaced -- not a synonym
|
|
149
|
+
* for "minor failure"; a `warn` never fails `Verdict.passed` (see
|
|
150
|
+
* `runPolicies` in `src/policy/run-policies.ts`).
|
|
151
|
+
*/
|
|
152
|
+
export type PolicyOutcome = "pass" | "fail" | "warn";
|
|
153
|
+
/**
|
|
154
|
+
* A repository-owned policy's interpretation of one check's captured
|
|
155
|
+
* evidence -- fully JSON-serializable (a plain object of primitives only,
|
|
156
|
+
* never an `Error`, class instance, function, or tool-specific object) so it
|
|
157
|
+
* can be persisted, transmitted, aggregated across parallel checks, and
|
|
158
|
+
* consumed directly by a human or an AI without rerunning anything.
|
|
159
|
+
*
|
|
160
|
+
* `rationale` is mandatory and must contain enough actionable detail --
|
|
161
|
+
* specific file/line locations, rule ids, test names, counts -- for a
|
|
162
|
+
* consumer to understand *why* the policy reached its outcome from this
|
|
163
|
+
* value alone. A rationale like "see output above" or "check the report for
|
|
164
|
+
* details" defeats the purpose: it forces the consumer back to raw,
|
|
165
|
+
* unstructured command output, exactly what this type exists to avoid. See
|
|
166
|
+
* specs/architecture.md for the evidence/rationale/judgment distinction this
|
|
167
|
+
* type is built around: evidence answers "what happened?", `rationale`
|
|
168
|
+
* answers "what does the repository's policy conclude about what
|
|
169
|
+
* happened?", and a policy's `outcome` is not the final word -- a human or
|
|
170
|
+
* AI consumer still makes the final judgment call using both.
|
|
171
|
+
*/
|
|
172
|
+
export interface PolicyResult {
|
|
173
|
+
/** The policy's pass/fail/warn decision. */
|
|
174
|
+
readonly outcome: PolicyOutcome;
|
|
175
|
+
/** Why the policy reached `outcome`, in enough detail to act on without rerunning anything. */
|
|
176
|
+
readonly rationale: string;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* A repository-owned decision about whether one check's evidence is
|
|
180
|
+
* acceptable. Returns a `PolicyResult` -- never a bare boolean or string --
|
|
181
|
+
* so a policy can communicate more than pass/fail even when the run is
|
|
182
|
+
* otherwise acceptable (see `PolicyResult`/`PolicyOutcome`). May be
|
|
183
|
+
* synchronous or return a `Promise`. repo-contract does not interpret
|
|
184
|
+
* `rationale` beyond storing and surfacing it verbatim; the package has no
|
|
185
|
+
* opinion about what makes a check pass, fail, or warrant a `warn`.
|
|
186
|
+
*/
|
|
187
|
+
export type Policy<TChecks extends CheckSchema = CheckSchema> = (ctx: PolicyContext<TChecks>) => PolicyResult | Promise<PolicyResult>;
|
|
188
|
+
/** 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. */
|
|
189
|
+
export interface CheckDefinitionConfig {
|
|
190
|
+
/**
|
|
191
|
+
* The command to run. A `string` is tokenized into executable + arguments
|
|
192
|
+
* without invoking a shell -- shell operators (`;`, `&`, `|`, backticks,
|
|
193
|
+
* `$(`, `<`, `>`, newlines) are rejected with a configuration error rather
|
|
194
|
+
* than silently passed through as literal arguments, since a string
|
|
195
|
+
* containing one almost always reflects a mistaken assumption that shell
|
|
196
|
+
* interpretation is happening. Glob characters (`*`, `?`, `~`, `[`, `]`,
|
|
197
|
+
* `{`, `}`) are NOT rejected -- they are common, legitimate literal argv
|
|
198
|
+
* content (e.g. `eslint "src/**\/*.ts"`) that many CLI tools glob-expand
|
|
199
|
+
* internally, and carry no shell-injection risk when no shell is invoked.
|
|
200
|
+
* An array bypasses tokenization entirely and is used as argv verbatim.
|
|
201
|
+
*/
|
|
202
|
+
readonly run: string | readonly string[];
|
|
203
|
+
/**
|
|
204
|
+
* Opt into real shell execution instead of the safe argv-only default.
|
|
205
|
+
* When `true`, `run` must be a `string` and is passed to the platform
|
|
206
|
+
* shell as-is (via cross-spawn's own `shell` option) -- shell metacharacter
|
|
207
|
+
* rejection does not apply. See SECURITY.md before enabling this.
|
|
208
|
+
*/
|
|
209
|
+
readonly shell?: boolean;
|
|
210
|
+
/** Working directory for the spawned process. Defaults to the current process's `cwd`. */
|
|
211
|
+
readonly cwd?: string;
|
|
212
|
+
/** Additional environment variables for the spawned process, applied on top of (or, if `inheritEnv` is `false`, instead of) the inherited environment. */
|
|
213
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
214
|
+
/** 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). */
|
|
215
|
+
readonly inheritEnv?: boolean;
|
|
216
|
+
/** Maximum time to let this check's process run before it is terminated and recorded with `status: "timed_out"`. No timeout by default. */
|
|
217
|
+
readonly timeoutMs?: number;
|
|
218
|
+
/** Request that stdout be parsed as this format. Omit for no parsing -- the consumer gets raw stdout/stderr only. */
|
|
219
|
+
readonly output?: {
|
|
220
|
+
readonly format: OutputFormat;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* A full scheduling barrier at this check's own position in the `checks` object: it does not
|
|
224
|
+
* spawn until every check declared *earlier* has reached a terminal status (nothing "currently in
|
|
225
|
+
* flight" when its turn comes can be anything other than an earlier-declared check, since nothing
|
|
226
|
+
* declared later has even been reached yet), and every check declared *after* it waits for it in
|
|
227
|
+
* turn -- so nothing overlaps it in either direction. Purely a scheduling primitive for a check
|
|
228
|
+
* whose own tooling spawns concurrent workers that would otherwise contend with the rest of the
|
|
229
|
+
* run for machine resources (e.g. Stryker's own worker pool); it expresses no need for any other
|
|
230
|
+
* check's evidence, only for the machine to itself, and never appears in a policy's
|
|
231
|
+
* `ctx.dependencies` or in a partial `options.checks` run's transitive closure on its own (an
|
|
232
|
+
* explicit `dependsOn` alongside it still works exactly as it would on any other check). Defaults
|
|
233
|
+
* to `false` -- unaffected, fully-parallel scheduling in declaration order, unchanged. Two
|
|
234
|
+
* isolated checks are always sequential relative to each other (whichever is declared second
|
|
235
|
+
* waits for the first, as one of the "every check declared earlier" it's barred behind). See
|
|
236
|
+
* specs/decisions/0002-dependson-and-isolated-are-two-scheduling-primitives.md.
|
|
237
|
+
*/
|
|
238
|
+
readonly isolated?: boolean;
|
|
239
|
+
/** Decides whether this check's evidence is acceptable, once its process has finished running. */
|
|
240
|
+
readonly policy: Policy;
|
|
241
|
+
}
|
|
242
|
+
/** 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. */
|
|
243
|
+
export interface CheckDefinition extends CheckDefinitionConfig {
|
|
244
|
+
/**
|
|
245
|
+
* Other check ids (from this same `checks` record) that must reach a
|
|
246
|
+
* terminal status -- not necessarily a passing policy -- before this
|
|
247
|
+
* check's own process is spawned. Execution ordering only: whether a
|
|
248
|
+
* dependency's *policy* passed is never consulted by repo-contract to
|
|
249
|
+
* decide whether this check's process spawns -- that is this check's
|
|
250
|
+
* own command or policy's decision (see `ctx.dependencies` on
|
|
251
|
+
* `PolicyContext`). Omit, or an empty array, for no dependencies -- the
|
|
252
|
+
* default, fully-parallel behavior, unchanged. Validated before any
|
|
253
|
+
* process spawns: every named id must exist in `checks`, a check cannot
|
|
254
|
+
* depend on itself, and -- since declaration order in the `checks` object
|
|
255
|
+
* doubles as the required topological order -- every named id must be
|
|
256
|
+
* declared *earlier* than the check declaring `dependsOn` on it (a
|
|
257
|
+
* forward reference throws `DependencyDeclaredLaterError`; a cycle is
|
|
258
|
+
* consequently impossible, since no edge can ever point forward). See
|
|
259
|
+
* specs/decisions/0002-dependson-and-isolated-are-two-scheduling-primitives.md.
|
|
260
|
+
*/
|
|
261
|
+
readonly dependsOn?: readonly string[];
|
|
262
|
+
}
|
|
263
|
+
/** The full set of checks in a `RepoContractConfig`, keyed by check id. */
|
|
264
|
+
export type CheckSchema = Record<string, CheckDefinition>;
|
|
265
|
+
/**
|
|
266
|
+
* Same shape as a check schema `T`, except each check's own `dependsOn` is
|
|
267
|
+
* narrowed from `readonly string[]` to only the *other* keys of that same
|
|
268
|
+
* `T` -- so a typo'd or self-referencing id fails to compile instead of
|
|
269
|
+
* only failing at runtime (`validate-config.ts` still enforces this at
|
|
270
|
+
* runtime too, for any config that reaches `runRepoContract` without having
|
|
271
|
+
* gone through `defineRepoContract`'s static checking first, e.g. one
|
|
272
|
+
* assembled dynamically from untyped data).
|
|
273
|
+
*
|
|
274
|
+
* Used as an additional constraint on `defineRepoContract`'s parameter,
|
|
275
|
+
* intersected with `RepoContractConfig<TChecks>` rather than substituted
|
|
276
|
+
* for it -- inferring `TChecks` from the *unwrapped* `RepoContractConfig<TChecks>`
|
|
277
|
+
* position first is what keeps every check's own `policy` callback
|
|
278
|
+
* contextually typed (a real TypeScript inference limitation: inferring
|
|
279
|
+
* `TChecks` directly from a mapped/conditional type over itself, as this
|
|
280
|
+
* type is, loses that contextual typing -- confirmed during implementation,
|
|
281
|
+
* not a hypothetical).
|
|
282
|
+
*/
|
|
283
|
+
export type ValidatedCheckSchema<T> = {
|
|
284
|
+
readonly [K in keyof T]: T[K] extends CheckDefinitionConfig ? Omit<T[K], "dependsOn"> & {
|
|
285
|
+
readonly dependsOn?: readonly (Exclude<keyof T, K> & string)[];
|
|
286
|
+
} : never;
|
|
287
|
+
};
|
|
288
|
+
/** Top-level configuration passed to `defineRepoContract`/`runRepoContract`. */
|
|
289
|
+
export interface RepoContractConfig<TChecks extends CheckSchema = CheckSchema> {
|
|
290
|
+
/** Every check to run, keyed by check id. */
|
|
291
|
+
readonly checks: TChecks;
|
|
292
|
+
/** Maximum number of checks to execute concurrently. Defaults to `os.availableParallelism()`. Must be a positive integer. */
|
|
293
|
+
readonly concurrency?: number;
|
|
294
|
+
}
|
|
295
|
+
/** Optional per-run controls for `runRepoContract`. */
|
|
296
|
+
export interface RunRepoContractOptions {
|
|
297
|
+
/** 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. */
|
|
298
|
+
readonly signal?: AbortSignal;
|
|
299
|
+
/** Restrict this run to only these check ids (and whatever they `dependsOn`, transitively). Every configured check runs when omitted. */
|
|
300
|
+
readonly checks?: string[];
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Versioned, immutable aggregate result of evaluating every configured
|
|
304
|
+
* check's policy against its evidence -- each check's own `PolicyResult`,
|
|
305
|
+
* verbatim, keyed by check id. `passed` is `true` only if every check's
|
|
306
|
+
* `outcome` is `"pass"` or `"warn"` -- `"fail"` is the only outcome that
|
|
307
|
+
* fails the run; one failing check never collapses into a single generic
|
|
308
|
+
* message, every check remains individually inspectable under `checks`.
|
|
309
|
+
* Versioned independently of `Evidence` (see VERSIONING.md's
|
|
310
|
+
* schema-versioning policy) -- `version: 2` reflects `checks[id]` changing
|
|
311
|
+
* shape from `{ passed, reason? }` to a full `PolicyResult`
|
|
312
|
+
* (`{ outcome, rationale }`); see ADR 0001.
|
|
313
|
+
*/
|
|
314
|
+
export interface Verdict<TChecks extends CheckSchema = CheckSchema> {
|
|
315
|
+
/** Schema version of this shape; see VERSIONING.md. */
|
|
316
|
+
readonly version: 2;
|
|
317
|
+
/** `true` only if every check's `outcome` is `"pass"` or `"warn"`. */
|
|
318
|
+
readonly passed: boolean;
|
|
319
|
+
/** Each configured check's own `PolicyResult`, verbatim, keyed by check id. */
|
|
320
|
+
readonly checks: {
|
|
321
|
+
readonly [K in keyof TChecks]: PolicyResult;
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
//# 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
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var os = require('os');
|
|
4
|
+
var crossSpawn = require('cross-spawn');
|
|
5
|
+
var string_decoder = require('string_decoder');
|
|
6
|
+
var child_process = require('child_process');
|
|
7
|
+
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
function _interopNamespace(e) {
|
|
11
|
+
if (e && e.__esModule) return e;
|
|
12
|
+
var n = Object.create(null);
|
|
13
|
+
if (e) {
|
|
14
|
+
Object.keys(e).forEach(function (k) {
|
|
15
|
+
if (k !== 'default') {
|
|
16
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
17
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: function () { return e[k]; }
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
n.default = e;
|
|
25
|
+
return Object.freeze(n);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var os__namespace = /*#__PURE__*/_interopNamespace(os);
|
|
29
|
+
var crossSpawn__default = /*#__PURE__*/_interopDefault(crossSpawn);
|
|
30
|
+
|
|
31
|
+
function defineRepoContract(config){return config}var RepoContractError=class extends Error{};var InvalidRepoContractConfigError=class extends RepoContractError{code="REPO_CONTRACT_INVALID_CONFIG";constructor(reason){super(`Invalid repo-contract config -- ${reason}`);this.name="InvalidRepoContractConfigError";}};var InvalidCheckConfigError=class extends RepoContractError{code="REPO_CONTRACT_INVALID_CHECK_CONFIG";checkId;constructor(checkId,reason){super(`Invalid check config for "${checkId}" -- ${reason}`);this.name="InvalidCheckConfigError";this.checkId=checkId;}};var UnknownCheckIdError=class extends RepoContractError{code="REPO_CONTRACT_UNKNOWN_CHECK_ID";checkId;constructor(checkId){super(`options.checks names "${checkId}", which is not a check id in this config's checks object.`);this.name="UnknownCheckIdError";this.checkId=checkId;}};var DependencyDeclaredLaterError=class extends RepoContractError{code="REPO_CONTRACT_DEPENDENCY_DECLARED_LATER";checkId;dependencyId;constructor(checkId,dependencyId){super(`Invalid check config for "${checkId}" -- dependsOn: ["${dependencyId}"], but "${dependencyId}" is declared later in the checks object. dependsOn may only reference a check declared earlier -- reorder the checks object so "${dependencyId}" is declared before "${checkId}".`);this.name="DependencyDeclaredLaterError";this.checkId=checkId;this.dependencyId=dependencyId;}};var ParserDependencyMissingError=class extends RepoContractError{code="REPO_CONTRACT_PARSER_DEPENDENCY_MISSING";checkId;format;constructor(checkId,format,cause){super(`Check "${checkId}" requested output.format: "${format}", but the optional "${format}" peer dependency is not installed -- run \`npm install ${format}\` to enable it.`,{cause});this.name="ParserDependencyMissingError";this.checkId=checkId;this.format=format;}};var PolicyThrewError=class extends RepoContractError{code="REPO_CONTRACT_POLICY_THREW";checkId;constructor(checkId,cause){super(`Policy for check "${checkId}" threw instead of returning a PolicyResult`,{cause});this.name="PolicyThrewError";this.checkId=checkId;}};var PolicyReadUnrequestedOutputError=class extends RepoContractError{code="REPO_CONTRACT_POLICY_READ_UNREQUESTED_OUTPUT";checkId;constructor(checkId,property,cause){super(`Policy for check "${checkId}" read \`result.output.${property}\`, but "${checkId}" never requested an output format, so \`result.output\` is undefined -- add \`output: { format: "json" }\` (or "yaml"/"text") to check "${checkId}"'s definition to parse its stdout, then narrow with \`result.output?.success\` before reading \`.value\`/\`.error\`.`,{cause});this.name="PolicyReadUnrequestedOutputError";this.checkId=checkId;}};var PolicyReadFailedParseValueError=class extends RepoContractError{code="REPO_CONTRACT_POLICY_READ_FAILED_PARSE_VALUE";checkId;constructor(checkId,property,cause){super(`Policy for check "${checkId}" read \`result.output.value.${property}\`, but "${checkId}"'s output failed to parse -- a failed parse has \`result.output.error\`, never \`.value\` -- check \`result.output.success\` before reading \`.value\`.`,{cause});this.name="PolicyReadFailedParseValueError";this.checkId=checkId;}};function consumeEscape(run,i,quote){if(run[i]!=="\\"||quote==="'")return void 0;const escaped=run[i+1];if(escaped===void 0)return void 0;return {value:escaped,next:i+2}}var UNQUOTED_SHELL_OPERATORS=new Set([";","&","|","`","<",">"]);function rejectUnquotedOperator(char,run,i,checkId){const reject=operator=>{throw new InvalidCheckConfigError(checkId,`run string contains an unquoted "${operator}" -- repo-contract never invokes a shell for string-form "run", so shell operators are not interpreted. Use "run: [...]" (array form) to pass "${operator}" as a literal argument, or set "shell: true" to opt into real shell execution.`)};if(char==="\n"||char==="\r")reject("newline");if(UNQUOTED_SHELL_OPERATORS.has(char))reject(char);if(char==="$"&&run[i+1]==="(")reject("$(");}function tokenizeRunString(run,checkId){const tokens=[];let current="";let hasCurrent=false;let quote=null;let i=0;for(let iterations=0;i<run.length&&iterations<=run.length;iterations+=1){const char=run[i];if(char===void 0)break;if(quote!==null){const escape2=consumeEscape(run,i,quote);if(escape2!==void 0){current+=escape2.value;i=escape2.next;continue}if(char===quote){quote=null;i+=1;continue}current+=char;i+=1;continue}if(char==="'"||char==='"'){quote=char;hasCurrent=true;i+=1;continue}const nextChar=run[i+1];if(char==="\\"&&(nextChar==="\n"||nextChar==="\r")){throw new InvalidCheckConfigError(checkId,`run string contains an unquoted line continuation (a backslash before a newline) -- repo-contract never invokes a shell for string-form "run". Use "run: [...]" (array form), or set "shell: true" to opt into real shell execution.`)}const escape=consumeEscape(run,i,null);if(escape!==void 0){current+=escape.value;hasCurrent=true;i=escape.next;continue}if(char===" "||char===" "){if(hasCurrent){tokens.push(current);current="";hasCurrent=false;}i+=1;continue}rejectUnquotedOperator(char,run,i,checkId);current+=char;hasCurrent=true;i+=1;}if(quote!==null){throw new InvalidCheckConfigError(checkId,`run string has an unterminated ${quote} quote.`)}if(hasCurrent)tokens.push(current);if(tokens.length===0){throw new InvalidCheckConfigError(checkId,"run string is empty or contains only whitespace.")}return tokens}var OUTPUT_FORMATS=["json","yaml","text"];function validateRepoContractConfig(config){const untrusted=config;if(untrusted===null||typeof untrusted!=="object"){throw new InvalidRepoContractConfigError("config must be an object.")}const{checks,concurrency}=untrusted;if(checks===null||typeof checks!=="object"||Array.isArray(checks)){throw new InvalidRepoContractConfigError("checks must be an object mapping check id to check definition.")}if(concurrency!==void 0){if(typeof concurrency!=="number"||!Number.isInteger(concurrency)||concurrency<1){throw new InvalidRepoContractConfigError("concurrency must be a positive integer when provided.")}}for(const[checkId,check]of Object.entries(checks)){validateCheckDefinition(checkId,check);}validateDependencyGraph(checks);}function validateCheckDefinition(checkId,check){if(/^(?:0|[1-9]\d*)$/.test(checkId)){throw new InvalidCheckConfigError(checkId,"check id must not be an integer-like string -- JavaScript would reorder it ahead of every other key and break the declaration-order-is-topological-order contract.")}if(check===null||typeof check!=="object"){throw new InvalidCheckConfigError(checkId,"check definition must be an object.")}const fields=check;const usesShell=validateShell(checkId,fields.shell);validateRun(checkId,fields.run,usesShell);validateCwd(checkId,fields.cwd);validateEnv(checkId,fields.env);validateInheritEnv(checkId,fields.inheritEnv);validateTimeoutMs(checkId,fields.timeoutMs);validateOutput(checkId,fields.output);validateDependsOn(checkId,fields.dependsOn);validateIsolated(checkId,fields.isolated);validatePolicy(checkId,fields.policy);}function validateShell(checkId,shell){if(shell!==void 0&&typeof shell!=="boolean"){throw new InvalidCheckConfigError(checkId,"shell must be a boolean when provided.")}return shell===true}function validateRun(checkId,run,usesShell){if(typeof run!=="string"&&!Array.isArray(run)){throw new InvalidCheckConfigError(checkId,"run must be a string or an array of strings.")}if(typeof run==="string"){validateStringRun(checkId,run,usesShell);}else {validateArrayRun(checkId,run,usesShell);}}function validateStringRun(checkId,run,usesShell){if(!usesShell){const[executable]=tokenizeRunString(run,checkId);if(executable?.trim().length===0){throw new InvalidCheckConfigError(checkId,"run string's first token (the executable) is empty or contains only whitespace.")}return}if(run.trim().length===0){throw new InvalidCheckConfigError(checkId,"run string is empty or contains only whitespace.")}}function validateArrayRun(checkId,items,usesShell){if(usesShell){throw new InvalidCheckConfigError(checkId,"shell: true requires run to be a string -- an array of arguments is individually escaped for the shell and cannot express shell operators like pipes or redirects, so combining the two forms would silently do nothing useful.")}if(items.length===0){throw new InvalidCheckConfigError(checkId,"run array must not be empty.")}if(items.some(item=>typeof item!=="string")){throw new InvalidCheckConfigError(checkId,"run array must contain only strings.")}if(items[0].trim().length===0){throw new InvalidCheckConfigError(checkId,"run array's first element (the executable) is empty or contains only whitespace.")}}function validateCwd(checkId,cwd){if(cwd!==void 0&&typeof cwd!=="string"){throw new InvalidCheckConfigError(checkId,"cwd must be a string when provided.")}}function validateEnv(checkId,env){if(env===void 0)return;if(env===null||typeof env!=="object"||Array.isArray(env)){throw new InvalidCheckConfigError(checkId,"env must be an object mapping name to value when provided.")}for(const[key,value]of Object.entries(env)){if(typeof value!=="string"){throw new InvalidCheckConfigError(checkId,`env["${key}"] must be a string.`)}}}function validateInheritEnv(checkId,inheritEnv){if(inheritEnv!==void 0&&typeof inheritEnv!=="boolean"){throw new InvalidCheckConfigError(checkId,"inheritEnv must be a boolean when provided.")}}function validateTimeoutMs(checkId,timeoutMs){if(timeoutMs===void 0)return;if(typeof timeoutMs!=="number"||!Number.isFinite(timeoutMs)||timeoutMs<=0){throw new InvalidCheckConfigError(checkId,"timeoutMs must be a positive number when provided.")}}function validateOutput(checkId,output){if(output===void 0)return;if(output===null||typeof output!=="object"){throw new InvalidCheckConfigError(checkId,"output must be an object when provided.")}const{format}=output;if(typeof format!=="string"||!OUTPUT_FORMATS.includes(format)){throw new InvalidCheckConfigError(checkId,`output.format must be one of ${OUTPUT_FORMATS.map(f=>`"${f}"`).join(", ")}.`)}}function validatePolicy(checkId,policy){if(typeof policy!=="function"){throw new InvalidCheckConfigError(checkId,"policy must be a function.")}}function validateDependsOn(checkId,dependsOn){if(dependsOn===void 0)return;if(!Array.isArray(dependsOn)||dependsOn.some(id=>typeof id!=="string")){throw new InvalidCheckConfigError(checkId,"dependsOn must be an array of check ids (strings) when provided.")}if(dependsOn.includes(checkId)){throw new InvalidCheckConfigError(checkId,"dependsOn must not include the check's own id.")}}function validateIsolated(checkId,isolated){if(isolated!==void 0&&typeof isolated!=="boolean"){throw new InvalidCheckConfigError(checkId,"isolated must be a boolean when provided.")}}function validateDependencyGraph(checks){const ids=Object.keys(checks);const indexById=new Map(ids.map((id,index)=>[id,index]));for(const[index,id]of ids.entries()){const check=checks[id];for(const depId of check?.dependsOn??[]){const depIndex=indexById.get(depId);if(depIndex===void 0){throw new InvalidCheckConfigError(id,`dependsOn references unknown check id "${depId}".`)}if(depIndex>=index){throw new DependencyDeclaredLaterError(id,depId)}}}}function parseJson(stdout){try{return {format:"json",success:true,value:JSON.parse(stdout)}}catch(error){return {format:"json",success:false,error:error instanceof Error?error.message:String(error)}}}function parseText(stdout){return {format:"text",success:true,value:stdout.trim()}}async function parseYaml(stdout,checkId){let yamlModule;try{yamlModule=await import('yaml');}catch(error){throw new ParserDependencyMissingError(checkId,"yaml",error)}try{return {format:"yaml",success:true,value:yamlModule.parse(stdout)}}catch(error){return {format:"yaml",success:false,error:error instanceof Error?error.message:String(error)}}}async function parseOutput(format,stdout,checkId){switch(format){case "json":return parseJson(stdout);case "yaml":return parseYaml(stdout,checkId);case "text":return parseText(stdout)}}async function buildEvidence(results,startedAt,completedAt){const thrown=[];const entries=await Promise.all(results.map(async([checkId,check,raw])=>{if(check.output===void 0)return [checkId,check,raw];try{const output=await parseOutput(check.output.format,raw.stdout,checkId);return [checkId,check,{...raw,output}]}catch(error){thrown.push(error);return [checkId,check,raw]}}));if(thrown.length===1){const[only]=thrown;throw only}if(thrown.length>1){throw new AggregateError(thrown,`${String(thrown.length)} check output(s) failed to parse.`)}const evidence={version:1,startedAt:startedAt.toISOString(),completedAt:completedAt.toISOString(),durationMs:completedAt.getTime()-startedAt.getTime(),checks:Object.fromEntries(entries.map(([checkId,,checkEvidence])=>[checkId,checkEvidence]))};return {evidence,entries}}function composeSignals(signals){if(typeof AbortSignal.any==="function"){return {signal:AbortSignal.any(signals),dispose:()=>void 0}}const controller=new AbortController;const attached=[];for(const signal of signals){if(signal.aborted){controller.abort(signal.reason);break}const listener=()=>{controller.abort(signal.reason);};signal.addEventListener("abort",listener);attached.push({signal,listener});}return {signal:controller.signal,dispose:()=>{for(const{signal,listener}of attached)signal.removeEventListener("abort",listener);}}}async function runWithConcurrency(items,concurrency,worker){const results=[];const entries=items.map((item,index)=>[item,index]);const iterator=entries[Symbol.iterator]();let firstRejection;async function runNext(){for(let next=iterator.next();!next.done;next=iterator.next()){if(firstRejection!==void 0)return;const[item,index]=next.value;try{results[index]=await worker(item,index);}catch(error){firstRejection??={error};return}}}const effectiveConcurrency=Number.isFinite(concurrency)?concurrency:1;const workerCount=Math.max(1,Math.min(effectiveConcurrency,items.length));await Promise.all(Array.from({length:workerCount},()=>runNext()));if(firstRejection!==void 0){throw firstRejection.error}return results}async function runWithConcurrencyGraph(items,concurrency,dependencyIndexes,worker){if(items.length===0)return [];const dependencies=items.map((item,index)=>[...new Set(dependencyIndexes(item,index))].filter(depIndex=>Number.isInteger(depIndex)&&depIndex>=0&&depIndex<items.length));const remaining=dependencies.map(indexes=>indexes.length);const dependents=items.map(()=>[]);dependencies.forEach((indexes,index)=>{for(const depIndex of indexes){dependents[depIndex]?.push(index);}});const ready=remaining.flatMap((count,index)=>count===0?[index]:[]);const concurrencyIsUsable=Number.isFinite(concurrency);const results=Array.from({length:items.length});const effectiveConcurrency=concurrencyIsUsable?Math.max(1,concurrency):1;let active=0;let settled=0;let done=false;return new Promise((resolvePromise,rejectPromise)=>{if(ready.length===0){fail(new Error("runWithConcurrencyGraph: stalled before starting -- the dependency graph passed in is not acyclic."));return}function fail(error){if(done)return;done=true;rejectPromise(error);}function launchNext(){while(active<effectiveConcurrency&&ready.length>0){const index=ready.shift();if(index===void 0)break;const item=items[index];if(item===void 0)continue;active+=1;worker(item,index).then(result=>{results[index]=result;active-=1;settled+=1;for(const dependentIndex of dependents[index]??[]){const nextRemaining=(remaining[dependentIndex]??0)-1;remaining[dependentIndex]=nextRemaining;if(nextRemaining===0)ready.push(dependentIndex);}if(done)return;if(settled===items.length){resolvePromise(results);return}if(active===0&&ready.length===0){fail(new Error("runWithConcurrencyGraph: stalled with unsettled items remaining -- the dependency graph passed in is not acyclic."));return}launchNext();}).catch(fail);}}launchNext();})}function shouldSpawnDetached(){return process.platform!=="win32"}function isErrnoException(error){return error instanceof Error&&"code"in error}function killTree(pid,signal){if(!Number.isInteger(pid)||pid<=0){return}if(process.platform==="win32"){const result=child_process.spawnSync("taskkill",["/pid",String(pid),"/t","/f"],{stdio:"ignore"});if(result.error!==void 0)throw result.error;return}try{process.kill(-pid,signal);}catch(error){if(isErrnoException(error)&&(error.code==="ESRCH"||error.code==="EPERM"))return;throw error}}var MAX_CAPTURED_OUTPUT_BYTES=10*1024*1024;var SIGKILL_GRACE_PERIOD_MS=2e3;function createBoundedCollector(){const decoder=new string_decoder.StringDecoder("utf8");let value="";let byteCount=0;let truncated=false;return {append(chunk){if(truncated)return;byteCount+=chunk.byteLength;value+=decoder.write(chunk);if(byteCount>MAX_CAPTURED_OUTPUT_BYTES){truncated=true;value=`${value.slice(0,MAX_CAPTURED_OUTPUT_BYTES)}
|
|
32
|
+
...[output truncated at ${String(MAX_CAPTURED_OUTPUT_BYTES)} bytes]`;}},value:()=>value}}function resolveCommand(checkId,check){const run=check.run;if(typeof run!=="string"){const[command2,...args2]=run;if(command2===void 0){throw new InvalidCheckConfigError(checkId,"run array must not be empty.")}return {command:command2,args:args2}}if(check.shell===true){return {command:run,args:[]}}const[command,...args]=tokenizeRunString(run,checkId);if(command===void 0){throw new InvalidCheckConfigError(checkId,"run string is empty or contains only whitespace.")}return {command,args}}function buildEnv(check){const base={};if(check.inheritEnv!==false){for(const[key,value]of Object.entries(process.env)){if(value!==void 0)base[key]=value;}}return {...base,...check.env}}function terminalEvidence(command,args,startedAt,status,exitCode,signal,stdout="",stderr="",spawnError,spawnErrorCode){const completedAt=new Date;return {command,args,startedAt:startedAt.toISOString(),completedAt:completedAt.toISOString(),durationMs:completedAt.getTime()-startedAt.getTime(),exitCode,signal,stdout,stderr,status,...spawnError!==void 0?{spawnError}:{},...spawnErrorCode!==void 0?{spawnErrorCode}:{}}}async function spawnCheck(checkId,check,runSignal,activeHandles){const startedAt=new Date;const{command,args}=resolveCommand(checkId,check);if(runSignal?.aborted===true){return terminalEvidence(command,args,startedAt,"aborted",null,null)}const env=buildEnv(check);let terminationReason=null;let timeoutHandle;const timeoutController=new AbortController;if(check.timeoutMs!==void 0){const MAX_TIMER_DELAY_MS=2147483647;const delay=Math.min(check.timeoutMs,MAX_TIMER_DELAY_MS);timeoutHandle=setTimeout(()=>{timeoutController.abort();},delay);}const{signal:effectiveSignal,dispose:disposeEffectiveSignal}=composeSignals(runSignal!==void 0?[runSignal,timeoutController.signal]:[timeoutController.signal]);return new Promise(resolve=>{const child=crossSpawn__default.default(command,args,{cwd:check.cwd,env,shell:check.shell===true,detached:shouldSpawnDetached(),windowsHide:true});const stdoutCollector=createBoundedCollector();const stderrCollector=createBoundedCollector();let handle;let escalationHandle;let hostTerminated=false;const bestEffortKillTree=(pid,signal)=>{try{killTree(pid,signal);}catch{}};const killWithEscalation=(pid,signal)=>{bestEffortKillTree(pid,signal);clearTimeout(escalationHandle);if(signal==="SIGKILL")return;escalationHandle=setTimeout(()=>{bestEffortKillTree(pid,"SIGKILL");},SIGKILL_GRACE_PERIOD_MS);};if(child.pid!==void 0){const pid=child.pid;handle={kill:signal=>{hostTerminated=true;killWithEscalation(pid,signal);}};activeHandles.add(handle);}child.stdout?.on("data",chunk=>{stdoutCollector.append(chunk);});child.stderr?.on("data",chunk=>{stderrCollector.append(chunk);});const onEffectiveAbort=()=>{terminationReason=runSignal?.aborted===true?"aborted":"timed_out";if(child.pid!==void 0)killWithEscalation(child.pid,"SIGTERM");};effectiveSignal.addEventListener("abort",onEffectiveAbort);const cleanup=()=>{clearTimeout(timeoutHandle);clearTimeout(escalationHandle);effectiveSignal.removeEventListener("abort",onEffectiveAbort);disposeEffectiveSignal();if(handle!==void 0)activeHandles.delete(handle);};child.once("error",error=>{cleanup();resolve(terminalEvidence(command,args,startedAt,"spawn_error",null,null,stdoutCollector.value(),stderrCollector.value(),error.message,error.code));});child.once("close",(code,signal)=>{cleanup();const status=hostTerminated?"host_terminated":terminationReason==="aborted"?"aborted":terminationReason==="timed_out"?"timed_out":code===null&&signal!==null?"signaled":"completed";resolve(terminalEvidence(command,args,startedAt,status,code,signal,stdoutCollector.value(),stderrCollector.value()));});})}var TERMINATION_SIGNALS=["SIGINT","SIGTERM"];var SELF_TERMINATE_DELAY_MS=SIGKILL_GRACE_PERIOD_MS+250;function installTerminationHandlers(activeHandles,hostAbortController){const handlers=new Map;for(const signal of TERMINATION_SIGNALS){const handler=()=>{hostAbortController.abort();const hadActiveChecks=activeHandles.size>0;for(const handle of activeHandles)handle.kill(signal);uninstall();if(!hadActiveChecks){process.kill(process.pid,signal);return}setTimeout(()=>{process.kill(process.pid,signal);},SELF_TERMINATE_DELAY_MS);};handlers.set(signal,handler);process.once(signal,handler);}function uninstall(){for(const[signal,handler]of handlers)process.removeListener(signal,handler);}return uninstall}function resolveCheckDependencies(checks,requestedChecks){const required=new Set;const visit=checkId=>{if(required.has(checkId))return;const check=checks[checkId];if(!check){throw new UnknownCheckIdError(checkId)}required.add(checkId);for(const dependency of check.dependsOn??[]){visit(dependency);}};for(const checkId of requestedChecks)visit(checkId);return Object.entries(checks).filter(([checkId])=>required.has(checkId))}async function runChecks(checks,concurrency,options){const entries=options?.checks?resolveCheckDependencies(checks,options.checks):Object.entries(checks);const activeHandles=new Set;const hostAbortController=new AbortController;const uninstall=installTerminationHandlers(activeHandles,hostAbortController);const{signal:runSignal,dispose:disposeRunSignal}=composeSignals(options?.signal!==void 0?[options.signal,hostAbortController.signal]:[hostAbortController.signal]);const worker=async([checkId,check])=>{const evidence=await spawnCheck(checkId,check,runSignal,activeHandles);return [checkId,check,evidence]};try{const hasDependencies=entries.some(([,check])=>(check.dependsOn?.length??0)>0||check.isolated===true);if(!hasDependencies){return await runWithConcurrency(entries,concurrency,worker)}const indexById=new Map(entries.map(([checkId],index)=>[checkId,index]));const isolatedIndexes=[];for(const[entryIndex,[,entryCheck]]of entries.entries()){if(entryCheck.isolated===true)isolatedIndexes.push(entryIndex);}const dependencyIndexesFor=([,check],index)=>{const declared=(check.dependsOn??[]).map(depId=>{const depIndex=indexById.get(depId);if(depIndex===void 0){throw new Error(`internal: dependsOn references unknown check id "${depId}".`)}return depIndex});if(check.isolated===true){const earlierIndexes=Array.from({length:index},(_,earlierIndex)=>earlierIndex);return [...new Set([...declared,...earlierIndexes])]}const earlierIsolated=isolatedIndexes.filter(isolatedIndex=>isolatedIndex<index);return [...new Set([...declared,...earlierIsolated])]};return await runWithConcurrencyGraph(entries,concurrency,dependencyIndexesFor,worker)}finally{uninstall();disposeRunSignal();}}var POLICY_OUTCOMES=["pass","fail","warn"];var OUTPUT_PROPERTIES=new Set(["success","value","error","format"]);function readPropertyOfUndefined(error){if(!(error instanceof TypeError))return void 0;const match=/^Cannot read properties of undefined \(reading '([^']+)'\)$/.exec(error.message);return match?.[1]}function unrequestedOutputProperty(error){const property=readPropertyOfUndefined(error);return property!==void 0&&OUTPUT_PROPERTIES.has(property)?property:void 0}function wrapPolicyFailure(checkId,output,error){if(output===void 0){const property=unrequestedOutputProperty(error);if(property!==void 0){return new PolicyReadUnrequestedOutputError(checkId,property,error)}}else if(!output.success){const property=readPropertyOfUndefined(error);if(property!==void 0){return new PolicyReadFailedParseValueError(checkId,property,error)}}return new PolicyThrewError(checkId,error)}function invalidPolicyResultReason(value){if(typeof value!=="object"||value===null){return `expected an object with "outcome" and "rationale", got ${value===null?"null":typeof value}`}const outcome=value.outcome;if(!POLICY_OUTCOMES.includes(outcome)){return `"outcome" must be "pass", "fail", or "warn", got ${typeof outcome==="string"?`"${outcome}"`:typeof outcome}`}const rationale=value.rationale;if(typeof rationale!=="string"){return `"rationale" must be a string, got ${typeof rationale}`}return void 0}async function runPolicies(entries,evidence){const thrown=[];const checkResults=[];await Promise.all(entries.map(async([checkId,check,checkEvidence],entryIndex)=>{const dependencies={};for(const depId of check.dependsOn??[]){const depEvidence=evidence.checks[depId];if(depEvidence!==void 0)dependencies[depId]=depEvidence;}let outcome;try{const rawResult=await check.policy({result:checkEvidence,evidence,dependencies});const invalidReason=invalidPolicyResultReason(rawResult);if(invalidReason!==void 0){throw new Error(`Policy for check "${checkId}" returned an invalid PolicyResult: ${invalidReason}.`)}outcome=rawResult;}catch(error){thrown.push(wrapPolicyFailure(checkId,checkEvidence.output,error));return}checkResults[entryIndex]=[checkId,outcome];}));if(thrown.length===1){const[only]=thrown;throw only}if(thrown.length>1){throw new AggregateError(thrown,`${String(thrown.length)} check policies threw instead of returning a PolicyResult.`)}const orderedResults=checkResults;const checks=Object.fromEntries(orderedResults);const passed=orderedResults.every(([,result])=>result.outcome!=="fail");return {version:2,passed,checks}}function runRepoContract(config,options){validateRepoContractConfig(config);return runRepoContractAfterValidation(config,options)}async function runRepoContractAfterValidation(config,options){const concurrency=config.concurrency??os__namespace.availableParallelism();const startedAt=new Date;const results=await runChecks(config.checks,concurrency,options);const completedAt=new Date;const{evidence,entries}=await buildEvidence(results,startedAt,completedAt);const verdict=await runPolicies(entries,evidence);return {evidence,verdict}}
|
|
33
|
+
|
|
34
|
+
exports.DependencyDeclaredLaterError = DependencyDeclaredLaterError;
|
|
35
|
+
exports.InvalidCheckConfigError = InvalidCheckConfigError;
|
|
36
|
+
exports.InvalidRepoContractConfigError = InvalidRepoContractConfigError;
|
|
37
|
+
exports.ParserDependencyMissingError = ParserDependencyMissingError;
|
|
38
|
+
exports.PolicyReadFailedParseValueError = PolicyReadFailedParseValueError;
|
|
39
|
+
exports.PolicyReadUnrequestedOutputError = PolicyReadUnrequestedOutputError;
|
|
40
|
+
exports.PolicyThrewError = PolicyThrewError;
|
|
41
|
+
exports.RepoContractError = RepoContractError;
|
|
42
|
+
exports.UnknownCheckIdError = UnknownCheckIdError;
|
|
43
|
+
exports.defineRepoContract = defineRepoContract;
|
|
44
|
+
exports.runRepoContract = runRepoContract;
|
|
45
|
+
//# sourceMappingURL=index.cjs.map
|
|
46
|
+
//# sourceMappingURL=index.cjs.map
|