@openpond/evals 0.5.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 (118) hide show
  1. package/CONTRACT.md +13 -1
  2. package/LEARNING.md +106 -0
  3. package/README.md +28 -2
  4. package/conformance/telemetry/v1/invalid-batch.json +17 -0
  5. package/conformance/telemetry/v1/valid-batch.json +58 -0
  6. package/dist/graders.js +26 -5
  7. package/dist/index.js +8 -0
  8. package/dist/javascript-verifier-contract.js +7 -0
  9. package/dist/javascript-verifier-node.js +56 -0
  10. package/dist/javascript-verifier-worker-source.js +1 -0
  11. package/dist/javascript-verifier-worker.js +6 -0
  12. package/dist/javascript-verifier.js +64 -0
  13. package/dist/learned-preference.js +334 -0
  14. package/dist/learning/admission.js +209 -0
  15. package/dist/learning/assets.js +37 -0
  16. package/dist/learning/contracts.js +267 -0
  17. package/dist/learning/errors.js +12 -0
  18. package/dist/learning/grade-worker.js +99 -0
  19. package/dist/learning/index.js +9 -0
  20. package/dist/learning/operations.js +33 -0
  21. package/dist/learning/repository.js +48 -0
  22. package/dist/learning/service.js +367 -0
  23. package/dist/learning/transport.js +25 -0
  24. package/dist/preferences.js +77 -11
  25. package/dist/rewards.js +230 -0
  26. package/dist/task-schema.js +142 -0
  27. package/dist/tasksets.js +3 -0
  28. package/dist/telemetry/index.js +4 -0
  29. package/dist/telemetry-analysis.js +183 -0
  30. package/dist/telemetry-bundle.js +60 -0
  31. package/dist/telemetry-catalog.js +50 -0
  32. package/dist/telemetry.js +112 -0
  33. package/dist/types/builtin-benchmarks/harness-refiner.d.ts +7 -0
  34. package/dist/types/builtin-benchmarks/harness-refiner.d.ts.map +1 -1
  35. package/dist/types/conformance.d.ts +14 -0
  36. package/dist/types/conformance.d.ts.map +1 -1
  37. package/dist/types/execution-contracts.d.ts +14 -0
  38. package/dist/types/execution-contracts.d.ts.map +1 -1
  39. package/dist/types/graders.d.ts.map +1 -1
  40. package/dist/types/index.d.ts +8 -0
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/dist/types/javascript-verifier-contract.d.ts +9 -0
  43. package/dist/types/javascript-verifier-contract.d.ts.map +1 -0
  44. package/dist/types/javascript-verifier-node.d.ts +5 -0
  45. package/dist/types/javascript-verifier-node.d.ts.map +1 -0
  46. package/dist/types/javascript-verifier-worker-source.d.ts +2 -0
  47. package/dist/types/javascript-verifier-worker.d.ts +2 -0
  48. package/dist/types/javascript-verifier-worker.d.ts.map +1 -0
  49. package/dist/types/javascript-verifier.d.ts +15 -0
  50. package/dist/types/javascript-verifier.d.ts.map +1 -0
  51. package/dist/types/learned-preference.d.ts +282 -0
  52. package/dist/types/learned-preference.d.ts.map +1 -0
  53. package/dist/types/learning/admission.d.ts +335 -0
  54. package/dist/types/learning/admission.d.ts.map +1 -0
  55. package/dist/types/learning/assets.d.ts +49 -0
  56. package/dist/types/learning/assets.d.ts.map +1 -0
  57. package/dist/types/learning/contracts.d.ts +2149 -0
  58. package/dist/types/learning/contracts.d.ts.map +1 -0
  59. package/dist/types/learning/errors.d.ts +8 -0
  60. package/dist/types/learning/errors.d.ts.map +1 -0
  61. package/dist/types/learning/grade-worker.d.ts +31 -0
  62. package/dist/types/learning/grade-worker.d.ts.map +1 -0
  63. package/dist/types/learning/index.d.ts +10 -0
  64. package/dist/types/learning/index.d.ts.map +1 -0
  65. package/dist/types/learning/operations.d.ts +2061 -0
  66. package/dist/types/learning/operations.d.ts.map +1 -0
  67. package/dist/types/learning/repository.d.ts +1143 -0
  68. package/dist/types/learning/repository.d.ts.map +1 -0
  69. package/dist/types/learning/service.d.ts +22 -0
  70. package/dist/types/learning/service.d.ts.map +1 -0
  71. package/dist/types/learning/transport.d.ts +2139 -0
  72. package/dist/types/learning/transport.d.ts.map +1 -0
  73. package/dist/types/preferences.d.ts +55 -7
  74. package/dist/types/preferences.d.ts.map +1 -1
  75. package/dist/types/review-conformance.d.ts +5 -5
  76. package/dist/types/rewards.d.ts +611 -0
  77. package/dist/types/rewards.d.ts.map +1 -0
  78. package/dist/types/task-schema.d.ts +18 -0
  79. package/dist/types/task-schema.d.ts.map +1 -0
  80. package/dist/types/tasksets.d.ts +28 -0
  81. package/dist/types/tasksets.d.ts.map +1 -1
  82. package/dist/types/telemetry/index.d.ts +5 -0
  83. package/dist/types/telemetry/index.d.ts.map +1 -0
  84. package/dist/types/telemetry-analysis.d.ts +116 -0
  85. package/dist/types/telemetry-analysis.d.ts.map +1 -0
  86. package/dist/types/telemetry-bundle.d.ts +309 -0
  87. package/dist/types/telemetry-bundle.d.ts.map +1 -0
  88. package/dist/types/telemetry-catalog.d.ts +269 -0
  89. package/dist/types/telemetry-catalog.d.ts.map +1 -0
  90. package/dist/types/telemetry.d.ts +266 -0
  91. package/dist/types/telemetry.d.ts.map +1 -0
  92. package/package.json +40 -3
  93. package/schemas/learning/v1/asset.schema.json +89 -0
  94. package/schemas/learning/v1/batch.schema.json +132 -0
  95. package/schemas/learning/v1/binding.schema.json +207 -0
  96. package/schemas/learning/v1/command-request.schema.json +2414 -0
  97. package/schemas/learning/v1/decision.schema.json +440 -0
  98. package/schemas/learning/v1/definition.schema.json +397 -0
  99. package/schemas/learning/v1/evidence.schema.json +317 -0
  100. package/schemas/learning/v1/example-submission.schema.json +253 -0
  101. package/schemas/learning/v1/feedback-submission.schema.json +114 -0
  102. package/schemas/learning/v1/feedback.schema.json +233 -0
  103. package/schemas/learning/v1/grade.schema.json +412 -0
  104. package/schemas/learning/v1/iteration.schema.json +236 -0
  105. package/schemas/learning/v1/package.schema.json +776 -0
  106. package/schemas/learning/v1/policy.schema.json +373 -0
  107. package/schemas/learning/v1/read-request.schema.json +101 -0
  108. package/schemas/learning/v1/reward.schema.json +302 -0
  109. package/schemas/learning/v1/source.schema.json +198 -0
  110. package/schemas/telemetry/v1/evidence-completeness.schema.json +82 -0
  111. package/schemas/telemetry/v1/evidence-reference.schema.json +41 -0
  112. package/schemas/telemetry/v1/metric-definition.schema.json +96 -0
  113. package/schemas/telemetry/v1/metric-observation.schema.json +182 -0
  114. package/schemas/telemetry/v1/run-metric-summary.schema.json +98 -0
  115. package/schemas/telemetry/v1/run-telemetry-batch.schema.json +405 -0
  116. package/schemas/telemetry/v1/run-telemetry-event.schema.json +201 -0
  117. package/schemas/telemetry/v1/telemetry-cohort.schema.json +100 -0
  118. package/schemas/telemetry/v1/telemetry-export-bundle.schema.json +655 -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,334 @@
