@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
@@ -0,0 +1,6 @@
1
+ import { parentPort, workerData } from "node:worker_threads";
2
+ import { executeJavaScriptVerifier } from "./javascript-verifier.js";
3
+ // Only this trusted host program runs in Node. Authored source is interpreter data.
4
+ if (!parentPort)
5
+ throw new Error("Verifier worker requires its execution owner.");
6
+ void executeJavaScriptVerifier(workerData).then((result) => parentPort.postMessage({ ok: true, result }), (error) => parentPort.postMessage({ ok: false, error: error instanceof Error ? error.message : "Verifier execution failed." })).finally(() => parentPort.close());
@@ -0,0 +1,64 @@
1
+ import { newQuickJSWASMModuleFromVariant, Scope } from "quickjs-emscripten-core";
2
+ import variant from "@jitl/quickjs-singlefile-cjs-release-sync";
3
+ import { assertBoundedTaskJson } from "./task-schema.js";
4
+ import { JavaScriptVerifierResultSchema } from "./javascript-verifier-contract.js";
5
+ export { JavaScriptVerifierResultSchema } from "./javascript-verifier-contract.js";
6
+ /**
7
+ * A fresh WebAssembly interpreter with no host functions, filesystem, network,
8
+ * timers, or module loader. Hosts should run this on a worker to remain responsive
9
+ * and terminate that worker when cancellation is requested.
10
+ */
11
+ export async function executeJavaScriptVerifier(input) {
12
+ if (!Number.isInteger(input.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > 300_000)
13
+ throw new Error("verifier_timeout_invalid");
14
+ if (new TextEncoder().encode(input.source).byteLength > 524_288)
15
+ throw new Error("verifier_source_too_large");
16
+ assertBoundedTaskJson(input.value, 4_194_304);
17
+ input.signal?.throwIfAborted();
18
+ const deadline = Date.now() + input.timeoutMs;
19
+ const module = await newQuickJSWASMModuleFromVariant(variant);
20
+ input.signal?.throwIfAborted();
21
+ return Scope.withScope((scope) => {
22
+ const runtime = scope.manage(module.newRuntime());
23
+ runtime.setMemoryLimit(33_554_432);
24
+ runtime.setMaxStackSize(262_144);
25
+ runtime.setInterruptHandler(() => Date.now() >= deadline || input.signal?.aborted === true);
26
+ const context = scope.manage(runtime.newContext());
27
+ const stringify = scope.manage(context.unwrapResult(context.evalCode("JSON.stringify")));
28
+ const argument = scope.manage(context.unwrapResult(context.evalCode(`JSON.parse(${JSON.stringify(JSON.stringify(input.value))})`)));
29
+ const exported = scope.manage(context.unwrapResult(context.evalCode(input.source, "verifier.mjs", { type: "module" })));
30
+ const namespace = scope.manage(settle(context, exported, deadline, input.signal));
31
+ const fn = scope.manage(context.getProp(namespace, input.exportName ?? "verify"));
32
+ if (context.typeof(fn) !== "function")
33
+ throw new Error("verifier_function_export_missing");
34
+ const returned = scope.manage(context.unwrapResult(context.callFunction(fn, context.undefined, argument)));
35
+ const result = scope.manage(settle(context, returned, deadline, input.signal));
36
+ const serialized = scope.manage(context.unwrapResult(context.callFunction(stringify, context.undefined, result)));
37
+ if (context.typeof(serialized) !== "string")
38
+ throw new Error("verifier_result_not_json");
39
+ const json = context.getString(serialized);
40
+ if (new TextEncoder().encode(json).byteLength > 65_536)
41
+ throw new Error("verifier_result_too_large");
42
+ input.signal?.throwIfAborted();
43
+ if (Date.now() >= deadline)
44
+ throw new Error("verifier_timeout");
45
+ const value = JSON.parse(json);
46
+ assertBoundedTaskJson(value, 65_536);
47
+ return JavaScriptVerifierResultSchema.parse(value);
48
+ });
49
+ }
50
+ function settle(context, handle, deadline, signal) {
51
+ for (;;) {
52
+ signal?.throwIfAborted();
53
+ if (Date.now() >= deadline)
54
+ throw new Error("verifier_timeout");
55
+ const state = context.getPromiseState(handle);
56
+ if (state.type === "fulfilled")
57
+ return state.notAPromise ? state.value.dup() : state.value;
58
+ if (state.type === "rejected")
59
+ return context.unwrapResult(state);
60
+ const jobs = context.runtime.executePendingJobs(1);
61
+ if (jobs.unwrap() === 0)
62
+ throw new Error("verifier_unresolved_promise");
63
+ }
64
+ }
@@ -0,0 +1,209 @@
1
+ import { LearningDomainError } from "./errors.js";
2
+ import { z } from "zod";
3
+ import { contentHash } from "@openpond/harness";
4
+ import { compileBoundGraders, resolveBoundRewards, RewardBindingSchema, RewardReleaseSchema } from "../rewards.js";
5
+ import { TaskRecordSchema, TasksetReleaseContentSchema, TasksetReleaseSchema } from "../tasksets.js";
6
+ import { assertBoundedTaskJson, validateTaskValue } from "../task-schema.js";
7
+ import { assertLearningContentHash, LearningJsonObjectSchema, learningRef, LearningRevisionRefSchema, LearningSourceSchema, sameLearningRef, sealLearningContent, TaskAdmissionDecisionSchema, TaskBatchContentSchema, TaskBatchSchema, TaskDefinitionSchema, TaskEvidenceSchema, TaskExampleSubmissionSchema } from "./contracts.js";
8
+ export function inspectTaskEvidence(evidence, definition) {
9
+ assertLearningContentHash(TaskEvidenceSchema.parse(evidence));
10
+ assertLearningContentHash(TaskDefinitionSchema.parse(definition));
11
+ const example = evidence.submission;
12
+ const issues = [];
13
+ if (!sameLearningRef(example.taskDefinition, learningRef(definition)))
14
+ issues.push({ path: "/taskDefinition", code: "definition_mismatch", message: "Evidence must pin this exact task definition." });
15
+ const input = validateTaskValue(definition.inputSchema, example.input);
16
+ issues.push(...input.issues.map((issue) => ({ ...issue, path: `/input${issue.path}` })));
17
+ if (example.expected !== null) {
18
+ const expected = validateTaskValue(definition.outputSchema, example.expected);
19
+ issues.push(...expected.issues.map((issue) => ({ ...issue, path: `/expected${issue.path}` })));
20
+ }
21
+ const evidenceValidity = issues.length ? "invalid" : "valid";
22
+ if (!example.familyKey)
23
+ issues.push({ path: "/familyKey", code: "family_unresolved", message: "Resolve the source family before admitting this task." });
24
+ return { evidenceValidity, taskReady: issues.length === 0, observedOutputValid: example.observedOutput === null ? null : validateTaskValue(definition.outputSchema, example.observedOutput).valid, issues };
25
+ }
26
+ export function validateSourceSubmission(sourceInput, submissionInput) {
27
+ const source = LearningSourceSchema.parse(sourceInput);
28
+ assertLearningContentHash(source);
29
+ assertBoundedTaskJson(submissionInput);
30
+ const submission = TaskExampleSubmissionSchema.parse(submissionInput);
31
+ if (!source.enabled)
32
+ throw new LearningDomainError("learning_source_disabled", 409);
33
+ if (source.id !== submission.sourceId || !sameLearningRef(source.taskDefinition, submission.taskDefinition))
34
+ throw new LearningDomainError("learning_source_definition_mismatch", 422);
35
+ if (!source.allowedSplits.includes(submission.split))
36
+ throw new LearningDomainError("learning_source_split_not_allowed", 422);
37
+ const mappingHash = source.mapping ? contentHash(source.mapping) : null;
38
+ if (mappingHash !== submission.provenance.mappingHash)
39
+ throw new LearningDomainError("learning_source_mapping_mismatch", 422);
40
+ return submission;
41
+ }
42
+ export function mapTaskSourceRecord(input) {
43
+ assertBoundedTaskJson(input.record);
44
+ const source = LearningSourceSchema.parse(input.source);
45
+ assertLearningContentHash(source);
46
+ if (!source.mapping)
47
+ throw new LearningDomainError("learning_source_mapping_required", 422);
48
+ const mapping = source.mapping;
49
+ const fields = ["exampleId", "attemptId", "occurredAt", "input", "familyKey"];
50
+ const values = Object.fromEntries(fields.map((field) => [field, readTaskJsonPointer(input.record, mapping[field])]));
51
+ const missing = fields.filter((field) => values[field] === undefined || values[field] === null).map(String);
52
+ if (missing.length)
53
+ return { status: "pending", missing };
54
+ const example = TaskExampleSubmissionSchema.parse({
55
+ schemaVersion: "openpond.taskExample.v1", sourceId: source.id, idempotencyKey: input.idempotencyKey,
56
+ taskDefinition: source.taskDefinition, ...values, split: mapping.split,
57
+ observedOutput: mapping.observedOutput === null ? null : readTaskJsonPointer(input.record, mapping.observedOutput) ?? null,
58
+ expected: mapping.expected === null ? null : readTaskJsonPointer(input.record, mapping.expected) ?? null,
59
+ evaluatorContext: mapping.evaluatorContext === null ? null : readTaskJsonPointer(input.record, mapping.evaluatorContext) ?? null,
60
+ assets: [], provenance: { sourceRecordRef: null, mappingHash: contentHash(mapping) },
61
+ });
62
+ return { status: "mapped", example: validateSourceSubmission(source, example) };
63
+ }
64
+ export function readTaskJsonPointer(value, pointer) {
65
+ if (pointer === "")
66
+ return value;
67
+ if (!pointer.startsWith("/") || /~(?![01])/u.test(pointer))
68
+ throw new LearningDomainError("task_json_pointer_invalid", 422);
69
+ let current = value;
70
+ for (const part of pointer.slice(1).split("/")) {
71
+ const key = part.replaceAll("~1", "/").replaceAll("~0", "~");
72
+ if (!current || typeof current !== "object" || !Object.hasOwn(current, key))
73
+ return undefined;
74
+ current = current[key];
75
+ }
76
+ return current;
77
+ }
78
+ export function taskRecordFromEvidence(evidence, definition) {
79
+ const inspection = inspectTaskEvidence(evidence, definition);
80
+ if (!inspection.taskReady)
81
+ throw new LearningDomainError("task_evidence_not_ready", 422, inspection.issues[0].code);
82
+ return TaskRecordSchema.parse({
83
+ id: evidence.id, clusterKey: evidence.submission.familyKey, split: evidence.submission.split,
84
+ input: evidence.submission.input, expectedOutput: evidence.submission.expected,
85
+ policyVisibleContext: { instructions: definition.instructions },
86
+ privilegedContextRef: evidence.submission.evaluatorContext === null ? null : `task-evidence:${evidence.contentHash}`,
87
+ artifactRefs: evidence.submission.assets, tags: [definition.category],
88
+ });
89
+ }
90
+ export function taskAttemptEvidence(evidence, output) {
91
+ return { output, runtimeEventRefs: [], artifactRefs: evidence.submission.assets.map((asset) => asset.id) };
92
+ }
93
+ export function assertAdmissionDecision(input) {
94
+ const decision = TaskAdmissionDecisionSchema.parse(input.decision);
95
+ assertLearningContentHash(decision);
96
+ if (!sameLearningRef(decision.evidence, learningRef(input.evidence)))
97
+ throw new LearningDomainError("task_admission_evidence_mismatch", 422);
98
+ const inspection = inspectTaskEvidence(input.evidence, input.definition);
99
+ if (!inspection.taskReady || decision.evidenceValidity !== "valid" || decision.taskAdmissibility !== "approved")
100
+ throw new LearningDomainError("task_admission_not_approved", 422);
101
+ if (input.purpose === "supervised_training") {
102
+ if (decision.targetApproval !== "approved" || decision.approvedTarget === null)
103
+ throw new LearningDomainError("supervised_target_not_approved", 422);
104
+ if (!validateTaskValue(input.definition.outputSchema, decision.approvedTarget).valid)
105
+ throw new LearningDomainError("supervised_target_schema_invalid", 422);
106
+ if (!decision.targetGrade)
107
+ throw new LearningDomainError("supervised_target_grade_required", 422);
108
+ assertGradeIdentity(decision.targetGrade, input.evidence, input.definition, decision.approvedTarget);
109
+ const scores = [decision.targetGrade.training, decision.targetGrade.evaluation].filter((score) => score.status !== "not_configured");
110
+ if (!scores.length || scores.some((score) => score.status !== "scored" || score.passed !== true))
111
+ throw new LearningDomainError("supervised_target_checks_not_passed", 422);
112
+ }
113
+ if (decision.grade) {
114
+ if (!input.evidence.submission.observedOutput)
115
+ throw new LearningDomainError("observed_grade_without_output", 422);
116
+ assertGradeIdentity(decision.grade, input.evidence, input.definition, input.evidence.submission.observedOutput);
117
+ }
118
+ }
119
+ export function assertGradeIdentity(grade, evidence, definition, output) {
120
+ assertLearningContentHash(grade);
121
+ if (!sameLearningRef(grade.binding, definition.rewardBinding) || grade.taskHash !== contentHash(taskRecordFromEvidence(evidence, definition)) || grade.outputHash !== contentHash(taskAttemptEvidence(evidence, output)))
122
+ throw new LearningDomainError("task_grade_identity_mismatch", 422);
123
+ }
124
+ export function taskFamilyReservations(evidence, definition) {
125
+ if (!evidence.submission.familyKey)
126
+ throw new LearningDomainError("task_family_unresolved", 422);
127
+ return [
128
+ { namespace: definition.familyNamespace, kind: "family", key: evidence.submission.familyKey, split: evidence.submission.split },
129
+ { namespace: definition.familyNamespace, kind: "input", key: contentHash(evidence.submission.input), split: evidence.submission.split },
130
+ ];
131
+ }
132
+ export function sealTaskBatch(input) {
133
+ const definition = TaskDefinitionSchema.parse(input.definition);
134
+ assertLearningContentHash(definition);
135
+ if (!sameLearningRef(definition.rewardBinding, learningRef(input.binding)))
136
+ throw new LearningDomainError("task_definition_reward_binding_mismatch", 422);
137
+ resolveBoundRewards(input.binding, input.rewards);
138
+ if (input.purpose === "reward_training" && !input.binding.sources.some((source) => source.role === "training" && source.weight > 0))
139
+ throw new LearningDomainError("training_reward_not_configured", 422);
140
+ if (!input.evidence.length || new Set(input.evidence.map((evidence) => evidence.id)).size !== input.evidence.length)
141
+ throw new LearningDomainError("task_batch_evidence_set_invalid", 422);
142
+ const splits = new Map(input.priorSplits.map((reservation) => [contentHash([reservation.namespace, reservation.kind, reservation.key]), reservation.split]));
143
+ const examples = input.evidence.map((evidence) => {
144
+ const decision = input.decisions.find((candidate) => sameLearningRef(candidate.evidence, learningRef(evidence)));
145
+ if (!decision)
146
+ throw new LearningDomainError("task_admission_missing", 422, evidence.id);
147
+ assertAdmissionDecision({ decision, evidence, definition, purpose: input.purpose });
148
+ if ((input.purpose === "evaluation") === (evidence.submission.split === "train"))
149
+ throw new LearningDomainError("task_batch_split_not_allowed", 422);
150
+ for (const reservation of taskFamilyReservations(evidence, definition)) {
151
+ const key = contentHash([reservation.namespace, reservation.kind, reservation.key]);
152
+ const prior = splits.get(key);
153
+ if (prior !== undefined && prior !== reservation.split)
154
+ throw new LearningDomainError("task_family_split_contamination", 409);
155
+ splits.set(key, reservation.split);
156
+ }
157
+ return { evidence: learningRef(evidence), decision: learningRef(decision), familyKey: evidence.submission.familyKey, inputHash: contentHash(evidence.submission.input), split: evidence.submission.split };
158
+ });
159
+ const content = TaskBatchContentSchema.parse({ schemaVersion: "openpond.taskBatch.v1", id: input.id, revision: 1, taskDefinition: learningRef(definition), rewardBinding: learningRef(input.binding), purpose: input.purpose, examples, sealedAt: input.now, sealedBy: input.actorId });
160
+ return TaskBatchSchema.parse(sealLearningContent(content));
161
+ }
162
+ export const TaskBatchPackageMetadataSchema = z.object({
163
+ schemaVersion: z.literal("openpond.taskBatchPackage.v1"),
164
+ batch: LearningRevisionRefSchema,
165
+ definition: TaskDefinitionSchema,
166
+ binding: RewardBindingSchema,
167
+ rewards: z.array(RewardReleaseSchema).min(1).max(100),
168
+ admissions: z.array(z.object({ taskId: z.string(), evidence: LearningRevisionRefSchema, decision: LearningRevisionRefSchema, supervisedTarget: LearningJsonObjectSchema.nullable() }).strict()).min(1).max(10_000),
169
+ }).strict();
170
+ export function compileTaskBatch(input) {
171
+ assertLearningContentHash(TaskBatchSchema.parse(input.batch));
172
+ assertLearningContentHash(TaskDefinitionSchema.parse(input.definition));
173
+ if (!sameLearningRef(input.definition.rewardBinding, learningRef(input.binding)))
174
+ throw new LearningDomainError("task_definition_reward_binding_mismatch", 422);
175
+ if (!sameLearningRef(input.batch.taskDefinition, learningRef(input.definition)) || !sameLearningRef(input.batch.rewardBinding, learningRef(input.binding)))
176
+ throw new LearningDomainError("task_batch_release_mismatch", 422);
177
+ const tasks = [];
178
+ const admissions = [];
179
+ for (const entry of input.batch.examples) {
180
+ const evidence = input.evidence.find((candidate) => sameLearningRef(learningRef(candidate), entry.evidence));
181
+ const decision = input.decisions.find((candidate) => sameLearningRef(learningRef(candidate), entry.decision));
182
+ if (!evidence || !decision)
183
+ throw new LearningDomainError("task_batch_admission_revision_missing", 422);
184
+ assertAdmissionDecision({ decision, evidence, definition: input.definition, purpose: input.batch.purpose });
185
+ if (entry.familyKey !== evidence.submission.familyKey || entry.inputHash !== contentHash(evidence.submission.input) || entry.split !== evidence.submission.split)
186
+ throw new LearningDomainError("task_batch_evidence_snapshot_mismatch", 422);
187
+ tasks.push(taskRecordFromEvidence(evidence, input.definition));
188
+ admissions.push({ taskId: evidence.id, evidence: entry.evidence, decision: entry.decision, supervisedTarget: input.batch.purpose === "supervised_training" ? decision.approvedTarget : null });
189
+ }
190
+ const learning = TaskBatchPackageMetadataSchema.parse({ schemaVersion: "openpond.taskBatchPackage.v1", batch: learningRef(input.batch), definition: input.definition, binding: input.binding, rewards: input.rewards, admissions });
191
+ const content = TasksetReleaseContentSchema.parse({
192
+ schemaVersion: "openpond.tasksetRelease.v2", id: `task-batch-${input.batch.id}`, revision: 1,
193
+ ...input.definition.execution, tasks, graders: compileBoundGraders(input.binding, input.rewards),
194
+ metadata: { learning },
195
+ });
196
+ return TasksetReleaseSchema.parse(sealLearningContent(content));
197
+ }
198
+ export function taskBatchPackageMetadata(release) {
199
+ assertLearningContentHash(TasksetReleaseSchema.parse(release));
200
+ const metadata = TaskBatchPackageMetadataSchema.parse(release.metadata.learning);
201
+ assertLearningContentHash(metadata.definition);
202
+ if (!sameLearningRef(metadata.definition.rewardBinding, learningRef(metadata.binding)))
203
+ throw new LearningDomainError("task_definition_reward_binding_mismatch", 422);
204
+ if (contentHash(release.graders) !== contentHash(compileBoundGraders(metadata.binding, metadata.rewards)))
205
+ throw new LearningDomainError("taskset_reward_snapshot_mismatch", 422);
206
+ if (metadata.admissions.length !== release.tasks.length || new Set(metadata.admissions.map((entry) => entry.taskId)).size !== release.tasks.length || metadata.admissions.some((entry) => !release.tasks.some((task) => task.id === entry.taskId)))
207
+ throw new LearningDomainError("taskset_admission_inventory_mismatch", 422);
208
+ return metadata;
209
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import { ImmutableAssetRefSchema, ReleaseHashSchema, ReleaseIdSchema, contentHash, sha256 } from "@openpond/harness";
3
+ import { assertBoundedTaskJson } from "../task-schema.js";
4
+ import { LearningDomainError } from "./errors.js";
5
+ /** Small authored source files. Large binary evidence uses the host asset API. */
6
+ export const LearningTextAssetContentSchema = z.object({
7
+ schemaVersion: z.literal("openpond.learningTextAsset.v1"),
8
+ id: ReleaseIdSchema,
9
+ revision: z.literal(1),
10
+ asset: ImmutableAssetRefSchema,
11
+ text: z.string().max(524_288),
12
+ }).strict().superRefine((value, context) => {
13
+ if (value.asset.id !== value.id)
14
+ context.addIssue({ code: "custom", path: ["asset", "id"], message: "Asset identity must match the stored resource." });
15
+ if (value.asset.sizeBytes !== new TextEncoder().encode(value.text).byteLength || value.asset.sizeBytes > 524_288)
16
+ context.addIssue({ code: "custom", path: ["asset", "sizeBytes"], message: "Authored source must match its byte count and fit within 512 KiB." });
17
+ if (value.asset.contentHash !== sha256(value.text))
18
+ context.addIssue({ code: "custom", path: ["asset", "contentHash"], message: "Asset source does not match its SHA-256." });
19
+ });
20
+ export const LearningTextAssetSchema = LearningTextAssetContentSchema.safeExtend({ contentHash: ReleaseHashSchema }).strict();
21
+ export function createLearningTextAsset(input) {
22
+ assertBoundedTaskJson(input);
23
+ const { text, ...attributes } = input;
24
+ const hash = sha256(text);
25
+ const id = `asset-${hash}-${contentHash(attributes).slice(0, 16)}`;
26
+ const content = LearningTextAssetContentSchema.parse({
27
+ schemaVersion: "openpond.learningTextAsset.v1", id, revision: 1,
28
+ asset: { ...attributes, id, contentHash: hash, sizeBytes: new TextEncoder().encode(text).byteLength }, text,
29
+ });
30
+ return LearningTextAssetSchema.parse({ ...content, contentHash: contentHash(content) });
31
+ }
32
+ export function verifyLearningTextAsset(asset, reference) {
33
+ const { contentHash: hash, ...content } = LearningTextAssetSchema.parse(asset);
34
+ if (contentHash(content) !== hash || contentHash(content.asset) !== contentHash(reference))
35
+ throw new LearningDomainError("learning_asset_reference_mismatch", 409);
36
+ return content.text;
37
+ }
@@ -0,0 +1,267 @@
1
+ import { LearningDomainError } from "./errors.js";
2
+ import { z } from "zod";
3
+ import { ImmutableAssetRefSchema, ImmutableReleaseRefSchema, ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema, contentHash } from "@openpond/harness";
4
+ import { RewardBindingSchema, RewardCompositionSchema, RewardReleaseRefSchema, RewardReleaseSchema } from "../rewards.js";
5
+ import { TaskSplitSchema, TasksetReleaseContentSchema } from "../tasksets.js";
6
+ import { assertBoundedTaskJson, validateTaskSchema } from "../task-schema.js";
7
+ export const LearningJsonObjectSchema = z.record(z.string(), z.json());
8
+ export const LearningRevisionRefSchema = RewardReleaseRefSchema;
9
+ export const LearningJsonPointerSchema = z.string().max(2_000).refine((value) => value === "" || (value.startsWith("/") && !/~(?![01])/u.test(value)), "Use an RFC 6901 JSON pointer.");
10
+ export const TaskDefinitionContentSchema = z.object({
11
+ schemaVersion: z.literal("openpond.taskDefinition.v1"),
12
+ id: ReleaseIdSchema,
13
+ revision: z.number().int().positive(),
14
+ name: z.string().trim().min(1).max(500),
15
+ description: z.string().max(10_000),
16
+ instructions: z.string().trim().min(1).max(20_000),
17
+ category: z.enum(["structured", "question_answering", "coding", "tool_workflow", "custom"]),
18
+ familyNamespace: ReleaseIdSchema,
19
+ inputSchema: z.record(z.string(), z.unknown()),
20
+ outputSchema: z.record(z.string(), z.unknown()),
21
+ rewardBinding: LearningRevisionRefSchema,
22
+ harness: ImmutableReleaseRefSchema.nullable(),
23
+ execution: TasksetReleaseContentSchema.pick({ policy: true, environment: true, environmentRelease: true, tools: true, capabilities: true, verifierSetRelease: true }).strict(),
24
+ }).strict().superRefine((definition, context) => {
25
+ for (const field of ["inputSchema", "outputSchema"]) {
26
+ const schema = definition[field];
27
+ if (schema.type !== "object")
28
+ context.addIssue({ code: "custom", path: [field], message: "Task input/output use object envelopes; declare type: object." });
29
+ const report = validateTaskSchema(schema);
30
+ if (!report.valid)
31
+ context.addIssue({ code: "custom", path: [field], message: report.issues[0].message });
32
+ }
33
+ if (Boolean(definition.execution.environmentRelease) !== Boolean(definition.execution.verifierSetRelease))
34
+ context.addIssue({ code: "custom", path: ["execution"], message: "Bind both the environment and verifier-set releases." });
35
+ });
36
+ export const TaskDefinitionSchema = TaskDefinitionContentSchema.safeExtend({ contentHash: ReleaseHashSchema }).strict();
37
+ export const TaskSourceMappingSchema = z.object({
38
+ schemaVersion: z.literal("openpond.taskSourceMapping.v1"),
39
+ exampleId: LearningJsonPointerSchema,
40
+ attemptId: LearningJsonPointerSchema,
41
+ occurredAt: LearningJsonPointerSchema,
42
+ input: LearningJsonPointerSchema,
43
+ observedOutput: LearningJsonPointerSchema.nullable(),
44
+ expected: LearningJsonPointerSchema.nullable(),
45
+ evaluatorContext: LearningJsonPointerSchema.nullable(),
46
+ familyKey: LearningJsonPointerSchema,
47
+ split: TaskSplitSchema,
48
+ }).strict();
49
+ export const LearningSourceContentSchema = z.object({
50
+ schemaVersion: z.literal("openpond.learningSource.v1"),
51
+ id: ReleaseIdSchema,
52
+ revision: z.number().int().positive(),
53
+ name: z.string().trim().min(1).max(500),
54
+ kind: z.enum(["direct", "benchmark", "work", "opentelemetry", "provider"]),
55
+ taskDefinition: LearningRevisionRefSchema,
56
+ enabled: z.boolean(),
57
+ allowedSplits: z.array(TaskSplitSchema).min(1).max(4),
58
+ mapping: TaskSourceMappingSchema.nullable(),
59
+ adapterVersion: z.string().trim().min(1).max(200).nullable(),
60
+ }).strict().superRefine((source, context) => {
61
+ if (source.kind !== "direct" && !source.mapping)
62
+ context.addIssue({ code: "custom", path: ["mapping"], message: "Mapped sources require an explicit mapping." });
63
+ if (source.mapping && !source.allowedSplits.includes(source.mapping.split))
64
+ context.addIssue({ code: "custom", path: ["allowedSplits"], message: "The mapping split must be allowed by this source." });
65
+ if (["opentelemetry", "provider"].includes(source.kind) && !source.adapterVersion)
66
+ context.addIssue({ code: "custom", path: ["adapterVersion"], message: "Trace adapters require a pinned version." });
67
+ });
68
+ export const LearningSourceSchema = LearningSourceContentSchema.safeExtend({ contentHash: ReleaseHashSchema }).strict();
69
+ export const TaskExampleSubmissionSchema = z.object({
70
+ schemaVersion: z.literal("openpond.taskExample.v1"),
71
+ sourceId: ReleaseIdSchema,
72
+ idempotencyKey: ReleaseIdSchema,
73
+ taskDefinition: LearningRevisionRefSchema,
74
+ exampleId: ReleaseIdSchema,
75
+ attemptId: ReleaseIdSchema,
76
+ occurredAt: ReleaseTimestampSchema,
77
+ familyKey: ReleaseIdSchema.nullable(),
78
+ split: TaskSplitSchema,
79
+ input: LearningJsonObjectSchema,
80
+ observedOutput: LearningJsonObjectSchema.nullable(),
81
+ expected: LearningJsonObjectSchema.nullable(),
82
+ evaluatorContext: LearningJsonObjectSchema.nullable(),
83
+ assets: z.array(ImmutableAssetRefSchema).max(1_000),
84
+ provenance: z.object({ sourceRecordRef: z.string().max(2_000).nullable(), mappingHash: ReleaseHashSchema.nullable() }).strict(),
85
+ }).strict();
86
+ export const TaskEvidenceContentSchema = z.object({
87
+ schemaVersion: z.literal("openpond.taskEvidence.v1"),
88
+ id: ReleaseIdSchema,
89
+ revision: z.number().int().positive(),
90
+ source: LearningRevisionRefSchema,
91
+ submission: TaskExampleSubmissionSchema,
92
+ supersedes: LearningRevisionRefSchema.nullable(),
93
+ correctionFeedbackId: ReleaseIdSchema.nullable(),
94
+ receivedAt: ReleaseTimestampSchema,
95
+ }).strict();
96
+ export const TaskEvidenceSchema = TaskEvidenceContentSchema.extend({ contentHash: ReleaseHashSchema }).strict();
97
+ export const TaskFeedbackSubmissionSchema = z.object({
98
+ schemaVersion: z.literal("openpond.taskFeedback.v1"),
99
+ sourceId: ReleaseIdSchema,
100
+ idempotencyKey: ReleaseIdSchema,
101
+ exampleId: ReleaseIdSchema,
102
+ attemptId: ReleaseIdSchema,
103
+ expectedEvidenceHash: ReleaseHashSchema.nullable(),
104
+ occurredAt: ReleaseTimestampSchema,
105
+ kind: z.enum(["outcome", "target_correction", "ground_truth_correction", "input_correction", "family_resolution"]),
106
+ value: LearningJsonObjectSchema,
107
+ note: z.string().max(20_000),
108
+ }).strict();
109
+ export const TaskFeedbackSchema = z.object({
110
+ schemaVersion: z.literal("openpond.taskFeedbackRecord.v1"),
111
+ id: ReleaseIdSchema,
112
+ submission: TaskFeedbackSubmissionSchema,
113
+ status: z.enum(["pending_example", "pending_review", "applied", "superseded", "rejected"]),
114
+ evidence: LearningRevisionRefSchema.nullable(),
115
+ createdAt: ReleaseTimestampSchema,
116
+ revision: z.number().int().positive(),
117
+ review: z.object({ actorId: ReleaseIdSchema, decision: LearningRevisionRefSchema.nullable(), note: z.string().max(20_000), resolvedAt: ReleaseTimestampSchema }).strict().nullable().default(null),
118
+ }).strict();
119
+ export const TaskAdmissionDecisionContentSchema = z.object({
120
+ schemaVersion: z.literal("openpond.taskAdmissionDecision.v1"),
121
+ id: ReleaseIdSchema,
122
+ revision: z.number().int().positive(),
123
+ evidence: LearningRevisionRefSchema,
124
+ supersedes: LearningRevisionRefSchema.nullable(),
125
+ actor: z.object({ kind: z.enum(["human", "qualified_policy"]), id: ReleaseIdSchema, policy: LearningRevisionRefSchema.nullable() }).strict(),
126
+ evidenceValidity: z.enum(["valid", "invalid", "pending"]),
127
+ taskAdmissibility: z.enum(["approved", "rejected", "pending"]),
128
+ observedQuality: z.enum(["passed", "failed", "unscored", "unavailable"]),
129
+ targetApproval: z.enum(["approved", "rejected", "pending", "not_required"]),
130
+ approvedTarget: LearningJsonObjectSchema.nullable(),
131
+ grade: RewardCompositionSchema.nullable(),
132
+ targetGrade: RewardCompositionSchema.nullable(),
133
+ note: z.string().max(20_000),
134
+ decidedAt: ReleaseTimestampSchema,
135
+ }).strict().superRefine((decision, context) => {
136
+ if (decision.targetApproval === "approved" && decision.approvedTarget === null)
137
+ context.addIssue({ code: "custom", path: ["approvedTarget"], message: "Target approval requires an actual target." });
138
+ if (decision.targetApproval !== "approved" && decision.approvedTarget !== null)
139
+ context.addIssue({ code: "custom", path: ["approvedTarget"], message: "Only approved targets may be selected for supervised learning." });
140
+ if (decision.actor.kind === "qualified_policy" && !decision.actor.policy)
141
+ context.addIssue({ code: "custom", path: ["actor"], message: "Automatic admission must pin its qualified policy." });
142
+ if (decision.taskAdmissibility === "approved" && decision.evidenceValidity !== "valid")
143
+ context.addIssue({ code: "custom", path: ["taskAdmissibility"], message: "Only valid evidence can produce an approved task." });
144
+ });
145
+ export const TaskAdmissionDecisionSchema = TaskAdmissionDecisionContentSchema.safeExtend({ contentHash: ReleaseHashSchema }).strict();
146
+ export const TaskBatchContentSchema = z.object({
147
+ schemaVersion: z.literal("openpond.taskBatch.v1"),
148
+ id: ReleaseIdSchema,
149
+ revision: z.literal(1),
150
+ taskDefinition: LearningRevisionRefSchema,
151
+ rewardBinding: LearningRevisionRefSchema,
152
+ purpose: z.enum(["supervised_training", "reward_training", "evaluation"]),
153
+ examples: z.array(z.object({ evidence: LearningRevisionRefSchema, decision: LearningRevisionRefSchema, familyKey: ReleaseIdSchema, inputHash: ReleaseHashSchema, split: TaskSplitSchema }).strict()).min(1).max(10_000),
154
+ sealedAt: ReleaseTimestampSchema,
155
+ sealedBy: ReleaseIdSchema,
156
+ }).strict().superRefine((batch, context) => {
157
+ if (new Set(batch.examples.map((entry) => entry.evidence.id)).size !== batch.examples.length)
158
+ context.addIssue({ code: "custom", path: ["examples"], message: "Each evidence identity can occur only once in a batch." });
159
+ const splits = new Map();
160
+ for (const [index, entry] of batch.examples.entries()) {
161
+ if ((batch.purpose === "evaluation") === (entry.split === "train"))
162
+ context.addIssue({ code: "custom", path: ["examples", index, "split"], message: "Training uses train examples; evaluation uses held-out examples." });
163
+ for (const key of [`family:${entry.familyKey}`, `input:${entry.inputHash}`]) {
164
+ if (splits.has(key) && splits.get(key) !== entry.split)
165
+ context.addIssue({ code: "custom", path: ["examples", index], message: "A task family or identical input cannot cross splits." });
166
+ splits.set(key, entry.split);
167
+ }
168
+ }
169
+ });
170
+ export const TaskBatchSchema = TaskBatchContentSchema.safeExtend({ contentHash: ReleaseHashSchema }).strict();
171
+ export const LearningPolicyContentSchema = z.object({
172
+ schemaVersion: z.literal("openpond.learningPolicy.v1"),
173
+ id: ReleaseIdSchema,
174
+ revision: z.number().int().positive(),
175
+ modelProjectId: ReleaseIdSchema,
176
+ executionOwner: z.enum(["local", "hosted"]),
177
+ enabled: z.boolean(),
178
+ sources: z.array(LearningRevisionRefSchema).min(1).max(100),
179
+ taskDefinition: LearningRevisionRefSchema,
180
+ rewardBinding: LearningRevisionRefSchema,
181
+ admission: z.object({ mode: z.enum(["human", "qualified_automatic"]), qualification: ImmutableReleaseRefSchema.nullable(), minimumApprovedExamples: z.number().int().positive().max(100_000) }).strict(),
182
+ trigger: z.discriminatedUnion("kind", [
183
+ z.object({ kind: z.literal("manual") }).strict(),
184
+ z.object({ kind: z.literal("approved_count") }).strict(),
185
+ z.object({ kind: z.literal("schedule"), intervalSeconds: z.number().int().min(60).max(31_536_000) }).strict(),
186
+ z.object({ kind: z.literal("upstream_accepted"), modelProjectId: ReleaseIdSchema }).strict(),
187
+ ]),
188
+ trainingParent: ImmutableReleaseRefSchema,
189
+ teacher: ImmutableReleaseRefSchema.nullable(),
190
+ training: z.object({ method: z.enum(["sft", "dpo", "grpo", "ppo", "sdft", "opd", "opsd", "sdpo"]), recipe: ImmutableReleaseRefSchema, retentionEvaluation: ImmutableReleaseRefSchema, replayBatches: z.array(ImmutableReleaseRefSchema).max(100) }).strict(),
191
+ limits: z.object({ maxIterationSpendUsd: z.number().positive().max(100_000), maxDailySpendUsd: z.number().positive().max(1_000_000), cooldownSeconds: z.number().int().nonnegative().max(31_536_000), maxRetries: z.number().int().nonnegative().max(10), maxBatchExamples: z.number().int().positive().max(10_000), maxBacklogExamples: z.number().int().positive().max(1_000_000) }).strict(),
192
+ automation: z.object({ collect: z.boolean(), train: z.boolean(), accept: z.boolean(), serve: z.boolean() }).strict(),
193
+ acceptance: z.object({ minimumScore: z.number().min(0).max(1), maximumRetentionRegression: z.number().min(0).max(1), requireImprovement: z.boolean(), rollbackVersion: ImmutableReleaseRefSchema.nullable() }).strict(),
194
+ }).strict().superRefine((policy, context) => {
195
+ if (policy.admission.mode === "qualified_automatic" && !policy.admission.qualification)
196
+ context.addIssue({ code: "custom", path: ["admission"], message: "Automatic admission requires qualification evidence." });
197
+ if (policy.automation.serve && !policy.automation.accept)
198
+ context.addIssue({ code: "custom", path: ["automation"], message: "Automatic serving requires configured acceptance." });
199
+ if (policy.automation.accept && !policy.acceptance.rollbackVersion)
200
+ context.addIssue({ code: "custom", path: ["acceptance"], message: "Automatic acceptance requires a rollback version." });
201
+ if (policy.trigger.kind === "upstream_accepted" && policy.trigger.modelProjectId === policy.modelProjectId)
202
+ context.addIssue({ code: "custom", path: ["trigger"], message: "A model cannot trigger itself through upstream acceptance." });
203
+ if (policy.limits.maxBatchExamples < policy.admission.minimumApprovedExamples)
204
+ context.addIssue({ code: "custom", path: ["limits"], message: "The batch limit must permit the minimum approved example count." });
205
+ if (policy.limits.maxDailySpendUsd < policy.limits.maxIterationSpendUsd)
206
+ context.addIssue({ code: "custom", path: ["limits"], message: "Daily spend must permit the iteration budget." });
207
+ });
208
+ export const LearningPolicySchema = LearningPolicyContentSchema.safeExtend({ contentHash: ReleaseHashSchema }).strict();
209
+ export const LearningIterationStatusSchema = z.enum(["waiting_for_data", "waiting_for_review", "ready", "training", "evaluating", "candidate_ready", "accepted", "rejected", "paused", "failed", "cancelling", "cancelled"]);
210
+ export const LearningIterationSchema = z.object({
211
+ schemaVersion: z.literal("openpond.learningIteration.v1"),
212
+ id: ReleaseIdSchema,
213
+ revision: z.number().int().positive(),
214
+ policy: LearningRevisionRefSchema,
215
+ status: LearningIterationStatusSchema,
216
+ triggerIdentity: ReleaseIdSchema,
217
+ batch: LearningRevisionRefSchema.nullable(),
218
+ sourceWatermarks: z.record(ReleaseIdSchema, z.number().int().nonnegative()),
219
+ trainingParent: ImmutableReleaseRefSchema,
220
+ teacher: ImmutableReleaseRefSchema.nullable(),
221
+ upstreamEvent: ImmutableReleaseRefSchema.nullable(),
222
+ trainingJob: ImmutableReleaseRefSchema.nullable(),
223
+ evaluationJob: ImmutableReleaseRefSchema.nullable(),
224
+ candidateVersion: ImmutableReleaseRefSchema.nullable(),
225
+ dispatchId: ReleaseIdSchema,
226
+ retryCount: z.number().int().nonnegative(),
227
+ spendUsd: z.number().nonnegative(),
228
+ failure: z.object({ code: ReleaseIdSchema, message: z.string().max(20_000) }).strict().nullable(),
229
+ createdAt: ReleaseTimestampSchema,
230
+ updatedAt: ReleaseTimestampSchema,
231
+ }).strict();
232
+ export const TaskGradeRunSchema = z.object({
233
+ schemaVersion: z.literal("openpond.taskGradeRun.v1"),
234
+ id: ReleaseIdSchema,
235
+ revision: z.number().int().positive(),
236
+ evidence: LearningRevisionRefSchema,
237
+ binding: LearningRevisionRefSchema,
238
+ target: z.enum(["observed", "proposed_target"]),
239
+ output: LearningJsonObjectSchema,
240
+ status: z.enum(["queued", "running", "completed", "failed", "cancelling", "cancelled"]),
241
+ composition: RewardCompositionSchema.nullable(),
242
+ leaseOwner: ReleaseIdSchema.nullable(),
243
+ leaseExpiresAt: ReleaseTimestampSchema.nullable(),
244
+ attemptCount: z.number().int().nonnegative(),
245
+ timeoutMs: z.number().int().min(100).max(300_000),
246
+ maximumSpendUsd: z.number().nonnegative().max(1_000),
247
+ failure: z.string().max(20_000).nullable(),
248
+ createdAt: ReleaseTimestampSchema,
249
+ updatedAt: ReleaseTimestampSchema,
250
+ }).strict();
251
+ export const LearningResourceSchema = z.union([TaskDefinitionSchema, RewardReleaseSchema, RewardBindingSchema, LearningSourceSchema, TaskEvidenceSchema, TaskFeedbackSchema, TaskAdmissionDecisionSchema, TaskBatchSchema, LearningPolicySchema, LearningIterationSchema, TaskGradeRunSchema]);
252
+ export function learningRef(resource) {
253
+ return LearningRevisionRefSchema.parse({ id: resource.id, revision: resource.revision, contentHash: resource.contentHash });
254
+ }
255
+ export function sameLearningRef(left, right) {
256
+ return left.id === right.id && left.revision === right.revision && left.contentHash === right.contentHash;
257
+ }
258
+ export function sealLearningContent(content) {
259
+ assertBoundedTaskJson(content, 16_777_216);
260
+ return { ...content, contentHash: contentHash(content) };
261
+ }
262
+ export function assertLearningContentHash(resource) {
263
+ const { contentHash: hash, ...content } = resource;
264
+ assertBoundedTaskJson(content, 16_777_216);
265
+ if (contentHash(content) !== hash)
266
+ throw new LearningDomainError("learning_content_hash_mismatch", 422);
267
+ }
@@ -0,0 +1,12 @@
1
+ /** Stable domain failures that hosts may expose without treating rejected work
2
+ * as an infrastructure failure. Unexpected exceptions remain server errors. */
3
+ export class LearningDomainError extends Error {
4
+ code;
5
+ status;
6
+ constructor(code, status = 422, description) {
7
+ super(description ? `${code}: ${description}` : code);
8
+ this.code = code;
9
+ this.status = status;
10
+ this.name = "LearningDomainError";
11
+ }
12
+ }