@openpond/evals 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LEARNING.md +106 -0
  2. package/README.md +2 -0
  3. package/dist/graders.js +26 -5
  4. package/dist/index.js +3 -0
  5. package/dist/javascript-verifier-contract.js +7 -0
  6. package/dist/javascript-verifier-node.js +56 -0
  7. package/dist/javascript-verifier-worker-source.js +1 -0
  8. package/dist/javascript-verifier-worker.js +6 -0
  9. package/dist/javascript-verifier.js +64 -0
  10. package/dist/learning/admission.js +209 -0
  11. package/dist/learning/assets.js +37 -0
  12. package/dist/learning/contracts.js +267 -0
  13. package/dist/learning/errors.js +12 -0
  14. package/dist/learning/grade-worker.js +99 -0
  15. package/dist/learning/index.js +9 -0
  16. package/dist/learning/operations.js +33 -0
  17. package/dist/learning/repository.js +48 -0
  18. package/dist/learning/service.js +367 -0
  19. package/dist/learning/transport.js +25 -0
  20. package/dist/rewards.js +230 -0
  21. package/dist/task-schema.js +142 -0
  22. package/dist/tasksets.js +3 -0
  23. package/dist/telemetry-catalog.js +1 -1
  24. package/dist/types/builtin-benchmarks/harness-refiner.d.ts +7 -0
  25. package/dist/types/builtin-benchmarks/harness-refiner.d.ts.map +1 -1
  26. package/dist/types/conformance.d.ts +14 -0
  27. package/dist/types/conformance.d.ts.map +1 -1
  28. package/dist/types/execution-contracts.d.ts +14 -0
  29. package/dist/types/execution-contracts.d.ts.map +1 -1
  30. package/dist/types/graders.d.ts.map +1 -1
  31. package/dist/types/index.d.ts +3 -0
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/dist/types/javascript-verifier-contract.d.ts +9 -0
  34. package/dist/types/javascript-verifier-contract.d.ts.map +1 -0
  35. package/dist/types/javascript-verifier-node.d.ts +5 -0
  36. package/dist/types/javascript-verifier-node.d.ts.map +1 -0
  37. package/dist/types/javascript-verifier-worker-source.d.ts +2 -0
  38. package/dist/types/javascript-verifier-worker.d.ts +2 -0
  39. package/dist/types/javascript-verifier-worker.d.ts.map +1 -0
  40. package/dist/types/javascript-verifier.d.ts +15 -0
  41. package/dist/types/javascript-verifier.d.ts.map +1 -0
  42. package/dist/types/learning/admission.d.ts +335 -0
  43. package/dist/types/learning/admission.d.ts.map +1 -0
  44. package/dist/types/learning/assets.d.ts +49 -0
  45. package/dist/types/learning/assets.d.ts.map +1 -0
  46. package/dist/types/learning/contracts.d.ts +2149 -0
  47. package/dist/types/learning/contracts.d.ts.map +1 -0
  48. package/dist/types/learning/errors.d.ts +8 -0
  49. package/dist/types/learning/errors.d.ts.map +1 -0
  50. package/dist/types/learning/grade-worker.d.ts +31 -0
  51. package/dist/types/learning/grade-worker.d.ts.map +1 -0
  52. package/dist/types/learning/index.d.ts +10 -0
  53. package/dist/types/learning/index.d.ts.map +1 -0
  54. package/dist/types/learning/operations.d.ts +2061 -0
  55. package/dist/types/learning/operations.d.ts.map +1 -0
  56. package/dist/types/learning/repository.d.ts +1143 -0
  57. package/dist/types/learning/repository.d.ts.map +1 -0
  58. package/dist/types/learning/service.d.ts +22 -0
  59. package/dist/types/learning/service.d.ts.map +1 -0
  60. package/dist/types/learning/transport.d.ts +2139 -0
  61. package/dist/types/learning/transport.d.ts.map +1 -0
  62. package/dist/types/preferences.d.ts +3 -3
  63. package/dist/types/review-conformance.d.ts +5 -5
  64. package/dist/types/rewards.d.ts +611 -0
  65. package/dist/types/rewards.d.ts.map +1 -0
  66. package/dist/types/task-schema.d.ts +18 -0
  67. package/dist/types/task-schema.d.ts.map +1 -0
  68. package/dist/types/tasksets.d.ts +28 -0
  69. package/dist/types/tasksets.d.ts.map +1 -1
  70. package/package.json +30 -3
  71. package/schemas/learning/v1/asset.schema.json +89 -0
  72. package/schemas/learning/v1/batch.schema.json +132 -0
  73. package/schemas/learning/v1/binding.schema.json +207 -0
  74. package/schemas/learning/v1/command-request.schema.json +2414 -0
  75. package/schemas/learning/v1/decision.schema.json +440 -0
  76. package/schemas/learning/v1/definition.schema.json +397 -0
  77. package/schemas/learning/v1/evidence.schema.json +317 -0
  78. package/schemas/learning/v1/example-submission.schema.json +253 -0
  79. package/schemas/learning/v1/feedback-submission.schema.json +114 -0
  80. package/schemas/learning/v1/feedback.schema.json +233 -0
  81. package/schemas/learning/v1/grade.schema.json +412 -0
  82. package/schemas/learning/v1/iteration.schema.json +236 -0
  83. package/schemas/learning/v1/package.schema.json +776 -0
  84. package/schemas/learning/v1/policy.schema.json +373 -0
  85. package/schemas/learning/v1/read-request.schema.json +101 -0
  86. package/schemas/learning/v1/reward.schema.json +302 -0
  87. package/schemas/learning/v1/source.schema.json +198 -0
