@springbrand/eval-harness 0.1.0-alpha.0 → 0.1.0-alpha.1

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/README.md CHANGED
@@ -38,11 +38,30 @@ independent Eval SP deployment; the shared SP dev API and database are not used.
38
38
 
39
39
  The initialization body includes the Data sample and, only when referenced by
40
40
  that sample, `initialSession: {bundleId, sessionId, context?: {format, content}}`
41
- and/or `initialMaterials: {bundleId, materialId, files}`. `content` is canonical
41
+ and/or `initialMaterials: [{bundleId, materialId, files}]`. `content` is canonical
42
42
  Session JSON or JSONL; each material file supplies its path, media type and
43
- base64 bytes. Supplied Session context is imported into the Chat before the Prompt and only
44
- material files are written to SpaceDO. A sample may specify either source, both,
45
- or neither. There is no Dataset-level source binding.
43
+ base64 bytes. The material list must match the sample's `initialMaterials`
44
+ references position by position, without duplicates. Supplied Session context is
45
+ imported into the Chat before the Prompt and only files are written to SpaceDO,
46
+ each material under its own `evalMaterialRoot`. A sample may specify either
47
+ source, both, or neither. There is no Dataset-level source binding. An Evaluator
48
+ initialization must carry `evaluationOutputSchema: {content}`, written as UTF-8
49
+ `application/schema+json` to `EVAL_OUTPUT_SCHEMA_PATH`
50
+ (`/evaluation-output.schema.json`); a Subject Chat rejects it. Evaluators do not
51
+ import an initial Session. Schema validity and result validation belong to Eval
52
+ Platform; SP preserves the supplied text. `evalMaterialFilesSchema`
53
+ validates one material's `EvalMaterialFile` list: at most `EVAL_MATERIAL_MAX_FILES`, with unique
54
+ paths. One initialization carries at most `EVAL_INITIAL_FILE_MAX_BYTES` per file
55
+ and `EVAL_INITIALIZATION_MAX_BYTES` in total; size violations are Zod `too_big`
56
+ issues. `evalMaterialFilesByteSize` returns one material's decoded bytes exactly as
57
+ that total counts them. `readEvalCanonicalContext` accepts at most
58
+ `EVAL_SESSION_CONTENT_MAX_CHARS` characters and `EVAL_SESSION_MAX_ENTRIES`
59
+ entries. `EVAL_INITIALIZATION_BODY_MAX_BYTES` bounds transport JSON, accounting
60
+ for base64 material bytes, escaped Session or Schema text, and JSON framing.
61
+ Eval consumes these constants instead of copying the values. Its result is
62
+ `evalInitializationResultSchema`: `{manifestHash, target, materials:
63
+ [{bundleId, materialId, rootPath}], evaluationOutputSchemaPath?}` in request
64
+ order; replaying the same request returns the stored result.
46
65
 
47
66
  An Evaluator is an ordinary Eval Chat with its own Session, Submission and
48
67
  Space; its creation facts add only `evaluationId`. It never reads or writes the
@@ -61,6 +80,46 @@ hash, writes `/evidence/<path>` and returns `{path, size, sha256}`. Only an
61
80
  Evaluator imports; an identical repeat is a no-op, different content is a 409,
62
81
  and imports stop with the Chat's stop latch.
63
82
 
83
+ The Evaluator Workspace layout is one public contract, exported by this package:
84
+
85
+ ```text
86
+ /initial-materials/{bundleId}/{materialId}/... # Evaluator's own materials
87
+ /evidence/subject-inputs/{bundleId}/{materialId}/... # Subject's original input materials
88
+ /evidence/artifacts/{original-relative-path} # Subject's produced files
89
+ /evidence/trajectory/events-000001.jsonl # Complete raw trajectory, ordered parts
90
+ /evidence/manifest.json # Evidence bundle inventory and provenance
91
+ /evaluation-output.schema.json # Evaluation output format
92
+ /output/result.json # Evaluator's scored result
93
+ ```
94
+
95
+ `EVAL_EVIDENCE_PATHS` names the bundle-relative directories. The import route takes
96
+ paths **without** the `/evidence` prefix: `/subject-inputs/...`, `/artifacts/...`,
97
+ `/trajectory/...` or `/manifest.json`. Other paths are rejected. The exported
98
+ `evalSubjectInputEvidencePath`, `evalArtifactEvidencePath` and
99
+ `evalTrajectoryEvidencePath` build those import paths; `evalEvidencePath` turns
100
+ an import path into its absolute Workspace path. IDs are encoded as individual
101
+ path segments and original file hierarchy is preserved, so equal basenames in
102
+ different materials or evidence categories cannot overwrite each other.
103
+
104
+ Eval Platform copies Subject inputs from the original immutable Material bundles
105
+ referenced by this Sampling, not from the Subject's potentially edited working
106
+ files. SP continues to exclude these inputs from artifact exports. Eval Platform
107
+ collects **every** trajectory page until `nextAfterSeq` is null, retaining raw
108
+ `{ingestSeq,event}` records in order; it splits oversized JSONL at event boundaries
109
+ so every part fits `EVAL_EVIDENCE_FILE_MAX_BYTES`. Eval Platform owns the frozen
110
+ bundle, manifest body, file hashes and preparation retries. It imports all files,
111
+ verifies their receipts and uploads `manifest.json` last, then submits its Prompt
112
+ with explicit evidence, Schema and result paths.
113
+
114
+ SP requires completed initialization before evidence import, and a nonempty
115
+ evidence manifest before an Evaluator's first Submission. Input writes and
116
+ Submission admission are serialized; the first reserved Submission durably closes
117
+ evidence imports, including after a DO restart. Replaying initialization returns
118
+ its saved result without restoring files. Stop closes admission immediately and
119
+ waits for accepted input writes and submissions. Evidence, initial materials and
120
+ the fixed Schema input path are never exported as produced artifacts. SP does not
121
+ interpret the manifest or implement Eval Platform's bundle assembly or scoring.
122
+
64
123
  Eval Platform writes the Evaluator instructions into the Prompt; SP submits it
