@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,367 @@
1
+ import { LearningDomainError } from "./errors.js";
2
+ import { contentHash } from "@openpond/harness";
3
+ import { createRewardBinding, createRewardRelease, resolveBoundRewards } from "../rewards.js";
4
+ import { assertBoundedTaskJson, validateTaskValue } from "../task-schema.js";
5
+ import { assertAdmissionDecision, assertGradeIdentity, compileTaskBatch, inspectTaskEvidence, sealTaskBatch, taskFamilyReservations } from "./admission.js";
6
+ import { learningRef, LearningSourceSchema, LearningPolicySchema, sameLearningRef, sealLearningContent, TaskAdmissionDecisionContentSchema, TaskAdmissionDecisionSchema, TaskDefinitionSchema, TaskEvidenceContentSchema, TaskEvidenceSchema, TaskFeedbackSchema, TaskGradeRunSchema } from "./contracts.js";
7
+ import { LearningCommandSchema } from "./operations.js";
8
+ import { LearningConflictError, learningEvidenceId, learningOperationId, requireLearningRelease, requireLearningResource } from "./repository.js";
9
+ import { validateSourceSubmission } from "./admission.js";
10
+ import { LearningTextAssetSchema, verifyLearningTextAsset } from "./assets.js";
11
+ export function createLearningService(repository, options = {}) {
12
+ const now = options.now ?? (() => new Date().toISOString());
13
+ async function command(context, raw) {
14
+ assertBoundedTaskJson(raw, 16_777_216);
15
+ const input = LearningCommandSchema.parse(raw);
16
+ authorize(context, input);
17
+ const producer = input.action === "submit_example" ? input.example : input.action === "submit_feedback" ? input.feedback : null;
18
+ const operationId = producer ? contentHash([input.action, producer.sourceId, producer.idempotencyKey]) : learningOperationId(context.actor.id, input.operationId);
19
+ const requestHash = contentHash(producer ? { action: input.action, value: producer } : input);
20
+ return repository.transaction(context.scope, async (transaction) => {
21
+ const previous = await transaction.operation(operationId);
22
+ if (previous) {
23
+ if (previous.requestHash !== requestHash)
24
+ throw new LearningDomainError("learning_idempotency_conflict", 409);
25
+ return { operationId: input.operationId, resources: await Promise.all(previous.resources.map((pointer) => requireLearningResource(transaction, pointer.kind, pointer.id, pointer.revision))) };
26
+ }
27
+ let pointers;
28
+ switch (input.action) {
29
+ case "publish":
30
+ pointers = [await publish(transaction, input)];
31
+ break;
32
+ case "publish_resources": {
33
+ pointers = [];
34
+ for (const resource of input.resources)
35
+ pointers.push(await publish(transaction, { ...resource, action: "publish", operationId: input.operationId }));
36
+ break;
37
+ }
38
+ case "submit_example":
39
+ pointers = [await submit(transaction, input)];
40
+ break;
41
+ case "submit_feedback":
42
+ pointers = [await feedback(transaction, input)];
43
+ break;
44
+ case "apply_correction":
45
+ pointers = await correct(transaction, input, context.actor.id);
46
+ break;
47
+ case "resolve_feedback":
48
+ pointers = [await resolveFeedback(transaction, input, context.actor.id)];
49
+ break;
50
+ case "queue_grade":
51
+ pointers = [await queueGrade(transaction, input, operationId)];
52
+ break;
53
+ case "cancel_grade":
54
+ pointers = [await cancelGrade(transaction, input)];
55
+ break;
56
+ case "review":
57
+ pointers = [await review(transaction, input, context.actor.id)];
58
+ break;
59
+ case "seal_batch":
60
+ pointers = await seal(transaction, input, context.actor.id);
61
+ break;
62
+ }
63
+ await transaction.saveOperation(operationId, { requestHash, resources: pointers });
64
+ return { operationId: input.operationId, resources: await Promise.all(pointers.map((pointer) => requireLearningResource(transaction, pointer.kind, pointer.id, pointer.revision))) };
65
+ });
66
+ }
67
+ async function publish(transaction, input) {
68
+ if (input.content.revision !== input.expectedRevision + 1)
69
+ throw new LearningDomainError("learning_publication_revision_invalid", 422);
70
+ let resource;
71
+ switch (input.kind) {
72
+ case "asset": {
73
+ resource = LearningTextAssetSchema.parse(sealLearningContent(input.content));
74
+ const existing = await transaction.get("asset", resource.id, 1);
75
+ if (existing?.contentHash === resource.contentHash)
76
+ return pointer("asset", existing);
77
+ break;
78
+ }
79
+ case "reward": {
80
+ resource = createRewardRelease(input.content);
81
+ const implementation = input.content.implementation;
82
+ const references = [
83
+ ...input.content.assets,
84
+ ...("verifierRef" in implementation ? [implementation.verifierRef] : []),
85
+ ...("rubricRef" in implementation ? [implementation.rubricRef] : []),
86
+ ...("inputContract" in implementation ? [implementation.inputContract] : []),
87
+ ];
88
+ for (const reference of references) {
89
+ const asset = await requireLearningResource(transaction, "asset", reference.id, 1);
90
+ verifyLearningTextAsset(asset, reference);
91
+ if (reference.visibility === "policy")
92
+ throw new LearningDomainError("reward_asset_visibility_invalid", 422, "Reward source and rubrics must be private to the evaluator.");
93
+ }
94
+ break;
95
+ }
96
+ case "binding": {
97
+ if (input.content.recipeRef)
98
+ await requireLearningRelease(transaction, "binding", input.content.recipeRef);
99
+ const rewards = await Promise.all(input.content.sources.map((source) => requireLearningRelease(transaction, "reward", source.reward)));
100
+ resource = createRewardBinding(input.content, rewards);
101
+ break;
102
+ }
103
+ case "definition": {
104
+ await requireLearningRelease(transaction, "binding", input.content.rewardBinding);
105
+ resource = TaskDefinitionSchema.parse(sealLearningContent(input.content));
106
+ break;
107
+ }
108
+ case "source": {
109
+ await requireLearningRelease(transaction, "definition", input.content.taskDefinition);
110
+ resource = LearningSourceSchema.parse(sealLearningContent(input.content));
111
+ break;
112
+ }
113
+ case "policy": {
114
+ const definition = await requireLearningRelease(transaction, "definition", input.content.taskDefinition);
115
+ if (!sameLearningRef(definition.rewardBinding, input.content.rewardBinding))
116
+ throw new LearningDomainError("learning_policy_binding_mismatch", 422);
117
+ for (const source of input.content.sources) {
118
+ const resource = await requireLearningRelease(transaction, "source", source);
119
+ if (!sameLearningRef(resource.taskDefinition, input.content.taskDefinition))
120
+ throw new LearningDomainError("learning_policy_source_definition_mismatch", 422);
121
+ }
122
+ resource = LearningPolicySchema.parse(sealLearningContent(input.content));
123
+ break;
124
+ }
125
+ }
126
+ await transaction.put(input.kind, resource, input.expectedRevision);
127
+ return pointer(input.kind, resource);
128
+ }
129
+ async function submit(transaction, input) {
130
+ const source = await requireLearningResource(transaction, "source", input.example.sourceId);
131
+ const submission = validateSourceSubmission(source, input.example);
132
+ await requireLearningRelease(transaction, "definition", submission.taskDefinition);
133
+ const id = learningEvidenceId(submission.sourceId, submission.exampleId, submission.attemptId);
134
+ const existing = await transaction.get("evidence", id, 1);
135
+ if (existing) {
136
+ const { idempotencyKey: _oldKey, ...oldPayload } = existing.submission;
137
+ const { idempotencyKey: _newKey, ...newPayload } = submission;
138
+ if (contentHash(oldPayload) !== contentHash(newPayload))
139
+ throw new LearningDomainError("task_evidence_identity_conflict", 409);
140
+ return pointer("evidence", existing);
141
+ }
142
+ const evidence = TaskEvidenceSchema.parse(sealLearningContent(TaskEvidenceContentSchema.parse({
143
+ schemaVersion: "openpond.taskEvidence.v1", id, revision: 1, source: learningRef(source), submission,
144
+ supersedes: null, correctionFeedbackId: null, receivedAt: now(),
145
+ })));
146
+ await transaction.put("evidence", evidence, 0, { parentId: source.id });
147
+ let afterId;
148
+ do {
149
+ const page = await transaction.list("feedback", { parentId: evidence.id, status: "pending_example", limit: 100, ...(afterId ? { afterId } : {}) });
150
+ for (const feedback of page.items) {
151
+ const updated = TaskFeedbackSchema.parse({ ...feedback, revision: feedback.revision + 1, status: "pending_review", evidence: learningRef(evidence) });
152
+ await transaction.put("feedback", updated, feedback.revision, { parentId: evidence.id, status: updated.status });
153
+ }
154
+ afterId = page.nextCursor ?? undefined;
155
+ } while (afterId);
156
+ return pointer("evidence", evidence);
157
+ }
158
+ async function feedback(transaction, input) {
159
+ const source = await requireLearningResource(transaction, "source", input.feedback.sourceId);
160
+ if (!source.enabled)
161
+ throw new LearningDomainError("learning_source_disabled", 409);
162
+ const evidenceId = learningEvidenceId(source.id, input.feedback.exampleId, input.feedback.attemptId);
163
+ const evidence = await transaction.get("evidence", evidenceId);
164
+ const record = TaskFeedbackSchema.parse({
165
+ schemaVersion: "openpond.taskFeedbackRecord.v1", id: `feedback-${contentHash([source.id, input.feedback.idempotencyKey])}`,
166
+ submission: input.feedback, status: evidence ? "pending_review" : "pending_example",
167
+ evidence: evidence ? learningRef(evidence) : null, createdAt: now(), revision: 1,
168
+ });
169
+ await transaction.put("feedback", record, 0, { parentId: evidenceId, status: record.status });
170
+ return pointer("feedback", record);
171
+ }
172
+ async function correct(transaction, input, actorId) {
173
+ const feedback = await requireLearningResource(transaction, "feedback", input.feedbackId);
174
+ if (["applied", "rejected", "superseded"].includes(feedback.status))
175
+ throw new LearningDomainError("task_feedback_already_resolved", 409);
176
+ const evidence = await currentEvidence(transaction, input.evidence);
177
+ const expectedId = learningEvidenceId(feedback.submission.sourceId, feedback.submission.exampleId, feedback.submission.attemptId);
178
+ if (expectedId !== evidence.id || (feedback.submission.expectedEvidenceHash !== null && feedback.submission.expectedEvidenceHash !== evidence.contentHash))
179
+ throw new LearningDomainError("task_feedback_evidence_mismatch", 409);
180
+ const definition = await requireLearningRelease(transaction, "definition", evidence.submission.taskDefinition);
181
+ const submission = { ...evidence.submission };
182
+ const { kind, value } = feedback.submission;
183
+ if (kind === "ground_truth_correction") {
184
+ if (!validateTaskValue(definition.outputSchema, value).valid)
185
+ throw new LearningDomainError("task_corrected_expected_schema_invalid", 422);
186
+ submission.expected = value;
187
+ }
188
+ else if (kind === "input_correction") {
189
+ if (!validateTaskValue(definition.inputSchema, value).valid)
190
+ throw new LearningDomainError("task_corrected_input_schema_invalid", 422);
191
+ submission.input = value;
192
+ }
193
+ else if (kind === "family_resolution") {
194
+ if (typeof value.familyKey !== "string" || !value.familyKey.trim())
195
+ throw new LearningDomainError("task_family_resolution_invalid", 422);
196
+ submission.familyKey = value.familyKey;
197
+ }
198
+ else
199
+ throw new LearningDomainError("task_feedback_requires_target_review", 422);
200
+ const { contentHash: _priorHash, ...priorContent } = evidence;
201
+ const revised = TaskEvidenceSchema.parse(sealLearningContent(TaskEvidenceContentSchema.parse({
202
+ ...priorContent, revision: evidence.revision + 1, submission, supersedes: learningRef(evidence), correctionFeedbackId: feedback.id, receivedAt: now(),
203
+ })));
204
+ const resolved = TaskFeedbackSchema.parse({ ...feedback, revision: feedback.revision + 1, status: "applied", evidence: learningRef(revised), review: { actorId, decision: null, note: "Applied as a corrected evidence revision.", resolvedAt: now() } });
205
+ await transaction.put("evidence", revised, evidence.revision, { parentId: submission.sourceId });
206
+ await transaction.put("feedback", resolved, feedback.revision, { parentId: evidence.id, status: resolved.status });
207
+ return [pointer("evidence", revised), pointer("feedback", resolved)];
208
+ }
209
+ async function resolveFeedback(transaction, input, actorId) {
210
+ const feedback = await requireLearningResource(transaction, "feedback", input.feedbackId);
211
+ if (feedback.revision !== input.expectedRevision)
212
+ throw new LearningConflictError("feedback", feedback.id, input.expectedRevision, feedback.revision);
213
+ if (["applied", "rejected", "superseded"].includes(feedback.status))
214
+ throw new LearningDomainError("task_feedback_already_resolved", 409);
215
+ if (input.disposition === "applied" && !input.decision)
216
+ throw new LearningDomainError("task_feedback_decision_required");
217
+ if (input.decision) {
218
+ const decision = await requireLearningRelease(transaction, "decision", input.decision);
219
+ const current = await requireLearningResource(transaction, "decision", decision.id);
220
+ if (!sameLearningRef(learningRef(current), input.decision))
221
+ throw new LearningDomainError("task_admission_revision_stale", 409);
222
+ const evidence = await currentEvidence(transaction, decision.evidence);
223
+ if (learningEvidenceId(feedback.submission.sourceId, feedback.submission.exampleId, feedback.submission.attemptId) !== evidence.id || (feedback.submission.expectedEvidenceHash !== null && feedback.submission.expectedEvidenceHash !== evidence.contentHash))
224
+ throw new LearningDomainError("task_feedback_evidence_mismatch", 409);
225
+ if (input.disposition === "applied") {
226
+ if (decision.taskAdmissibility === "pending")
227
+ throw new LearningDomainError("task_feedback_decision_pending");
228
+ if (feedback.submission.kind === "target_correction") {
229
+ if (decision.targetApproval !== "approved" || contentHash(decision.approvedTarget) !== contentHash(feedback.submission.value))
230
+ throw new LearningDomainError("task_feedback_target_not_approved");
231
+ }
232
+ else if (feedback.submission.kind !== "outcome")
233
+ throw new LearningDomainError("task_feedback_requires_evidence_correction");
234
+ }
235
+ }
236
+ const resolved = TaskFeedbackSchema.parse({ ...feedback, revision: feedback.revision + 1, status: input.disposition, review: { actorId, decision: input.decision, note: input.note, resolvedAt: now() } });
237
+ await transaction.put("feedback", resolved, feedback.revision, { parentId: learningEvidenceId(feedback.submission.sourceId, feedback.submission.exampleId, feedback.submission.attemptId), status: resolved.status });
238
+ return pointer("feedback", resolved);
239
+ }
240
+ async function queueGrade(transaction, input, operationId) {
241
+ const evidence = await currentEvidence(transaction, input.evidence);
242
+ const definition = await requireLearningRelease(transaction, "definition", evidence.submission.taskDefinition);
243
+ if (!inspectTaskEvidence(evidence, definition).taskReady)
244
+ throw new LearningDomainError("task_evidence_not_ready", 422);
245
+ const output = input.target === "observed" ? evidence.submission.observedOutput : input.proposedTarget;
246
+ if (output === null)
247
+ throw new LearningDomainError("task_grade_output_required", 422);
248
+ if (input.target === "observed" && input.proposedTarget !== null)
249
+ throw new LearningDomainError("observed_grade_cannot_replace_output", 422);
250
+ const grade = TaskGradeRunSchema.parse({
251
+ schemaVersion: "openpond.taskGradeRun.v1", id: `grade-${operationId}`, revision: 1,
252
+ evidence: learningRef(evidence), binding: definition.rewardBinding, target: input.target, output,
253
+ status: "queued", composition: null, leaseOwner: null, leaseExpiresAt: null, attemptCount: 0,
254
+ timeoutMs: input.timeoutMs, maximumSpendUsd: input.maximumSpendUsd, failure: null, createdAt: now(), updatedAt: now(),
255
+ });
256
+ await transaction.put("grade", grade, 0, { parentId: evidence.id, status: grade.status });
257
+ return pointer("grade", grade);
258
+ }
259
+ async function cancelGrade(transaction, input) {
260
+ const grade = await requireLearningResource(transaction, "grade", input.gradeId);
261
+ if (grade.revision !== input.expectedRevision)
262
+ throw new LearningConflictError("grade", grade.id, input.expectedRevision, grade.revision);
263
+ if (["completed", "failed", "cancelled"].includes(grade.status))
264
+ return pointer("grade", grade);
265
+ const updated = TaskGradeRunSchema.parse({ ...grade, revision: grade.revision + 1, status: grade.status === "queued" ? "cancelled" : "cancelling", updatedAt: now() });
266
+ await transaction.put("grade", updated, grade.revision, { parentId: grade.evidence.id, status: updated.status });
267
+ return pointer("grade", updated);
268
+ }
269
+ async function review(transaction, input, actorId) {
270
+ const evidence = await currentEvidence(transaction, input.evidence);
271
+ const definition = await requireLearningRelease(transaction, "definition", evidence.submission.taskDefinition);
272
+ const inspection = inspectTaskEvidence(evidence, definition);
273
+ const id = `decision-${evidence.id}`;
274
+ const previous = await transaction.get("decision", id);
275
+ const observedGrade = await gradeResult(transaction, input.observedGradeId, evidence, "observed");
276
+ const targetGrade = await gradeResult(transaction, input.targetGradeId, evidence, "proposed_target");
277
+ const measured = observedGrade?.training.status === "scored" ? observedGrade.training : observedGrade?.evaluation;
278
+ const decision = TaskAdmissionDecisionSchema.parse(sealLearningContent(TaskAdmissionDecisionContentSchema.parse({
279
+ schemaVersion: "openpond.taskAdmissionDecision.v1", id, revision: input.expectedRevision + 1,
280
+ evidence: learningRef(evidence), supersedes: previous ? learningRef(previous) : null,
281
+ actor: { kind: "human", id: actorId, policy: null }, evidenceValidity: inspection.evidenceValidity,
282
+ taskAdmissibility: input.disposition, observedQuality: measured?.status === "scored" ? measured.passed ? "passed" : "failed" : observedGrade ? "unavailable" : "unscored",
283
+ targetApproval: input.targetApproval, approvedTarget: input.approvedTarget, grade: observedGrade, targetGrade,
284
+ note: input.note, decidedAt: now(),
285
+ })));
286
+ if (input.disposition === "approved")
287
+ assertAdmissionDecision({ decision, evidence, definition, purpose: input.targetApproval === "approved" ? "supervised_training" : "reward_training" });
288
+ await transaction.put("decision", decision, input.expectedRevision, { parentId: evidence.id, status: decision.taskAdmissibility });
289
+ return pointer("decision", decision);
290
+ }
291
+ async function seal(transaction, input, actorId) {
292
+ const definition = await requireLearningRelease(transaction, "definition", input.taskDefinition);
293
+ const binding = await requireLearningRelease(transaction, "binding", definition.rewardBinding);
294
+ const rewards = await Promise.all(binding.sources.map((source) => requireLearningRelease(transaction, "reward", source.reward)));
295
+ resolveBoundRewards(binding, rewards);
296
+ const evidence = await Promise.all(input.evidence.map((ref) => currentEvidence(transaction, ref)));
297
+ const decisions = await Promise.all(input.decisions.map(async (ref) => {
298
+ const decision = await requireLearningRelease(transaction, "decision", ref);
299
+ const current = await requireLearningResource(transaction, "decision", ref.id);
300
+ if (!sameLearningRef(learningRef(current), ref))
301
+ throw new LearningDomainError("task_admission_revision_stale", 409);
302
+ return decision;
303
+ }));
304
+ const priorSplits = [];
305
+ for (const item of evidence) {
306
+ for (const reservation of taskFamilyReservations(item, definition)) {
307
+ const split = await transaction.familySplit(reservation.namespace, reservation.kind, reservation.key);
308
+ if (split !== null)
309
+ priorSplits.push({ ...reservation, split: split });
310
+ }
311
+ }
312
+ const batch = sealTaskBatch({ id: input.batchId, definition, binding, rewards, purpose: input.purpose, evidence, decisions, priorSplits, actorId, now: now() });
313
+ const release = compileTaskBatch({ batch, definition, binding, rewards, evidence, decisions });
314
+ for (const item of evidence) {
315
+ for (const reservation of taskFamilyReservations(item, definition))
316
+ await transaction.reserveFamilySplit(reservation.namespace, reservation.kind, reservation.key, reservation.split);
317
+ }
318
+ await transaction.put("batch", batch, 0, { parentId: definition.id });
319
+ await transaction.put("package", release, 0, { parentId: batch.id });
320
+ return [pointer("batch", batch), pointer("package", release)];
321
+ }
322
+ return {
323
+ command,
324
+ async get(context, kind, id, revision) {
325
+ authorizeRead(context);
326
+ return repository.transaction(context.scope, (transaction) => requireLearningResource(transaction, kind, id, revision));
327
+ },
328
+ async list(context, kind, query = {}) {
329
+ authorizeRead(context);
330
+ const limit = Math.max(1, Math.min(100, Math.trunc(query.limit ?? 50)));
331
+ return repository.transaction(context.scope, (transaction) => transaction.list(kind, { ...query, limit }));
332
+ },
333
+ };
334
+ }
335
+ async function currentEvidence(transaction, ref) {
336
+ const evidence = await requireLearningRelease(transaction, "evidence", ref);
337
+ const current = await requireLearningResource(transaction, "evidence", ref.id);
338
+ if (!sameLearningRef(learningRef(current), ref))
339
+ throw new LearningDomainError("task_evidence_revision_stale", 409);
340
+ return evidence;
341
+ }
342
+ async function gradeResult(transaction, id, evidence, target) {
343
+ if (id === null)
344
+ return null;
345
+ const grade = await requireLearningResource(transaction, "grade", id);
346
+ if (grade.status !== "completed" || !grade.composition || grade.target !== target || !sameLearningRef(grade.evidence, learningRef(evidence)))
347
+ throw new LearningDomainError("task_grade_not_applicable", 422);
348
+ const definition = await requireLearningRelease(transaction, "definition", evidence.submission.taskDefinition);
349
+ assertGradeIdentity(grade.composition, evidence, definition, grade.output);
350
+ return grade.composition;
351
+ }
352
+ function pointer(kind, resource) { return { kind, id: resource.id, revision: resource.revision }; }
353
+ function authorizeRead(context) {
354
+ if (!context.scope.trim() || !context.actor.id.trim() || context.actor.role === "source")
355
+ throw new LearningDomainError("learning_read_not_authorized", 403);
356
+ }
357
+ function authorize(context, input) {
358
+ if (!context.scope.trim() || !context.actor.id.trim())
359
+ throw new LearningDomainError("learning_scope_required", 400);
360
+ if (context.actor.role === "source") {
361
+ const sourceId = input.action === "submit_example" ? input.example.sourceId : input.action === "submit_feedback" ? input.feedback.sourceId : null;
362
+ if (!sourceId || sourceId !== context.actor.sourceId)
363
+ throw new LearningDomainError("learning_source_not_authorized", 403);
364
+ }
365
+ if (["review", "apply_correction", "resolve_feedback", "seal_batch"].includes(input.action) && context.actor.role !== "reviewer")
366
+ throw new LearningDomainError("learning_review_not_authorized", 403);
367
+ }
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import { ReleaseIdSchema } from "@openpond/harness";
3
+ import { LearningCommandSchema } from "./operations.js";
4
+ import { LearningResourceKindSchema, learningResourceSchemas } from "./repository.js";
5
+ import { assertBoundedTaskJson } from "../task-schema.js";
6
+ import { LearningDomainError } from "./errors.js";
7
+ /** Run before recursive schema parsing, including for non-TypeScript producers. */
8
+ export function assertLearningRequestJson(raw) {
9
+ try {
10
+ assertBoundedTaskJson(raw, 16_777_216);
11
+ }
12
+ catch (error) {
13
+ throw new LearningDomainError("learning_json_invalid", 400, error instanceof Error ? error.message : "Expected bounded JSON.");
14
+ }
15
+ }
16
+ /** Scope selection is never authorization; each host resolves its authenticated owner. */
17
+ export const LearningCommandRequestSchema = z.object({ scope: ReleaseIdSchema, command: LearningCommandSchema }).strict();
18
+ export const LearningReadRequestSchema = z.discriminatedUnion("action", [
19
+ z.object({ action: z.literal("get"), scope: ReleaseIdSchema, kind: LearningResourceKindSchema, id: ReleaseIdSchema, revision: z.number().int().positive().optional() }).strict(),
20
+ z.object({ action: z.literal("list"), scope: ReleaseIdSchema, kind: LearningResourceKindSchema, parentId: ReleaseIdSchema.optional(), status: z.string().min(1).max(200).optional(), afterId: ReleaseIdSchema.optional(), limit: z.number().int().min(1).max(100).default(50) }).strict(),
21
+ ]);
22
+ export const LearningOperationResultSchema = z.object({
23
+ operationId: ReleaseIdSchema,
24
+ resources: z.array(z.union(Object.values(learningResourceSchemas))).max(100),
25
+ }).strict();
@@ -13,6 +13,11 @@ export const PreferenceReviewerKindSchema = z.enum([
13
13
  "human",
14
14
  "model",
15
15
  "deterministic",
16
+ "fixture",
17
+ ]);
18
+ export const PreferenceRewardScopeSchema = z.enum([
19
+ "production",
20
+ "synthetic_smoke",
16
21
  ]);
