@bjoernrochel/sevro 0.1.0-rc.2 → 0.1.0-rc.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/CONTRIBUTING.md +170 -0
  2. package/LICENSE +63 -136
  3. package/README.md +83 -66
  4. package/docs/README.md +44 -0
  5. package/docs/acknowledgements.md +14 -0
  6. package/docs/architecture.md +146 -0
  7. package/docs/assets/README.md +27 -0
  8. package/docs/assets/sevro-logo.png +0 -0
  9. package/docs/assets/sevro-symbol.svg +1 -0
  10. package/docs/assets/sevro-wordmark.svg +1 -0
  11. package/docs/builtin-graders-v1.md +41 -13
  12. package/docs/codex-host.md +184 -52
  13. package/docs/contracts.md +19 -0
  14. package/docs/creating-cases.md +139 -0
  15. package/docs/development-cli.md +190 -30
  16. package/docs/documentation-quality.md +61 -0
  17. package/docs/evaluation-workflow.md +110 -0
  18. package/docs/evidence/code-mode-controls-71.md +64 -0
  19. package/docs/evidence/code-mode-programs-85.md +64 -0
  20. package/docs/evidence/codex-state-73/README.md +204 -0
  21. package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
  22. package/docs/evidence/codex-state-73/results.json +661 -0
  23. package/docs/evidence/local-http-74.md +104 -0
  24. package/docs/evidence/runtime-startup-30/README.md +185 -0
  25. package/docs/evidence/runtime-startup-30/after.json +319 -0
  26. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
  27. package/docs/evidence/runtime-startup-30/before.json +403 -0
  28. package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
  29. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
  30. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
  31. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
  32. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
  33. package/docs/evidence/skill-reads-70/results.json +823 -0
  34. package/docs/evidence.md +21 -0
  35. package/docs/extension-protocol-v1.md +50 -16
  36. package/docs/fixture-cleanup-validation.md +3 -0
  37. package/docs/getting-started.md +102 -0
  38. package/docs/guide-evaluation.md +89 -0
  39. package/docs/guide-verification.json +239 -0
  40. package/docs/identity-v1.md +35 -1
  41. package/docs/installing.md +88 -0
  42. package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
  43. package/docs/licensing.md +42 -0
  44. package/docs/native-controls.md +76 -5
  45. package/docs/native-hosts.md +228 -0
  46. package/docs/reading-results.md +141 -0
  47. package/docs/recipes.md +489 -0
  48. package/docs/releases.md +81 -40
  49. package/docs/report-v1.md +29 -1
  50. package/docs/results-v1.md +180 -0
  51. package/docs/run-settings-v1.md +147 -0
  52. package/docs/running-evaluations.md +31 -0
  53. package/docs/runtime-v1.md +614 -0
  54. package/docs/specs/repository-guide.md +58 -0
  55. package/docs/troubleshooting.md +30 -0
  56. package/docs/typescript-quality.md +234 -0
  57. package/docs/usability-study.md +114 -0
  58. package/examples/basic/host.ts +2 -2
  59. package/examples/recipes/candidate.ts +26 -0
  60. package/examples/recipes/cases/coding-change.json +21 -0
  61. package/examples/recipes/cases/host-tools.json +21 -0
  62. package/examples/recipes/cases/mise.json +21 -0
  63. package/examples/recipes/cases/node.json +21 -0
  64. package/examples/recipes/cases/python-uv.json +21 -0
  65. package/examples/recipes/cases/semantic.json +17 -0
  66. package/examples/recipes/coding-change/check.ts +23 -0
  67. package/examples/recipes/extension-artifacts.ts +65 -0
  68. package/examples/recipes/extension-methods.ts +174 -0
  69. package/examples/recipes/extension.ts +37 -0
  70. package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
  71. package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
  72. package/examples/recipes/repository-skill/README.md +3 -0
  73. package/examples/recipes/run-inputs.ts +188 -0
  74. package/examples/recipes/run-options.ts +131 -0
  75. package/examples/recipes/run.ts +63 -0
  76. package/examples/recipes/runtime/empty.json +1 -0
  77. package/examples/recipes/runtime/goals.json +1 -0
  78. package/examples/recipes/runtime/mise.json +6 -0
  79. package/examples/recipes/runtime/path.json +1 -0
  80. package/examples/recipes/runtime/plugins.json +4 -0
  81. package/examples/recipes/runtime/python-uv.json +10 -0
  82. package/examples/recipes/semantic-judge.ts +29 -0
  83. package/examples/templates/coding/adapter.ts +17 -0
  84. package/examples/templates/coding/case.json +15 -0
  85. package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
  86. package/examples/templates/repository-skill/extension-data.ts +64 -0
  87. package/examples/templates/repository-skill/extension.ts +31 -0
  88. package/examples/templates/response/adapter.ts +12 -0
  89. package/examples/templates/response/case.json +13 -0
  90. package/package.json +39 -6
  91. package/schemas/report-v1.schema.json +9 -0
  92. package/schemas/run-evidence-v1.schema.json +2 -1
  93. package/schemas/runtime-v1.schema.json +193 -0
  94. package/src/advisory.ts +10 -5
  95. package/src/candidate-artifacts.ts +208 -0
  96. package/src/candidate-codex-state.ts +185 -0
  97. package/src/case-validation.ts +105 -0
  98. package/src/cli/cancellation.ts +20 -0
  99. package/src/cli/command.ts +81 -0
  100. package/src/cli/completion.ts +15 -0
  101. package/src/cli/demo.ts +59 -0
  102. package/src/cli/evaluation.ts +55 -0
  103. package/src/cli/extension.ts +79 -0
  104. package/src/cli/help.ts +222 -0
  105. package/src/cli/hosts.ts +59 -0
  106. package/src/cli/init-settings.ts +30 -0
  107. package/src/cli/init.ts +111 -0
  108. package/src/cli/invocation.ts +627 -0
  109. package/src/cli/loading.ts +151 -0
  110. package/src/cli/preparation.ts +163 -0
  111. package/src/cli/presentation.ts +91 -0
  112. package/src/cli/progress.ts +6 -0
  113. package/src/cli/run-manifest.ts +133 -0
  114. package/src/cli/run-options.ts +101 -0
  115. package/src/cli/run-overrides.ts +83 -0
  116. package/src/cli/settings.ts +160 -0
  117. package/src/cli/summary.ts +43 -0
  118. package/src/cli.ts +13 -843
  119. package/src/contract-types.ts +94 -0
  120. package/src/engine.ts +19 -2373
  121. package/src/evaluation/advisory.ts +150 -0
  122. package/src/evaluation/check-plan.ts +510 -0
  123. package/src/evaluation/collection.ts +88 -0
  124. package/src/evaluation/context.ts +10 -0
  125. package/src/evaluation/grading-host.ts +39 -0
  126. package/src/evaluation/grading.ts +258 -0
  127. package/src/evaluation/invocation-plan.ts +548 -0
  128. package/src/evaluation/plan.ts +736 -0
  129. package/src/evaluation/progress.ts +19 -0
  130. package/src/evaluation/resources.ts +241 -0
  131. package/src/evaluation/run.ts +206 -0
  132. package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
  133. package/src/evaluation/semantic.ts +312 -0
  134. package/src/evaluation/trial.ts +866 -0
  135. package/src/evaluation/types.ts +207 -0
  136. package/src/evidence/host-results.ts +92 -0
  137. package/src/evidence/retained-artifacts.ts +18 -0
  138. package/src/evidence/trial-writer.ts +203 -0
  139. package/src/evidence/usage.ts +17 -0
  140. package/src/extension-client.ts +228 -148
  141. package/src/extension-session.ts +326 -195
  142. package/src/fixtures/advisory.ts +338 -0
  143. package/src/fixtures/bin.ts +57 -0
  144. package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
  145. package/src/fixtures/create.ts +187 -0
  146. package/src/fixtures/generated.ts +300 -0
  147. package/src/fixtures/git-hooks.ts +24 -0
  148. package/src/fixtures/preparation.ts +270 -0
  149. package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
  150. package/src/fixtures/setup.ts +224 -0
  151. package/src/fixtures/tools.ts +88 -0
  152. package/src/fixtures/workspace-state.ts +212 -0
  153. package/src/generated/cli-result.cjs +2 -0
  154. package/src/generated/cli-result.d.cts +3 -0
  155. package/src/generated/extension.cjs +2 -0
  156. package/src/generated/extension.d.cts +3 -0
  157. package/src/generated/report.cjs +2 -0
  158. package/src/generated/report.d.cts +3 -0
  159. package/src/generated/run-evidence.cjs +2 -0
  160. package/src/generated/run-evidence.d.cts +3 -0
  161. package/src/generated/runtime.cjs +2 -0
  162. package/src/generated/runtime.d.cts +3 -0
  163. package/src/graders/artifact.ts +56 -23
  164. package/src/graders/git-head.ts +79 -75
  165. package/src/graders/output.ts +222 -140
  166. package/src/graders/semantic.ts +102 -43
  167. package/src/graders/shell.ts +379 -207
  168. package/src/host-diagnostic-redaction.ts +52 -0
  169. package/src/host-diagnostics.ts +319 -0
  170. package/src/hosts/claude-continuation.ts +63 -18
  171. package/src/hosts/claude-credential.ts +43 -62
  172. package/src/hosts/claude-events.ts +86 -51
  173. package/src/hosts/claude-keychain.ts +59 -0
  174. package/src/hosts/claude-native-goal.ts +57 -33
  175. package/src/hosts/claude-process.ts +111 -0
  176. package/src/hosts/claude-repository-invocation.ts +208 -117
  177. package/src/hosts/claude-settings.ts +240 -21
  178. package/src/hosts/claude-skill-activations.ts +166 -0
  179. package/src/hosts/claude-state.ts +403 -0
  180. package/src/hosts/claude-tool-calls.ts +171 -106
  181. package/src/hosts/claude.ts +561 -451
  182. package/src/hosts/codex-app-server.ts +326 -178
  183. package/src/hosts/codex-configuration.ts +44 -18
  184. package/src/hosts/codex-events.ts +97 -60
  185. package/src/hosts/codex-exec-controls.ts +516 -0
  186. package/src/hosts/codex-invocation.ts +70 -0
  187. package/src/hosts/codex-marketplace.ts +283 -0
  188. package/src/hosts/codex-native-calls.ts +546 -500
  189. package/src/hosts/codex-process.ts +202 -0
  190. package/src/hosts/codex-profile.ts +257 -28
  191. package/src/hosts/codex-session-files.ts +106 -0
  192. package/src/hosts/codex-state.ts +492 -0
  193. package/src/hosts/codex.ts +783 -832
  194. package/src/hosts/isolation-common.ts +94 -0
  195. package/src/hosts/isolation-roots.ts +54 -27
  196. package/src/hosts/isolation.ts +10 -0
  197. package/src/hosts/linux-sandbox.ts +159 -0
  198. package/src/hosts/mac-sandbox.ts +187 -61
  199. package/src/hosts/native-controls.ts +152 -62
  200. package/src/hosts/process-lifecycle.ts +37 -0
  201. package/src/hosts/protected-worktrees.ts +51 -19
  202. package/src/hosts/skill-hook-catalog.ts +131 -0
  203. package/src/hosts/skill-hook-handler.ts +374 -0
  204. package/src/hosts/skill-hooks.ts +580 -0
  205. package/src/hosts/workspace-fingerprint.ts +55 -35
  206. package/src/identity.ts +102 -48
  207. package/src/instrumentation.ts +109 -48
  208. package/src/native-transcript-state.ts +131 -0
  209. package/src/native-transcript-view.ts +90 -0
  210. package/src/native-transcripts.ts +286 -0
  211. package/src/process-lifecycle.ts +150 -0
  212. package/src/provenance.ts +161 -81
  213. package/src/prune.ts +215 -0
  214. package/src/report.ts +172 -140
  215. package/src/resolved-case.ts +33 -0
  216. package/src/results-lock.ts +29 -0
  217. package/src/results.ts +133 -86
  218. package/src/run-owner.ts +154 -63
  219. package/src/runtime/config.ts +456 -0
  220. package/src/runtime/discovery-apple.ts +90 -0
  221. package/src/runtime/discovery-providers.ts +63 -0
  222. package/src/runtime/discovery.ts +70 -0
  223. package/src/runtime/hook-processes.ts +97 -0
  224. package/src/runtime/hooks.ts +476 -0
  225. package/src/runtime/paths.ts +313 -0
  226. package/src/runtime/query.ts +85 -0
  227. package/src/runtime/seeds.ts +134 -0
  228. package/src/runtime/shared-cache.ts +143 -0
  229. package/src/runtime/state.ts +143 -0
  230. package/src/schema-types.ts +198 -0
  231. package/src/schema.ts +15 -19
  232. package/src/transcript-retention.ts +146 -0
  233. package/src/value-guards.ts +20 -0
  234. package/src/advisory-fixture.ts +0 -282
  235. package/src/fixture-bin.ts +0 -87
  236. package/src/fixture-setup.ts +0 -163
  237. package/src/generated-fixture.ts +0 -220
  238. package/src/git-hooks.ts +0 -63
  239. package/src/hosts/claude-nested-skills.ts +0 -218
  240. package/src/hosts/codex-command-output.ts +0 -399
  241. package/src/hosts/codex-executor-command.ts +0 -195
  242. package/src/hosts/codex-skill-reads.ts +0 -391
  243. package/src/preparation.ts +0 -211