65
124
  unchanged. The Evaluator's `output` adds `structuredOutput: {path, content,
66
125
  contentHash}` with the raw UTF-8 text of `/output/result.json` in its own Space
package/dist/index.d.ts CHANGED
@@ -4,8 +4,8 @@ export declare const EVAL_HARNESS_PATH = "/internal/v1/eval-harness";
4
4
  export declare const evalEnsureChatSchema: z.ZodObject<{
5
5
  chatId: z.ZodUUID;
6
6
  role: z.ZodEnum<{
7
- evaluator: "evaluator";
8
7
  subject: "subject";
8
+ evaluator: "evaluator";
9
9
  }>;
10
10
  framework: z.ZodLiteral<"sp">;
11
11
  samplingId: z.ZodString;
@@ -19,11 +19,29 @@ export declare const evalEnsureChatSchema: z.ZodObject<{
19
19
  timeoutMs: z.ZodOptional<z.ZodNumber>;
20
20
  }, z.core.$strict>>;
21
21
  }, z.core.$strict>;
22
- export declare const evalMaterialFileSchema: z.ZodObject<{
22
+ export declare const EVAL_INITIAL_FILE_MAX_BYTES = 5000000;
23
+ export declare const EVAL_INITIALIZATION_MAX_BYTES = 20000000;
24
+ export declare const EVAL_MATERIAL_MAX_FILES = 500;
25
+ export declare const EVAL_SESSION_CONTENT_MAX_CHARS = 2000000;
26
+ export declare const EVAL_SESSION_MAX_ENTRIES = 10000;
27
+ export declare const EVAL_OUTPUT_SCHEMA_PATH = "/evaluation-output.schema.json";
28
+ export declare const EVAL_INITIAL_MATERIALS_ROOT = "/initial-materials";
29
+ /** Subject Session JSON and Evaluator Schema JSON can expand to six bytes per character. */
30
+ export declare const EVAL_INITIALIZATION_BODY_MAX_BYTES: number;
31
+ /** Decoded bytes of validated Material files, counted exactly as the initialization budget counts them. */
32
+ export declare const evalMaterialFilesByteSize: (files: readonly {
33
+ readonly contentBase64: string;
34
+ }[]) => number;
35
+ declare const evalMaterialFileSchema: z.ZodObject<{
23
36
  path: z.ZodString;
24
37
  mediaType: z.ZodString;
25
38
  contentBase64: z.ZodBase64;
26
39
  }, z.core.$strict>;
40
+ export declare const evalMaterialFilesSchema: z.ZodArray<z.ZodObject<{
41
+ path: z.ZodString;
42
+ mediaType: z.ZodString;
43
+ contentBase64: z.ZodBase64;
44
+ }, z.core.$strict>>;
27
45
  export declare const evalInitialContextSchema: z.ZodObject<{
28
46
  format: z.ZodEnum<{
29
47
  json: "json";
@@ -47,7 +65,7 @@ export declare const evalInitializationSchema: z.ZodObject<{
47
65
  content: z.ZodString;
48
66
  }, z.core.$strict>>;
49
67
  }, z.core.$strict>>;
50
- initialMaterials: z.ZodOptional<z.ZodObject<{
68
+ initialMaterials: z.ZodOptional<z.ZodArray<z.ZodObject<{
51
69
  bundleId: z.ZodString;
52
70
  materialId: z.ZodString;
53
71
  files: z.ZodArray<z.ZodObject<{
@@ -55,8 +73,21 @@ export declare const evalInitializationSchema: z.ZodObject<{
55
73
  mediaType: z.ZodString;
56
74
  contentBase64: z.ZodBase64;
57
75
  }, z.core.$strict>>;
76
+ }, z.core.$strict>>>;
77
+ evaluationOutputSchema: z.ZodOptional<z.ZodObject<{
78
+ content: z.ZodString;
58
79
  }, z.core.$strict>>;
59
80
  }, z.core.$strict>;
81
+ export declare const evalInitializationResultSchema: z.ZodObject<{
82
+ manifestHash: z.ZodString;
83
+ target: z.ZodLiteral<"workspace">;
84
+ materials: z.ZodArray<z.ZodObject<{
85
+ bundleId: z.ZodString;
86
+ materialId: z.ZodString;
87
+ rootPath: z.ZodString;
88
+ }, z.core.$strict>>;
89
+ evaluationOutputSchemaPath: z.ZodExactOptional<z.ZodLiteral<"/evaluation-output.schema.json">>;
90
+ }, z.core.$strict>;
60
91
  export declare const evalSubmitSchema: z.ZodObject<{
61
92
  idempotencyKey: z.ZodString;
62
93
  prompt: z.ZodString;
@@ -66,7 +97,9 @@ export declare const evalSubmitSchema: z.ZodObject<{
66
97
  }, z.core.$strict>>;
67
98
  }, z.core.$strict>;
68
99
  export type EvalEnsureChat = z.output<typeof evalEnsureChatSchema>;
100
+ export type EvalMaterialFile = z.output<typeof evalMaterialFileSchema>;
69
101
  export type EvalInitialization = z.output<typeof evalInitializationSchema>;
102
+ export type EvalInitializationResult = z.output<typeof evalInitializationResultSchema>;
70
103
  export type EvalSubmit = z.output<typeof evalSubmitSchema>;
71
104
  export type EvalSubmissionReceipt = {
72
105
  readonly submissionId: string;
@@ -135,6 +168,13 @@ export declare function evalMaterialRoot(bundleId: string, materialId: string):
135
168
  export declare function evalMaterialPath(root: string, path: string): string;
136
169
  /** Where imported evidence lives in the Evaluator's own Space. */
137
170
  export declare const EVAL_EVIDENCE_ROOT = "/evidence";
171
+ /** Bundle-relative paths, used by the import route; evalEvidencePath returns their Workspace paths. */
172
+ export declare const EVAL_EVIDENCE_PATHS: {
173
+ readonly subjectInputs: "/subject-inputs";
174
+ readonly artifacts: "/artifacts";
175
+ readonly trajectory: "/trajectory";
176
+ readonly manifest: "/manifest.json";
177
+ };
138
178
  /** One exported or imported file crosses a single Durable Object RPC as bytes. */
139
179
  export declare const EVAL_EVIDENCE_FILE_MAX_BYTES = 25000000;
140
180
  export declare const EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX = 200;
@@ -169,6 +209,19 @@ export declare function readEvalTrajectoryPageQuery(params: URLSearchParams): {
169
209
  };
170
210
  /** A Space path for one evidence file; `relative` keeps the bundle's own layout below `/evidence`. */
171
211
  export declare function evalEvidencePath(relative: string): string;
212
+ /** Original Subject inputs stay separate from produced artifacts and the Evaluator's own materials. */
213
+ export declare function evalSubjectInputEvidencePath(bundleId: string, materialId: string, path: string): string;
214
+ export declare function evalArtifactEvidencePath(path: string): string;
215
+ /** Positive part numbers preserve the complete raw JSONL trajectory's order. */
216
+ export declare function evalTrajectoryEvidencePath(part: number): string;
217
+ /** One raw evidence file imported into the Evaluator's own Space. */
218
+ export type EvalEvidenceFileInput = {
219
+ /** Bundle-relative path under one of EVAL_EVIDENCE_PATHS, without the /evidence prefix. */
220
+ readonly path: string;
221
+ readonly mediaType: string;
222
+ readonly sha256: string;
223
+ readonly bytes: Uint8Array;
224
+ };
172
225
  /** The receipt for one imported file; identical repeats return the same receipt. */
173
226
  export type EvalEvidenceFileReceipt = {
174
227
  readonly path: string;
@@ -178,3 +231,4 @@ export type EvalEvidenceFileReceipt = {
178
231
  export declare function isEvalSha256(value: string | null): value is string;
179
232
  /** Compare creation and operation facts without depending on JSON property order. */
180
233
  export declare function stableEvalFacts(value: unknown): string;
234
+ export {};
package/dist/index.js CHANGED
@@ -1,196 +1,294 @@
1
+ // packages/eval-harness/src/index.ts
1
2
  import { z } from "zod";
2
- export const EVAL_SERVICE_HEADER = "X-Eval-Service-Token";
3
- export const EVAL_HARNESS_PATH = "/internal/v1/eval-harness";
4
- const id = z.string().trim().min(1);
5
- const uuid = z.uuid();
6
- export const evalEnsureChatSchema = z.strictObject({
7
- chatId: uuid,
8
- role: z.enum(["subject", "evaluator"]),
9
- framework: z.literal("sp"),
10
- samplingId: id,
11
- projectId: id,
12
- agentConfig: z.strictObject({ ownerUserId: uuid, userAgentId: uuid }),
13
- /** Only an Evaluator carries it; the Evaluator still owns an ordinary Chat and its own Space. */
14
- evaluationId: id.optional(),
15
- runtime: z.strictObject({ timeoutMs: z.number().finite().positive().optional() }).optional(),
3
+
4
+ // packages/utils/src/json.ts
5
+ function canonicalJsonStringify(value) {
6
+ return JSON.stringify(
7
+ value,
8
+ (_key, item) => item !== null && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : item
9
+ );
10
+ }
11
+
12
+ // packages/utils/src/content-hash.ts
13
+ async function sha256Hex(value) {
14
+ const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value;
15
+ const buffer = new ArrayBuffer(bytes.byteLength);
16
+ new Uint8Array(buffer).set(bytes);
17
+ const digest = await crypto.subtle.digest("SHA-256", buffer);
18
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
19
+ }
20
+
21
+ // packages/eval-harness/src/index.ts
22
+ var EVAL_SERVICE_HEADER = "X-Eval-Service-Token";
23
+ var EVAL_HARNESS_PATH = "/internal/v1/eval-harness";
24
+ var id = z.string().trim().min(1);
25
+ var uuid = z.uuid();
26
+ var evalEnsureChatSchema = z.strictObject({
27
+ chatId: uuid,
28
+ role: z.enum(["subject", "evaluator"]),
29
+ framework: z.literal("sp"),
30
+ samplingId: id,
31
+ projectId: id,
32
+ agentConfig: z.strictObject({ ownerUserId: uuid, userAgentId: uuid }),
33
+ /** Only an Evaluator carries it; the Evaluator still owns an ordinary Chat and its own Space. */
34
+ evaluationId: id.optional(),
35
+ runtime: z.strictObject({ timeoutMs: z.number().finite().positive().optional() }).optional()
16
36
  }).superRefine((value, context) => {
17
- if ((value.role === "evaluator") !== (value.evaluationId !== undefined)) {
18
- context.addIssue({ code: "custom", path: ["evaluationId"], message: "Evaluation ID must match role" });
19
- }
37
+ if (value.role === "evaluator" !== (value.evaluationId !== void 0)) {
38
+ context.addIssue({ code: "custom", path: ["evaluationId"], message: "Evaluation ID must match role" });
39
+ }
40
+ });
41
+ var EVAL_INITIAL_FILE_MAX_BYTES = 5e6;
42
+ var EVAL_INITIALIZATION_MAX_BYTES = 2e7;
43
+ var EVAL_MATERIAL_MAX_FILES = 500;
44
+ var EVAL_SESSION_CONTENT_MAX_CHARS = 2e6;
45
+ var EVAL_SESSION_MAX_ENTRIES = 1e4;
46
+ var EVAL_OUTPUT_SCHEMA_PATH = "/evaluation-output.schema.json";
47
+ var EVAL_INITIAL_MATERIALS_ROOT = "/initial-materials";
48
+ var EVAL_INITIALIZATION_BODY_MAX_BYTES = Math.max(
49
+ Math.ceil(EVAL_INITIALIZATION_MAX_BYTES * 4 / 3) + EVAL_SESSION_CONTENT_MAX_CHARS * 6,
50
+ Math.ceil((EVAL_INITIALIZATION_MAX_BYTES - EVAL_INITIAL_FILE_MAX_BYTES) * 4 / 3) + EVAL_INITIAL_FILE_MAX_BYTES * 6
51
+ ) + 1e6;
52
+ var base64Bytes = (value) => value.length / 4 * 3 - (value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0);
53
+ var utf8Bytes = (value) => new TextEncoder().encode(value).byteLength;
54
+ var evalMaterialFilesByteSize = (files) => files.reduce((sum, file) => sum + base64Bytes(file.contentBase64), 0);
55
+ var tooBig = (context, maximum, message, path = []) => context.addIssue({ code: "too_big", origin: "file", maximum, inclusive: true, path, message });
56
+ var maxFileBytes = (bytes) => (value, context) => {
57
+ if (bytes(value) > EVAL_INITIAL_FILE_MAX_BYTES) tooBig(context, EVAL_INITIAL_FILE_MAX_BYTES, "File is too large");
58
+ };
59
+ var evalMaterialFileSchema = z.strictObject({
60
+ path: z.string().refine((path) => evalPathParts(path) !== null, "Invalid initial material path"),
61
+ mediaType: id,
62
+ contentBase64: z.base64().superRefine(maxFileBytes(base64Bytes))
20
63
  });
21
- export const evalMaterialFileSchema = z.strictObject({
22
- path: z.string().startsWith("/").min(2),
23
- mediaType: id,
24
- contentBase64: z.base64(),
64
+ var evalMaterialFilesSchema = z.array(evalMaterialFileSchema).max(EVAL_MATERIAL_MAX_FILES).superRefine((files, context) => {
65
+ const seen = /* @__PURE__ */ new Set();
66
+ files.forEach(({ path }, index) => {
67
+ if (seen.has(path)) context.addIssue({ code: "custom", path: [index, "path"], message: "Duplicate material path" });
68
+ seen.add(path);
69
+ });
25
70
  });
26
- export const evalInitialContextSchema = z.strictObject({
27
- format: z.enum(["json", "jsonl"]),
28
- content: z.string().min(1),
71
+ var evalInitialMaterialSchema = z.strictObject({
72
+ bundleId: id,
73
+ materialId: id,
74
+ files: evalMaterialFilesSchema
29
75
  });
30
- export const evalInitializationSchema = z.strictObject({
31
- idempotencyKey: id,
32
- data: z.record(z.string(), z.unknown()).and(z.object({ userPrompt: z.string().trim().min(1) })),
33
- initialSession: z.strictObject({
34
- bundleId: id,
35
- sessionId: id,
36
- context: evalInitialContextSchema.optional(),
37
- }).optional(),
38
- initialMaterials: z.strictObject({
39
- bundleId: id,
40
- materialId: id,
41
- files: z.array(evalMaterialFileSchema).max(500),
42
- }).optional(),
76
+ var evalInitialContextSchema = z.strictObject({
77
+ format: z.enum(["json", "jsonl"]),
78
+ content: z.string().min(1)
79
+ });
80
+ var evalInitializationSchema = z.strictObject({
81
+ idempotencyKey: id,
82
+ data: z.record(z.string(), z.unknown()).and(z.object({ userPrompt: z.string().trim().min(1) })),
83
+ initialSession: z.strictObject({
84
+ bundleId: id,
85
+ sessionId: id,
86
+ context: evalInitialContextSchema.optional()
87
+ }).optional(),
88
+ initialMaterials: z.array(evalInitialMaterialSchema).min(1).optional(),
89
+ evaluationOutputSchema: z.strictObject({
90
+ content: z.string().min(1).superRefine(maxFileBytes(utf8Bytes))
91
+ }).optional()
43
92
  }).superRefine((value, context) => {
44
- const session = value.data.initialSession;
45
- const materials = value.data.initialMaterials;
46
- if (session ? !value.initialSession || session.bundleId !== value.initialSession.bundleId ||
47
- session.sessionId !== value.initialSession.sessionId : value.initialSession !== undefined) {
48
- context.addIssue({ code: "custom", path: ["initialSession"], message: "Session content must match the Data reference" });
49
- }
50
- if (materials ? !value.initialMaterials || materials.bundleId !== value.initialMaterials.bundleId ||
51
- materials.materialId !== value.initialMaterials.materialId : value.initialMaterials !== undefined) {
52
- context.addIssue({ code: "custom", path: ["initialMaterials"], message: "Material content must match the Data reference" });
53
- }
93
+ const session = value.data.initialSession;
94
+ const refs = value.data.initialMaterials;
95
+ const materials = value.initialMaterials;
96
+ if (session ? !value.initialSession || session.bundleId !== value.initialSession.bundleId || session.sessionId !== value.initialSession.sessionId : value.initialSession !== void 0) {
97
+ context.addIssue({ code: "custom", path: ["initialSession"], message: "Session content must match the Data reference" });
98
+ }
99
+ if (refs === void 0 ? materials !== void 0 : !Array.isArray(refs) || refs.length !== materials?.length || materials.some((material, index) => {
100
+ const ref = refs[index];
101
+ return ref?.bundleId !== material.bundleId || ref?.materialId !== material.materialId;
102
+ })) {
103
+ context.addIssue({ code: "custom", path: ["initialMaterials"], message: "Material content must match the Data reference" });
104
+ }
105
+ const seen = /* @__PURE__ */ new Set();
106
+ materials?.forEach(({ bundleId, materialId }, index) => {
107
+ const key = JSON.stringify([bundleId, materialId]);
108
+ if (seen.has(key)) context.addIssue({ code: "custom", path: ["initialMaterials", index], message: "Duplicate material" });
109
+ seen.add(key);
110
+ });
111
+ const total = (materials ?? []).reduce(
112
+ (sum, material) => sum + evalMaterialFilesByteSize(material.files),
113
+ value.evaluationOutputSchema ? utf8Bytes(value.evaluationOutputSchema.content) : 0
114
+ );
115
+ if (total > EVAL_INITIALIZATION_MAX_BYTES) {
116
+ tooBig(context, EVAL_INITIALIZATION_MAX_BYTES, "Initialization files are too large");
117
+ }
54
118
  });
55
- export const evalSubmitSchema = z.strictObject({
56
- idempotencyKey: id,
57
- prompt: z.string().trim().min(1),
58
- unattended: z.literal(true),
59
- runtime: z.strictObject({ timeoutMs: z.number().finite().positive().optional() }).optional(),
119
+ var evalInitializationResultSchema = z.strictObject({
120
+ manifestHash: id,
121
+ target: z.literal("workspace"),
122
+ materials: z.array(z.strictObject({ bundleId: id, materialId: id, rootPath: z.string().startsWith("/") })),
123
+ evaluationOutputSchemaPath: z.literal(EVAL_OUTPUT_SCHEMA_PATH).exactOptional()
60
124
  });
61
- export const evalBillingResourcesSchema = z.strictObject({
62
- usageEvents: z.array(z.record(z.string(), z.unknown())),
63
- billingSettlements: z.array(z.record(z.string(), z.unknown())),
125
+ var evalSubmitSchema = z.strictObject({
126
+ idempotencyKey: id,
127
+ prompt: z.string().trim().min(1),
128
+ unattended: z.literal(true),
129
+ runtime: z.strictObject({ timeoutMs: z.number().finite().positive().optional() }).optional()
64
130
  });
65
- export function projectEvalSubmission(input) {
66
- const normalizedStatus = {
67
- pending: "queued",
68
- running: "running",
69
- completed: "completed",
70
- error: "failed",
71
- skipped: "failed",
72
- aborted: "interrupted",
73
- }[input.status];
74
- if (!normalizedStatus)
75
- throw new Error("Unknown SP Submission status");
76
- return { ...input, normalizedStatus: normalizedStatus };
77
- }
78
- /** JSON stores one canonical snapshot; JSONL stores one canonical entry per line. */
79
- export function readEvalCanonicalContext(input) {
80
- if (input.content.length > 2_000_000)
81
- throw new Error("Initial context is too large");
82
- const parsed = input.format === "json"
83
- ? JSON.parse(input.content)
84
- : { entries: input.content.split(/\r?\n/u).filter((line) => line.trim()).map((line) => JSON.parse(line)) };
85
- if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.entries)) {
86
- throw new Error("Initial context must contain canonical entries");
87
- }
88
- const entries = parsed.entries;
89
- if (entries.length > 10_000)
90
- throw new Error("Initial context has too many entries");
91
- const seen = new Set();
92
- for (const entry of entries) {
93
- if (!entry || typeof entry !== "object")
94
- throw new Error("Invalid canonical context entry");
95
- const row = entry;
96
- if (typeof row.id !== "string" || !row.id || seen.has(row.id) ||
97
- !Number.isSafeInteger(row.createdAt) || Number(row.createdAt) < 0 ||
98
- !row.message || typeof row.message !== "object" || Array.isArray(row.message) ||
99
- !["user", "assistant", "toolResult"].includes(row.message.role ?? "")) {
100
- throw new Error("Invalid or duplicate canonical context entry");
101
- }
102
- seen.add(row.id);
103
- }
104
- if (entries.length > 0 && entries[0].message.role !== "user") {
105
- throw new Error("Initial context must begin with a user message");
106
- }
107
- return { entries: entries };
108
- }
109
- /** The one file an Evaluator writes its scored result to, in its own Space. */
110
- export const EVAL_RESULT_FILE = "result.json";
111
- export const EVAL_RESULT_PATH = `/output/${EVAL_RESULT_FILE}`;
112
- export const EVAL_RESULT_MAX_BYTES = 1_000_000;
113
- /** Reads the result file as raw UTF-8 text; any byte order mark and formatting are kept. */
114
- export async function readEvalStructuredOutput(path, bytes) {
115
- if (bytes.byteLength > EVAL_RESULT_MAX_BYTES)
116
- throw new Error(`${EVAL_RESULT_FILE} exceeds ${EVAL_RESULT_MAX_BYTES} bytes`);
117
- let content;
118
- try {
119
- content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
120
- }
121
- catch {
122
- throw new Error(`${EVAL_RESULT_FILE} is not valid UTF-8`);
131
+ var evalBillingResourcesSchema = z.strictObject({
132
+ usageEvents: z.array(z.record(z.string(), z.unknown())),
133
+ billingSettlements: z.array(z.record(z.string(), z.unknown()))
134
+ });
135
+ function projectEvalSubmission(input) {
136
+ const normalizedStatus = {
137
+ pending: "queued",
138
+ running: "running",
139
+ completed: "completed",
140
+ error: "failed",
141
+ skipped: "failed",
142
+ aborted: "interrupted"
143
+ }[input.status];
144
+ if (!normalizedStatus) throw new Error("Unknown SP Submission status");
145
+ return { ...input, normalizedStatus };
146
+ }
147
+ function readEvalCanonicalContext(input) {
148
+ if (input.content.length > EVAL_SESSION_CONTENT_MAX_CHARS) throw new Error("Initial context is too large");
149
+ const parsed = input.format === "json" ? JSON.parse(input.content) : { entries: input.content.split(/\r?\n/u).filter((line) => line.trim()).map((line) => JSON.parse(line)) };
150
+ if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.entries)) {
151
+ throw new Error("Initial context must contain canonical entries");
152
+ }
153
+ const entries = parsed.entries;
154
+ if (entries.length > EVAL_SESSION_MAX_ENTRIES) throw new Error("Initial context has too many entries");
155
+ const seen = /* @__PURE__ */ new Set();
156
+ for (const entry of entries) {
157
+ if (!entry || typeof entry !== "object") throw new Error("Invalid canonical context entry");
158
+ const row = entry;
159
+ if (typeof row.id !== "string" || !row.id || seen.has(row.id) || !Number.isSafeInteger(row.createdAt) || Number(row.createdAt) < 0 || !row.message || typeof row.message !== "object" || Array.isArray(row.message) || !["user", "assistant", "toolResult"].includes(row.message.role ?? "")) {
160
+ throw new Error("Invalid or duplicate canonical context entry");
123
161
  }
124
- const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(bytes)));
125
- const contentHash = `sha256:${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
126
- return { path, content, contentHash };
162
+ seen.add(row.id);
163
+ }
164
+ if (entries.length > 0 && entries[0].message.role !== "user") {
165
+ throw new Error("Initial context must begin with a user message");
166
+ }
167
+ return { entries };
127
168
  }
