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,368 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DependencyDeclaredLaterError,
|
|
3
|
+
InvalidCheckConfigError,
|
|
4
|
+
InvalidRepoContractConfigError,
|
|
5
|
+
} from "../errors.js"
|
|
6
|
+
import type { OutputFormat, RepoContractConfig } from "../types.js"
|
|
7
|
+
import { tokenizeRunString } from "./tokenize-command.js"
|
|
8
|
+
|
|
9
|
+
const OUTPUT_FORMATS: readonly OutputFormat[] = ["json", "yaml", "text"]
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validates a `RepoContractConfig` structurally and throws before any
|
|
13
|
+
* process spawns. A config with zero checks is valid (an empty `checks`
|
|
14
|
+
* object vacuously satisfies "every check passed"); everything else here is
|
|
15
|
+
* a genuine structural problem. Runtime checks are deliberately defensive
|
|
16
|
+
* about field types rather than trusting the compile-time type, since a
|
|
17
|
+
* config can arrive from plain JavaScript or a widened/cast value.
|
|
18
|
+
* @param config - the config to validate; throws if structurally invalid.
|
|
19
|
+
*/
|
|
20
|
+
export function validateRepoContractConfig(config: RepoContractConfig): void {
|
|
21
|
+
// Typed as `unknown` at this internal boundary (not the declared
|
|
22
|
+
// `RepoContractConfig` parameter type) deliberately -- this function's
|
|
23
|
+
// whole purpose is verifying something whose actual runtime shape isn't
|
|
24
|
+
// trusted (plain JavaScript callers, a widened or cast value), so
|
|
25
|
+
// narrowing from `unknown` keeps every check below genuinely meaningful
|
|
26
|
+
// rather than flagged as redundant against a type that's assumed valid.
|
|
27
|
+
const untrusted: unknown = config
|
|
28
|
+
|
|
29
|
+
if (untrusted === null || typeof untrusted !== "object") {
|
|
30
|
+
throw new InvalidRepoContractConfigError("config must be an object.")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { checks, concurrency } = untrusted as Record<string, unknown>
|
|
34
|
+
|
|
35
|
+
if (checks === null || typeof checks !== "object" || Array.isArray(checks)) {
|
|
36
|
+
throw new InvalidRepoContractConfigError(
|
|
37
|
+
"checks must be an object mapping check id to check definition.",
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (concurrency !== undefined) {
|
|
42
|
+
// `typeof concurrency !== "number"` is behaviorally redundant with the
|
|
43
|
+
// clause after it: `Number.isInteger` returns `false` (never throws or
|
|
44
|
+
// coerces) for every non-number value, so `!Number.isInteger(...)`
|
|
45
|
+
// alone already rejects every case the typeof check would -- kept for
|
|
46
|
+
// readability at the call site, not because it changes behavior.
|
|
47
|
+
// Stryker disable next-line ConditionalExpression -- the typeof check is redundant with Number.isInteger, which returns false (never throws) for every non-number value, so it's kept only for readability at the call site.
|
|
48
|
+
if (typeof concurrency !== "number" || !Number.isInteger(concurrency) || concurrency < 1) {
|
|
49
|
+
throw new InvalidRepoContractConfigError(
|
|
50
|
+
"concurrency must be a positive integer when provided.",
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const [checkId, check] of Object.entries(checks)) {
|
|
56
|
+
validateCheckDefinition(checkId, check)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
validateDependencyGraph(checks as Record<string, { dependsOn?: readonly string[] }>)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Orchestrates one check's field-level validators. Deliberately just a flat
|
|
64
|
+
* sequence of calls with no branching of its own -- each field's validation
|
|
65
|
+
* logic (and its own complexity) lives in its own small function below, so
|
|
66
|
+
* this function's own cyclomatic complexity (and CRAP score) stays low
|
|
67
|
+
* regardless of how many fields exist to check.
|
|
68
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages.
|
|
69
|
+
* @param check - the check definition to validate.
|
|
70
|
+
*/
|
|
71
|
+
function validateCheckDefinition(checkId: string, check: unknown): void {
|
|
72
|
+
// An integer-like key ("0", "42", ...) is enumerated by `Object.keys` in
|
|
73
|
+
// ascending numeric order ahead of every other key, regardless of insertion
|
|
74
|
+
// order -- so `validateDependencyGraph`'s `ids`/`indexById` (and the runtime
|
|
75
|
+
// scheduler) would see a different order than the source declares, silently
|
|
76
|
+
// breaking the "declaration order is the required topological order" contract.
|
|
77
|
+
if (/^(?:0|[1-9]\d*)$/.test(checkId)) {
|
|
78
|
+
throw new InvalidCheckConfigError(
|
|
79
|
+
checkId,
|
|
80
|
+
"check id must not be an integer-like string -- JavaScript would reorder it ahead of every " +
|
|
81
|
+
"other key and break the declaration-order-is-topological-order contract.",
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (check === null || typeof check !== "object") {
|
|
86
|
+
throw new InvalidCheckConfigError(checkId, "check definition must be an object.")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const fields = check as Record<string, unknown>
|
|
90
|
+
const usesShell = validateShell(checkId, fields.shell)
|
|
91
|
+
validateRun(checkId, fields.run, usesShell)
|
|
92
|
+
validateCwd(checkId, fields.cwd)
|
|
93
|
+
validateEnv(checkId, fields.env)
|
|
94
|
+
validateInheritEnv(checkId, fields.inheritEnv)
|
|
95
|
+
validateTimeoutMs(checkId, fields.timeoutMs)
|
|
96
|
+
validateOutput(checkId, fields.output)
|
|
97
|
+
validateDependsOn(checkId, fields.dependsOn)
|
|
98
|
+
validateIsolated(checkId, fields.isolated)
|
|
99
|
+
validatePolicy(checkId, fields.policy)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns whether `shell: true` was set, having already validated `shell`'s own type.
|
|
104
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages.
|
|
105
|
+
* @param shell - the check's raw `shell` field to validate.
|
|
106
|
+
* @returns `true` if `shell` was set to `true`, `false` otherwise (including when omitted).
|
|
107
|
+
*/
|
|
108
|
+
function validateShell(checkId: string, shell: unknown): boolean {
|
|
109
|
+
if (shell !== undefined && typeof shell !== "boolean") {
|
|
110
|
+
throw new InvalidCheckConfigError(checkId, "shell must be a boolean when provided.")
|
|
111
|
+
}
|
|
112
|
+
return shell === true
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Validates that `run` is a string or an array of strings, then delegates to the matching shape-specific validator.
|
|
117
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages.
|
|
118
|
+
* @param run - the check's raw `run` field to validate.
|
|
119
|
+
* @param usesShell - whether `shell: true` was set, from `validateShell`.
|
|
120
|
+
*/
|
|
121
|
+
function validateRun(checkId: string, run: unknown, usesShell: boolean): void {
|
|
122
|
+
if (typeof run !== "string" && !Array.isArray(run)) {
|
|
123
|
+
throw new InvalidCheckConfigError(checkId, "run must be a string or an array of strings.")
|
|
124
|
+
}
|
|
125
|
+
if (typeof run === "string") {
|
|
126
|
+
validateStringRun(checkId, run, usesShell)
|
|
127
|
+
} else {
|
|
128
|
+
validateArrayRun(checkId, run as unknown[], usesShell)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Validates a string-form `run`: tokenizes it (for its side effect of rejecting unquoted shell operators) when no shell is used, or rejects an empty/whitespace-only string when a shell is used.
|
|
134
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages.
|
|
135
|
+
* @param run - the check's raw `run` string to validate.
|
|
136
|
+
* @param usesShell - whether `shell: true` was set, from `validateShell`.
|
|
137
|
+
*/
|
|
138
|
+
function validateStringRun(checkId: string, run: string, usesShell: boolean): void {
|
|
139
|
+
if (!usesShell) {
|
|
140
|
+
// Tokenized here for its side effect (throws on an empty string or an
|
|
141
|
+
// unquoted shell operator); the result is recomputed at spawn time,
|
|
142
|
+
// since tokenization is cheap and pure. The first token is additionally
|
|
143
|
+
// checked for emptiness: `run: "''"` / `run: "' ' build"` tokenizes to
|
|
144
|
+
// a non-empty array whose executable is `""`, which would otherwise fail
|
|
145
|
+
// only later as an opaque spawn error rather than a synchronous config
|
|
146
|
+
// error like every other structurally-broken `run`.
|
|
147
|
+
const [executable] = tokenizeRunString(run, checkId)
|
|
148
|
+
// `noUncheckedIndexedAccess` types the destructured `executable` as
|
|
149
|
+
// `string | undefined`, but `tokenizeRunString` throws on any input that
|
|
150
|
+
// would produce an empty token array (it rejects an empty or
|
|
151
|
+
// whitespace-only string outright), so `executable` is always a real
|
|
152
|
+
// string by the time control reaches here. The `?.` exists only to
|
|
153
|
+
// satisfy the compiler; removing it is behaviourally equivalent, so the
|
|
154
|
+
// OptionalChaining mutant here is an equivalent mutant with no test that
|
|
155
|
+
// could ever distinguish it.
|
|
156
|
+
// Stryker disable next-line OptionalChaining -- equivalent mutant: `executable` is provably always a string here (see comment above), so `executable?.trim()` and `executable.trim()` are identical.
|
|
157
|
+
if (executable?.trim().length === 0) {
|
|
158
|
+
throw new InvalidCheckConfigError(
|
|
159
|
+
checkId,
|
|
160
|
+
"run string's first token (the executable) is empty or contains only whitespace.",
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
if (run.trim().length === 0) {
|
|
166
|
+
throw new InvalidCheckConfigError(checkId, "run string is empty or contains only whitespace.")
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Validates an array-form `run`: rejects it outright when a shell is used (array args can't express shell operators), and otherwise requires a non-empty array of strings.
|
|
172
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages.
|
|
173
|
+
* @param items - the check's raw `run` array to validate.
|
|
174
|
+
* @param usesShell - whether `shell: true` was set, from `validateShell`.
|
|
175
|
+
*/
|
|
176
|
+
function validateArrayRun(checkId: string, items: readonly unknown[], usesShell: boolean): void {
|
|
177
|
+
if (usesShell) {
|
|
178
|
+
throw new InvalidCheckConfigError(
|
|
179
|
+
checkId,
|
|
180
|
+
"shell: true requires run to be a string -- an array of arguments is individually " +
|
|
181
|
+
"escaped for the shell and cannot express shell operators like pipes or redirects, " +
|
|
182
|
+
"so combining the two forms would silently do nothing useful.",
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
if (items.length === 0) {
|
|
186
|
+
throw new InvalidCheckConfigError(checkId, "run array must not be empty.")
|
|
187
|
+
}
|
|
188
|
+
if (items.some((item) => typeof item !== "string")) {
|
|
189
|
+
throw new InvalidCheckConfigError(checkId, "run array must contain only strings.")
|
|
190
|
+
}
|
|
191
|
+
// The first element is the executable; an empty or whitespace-only value
|
|
192
|
+
// there fails only later as an opaque spawn error, so reject it here like
|
|
193
|
+
// every other structurally-broken `run`.
|
|
194
|
+
if ((items[0] as string).trim().length === 0) {
|
|
195
|
+
throw new InvalidCheckConfigError(
|
|
196
|
+
checkId,
|
|
197
|
+
"run array's first element (the executable) is empty or contains only whitespace.",
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Validates that `cwd`, if provided, is a string.
|
|
204
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages.
|
|
205
|
+
* @param cwd - the check's raw `cwd` field to validate.
|
|
206
|
+
*/
|
|
207
|
+
function validateCwd(checkId: string, cwd: unknown): void {
|
|
208
|
+
if (cwd !== undefined && typeof cwd !== "string") {
|
|
209
|
+
throw new InvalidCheckConfigError(checkId, "cwd must be a string when provided.")
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Validates that `env`, if provided, is an object mapping names to string values.
|
|
215
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages.
|
|
216
|
+
* @param env - the check's raw `env` field to validate.
|
|
217
|
+
*/
|
|
218
|
+
function validateEnv(checkId: string, env: unknown): void {
|
|
219
|
+
if (env === undefined) return
|
|
220
|
+
if (env === null || typeof env !== "object" || Array.isArray(env)) {
|
|
221
|
+
throw new InvalidCheckConfigError(
|
|
222
|
+
checkId,
|
|
223
|
+
"env must be an object mapping name to value when provided.",
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
for (const [key, value] of Object.entries(env)) {
|
|
227
|
+
if (typeof value !== "string") {
|
|
228
|
+
throw new InvalidCheckConfigError(checkId, `env["${key}"] must be a string.`)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Validates that `inheritEnv`, if provided, is a boolean.
|
|
235
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages.
|
|
236
|
+
* @param inheritEnv - the check's raw `inheritEnv` field to validate.
|
|
237
|
+
*/
|
|
238
|
+
function validateInheritEnv(checkId: string, inheritEnv: unknown): void {
|
|
239
|
+
if (inheritEnv !== undefined && typeof inheritEnv !== "boolean") {
|
|
240
|
+
throw new InvalidCheckConfigError(checkId, "inheritEnv must be a boolean when provided.")
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Validates that `timeoutMs`, if provided, is a positive finite number.
|
|
246
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages.
|
|
247
|
+
* @param timeoutMs - the check's raw `timeoutMs` field to validate.
|
|
248
|
+
*/
|
|
249
|
+
function validateTimeoutMs(checkId: string, timeoutMs: unknown): void {
|
|
250
|
+
if (timeoutMs === undefined) return
|
|
251
|
+
// Same redundancy as concurrency's typeof check above: `Number.isFinite`
|
|
252
|
+
// returns `false` (never throws or coerces) for every non-number value.
|
|
253
|
+
// Stryker disable next-line ConditionalExpression -- same redundancy as the concurrency check above: the typeof check is redundant with Number.isFinite, which returns false for every non-number value.
|
|
254
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
255
|
+
throw new InvalidCheckConfigError(checkId, "timeoutMs must be a positive number when provided.")
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Validates that `output`, if provided, is an object whose `format` is one of the supported `OUTPUT_FORMATS`.
|
|
261
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages.
|
|
262
|
+
* @param output - the check's raw `output` field to validate.
|
|
263
|
+
*/
|
|
264
|
+
function validateOutput(checkId: string, output: unknown): void {
|
|
265
|
+
if (output === undefined) return
|
|
266
|
+
if (output === null || typeof output !== "object") {
|
|
267
|
+
throw new InvalidCheckConfigError(checkId, "output must be an object when provided.")
|
|
268
|
+
}
|
|
269
|
+
const { format } = output as Record<string, unknown>
|
|
270
|
+
// `typeof format !== "string"` is behaviorally redundant with the clause
|
|
271
|
+
// after it: `Array#includes` uses strict equality, so a non-string
|
|
272
|
+
// `format` can never match an entry of `OUTPUT_FORMATS` (all strings),
|
|
273
|
+
// meaning `!OUTPUT_FORMATS.includes(...)` alone already rejects every
|
|
274
|
+
// case the typeof check would.
|
|
275
|
+
// Stryker disable next-line ConditionalExpression -- the typeof check is redundant with Array#includes' strict equality against OUTPUT_FORMATS (all strings), so a non-string format can never match regardless of the typeof check.
|
|
276
|
+
if (typeof format !== "string" || !OUTPUT_FORMATS.includes(format as OutputFormat)) {
|
|
277
|
+
throw new InvalidCheckConfigError(
|
|
278
|
+
checkId,
|
|
279
|
+
`output.format must be one of ${OUTPUT_FORMATS.map((f) => `"${f}"`).join(", ")}.`,
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Validates that `policy` is a function.
|
|
286
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages.
|
|
287
|
+
* @param policy - the check's raw `policy` field to validate.
|
|
288
|
+
*/
|
|
289
|
+
function validatePolicy(checkId: string, policy: unknown): void {
|
|
290
|
+
if (typeof policy !== "function") {
|
|
291
|
+
throw new InvalidCheckConfigError(checkId, "policy must be a function.")
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Only this check's own shape (array of strings) and self-dependency --
|
|
297
|
+
* both need nothing beyond this one check's own id/field. Self-dependency
|
|
298
|
+
* is a degenerate one-node cycle, but is deliberately caught here rather
|
|
299
|
+
* than deferred to `validateDependencyGraph`'s cycle detector below: it
|
|
300
|
+
* needs no other check's data, gives a clearer message, and fails in this
|
|
301
|
+
* same per-check pass instead of waiting for a second one.
|
|
302
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages and to detect self-dependency.
|
|
303
|
+
* @param dependsOn - the check's raw `dependsOn` field to validate.
|
|
304
|
+
*/
|
|
305
|
+
function validateDependsOn(checkId: string, dependsOn: unknown): void {
|
|
306
|
+
if (dependsOn === undefined) return
|
|
307
|
+
if (!Array.isArray(dependsOn) || dependsOn.some((id) => typeof id !== "string")) {
|
|
308
|
+
throw new InvalidCheckConfigError(
|
|
309
|
+
checkId,
|
|
310
|
+
"dependsOn must be an array of check ids (strings) when provided.",
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
if (dependsOn.includes(checkId)) {
|
|
314
|
+
throw new InvalidCheckConfigError(checkId, "dependsOn must not include the check's own id.")
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Validates that `isolated`, if provided, is a boolean.
|
|
320
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages.
|
|
321
|
+
* @param isolated - the check's raw `isolated` field to validate.
|
|
322
|
+
*/
|
|
323
|
+
function validateIsolated(checkId: string, isolated: unknown): void {
|
|
324
|
+
if (isolated !== undefined && typeof isolated !== "boolean") {
|
|
325
|
+
throw new InvalidCheckConfigError(checkId, "isolated must be a boolean when provided.")
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Whole-graph properties that can't be checked per-check in isolation --
|
|
331
|
+
* run once, after every check's own `dependsOn` shape has already been
|
|
332
|
+
* validated by `validateDependsOn` above, so this can walk every
|
|
333
|
+
* `dependsOn` array without re-checking its shape.
|
|
334
|
+
*
|
|
335
|
+
* Declaration order in the `checks` object doubles as the required topological order (see
|
|
336
|
+
* `CheckDefinition.dependsOn`'s own doc comment): every `dependsOn` id must name a check declared
|
|
337
|
+
* earlier* than the check declaring it. This single backward-reference check subsumes what used
|
|
338
|
+
* to be two separate passes (an unknown-id check, then a DFS cycle detector) -- a cycle is no
|
|
339
|
+
* longer expressible at all once every edge is required to point backward, so there is nothing
|
|
340
|
+
* left for a separate cycle detector to catch. `isolated` needs no validation-time edge computation
|
|
341
|
+
* here: its own implied positional edges (see `run-checks.ts`'s `dependencyIndexesFor`) always
|
|
342
|
+
* point backward (to earlier-declared checks) or are pointed at by later-declared checks, by
|
|
343
|
+
* construction, so they can never introduce a cycle either.
|
|
344
|
+
* @param checks - the full check map, keyed by check id, in declaration order.
|
|
345
|
+
*/
|
|
346
|
+
export function validateDependencyGraph(
|
|
347
|
+
checks: Record<string, { dependsOn?: readonly string[] }>,
|
|
348
|
+
): void {
|
|
349
|
+
const ids = Object.keys(checks)
|
|
350
|
+
const indexById = new Map(ids.map((id, index) => [id, index]))
|
|
351
|
+
|
|
352
|
+
for (const [index, id] of ids.entries()) {
|
|
353
|
+
const check = checks[id]
|
|
354
|
+
// `id` always comes from `Object.keys(checks)`, so `checks[id]` can
|
|
355
|
+
// never actually be undefined -- kept only because
|
|
356
|
+
// `noUncheckedIndexedAccess` can't itself express that invariant.
|
|
357
|
+
// Stryker disable next-line OptionalChaining -- id always comes from Object.keys(checks), so check can never be undefined here; the optional chaining exists only to satisfy noUncheckedIndexedAccess.
|
|
358
|
+
for (const depId of check?.dependsOn ?? []) {
|
|
359
|
+
const depIndex = indexById.get(depId)
|
|
360
|
+
if (depIndex === undefined) {
|
|
361
|
+
throw new InvalidCheckConfigError(id, `dependsOn references unknown check id "${depId}".`)
|
|
362
|
+
}
|
|
363
|
+
if (depIndex >= index) {
|
|
364
|
+
throw new DependencyDeclaredLaterError(id, depId)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error hierarchy for repo-contract. Every concrete error carries a stable
|
|
3
|
+
* `code` string for programmatic handling, and never embeds raw
|
|
4
|
+
* stdout/stderr/env values in its message -- only check ids and field names
|
|
5
|
+
* (see SECURITY.md).
|
|
6
|
+
*
|
|
7
|
+
* Three distinct failure classes are kept deliberately separate (see
|
|
8
|
+
* specs/architecture.md):
|
|
9
|
+
* - Structural config problems throw synchronously, before any process
|
|
10
|
+
* spawns (`InvalidRepoContractConfigError`, `InvalidCheckConfigError`).
|
|
11
|
+
* - Anything only discoverable by attempting execution becomes evidence,
|
|
12
|
+
* never a throw (a bad `cwd`, a missing binary -- recorded as
|
|
13
|
+
* `status: "spawn_error"` on that check's `CheckEvidence`).
|
|
14
|
+
* - A policy function throwing or rejecting is a bug in consumer code, not
|
|
15
|
+
* a check failing its contract -- it propagates as a rejected
|
|
16
|
+
* `runRepoContract()` promise (`PolicyThrewError`), never silently
|
|
17
|
+
* turned into a failed verdict entry.
|
|
18
|
+
*/
|
|
19
|
+
export abstract class RepoContractError extends Error {
|
|
20
|
+
/** Stable, machine-readable identifier for this error's specific failure mode. */
|
|
21
|
+
abstract readonly code: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The top-level `RepoContractConfig` itself is structurally invalid -- e.g. `checks` is not an object, or `concurrency` is not a positive integer. (A `checks` object with zero entries is deliberately valid: the run produces an empty, passing `Verdict`.) Thrown synchronously by `runRepoContract`, before anything spawns -- not by `defineRepoContract`, which performs no runtime validation of its own (see its own doc comment). */
|
|
25
|
+
export class InvalidRepoContractConfigError extends RepoContractError {
|
|
26
|
+
/** Always `"REPO_CONTRACT_INVALID_CONFIG"`. */
|
|
27
|
+
readonly code = "REPO_CONTRACT_INVALID_CONFIG"
|
|
28
|
+
|
|
29
|
+
constructor(reason: string) {
|
|
30
|
+
super(`Invalid repo-contract config -- ${reason}`)
|
|
31
|
+
this.name = "InvalidRepoContractConfigError"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One check's `CheckDefinition` is structurally invalid -- e.g. an empty `run`, a `run` string containing an unquoted shell operator without `shell: true`, or a missing `policy`. Thrown synchronously, before that check (or any other) spawns. */
|
|
36
|
+
export class InvalidCheckConfigError extends RepoContractError {
|
|
37
|
+
/** Always `"REPO_CONTRACT_INVALID_CHECK_CONFIG"`. */
|
|
38
|
+
readonly code = "REPO_CONTRACT_INVALID_CHECK_CONFIG"
|
|
39
|
+
/** The id of the check whose configuration was invalid. */
|
|
40
|
+
readonly checkId: string
|
|
41
|
+
|
|
42
|
+
constructor(checkId: string, reason: string) {
|
|
43
|
+
super(`Invalid check config for "${checkId}" -- ${reason}`)
|
|
44
|
+
this.name = "InvalidCheckConfigError"
|
|
45
|
+
this.checkId = checkId
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `RunRepoContractOptions.checks` (a partial-run request) names a check id that doesn't exist in
|
|
51
|
+
* the configured `checks`. Unlike a `dependsOn` id (already validated to exist by
|
|
52
|
+
* `validateRepoContractConfig` before any run starts), `options.checks` is only ever checked once
|
|
53
|
+
* `runChecks` actually resolves it -- there is no earlier structural-validation pass for it.
|
|
54
|
+
*/
|
|
55
|
+
export class UnknownCheckIdError extends RepoContractError {
|
|
56
|
+
/** Always `"REPO_CONTRACT_UNKNOWN_CHECK_ID"`. */
|
|
57
|
+
readonly code = "REPO_CONTRACT_UNKNOWN_CHECK_ID"
|
|
58
|
+
/** The unrecognized check id named in `options.checks`. */
|
|
59
|
+
readonly checkId: string
|
|
60
|
+
|
|
61
|
+
constructor(checkId: string) {
|
|
62
|
+
super(
|
|
63
|
+
`options.checks names "${checkId}", which is not a check id in this config's checks object.`,
|
|
64
|
+
)
|
|
65
|
+
this.name = "UnknownCheckIdError"
|
|
66
|
+
this.checkId = checkId
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A check's `dependsOn` names a check declared *later* in the same `checks` object.
|
|
72
|
+
* `dependsOn` may only reference a check declared earlier -- see `CheckDefinition.dependsOn`'s own
|
|
73
|
+
* doc comment. Declaration order doubles as the required topological order, so this is the only
|
|
74
|
+
* way an invalid dependency graph can arise; a real cycle is structurally impossible once every
|
|
75
|
+
* edge points backward. Thrown synchronously, before any check spawns.
|
|
76
|
+
*/
|
|
77
|
+
export class DependencyDeclaredLaterError extends RepoContractError {
|
|
78
|
+
/** Always `"REPO_CONTRACT_DEPENDENCY_DECLARED_LATER"`. */
|
|
79
|
+
readonly code = "REPO_CONTRACT_DEPENDENCY_DECLARED_LATER"
|
|
80
|
+
/** The id of the check whose `dependsOn` names a later-declared check. */
|
|
81
|
+
readonly checkId: string
|
|
82
|
+
/** The later-declared check id named in `checkId`'s `dependsOn`. */
|
|
83
|
+
readonly dependencyId: string
|
|
84
|
+
|
|
85
|
+
constructor(checkId: string, dependencyId: string) {
|
|
86
|
+
super(
|
|
87
|
+
`Invalid check config for "${checkId}" -- dependsOn: ["${dependencyId}"], but "${dependencyId}" ` +
|
|
88
|
+
`is declared later in the checks object. dependsOn may only reference a check declared ` +
|
|
89
|
+
`earlier -- reorder the checks object so "${dependencyId}" is declared before "${checkId}".`,
|
|
90
|
+
)
|
|
91
|
+
this.name = "DependencyDeclaredLaterError"
|
|
92
|
+
this.checkId = checkId
|
|
93
|
+
this.dependencyId = dependencyId
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A check requested `output: { format: "yaml" }` but the optional `yaml` peer dependency is not installed. Thrown when that check's output is parsed, not at config-validation time (parsing only happens after the process has already run). */
|
|
98
|
+
export class ParserDependencyMissingError extends RepoContractError {
|
|
99
|
+
/** Always `"REPO_CONTRACT_PARSER_DEPENDENCY_MISSING"`. */
|
|
100
|
+
readonly code = "REPO_CONTRACT_PARSER_DEPENDENCY_MISSING"
|
|
101
|
+
/** The id of the check whose output could not be parsed. */
|
|
102
|
+
readonly checkId: string
|
|
103
|
+
/** The output format that was requested but whose optional peer dependency is missing. */
|
|
104
|
+
readonly format: OutputFormatForError
|
|
105
|
+
|
|
106
|
+
constructor(checkId: string, format: OutputFormatForError, cause: unknown) {
|
|
107
|
+
super(
|
|
108
|
+
`Check "${checkId}" requested output.format: "${format}", but the optional "${format}" ` +
|
|
109
|
+
`peer dependency is not installed -- run \`npm install ${format}\` to enable it.`,
|
|
110
|
+
{ cause },
|
|
111
|
+
)
|
|
112
|
+
this.name = "ParserDependencyMissingError"
|
|
113
|
+
this.checkId = checkId
|
|
114
|
+
this.format = format
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Kept narrow and local rather than importing OutputFormat from types.ts --
|
|
119
|
+
// only "yaml" can ever produce this error today (json/text have no external
|
|
120
|
+
// dependency to be missing), but the field stays named/typed generically in
|
|
121
|
+
// case a future optional format needs the same treatment.
|
|
122
|
+
type OutputFormatForError = "yaml"
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A check's `policy` function threw synchronously, or returned a `Promise`
|
|
126
|
+
* that later rejected. Both failure modes are wrapped identically. The
|
|
127
|
+
* original thrown/rejected value is preserved verbatim via the native
|
|
128
|
+
* `Error` `cause` chain -- never stringified, summarized, or discarded --
|
|
129
|
+
* so a consumer catching this can still inspect exactly what their own
|
|
130
|
+
* policy code did wrong.
|
|
131
|
+
*
|
|
132
|
+
* A policy throwing never stops any other check's policy from running --
|
|
133
|
+
* every configured check's policy is invoked exactly once regardless of
|
|
134
|
+
* what any other policy does. If more than one policy fails this way in the
|
|
135
|
+
* same run, `runRepoContract()` rejects with a native `AggregateError` whose
|
|
136
|
+
* `errors` array holds one error per failing check -- `PolicyThrewError`,
|
|
137
|
+
* or one of its two narrower siblings below (`PolicyReadUnrequestedOutputError`,
|
|
138
|
+
* `PolicyReadFailedParseValueError`) when the failure matches one of their
|
|
139
|
+
* more specific shapes -- rather than surfacing only the first one found.
|
|
140
|
+
*/
|
|
141
|
+
export class PolicyThrewError extends RepoContractError {
|
|
142
|
+
/** Always `"REPO_CONTRACT_POLICY_THREW"`. */
|
|
143
|
+
readonly code = "REPO_CONTRACT_POLICY_THREW"
|
|
144
|
+
/** The id of the check whose policy threw or rejected. */
|
|
145
|
+
readonly checkId: string
|
|
146
|
+
|
|
147
|
+
constructor(checkId: string, cause: unknown) {
|
|
148
|
+
super(`Policy for check "${checkId}" threw instead of returning a PolicyResult`, { cause })
|
|
149
|
+
this.name = "PolicyThrewError"
|
|
150
|
+
this.checkId = checkId
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A narrower `runPolicies` throws instead of `PolicyThrewError` when the
|
|
156
|
+
* thrown `TypeError`'s own message shows it came from reading a
|
|
157
|
+
* `result.output` property (`.success`, `.value`, `.error`, or `.format`),
|
|
158
|
+
* and this check's `CheckEvidence.output` really is `undefined` -- which
|
|
159
|
+
* happens only when its config never requested `output: { format: ... }` in
|
|
160
|
+
* the first place (see `CheckEvidence.output` in types.ts). Detected from
|
|
161
|
+
* the `TypeError`'s exact V8-produced message ("Cannot read properties of
|
|
162
|
+
* undefined (reading '...')") cross-checked against that `undefined` fact,
|
|
163
|
+
* so it is thrown only for a policy that plausibly hit this exact mistake --
|
|
164
|
+
* any other `TypeError`, or a check that did request a format, still becomes
|
|
165
|
+
* a plain `PolicyThrewError`. This is best-effort inference from the
|
|
166
|
+
* `TypeError`'s message text, not a verified trace back to `result.output`
|
|
167
|
+
* itself -- V8's message carries only the property name, so an unrelated
|
|
168
|
+
* bug that happens to read the same property name off some other
|
|
169
|
+
* `undefined` value can still be misclassified this way (see
|
|
170
|
+
* `unrequestedOutputProperty` in run-policies.ts). `cause` still holds the
|
|
171
|
+
* original `TypeError` verbatim, exactly as `PolicyThrewError` guarantees
|
|
172
|
+
* for every other policy failure, so the true cause remains recoverable
|
|
173
|
+
* either way.
|
|
174
|
+
*/
|
|
175
|
+
export class PolicyReadUnrequestedOutputError extends RepoContractError {
|
|
176
|
+
/** Always `"REPO_CONTRACT_POLICY_READ_UNREQUESTED_OUTPUT"`. */
|
|
177
|
+
readonly code = "REPO_CONTRACT_POLICY_READ_UNREQUESTED_OUTPUT"
|
|
178
|
+
/** The id of the check whose policy read `result.output` without requesting a format. */
|
|
179
|
+
readonly checkId: string
|
|
180
|
+
|
|
181
|
+
constructor(checkId: string, property: string, cause: unknown) {
|
|
182
|
+
super(
|
|
183
|
+
`Policy for check "${checkId}" read \`result.output.${property}\`, but "${checkId}" never ` +
|
|
184
|
+
`requested an output format, so \`result.output\` is undefined -- add ` +
|
|
185
|
+
`\`output: { format: "json" }\` (or "yaml"/"text") to check "${checkId}"'s definition to ` +
|
|
186
|
+
`parse its stdout, then narrow with \`result.output?.success\` before reading ` +
|
|
187
|
+
`\`.value\`/\`.error\`.`,
|
|
188
|
+
{ cause },
|
|
189
|
+
)
|
|
190
|
+
this.name = "PolicyReadUnrequestedOutputError"
|
|
191
|
+
this.checkId = checkId
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* `PolicyThrewError`'s other special case, alongside
|
|
197
|
+
* `PolicyReadUnrequestedOutputError`: this check *did* request
|
|
198
|
+
* `output: { format: ... }`, but that parse itself failed
|
|
199
|
+
* (`result.output.success === false` -- see `ParsedOutputFailure` in types.ts, which
|
|
200
|
+
* has no `value` field, only `error`), and the policy read a property off
|
|
201
|
+
* `result.output.value` anyway without checking `.success` first. Detected the same
|
|
202
|
+
* way `runPolicies` detects the sibling case -- a thrown `TypeError` whose own
|
|
203
|
+
* message shows a property read off `undefined` -- cross-checked against this
|
|
204
|
+
* check's own evidence having `output.success === false`, so a genuinely unrelated
|
|
205
|
+
* `TypeError`, or a check whose parse actually succeeded, still becomes a plain
|
|
206
|
+
* `PolicyThrewError`. Unlike `PolicyReadUnrequestedOutputError`, the read property
|
|
207
|
+
* isn't restricted to a known list here -- `result.output.value`'s shape is whatever
|
|
208
|
+
* the external tool printed, entirely unknown to repo-contract -- so this match is
|
|
209
|
+
* necessarily a little broader. `cause` still holds the original `TypeError`
|
|
210
|
+
* verbatim; the message never repeats `result.output.error`'s own text, which may
|
|
211
|
+
* contain raw stdout content (see SECURITY.md).
|
|
212
|
+
*/
|
|
213
|
+
export class PolicyReadFailedParseValueError extends RepoContractError {
|
|
214
|
+
/** Always `"REPO_CONTRACT_POLICY_READ_FAILED_PARSE_VALUE"`. */
|
|
215
|
+
readonly code = "REPO_CONTRACT_POLICY_READ_FAILED_PARSE_VALUE"
|
|
216
|
+
/** The id of the check whose policy read `result.output.value` after a failed parse. */
|
|
217
|
+
readonly checkId: string
|
|
218
|
+
|
|
219
|
+
constructor(checkId: string, property: string, cause: unknown) {
|
|
220
|
+
super(
|
|
221
|
+
`Policy for check "${checkId}" read \`result.output.value.${property}\`, but "${checkId}"'s ` +
|
|
222
|
+
`output failed to parse -- a failed parse has \`result.output.error\`, never \`.value\` -- ` +
|
|
223
|
+
`check \`result.output.success\` before reading \`.value\`.`,
|
|
224
|
+
{ cause },
|
|
225
|
+
)
|
|
226
|
+
this.name = "PolicyReadFailedParseValueError"
|
|
227
|
+
this.checkId = checkId
|
|
228
|
+
}
|
|
229
|
+
}
|