@@ -1,3 +1,4 @@
1
+ import { isRecord, isUnknownArray } from "../value-guards";
1
2
  import { isDeepStrictEqual } from "node:util";
2
3
  import Ajv2020, { type ValidateFunction } from "ajv/dist/2020.js";
3
4
  import { canonicalJson } from "../identity";
@@ -83,144 +84,177 @@ function pointerValue(value: unknown, pointer: string): unknown {
83
84
  }, value);
84
85
  }
85
86
 
87
+ function subsetRegexPattern(value: Record<string, unknown>): boolean {
88
+ return Object.keys(value).length === 1 && Object.hasOwn(value, "$regex");
89
+ }
90
+ function validateSubsetRegex(pattern: unknown): void {
91
+ if (typeof pattern !== "string") throw new Error("invalid subset regex");
92
+ try {
93
+ new RegExp(pattern);
94
+ } catch (cause) {
95
+ throw new Error("invalid subset regex", { cause });
96
+ }
97
+ }
86
98
  function validateSubsetPatterns(value: unknown): void {
87
- if (!value || typeof value !== "object") return;
88
- if (Array.isArray(value)) {
99
+ if (isUnknownArray(value)) {
89
100
  value.forEach(validateSubsetPatterns);
90
101
  return;
91
102
  }
92
- const record = value as Record<string, unknown>;
93
- if (Object.keys(record).length === 1 && Object.hasOwn(record, "$regex")) {
94
- if (typeof record.$regex !== "string")
95
- throw new Error("invalid subset regex");
96
- try {
97
- new RegExp(record.$regex);
98
- } catch {
99
- throw new Error("invalid subset regex");
100
- }
103
+ if (!isRecord(value)) return;
104
+ if (subsetRegexPattern(value)) {
105
+ validateSubsetRegex(value.$regex);
101
106
  return;
102
107
  }
103
- Object.values(record).forEach(validateSubsetPatterns);
108
+ Object.values(value).forEach(validateSubsetPatterns);
104
109
  }
105
110
 
106
111
  function containsSubset(value: unknown, expected: unknown): boolean {
107
- if (Array.isArray(expected))
108
- return (
109
- Array.isArray(value) &&
110
- expected.every((part) => value.some((item) => containsSubset(item, part)))
111
- );
112
- if (expected && typeof expected === "object") {
113
- const record = expected as Record<string, unknown>;
114
- if (Object.keys(record).length === 1 && typeof record.$regex === "string")
115
- return typeof value === "string" && new RegExp(record.$regex).test(value);
116
- return Boolean(
117
- value &&
118
- typeof value === "object" &&
119
- Object.entries(record).every(([key, part]) =>
120
- containsSubset((value as Record<string, unknown>)[key], part),
121
- ),
122
- );
123
- }
112
+ if (isUnknownArray(expected)) return arraySubset(value, expected);
113
+ if (isRecord(expected)) return recordSubset(value, expected);
124
114
  return Object.is(value, expected);
125
115
  }
116
+ function arraySubset(value: unknown, expected: unknown[]): boolean {
117
+ return (
118
+ isUnknownArray(value) &&
119
+ expected.every((part) => value.some((item) => containsSubset(item, part)))
120
+ );
121
+ }
122
+ function recordSubset(
123
+ value: unknown,
124
+ expected: Record<string, unknown>,
125
+ ): boolean {
126
+ if (Object.keys(expected).length === 1 && typeof expected.$regex === "string")
127
+ return typeof value === "string" && new RegExp(expected.$regex).test(value);
128
+ return objectSubset(value, expected);
129
+ }
130
+ function objectSubset(
131
+ value: unknown,
132
+ expected: Record<string, unknown>,
133
+ ): boolean {
134
+ if (!value || typeof value !== "object") return false;
135
+ const actual = value as Record<string, unknown>;
136
+ return Object.entries(expected).every(([key, part]) =>
137
+ containsSubset(actual[key], part),
138
+ );
139
+ }
140
+
141
+ type JsonConfiguration = Record<string, unknown> & {
142
+ pointer?: string;
143
+ exactDocument?: boolean;
144
+ };
145
+ function validPointer(pointer: string): boolean {
146
+ return (
147
+ pointer === "" || (pointer.startsWith("/") && !/~(?![01])/.test(pointer))
148
+ );
149
+ }
150
+ function requireOptionalPointer(pointer: unknown): void {
151
+ if (pointer === undefined) return;
152
+ if (typeof pointer !== "string")
153
+ throw new Error("JSON pointer must be a string");
154
+ if (!validPointer(pointer)) throw new Error("invalid JSON pointer");
155
+ }
156
+ function requireExactDocument(value: unknown): void {
157
+ if (value !== undefined && typeof value !== "boolean")
158
+ throw new Error("exactDocument must be boolean");
159
+ }
160
+ function requireJsonConfiguration(
161
+ config: Record<string, unknown>,
162
+ ): asserts config is JsonConfiguration {
163
+ requireOptionalPointer(config.pointer);
164
+ requireExactDocument(config.exactDocument);
165
+ }
126
166
 
127
167
  function compileJson(
128
168
  config: Record<string, unknown>,
129
169
  ): PreparedOutputCheck["evaluate"] {
130
170
  configKeys(config, ["pointer", "equals", "contains", "exactDocument"]);
131
- if (config.pointer !== undefined && typeof config.pointer !== "string")
132
- throw new Error("JSON pointer must be a string");
133
- if (
134
- config.pointer !== undefined &&
135
- config.pointer !== "" &&
136
- (!config.pointer.startsWith("/") || /~(?![01])/.test(config.pointer))
137
- )
138
- throw new Error("invalid JSON pointer");
139
- if (
140
- config.exactDocument !== undefined &&
141
- typeof config.exactDocument !== "boolean"
142
- )
143
- throw new Error("exactDocument must be boolean");
171
+ requireJsonConfiguration(config);
144
172
  if (Object.hasOwn(config, "equals")) canonicalJson(config.equals);
145
173
  if (Object.hasOwn(config, "contains")) {
146
174
  canonicalJson(config.contains);
147
175
  validateSubsetPatterns(config.contains);
148
176
  }
149
- return (_text, document) => {
150
- if (document.error) return "output is not valid JSON";
151
- if (config.exactDocument && !document.exact)
152
- return "output is not exactly one JSON document";
153
- let selected: unknown;
154
- try {
155
- selected = pointerValue(
156
- document.value,
157
- (config.pointer as string | undefined) ?? "",
158
- );
159
- } catch {
160
- return "JSON pointer does not resolve";
161
- }
162
- if (
163
- Object.hasOwn(config, "equals") &&
164
- !isDeepStrictEqual(selected, config.equals)
165
- )
166
- return "JSON value did not equal expectation";
167
- if (
168
- Object.hasOwn(config, "contains") &&
169
- (!Array.isArray(selected) ||
170
- !selected.some((item) => containsSubset(item, config.contains)))
171
- )
172
- return "JSON array did not contain expected item";
173
- return undefined;
174
- };
177
+ return (_text, document) => evaluateJson(config, document);
178
+ }
179
+ function documentFailure(
180
+ document: ParsedDocument,
181
+ exactDocument: unknown,
182
+ ): string | undefined {
183
+ if (document.error) return "output is not valid JSON";
184
+ if (exactDocument && !document.exact)
185
+ return "output is not exactly one JSON document";
186
+ return undefined;
187
+ }
188
+ function selectedDocument(
189
+ config: JsonConfiguration,
190
+ document: ParsedDocument,
191
+ ): { value: unknown } | { failure: string } {
192
+ try {
193
+ return { value: pointerValue(document.value, config.pointer ?? "") };
194
+ } catch {
195
+ return { failure: "JSON pointer does not resolve" };
196
+ }
197
+ }
198
+ function equalsFailure(
199
+ selected: unknown,
200
+ config: Record<string, unknown>,
201
+ ): string | undefined {
202
+ return Object.hasOwn(config, "equals") &&
203
+ !isDeepStrictEqual(selected, config.equals)
204
+ ? "JSON value did not equal expectation"
205
+ : undefined;
206
+ }
207
+ function containsFailure(
208
+ selected: unknown,
209
+ config: Record<string, unknown>,
210
+ ): string | undefined {
211
+ if (!Object.hasOwn(config, "contains")) return undefined;
212
+ if (
213
+ !isUnknownArray(selected) ||
214
+ !selected.some((item) => containsSubset(item, config.contains))
215
+ )
216
+ return "JSON array did not contain expected item";
217
+ return undefined;
218
+ }
219
+ function evaluateJson(
220
+ config: JsonConfiguration,
221
+ document: ParsedDocument,
222
+ ): string | undefined {
223
+ const failure = documentFailure(document, config.exactDocument);
224
+ if (failure) return failure;
225
+ const selected = selectedDocument(config, document);
226
+ if ("failure" in selected) return selected.failure;
227
+ return (
228
+ equalsFailure(selected.value, config) ??
229
+ containsFailure(selected.value, config)
230
+ );
175
231
  }
176
232
 
233
+ function requireSchema(value: unknown): asserts value is object {
234
+ if (!isRecord(value))
235
+ throw new Error("schema check requires an inline JSON Schema object");
236
+ }
177
237
  function compileSchema(
178
238
  config: Record<string, unknown>,
179
239
  ): PreparedOutputCheck["evaluate"] {
180
240
  configKeys(config, ["schema", "exactDocument"]);
181
- if (
182
- !config.schema ||
183
- typeof config.schema !== "object" ||
184
- Array.isArray(config.schema)
185
- )
186
- throw new Error("schema check requires an inline JSON Schema object");
187
- if (
188
- config.exactDocument !== undefined &&
189
- typeof config.exactDocument !== "boolean"
190
- )
191
- throw new Error("exactDocument must be boolean");
241
+ requireSchema(config.schema);
242
+ requireExactDocument(config.exactDocument);
192
243
  canonicalJson(config.schema);
193
244
  let validate: ValidateFunction;
194
245
  try {
195
246
  validate = new Ajv2020({ strict: false }).compile(config.schema);
196
- } catch {
197
- throw new Error("invalid inline JSON Schema");
247
+ } catch (cause) {
248
+ throw new Error("invalid inline JSON Schema", { cause });
198
249
  }
199
- return (_text, document) => {
200
- if (document.error) return "output is not valid JSON";
201
- if (config.exactDocument && !document.exact)
202
- return "output is not exactly one JSON document";
203
- return validate(document.value)
204
- ? undefined
205
- : "output did not match JSON Schema";
206
- };
250
+ return (_text, document) =>
251
+ documentFailure(document, config.exactDocument) ??
252
+ (validate(document.value) ? undefined : "output did not match JSON Schema");
207
253
  }
208
254
 
209
255
  /** One named assertion may combine independent final-message predicates. */
210
- function compileCompositeOutput(
211
- config: Record<string, unknown>,
212
- ): PreparedOutputCheck["evaluate"] {
213
- configKeys(config, [
214
- "validJson",
215
- "schema",
216
- "jsonPath",
217
- "expectJson",
218
- "containsJson",
219
- "expectExact",
220
- "expectRegex",
221
- "notRegex",
222
- "flags",
223
- ]);
256
+
257
+ function requireCompositeTypes(config: Record<string, unknown>): void {
224
258
  if (config.validJson !== undefined && typeof config.validJson !== "boolean")
225
259
  throw new Error("validJson must be a boolean");
226
260
  if (
@@ -228,34 +262,49 @@ function compileCompositeOutput(
228
262
  typeof config.expectExact !== "string"
229
263
  )
230
264
  throw new Error("expectExact must be a string");
265
+ }
266
+ function requireCompositePointer(config: Record<string, unknown>): void {
231
267
  if (
232
268
  (Object.hasOwn(config, "expectJson") ||
233
269
  Object.hasOwn(config, "containsJson")) &&
234
270
  config.jsonPath === undefined
235
271
  )
236
272
  throw new Error("JSON expectations require a pointer");
273
+ }
274
+ function requireCompositeRegex(config: Record<string, unknown>): void {
237
275
  if (
238
276
  config.flags !== undefined &&
239
277
  config.expectRegex === undefined &&
240
278
  config.notRegex === undefined
241
279
  )
242
280
  throw new Error("regex flags require a pattern");
281
+ }
282
+ function compositePointerConfiguration(config: Record<string, unknown>) {
283
+ return {
284
+ pointer: config.jsonPath,
285
+ ...(Object.hasOwn(config, "expectJson")
286
+ ? { equals: config.expectJson }
287
+ : {}),
288
+ ...(Object.hasOwn(config, "containsJson")
289
+ ? { contains: config.containsJson }
290
+ : {}),
291
+ };
292
+ }
293
+ function compositeJsonStages(
294
+ config: Record<string, unknown>,
295
+ ): PreparedOutputCheck["evaluate"][] {
243
296
  const stages: PreparedOutputCheck["evaluate"][] = [];
244
297
  if (config.validJson) stages.push(compileJson({ exactDocument: true }));
245
298
  if (config.schema !== undefined)
246
299
  stages.push(compileSchema({ schema: config.schema }));
247
300
  if (config.jsonPath !== undefined)
248
- stages.push(
249
- compileJson({
250
- pointer: config.jsonPath,
251
- ...(Object.hasOwn(config, "expectJson")
252
- ? { equals: config.expectJson }
253
- : {}),
254
- ...(Object.hasOwn(config, "containsJson")
255
- ? { contains: config.containsJson }
256
- : {}),
257
- }),
258
- );
301
+ stages.push(compileJson(compositePointerConfiguration(config)));
302
+ return stages;
303
+ }
304
+ function compositeTextStages(
305
+ config: Record<string, unknown>,
306
+ ): PreparedOutputCheck["evaluate"][] {
307
+ const stages: PreparedOutputCheck["evaluate"][] = [];
259
308
  if (config.expectExact !== undefined) {
260
309
  const expected = config.expectExact;
261
310
  stages.push((text) =>
@@ -274,6 +323,29 @@ function compileCompositeOutput(
274
323
  flags: config.flags,
275
324
  }),
276
325
  );
326
+ return stages;
327
+ }
328
+ function compileCompositeOutput(
329
+ config: Record<string, unknown>,
330
+ ): PreparedOutputCheck["evaluate"] {
331
+ configKeys(config, [
332
+ "validJson",
333
+ "schema",
334
+ "jsonPath",
335
+ "expectJson",
336
+ "containsJson",
337
+ "expectExact",
338
+ "expectRegex",
339
+ "notRegex",
340
+ "flags",
341
+ ]);
342
+ requireCompositeTypes(config);
343
+ requireCompositePointer(config);
344
+ requireCompositeRegex(config);
345
+ const stages = [
346
+ ...compositeJsonStages(config),
347
+ ...compositeTextStages(config),
348
+ ];
277
349
  return (text, document) => {
278
350
  for (const stage of stages) {
279
351
  const failure = stage(text, document);
@@ -288,29 +360,37 @@ export function prepareOutputChecks(
288
360
  declarations: OutputCheckDeclaration[],
289
361
  ): PreparedOutputCheck[] {
290
362
  const seen = new Set<string>();
291
- return declarations.map(({ id, grader, configuration }) => {
292
- if (!id || seen.has(id))
293
- throw new Error(`duplicate or empty check ID: ${id}`);
294
- seen.add(id);
295
- if (
296
- !configuration ||
297
- typeof configuration !== "object" ||
298
- Array.isArray(configuration)
299
- )
300
- throw new Error(`invalid check configuration: ${id}`);
301
- const evaluate =
302
- grader === "sevro.regex"
303
- ? compileRegex(configuration)
304
- : grader === "sevro.json"
305
- ? compileJson(configuration)
306
- : grader === "sevro.schema"
307
- ? compileSchema(configuration)
308
- : grader === "sevro.output"
309
- ? compileCompositeOutput(configuration)
310
- : undefined;
311
- if (!evaluate) throw new Error(`unsupported output grader: ${grader}`);
312
- return { id, grader, evaluate };
313
- });
363
+ return declarations.map((declaration) =>
364
+ prepareOutputCheck(declaration, seen),
365
+ );
366
+ }
367
+
368
+ function requireOutputConfiguration(
369
+ value: unknown,
370
+ id: string,
371
+ ): asserts value is Record<string, unknown> {
372
+ if (!isRecord(value)) throw new Error(`invalid check configuration: ${id}`);
373
+ }
374
+ const OUTPUT_COMPILERS = new Map<
375
+ string,
376
+ (config: Record<string, unknown>) => PreparedOutputCheck["evaluate"]
377
+ >([
378
+ ["sevro.regex", compileRegex],
379
+ ["sevro.json", compileJson],
380
+ ["sevro.schema", compileSchema],
381
+ ["sevro.output", compileCompositeOutput],
382
+ ]);
383
+ function prepareOutputCheck(
384
+ { id, grader, configuration }: OutputCheckDeclaration,
385
+ seen: Set<string>,
386
+ ): PreparedOutputCheck {
387
+ if (!id || seen.has(id))
388
+ throw new Error(`duplicate or empty check ID: ${id}`);
389
+ seen.add(id);
390
+ requireOutputConfiguration(configuration, id);
391
+ const compiler = OUTPUT_COMPILERS.get(grader);
392
+ if (!compiler) throw new Error(`unsupported output grader: ${grader}`);
393
+ return { id, grader, evaluate: compiler(configuration) };
314
394
  }
315
395
 
316
396
  function parseJsonDocument(text: string): ParsedDocument {
@@ -318,11 +398,13 @@ function parseJsonDocument(text: string): ParsedDocument {
318
398
  return { value: JSON.parse(text), exact: true, error: false };
319
399
  } catch {
320
400
  const fences = [...text.matchAll(/```json[ \t]*\r?\n([\s\S]*?)\r?\n```/gi)];
321
- if (fences.length !== 1) return { value: null, exact: false, error: true };
401
+ const [fence] = fences;
402
+ if (fences.length !== 1 || fence === undefined)
403
+ return { value: null, exact: false, error: true };
322
404
  try {
323
405
  return {
324
- value: JSON.parse(fences[0]![1]!),
325
- exact: text.trim() === fences[0]![0],
406
+ value: JSON.parse(fence.slice(1, 2).join("")),
407
+ exact: text.trim() === fence[0],
326
408
  error: false,
327
409
  };
328
410
  } catch {
@@ -1,3 +1,4 @@
1
+ import { isRecord, isUnknownArray } from "../value-guards";
1
2
  import { validateArtifactPath } from "./artifact";
2
3
  export interface SemanticCheckDeclaration {
3
4
  id: string;
@@ -29,22 +30,12 @@ export function prepareSemanticChecks(
29
30
  if (!id || ids.has(id))
30
31
  throw new Error("semantic check IDs must be unique");
31
32
  ids.add(id);
32
- if (
33
- !configuration ||
34
- Object.keys(configuration).some(
35
- (key) => key !== "proposition" && key !== "artifactPath",
36
- ) ||
37
- typeof configuration.proposition !== "string" ||
38
- !configuration.proposition.trim() ||
39
- Buffer.byteLength(configuration.proposition, "utf8") >
40
- MAX_PROPOSITION_BYTES
41
- )
42
- throw new Error("invalid semantic proposition");
33
+ const proposition = semanticProposition(configuration);
43
34
  if (configuration.artifactPath !== undefined)
44
35
  validateArtifactPath(configuration.artifactPath);
45
36
  return {
46
37
  id,
47
- proposition: configuration.proposition,
38
+ proposition,
48
39
  ...(configuration.artifactPath === undefined
49
40
  ? {}
50
41
  : { artifactPath: configuration.artifactPath }),
@@ -52,6 +43,26 @@ export function prepareSemanticChecks(
52
43
  });
53
44
  }
54
45
 
46
+ function validProposition(value: unknown): value is string {
47
+ return (
48
+ typeof value === "string" &&
49
+ Boolean(value.trim()) &&
50
+ Buffer.byteLength(value, "utf8") <= MAX_PROPOSITION_BYTES
51
+ );
52
+ }
53
+
54
+ function semanticProposition(value: unknown): string {
55
+ if (
56
+ !isRecord(value) ||
57
+ Object.keys(value).some(
58
+ (key) => key !== "proposition" && key !== "artifactPath",
59
+ ) ||
60
+ !validProposition(value.proposition)
61
+ )
62
+ throw new Error("invalid semantic proposition");
63
+ return value.proposition;
64
+ }
65
+
55
66
  function safeJson(value: unknown): string {
56
67
  return JSON.stringify(value)
57
68
  .replaceAll("&", "\\u0026")
@@ -84,7 +95,7 @@ Return exactly one JSON object with one result for each ID and no others:
84
95
  function parseDocument(text: string): unknown {
85
96
  const trimmed = text.trim();
86
97
  const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/i.exec(trimmed);
87
- return JSON.parse(fenced ? fenced[1]! : trimmed);
98
+ return JSON.parse(fenced?.[1] ?? trimmed);
88
99
  }
89
100
 
90
101
  /** Require one bounded verdict for each declared proposition. */
@@ -97,48 +108,96 @@ export function parseSemanticVerdicts(
97
108
  let value: unknown;
98
109
  try {
99
110
  value = parseDocument(text);
100
- } catch {
101
- throw new Error("semantic grader did not return JSON");
111
+ } catch (cause) {
112
+ throw new Error("semantic grader did not return JSON", { cause });
102
113
  }
103
- if (!value || typeof value !== "object" || Array.isArray(value))
104
- throw new Error("semantic grader result must be an object");
105
- const record = value as Record<string, unknown>;
106
- if (Object.keys(record).length !== 1 || !Array.isArray(record.checks))
107
- throw new Error("semantic grader result must contain only checks");
114
+ const entries = semanticVerdictEntries(value);
108
115
  const declared = new Set(checks.map((item) => item.id));
109
116
  const seen = new Set<string>();
110
- const verdicts: SemanticVerdict[] = record.checks.map((item: unknown) => {
111
- if (!item || typeof item !== "object" || Array.isArray(item))
112
- throw new Error("invalid semantic verdict");
113
- const entry = item as Record<string, unknown>;
114
- if (
115
- Object.keys(entry).some(
116
- (key) => key !== "id" && key !== "verdict" && key !== "reason",
117
- ) ||
118
- typeof entry.id !== "string" ||
119
- !declared.has(entry.id) ||
120
- seen.has(entry.id) ||
121
- (entry.verdict !== "pass" && entry.verdict !== "fail") ||
122
- typeof entry.reason !== "string" ||
123
- !entry.reason.trim() ||
124
- entry.reason.length > 4096
125
- )
126
- throw new Error("invalid semantic verdict");
127
- seen.add(entry.id);
128
- return entry as unknown as SemanticVerdict;
129
- });
117
+ const verdicts = entries.map((item) =>
118
+ validatedSemanticVerdict(item, declared, seen),
119
+ );
130
120
  if (seen.size !== declared.size)
131
121
  throw new Error("semantic grader omitted a declared check");
132
122
  return verdicts;
133
123
  }
134
124
 
125
+ function semanticVerdictEntries(value: unknown): unknown[] {
126
+ if (!isRecord(value))
127
+ throw new Error("semantic grader result must be an object");
128
+ if (Object.keys(value).length !== 1 || !isUnknownArray(value.checks))
129
+ throw new Error("semantic grader result must contain only checks");
130
+ return value.checks;
131
+ }
132
+
133
+ function semanticVerdictRecord(value: unknown): Record<string, unknown> {
134
+ if (!isRecord(value)) throw new Error("invalid semantic verdict");
135
+ return value;
136
+ }
137
+
138
+ function validVerdictKeys(entry: Record<string, unknown>): boolean {
139
+ return !Object.keys(entry).some(
140
+ (key) => key !== "id" && key !== "verdict" && key !== "reason",
141
+ );
142
+ }
143
+
144
+ function validVerdictId(
145
+ value: unknown,
146
+ declared: Set<string>,
147
+ seen: Set<string>,
148
+ ): value is string {
149
+ return typeof value === "string" && declared.has(value) && !seen.has(value);
150
+ }
151
+
152
+ function validVerdict(value: unknown): value is "pass" | "fail" {
153
+ return value === "pass" || value === "fail";
154
+ }
155
+
156
+ function validVerdictReason(value: unknown): value is string {
157
+ return (
158
+ typeof value === "string" && Boolean(value.trim()) && value.length <= 4096
159
+ );
160
+ }
161
+
162
+ function assertSemanticVerdict(
163
+ entry: Record<string, unknown>,
164
+ declared: Set<string>,
165
+ seen: Set<string>,
166
+ ): asserts entry is Record<string, unknown> & SemanticVerdict {
167
+ if (
168
+ !validVerdictKeys(entry) ||
169
+ !validVerdictId(entry.id, declared, seen) ||
170
+ !validVerdict(entry.verdict) ||
171
+ !validVerdictReason(entry.reason)
172
+ )
173
+ throw new Error("invalid semantic verdict");
174
+ }
175
+
176
+ function validatedSemanticVerdict(
177
+ value: unknown,
178
+ declared: Set<string>,
179
+ seen: Set<string>,
180
+ ): SemanticVerdict {
181
+ const entry = semanticVerdictRecord(value);
182
+ assertSemanticVerdict(entry, declared, seen);
183
+ seen.add(entry.id);
184
+ return entry;
185
+ }
186
+
187
+ export type SemanticCheckGroup = [
188
+ PreparedSemanticCheck,
189
+ ...PreparedSemanticCheck[],
190
+ ];
191
+
135
192
  export function semanticCheckGroups(
136
193
  checks: PreparedSemanticCheck[],
137
- ): PreparedSemanticCheck[][] {
138
- const groups = new Map<string, PreparedSemanticCheck[]>();
194
+ ): SemanticCheckGroup[] {
195
+ const groups = new Map<string, SemanticCheckGroup>();
139
196
  for (const check of checks) {
140
197
  const key = check.artifactPath ?? "";
141
- groups.set(key, [...(groups.get(key) ?? []), check]);
198
+ const existing = groups.get(key);
199
+ if (existing) existing.push(check);
200
+ else groups.set(key, [check]);
142
201
  }
143
202
  return [...groups.values()];
144
203
  }