repo-contract 0.2.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +47 -0
- package/dist/.dts/config/validate-config.d.ts.map +1 -1
- package/dist/.dts/errors.d.ts +17 -0
- package/dist/.dts/errors.d.ts.map +1 -1
- package/dist/.dts/evidence/build-evidence.d.ts.map +1 -1
- package/dist/.dts/index.d.ts +2 -1
- package/dist/.dts/index.d.ts.map +1 -1
- package/dist/.dts/parsing/format-schema-issues.d.ts +11 -0
- package/dist/.dts/parsing/format-schema-issues.d.ts.map +1 -0
- package/dist/.dts/parsing/parse-output.d.ts +19 -4
- package/dist/.dts/parsing/parse-output.d.ts.map +1 -1
- package/dist/.dts/standard-schema/types.d.ts +76 -0
- package/dist/.dts/standard-schema/types.d.ts.map +1 -0
- package/dist/.dts/types.d.ts +44 -11
- package/dist/.dts/types.d.ts.map +1 -1
- package/dist/index.cjs +99 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +99 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/evidence.schema.json +2 -2
- package/src/config/validate-config.ts +55 -1
- package/src/errors.ts +26 -0
- package/src/evidence/build-evidence.ts +6 -1
- package/src/index.ts +3 -0
- package/src/parsing/format-schema-issues.ts +56 -0
- package/src/parsing/parse-output.ts +50 -3
- package/src/standard-schema/types.ts +93 -0
- package/src/types.ts +48 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "repo-contract",
|
|
3
|
-
"version": "0.2
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Enforce your repository's own quality bar before CI even spins up. Configure arbitrary checks, capture what happened as evidence, and let repository-owned policies determine what passes. repo-contract provides the execution and evidence layer without becoming another CI system or imposing its own definition of code quality.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"contract",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
},
|
|
112
112
|
"output": {
|
|
113
113
|
"$ref": "#/definitions/ParsedOutput%3Cunknown%3E",
|
|
114
|
-
"description": "The parsed interpretation of `stdout`, present only if this check's config requested a `format`."
|
|
114
|
+
"description": "The parsed interpretation of `stdout`, present only if this check's config requested a `format`. If that config also supplied `output.schema`, a successful parse is additionally validated (and possibly transformed) by that schema before landing here -- see this interface's own doc comment above and `CheckDefinitionConfig.output.schema`."
|
|
115
115
|
}
|
|
116
116
|
},
|
|
117
117
|
"required": [
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"stderr",
|
|
127
127
|
"status"
|
|
128
128
|
],
|
|
129
|
-
"description": "What actually happened when one configured check ran. `output` is present only if that check's config requested a format, and is otherwise `undefined` -- a policy narrows with `ctx.result.output?.success` (or an `if (ctx.result.output) { ... }` guard) before reading `.value`/`.error`.\n\n`output.value` is typed `unknown` for every format, including `\"text\"` (even though `parseText` always produces a `string` at runtime) -- neither repo-contract nor TypeScript's generic inference can reliably carry a specific check's own literal `output.format` through to that same check's `policy` parameter once several checks with heterogeneous formats live together in one `checks` record (a real TypeScript inference limitation hit and confirmed during implementation, not a hypothetical -- see specs/decisions/ for the isolated repro). A policy author narrows or casts `.value` themselves, exactly as they already must for `\"json\"`/`\"yaml\"`
|
|
129
|
+
"description": "What actually happened when one configured check ran. `output` is present only if that check's config requested a format, and is otherwise `undefined` -- a policy narrows with `ctx.result.output?.success` (or an `if (ctx.result.output) { ... }` guard) before reading `.value`/`.error`.\n\n`output.value` is the value produced by the configured `format` parser, optionally validated and (if a schema transforms/coerces it) replaced by `output.schema` (see `CheckDefinitionConfig.output.schema`) -- so it is not necessarily the raw parser result once a check's config supplies a schema. It is typed `unknown` for every format regardless, including `\"text\"` (even though `parseText` always produces a `string` at runtime) and regardless of whether a schema was supplied -- neither repo-contract nor TypeScript's generic inference can reliably carry a specific check's own literal `output.format` (or a specific `schema`'s own inferred output type) through to that same check's `policy` parameter once several checks with heterogeneous formats live together in one `checks` record (a real TypeScript inference limitation hit and confirmed during implementation, not a hypothetical -- see specs/decisions/ for the isolated repro). This is a deliberate, acknowledged tradeoff, not a gap this package is attempting to close: a schema still gives its own author real compile-time input/output typing via `StandardSchemaV1<Input, Output>` for their own code, just not threaded through this shared `CheckEvidence` shape. A policy author narrows or casts `.value` themselves, exactly as they already must for `\"json\"`/`\"yaml\"` with no schema supplied."
|
|
130
130
|
},
|
|
131
131
|
"global.NodeJS.Signals": {
|
|
132
132
|
"type": "string",
|
|
@@ -321,7 +321,7 @@ function validateOutput(checkId: string, output: unknown): void {
|
|
|
321
321
|
if (output === null || typeof output !== "object") {
|
|
322
322
|
throw new InvalidCheckConfigError(checkId, "output must be an object when provided.")
|
|
323
323
|
}
|
|
324
|
-
const { format } = output as Record<string, unknown>
|
|
324
|
+
const { format, schema } = output as Record<string, unknown>
|
|
325
325
|
// `typeof format !== "string"` is behaviorally redundant with the clause
|
|
326
326
|
// after it: `Array#includes` uses strict equality, so a non-string
|
|
327
327
|
// `format` can never match an entry of `OUTPUT_FORMATS` (all strings),
|
|
@@ -334,6 +334,60 @@ function validateOutput(checkId: string, output: unknown): void {
|
|
|
334
334
|
`output.format must be one of ${OUTPUT_FORMATS.map((f) => `"${f}"`).join(", ")}.`,
|
|
335
335
|
)
|
|
336
336
|
}
|
|
337
|
+
validateOutputSchema(checkId, schema)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Validates that `schema`, when provided, is at least *plausibly* a `StandardSchemaV1`-compliant
|
|
342
|
+
* object (https://standardschema.dev) -- checks only the three fields every
|
|
343
|
+
* `StandardSchemaV1.Props` object must have (`version`, `vendor`, `validate`), since `schema` is
|
|
344
|
+
* an opaque, consumer-supplied object and anything else in its shape (e.g. the optional `types`
|
|
345
|
+
* field) is not part of the contract this function can or should enforce. Never calls `validate`
|
|
346
|
+
* itself -- that only happens once a check's stdout actually parses (see `parseOutput`); this
|
|
347
|
+
* function's whole job is structural, not behavioral.
|
|
348
|
+
*
|
|
349
|
+
* Accepts a callable `schema` (`typeof schema === "function"`), not just a plain object -- ArkType's
|
|
350
|
+
* `Type` values are themselves callable functions with `"~standard"` attached as a property, exactly
|
|
351
|
+
* as valid a Standard Schema-compliant value as a plain object one (e.g. Zod's), since `["~standard"]`
|
|
352
|
+
* property access works identically on either. Rejecting callable schemas outright would incorrectly
|
|
353
|
+
* reject a real, popular Standard Schema implementation.
|
|
354
|
+
* @param checkId - identifies which check is being validated, used in thrown error messages.
|
|
355
|
+
* @param schema - the check's raw `output.schema` field to validate.
|
|
356
|
+
*/
|
|
357
|
+
function validateOutputSchema(checkId: string, schema: unknown): void {
|
|
358
|
+
if (schema === undefined) return
|
|
359
|
+
// eslint-disable-next-line secure-coding/no-improper-type-validation -- `null` is already excluded by the preceding `schema === null` short-circuit, and an array slipping past this check (typeof [] === "object") is still correctly rejected by the "~standard" property check just below, since no array has one -- the class of bug this rule guards against (silently treating null/an array as a valid object) can't actually happen here.
|
|
360
|
+
if (schema === null || (typeof schema !== "object" && typeof schema !== "function")) {
|
|
361
|
+
throw new InvalidCheckConfigError(
|
|
362
|
+
checkId,
|
|
363
|
+
"output.schema must be a Standard Schema-compliant object or function when provided (see https://standardschema.dev).",
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const standard = (schema as Record<PropertyKey, unknown>)["~standard"]
|
|
368
|
+
if (standard === null || typeof standard !== "object") {
|
|
369
|
+
throw new InvalidCheckConfigError(
|
|
370
|
+
checkId,
|
|
371
|
+
'output.schema must have a "~standard" property (see https://standardschema.dev).',
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const { version, vendor, validate } = standard as Record<string, unknown>
|
|
376
|
+
if (version !== 1) {
|
|
377
|
+
throw new InvalidCheckConfigError(checkId, 'output.schema["~standard"].version must be 1.')
|
|
378
|
+
}
|
|
379
|
+
if (typeof vendor !== "string") {
|
|
380
|
+
throw new InvalidCheckConfigError(
|
|
381
|
+
checkId,
|
|
382
|
+
'output.schema["~standard"].vendor must be a string.',
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
if (typeof validate !== "function") {
|
|
386
|
+
throw new InvalidCheckConfigError(
|
|
387
|
+
checkId,
|
|
388
|
+
'output.schema["~standard"].validate must be a function.',
|
|
389
|
+
)
|
|
390
|
+
}
|
|
337
391
|
}
|
|
338
392
|
|
|
339
393
|
/**
|
package/src/errors.ts
CHANGED
|
@@ -121,6 +121,32 @@ export class ParserDependencyMissingError extends RepoContractError {
|
|
|
121
121
|
// case a future optional format needs the same treatment.
|
|
122
122
|
type OutputFormatForError = "yaml"
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* A check's `output.schema["~standard"].validate()` threw synchronously, or returned a `Promise`
|
|
126
|
+
* that rejected, instead of returning a `Result`. This is a bug in the consumer-supplied schema
|
|
127
|
+
* object, not malformed check output -- the same distinction `PolicyThrewError` below draws for a
|
|
128
|
+
* throwing policy: a schema *returning* failure `issues` becomes an ordinary
|
|
129
|
+
* `ParsedOutputFailure` (reported as data, exactly like a malformed-JSON/YAML parse failure), but
|
|
130
|
+
* a schema *throwing* means the validator itself is broken, so it propagates as a rejected
|
|
131
|
+
* `runRepoContract()` promise instead. The original thrown/rejected value is preserved verbatim
|
|
132
|
+
* via the native `Error` `cause` chain.
|
|
133
|
+
*/
|
|
134
|
+
export class StandardSchemaValidateThrewError extends RepoContractError {
|
|
135
|
+
/** Always `"REPO_CONTRACT_STANDARD_SCHEMA_VALIDATE_THREW"`. */
|
|
136
|
+
readonly code = "REPO_CONTRACT_STANDARD_SCHEMA_VALIDATE_THREW"
|
|
137
|
+
/** The id of the check whose `output.schema` threw during validation. */
|
|
138
|
+
readonly checkId: string
|
|
139
|
+
|
|
140
|
+
constructor(checkId: string, cause: unknown) {
|
|
141
|
+
super(
|
|
142
|
+
`Check "${checkId}"'s output.schema["~standard"].validate() threw instead of returning a Result`,
|
|
143
|
+
{ cause },
|
|
144
|
+
)
|
|
145
|
+
this.name = "StandardSchemaValidateThrewError"
|
|
146
|
+
this.checkId = checkId
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
124
150
|
/**
|
|
125
151
|
* A check's `policy` function threw synchronously, or returned a `Promise`
|
|
126
152
|
* that later rejected. Both failure modes are wrapped identically. The
|
|
@@ -45,7 +45,12 @@ export async function buildEvidence(
|
|
|
45
45
|
results.map(async ([checkId, check, raw]): Promise<ParsedCheckEntry> => {
|
|
46
46
|
if (check.output === undefined) return [checkId, check, raw]
|
|
47
47
|
try {
|
|
48
|
-
const output = await parseOutput(
|
|
48
|
+
const output = await parseOutput(
|
|
49
|
+
check.output.format,
|
|
50
|
+
raw.stdout,
|
|
51
|
+
checkId,
|
|
52
|
+
check.output.schema,
|
|
53
|
+
)
|
|
49
54
|
return [checkId, check, { ...raw, output }]
|
|
50
55
|
} catch (error) {
|
|
51
56
|
thrown.push(error)
|
package/src/index.ts
CHANGED
|
@@ -19,9 +19,12 @@ export {
|
|
|
19
19
|
PolicyReadUnrequestedOutputError,
|
|
20
20
|
PolicyThrewError,
|
|
21
21
|
RepoContractError,
|
|
22
|
+
StandardSchemaValidateThrewError,
|
|
22
23
|
UnknownCheckIdError,
|
|
23
24
|
} from "./errors.js"
|
|
24
25
|
|
|
26
|
+
export type { StandardSchemaV1 } from "./standard-schema/types.js"
|
|
27
|
+
|
|
25
28
|
export type {
|
|
26
29
|
CheckDefinition,
|
|
27
30
|
CheckDefinitionConfig,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "../standard-schema/types.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders a failed `output.schema` validation's issues as a single, human-readable string for
|
|
5
|
+
* `ParsedOutputFailure.error` -- e.g. `Schema validation failed: foo.bar: Expected string;
|
|
6
|
+
* items[2].name: Required`. Kept as its own small module, independent of `parseOutput`, so its
|
|
7
|
+
* path-rendering logic (the only non-trivial part) can be unit-tested directly.
|
|
8
|
+
* @param issues - the failed `StandardSchemaV1.Result`'s `issues`.
|
|
9
|
+
* @returns the formatted error string.
|
|
10
|
+
*/
|
|
11
|
+
export function formatSchemaIssues(issues: readonly StandardSchemaV1.Issue[]): string {
|
|
12
|
+
return `Schema validation failed: ${issues.map(formatIssue).join("; ")}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Renders one issue as `path: message`, or just `message` when it has no path.
|
|
17
|
+
* @param issue - the issue to render.
|
|
18
|
+
* @returns the rendered issue.
|
|
19
|
+
*/
|
|
20
|
+
function formatIssue(issue: StandardSchemaV1.Issue): string {
|
|
21
|
+
const path = formatPath(issue.path)
|
|
22
|
+
return path === "" ? issue.message : `${path}: ${issue.message}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Renders an issue's `path` as a dotted/bracketed accessor string, e.g. `foo.bar` or
|
|
27
|
+
* `items[2].name`. Each segment is either a bare `PropertyKey` (`string | number | symbol` -- a
|
|
28
|
+
* `symbol` must never be interpolated directly into a template literal, which throws `TypeError:
|
|
29
|
+
* Cannot convert a Symbol value to a string`; always going through `String(key)` avoids that for
|
|
30
|
+
* every segment type uniformly) or a `{ key: PropertyKey }` object per the spec.
|
|
31
|
+
* @param path - the issue's raw `path`, if any.
|
|
32
|
+
* @returns the rendered path, or `""` for a pathless issue.
|
|
33
|
+
*/
|
|
34
|
+
function formatPath(
|
|
35
|
+
path: readonly (PropertyKey | StandardSchemaV1.PathSegment)[] | undefined,
|
|
36
|
+
): string {
|
|
37
|
+
// Equivalent mutant: mutating `path.length === 0` to `false` makes an empty (but defined) `path`
|
|
38
|
+
// fall through to `path.map(...).join("")` below instead of returning `""` here directly -- but
|
|
39
|
+
// mapping and joining an empty array always produces `""` regardless of the mapping function, so
|
|
40
|
+
// both branches are observably identical for every possible input; no test could ever distinguish
|
|
41
|
+
// them by `formatPath`'s return value.
|
|
42
|
+
// Stryker disable next-line ConditionalExpression -- equivalent mutant: `path.length === 0` falling through to `path.map(...).join("")` on an empty array also produces `""`, identical to this early return, for every possible input -- no test could ever distinguish them.
|
|
43
|
+
if (path === undefined || path.length === 0) return ""
|
|
44
|
+
|
|
45
|
+
return path
|
|
46
|
+
.map((segment) =>
|
|
47
|
+
// eslint-disable-next-line secure-coding/no-improper-type-validation -- `segment` is declared `PropertyKey | StandardSchemaV1.PathSegment` (never `null` or an array), so `typeof segment === "object"` can only match the `PathSegment` object form here; the null/array-safe rewrite this rule suggests is flagged as an unreachable condition by @typescript-eslint/no-unnecessary-condition given that exact type, so satisfying both rules at once is impossible -- the type itself is the guarantee this check would otherwise add at runtime.
|
|
48
|
+
typeof segment === "object" ? segment.key : segment,
|
|
49
|
+
)
|
|
50
|
+
.map((key, index) => {
|
|
51
|
+
if (typeof key === "number") return `[${String(key)}]`
|
|
52
|
+
const rendered = String(key)
|
|
53
|
+
return index === 0 ? rendered : `.${rendered}`
|
|
54
|
+
})
|
|
55
|
+
.join("")
|
|
56
|
+
}
|
|
@@ -1,19 +1,66 @@
|
|
|
1
|
+
import { StandardSchemaValidateThrewError } from "../errors.js"
|
|
2
|
+
import type { StandardSchemaV1 } from "../standard-schema/types.js"
|
|
1
3
|
import type { OutputFormat, ParsedOutput } from "../types.js"
|
|
4
|
+
import { formatSchemaIssues } from "./format-schema-issues.js"
|
|
2
5
|
import { parseJson } from "./parse-json.js"
|
|
3
6
|
import { parseText } from "./parse-text.js"
|
|
4
7
|
import { parseYaml } from "./parse-yaml.js"
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
|
-
* Dispatches to the parser for `format
|
|
10
|
+
* Dispatches to the parser for `format`, then -- only on a successful parse, and only if `schema`
|
|
11
|
+
* is supplied -- runs `schema["~standard"].validate()` against the parsed value
|
|
12
|
+
* (https://standardschema.dev). `parse-json.ts`/`parse-yaml.ts`/`parse-text.ts` stay entirely
|
|
13
|
+
* unaware of `schema`; this is the sole orchestration point. `validate()` may return its `Result`
|
|
14
|
+
* synchronously or as a `Promise` -- `await`ing either uniformly is a no-op for the synchronous
|
|
15
|
+
* case, so this doesn't need its own reason to be async on top of the one this function already
|
|
16
|
+
* has (the dynamic `import("yaml")` for `format: "yaml"`). A successful `Result` (`issues ===
|
|
17
|
+
* undefined`) *replaces* `value` with the schema's own (possibly transformed) output; a failing
|
|
18
|
+
* `Result` becomes a `ParsedOutputFailure` whose `error` is built by `formatSchemaIssues`.
|
|
19
|
+
* `schema.validate()` itself throwing or rejecting is a bug in the consumer-supplied schema, not
|
|
20
|
+
* malformed output -- it throws `StandardSchemaValidateThrewError` rather than becoming a
|
|
21
|
+
* `ParsedOutputFailure`. Never throws for a malformed-output parse failure -- see each parser's
|
|
22
|
+
* own documentation. May throw `ParserDependencyMissingError` for `format: "yaml"` if the
|
|
23
|
+
* optional `yaml` peer dependency is unavailable.
|
|
8
24
|
* @param format - which parser to dispatch to.
|
|
9
25
|
* @param stdout - the check's raw stdout to parse.
|
|
10
|
-
* @param checkId - identifies which check's output is being parsed, used in
|
|
11
|
-
* @
|
|
26
|
+
* @param checkId - identifies which check's output is being parsed, used in a thrown `ParserDependencyMissingError`/`StandardSchemaValidateThrewError`.
|
|
27
|
+
* @param schema - an optional Standard Schema-compliant validator to run against a successful parse's value.
|
|
28
|
+
* @returns the parsed (and, if `schema` was supplied and validation succeeded, schema-transformed) output.
|
|
12
29
|
*/
|
|
13
30
|
export async function parseOutput(
|
|
14
31
|
format: OutputFormat,
|
|
15
32
|
stdout: string,
|
|
16
33
|
checkId: string,
|
|
34
|
+
schema?: StandardSchemaV1,
|
|
35
|
+
): Promise<ParsedOutput<unknown>> {
|
|
36
|
+
const parsed = await dispatch(format, stdout, checkId)
|
|
37
|
+
if (!parsed.success || schema === undefined) return parsed
|
|
38
|
+
|
|
39
|
+
let result: StandardSchemaV1.Result<unknown>
|
|
40
|
+
try {
|
|
41
|
+
result = await schema["~standard"].validate(parsed.value)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new StandardSchemaValidateThrewError(checkId, error)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (result.issues === undefined) {
|
|
47
|
+
return { format, success: true, value: result.value }
|
|
48
|
+
}
|
|
49
|
+
return { format, success: false, error: formatSchemaIssues(result.issues) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Dispatches to the parser for `format` -- the pre-existing behavior of `parseOutput`, extracted
|
|
54
|
+
* so `parseOutput` itself reads as: parse, then optionally validate.
|
|
55
|
+
* @param format - which parser to dispatch to.
|
|
56
|
+
* @param stdout - the check's raw stdout to parse.
|
|
57
|
+
* @param checkId - identifies which check's output is being parsed, used in a thrown `ParserDependencyMissingError`.
|
|
58
|
+
* @returns the parsed output produced by the selected parser.
|
|
59
|
+
*/
|
|
60
|
+
async function dispatch(
|
|
61
|
+
format: OutputFormat,
|
|
62
|
+
stdout: string,
|
|
63
|
+
checkId: string,
|
|
17
64
|
): Promise<ParsedOutput<unknown>> {
|
|
18
65
|
switch (format) {
|
|
19
66
|
case "json":
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-vendored from `@standard-schema/spec@1.1.0` (https://standardschema.dev), pinned
|
|
3
|
+
* 2026-09-04 -- see specs/decisions/0012-hand-vendored-standard-schema-support-for-optional-output-validation.md for why this is
|
|
4
|
+
* vendored rather than an installed dependency, and for the version-pin/re-diff process. Pure type
|
|
5
|
+
* declarations, zero runtime code -- assigning any real Zod/Valibot/ArkType (etc.) schema to this
|
|
6
|
+
* type costs nothing at runtime. Only `StandardSchemaV1` (validation) is vendored here -- the
|
|
7
|
+
* separate, optional `StandardJSONSchemaV1` (JSON Schema conversion) extension
|
|
8
|
+
* (https://standardschema.dev/json-schema) is out of scope; see the ADR.
|
|
9
|
+
*
|
|
10
|
+
* Upstream's `StandardSchemaV1.Props` actually extends a shared `StandardTypedV1.Props` base
|
|
11
|
+
* (`version`/`vendor`/`types`); this vendored copy inlines those fields directly into one flat
|
|
12
|
+
* interface, since repo-contract has no use for the shared base on its own -- structurally
|
|
13
|
+
* identical for any real schema object assigned to it. Re-diff against
|
|
14
|
+
* `@standard-schema/spec`'s published `dist/index.d.ts` if this file is ever touched.
|
|
15
|
+
*/
|
|
16
|
+
export interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
17
|
+
/** The Standard Schema properties. */
|
|
18
|
+
readonly "~standard": StandardSchemaV1.Props<Input, Output>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Namespaced members of {@link StandardSchemaV1}: `Props`, `Result`, `SuccessResult`, `FailureResult`, `Issue`, `PathSegment`, `Types`, `InferInput`, `InferOutput`. */
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace -- the interface+namespace declaration-merging pattern is required here to mirror @standard-schema/spec's own published shape (StandardSchemaV1.Props/.Result/.Issue/... nested under the interface's own name) -- an ES2015 module can't merge with an interface of the same name the way a namespace does, and diverging from upstream's exact shape would defeat this file's whole purpose of being a faithful, re-diffable vendor copy (see this file's own top comment).
|
|
23
|
+
export declare namespace StandardSchemaV1 {
|
|
24
|
+
/** The Standard Schema properties interface. */
|
|
25
|
+
export interface Props<Input = unknown, Output = Input> {
|
|
26
|
+
/** The version number of the standard. */
|
|
27
|
+
readonly version: 1
|
|
28
|
+
/** The vendor name of the schema library. */
|
|
29
|
+
readonly vendor: string
|
|
30
|
+
/** Validates unknown input values. */
|
|
31
|
+
readonly validate: (
|
|
32
|
+
value: unknown,
|
|
33
|
+
options?: Options,
|
|
34
|
+
) => Result<Output> | Promise<Result<Output>>
|
|
35
|
+
/** Inferred types associated with the schema. */
|
|
36
|
+
readonly types?: Types<Input, Output> | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Options passable to `validate`. */
|
|
40
|
+
export interface Options {
|
|
41
|
+
/** Explicit support for additional vendor-specific parameters, if needed. */
|
|
42
|
+
readonly libraryOptions?: Record<string, unknown> | undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The result interface of the validate function. */
|
|
46
|
+
export type Result<Output> = SuccessResult<Output> | FailureResult
|
|
47
|
+
|
|
48
|
+
/** The result interface if validation succeeds. */
|
|
49
|
+
export interface SuccessResult<Output> {
|
|
50
|
+
/** The typed output value. */
|
|
51
|
+
readonly value: Output
|
|
52
|
+
/** A falsy value for `issues` indicates success. */
|
|
53
|
+
readonly issues?: undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The result interface if validation fails. */
|
|
57
|
+
export interface FailureResult {
|
|
58
|
+
/** The issues of failed validation. */
|
|
59
|
+
readonly issues: readonly Issue[]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The issue interface of the failure output. */
|
|
63
|
+
export interface Issue {
|
|
64
|
+
/** The error message of the issue. */
|
|
65
|
+
readonly message: string
|
|
66
|
+
/** The path of the issue, if any. */
|
|
67
|
+
readonly path?: readonly (PropertyKey | PathSegment)[] | undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The path segment interface of the issue. */
|
|
71
|
+
export interface PathSegment {
|
|
72
|
+
/** The key representing a path segment. */
|
|
73
|
+
readonly key: PropertyKey
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The Standard Schema types interface. */
|
|
77
|
+
export interface Types<Input = unknown, Output = Input> {
|
|
78
|
+
/** The input type of the schema. */
|
|
79
|
+
readonly input: Input
|
|
80
|
+
/** The output type of the schema. */
|
|
81
|
+
readonly output: Output
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Infers the input type of a Standard Schema. */
|
|
85
|
+
export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
|
|
86
|
+
Schema["~standard"]["types"]
|
|
87
|
+
>["input"]
|
|
88
|
+
|
|
89
|
+
/** Infers the output type of a Standard Schema. */
|
|
90
|
+
export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
|
|
91
|
+
Schema["~standard"]["types"]
|
|
92
|
+
>["output"]
|
|
93
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
SpawnSyncReturns,
|
|
13
13
|
} from "node:child_process"
|
|
14
14
|
|
|
15
|
+
import type { StandardSchemaV1 } from "./standard-schema/types.js"
|
|
16
|
+
|
|
15
17
|
/** Output interpretation a check can explicitly request. No format requested means no parsing -- the consumer gets raw stdout/stderr only. */
|
|
16
18
|
export type OutputFormat = "json" | "yaml" | "text"
|
|
17
19
|
|
|
@@ -66,16 +68,21 @@ export type ParsedOutput<T> = ParsedOutputSuccess<T> | ParsedOutputFailure
|
|
|
66
68
|
* `undefined` -- a policy narrows with `ctx.result.output?.success` (or an
|
|
67
69
|
* `if (ctx.result.output) { ... }` guard) before reading `.value`/`.error`.
|
|
68
70
|
*
|
|
69
|
-
* `output.value` is
|
|
70
|
-
* (
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
71
|
+
* `output.value` is the value produced by the configured `format` parser, optionally validated
|
|
72
|
+
* and (if a schema transforms/coerces it) replaced by `output.schema` (see
|
|
73
|
+
* `CheckDefinitionConfig.output.schema`) -- so it is not necessarily the raw parser result once a
|
|
74
|
+
* check's config supplies a schema. It is typed `unknown` for every format regardless, including
|
|
75
|
+
* `"text"` (even though `parseText` always produces a `string` at runtime) and regardless of
|
|
76
|
+
* whether a schema was supplied -- neither repo-contract nor TypeScript's generic inference can
|
|
77
|
+
* reliably carry a specific check's own literal `output.format` (or a specific `schema`'s own
|
|
78
|
+
* inferred output type) through to that same check's `policy` parameter once several checks with
|
|
79
|
+
* heterogeneous formats live together in one `checks` record (a real TypeScript inference
|
|
80
|
+
* limitation hit and confirmed during implementation, not a hypothetical -- see specs/decisions/
|
|
81
|
+
* for the isolated repro). This is a deliberate, acknowledged tradeoff, not a gap this package
|
|
82
|
+
* is attempting to close: a schema still gives its own author real compile-time input/output
|
|
83
|
+
* typing via `StandardSchemaV1<Input, Output>` for their own code, just not threaded through this
|
|
84
|
+
* shared `CheckEvidence` shape. A policy author narrows or casts `.value` themselves, exactly as
|
|
85
|
+
* they already must for `"json"`/`"yaml"` with no schema supplied.
|
|
79
86
|
*/
|
|
80
87
|
export interface CheckEvidence {
|
|
81
88
|
/** The executable that was actually spawned (after tokenization, if `run` was a string). */
|
|
@@ -102,7 +109,12 @@ export interface CheckEvidence {
|
|
|
102
109
|
readonly spawnError?: string
|
|
103
110
|
/** 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. */
|
|
104
111
|
readonly spawnErrorCode?: string
|
|
105
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* The parsed interpretation of `stdout`, present only if this check's config requested a
|
|
114
|
+
* `format`. If that config also supplied `output.schema`, a successful parse is additionally
|
|
115
|
+
* validated (and possibly transformed) by that schema before landing here -- see this
|
|
116
|
+
* interface's own doc comment above and `CheckDefinitionConfig.output.schema`.
|
|
117
|
+
*/
|
|
106
118
|
readonly output?: ParsedOutput<unknown>
|
|
107
119
|
}
|
|
108
120
|
|
|
@@ -238,7 +250,31 @@ export interface CheckDefinitionConfig {
|
|
|
238
250
|
/** Maximum time to let this check's process run before it is terminated and recorded with `status: "timed_out"`. No timeout by default. */
|
|
239
251
|
readonly timeoutMs?: number
|
|
240
252
|
/** Request that stdout be parsed as this format. Omit for no parsing -- the consumer gets raw stdout/stderr only. */
|
|
241
|
-
readonly output?: {
|
|
253
|
+
readonly output?: {
|
|
254
|
+
readonly format: OutputFormat
|
|
255
|
+
/**
|
|
256
|
+
* An optional Standard Schema-compliant validator (Zod, Valibot, ArkType, or any other
|
|
257
|
+
* implementation of https://standardschema.dev), run once the requested `format` parse
|
|
258
|
+
* itself succeeds. repo-contract never imports a schema library itself --
|
|
259
|
+
* `StandardSchemaV1` is hand-vendored (see `src/standard-schema/types.ts`) purely as a
|
|
260
|
+
* type-level contract, so accepting any consumer's own schema object costs zero new
|
|
261
|
+
* runtime or dev dependencies (see
|
|
262
|
+
* specs/decisions/0012-hand-vendored-standard-schema-support-for-optional-output-validation.md). A successful
|
|
263
|
+
* `~standard.validate()` result *replaces* the parsed value on `CheckEvidence.output.value`
|
|
264
|
+
* (letting a schema transform, not just check, its input); a failing result becomes an
|
|
265
|
+
* ordinary `ParsedOutputFailure`, indistinguishable in shape from a malformed-JSON/YAML
|
|
266
|
+
* parse failure -- both are "this check's output isn't what was expected," reported as
|
|
267
|
+
* data, never a throw (see `parseOutput`). `validate()` itself throwing or rejecting is a
|
|
268
|
+
* different case -- a bug in the supplied schema, not malformed output -- and surfaces as
|
|
269
|
+
* `StandardSchemaValidateThrewError` instead (see that error's own doc comment). Like every
|
|
270
|
+
* other format's `value`, `schema`'s inferred output type is *not* threaded to
|
|
271
|
+
* `CheckEvidence.output.value`'s declared type -- it stays `unknown`, for the same
|
|
272
|
+
* heterogeneous-checks-in-one-record TypeScript inference limitation already documented on
|
|
273
|
+
* `CheckEvidence` above; a policy still narrows or casts `.value` itself, now
|
|
274
|
+
* shaped/transformed by `schema` at runtime even though the type alone doesn't say so.
|
|
275
|
+
*/
|
|
276
|
+
readonly schema?: StandardSchemaV1
|
|
277
|
+
}
|
|
242
278
|
/**
|
|
243
279
|
* A full scheduling barrier at this check's own position in the `checks` object: it does not
|
|
244
280
|
* spawn until every check declared *earlier* has reached a terminal status (nothing "currently in
|