128
- export function evalMaterialRoot(bundleId, materialId) {
129
- return `/initial-materials/${encodeURIComponent(bundleId)}/${encodeURIComponent(materialId)}`;
169
+ var EVAL_RESULT_FILE = "result.json";
170
+ var EVAL_RESULT_PATH = `/output/${EVAL_RESULT_FILE}`;
171
+ var EVAL_RESULT_MAX_BYTES = 1e6;
172
+ async function readEvalStructuredOutput(path, bytes) {
173
+ if (bytes.byteLength > EVAL_RESULT_MAX_BYTES) throw new Error(`${EVAL_RESULT_FILE} exceeds ${EVAL_RESULT_MAX_BYTES} bytes`);
174
+ let content;
175
+ try {
176
+ content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
177
+ } catch {
178
+ throw new Error(`${EVAL_RESULT_FILE} is not valid UTF-8`);
179
+ }
180
+ const contentHash = `sha256:${await sha256Hex(bytes)}`;
181
+ return { path, content, contentHash };
182
+ }
183
+ function evalMaterialRoot(bundleId, materialId) {
184
+ return `${EVAL_INITIAL_MATERIALS_ROOT}/${evalPathSegment(bundleId)}/${evalPathSegment(materialId)}`;
185
+ }
186
+ function evalPathSegment(value) {
187
+ return encodeURIComponent(value).replaceAll(".", "%2E");
130
188
  }
131
- /** Absolute paths below a root; `.`/`..`, `.git`, backslashes and NUL never reach the Space. */
132
189
  function evalPathParts(path) {
133
- const parts = path.split("/").slice(1);
134
- if (!path.startsWith("/") || parts.length === 0 || parts.some((part) => !part || part === "." || part === ".." || part === ".git" || part.includes("\\") || part.includes("\0"))) {
135
- return null;
136
- }
137
- return parts;
138
- }
139
- export function evalMaterialPath(root, path) {
140
- const parts = evalPathParts(path);
141
- if (!parts)
142
- throw new Error("Invalid initial material path");
143
- return `${root}/${parts.join("/")}`;
144
- }
145
- /*
146
- * Evidence: a Subject's artifacts and complete trajectory are exported once, then imported into
147
- * the Evaluator's own Space under `/evidence`. Neither Chat ever reads the other's Space.
148
- */
149
- /** Where imported evidence lives in the Evaluator's own Space. */
150
- export const EVAL_EVIDENCE_ROOT = "/evidence";
151
- /** One exported or imported file crosses a single Durable Object RPC as bytes. */
152
- export const EVAL_EVIDENCE_FILE_MAX_BYTES = 25_000_000;
153
- export const EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX = 200;
154
- export const EVAL_EVIDENCE_SHA256_HEADER = "X-Eval-Content-Sha256";
155
- /** One Subject artifact; its bytes are read separately so the inventory stays small. */
156
- export const evalEvidenceArtifactSchema = z.strictObject({
157
- path: z.string().startsWith("/").min(2),
158
- size: z.number().int().nonnegative(),
159
- mediaType: id,
190
+ const parts = path.split("/").slice(1);
191
+ if (!path.startsWith("/") || parts.length === 0 || parts.some((part) => !part || part === "." || part === ".." || part === ".git" || part.includes("\\") || part.includes("\0"))) {
192
+ return null;
193
+ }
194
+ return parts;
195
+ }
196
+ function evalMaterialPath(root, path) {
197
+ const parts = evalPathParts(path);
198
+ if (!parts) throw new Error("Invalid initial material path");
199
+ return `${root}/${parts.join("/")}`;
200
+ }
201
+ var EVAL_EVIDENCE_ROOT = "/evidence";
202
+ var EVAL_EVIDENCE_PATHS = {
203
+ subjectInputs: "/subject-inputs",
204
+ artifacts: "/artifacts",
205
+ trajectory: "/trajectory",
206
+ manifest: "/manifest.json"
207
+ };
208
+ var EVAL_EVIDENCE_FILE_MAX_BYTES = 25e6;
209
+ var EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX = 200;
210
+ var EVAL_EVIDENCE_SHA256_HEADER = "X-Eval-Content-Sha256";
211
+ var evalEvidenceArtifactSchema = z.strictObject({
212
+ path: z.string().startsWith("/").min(2),
213
+ size: z.number().int().nonnegative(),
214
+ mediaType: id
160
215
  });
161
- export const evalEvidenceArtifactsSchema = z.strictObject({
162
- artifacts: z.array(evalEvidenceArtifactSchema),
216
+ var evalEvidenceArtifactsSchema = z.strictObject({
217
+ artifacts: z.array(evalEvidenceArtifactSchema)
163
218
  });
164
- export function readEvalTrajectoryPageQuery(params) {
165
- const afterSeq = Number(params.get("afterSeq") ?? "0");
166
- const limit = Number(params.get("limit") ?? String(EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX));
167
- if (!Number.isSafeInteger(afterSeq) || afterSeq < 0 ||
168
- !Number.isSafeInteger(limit) || limit < 1 || limit > EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX) {
169
- throw new Error("Invalid trajectory page");
170
- }
171
- return { afterSeq, limit };
172
- }
173
- /** A Space path for one evidence file; `relative` keeps the bundle's own layout below `/evidence`. */
174
- export function evalEvidencePath(relative) {
175
- const parts = evalPathParts(relative);
176
- if (!parts)
177
- throw new Error("Invalid evidence path");
178
- return `${EVAL_EVIDENCE_ROOT}/${parts.join("/")}`;
179
- }
180
- export function isEvalSha256(value) {
181
- return value !== null && /^[0-9a-f]{64}$/u.test(value);
182
- }
183
- /** Compare creation and operation facts without depending on JSON property order. */
184
- export function stableEvalFacts(value) {
185
- return JSON.stringify(sortFacts(value));
186
- }
187
- function sortFacts(value) {
188
- if (Array.isArray(value))
189
- return value.map(sortFacts);
190
- if (!value || typeof value !== "object")
191
- return value;
192
- return Object.fromEntries(Object.entries(value)
193
- .filter(([, item]) => item !== undefined)
194
- .sort(([left], [right]) => left.localeCompare(right))
195
- .map(([key, item]) => [key, sortFacts(item)]));
219
+ function readEvalTrajectoryPageQuery(params) {
220
+ const afterSeq = Number(params.get("afterSeq") ?? "0");
221
+ const limit = Number(params.get("limit") ?? String(EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX));
222
+ if (!Number.isSafeInteger(afterSeq) || afterSeq < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX) {
223
+ throw new Error("Invalid trajectory page");
224
+ }
225
+ return { afterSeq, limit };
226
+ }
227
+ function evalEvidencePath(relative) {
228
+ const parts = evalPathParts(relative);
229
+ if (!parts || relative !== EVAL_EVIDENCE_PATHS.manifest && ![EVAL_EVIDENCE_PATHS.subjectInputs, EVAL_EVIDENCE_PATHS.artifacts, EVAL_EVIDENCE_PATHS.trajectory].some((root) => relative.startsWith(`${root}/`))) throw new Error("Invalid evidence path");
230
+ return `${EVAL_EVIDENCE_ROOT}/${parts.join("/")}`;
231
+ }
232
+ function evalSubjectInputEvidencePath(bundleId, materialId, path) {
233
+ return evalMaterialPath(
234
+ `${EVAL_EVIDENCE_PATHS.subjectInputs}/${evalPathSegment(bundleId)}/${evalPathSegment(materialId)}`,
235
+ path
236
+ );
237
+ }
238
+ function evalArtifactEvidencePath(path) {
239
+ return evalMaterialPath(EVAL_EVIDENCE_PATHS.artifacts, path);
240
+ }
241
+ function evalTrajectoryEvidencePath(part) {
242
+ if (!Number.isSafeInteger(part) || part < 1) throw new Error("Invalid trajectory part");
243
+ return `${EVAL_EVIDENCE_PATHS.trajectory}/events-${String(part).padStart(6, "0")}.jsonl`;
244
+ }
245
+ function isEvalSha256(value) {
246
+ return value !== null && /^[0-9a-f]{64}$/u.test(value);
247
+ }
248
+ function stableEvalFacts(value) {
249
+ const facts = canonicalJsonStringify(value);
250
+ if (facts === void 0) throw new TypeError("Eval facts must be JSON");
251
+ return facts;
196
252
  }
253
+ export {
254
+ EVAL_EVIDENCE_FILE_MAX_BYTES,
255
+ EVAL_EVIDENCE_PATHS,
256
+ EVAL_EVIDENCE_ROOT,
257
+ EVAL_EVIDENCE_SHA256_HEADER,
258
+ EVAL_EVIDENCE_TRAJECTORY_PAGE_MAX,
259
+ EVAL_HARNESS_PATH,
260
+ EVAL_INITIALIZATION_BODY_MAX_BYTES,
261
+ EVAL_INITIALIZATION_MAX_BYTES,
262
+ EVAL_INITIAL_FILE_MAX_BYTES,
263
+ EVAL_INITIAL_MATERIALS_ROOT,
264
+ EVAL_MATERIAL_MAX_FILES,
265
+ EVAL_OUTPUT_SCHEMA_PATH,
266
+ EVAL_RESULT_FILE,
267
+ EVAL_RESULT_MAX_BYTES,
268
+ EVAL_RESULT_PATH,
269
+ EVAL_SERVICE_HEADER,
270
+ EVAL_SESSION_CONTENT_MAX_CHARS,
271
+ EVAL_SESSION_MAX_ENTRIES,
272
+ evalArtifactEvidencePath,
273
+ evalBillingResourcesSchema,
274
+ evalEnsureChatSchema,
275
+ evalEvidenceArtifactSchema,
276
+ evalEvidenceArtifactsSchema,
277
+ evalEvidencePath,
278
+ evalInitialContextSchema,
279
+ evalInitializationResultSchema,
280
+ evalInitializationSchema,
281
+ evalMaterialFilesByteSize,
282
+ evalMaterialFilesSchema,
283
+ evalMaterialPath,
284
+ evalMaterialRoot,
285
+ evalSubjectInputEvidencePath,
286
+ evalSubmitSchema,
287
+ evalTrajectoryEvidencePath,
288
+ isEvalSha256,
289
+ projectEvalSubmission,
290
+ readEvalCanonicalContext,
291
+ readEvalStructuredOutput,
292
+ readEvalTrajectoryPageQuery,
293
+ stableEvalFacts
294
+ };
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "@springbrand/eval-harness",
3
- "version": "0.1.0-alpha.0",
4
- "description": "SP service contract for Eval-managed Chats: routes, headers and wire schemas.",
5
- "license": "MIT",
3
+ "version": "0.1.0-alpha.1",
6
4
  "type": "module",
5
+ "license": "UNLICENSED",
7
6
  "exports": {
8
7
  ".": {
9
8
  "types": "./dist/index.d.ts",
@@ -14,21 +13,17 @@
14
13
  "dist",
15
14
  "README.md"
16
15
  ],
17
- "publishConfig": {
18
- "access": "public",
19
- "tag": "alpha"
20
- },
16
+ "sideEffects": false,
21
17
  "dependencies": {
22
18
  "zod": "^4.6.5"
23
19
  },
24
- "devDependencies": {
25
- "typescript": "^7.0.2",
26
- "vitest": "^4.1.10"
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "tag": "alpha"
27
23
  },
28
- "scripts": {
29
- "build": "tsc --noEmit",
30
- "check": "tsc --noEmit",
31
- "test": "vitest run",
32
- "build:dist": "rm -rf dist && tsc -p tsconfig.build.json"
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/springbrand-lab/sp-platform.git",
27
+ "directory": "packages/eval-harness"
33
28
  }
34
- }
29
+ }