@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.
- package/CONTRIBUTING.md +170 -0
- package/LICENSE +63 -136
- package/README.md +83 -66
- package/docs/README.md +44 -0
- package/docs/acknowledgements.md +14 -0
- package/docs/architecture.md +146 -0
- package/docs/assets/README.md +27 -0
- package/docs/assets/sevro-logo.png +0 -0
- package/docs/assets/sevro-symbol.svg +1 -0
- package/docs/assets/sevro-wordmark.svg +1 -0
- package/docs/builtin-graders-v1.md +41 -13
- package/docs/codex-host.md +184 -52
- package/docs/contracts.md +19 -0
- package/docs/creating-cases.md +139 -0
- package/docs/development-cli.md +190 -30
- package/docs/documentation-quality.md +61 -0
- package/docs/evaluation-workflow.md +110 -0
- package/docs/evidence/code-mode-controls-71.md +64 -0
- package/docs/evidence/code-mode-programs-85.md +64 -0
- package/docs/evidence/codex-state-73/README.md +204 -0
- package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
- package/docs/evidence/codex-state-73/results.json +661 -0
- package/docs/evidence/local-http-74.md +104 -0
- package/docs/evidence/runtime-startup-30/README.md +185 -0
- package/docs/evidence/runtime-startup-30/after.json +319 -0
- package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
- package/docs/evidence/runtime-startup-30/before.json +403 -0
- package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
- package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
- package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
- package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
- package/docs/evidence/skill-reads-70/results.json +823 -0
- package/docs/evidence.md +21 -0
- package/docs/extension-protocol-v1.md +50 -16
- package/docs/fixture-cleanup-validation.md +3 -0
- package/docs/getting-started.md +102 -0
- package/docs/guide-evaluation.md +89 -0
- package/docs/guide-verification.json +239 -0
- package/docs/identity-v1.md +35 -1
- package/docs/installing.md +88 -0
- package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
- package/docs/licensing.md +42 -0
- package/docs/native-controls.md +76 -5
- package/docs/native-hosts.md +228 -0
- package/docs/reading-results.md +141 -0
- package/docs/recipes.md +489 -0
- package/docs/releases.md +81 -40
- package/docs/report-v1.md +29 -1
- package/docs/results-v1.md +180 -0
- package/docs/run-settings-v1.md +147 -0
- package/docs/running-evaluations.md +31 -0
- package/docs/runtime-v1.md +614 -0
- package/docs/specs/repository-guide.md +58 -0
- package/docs/troubleshooting.md +30 -0
- package/docs/typescript-quality.md +234 -0
- package/docs/usability-study.md +114 -0
- package/examples/basic/host.ts +2 -2
- package/examples/recipes/candidate.ts +26 -0
- package/examples/recipes/cases/coding-change.json +21 -0
- package/examples/recipes/cases/host-tools.json +21 -0
- package/examples/recipes/cases/mise.json +21 -0
- package/examples/recipes/cases/node.json +21 -0
- package/examples/recipes/cases/python-uv.json +21 -0
- package/examples/recipes/cases/semantic.json +17 -0
- package/examples/recipes/coding-change/check.ts +23 -0
- package/examples/recipes/extension-artifacts.ts +65 -0
- package/examples/recipes/extension-methods.ts +174 -0
- package/examples/recipes/extension.ts +37 -0
- package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
- package/examples/recipes/repository-skill/README.md +3 -0
- package/examples/recipes/run-inputs.ts +188 -0
- package/examples/recipes/run-options.ts +131 -0
- package/examples/recipes/run.ts +63 -0
- package/examples/recipes/runtime/empty.json +1 -0
- package/examples/recipes/runtime/goals.json +1 -0
- package/examples/recipes/runtime/mise.json +6 -0
- package/examples/recipes/runtime/path.json +1 -0
- package/examples/recipes/runtime/plugins.json +4 -0
- package/examples/recipes/runtime/python-uv.json +10 -0
- package/examples/recipes/semantic-judge.ts +29 -0
- package/examples/templates/coding/adapter.ts +17 -0
- package/examples/templates/coding/case.json +15 -0
- package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
- package/examples/templates/repository-skill/extension-data.ts +64 -0
- package/examples/templates/repository-skill/extension.ts +31 -0
- package/examples/templates/response/adapter.ts +12 -0
- package/examples/templates/response/case.json +13 -0
- package/package.json +39 -6
- package/schemas/report-v1.schema.json +9 -0
- package/schemas/run-evidence-v1.schema.json +2 -1
- package/schemas/runtime-v1.schema.json +193 -0
- package/src/advisory.ts +10 -5
- package/src/candidate-artifacts.ts +208 -0
- package/src/candidate-codex-state.ts +185 -0
- package/src/case-validation.ts +105 -0
- package/src/cli/cancellation.ts +20 -0
- package/src/cli/command.ts +81 -0
- package/src/cli/completion.ts +15 -0
- package/src/cli/demo.ts +59 -0
- package/src/cli/evaluation.ts +55 -0
- package/src/cli/extension.ts +79 -0
- package/src/cli/help.ts +222 -0
- package/src/cli/hosts.ts +59 -0
- package/src/cli/init-settings.ts +30 -0
- package/src/cli/init.ts +111 -0
- package/src/cli/invocation.ts +627 -0
- package/src/cli/loading.ts +151 -0
- package/src/cli/preparation.ts +163 -0
- package/src/cli/presentation.ts +91 -0
- package/src/cli/progress.ts +6 -0
- package/src/cli/run-manifest.ts +133 -0
- package/src/cli/run-options.ts +101 -0
- package/src/cli/run-overrides.ts +83 -0
- package/src/cli/settings.ts +160 -0
- package/src/cli/summary.ts +43 -0
- package/src/cli.ts +13 -843
- package/src/contract-types.ts +94 -0
- package/src/engine.ts +19 -2373
- package/src/evaluation/advisory.ts +150 -0
- package/src/evaluation/check-plan.ts +510 -0
- package/src/evaluation/collection.ts +88 -0
- package/src/evaluation/context.ts +10 -0
- package/src/evaluation/grading-host.ts +39 -0
- package/src/evaluation/grading.ts +258 -0
- package/src/evaluation/invocation-plan.ts +548 -0
- package/src/evaluation/plan.ts +736 -0
- package/src/evaluation/progress.ts +19 -0
- package/src/evaluation/resources.ts +241 -0
- package/src/evaluation/run.ts +206 -0
- package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
- package/src/evaluation/semantic.ts +312 -0
- package/src/evaluation/trial.ts +866 -0
- package/src/evaluation/types.ts +207 -0
- package/src/evidence/host-results.ts +92 -0
- package/src/evidence/retained-artifacts.ts +18 -0
- package/src/evidence/trial-writer.ts +203 -0
- package/src/evidence/usage.ts +17 -0
- package/src/extension-client.ts +228 -148
- package/src/extension-session.ts +326 -195
- package/src/fixtures/advisory.ts +338 -0
- package/src/fixtures/bin.ts +57 -0
- package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
- package/src/fixtures/create.ts +187 -0
- package/src/fixtures/generated.ts +300 -0
- package/src/fixtures/git-hooks.ts +24 -0
- package/src/fixtures/preparation.ts +270 -0
- package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
- package/src/fixtures/setup.ts +224 -0
- package/src/fixtures/tools.ts +88 -0
- package/src/fixtures/workspace-state.ts +212 -0
- package/src/generated/cli-result.cjs +2 -0
- package/src/generated/cli-result.d.cts +3 -0
- package/src/generated/extension.cjs +2 -0
- package/src/generated/extension.d.cts +3 -0
- package/src/generated/report.cjs +2 -0
- package/src/generated/report.d.cts +3 -0
- package/src/generated/run-evidence.cjs +2 -0
- package/src/generated/run-evidence.d.cts +3 -0
- package/src/generated/runtime.cjs +2 -0
- package/src/generated/runtime.d.cts +3 -0
- package/src/graders/artifact.ts +56 -23
- package/src/graders/git-head.ts +79 -75
- package/src/graders/output.ts +222 -140
- package/src/graders/semantic.ts +102 -43
- package/src/graders/shell.ts +379 -207
- package/src/host-diagnostic-redaction.ts +52 -0
- package/src/host-diagnostics.ts +319 -0
- package/src/hosts/claude-continuation.ts +63 -18
- package/src/hosts/claude-credential.ts +43 -62
- package/src/hosts/claude-events.ts +86 -51
- package/src/hosts/claude-keychain.ts +59 -0
- package/src/hosts/claude-native-goal.ts +57 -33
- package/src/hosts/claude-process.ts +111 -0
- package/src/hosts/claude-repository-invocation.ts +208 -117
- package/src/hosts/claude-settings.ts +240 -21
- package/src/hosts/claude-skill-activations.ts +166 -0
- package/src/hosts/claude-state.ts +403 -0
- package/src/hosts/claude-tool-calls.ts +171 -106
- package/src/hosts/claude.ts +561 -451
- package/src/hosts/codex-app-server.ts +326 -178
- package/src/hosts/codex-configuration.ts +44 -18
- package/src/hosts/codex-events.ts +97 -60
- package/src/hosts/codex-exec-controls.ts +516 -0
- package/src/hosts/codex-invocation.ts +70 -0
- package/src/hosts/codex-marketplace.ts +283 -0
- package/src/hosts/codex-native-calls.ts +546 -500
- package/src/hosts/codex-process.ts +202 -0
- package/src/hosts/codex-profile.ts +257 -28
- package/src/hosts/codex-session-files.ts +106 -0
- package/src/hosts/codex-state.ts +492 -0
- package/src/hosts/codex.ts +783 -832
- package/src/hosts/isolation-common.ts +94 -0
- package/src/hosts/isolation-roots.ts +54 -27
- package/src/hosts/isolation.ts +10 -0
- package/src/hosts/linux-sandbox.ts +159 -0
- package/src/hosts/mac-sandbox.ts +187 -61
- package/src/hosts/native-controls.ts +152 -62
- package/src/hosts/process-lifecycle.ts +37 -0
- package/src/hosts/protected-worktrees.ts +51 -19
- package/src/hosts/skill-hook-catalog.ts +131 -0
- package/src/hosts/skill-hook-handler.ts +374 -0
- package/src/hosts/skill-hooks.ts +580 -0
- package/src/hosts/workspace-fingerprint.ts +55 -35
- package/src/identity.ts +102 -48
- package/src/instrumentation.ts +109 -48
- package/src/native-transcript-state.ts +131 -0
- package/src/native-transcript-view.ts +90 -0
- package/src/native-transcripts.ts +286 -0
- package/src/process-lifecycle.ts +150 -0
- package/src/provenance.ts +161 -81
- package/src/prune.ts +215 -0
- package/src/report.ts +172 -140
- package/src/resolved-case.ts +33 -0
- package/src/results-lock.ts +29 -0
- package/src/results.ts +133 -86
- package/src/run-owner.ts +154 -63
- package/src/runtime/config.ts +456 -0
- package/src/runtime/discovery-apple.ts +90 -0
- package/src/runtime/discovery-providers.ts +63 -0
- package/src/runtime/discovery.ts +70 -0
- package/src/runtime/hook-processes.ts +97 -0
- package/src/runtime/hooks.ts +476 -0
- package/src/runtime/paths.ts +313 -0
- package/src/runtime/query.ts +85 -0
- package/src/runtime/seeds.ts +134 -0
- package/src/runtime/shared-cache.ts +143 -0
- package/src/runtime/state.ts +143 -0
- package/src/schema-types.ts +198 -0
- package/src/schema.ts +15 -19
- package/src/transcript-retention.ts +146 -0
- package/src/value-guards.ts +20 -0
- package/src/advisory-fixture.ts +0 -282
- package/src/fixture-bin.ts +0 -87
- package/src/fixture-setup.ts +0 -163
- package/src/generated-fixture.ts +0 -220
- package/src/git-hooks.ts +0 -63
- package/src/hosts/claude-nested-skills.ts +0 -218
- package/src/hosts/codex-command-output.ts +0 -399
- package/src/hosts/codex-executor-command.ts +0 -195
- package/src/hosts/codex-skill-reads.ts +0 -391
- package/src/preparation.ts +0 -211
package/src/graders/output.ts
CHANGED
|
@@ -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 (
|
|
88
|
-
if (Array.isArray(value)) {
|
|
99
|
+
if (isUnknownArray(value)) {
|
|
89
100
|
value.forEach(validateSubsetPatterns);
|
|
90
101
|
return;
|
|
91
102
|
}
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
|
|
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(
|
|
108
|
+
Object.values(value).forEach(validateSubsetPatterns);
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
function containsSubset(value: unknown, expected: unknown): boolean {
|
|
107
|
-
if (
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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((
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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(
|
|
325
|
-
exact: text.trim() ===
|
|
406
|
+
value: JSON.parse(fence.slice(1, 2).join("")),
|
|
407
|
+
exact: text.trim() === fence[0],
|
|
326
408
|
error: false,
|
|
327
409
|
};
|
|
328
410
|
} catch {
|
package/src/graders/semantic.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
):
|
|
138
|
-
const groups = new Map<string,
|
|
194
|
+
): SemanticCheckGroup[] {
|
|
195
|
+
const groups = new Map<string, SemanticCheckGroup>();
|
|
139
196
|
for (const check of checks) {
|
|
140
197
|
const key = check.artifactPath ?? "";
|
|
141
|
-
|
|
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
|
}
|