17
22
  export const PreferenceRendererSchema = z.enum([
18
23
  "markdown",
@@ -111,7 +116,7 @@ const ComparisonCandidateSchema = z.object({
111
116
  attemptRef: ImmutableReleaseRefSchema,
112
117
  runManifestRef: ImmutableReleaseRefSchema,
113
118
  artifactManifestRef: ImmutableReleaseRefSchema,
114
- visibleArtifactIds: z.array(ReleaseIdSchema).min(1).max(100_000),
119
+ visibleArtifactIds: z.array(ReleaseIdSchema).max(100_000),
115
120
  }).strict().superRefine((candidate, context) => {
116
121
  if (new Set(candidate.visibleArtifactIds).size !== candidate.visibleArtifactIds.length) {
117
122
  context.addIssue({
@@ -158,14 +163,24 @@ export const ComparisonAssignmentSchema = ComparisonAssignmentBaseSchema
158
163
  }
159
164
  });
160
165
  const PreferenceOrderSchema = z.array(z.array(ReleaseIdSchema).min(1).max(4)).max(4);
166
+ const PreferenceReviewerSchema = z.discriminatedUnion("kind", [
167
+ z.object({
168
+ kind: z.enum(["human", "model", "deterministic"]),
169
+ releaseRef: ImmutableReleaseRefSchema,
170
+ }).strict(),
171
+ z.object({
172
+ kind: z.literal("fixture"),
173
+ releaseRef: ImmutableReleaseRefSchema,
174
+ fixtureRelease: ImmutableReleaseRefSchema,
175
+ labelSource: z.literal("synthetic"),
176
+ qualificationEligibility: z.literal("smoke_only"),
177
+ }).strict(),
178
+ ]);
161
179
  const PreferenceReceiptBaseSchema = z.object({
162
180
  schemaVersion: z.literal("openpond.preferenceReceipt.v1"),
163
181
  id: ReleaseIdSchema,
164
182
  assignmentRef: ImmutableReleaseRefSchema,
165
- reviewer: z.object({
166
- kind: PreferenceReviewerKindSchema,
167
- releaseRef: ImmutableReleaseRefSchema,
168
- }).strict(),
183
+ reviewer: PreferenceReviewerSchema,
169
184
  order: PreferenceOrderSchema,
170
185
  rejectAll: z.boolean(),
171
186
  criterionScores: z.record(ReleaseIdSchema, z.record(ReleaseIdSchema, z.number().finite().min(0).max(1))),
@@ -244,7 +259,8 @@ export function createComparisonAssignment(input) {
244
259
  if (input.candidates.length !== release.candidateCount) {
245
260
  throw new Error("Comparison assignment candidate count does not match the immutable comparison release.");
246
261
  }
247
- const normalized = input.candidates.map((candidate) => validateComparisonCandidate(candidate, taskset));
262
+ const requiresVisibleArtifact = release.presentation.parts.some((part) => part.source === "artifact");
263
+ const normalized = input.candidates.map((candidate) => validateComparisonCandidate(candidate, taskset, requiresVisibleArtifact));
248
264
  const first = normalized[0];
249
265
  for (const candidate of normalized.slice(1)) {
250
266
  if (candidate.attempt.taskId !== first.attempt.taskId) {
@@ -331,6 +347,45 @@ export function createPreferenceReceipt(input) {
331
347
  validateCriterionScores(content.criterionScores, assignment, release);
332
348
  return PreferenceReceiptSchema.parse({ ...content, contentHash: contentHash(content) });
333
349
  }
350
+ export function createSyntheticFixturePreferenceReceipt(input) {
351
+ const candidateIds = input.assignment.candidates.map((candidate) => candidate.attemptRef.id);
352
+ const ratingIds = Object.keys(input.ratings);
353
+ if (ratingIds.length !== candidateIds.length
354
+ || candidateIds.some((candidateId) => !input.ratings[candidateId])
355
+ || ratingIds.some((candidateId) => !candidateIds.includes(candidateId))) {
356
+ throw new Error("Synthetic fixture ratings must label every assignment candidate exactly once.");
357
+ }
358
+ const orderedRatings = ["love", "like", "reject"];
359
+ const rejectAll = candidateIds.every((candidateId) => input.ratings[candidateId] === "reject");
360
+ const order = rejectAll ? [] : orderedRatings
361
+ .map((rating) => candidateIds.filter((candidateId) => input.ratings[candidateId] === rating))
362
+ .filter((bucket) => bucket.length > 0);
363
+ const score = { love: 1, like: 0.5, reject: 0 };
364
+ return createPreferenceReceipt({
365
+ id: input.id,
366
+ assignment: input.assignment,
367
+ comparisonRelease: input.comparisonRelease,
368
+ reviewer: {
369
+ kind: "fixture",
370
+ releaseRef: input.labelerRelease,
371
+ fixtureRelease: input.fixtureRelease,
372
+ labelSource: "synthetic",
373
+ qualificationEligibility: "smoke_only",
374
+ },
375
+ order,
376
+ rejectAll,
377
+ criterionScores: Object.fromEntries(candidateIds.map((candidateId) => [
378
+ candidateId,
379
+ Object.fromEntries(input.comparisonRelease.criteria.map((criterion) => [
380
+ criterion.id,
381
+ score[input.ratings[candidateId]],
382
+ ])),
383
+ ])),
384
+ startedAt: input.startedAt,
385
+ completedAt: input.completedAt,
386
+ metadata: input.metadata ?? {},
387
+ });
388
+ }
334
389
  export function verifyPreferenceReceipt(value) {
335
390
  return verifyHashed(value, PreferenceReceiptContentSchema, PreferenceReceiptSchema);
336
391
  }
@@ -436,12 +491,16 @@ export function createPreferenceRewardComponents(input) {
436
491
  ? input.result
437
492
  : null;
438
493
  const automatedReceipt = modelReceipt?.reviewer.kind === "model";
439
- const calibrated = !automatedReceipt || isCalibratedAutomatedReviewer(modelReceipt, release, input.calibrationReport ?? null);
494
+ const fixtureReceipt = modelReceipt?.reviewer.kind === "fixture";
495
+ const rewardScope = PreferenceRewardScopeSchema.parse(input.rewardScope ?? "production");
496
+ const admittedReviewer = fixtureReceipt
497
+ ? rewardScope === "synthetic_smoke"
498
+ : !automatedReceipt || isCalibratedAutomatedReviewer(modelReceipt, release, input.calibrationReport ?? null);
440
499
  return Object.fromEntries(assignment.candidates.map((candidate) => {
441
500
  const candidateEligibility = eligible.get(candidate.attemptRef.id);
442
501
  const canScore = candidateEligibility?.eligible ?? true;
443
502
  const score = scores[candidate.attemptRef.id];
444
- const component = canScore && calibrated
503
+ const component = canScore && admittedReviewer
445
504
  ? RewardComponentReceiptSchema.parse({
446
505
  verifierId: release.rewardProjection.verifierId,
447
506
  verifierVersion: release.rewardProjection.verifierVersion,
@@ -475,7 +534,11 @@ export function createPreferenceRewardComponents(input) {
475
534
  rewardEligible: false,
476
535
  rewardContribution: null,
477
536
  failureOwner: candidateEligibility?.failureOwner ?? "verifier",
478
- feedback: [canScore ? "Automated preference reviewer is uncalibrated or inconsistent." : "Candidate is not eligible for comparative preference scoring."],
537
+ feedback: [canScore
538
+ ? fixtureReceipt
539
+ ? "Synthetic fixture preference evidence is admitted only to synthetic-smoke reward scope."
540
+ : "Automated preference reviewer is uncalibrated or inconsistent."
541
+ : "Candidate is not eligible for comparative preference scoring."],
479
542
  visibleEvidenceRefs: [],
480
543
  privilegedEvidenceRefs: [],
481
544
  metadata: {
@@ -487,7 +550,7 @@ export function createPreferenceRewardComponents(input) {
487
550
  return [candidate.attemptRef.id, component];
488
551
  }));
489
552
  }
490
- function validateComparisonCandidate(input, taskset) {
553
+ function validateComparisonCandidate(input, taskset, requiresVisibleArtifact) {
491
554
  const attempt = AttemptReceiptSchema.parse(input.attempt);
492
555
  if (!verifyAttemptReceipt(attempt))
493
556
  throw new Error("Comparison candidate Attempt Receipt has an invalid content hash.");
@@ -514,7 +577,10 @@ function validateComparisonCandidate(input, taskset) {
514
577
  const available = new Set(artifactManifest.entries
515
578
  .filter((entry) => entry.status === "collected" && entry.artifact !== null)
516
579
  .map((entry) => entry.artifact.id));
517
- if (!visibleArtifactIds.length || visibleArtifactIds.some((id) => !available.has(id))) {
580
+ if (requiresVisibleArtifact && !visibleArtifactIds.length) {
581
+ throw new Error("Comparison presentation requires each candidate to expose a reviewable artifact.");
582
+ }
583
+ if (visibleArtifactIds.some((id) => !available.has(id))) {
518
584
  throw new Error("Comparison candidate exposes an artifact that is missing, uncollected, or unreviewable.");
519
585
  }
520
586
  if (visible.size !== visibleArtifactIds.length)