1
+ import { z } from "zod";
2
+ import { ImmutableReleaseRefSchema, MetadataSchema, ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema, contentHash, } from "@openpond/harness";
3
+ import { verifyComparisonAssignment, verifyPreferenceReceipt, } from "./preferences.js";
4
+ const PreferenceDatasetPartitionSchema = z.enum([
5
+ "reward_train",
6
+ "reward_validation",
7
+ "reward_qualification",
8
+ ]);
9
+ export const PreferenceEvidenceAuthoritySchema = z.enum([
10
+ "human",
11
+ "synthetic_fixture",
12
+ ]);
13
+ export const RewardModelQualificationKindSchema = z.enum([
14
+ "synthetic_smoke",
15
+ "human_heldout",
16
+ ]);
17
+ const PreferenceDatasetGroupSchema = z.object({
18
+ id: ReleaseIdSchema,
19
+ assignmentRef: ImmutableReleaseRefSchema,
20
+ scenarioRef: ImmutableReleaseRefSchema,
21
+ scenarioSplit: z.enum(["train", "validation"]),
22
+ preferenceResultRef: ImmutableReleaseRefSchema,
23
+ receiptRefs: z.array(ImmutableReleaseRefSchema).min(1).max(100),
24
+ attemptRefs: z.array(ImmutableReleaseRefSchema).min(2).max(4),
25
+ artifactManifestRefs: z.array(ImmutableReleaseRefSchema).min(2).max(4),
26
+ orderedBuckets: z.array(z.array(ReleaseIdSchema).min(1).max(4)).max(4),
27
+ rejectAll: z.boolean(),
28
+ partition: PreferenceDatasetPartitionSchema,
29
+ metadata: MetadataSchema,
30
+ }).strict().superRefine((group, context) => {
31
+ const attemptIds = group.attemptRefs.map((attempt) => attempt.id);
32
+ if (new Set(attemptIds).size !== attemptIds.length) {
33
+ context.addIssue({
34
+ code: "custom",
35
+ path: ["attemptRefs"],
36
+ message: "Preference dataset group Attempt refs must be unique.",
37
+ });
38
+ }
39
+ if (group.artifactManifestRefs.length !== group.attemptRefs.length) {
40
+ context.addIssue({
41
+ code: "custom",
42
+ path: ["artifactManifestRefs"],
43
+ message: "Every preference dataset Attempt requires one Artifact Manifest ref.",
44
+ });
45
+ }
46
+ const bucketIds = group.orderedBuckets.flat();
47
+ if (group.rejectAll && bucketIds.length > 0) {
48
+ context.addIssue({
49
+ code: "custom",
50
+ path: ["orderedBuckets"],
51
+ message: "A reject-all preference dataset group cannot contain ordered buckets.",
52
+ });
53
+ }
54
+ if (!group.rejectAll
55
+ && (bucketIds.length !== attemptIds.length
56
+ || new Set(bucketIds).size !== bucketIds.length
57
+ || bucketIds.some((attemptId) => !attemptIds.includes(attemptId)))) {
58
+ context.addIssue({
59
+ code: "custom",
60
+ path: ["orderedBuckets"],
61
+ message: "Ordered buckets must contain every group Attempt exactly once.",
62
+ });
63
+ }
64
+ });
65
+ const PreferenceDatasetPairSchema = z.object({
66
+ groupId: ReleaseIdSchema,
67
+ preferredAttemptRef: ImmutableReleaseRefSchema,
68
+ dispreferredAttemptRef: ImmutableReleaseRefSchema,
69
+ relation: z.enum(["preferred", "tie"]),
70
+ }).strict().superRefine((pair, context) => {
71
+ if (pair.preferredAttemptRef.id === pair.dispreferredAttemptRef.id
72
+ && pair.preferredAttemptRef.contentHash === pair.dispreferredAttemptRef.contentHash) {
73
+ context.addIssue({
74
+ code: "custom",
75
+ message: "A derived preference pair must reference two different Attempts.",
76
+ });
77
+ }
78
+ });
79
+ export const PreferenceDatasetReleaseContentSchema = z.object({
80
+ schemaVersion: z.literal("openpond.preferenceDatasetRelease.v1"),
81
+ id: ReleaseIdSchema,
82
+ revision: z.number().int().positive(),
83
+ tasksetRelease: ImmutableReleaseRefSchema,
84
+ comparisonRelease: ImmutableReleaseRefSchema,
85
+ authority: PreferenceEvidenceAuthoritySchema,
86
+ qualificationEligibility: z.enum(["smoke_only", "human_heldout"]),
87
+ fixtureRelease: ImmutableReleaseRefSchema.nullable(),
88
+ groups: z.array(PreferenceDatasetGroupSchema).min(1).max(100_000),
89
+ derivedPairs: z.array(PreferenceDatasetPairSchema).max(1_000_000),
90
+ createdAt: ReleaseTimestampSchema,
91
+ metadata: MetadataSchema,
92
+ }).strict().superRefine((release, context) => {
93
+ if (release.authority === "synthetic_fixture"
94
+ && (release.qualificationEligibility !== "smoke_only"
95
+ || release.fixtureRelease === null)) {
96
+ context.addIssue({
97
+ code: "custom",
98
+ path: ["qualificationEligibility"],
99
+ message: "Synthetic preference datasets require a fixture release and are smoke-only.",
100
+ });
101
+ }
102
+ if (release.authority === "human"
103
+ && (release.qualificationEligibility !== "human_heldout"
104
+ || release.fixtureRelease !== null)) {
105
+ context.addIssue({
106
+ code: "custom",
107
+ path: ["authority"],
108
+ message: "Human preference datasets must be human-heldout eligible and cannot bind a fixture release.",
109
+ });
110
+ }
111
+ const groupIds = release.groups.map((group) => group.id);
112
+ if (new Set(groupIds).size !== groupIds.length) {
113
+ context.addIssue({
114
+ code: "custom",
115
+ path: ["groups"],
116
+ message: "Preference dataset group IDs must be unique.",
117
+ });
118
+ }
119
+ const groups = new Map(release.groups.map((group) => [group.id, group]));
120
+ for (const [index, pair] of release.derivedPairs.entries()) {
121
+ const group = groups.get(pair.groupId);
122
+ if (!group) {
123
+ context.addIssue({
124
+ code: "custom",
125
+ path: ["derivedPairs", index, "groupId"],
126
+ message: "Derived preference pairs must reference a group in the same release.",
127
+ });
128
+ continue;
129
+ }
130
+ const attemptIds = new Set(group.attemptRefs.map((attempt) => attempt.id));
131
+ if (!attemptIds.has(pair.preferredAttemptRef.id)
132
+ || !attemptIds.has(pair.dispreferredAttemptRef.id)) {
133
+ context.addIssue({
134
+ code: "custom",
135
+ path: ["derivedPairs", index],
136
+ message: "Derived preference pairs must reference Attempts in their source group.",
137
+ });
138
+ }
139
+ }
140
+ });
141
+ export const PreferenceDatasetReleaseSchema = PreferenceDatasetReleaseContentSchema
142
+ .extend({ contentHash: ReleaseHashSchema })
143
+ .strict();
144
+ const RewardModelQualificationMetricsSchema = z.object({
145
+ sampleCount: z.number().int().positive(),
146
+ finiteScoreRate: z.number().min(0).max(1),
147
+ scoreVariance: z.number().nonnegative(),
148
+ checkpointReloadPassed: z.boolean(),
149
+ processorCompatibilityPassed: z.boolean(),
150
+ invalidAttemptExclusionPassed: z.boolean(),
151
+ orderedPairAccuracy: z.number().min(0).max(1).nullable(),
152
+ bucketAccuracy: z.number().min(0).max(1).nullable(),
153
+ tieAgreement: z.number().min(0).max(1).nullable(),
154
+ }).strict();
155
+ export const RewardModelQualificationReportContentSchema = z.object({
156
+ schemaVersion: z.literal("openpond.rewardModelQualificationReport.v1"),
157
+ id: ReleaseIdSchema,
158
+ kind: RewardModelQualificationKindSchema,
159
+ rewardModelVersion: ImmutableReleaseRefSchema,
160
+ preferenceDatasetRelease: ImmutableReleaseRefSchema,
161
+ tasksetRelease: ImmutableReleaseRefSchema,
162
+ processorRelease: ImmutableReleaseRefSchema,
163
+ metrics: RewardModelQualificationMetricsSchema,
164
+ passed: z.boolean(),
165
+ productionRewardEligible: z.boolean(),
166
+ createdAt: ReleaseTimestampSchema,
167
+ metadata: MetadataSchema,
168
+ }).strict().superRefine((report, context) => {
169
+ if (report.kind === "synthetic_smoke" && report.productionRewardEligible) {
170
+ context.addIssue({
171
+ code: "custom",
172
+ path: ["productionRewardEligible"],
173
+ message: "Synthetic-smoke Reward Model qualification can never grant production reward eligibility.",
174
+ });
175
+ }
176
+ if (report.kind === "human_heldout"
177
+ && report.productionRewardEligible !== report.passed) {
178
+ context.addIssue({
179
+ code: "custom",
180
+ path: ["productionRewardEligible"],
181
+ message: "Human-heldout reward eligibility must match the frozen qualification outcome.",
182
+ });
183
+ }
184
+ });
185
+ export const RewardModelQualificationReportSchema = RewardModelQualificationReportContentSchema
186
+ .extend({ contentHash: ReleaseHashSchema })
187
+ .strict();
188
+ export function createPreferenceDatasetRelease(input) {
189
+ const content = PreferenceDatasetReleaseContentSchema.parse(input);
190
+ return PreferenceDatasetReleaseSchema.parse({
191
+ ...content,
192
+ contentHash: contentHash(content),
193
+ });
194
+ }
195
+ export function materializePreferenceDatasetRelease(input) {
196
+ if (input.groups.length === 0) {
197
+ throw new Error("Preference dataset materialization requires at least one reviewed group.");
198
+ }
199
+ const tasksetRef = releaseRef(input.tasksetRelease);
200
+ const comparisonRef = releaseRef(input.comparisonRelease);
201
+ const fixtureRefs = new Map();
202
+ const groups = input.groups.map(({ assignment, receipt, partition }) => {
203
+ if (!verifyComparisonAssignment(assignment)) {
204
+ throw new Error("Preference dataset assignment failed immutable verification.");
205
+ }
206
+ if (!verifyPreferenceReceipt(receipt)) {
207
+ throw new Error("Preference dataset receipt failed immutable verification.");
208
+ }
209
+ if (!sameReleaseRef(assignment.lineage.tasksetRelease, tasksetRef)) {
210
+ throw new Error("Preference dataset assignment belongs to a different Taskset release.");
211
+ }
212
+ if (!sameReleaseRef(assignment.comparisonRelease, comparisonRef)) {
213
+ throw new Error("Preference dataset assignment belongs to a different comparison release.");
214
+ }
215
+ if (!sameReleaseRef(receipt.assignmentRef, releaseRef(assignment))) {
216
+ throw new Error("Preference dataset receipt belongs to a different assignment.");
217
+ }
218
+ if (input.authority === "synthetic_fixture") {
219
+ if (receipt.reviewer.kind !== "fixture") {
220
+ throw new Error("Synthetic preference datasets accept only fixture receipts.");
221
+ }
222
+ fixtureRefs.set(receipt.reviewer.fixtureRelease.contentHash, receipt.reviewer.fixtureRelease);
223
+ }
224
+ else if (receipt.reviewer.kind !== "human") {
225
+ throw new Error("Human preference datasets accept only human receipts.");
226
+ }
227
+ const scenario = input.tasksetRelease.tasks.find((task) => task.id === assignment.taskRef.id);
228
+ if (!scenario || (scenario.split !== "train" && scenario.split !== "validation")) {
229
+ throw new Error("Preference model datasets require train or validation scenarios.");
230
+ }
231
+ if (partition === "reward_train" && scenario.split !== "train") {
232
+ throw new Error("Reward training groups must use train scenarios.");
233
+ }
234
+ if (partition !== "reward_train" && scenario.split !== "validation") {
235
+ throw new Error("Reward validation and qualification groups must use validation scenarios.");
236
+ }
237
+ return {
238
+ id: `preference-group:${assignment.id}`,
239
+ assignmentRef: releaseRef(assignment),
240
+ scenarioRef: assignment.taskRef,
241
+ scenarioSplit: scenario.split,
242
+ preferenceResultRef: releaseRef(receipt),
243
+ receiptRefs: [releaseRef(receipt)],
244
+ attemptRefs: assignment.candidates.map((candidate) => candidate.attemptRef),
245
+ artifactManifestRefs: assignment.candidates.map((candidate) => candidate.artifactManifestRef),
246
+ orderedBuckets: receipt.order,
247
+ rejectAll: receipt.rejectAll,
248
+ partition,
249
+ metadata: { purpose: assignment.purpose, ...assignment.metadata },
250
+ };
251
+ });
252
+ if (input.authority === "synthetic_fixture" && fixtureRefs.size !== 1) {
253
+ throw new Error("Synthetic preference dataset groups must share one immutable fixture release.");
254
+ }
255
+ const fixtureRelease = input.authority === "synthetic_fixture"
256
+ ? [...fixtureRefs.values()][0]
257
+ : null;
258
+ return createPreferenceDatasetRelease({
259
+ schemaVersion: "openpond.preferenceDatasetRelease.v1",
260
+ id: input.id,
261
+ revision: input.revision,
262
+ tasksetRelease: tasksetRef,
263
+ comparisonRelease: comparisonRef,
264
+ authority: input.authority,
265
+ qualificationEligibility: input.authority === "synthetic_fixture" ? "smoke_only" : "human_heldout",
266
+ fixtureRelease,
267
+ groups,
268
+ derivedPairs: groups.flatMap(derivePairs),
269
+ createdAt: input.createdAt,
270
+ metadata: input.metadata ?? {},
271
+ });
272
+ }
273
+ export function verifyPreferenceDatasetRelease(value) {
274
+ return verifyHashed(value, PreferenceDatasetReleaseContentSchema, PreferenceDatasetReleaseSchema);
275
+ }
276
+ export function createRewardModelQualificationReport(input) {
277
+ const content = RewardModelQualificationReportContentSchema.parse(input);
278
+ return RewardModelQualificationReportSchema.parse({
279
+ ...content,
280
+ contentHash: contentHash(content),
281
+ });
282
+ }
283
+ export function verifyRewardModelQualificationReport(value) {
284
+ return verifyHashed(value, RewardModelQualificationReportContentSchema, RewardModelQualificationReportSchema);
285
+ }
286
+ function verifyHashed(value, contentSchema, fullSchema) {
287
+ const parsed = fullSchema.safeParse(value);
288
+ if (!parsed.success || !parsed.data)
289
+ return false;
290
+ const { contentHash: actual, ...content } = parsed.data;
291
+ try {
292
+ return contentHash(contentSchema.parse(content)) === actual;
293
+ }
294
+ catch {
295
+ return false;
296
+ }
297
+ }
298
+ function derivePairs(group) {
299
+ if (group.rejectAll)
300
+ return [];
301
+ const attemptById = new Map(group.attemptRefs.map((attempt) => [attempt.id, attempt]));
302
+ const pairs = [];
303
+ for (const [bucketIndex, bucket] of group.orderedBuckets.entries()) {
304
+ for (let left = 0; left < bucket.length; left += 1) {
305
+ for (let right = left + 1; right < bucket.length; right += 1) {
306
+ pairs.push({
307
+ groupId: group.id,
308
+ preferredAttemptRef: attemptById.get(bucket[left]),
309
+ dispreferredAttemptRef: attemptById.get(bucket[right]),
310
+ relation: "tie",
311
+ });
312
+ }
313
+ }
314
+ for (const lowerBucket of group.orderedBuckets.slice(bucketIndex + 1)) {
315
+ for (const preferredId of bucket) {
316
+ for (const dispreferredId of lowerBucket) {
317
+ pairs.push({
318
+ groupId: group.id,
319
+ preferredAttemptRef: attemptById.get(preferredId),
320
+ dispreferredAttemptRef: attemptById.get(dispreferredId),
321
+ relation: "preferred",
322
+ });
323
+ }
324
+ }
325
+ }
326
+ }
327
+ return pairs;
328
+ }
329
+ function releaseRef(value) {
330
+ return { id: value.id, contentHash: value.contentHash };
331
+ }
332
+ function sameReleaseRef(left, right) {
333
+ return left.id === right.id && left.contentHash === right.contentHash;
334
+ }
@@ -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
+ }