package/LEARNING.md ADDED
@@ -0,0 +1,106 @@
1
+ # Task evidence and reusable Rewards
2
+
3
+ `@openpond/evals/learning` owns the portable data and domain rules for task
4
+ definitions, sources, submissions, feedback, admission, immutable task batches,
5
+ learning policies and grading jobs. `@openpond/evals/rewards` owns immutable
6
+ Reward releases and exact versioned bindings. The package does not contain a
7
+ database, credentials, cloud client or execution infrastructure.
8
+
9
+ Use `openpond-sdk/learning` to submit the same commands to an authenticated local
10
+ or hosted execution owner. Plain HTTP producers use `POST /v1/learning/commands`
11
+ with `{ "scope": "<profile-or-team>", "command": { ... } }` and a Bearer token.
12
+ Reads use `POST /v1/learning/read`. Scope is a requested ownership boundary; the
13
+ host must authorize it from the credential and assign the actor itself.
14
+
15
+ ## Data flow
16
+
17
+ 1. Publish a Reward, then a binding, task definition and source. Use
18
+ `publish_resources` to publish dependent resources atomically. Pin an existing
19
+ release by `{ id, revision, contentHash }`; edits create the next revision with
20
+ an expected current revision.
21
+ 2. Submit task examples under a stable source/example/attempt identity. Preserve
22
+ the producer idempotency key on retries. Reusing a key with different content
23
+ fails with a conflict. Invalid task-specific evidence remains reviewable;
24
+ accepting the submission does not approve it for training.
25
+ 3. Queue durable grading of the observed output or a proposed supervised target.
26
+ Hosts execute the binding through `TaskGradeExecutor`; the built-in executor
27
+ supports portable deterministic checks and reports other implementations as
28
+ unavailable. Publication and schema validity are not execution receipts.
29
+ 4. Record a reviewer decision referencing completed grader-run IDs. The service
30
+ checks the exact task, evidence, binding and output hashes. A failed observed
31
+ response may reveal a valid task. Supervised learning additionally requires a
32
+ separately approved, schema-valid target whose required checks passed.
33
+ 5. Seal reviewed evidence into a batch. The service reserves both task-family and
34
+ exact-input identities across splits in the same transaction. Unresolved
35
+ families, stale evidence, stale decisions and held-out/training overlap fail.
36
+ `compileTaskBatch` creates the existing `openpond.tasksetRelease.v2` package.
37
+
38
+ Observed output, verifier expected output, private evaluator context and an
39
+ approved supervised target have separate fields. The public policy view excludes
40
+ expected output and private context. Compiled packages retain the approved target
41
+ in typed learning metadata for a training adapter; they do not replace verifier
42
+ ground truth with a demonstration.
43
+
44
+ Feedback addresses source/example/attempt identity and may arrive before the
45
+ example. Input, ground-truth and family corrections create new evidence revisions.
46
+ Existing batches retain their exact historical evidence. Proposed-target feedback
47
+ must be graded and reviewed before becoming a supervised target.
48
+
49
+ ## JSON and schema profile
50
+
51
+ Generated HTTP/producer schemas are in `schemas/learning/v1`. They describe the
52
+ structural contract. Use the domain service for authorization, revision/hash
53
+ identity, admission and transactional constraints; JSON Schema alone cannot prove
54
+ those relationships. Input/output schemas use object envelopes and JSON Schema
55
+ 2020-12 through Ajv, with a bounded portable profile:
56
+
57
+ - Local `#/$defs/...` references; no remote loading or recursive schemas.
58
+ - No regex, formats, custom keywords or runtime code inside schema validation.
59
+ - At most 32 KiB of schema, bounded expansion/alternatives and validation caches.
60
+ - Plain JSON values, finite numbers, no accessors/serializers/cycles, bounded depth,
61
+ node count and UTF-8 bytes. Host request limits apply in addition to field limits.
62
+
63
+ This profile follows [Ajv's security guidance](https://ajv.js.org/security.html)
64
+ for untrusted schemas and deliberately rejects unsupported capabilities.
65
+
66
+ ## Host responsibilities
67
+
68
+ Authored verifier code and rubrics use immutable `asset` resources. Publish source
69
+ and its Reward with `publish_resources`; the service checks UTF-8 size, SHA-256,
70
+ full reference identity, scope and evaluator visibility in the same transaction.
71
+ Editing source creates a new asset identity. Existing Rewards retain their bytes.
72
+
73
+ `@openpond/evals/javascript-verifier/node` executes ESM verifier source in a
74
+ worker containing a fresh QuickJS WebAssembly interpreter. The verifier receives
75
+ JSON data and returns `{ score, passed, feedback, evidenceRefs? }`. It has no host
76
+ functions or import loader. Memory, stack, source/result sizes and execution time
77
+ are bounded; cancellation settles only after the worker has terminated. The
78
+ interpreter and worker are embedded in the package, so a clean install needs no
79
+ separate WASM download or native compiler. Browser/worker hosts can use the
80
+ portable `@openpond/evals/javascript-verifier` entry point directly.
81
+
82
+ This boundary follows the [QuickJS runtime isolation and resource APIs](https://github.com/justjake/quickjs-emscripten).
83
+ Node's [VM documentation](https://nodejs.org/api/vm.html) explicitly excludes
84
+ untrusted-code isolation; the local Taskset adapter now uses the same public
85
+ interpreter worker as task-evidence grading.
86
+
87
+ Implement `LearningRepository.transaction` with serialized writes and atomic
88
+ rollback. Persist immutable historical revisions, exact operation receipts and
89
+ unique split reservations. Never share a transaction object after it closes.
90
+ List results are cursor-paginated at no more than 100 resources.
91
+
92
+ Authorize every operation. Source credentials can submit only to their assigned
93
+ source; human admission/correction/sealing require a reviewer. Derive identities
94
+ from authentication, never from a producer-supplied actor field.
95
+
96
+ Grading workers use leases, stable job identity, bounded timeout/spend and terminal
97
+ receipts. Execution adapters must deduplicate submission and confirm cleanup.
98
+ Cancellation remains `cancelling` until the execution owner confirms termination;
99
+ a UI cancellation request is not proof that compute stopped. Restart recovery
100
+ must use the same durable jobs. Unexpected host failures remain server errors;
101
+ `LearningDomainError` exposes rejected requests as typed 4xx failures.
102
+
103
+ Policy and iteration contracts distinguish training parent, optional teacher and
104
+ upstream trigger. The contracts themselves do not launch training, schedule jobs,
105
+ qualify a model, accept a candidate or promote a serving endpoint. Hosts must
106
+ implement those transitions and record actual execution/evaluation receipts.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # `@openpond/evals`
2
2
 
3
+ See [task evidence and learning](./LEARNING.md) for reusable Rewards, task-format publication, SDK/HTTP intake, grading, review and approved batches.
4
+
3
5
  Portable evaluation and benchmark contracts plus pure helpers for Tasksets,
4
6
  graders, run manifests, attempt and evaluation receipts, paired benchmark
5
7
  comparisons, execution adapters, conformance fixtures, Work-evidence
package/dist/graders.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { FailureClassSchema, ReleaseHashSchema, ReleaseIdSchema, contentHash } from "@openpond/harness";
3
+ import { validateTaskSchema, validateTaskValue } from "./task-schema.js";
3
4
  export const GraderEvidenceContentSchema = z.object({
4
5
  schemaVersion: z.literal("openpond.graderEvidence.v1"),
5
6
  graderId: ReleaseIdSchema,
@@ -48,22 +49,42 @@ function gradeDeterministic(grader, task, attempt) {
48
49
  const outputField = string(config.outputField) ?? "text";
49
50
  const actual = string(attempt.output[outputField]);
50
51
  const expected = string(config.expectedValue) ?? string(task.expectedOutput?.[string(config.expectedField) ?? "text"]);
52
+ if (expected === null || !normalize(expected))
53
+ return unavailable("Content grading requires a nonempty expected value.");
51
54
  passed = actual !== null && expected !== null && normalize(actual) === normalize(expected);
52
55
  }
53
56
  else if (grader.kind === "schema") {
54
- passed = strings(config.requiredKeys).every((key) => Object.hasOwn(attempt.output, key));
57
+ if (config.jsonSchema !== undefined) {
58
+ const schema = validateTaskSchema(config.jsonSchema);
59
+ if (!schema.valid)
60
+ return unavailable(schema.issues[0].message);
61
+ passed = validateTaskValue(config.jsonSchema, attempt.output).valid;
62
+ }
63
+ else {
64
+ const required = strings(config.requiredKeys);
65
+ if (!required.length)
66
+ return unavailable("Schema grading requires a JSON Schema or explicit required keys.");
67
+ passed = required.every((key) => Object.hasOwn(attempt.output, key));
68
+ }
55
69
  }
56
70
  else if (grader.kind === "artifact") {
57
71
  const contains = string(config.refIncludes) ?? "";
72
+ if (!contains)
73
+ return unavailable("Artifact grading requires a declared artifact reference.");
58
74
  passed = attempt.artifactRefs.some((ref) => ref.includes(contains));
59
75
  }
60
76
  else if (grader.kind === "runtime_event") {
61
- passed = strings(config.requiredEvents).every((required) => attempt.runtimeEventRefs.some((ref) => ref.includes(required)));
77
+ const requiredEvents = strings(config.requiredEvents);
78
+ if (!requiredEvents.length)
79
+ return unavailable("Runtime event grading requires declared events.");
80
+ passed = requiredEvents.every((required) => attempt.runtimeEventRefs.some((ref) => ref.includes(required)));
62
81
  }
63
82
  else {
64
83
  const fields = strings(config.fields);
65
84
  const compared = fields.length ? fields : Object.keys(task.expectedOutput ?? {});
66
- passed = compared.every((field) => Object.is(attempt.output[field], task.expectedOutput?.[field]));
85
+ if (!compared.length || compared.some((field) => !task.expectedOutput || !Object.hasOwn(task.expectedOutput, field)))
86
+ return unavailable("State grading requires expected values for every compared field.");
87
+ passed = compared.every((field) => Object.hasOwn(attempt.output, field) && contentHash(attempt.output[field]) === contentHash(task.expectedOutput[field]));
67
88
  }
68
89
  return {
69
90
  score: passed ? 1 : 0,
@@ -81,7 +102,7 @@ function evidence(grader, result) {
81
102
  graderId: grader.id,
82
103
  graderVersion: grader.version,
83
104
  ...result,
84
- rewardEligible: grader.rewardEligible && result.score !== null,
105
+ rewardEligible: grader.rewardEligible && result.rewardEligible && result.score !== null,
85
106
  });
86
107
  return GraderEvidenceSchema.parse({ ...content, contentHash: contentHash(content) });
87
108
  }
@@ -89,5 +110,5 @@ function unavailable(message) {
89
110
  return { score: null, passed: false, rewardEligible: false, failureClass: "grader_failure", feedback: [message], visibleEvidenceRefs: [], privilegedEvidenceRefs: [] };
90
111
  }
91
112
  function string(value) { return typeof value === "string" ? value : null; }
92
- function strings(value) { return Array.isArray(value) ? value.filter((item) => typeof item === "string") : []; }
113
+ function strings(value) { return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : []; }
93
114
  function normalize(value) { return value.normalize("NFKC").trim().replace(/[,,]/g, "").replace(/[.\s]+$/g, "").replace(/\s+/g, " "); }
package/dist/index.js CHANGED
@@ -17,3 +17,6 @@ export * from "./telemetry-catalog.js";
17
17
  export * from "./telemetry-analysis.js";
18
18
  export * from "./telemetry-bundle.js";
19
19
  export * from "./tasksets.js";
20
+ export * from "./task-schema.js";
21
+ export * from "./rewards.js";
22
+ export * from "./learning/index.js";
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ export const JavaScriptVerifierResultSchema = z.object({
3
+ score: z.number().finite().min(0).max(1),
4
+ passed: z.boolean(),
5
+ feedback: z.string().max(20_000),
6
+ evidenceRefs: z.array(z.string().max(240)).max(1_000).default([]),
7
+ }).strict();
@@ -0,0 +1,56 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import { JavaScriptVerifierResultSchema } from "./javascript-verifier-contract.js";
3
+ import { javascriptVerifierWorkerSource } from "./javascript-verifier-worker-source.js";
4
+ import { assertBoundedTaskJson } from "./task-schema.js";
5
+ /** Resolves/rejects only after the execution owner has stopped the worker. */
6
+ export async function executeJavaScriptVerifierInWorker(input) {
7
+ input.signal?.throwIfAborted();
8
+ if (!Number.isInteger(input.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > 300_000)
9
+ throw new Error("verifier_timeout_invalid");
10
+ assertBoundedTaskJson(input.value, 4_194_304);
11
+ if (new TextEncoder().encode(input.source).byteLength > 524_288)
12
+ throw new Error("verifier_source_too_large");
13
+ const { signal, ...workerData } = input;
14
+ return new Promise((resolve, reject) => {
15
+ const worker = new Worker(javascriptVerifierWorkerSource, {
16
+ eval: true, workerData, execArgv: [], env: {},
17
+ resourceLimits: { maxOldGenerationSizeMb: 64, maxYoungGenerationSizeMb: 16, stackSizeMb: 4 },
18
+ });
19
+ let settling = false;
20
+ const finish = (outcome) => {
21
+ if (settling)
22
+ return;
23
+ settling = true;
24
+ clearTimeout(timer);
25
+ signal?.removeEventListener("abort", cancel);
26
+ void worker.terminate().then(() => {
27
+ if ("error" in outcome)
28
+ reject(outcome.error);
29
+ else
30
+ resolve(outcome.result);
31
+ }, reject);
32
+ };
33
+ const cancel = () => finish({ error: signal?.reason instanceof Error ? signal.reason : new Error("verifier_cancelled") });
34
+ const timer = setTimeout(() => finish({ error: new Error("verifier_timeout") }), input.timeoutMs);
35
+ signal?.addEventListener("abort", cancel, { once: true });
36
+ worker.once("error", (error) => finish({ error }));
37
+ worker.once("exit", () => { if (!settling)
38
+ finish({ error: new Error("verifier_worker_exited_without_result") }); });
39
+ worker.once("message", (message) => {
40
+ try {
41
+ if (!message || typeof message !== "object" || !("ok" in message))
42
+ throw new Error("verifier_worker_invalid_response");
43
+ if (message.ok !== true)
44
+ throw new Error("error" in message && typeof message.error === "string" ? message.error : "verifier_execution_failed");
45
+ if (!("result" in message))
46
+ throw new Error("verifier_worker_invalid_response");
47
+ finish({ result: JavaScriptVerifierResultSchema.parse(message.result) });
48
+ }
49
+ catch (error) {
50
+ finish({ error: error instanceof Error ? error : new Error("verifier_worker_invalid_response") });
51
+ }
52
+ });
53
+ if (signal?.aborted)
54
+ cancel();
55
+ });
56
+ }