@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/extension-session.ts
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
+
import validExchange from "./generated/extension.cjs";
|
|
2
|
+
import { isRecord } from "./value-guards";
|
|
1
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
4
|
import { createReadStream } from "node:fs";
|
|
3
5
|
import { isAbsolute } from "node:path";
|
|
4
6
|
import { canonicalJson, hashJson } from "./identity";
|
|
5
|
-
import type { GeneratedFixture } from "./generated
|
|
6
|
-
import type { RepositoryFixture } from "./repository
|
|
7
|
-
import type { FixtureSetupDeclaration } from "./
|
|
7
|
+
import type { GeneratedFixture } from "./fixtures/generated";
|
|
8
|
+
import type { RepositoryFixture } from "./fixtures/repository";
|
|
9
|
+
import type { FixtureSetupDeclaration } from "./fixtures/setup";
|
|
10
|
+
import type {
|
|
11
|
+
ArtifactReference,
|
|
12
|
+
Assessment,
|
|
13
|
+
CheckDeclaration,
|
|
14
|
+
DomainOutcome,
|
|
15
|
+
ExtensionCheckOutcome,
|
|
16
|
+
MetricEvidence,
|
|
17
|
+
ObservationEvidence,
|
|
18
|
+
TaskVerdict,
|
|
19
|
+
} from "./contract-types";
|
|
20
|
+
import type { InstrumentationRequest } from "./instrumentation";
|
|
8
21
|
import {
|
|
9
22
|
ExtensionProtocolError,
|
|
10
23
|
exchangeExtension,
|
|
@@ -15,6 +28,13 @@ import {
|
|
|
15
28
|
|
|
16
29
|
const PROTOCOL = "sevro.extension.v1";
|
|
17
30
|
|
|
31
|
+
export class ExtensionSourceChangedError extends ExtensionProtocolError {
|
|
32
|
+
constructor() {
|
|
33
|
+
super("extension source changed during run");
|
|
34
|
+
this.name = "ExtensionSourceChangedError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
export interface ExtensionCase {
|
|
19
39
|
id: string;
|
|
20
40
|
prompt: string;
|
|
@@ -23,11 +43,7 @@ export interface ExtensionCase {
|
|
|
23
43
|
| { kind: "inline"; files: Record<string, string> }
|
|
24
44
|
| RepositoryFixture
|
|
25
45
|
| GeneratedFixture;
|
|
26
|
-
checks:
|
|
27
|
-
id: string;
|
|
28
|
-
grader: string;
|
|
29
|
-
configuration: Record<string, unknown>;
|
|
30
|
-
}[];
|
|
46
|
+
checks: CheckDeclaration[];
|
|
31
47
|
requiredEvidence: string[];
|
|
32
48
|
extensionData: Record<string, unknown>;
|
|
33
49
|
}
|
|
@@ -42,10 +58,7 @@ export interface PreparationResult {
|
|
|
42
58
|
gitExclude?: boolean;
|
|
43
59
|
executable?: boolean;
|
|
44
60
|
}[];
|
|
45
|
-
requestedInstrumentation:
|
|
46
|
-
id: string;
|
|
47
|
-
configuration: Record<string, unknown>;
|
|
48
|
-
}[];
|
|
61
|
+
requestedInstrumentation: InstrumentationRequest[];
|
|
49
62
|
fixtureSetup?: FixtureSetupDeclaration;
|
|
50
63
|
codexMarketplace?: {
|
|
51
64
|
artifactRoot: string;
|
|
@@ -68,48 +81,20 @@ export interface PreparationResult {
|
|
|
68
81
|
|
|
69
82
|
export interface EvaluationRequest {
|
|
70
83
|
caseId: string;
|
|
71
|
-
execution: {
|
|
72
|
-
status: "completed" | "failed" | "cancelled" | "not_run";
|
|
84
|
+
execution: Assessment["execution"] & {
|
|
73
85
|
errorCode?: string;
|
|
74
86
|
};
|
|
75
|
-
observations:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
completeness: "complete" | "partial" | "unavailable";
|
|
79
|
-
data: Record<string, unknown>;
|
|
80
|
-
}[];
|
|
81
|
-
builtinChecks: {
|
|
82
|
-
id: string;
|
|
83
|
-
status: "passed" | "failed" | "unavailable";
|
|
84
|
-
detail?: string;
|
|
85
|
-
evidenceRefs: string[];
|
|
86
|
-
}[];
|
|
87
|
-
artifacts: {
|
|
88
|
-
id: string;
|
|
89
|
-
path: string;
|
|
90
|
-
sha256: string;
|
|
91
|
-
gitExclude?: boolean;
|
|
92
|
-
executable?: boolean;
|
|
93
|
-
}[];
|
|
87
|
+
observations: ObservationEvidence[];
|
|
88
|
+
builtinChecks: ExtensionCheckOutcome[];
|
|
89
|
+
artifacts: ArtifactReference[];
|
|
94
90
|
extensionData: Record<string, unknown>;
|
|
95
91
|
}
|
|
96
92
|
|
|
97
93
|
export interface EvaluationResult {
|
|
98
|
-
checks:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
evidenceRefs: string[];
|
|
103
|
-
}[];
|
|
104
|
-
metrics: { id: string; value: number | null; unit: string }[];
|
|
105
|
-
domainOutcomes?: {
|
|
106
|
-
id: string;
|
|
107
|
-
status: "passed" | "failed" | "unavailable";
|
|
108
|
-
detail?: string;
|
|
109
|
-
evidenceRefs: string[];
|
|
110
|
-
data?: Record<string, unknown>;
|
|
111
|
-
}[];
|
|
112
|
-
taskVerdictRecommendation?: "passed" | "failed" | "not_assessed";
|
|
94
|
+
checks: ExtensionCheckOutcome[];
|
|
95
|
+
metrics: MetricEvidence[];
|
|
96
|
+
domainOutcomes?: DomainOutcome[];
|
|
97
|
+
taskVerdictRecommendation?: TaskVerdict;
|
|
113
98
|
}
|
|
114
99
|
|
|
115
100
|
export interface ExtensionSessionOptions extends ExchangeOptions {
|
|
@@ -130,19 +115,27 @@ export interface ExtensionIdentity extends NegotiatedExtension {
|
|
|
130
115
|
replacedBuiltinGraders: string[];
|
|
131
116
|
}
|
|
132
117
|
|
|
133
|
-
async function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
throw new ExtensionProtocolError(
|
|
142
|
-
"declared extension source is unreadable",
|
|
143
|
-
);
|
|
118
|
+
async function digestSource(path: string): Promise<string> {
|
|
119
|
+
try {
|
|
120
|
+
const hash = createHash("sha256");
|
|
121
|
+
for await (const value of createReadStream(path)) {
|
|
122
|
+
const chunk: unknown = value;
|
|
123
|
+
if (!(chunk instanceof Uint8Array))
|
|
124
|
+
throw new Error("invalid extension source stream");
|
|
125
|
+
hash.update(chunk);
|
|
144
126
|
}
|
|
127
|
+
return hash.digest("hex");
|
|
128
|
+
} catch (cause) {
|
|
129
|
+
throw new ExtensionProtocolError(
|
|
130
|
+
"declared extension source is unreadable",
|
|
131
|
+
{ cause },
|
|
132
|
+
);
|
|
145
133
|
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function digestSources(paths: string[]): Promise<string> {
|
|
137
|
+
const hashes: string[] = [];
|
|
138
|
+
for (const path of paths) hashes.push(await digestSource(path));
|
|
146
139
|
return hashJson(hashes);
|
|
147
140
|
}
|
|
148
141
|
|
|
@@ -150,11 +143,37 @@ function jsonCopy<T>(value: T): T {
|
|
|
150
143
|
return JSON.parse(canonicalJson(value)) as T;
|
|
151
144
|
}
|
|
152
145
|
|
|
153
|
-
|
|
154
|
-
|
|
146
|
+
function absoluteSessionCommand(
|
|
147
|
+
command: string[],
|
|
148
|
+
): command is [string, ...string[]] {
|
|
149
|
+
const executable = command[0];
|
|
150
|
+
return executable !== undefined && isAbsolute(executable);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function uniqueAbsoluteSources(sources: string[]): boolean {
|
|
154
|
+
return (
|
|
155
|
+
sources.length > 0 &&
|
|
156
|
+
sources.every((path) => isAbsolute(path)) &&
|
|
157
|
+
new Set(sources).size === sources.length
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function sessionSourceClosure(command: string[], sources: string[]): string[] {
|
|
162
|
+
if (!absoluteSessionCommand(command) || !uniqueAbsoluteSources(sources))
|
|
163
|
+
throw new ExtensionProtocolError(
|
|
164
|
+
"extension source files must be unique absolute paths",
|
|
165
|
+
);
|
|
166
|
+
return [...new Set([command[0], ...sources])];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function validateGraderReplacements(graders: string[]): void {
|
|
170
|
+
if (new Set(graders).size !== graders.length)
|
|
171
|
+
throw new ExtensionProtocolError("duplicate built-in grader replacement");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function sessionConfiguration(options: ExtensionSessionOptions) {
|
|
155
175
|
const command = [...options.command];
|
|
156
176
|
const sourceFiles = [...options.sourceFiles];
|
|
157
|
-
const sourceClosure = [...new Set([command[0], ...sourceFiles])];
|
|
158
177
|
const taskVerdictPolicy = options.taskVerdictPolicy;
|
|
159
178
|
const replacedBuiltinGraders = [
|
|
160
179
|
...(options.replaceBuiltinGraders ?? []),
|
|
@@ -164,172 +183,284 @@ export async function openExtensionSession(options: ExtensionSessionOptions) {
|
|
|
164
183
|
timeoutMs: options.timeoutMs,
|
|
165
184
|
signal: options.signal,
|
|
166
185
|
};
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
!isAbsolute(command[0] ?? "") ||
|
|
170
|
-
!sourceFiles.length ||
|
|
171
|
-
sourceFiles.some((path) => !isAbsolute(path)) ||
|
|
172
|
-
new Set(sourceFiles).size !== sourceFiles.length
|
|
173
|
-
)
|
|
174
|
-
throw new ExtensionProtocolError(
|
|
175
|
-
"extension source files must be unique absolute paths",
|
|
176
|
-
);
|
|
177
|
-
if (new Set(replacedBuiltinGraders).size !== replacedBuiltinGraders.length)
|
|
178
|
-
throw new ExtensionProtocolError("duplicate built-in grader replacement");
|
|
186
|
+
const sourceClosure = sessionSourceClosure(command, sourceFiles);
|
|
187
|
+
validateGraderReplacements(replacedBuiltinGraders);
|
|
179
188
|
const configuration = jsonCopy(options.configuration);
|
|
180
189
|
const redactedConfiguration = jsonCopy(options.redactedConfiguration);
|
|
181
|
-
|
|
182
|
-
const negotiated = await negotiateExtension(
|
|
190
|
+
return {
|
|
183
191
|
command,
|
|
192
|
+
sourceFiles,
|
|
193
|
+
taskVerdictPolicy,
|
|
194
|
+
replacedBuiltinGraders,
|
|
195
|
+
exchangeOptions,
|
|
196
|
+
sourceClosure,
|
|
197
|
+
configuration,
|
|
198
|
+
redactedConfiguration,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
type SessionConfiguration = ReturnType<typeof sessionConfiguration>;
|
|
203
|
+
|
|
204
|
+
function sessionIdentity(
|
|
205
|
+
context: SessionConfiguration,
|
|
206
|
+
negotiated: NegotiatedExtension,
|
|
207
|
+
sourceDigest: string,
|
|
208
|
+
): ExtensionIdentity {
|
|
209
|
+
const identity: ExtensionIdentity = {
|
|
210
|
+
...negotiated,
|
|
211
|
+
sourceDigest,
|
|
212
|
+
configurationDigest: hashJson({
|
|
213
|
+
command: context.command,
|
|
214
|
+
configuration: context.redactedConfiguration,
|
|
215
|
+
taskVerdictPolicy: context.taskVerdictPolicy ?? null,
|
|
216
|
+
replacedBuiltinGraders: context.replacedBuiltinGraders,
|
|
217
|
+
}),
|
|
218
|
+
selectedTaskVerdictPolicy: context.taskVerdictPolicy ?? null,
|
|
219
|
+
replacedBuiltinGraders: context.replacedBuiltinGraders,
|
|
220
|
+
};
|
|
221
|
+
Object.freeze(identity.capabilities);
|
|
222
|
+
Object.freeze(identity.graders);
|
|
223
|
+
Object.freeze(identity.taskVerdictPolicies);
|
|
224
|
+
Object.freeze(identity.replacedBuiltinGraders);
|
|
225
|
+
Object.freeze(identity);
|
|
226
|
+
return identity;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function discoverSession(
|
|
230
|
+
configuration: SessionConfiguration,
|
|
231
|
+
options: ExtensionSessionOptions,
|
|
232
|
+
) {
|
|
233
|
+
const sourceDigest = await digestSources(configuration.sourceClosure);
|
|
234
|
+
const negotiated = await negotiateExtension(
|
|
235
|
+
configuration.command,
|
|
184
236
|
{
|
|
185
237
|
engineCapabilities: options.engineCapabilities,
|
|
186
238
|
hostCapabilities: options.hostCapabilities,
|
|
187
239
|
},
|
|
188
|
-
exchangeOptions,
|
|
240
|
+
configuration.exchangeOptions,
|
|
189
241
|
);
|
|
190
|
-
if ((await digestSources(sourceClosure)) !== sourceDigest)
|
|
242
|
+
if ((await digestSources(configuration.sourceClosure)) !== sourceDigest)
|
|
191
243
|
throw new ExtensionProtocolError(
|
|
192
244
|
"extension source changed during discovery",
|
|
193
245
|
);
|
|
194
246
|
if (
|
|
195
|
-
taskVerdictPolicy &&
|
|
196
|
-
!negotiated.taskVerdictPolicies.includes(taskVerdictPolicy)
|
|
247
|
+
configuration.taskVerdictPolicy &&
|
|
248
|
+
!negotiated.taskVerdictPolicies.includes(configuration.taskVerdictPolicy)
|
|
197
249
|
)
|
|
198
250
|
throw new ExtensionProtocolError(
|
|
199
251
|
"extension did not advertise the selected task policy",
|
|
200
252
|
);
|
|
201
|
-
|
|
202
|
-
...
|
|
253
|
+
return {
|
|
254
|
+
...configuration,
|
|
203
255
|
sourceDigest,
|
|
204
|
-
|
|
205
|
-
command,
|
|
206
|
-
configuration: redactedConfiguration,
|
|
207
|
-
taskVerdictPolicy: taskVerdictPolicy ?? null,
|
|
208
|
-
replacedBuiltinGraders,
|
|
209
|
-
}),
|
|
210
|
-
selectedTaskVerdictPolicy: taskVerdictPolicy ?? null,
|
|
211
|
-
replacedBuiltinGraders,
|
|
256
|
+
identity: sessionIdentity(configuration, negotiated, sourceDigest),
|
|
212
257
|
};
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
type SessionContext = Awaited<ReturnType<typeof discoverSession>>;
|
|
261
|
+
interface SessionResults {
|
|
262
|
+
resolve: { cases: ExtensionCase[] };
|
|
263
|
+
prepare: PreparationResult;
|
|
264
|
+
evaluate: EvaluationResult;
|
|
265
|
+
}
|
|
218
266
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
267
|
+
function sessionResponse<M extends keyof SessionResults>(
|
|
268
|
+
value: unknown,
|
|
269
|
+
method: M,
|
|
270
|
+
): value is { method: M; result: SessionResults[M] } {
|
|
271
|
+
return (
|
|
272
|
+
validExchange(value) &&
|
|
273
|
+
isRecord(value) &&
|
|
274
|
+
value.method === method &&
|
|
275
|
+
Object.hasOwn(value, "result")
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function verifySessionSources(context: SessionContext): Promise<void> {
|
|
280
|
+
if ((await digestSources(context.sourceClosure)) !== context.sourceDigest)
|
|
281
|
+
throw new ExtensionSourceChangedError();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function callSession<M extends keyof SessionResults>(
|
|
285
|
+
context: SessionContext,
|
|
286
|
+
method: M,
|
|
287
|
+
params: object,
|
|
288
|
+
): Promise<SessionResults[M]> {
|
|
289
|
+
await verifySessionSources(context);
|
|
290
|
+
const response = await exchangeExtension(
|
|
291
|
+
context.command,
|
|
292
|
+
{
|
|
293
|
+
protocol: PROTOCOL,
|
|
294
|
+
id: randomUUID(),
|
|
295
|
+
method,
|
|
296
|
+
params: { ...params, configuration: context.configuration },
|
|
297
|
+
},
|
|
298
|
+
context.exchangeOptions,
|
|
299
|
+
);
|
|
300
|
+
await verifySessionSources(context);
|
|
301
|
+
if (!sessionResponse(response, method))
|
|
302
|
+
throw new ExtensionProtocolError("invalid extension response");
|
|
303
|
+
return response.result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
interface ResolvedHostRoute {
|
|
307
|
+
id: string;
|
|
308
|
+
model: string;
|
|
309
|
+
effort: string;
|
|
310
|
+
capabilities: string[];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function resolveSession(
|
|
314
|
+
context: SessionContext,
|
|
315
|
+
projectRoot: string,
|
|
316
|
+
selectors: Record<string, unknown>,
|
|
317
|
+
host: ResolvedHostRoute | undefined,
|
|
318
|
+
): Promise<ExtensionCase[]> {
|
|
319
|
+
const result = await callSession(context, "resolve", {
|
|
320
|
+
projectRoot,
|
|
321
|
+
selectors,
|
|
322
|
+
...(host ? { host } : {}),
|
|
323
|
+
});
|
|
324
|
+
const ids = result.cases.map((item) => item.id);
|
|
325
|
+
if (new Set(ids).size !== ids.length)
|
|
326
|
+
throw new ExtensionProtocolError("extension resolved duplicate case IDs");
|
|
327
|
+
return result.cases;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function prepareSession(
|
|
331
|
+
context: SessionContext,
|
|
332
|
+
resolvedCase: ExtensionCase,
|
|
333
|
+
host: { id: string; capabilities: string[] },
|
|
334
|
+
condition: "passive" | "enforced",
|
|
335
|
+
): Promise<PreparationResult> {
|
|
336
|
+
return callSession(context, "prepare", {
|
|
337
|
+
case: resolvedCase,
|
|
338
|
+
host,
|
|
339
|
+
condition,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function validateEvaluationIds(
|
|
344
|
+
result: EvaluationResult,
|
|
345
|
+
outcomes: NonNullable<EvaluationResult["domainOutcomes"]>,
|
|
346
|
+
extensionId: string,
|
|
347
|
+
): void {
|
|
348
|
+
const ids = [
|
|
349
|
+
...result.checks.map((check) => check.id),
|
|
350
|
+
...outcomes.map((outcome) => outcome.id),
|
|
351
|
+
];
|
|
352
|
+
if (
|
|
353
|
+
new Set(ids).size !== ids.length ||
|
|
354
|
+
ids.some((id) => !id.startsWith(`${extensionId}.`))
|
|
355
|
+
)
|
|
356
|
+
throw new ExtensionProtocolError(
|
|
357
|
+
"extension returned duplicate or foreign result IDs",
|
|
234
358
|
);
|
|
235
|
-
|
|
236
|
-
throw new ExtensionProtocolError("extension source changed during run");
|
|
237
|
-
return response.result!;
|
|
238
|
-
}
|
|
359
|
+
}
|
|
239
360
|
|
|
361
|
+
function validateSelectedTaskPolicy(
|
|
362
|
+
result: EvaluationResult,
|
|
363
|
+
policy: string | undefined,
|
|
364
|
+
): void {
|
|
365
|
+
if (result.taskVerdictRecommendation && !policy)
|
|
366
|
+
throw new ExtensionProtocolError("extension task policy was not selected");
|
|
367
|
+
if (policy && !result.taskVerdictRecommendation)
|
|
368
|
+
throw new ExtensionProtocolError(
|
|
369
|
+
"extension omitted the selected task policy recommendation",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function availableEvidence(request: EvaluationRequest): Map<string, string> {
|
|
374
|
+
const available = new Map<string, string>(
|
|
375
|
+
request.observations.map((item) => [item.id, item.completeness]),
|
|
376
|
+
);
|
|
377
|
+
for (const item of request.artifacts) available.set(item.id, "complete");
|
|
378
|
+
for (const item of request.builtinChecks)
|
|
379
|
+
available.set(
|
|
380
|
+
item.id,
|
|
381
|
+
item.status === "unavailable" ? "unavailable" : "complete",
|
|
382
|
+
);
|
|
383
|
+
return available;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
type EvidenceOutcome = EvaluationResult["checks"][number];
|
|
387
|
+
function passedWithoutEvidence(item: EvidenceOutcome): boolean {
|
|
388
|
+
return item.status === "passed" && item.evidenceRefs.length === 0;
|
|
389
|
+
}
|
|
390
|
+
function passedWithIncompleteEvidence(
|
|
391
|
+
item: EvidenceOutcome,
|
|
392
|
+
available: Map<string, string>,
|
|
393
|
+
): boolean {
|
|
394
|
+
return (
|
|
395
|
+
item.status === "passed" &&
|
|
396
|
+
item.evidenceRefs.some((id) => available.get(id) !== "complete")
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function validateResultEvidence(
|
|
401
|
+
item: EvidenceOutcome,
|
|
402
|
+
available: Map<string, string>,
|
|
403
|
+
): void {
|
|
404
|
+
if (passedWithoutEvidence(item))
|
|
405
|
+
throw new ExtensionProtocolError(
|
|
406
|
+
"extension passed a result without evidence",
|
|
407
|
+
);
|
|
408
|
+
if (item.evidenceRefs.some((id) => !available.has(id)))
|
|
409
|
+
throw new ExtensionProtocolError("extension result cites unknown evidence");
|
|
410
|
+
if (passedWithIncompleteEvidence(item, available))
|
|
411
|
+
throw new ExtensionProtocolError(
|
|
412
|
+
"extension passed a result with incomplete evidence",
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function evaluateSession(
|
|
417
|
+
context: SessionContext,
|
|
418
|
+
request: EvaluationRequest,
|
|
419
|
+
): Promise<EvaluationResult> {
|
|
420
|
+
const result = await callSession(context, "evaluate", {
|
|
421
|
+
...request,
|
|
422
|
+
...(context.taskVerdictPolicy
|
|
423
|
+
? { selectedTaskVerdictPolicy: context.taskVerdictPolicy }
|
|
424
|
+
: {}),
|
|
425
|
+
});
|
|
426
|
+
const outcomes = result.domainOutcomes ?? [];
|
|
427
|
+
validateEvaluationIds(result, outcomes, context.identity.id);
|
|
428
|
+
validateSelectedTaskPolicy(result, context.taskVerdictPolicy);
|
|
429
|
+
const available = availableEvidence(request);
|
|
430
|
+
for (const item of [...result.checks, ...outcomes])
|
|
431
|
+
validateResultEvidence(item, available);
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function extensionSession(context: SessionContext) {
|
|
240
436
|
return {
|
|
241
|
-
identity,
|
|
437
|
+
identity: context.identity,
|
|
242
438
|
get redactedConfiguration() {
|
|
243
|
-
return jsonCopy(redactedConfiguration);
|
|
439
|
+
return jsonCopy(context.redactedConfiguration);
|
|
244
440
|
},
|
|
245
|
-
|
|
441
|
+
resolve(
|
|
246
442
|
projectRoot: string,
|
|
247
443
|
selectors: Record<string, unknown>,
|
|
248
|
-
host?:
|
|
249
|
-
id: string;
|
|
250
|
-
model: string;
|
|
251
|
-
effort: string;
|
|
252
|
-
capabilities: string[];
|
|
253
|
-
},
|
|
444
|
+
host?: ResolvedHostRoute,
|
|
254
445
|
): Promise<ExtensionCase[]> {
|
|
255
|
-
|
|
256
|
-
projectRoot,
|
|
257
|
-
selectors,
|
|
258
|
-
...(host ? { host } : {}),
|
|
259
|
-
});
|
|
260
|
-
const cases = result.cases as ExtensionCase[];
|
|
261
|
-
const ids = cases.map((item) => item.id);
|
|
262
|
-
if (new Set(ids).size !== ids.length)
|
|
263
|
-
throw new ExtensionProtocolError(
|
|
264
|
-
"extension resolved duplicate case IDs",
|
|
265
|
-
);
|
|
266
|
-
return cases;
|
|
446
|
+
return resolveSession(context, projectRoot, selectors, host);
|
|
267
447
|
},
|
|
268
|
-
|
|
448
|
+
prepare(
|
|
269
449
|
resolvedCase: ExtensionCase,
|
|
270
450
|
host: { id: string; capabilities: string[] },
|
|
271
451
|
condition: "passive" | "enforced",
|
|
272
452
|
): Promise<PreparationResult> {
|
|
273
|
-
return (
|
|
274
|
-
case: resolvedCase,
|
|
275
|
-
host,
|
|
276
|
-
condition,
|
|
277
|
-
})) as unknown as PreparationResult;
|
|
453
|
+
return prepareSession(context, resolvedCase, host, condition);
|
|
278
454
|
},
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
...request,
|
|
282
|
-
...(taskVerdictPolicy
|
|
283
|
-
? { selectedTaskVerdictPolicy: taskVerdictPolicy }
|
|
284
|
-
: {}),
|
|
285
|
-
})) as unknown as EvaluationResult;
|
|
286
|
-
const outcomes = result.domainOutcomes ?? [];
|
|
287
|
-
const ids = [
|
|
288
|
-
...result.checks.map((check) => check.id),
|
|
289
|
-
...outcomes.map((outcome) => outcome.id),
|
|
290
|
-
];
|
|
291
|
-
if (
|
|
292
|
-
new Set(ids).size !== ids.length ||
|
|
293
|
-
ids.some((id) => !id.startsWith(`${identity.id}.`))
|
|
294
|
-
)
|
|
295
|
-
throw new ExtensionProtocolError(
|
|
296
|
-
"extension returned duplicate or foreign result IDs",
|
|
297
|
-
);
|
|
298
|
-
if (result.taskVerdictRecommendation && !taskVerdictPolicy)
|
|
299
|
-
throw new ExtensionProtocolError(
|
|
300
|
-
"extension task policy was not selected",
|
|
301
|
-
);
|
|
302
|
-
if (taskVerdictPolicy && !result.taskVerdictRecommendation)
|
|
303
|
-
throw new ExtensionProtocolError(
|
|
304
|
-
"extension omitted the selected task policy recommendation",
|
|
305
|
-
);
|
|
306
|
-
const available = new Map(
|
|
307
|
-
request.observations.map((item) => [item.id, item.completeness]),
|
|
308
|
-
);
|
|
309
|
-
for (const item of request.artifacts) available.set(item.id, "complete");
|
|
310
|
-
for (const item of request.builtinChecks)
|
|
311
|
-
available.set(
|
|
312
|
-
item.id,
|
|
313
|
-
item.status === "unavailable" ? "unavailable" : "complete",
|
|
314
|
-
);
|
|
315
|
-
for (const item of [...result.checks, ...outcomes]) {
|
|
316
|
-
if (item.status === "passed" && item.evidenceRefs.length === 0)
|
|
317
|
-
throw new ExtensionProtocolError(
|
|
318
|
-
"extension passed a result without evidence",
|
|
319
|
-
);
|
|
320
|
-
if (item.evidenceRefs.some((id) => !available.has(id)))
|
|
321
|
-
throw new ExtensionProtocolError(
|
|
322
|
-
"extension result cites unknown evidence",
|
|
323
|
-
);
|
|
324
|
-
if (
|
|
325
|
-
item.status === "passed" &&
|
|
326
|
-
item.evidenceRefs.some((id) => available.get(id) !== "complete")
|
|
327
|
-
)
|
|
328
|
-
throw new ExtensionProtocolError(
|
|
329
|
-
"extension passed a result with incomplete evidence",
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
return result;
|
|
455
|
+
evaluate(request: EvaluationRequest): Promise<EvaluationResult> {
|
|
456
|
+
return evaluateSession(context, request);
|
|
333
457
|
},
|
|
334
458
|
};
|
|
335
459
|
}
|
|
460
|
+
|
|
461
|
+
/** Bind a stateless extension command to a negotiated identity and source snapshot. */
|
|
462
|
+
export async function openExtensionSession(options: ExtensionSessionOptions) {
|
|
463
|
+
return extensionSession(
|
|
464
|
+
await discoverSession(sessionConfiguration(options), options),
|
|
465
|
+
);
|
|
466
|
+
}
|