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,253 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://maverickcer.github.io/repo-contract/schema/evidence.schema.json",
|
|
4
|
+
"title": "repo-contract Evidence",
|
|
5
|
+
"description": "Machine-readable record of what happened when a repo-contract configuration was executed -- one entry per configured check, describing the command run, its exit status, captured output, and (if requested) parsed output. Says nothing about whether the result was acceptable -- see the paired Verdict schema for that. Generated from src/types.ts's Evidence type -- never hand-authored.",
|
|
6
|
+
"$ref": "#/definitions/EvidenceSchemaSource",
|
|
7
|
+
"definitions": {
|
|
8
|
+
"EvidenceSchemaSource": {
|
|
9
|
+
"$ref": "#/definitions/Evidence"
|
|
10
|
+
},
|
|
11
|
+
"Evidence": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {
|
|
14
|
+
"version": {
|
|
15
|
+
"type": "number",
|
|
16
|
+
"const": 1,
|
|
17
|
+
"description": "Schema version of this shape; see VERSIONING.md."
|
|
18
|
+
},
|
|
19
|
+
"startedAt": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "ISO 8601 timestamp of when the run began."
|
|
22
|
+
},
|
|
23
|
+
"completedAt": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "ISO 8601 timestamp of when the last check finished."
|
|
26
|
+
},
|
|
27
|
+
"durationMs": {
|
|
28
|
+
"type": "number",
|
|
29
|
+
"description": "Wall-clock time for the run as a whole, in milliseconds."
|
|
30
|
+
},
|
|
31
|
+
"checks": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": {
|
|
34
|
+
"$ref": "#/definitions/CheckEvidence"
|
|
35
|
+
},
|
|
36
|
+
"description": "Each configured check's own evidence, keyed by check id."
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"required": [
|
|
40
|
+
"version",
|
|
41
|
+
"startedAt",
|
|
42
|
+
"completedAt",
|
|
43
|
+
"durationMs",
|
|
44
|
+
"checks"
|
|
45
|
+
],
|
|
46
|
+
"description": "Versioned, immutable record of one complete `runRepoContract` execution -- every configured check's evidence, plus timing for the run as a whole. Says nothing about whether any of it was acceptable; see `Verdict`. Additive fields are a compatible change; changing or removing an existing field requires bumping this version number (see VERSIONING.md)."
|
|
47
|
+
},
|
|
48
|
+
"CheckEvidence": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": {
|
|
51
|
+
"command": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "The executable that was actually spawned (after tokenization, if `run` was a string)."
|
|
54
|
+
},
|
|
55
|
+
"args": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": {
|
|
58
|
+
"type": "string"
|
|
59
|
+
},
|
|
60
|
+
"description": "The arguments passed to `command`, exactly as spawned."
|
|
61
|
+
},
|
|
62
|
+
"startedAt": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "ISO 8601 timestamp of when the process was spawned."
|
|
65
|
+
},
|
|
66
|
+
"completedAt": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "ISO 8601 timestamp of when the process reached its terminal state."
|
|
69
|
+
},
|
|
70
|
+
"durationMs": {
|
|
71
|
+
"type": "number",
|
|
72
|
+
"description": "Wall-clock time from spawn to termination, in milliseconds."
|
|
73
|
+
},
|
|
74
|
+
"exitCode": {
|
|
75
|
+
"type": [
|
|
76
|
+
"number",
|
|
77
|
+
"null"
|
|
78
|
+
],
|
|
79
|
+
"description": "`null` when the process never exited normally -- see `signal` and `status`."
|
|
80
|
+
},
|
|
81
|
+
"signal": {
|
|
82
|
+
"anyOf": [
|
|
83
|
+
{
|
|
84
|
+
"$ref": "#/definitions/global.NodeJS.Signals"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"type": "null"
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"description": "The signal that terminated the process, if any. `null` for a normal exit or a spawn failure."
|
|
91
|
+
},
|
|
92
|
+
"stdout": {
|
|
93
|
+
"type": "string",
|
|
94
|
+
"description": "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."
|
|
95
|
+
},
|
|
96
|
+
"stderr": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"description": "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."
|
|
99
|
+
},
|
|
100
|
+
"status": {
|
|
101
|
+
"$ref": "#/definitions/CheckStatus",
|
|
102
|
+
"description": "Why the process reached its terminal state; see `CheckStatus`."
|
|
103
|
+
},
|
|
104
|
+
"spawnError": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"description": "Populated only for `status === \"spawn_error\"` -- the underlying Node error message (e.g. \"spawn foo ENOENT\"). Never populated for any other status."
|
|
107
|
+
},
|
|
108
|
+
"spawnErrorCode": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"description": "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."
|
|
111
|
+
},
|
|
112
|
+
"output": {
|
|
113
|
+
"$ref": "#/definitions/ParsedOutput%3Cunknown%3E",
|
|
114
|
+
"description": "The parsed interpretation of `stdout`, present only if this check's config requested a `format`."
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"required": [
|
|
118
|
+
"command",
|
|
119
|
+
"args",
|
|
120
|
+
"startedAt",
|
|
121
|
+
"completedAt",
|
|
122
|
+
"durationMs",
|
|
123
|
+
"exitCode",
|
|
124
|
+
"signal",
|
|
125
|
+
"stdout",
|
|
126
|
+
"stderr",
|
|
127
|
+
"status"
|
|
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\"` where no schema knowledge exists either way."
|
|
130
|
+
},
|
|
131
|
+
"global.NodeJS.Signals": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"enum": [
|
|
134
|
+
"SIGABRT",
|
|
135
|
+
"SIGALRM",
|
|
136
|
+
"SIGBUS",
|
|
137
|
+
"SIGCHLD",
|
|
138
|
+
"SIGCONT",
|
|
139
|
+
"SIGFPE",
|
|
140
|
+
"SIGHUP",
|
|
141
|
+
"SIGILL",
|
|
142
|
+
"SIGINT",
|
|
143
|
+
"SIGIO",
|
|
144
|
+
"SIGIOT",
|
|
145
|
+
"SIGKILL",
|
|
146
|
+
"SIGPIPE",
|
|
147
|
+
"SIGPOLL",
|
|
148
|
+
"SIGPROF",
|
|
149
|
+
"SIGPWR",
|
|
150
|
+
"SIGQUIT",
|
|
151
|
+
"SIGSEGV",
|
|
152
|
+
"SIGSTKFLT",
|
|
153
|
+
"SIGSTOP",
|
|
154
|
+
"SIGSYS",
|
|
155
|
+
"SIGTERM",
|
|
156
|
+
"SIGTRAP",
|
|
157
|
+
"SIGTSTP",
|
|
158
|
+
"SIGTTIN",
|
|
159
|
+
"SIGTTOU",
|
|
160
|
+
"SIGUNUSED",
|
|
161
|
+
"SIGURG",
|
|
162
|
+
"SIGUSR1",
|
|
163
|
+
"SIGUSR2",
|
|
164
|
+
"SIGVTALRM",
|
|
165
|
+
"SIGWINCH",
|
|
166
|
+
"SIGXCPU",
|
|
167
|
+
"SIGXFSZ",
|
|
168
|
+
"SIGBREAK",
|
|
169
|
+
"SIGLOST",
|
|
170
|
+
"SIGINFO"
|
|
171
|
+
]
|
|
172
|
+
},
|
|
173
|
+
"CheckStatus": {
|
|
174
|
+
"type": "string",
|
|
175
|
+
"enum": [
|
|
176
|
+
"completed",
|
|
177
|
+
"timed_out",
|
|
178
|
+
"signaled",
|
|
179
|
+
"host_terminated",
|
|
180
|
+
"spawn_error",
|
|
181
|
+
"aborted"
|
|
182
|
+
],
|
|
183
|
+
"description": "Why a check's process ended up in its terminal state. `\"completed\"` means the process ran to exit on its own -- the exit code may still be non-zero, and that is for the check's policy to interpret, never this package. The other five values all mean the process did not exit on its own; repo-contract terminated it, or it was terminated for a reason repo-contract can observe but did not cause.\n\n`\"signaled\"` specifically means a signal repo-contract did *not* itself request -- an externally-caused termination. A check killed because the *host* process running repo-contract received its own SIGINT/SIGTERM (see `run-checks.ts`'s termination-handler cleanup) is instead `\"host_terminated\"`: repo-contract did request that signal, just not via `options.signal` or `timeoutMs` (see `\"aborted\"`/`\"timed_out\"`), so it must not be conflated with an externally-caused `\"signaled\"`."
|
|
184
|
+
},
|
|
185
|
+
"ParsedOutput<unknown>": {
|
|
186
|
+
"anyOf": [
|
|
187
|
+
{
|
|
188
|
+
"$ref": "#/definitions/ParsedOutputSuccess%3Cunknown%3E"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"$ref": "#/definitions/ParsedOutputFailure"
|
|
192
|
+
}
|
|
193
|
+
],
|
|
194
|
+
"description": "The result of a check's requested output-format parse: either a successful `ParsedOutputSuccess`, or a `ParsedOutputFailure`."
|
|
195
|
+
},
|
|
196
|
+
"ParsedOutputSuccess<unknown>": {
|
|
197
|
+
"type": "object",
|
|
198
|
+
"properties": {
|
|
199
|
+
"format": {
|
|
200
|
+
"$ref": "#/definitions/OutputFormat",
|
|
201
|
+
"description": "The format that was requested and successfully parsed."
|
|
202
|
+
},
|
|
203
|
+
"success": {
|
|
204
|
+
"type": "boolean",
|
|
205
|
+
"const": true,
|
|
206
|
+
"description": "Always `true`."
|
|
207
|
+
},
|
|
208
|
+
"value": {
|
|
209
|
+
"description": "The parsed value."
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
"required": [
|
|
213
|
+
"format",
|
|
214
|
+
"success",
|
|
215
|
+
"value"
|
|
216
|
+
],
|
|
217
|
+
"description": "A requested parse of a check's stdout succeeded."
|
|
218
|
+
},
|
|
219
|
+
"OutputFormat": {
|
|
220
|
+
"type": "string",
|
|
221
|
+
"enum": [
|
|
222
|
+
"json",
|
|
223
|
+
"yaml",
|
|
224
|
+
"text"
|
|
225
|
+
],
|
|
226
|
+
"description": "Output interpretation a check can explicitly request. No format requested means no parsing -- the consumer gets raw stdout/stderr only."
|
|
227
|
+
},
|
|
228
|
+
"ParsedOutputFailure": {
|
|
229
|
+
"type": "object",
|
|
230
|
+
"properties": {
|
|
231
|
+
"format": {
|
|
232
|
+
"$ref": "#/definitions/OutputFormat",
|
|
233
|
+
"description": "The format that was requested (and failed to parse)."
|
|
234
|
+
},
|
|
235
|
+
"success": {
|
|
236
|
+
"type": "boolean",
|
|
237
|
+
"const": false,
|
|
238
|
+
"description": "Always `false`."
|
|
239
|
+
},
|
|
240
|
+
"error": {
|
|
241
|
+
"type": "string",
|
|
242
|
+
"description": "The parse error's message."
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
"required": [
|
|
246
|
+
"format",
|
|
247
|
+
"success",
|
|
248
|
+
"error"
|
|
249
|
+
],
|
|
250
|
+
"description": "A requested parse of a check's stdout failed. The raw stdout on the parent `CheckEvidence` is preserved unchanged -- a parse failure is never silently reinterpreted or discarded."
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://maverickcer.github.io/repo-contract/schema/verdict.schema.json",
|
|
4
|
+
"title": "repo-contract Verdict",
|
|
5
|
+
"description": "Machine-readable aggregate pass/fail result produced by evaluating repository-owned policies against an Evidence object -- one entry per configured check, each individually inspectable. Generated from src/types.ts's Verdict type -- never hand-authored.",
|
|
6
|
+
"$ref": "#/definitions/VerdictSchemaSource",
|
|
7
|
+
"definitions": {
|
|
8
|
+
"VerdictSchemaSource": {
|
|
9
|
+
"$ref": "#/definitions/Verdict"
|
|
10
|
+
},
|
|
11
|
+
"Verdict": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"properties": {
|
|
14
|
+
"version": {
|
|
15
|
+
"type": "number",
|
|
16
|
+
"const": 2,
|
|
17
|
+
"description": "Schema version of this shape; see VERSIONING.md."
|
|
18
|
+
},
|
|
19
|
+
"passed": {
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"description": "`true` only if every check's `outcome` is `\"pass\"` or `\"warn\"`."
|
|
22
|
+
},
|
|
23
|
+
"checks": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": {
|
|
26
|
+
"$ref": "#/definitions/PolicyResult"
|
|
27
|
+
},
|
|
28
|
+
"description": "Each configured check's own `PolicyResult`, verbatim, keyed by check id."
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"required": [
|
|
32
|
+
"version",
|
|
33
|
+
"passed",
|
|
34
|
+
"checks"
|
|
35
|
+
],
|
|
36
|
+
"description": "Versioned, immutable aggregate result of evaluating every configured check's policy against its evidence -- each check's own `PolicyResult`, verbatim, keyed by check id. `passed` is `true` only if every check's `outcome` is `\"pass\"` or `\"warn\"` -- `\"fail\"` is the only outcome that fails the run; one failing check never collapses into a single generic message, every check remains individually inspectable under `checks`. Versioned independently of `Evidence` (see VERSIONING.md's schema-versioning policy) -- `version: 2` reflects `checks[id]` changing shape from `{ passed, reason? }` to a full `PolicyResult` (`{ outcome, rationale }`); see ADR 0001."
|
|
37
|
+
},
|
|
38
|
+
"PolicyResult": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"outcome": {
|
|
42
|
+
"$ref": "#/definitions/PolicyOutcome",
|
|
43
|
+
"description": "The policy's pass/fail/warn decision."
|
|
44
|
+
},
|
|
45
|
+
"rationale": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "Why the policy reached `outcome`, in enough detail to act on without rerunning anything."
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"required": [
|
|
51
|
+
"outcome",
|
|
52
|
+
"rationale"
|
|
53
|
+
],
|
|
54
|
+
"description": "A repository-owned policy's interpretation of one check's captured evidence -- fully JSON-serializable (a plain object of primitives only, never an `Error`, class instance, function, or tool-specific object) so it can be persisted, transmitted, aggregated across parallel checks, and consumed directly by a human or an AI without rerunning anything.\n\n`rationale` is mandatory and must contain enough actionable detail -- specific file/line locations, rule ids, test names, counts -- for a consumer to understand *why* the policy reached its outcome from this value alone. A rationale like \"see output above\" or \"check the report for details\" defeats the purpose: it forces the consumer back to raw, unstructured command output, exactly what this type exists to avoid. See specs/architecture.md for the evidence/rationale/judgment distinction this type is built around: evidence answers \"what happened?\", `rationale` answers \"what does the repository's policy conclude about what happened?\", and a policy's `outcome` is not the final word -- a human or AI consumer still makes the final judgment call using both."
|
|
55
|
+
},
|
|
56
|
+
"PolicyOutcome": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"enum": [
|
|
59
|
+
"pass",
|
|
60
|
+
"fail",
|
|
61
|
+
"warn"
|
|
62
|
+
],
|
|
63
|
+
"description": "`\"pass\"`: the repository-owned policy evaluated the captured evidence as satisfying its configured requirements. `\"fail\"`: the evidence did not satisfy them. `\"warn\"`: the evidence does not violate the policy's blocking requirements, but the policy has intentionally decided the condition is materially relevant and wants it surfaced -- not a synonym for \"minor failure\"; a `warn` never fails `Verdict.passed` (see `runPolicies` in `src/policy/run-policies.ts`)."
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CheckSchema, RepoContractConfig, ValidatedCheckSchema } from "../types.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Identity function whose only job is type inference: authoring a config
|
|
5
|
+
* through `defineRepoContract` lets each check's `output` (present or
|
|
6
|
+
* absent, and which format) flow into that same check's `policy` parameter
|
|
7
|
+
* type, without the consumer writing any type annotations themselves. It
|
|
8
|
+
* also statically validates every check's `dependsOn` against its sibling
|
|
9
|
+
* check ids (see `ValidatedCheckSchema`) -- a typo'd or self-referencing id
|
|
10
|
+
* fails to compile here rather than only failing at runtime. Performs no
|
|
11
|
+
* other validation and no cloning -- `runRepoContract` validates whatever
|
|
12
|
+
* config it is ultimately given, whether or not it passed through this
|
|
13
|
+
* function first.
|
|
14
|
+
*
|
|
15
|
+
* `const TChecks` (TypeScript 5.0's `const` type parameter modifier) keeps
|
|
16
|
+
* each check's own configuration -- notably whether `output` is present at
|
|
17
|
+
* all -- from being widened during inference; without it, TypeScript's
|
|
18
|
+
* inference for a `Record` of heterogeneous generic entries does not
|
|
19
|
+
* reliably preserve that per-check shape once a callback property
|
|
20
|
+
* (`policy`) is also present. See `InferParsedValue` in types.ts for the
|
|
21
|
+
* related limitation this does not fully solve.
|
|
22
|
+
*
|
|
23
|
+
* `TChecks` is inferred from the plain, unwrapped `RepoContractConfig<TChecks>`
|
|
24
|
+
* position -- the `ValidatedCheckSchema<TChecks>` constraint on `checks` is
|
|
25
|
+
* intersected in afterward, computed from that already-inferred `TChecks`,
|
|
26
|
+
* rather than substituted in its place. Inferring `TChecks` directly from a
|
|
27
|
+
* mapped/conditional type over itself (as `ValidatedCheckSchema` is) loses
|
|
28
|
+
* the contextual typing every check's `policy` callback otherwise gets --
|
|
29
|
+
* another real, confirmed TypeScript inference limitation, distinct from
|
|
30
|
+
* the `output`-to-`policy` one above.
|
|
31
|
+
* @param config - the config to type-check and return unchanged.
|
|
32
|
+
* @returns the same `config` object, untouched and uncloned.
|
|
33
|
+
*/
|
|
34
|
+
export function defineRepoContract<const TChecks extends CheckSchema>(
|
|
35
|
+
config: RepoContractConfig<TChecks> & { readonly checks: ValidatedCheckSchema<TChecks> },
|
|
36
|
+
): RepoContractConfig<TChecks> {
|
|
37
|
+
return config
|
|
38
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { InvalidCheckConfigError } from "../errors.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Consumes a `\`-escape sequence starting at `run[i]`, if one applies here. A backslash escapes the
|
|
5
|
+
* next character (and is itself dropped) when unquoted or inside a double-quoted span; inside
|
|
6
|
+
* single quotes it is a literal character, exactly as in every POSIX shell -- so a Windows path in
|
|
7
|
+
* single quotes survives intact. A trailing backslash with nothing left to escape is not an escape
|
|
8
|
+
* and is kept literally by the caller.
|
|
9
|
+
* @param run - the full command string being tokenized.
|
|
10
|
+
* @param i - the index of the candidate backslash.
|
|
11
|
+
* @param quote - the current quote context (`"'"`, `'"'`, or `null` for unquoted).
|
|
12
|
+
* @returns the escaped character and the index just past the two-character sequence, or `undefined` when no escape applies at `i`.
|
|
13
|
+
*/
|
|
14
|
+
function consumeEscape(
|
|
15
|
+
run: string,
|
|
16
|
+
i: number,
|
|
17
|
+
quote: "'" | '"' | null,
|
|
18
|
+
): { readonly value: string; readonly next: number } | undefined {
|
|
19
|
+
if (run[i] !== "\\" || quote === "'") return undefined
|
|
20
|
+
// No explicit `i + 1 < run.length` pre-check: a trailing backslash at
|
|
21
|
+
// end-of-string reads `run[i + 1]` as `undefined` (which `noUncheckedIndexedAccess`
|
|
22
|
+
// already types), and the `escaped === undefined` guard below returns
|
|
23
|
+
// `undefined` for exactly that case -- so the caller keeps the backslash
|
|
24
|
+
// literal. One guard, one behavior, nothing to mutation-suppress.
|
|
25
|
+
const escaped: string | undefined = run[i + 1]
|
|
26
|
+
if (escaped === undefined) return undefined
|
|
27
|
+
return { value: escaped, next: i + 2 }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Single-character shell/multi-command operators rejected outright when they
|
|
31
|
+
// appear unquoted. A literal newline (`\n`/`\r`) and the two-character `$(`
|
|
32
|
+
// are handled separately in `rejectUnquotedOperator` -- every entry here is
|
|
33
|
+
// exactly one character, matched by a single Set lookup rather than a chain
|
|
34
|
+
// of per-character `if`s.
|
|
35
|
+
const UNQUOTED_SHELL_OPERATORS: ReadonlySet<string> = new Set([";", "&", "|", "`", "<", ">"])
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Throws `InvalidCheckConfigError` when the unquoted character `char` at `run[i]` is a
|
|
39
|
+
* shell/multi-command operator repo-contract never interprets (`;`, `&`, `|`, a backtick, `<`, `>`,
|
|
40
|
+
* `$(`, or a literal newline). Returns normally when `char` is legitimate literal argv content --
|
|
41
|
+
* glob characters and a bare `$` deliberately included (see `tokenizeRunString`'s doc comment).
|
|
42
|
+
* @param char - the character under the cursor, already known defined by the caller.
|
|
43
|
+
* @param run - the full command string, needed only to look one character ahead for `$(`.
|
|
44
|
+
* @param i - the index of `char` within `run`.
|
|
45
|
+
* @param checkId - identifies which check's `run` was invalid, used in the thrown error message.
|
|
46
|
+
*/
|
|
47
|
+
function rejectUnquotedOperator(char: string, run: string, i: number, checkId: string): void {
|
|
48
|
+
const reject = (operator: string): never => {
|
|
49
|
+
throw new InvalidCheckConfigError(
|
|
50
|
+
checkId,
|
|
51
|
+
`run string contains an unquoted "${operator}" -- repo-contract never invokes a shell for ` +
|
|
52
|
+
`string-form "run", so shell operators are not interpreted. Use "run: [...]" (array ` +
|
|
53
|
+
`form) to pass "${operator}" as a literal argument, or set "shell: true" to opt into ` +
|
|
54
|
+
`real shell execution.`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (char === "\n" || char === "\r") reject("newline")
|
|
59
|
+
if (UNQUOTED_SHELL_OPERATORS.has(char)) reject(char)
|
|
60
|
+
if (char === "$" && run[i + 1] === "(") reject("$(")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Splits a `run` string into argv (executable + arguments) without invoking
|
|
65
|
+
* a shell -- no shell operator is ever executed, no glob is ever expanded by
|
|
66
|
+
* this package, no environment variable is ever substituted. The result is
|
|
67
|
+
* deterministic: the same input string always produces the same argv array.
|
|
68
|
+
*
|
|
69
|
+
* Quoting: `'...'` and `"..."` group whitespace into a single argument and
|
|
70
|
+
* are themselves stripped from the resulting token. `\` escapes the next
|
|
71
|
+
* character (and is itself stripped) when unquoted or inside a double-quoted
|
|
72
|
+
* span; inside single quotes it is a literal character, exactly as in every
|
|
73
|
+
* POSIX shell -- so a Windows path in single quotes survives intact.
|
|
74
|
+
* Unquoted whitespace (space, tab) separates tokens.
|
|
75
|
+
*
|
|
76
|
+
* Rejected outright (throws `InvalidCheckConfigError`, checkId identifies
|
|
77
|
+
* which check's `run` was invalid): any *unquoted* occurrence of a true
|
|
78
|
+
* shell/multi-command operator -- `;`, `&`, `|`, a backtick, `$(`, `<`, `>`,
|
|
79
|
+
* or a literal newline. A string containing one of these almost always
|
|
80
|
+
* reflects a mistaken assumption that shell interpretation is happening;
|
|
81
|
+
* the fix is either `run: [...]` (array form, bypasses tokenization
|
|
82
|
+
* entirely) or explicit `shell: true`.
|
|
83
|
+
*
|
|
84
|
+
* Deliberately NOT rejected: glob characters (`*`, `?`, `~`, `[`, `]`, `{`,
|
|
85
|
+
* `}`) and a bare `$`. These are common, legitimate literal argv content --
|
|
86
|
+
* many CLI tools (eslint, prettier, tsc) accept and internally expand glob
|
|
87
|
+
* patterns themselves, e.g. `eslint "src/**\/*.ts"` -- and since no shell is
|
|
88
|
+
* ever invoked here, they carry zero shell-injection risk regardless of
|
|
89
|
+
* where they appear in the string.
|
|
90
|
+
* @param run - the command string to tokenize.
|
|
91
|
+
* @param checkId - identifies which check's `run` was invalid, used in the thrown error message.
|
|
92
|
+
* @returns the tokenized argv (executable followed by its arguments).
|
|
93
|
+
*/
|
|
94
|
+
export function tokenizeRunString(run: string, checkId: string): readonly string[] {
|
|
95
|
+
const tokens: string[] = []
|
|
96
|
+
let current = ""
|
|
97
|
+
let hasCurrent = false
|
|
98
|
+
let quote: "'" | '"' | null = null
|
|
99
|
+
let i = 0
|
|
100
|
+
|
|
101
|
+
// Loosening the `i < run.length` bound to `i <= run.length` is
|
|
102
|
+
// behaviorally invisible: the one extra iteration it would permit reads
|
|
103
|
+
// `run[run.length]`, which is `undefined`, and is caught immediately below
|
|
104
|
+
// by the (itself unmutatable, for the same `noUncheckedIndexedAccess`
|
|
105
|
+
// reason) `char === undefined` check -- confirmed equivalent by exhaustive
|
|
106
|
+
// differential testing against a wide corpus of inputs, not assumed.
|
|
107
|
+
//
|
|
108
|
+
// `iterations` is a second, independent forward-progress bound: every loop
|
|
109
|
+
// pass that doesn't throw/break advances `i` by exactly 1 or 2, so no
|
|
110
|
+
// correct execution ever needs more than `run.length` passes -- a
|
|
111
|
+
// regression that makes `i` stand still or move backward (a `+=`
|
|
112
|
+
// accidentally becoming `-=`), or that wipes the loop body entirely,
|
|
113
|
+
// would otherwise hang forever instead of failing loudly. It is
|
|
114
|
+
// deliberately tracked in the `for` statement's own update/condition
|
|
115
|
+
// clauses rather than inside the loop body: those clauses sit outside the
|
|
116
|
+
// body's own `{ ... }` block, so they keep running (and keep bounding the
|
|
117
|
+
// loop) even under a mutation that replaces the entire body with `{}`,
|
|
118
|
+
// which a bound placed inside the body could not survive.
|
|
119
|
+
//
|
|
120
|
+
// Every mutation of this line's own clauses (loosening either half of the
|
|
121
|
+
// `&&`, swapping it for `||`, or reversing `iterations`' own direction) is
|
|
122
|
+
// itself equivalent as long as the *body* still advances `i` correctly:
|
|
123
|
+
// `i < run.length` alone already terminates the loop at the right point
|
|
124
|
+
// for correct code, with the `iterations` bound only ever mattering in
|
|
125
|
+
// combination with a genuine body regression -- confirmed empirically:
|
|
126
|
+
// mutating this line in isolation (leaving the body untouched) produces no
|
|
127
|
+
// observable difference. It exists precisely to convert the *body*
|
|
128
|
+
// mutations described above from an unkillable hang into a fast, visible
|
|
129
|
+
// test failure, not to be independently killable itself.
|
|
130
|
+
// Stryker disable next-line ConditionalExpression,EqualityOperator,LogicalOperator,AssignmentOperator,BlockStatement -- loosening i < run.length to i <= run.length is behaviorally invisible since the unmutatable char === undefined check right after already catches it, and the iterations bound is a second, independent forward-progress bound tracked in this line's own clauses (outside the body's braces) specifically so a body-emptying mutation can't produce an unkillable hang; every mutation of this line's own clauses is equivalent as long as the body still advances i correctly, confirmed empirically.
|
|
131
|
+
for (let iterations = 0; i < run.length && iterations <= run.length; iterations += 1) {
|
|
132
|
+
const char = run[i]
|
|
133
|
+
// Unreachable given the loop condition (`i < run.length` already
|
|
134
|
+
// guarantees `run[i]` is defined) -- kept only because
|
|
135
|
+
// `noUncheckedIndexedAccess` can't itself express that invariant.
|
|
136
|
+
// Stryker disable next-line ConditionalExpression -- unreachable given the loop's own i < run.length guard already ensures run[i] is defined; kept only because noUncheckedIndexedAccess can't itself express that invariant.
|
|
137
|
+
if (char === undefined) break
|
|
138
|
+
|
|
139
|
+
if (quote !== null) {
|
|
140
|
+
const escape = consumeEscape(run, i, quote)
|
|
141
|
+
if (escape !== undefined) {
|
|
142
|
+
current += escape.value
|
|
143
|
+
i = escape.next
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
if (char === quote) {
|
|
147
|
+
quote = null
|
|
148
|
+
i += 1
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
current += char
|
|
152
|
+
i += 1
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (char === "'" || char === '"') {
|
|
157
|
+
quote = char
|
|
158
|
+
hasCurrent = true
|
|
159
|
+
i += 1
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// An unquoted backslash before a newline is a shell line-continuation.
|
|
164
|
+
// repo-contract never interprets one -- and it must be rejected *here*,
|
|
165
|
+
// before `consumeEscape` below, because `consumeEscape` would otherwise
|
|
166
|
+
// treat `\<newline>` as an ordinary escape: splice a literal newline into
|
|
167
|
+
// the token and advance the cursor past the newline, so the bare-newline
|
|
168
|
+
// rejection further down never runs.
|
|
169
|
+
const nextChar = run[i + 1]
|
|
170
|
+
if (char === "\\" && (nextChar === "\n" || nextChar === "\r")) {
|
|
171
|
+
throw new InvalidCheckConfigError(
|
|
172
|
+
checkId,
|
|
173
|
+
`run string contains an unquoted line continuation (a backslash before a newline) -- ` +
|
|
174
|
+
`repo-contract never invokes a shell for string-form "run". Use "run: [...]" (array ` +
|
|
175
|
+
`form), or set "shell: true" to opt into real shell execution.`,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const escape = consumeEscape(run, i, null)
|
|
180
|
+
if (escape !== undefined) {
|
|
181
|
+
current += escape.value
|
|
182
|
+
hasCurrent = true
|
|
183
|
+
i = escape.next
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (char === " " || char === "\t") {
|
|
188
|
+
if (hasCurrent) {
|
|
189
|
+
tokens.push(current)
|
|
190
|
+
current = ""
|
|
191
|
+
hasCurrent = false
|
|
192
|
+
}
|
|
193
|
+
i += 1
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
rejectUnquotedOperator(char, run, i, checkId)
|
|
198
|
+
|
|
199
|
+
current += char
|
|
200
|
+
hasCurrent = true
|
|
201
|
+
i += 1
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (quote !== null) {
|
|
205
|
+
throw new InvalidCheckConfigError(checkId, `run string has an unterminated ${quote} quote.`)
|
|
206
|
+
}
|
|
207
|
+
if (hasCurrent) tokens.push(current)
|
|
208
|
+
|
|
209
|
+
if (tokens.length === 0) {
|
|
210
|
+
throw new InvalidCheckConfigError(checkId, "run string is empty or contains only whitespace.")
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return tokens
|
|
214
|
+
}
|