@openpond/evals 0.11.16 → 0.11.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connected-evidence/contracts.js +72 -0
- package/dist/connected-evidence/imports.js +71 -0
- package/dist/connected-evidence/index.js +5 -0
- package/dist/connected-evidence/normalize.js +97 -0
- package/dist/connected-evidence/python-correctness.js +73 -0
- package/dist/connected-evidence/sources/claude-code.js +71 -0
- package/dist/connected-evidence/sources/codex.js +52 -0
- package/dist/connected-evidence/sources/history.js +86 -0
- package/dist/connected-evidence/sources/usage.js +15 -0
- package/dist/connected-evidence/usage-summary.js +52 -0
- package/dist/experiments.js +16 -4
- package/dist/graders.js +9 -6
- package/dist/human-review/comparison.js +161 -0
- package/dist/human-review/contracts.js +110 -0
- package/dist/human-review/execution.js +61 -0
- package/dist/human-review/handoff.js +18 -0
- package/dist/human-review/index.js +9 -0
- package/dist/human-review/local-publication.js +18 -0
- package/dist/human-review/repository.js +1 -0
- package/dist/human-review/results.js +18 -0
- package/dist/human-review/service.js +268 -0
- package/dist/human-review/task-proposals.js +39 -0
- package/dist/index.js +1 -0
- package/dist/javascript-verifier-contract.js +2 -2
- package/dist/learning/authoring.js +2 -1
- package/dist/learning/contracts.js +353 -81
- package/dist/learning/index.js +1 -0
- package/dist/learning/reward-authoring.js +5 -2
- package/dist/learning/training-handoff.js +32 -0
- package/dist/metrics-node.js +2 -0
- package/dist/profile-evaluation-target.js +7 -0
- package/dist/profile-evaluations.js +7 -10
- package/dist/profile-external-evaluation.js +35 -0
- package/dist/profile-source-candidate.js +5 -0
- package/dist/rewards.js +5 -3
- package/dist/taskset-run-contract.js +10 -0
- package/dist/tasksets.js +2 -0
- package/dist/types/builtin-benchmarks/harness-refiner.d.ts +75 -1
- package/dist/types/builtin-benchmarks/harness-refiner.d.ts.map +1 -1
- package/dist/types/conformance.d.ts +150 -2
- package/dist/types/conformance.d.ts.map +1 -1
- package/dist/types/connected-evidence/contracts.d.ts +317 -0
- package/dist/types/connected-evidence/contracts.d.ts.map +1 -0
- package/dist/types/connected-evidence/imports.d.ts +19 -0
- package/dist/types/connected-evidence/imports.d.ts.map +1 -0
- package/dist/types/connected-evidence/index.d.ts +6 -0
- package/dist/types/connected-evidence/index.d.ts.map +1 -0
- package/dist/types/connected-evidence/normalize.d.ts +25 -0
- package/dist/types/connected-evidence/normalize.d.ts.map +1 -0
- package/dist/types/connected-evidence/python-correctness.d.ts +196 -0
- package/dist/types/connected-evidence/python-correctness.d.ts.map +1 -0
- package/dist/types/connected-evidence/sources/claude-code.d.ts +74 -0
- package/dist/types/connected-evidence/sources/claude-code.d.ts.map +1 -0
- package/dist/types/connected-evidence/sources/codex.d.ts +74 -0
- package/dist/types/connected-evidence/sources/codex.d.ts.map +1 -0
- package/dist/types/connected-evidence/sources/history.d.ts +146 -0
- package/dist/types/connected-evidence/sources/history.d.ts.map +1 -0
- package/dist/types/connected-evidence/sources/usage.d.ts +3 -0
- package/dist/types/connected-evidence/sources/usage.d.ts.map +1 -0
- package/dist/types/connected-evidence/usage-summary.d.ts +162 -0
- package/dist/types/connected-evidence/usage-summary.d.ts.map +1 -0
- package/dist/types/evidence/conformance.d.ts +28 -28
- package/dist/types/evidence/contracts.d.ts +27 -27
- package/dist/types/execution-contracts.d.ts +159 -5
- package/dist/types/execution-contracts.d.ts.map +1 -1
- package/dist/types/experiments.d.ts +476 -7
- package/dist/types/experiments.d.ts.map +1 -1
- package/dist/types/graders.d.ts +21 -1
- package/dist/types/graders.d.ts.map +1 -1
- package/dist/types/human-review/comparison.d.ts +1959 -0
- package/dist/types/human-review/comparison.d.ts.map +1 -0
- package/dist/types/human-review/contracts.d.ts +2719 -0
- package/dist/types/human-review/contracts.d.ts.map +1 -0
- package/dist/types/human-review/execution.d.ts +298 -0
- package/dist/types/human-review/execution.d.ts.map +1 -0
- package/dist/types/human-review/handoff.d.ts +124 -0
- package/dist/types/human-review/handoff.d.ts.map +1 -0
- package/dist/types/human-review/index.d.ts +10 -0
- package/dist/types/human-review/index.d.ts.map +1 -0
- package/dist/types/human-review/local-publication.d.ts +165 -0
- package/dist/types/human-review/local-publication.d.ts.map +1 -0
- package/dist/types/human-review/repository.d.ts +43 -0
- package/dist/types/human-review/repository.d.ts.map +1 -0
- package/dist/types/human-review/results.d.ts +118 -0
- package/dist/types/human-review/results.d.ts.map +1 -0
- package/dist/types/human-review/service.d.ts +878 -0
- package/dist/types/human-review/service.d.ts.map +1 -0
- package/dist/types/human-review/task-proposals.d.ts +70 -0
- package/dist/types/human-review/task-proposals.d.ts.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/javascript-environment-contract.d.ts +1 -1
- package/dist/types/javascript-verifier-contract.d.ts +1 -1
- package/dist/types/javascript-verifier-contract.d.ts.map +1 -1
- package/dist/types/learned-preference.d.ts +2 -2
- package/dist/types/learning/acceptance-group.d.ts +13 -13
- package/dist/types/learning/acceptance-plan.d.ts +1 -1
- package/dist/types/learning/admission.d.ts +78 -1
- package/dist/types/learning/admission.d.ts.map +1 -1
- package/dist/types/learning/authoring-service.d.ts +2 -0
- package/dist/types/learning/authoring-service.d.ts.map +1 -1
- package/dist/types/learning/authoring.d.ts +20 -0
- package/dist/types/learning/authoring.d.ts.map +1 -1
- package/dist/types/learning/batch-service.d.ts +76 -2
- package/dist/types/learning/batch-service.d.ts.map +1 -1
- package/dist/types/learning/contracts.d.ts +759 -24
- package/dist/types/learning/contracts.d.ts.map +1 -1
- package/dist/types/learning/index.d.ts +1 -0
- package/dist/types/learning/index.d.ts.map +1 -1
- package/dist/types/learning/iteration-candidate-decision.d.ts +2 -2
- package/dist/types/learning/iteration-dispatch-contracts.d.ts +2 -2
- package/dist/types/learning/operations.d.ts +1118 -8
- package/dist/types/learning/operations.d.ts.map +1 -1
- package/dist/types/learning/repository.d.ts +402 -15
- package/dist/types/learning/repository.d.ts.map +1 -1
- package/dist/types/learning/reward-authoring.d.ts.map +1 -1
- package/dist/types/learning/reward-calibration.d.ts +74 -0
- package/dist/types/learning/reward-calibration.d.ts.map +1 -1
- package/dist/types/learning/reward-checks.d.ts +14 -2
- package/dist/types/learning/reward-checks.d.ts.map +1 -1
- package/dist/types/learning/training-handoff.d.ts +201 -0
- package/dist/types/learning/training-handoff.d.ts.map +1 -0
- package/dist/types/learning/transport.d.ts +963 -21
- package/dist/types/learning/transport.d.ts.map +1 -1
- package/dist/types/metrics-node.d.ts +3 -1
- package/dist/types/metrics-node.d.ts.map +1 -1
- package/dist/types/model-improvement-qualification.d.ts +3 -3
- package/dist/types/native-harness-experiment-attempt.d.ts +2 -2
- package/dist/types/profile-evaluation-comparisons.d.ts +230 -0
- package/dist/types/profile-evaluation-comparisons.d.ts.map +1 -1
- package/dist/types/profile-evaluation-target.d.ts +14 -0
- package/dist/types/profile-evaluation-target.d.ts.map +1 -0
- package/dist/types/profile-evaluations.d.ts +117 -14
- package/dist/types/profile-evaluations.d.ts.map +1 -1
- package/dist/types/profile-external-evaluation.d.ts +404 -0
- package/dist/types/profile-external-evaluation.d.ts.map +1 -0
- package/dist/types/profile-source-candidate.d.ts +15 -0
- package/dist/types/profile-source-candidate.d.ts.map +1 -0
- package/dist/types/review-conformance.d.ts +7 -7
- package/dist/types/rewards.d.ts +253 -0
- package/dist/types/rewards.d.ts.map +1 -1
- package/dist/types/rollouts.d.ts +3 -3
- package/dist/types/taskset-run-contract.d.ts +230 -0
- package/dist/types/taskset-run-contract.d.ts.map +1 -1
- package/dist/types/tasksets.d.ts +311 -3
- package/dist/types/tasksets.d.ts.map +1 -1
- package/dist/types/telemetry-analysis.d.ts +2 -2
- package/dist/types/telemetry-bundle.d.ts +6 -6
- package/dist/types/telemetry.d.ts +3 -3
- package/package.json +9 -1
- package/schemas/learning/v1/command-request.schema.json +1745 -409
- package/schemas/learning/v1/decision.schema.json +11 -0
- package/schemas/learning/v1/draft.schema.json +11 -0
- package/schemas/learning/v1/grade.schema.json +11 -0
- package/schemas/learning/v1/package.schema.json +532 -12
- package/schemas/learning/v1/policy.schema.json +836 -28
- package/schemas/learning/v1/reward.schema.json +520 -0
- package/schemas/learning/v1/reward_check.schema.json +9 -0
package/dist/experiments.js
CHANGED
|
@@ -33,7 +33,7 @@ export const ExperimentEvaluatorSchema = z.object({
|
|
|
33
33
|
configurationHash: ReleaseHashSchema.optional(),
|
|
34
34
|
output: z.enum(["boolean", "score", "category"]),
|
|
35
35
|
/** Category labels are meaningful only for category output. */
|
|
36
|
-
categories: z.array(z.string().trim().min(1).max(
|
|
36
|
+
categories: z.array(z.string().trim().min(1).max(500)).max(100),
|
|
37
37
|
}).strict().superRefine((value, context) => {
|
|
38
38
|
if ((value.output === "category") !== (value.categories.length > 0))
|
|
39
39
|
context.addIssue({ code: "custom", path: ["categories"], message: "Only category feedback declares categories." });
|
|
@@ -49,6 +49,9 @@ export function experimentCaseKey(identity) {
|
|
|
49
49
|
const value = ExperimentCaseIdentitySchema.parse(identity);
|
|
50
50
|
return JSON.stringify([value.caseId, value.seed, value.fixtureId]);
|
|
51
51
|
}
|
|
52
|
+
export function humanProjectionViewId(originalManifest, selectionHash, rulesHash) {
|
|
53
|
+
return `human-projection-${contentHash({ originalManifest, selectionHash, rulesHash }).slice(0, 48)}`;
|
|
54
|
+
}
|
|
52
55
|
export const ExperimentManifestContentSchema = z.object({
|
|
53
56
|
schemaVersion: z.literal("openpond.experimentManifest.v1"),
|
|
54
57
|
id: ReleaseIdSchema,
|
|
@@ -74,6 +77,8 @@ export const ExperimentManifestContentSchema = z.object({
|
|
|
74
77
|
definition: ImmutableReleaseRefSchema.extend({ revision: z.number().int().positive() }).strict().nullable(),
|
|
75
78
|
execution: ImmutableReleaseRefSchema,
|
|
76
79
|
scoringPassId: ReleaseIdSchema.nullable(),
|
|
80
|
+
/** Explicit accepted Human view; original execution/pass identity is retained. */
|
|
81
|
+
humanProjection: z.object({ originalManifest: ImmutableReleaseRefSchema, selectionHash: ReleaseHashSchema, rulesHash: ReleaseHashSchema }).strict().optional(),
|
|
77
82
|
}).strict().optional(),
|
|
78
83
|
evaluators: z.array(ExperimentEvaluatorSchema).max(100),
|
|
79
84
|
population: z.array(ExperimentCaseIdentitySchema).min(1).max(10_000),
|
|
@@ -81,8 +86,15 @@ export const ExperimentManifestContentSchema = z.object({
|
|
|
81
86
|
}).strict().superRefine((value, context) => {
|
|
82
87
|
if (value.execution?.compatibility && value.execution.compatibility.targetKind !== value.target.kind)
|
|
83
88
|
context.addIssue({ code: "custom", path: ["execution", "compatibility"], message: "Execution compatibility differs from its admitted target kind." });
|
|
84
|
-
if (value.lineage
|
|
85
|
-
|
|
89
|
+
if (value.lineage) {
|
|
90
|
+
const originalId = value.lineage.scoringPassId ?? value.lineage.execution.id, projection = value.lineage.humanProjection;
|
|
91
|
+
if (projection) {
|
|
92
|
+
if (projection.originalManifest.id !== originalId || projection.originalManifest.id.startsWith("human-projection-") || value.id !== humanProjectionViewId(projection.originalManifest, projection.selectionHash, projection.rulesHash))
|
|
93
|
+
context.addIssue({ code: "custom", path: ["lineage", "humanProjection"], message: "Human view must bind its original execution or scoring pass and deterministic selection identity." });
|
|
94
|
+
}
|
|
95
|
+
else if (value.id !== originalId)
|
|
96
|
+
context.addIssue({ code: "custom", path: ["lineage"], message: "Experiment identity must match its execution or independent scoring pass." });
|
|
97
|
+
}
|
|
86
98
|
if (new Set(value.population.map(experimentCaseKey)).size !== value.population.length)
|
|
87
99
|
context.addIssue({ code: "custom", path: ["population"], message: "Experiment cases must be unique." });
|
|
88
100
|
if (new Set(value.evaluators.map((item) => item.feedbackKey)).size !== value.evaluators.length)
|
|
@@ -93,7 +105,7 @@ export const ExperimentFeedbackSchema = z.object({
|
|
|
93
105
|
feedbackKey: FeedbackKeySchema,
|
|
94
106
|
evaluator: ExperimentEvaluatorSchema.shape.release,
|
|
95
107
|
status: z.enum(["scored", "unavailable", "failed", "pending"]),
|
|
96
|
-
value: z.union([z.boolean(), z.number().finite(), z.string().max(
|
|
108
|
+
value: z.union([z.boolean(), z.number().finite(), z.string().max(500)]).nullable(),
|
|
97
109
|
passed: z.boolean().nullable().optional(),
|
|
98
110
|
reasoning: z.string().max(20_000).nullable(),
|
|
99
111
|
evidenceRefs: z.array(ImmutableArtifactRefSchema).max(1_000),
|
package/dist/graders.js
CHANGED
|
@@ -18,6 +18,7 @@ export const GraderEvidenceContentSchema = z.object({
|
|
|
18
18
|
schemaVersion: z.literal("openpond.graderEvidence.v1"),
|
|
19
19
|
graderId: ReleaseIdSchema,
|
|
20
20
|
graderVersion: z.string().trim().min(1).max(100),
|
|
21
|
+
status: z.enum(["scored", "pending", "unavailable", "failed"]).optional(),
|
|
21
22
|
score: z.number().min(0).max(1).nullable(),
|
|
22
23
|
passed: z.boolean(),
|
|
23
24
|
rewardEligible: z.boolean(),
|
|
@@ -54,7 +55,7 @@ export async function gradeEvidence(input) {
|
|
|
54
55
|
return evidence(grader, await input.customVerifier({ grader, task: input.task, evidence: input.evidence }));
|
|
55
56
|
}
|
|
56
57
|
if (grader.kind === "human")
|
|
57
|
-
return evidence(grader,
|
|
58
|
+
return evidence(grader, { score: null, passed: false, rewardEligible: false, failureClass: null, status: "pending", feedback: ["Human review is pending."], visibleEvidenceRefs: [], privilegedEvidenceRefs: [] });
|
|
58
59
|
return evidence(grader, gradeDeterministic(grader, input.task, input.evidence));
|
|
59
60
|
}));
|
|
60
61
|
}
|
|
@@ -88,7 +89,7 @@ function unavailable(message) {
|
|
|
88
89
|
const GradeSummarySchema = z.object({
|
|
89
90
|
score: z.number().min(0).max(1).nullable(), passed: z.boolean(),
|
|
90
91
|
rewardEligible: z.boolean(), failureClass: FailureClassSchema.nullable(),
|
|
91
|
-
gradingStatus: z.enum(["scored", "unscorable", "not_configured"]),
|
|
92
|
+
gradingStatus: z.enum(["scored", "pending", "unscorable", "not_configured"]),
|
|
92
93
|
}).strict();
|
|
93
94
|
/** Shared ordinary grading policy. Results must match the declared population;
|
|
94
95
|
* missing checks cannot disappear from the denominator or the hard gates. */
|
|
@@ -105,6 +106,7 @@ export function aggregateGraderScores(input) {
|
|
|
105
106
|
let passed = true;
|
|
106
107
|
let eligible = false;
|
|
107
108
|
let unavailable = false;
|
|
109
|
+
let pending = false;
|
|
108
110
|
let infrastructure = !!input.infrastructureError;
|
|
109
111
|
for (const grader of input.graders) {
|
|
110
112
|
const item = components.get(grader.id);
|
|
@@ -113,18 +115,19 @@ export function aggregateGraderScores(input) {
|
|
|
113
115
|
if (!Number.isFinite(grader.weight) || grader.weight < 0 || (item.score !== null && (!Number.isFinite(item.score) || item.score < 0 || item.score > 1)))
|
|
114
116
|
throw new Error("Grade scores and weights must be finite and within their declared range.");
|
|
115
117
|
infrastructure ||= item.failureClass === "infrastructure_failure";
|
|
116
|
-
|
|
118
|
+
pending ||= item.status === "pending";
|
|
119
|
+
unavailable ||= item.status !== "pending" && (item.score === null || (item.failureClass !== null && item.failureClass !== "policy_failure"));
|
|
117
120
|
hardGateFailed ||= grader.hardGate && !item.passed;
|
|
118
121
|
passed &&= item.passed;
|
|
119
122
|
eligible ||= grader.rewardEligible && item.rewardEligible;
|
|
120
123
|
totalWeight += grader.weight;
|
|
121
124
|
weighted += (item.score ?? 0) * grader.weight;
|
|
122
125
|
}
|
|
123
|
-
const score = infrastructure || unavailable || totalWeight <= 0 ? null : hardGateFailed ? 0 : weighted / totalWeight;
|
|
126
|
+
const score = infrastructure || unavailable || pending || totalWeight <= 0 ? null : hardGateFailed ? 0 : weighted / totalWeight;
|
|
124
127
|
passed = score !== null && !hardGateFailed && passed;
|
|
125
128
|
return GradeSummarySchema.parse({ score, passed, rewardEligible: score !== null && eligible,
|
|
126
|
-
failureClass: infrastructure ? "infrastructure_failure" : score === null ? "grader_failure" : passed ? null : "policy_failure",
|
|
127
|
-
gradingStatus: !input.graders.length && !infrastructure ? "not_configured" : score === null ? "unscorable" : "scored" });
|
|
129
|
+
failureClass: infrastructure ? "infrastructure_failure" : pending && !unavailable ? null : score === null ? "grader_failure" : passed ? null : "policy_failure",
|
|
130
|
+
gradingStatus: !input.graders.length && !infrastructure ? "not_configured" : pending && !unavailable && !infrastructure ? "pending" : score === null ? "unscorable" : "scored" });
|
|
128
131
|
}
|
|
129
132
|
export const TaskGradeSchema = /* @__PURE__ */ (() => GradeSummarySchema.extend({
|
|
130
133
|
schemaVersion: z.literal("openpond.taskGrade.v1"),
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { contentHash, ImmutableReleaseRefSchema } from '@openpond/harness';
|
|
3
|
+
import { humanProjectionViewId, createExperimentManifest, createExperimentResult, verifyExperimentEvidence, experimentCaseKey, ExperimentManifestSchema, ExperimentResultSchema } from '../experiments.js';
|
|
4
|
+
import { ReviewReleaseSchema } from './contracts.js';
|
|
5
|
+
import { acceptedHumanEvidence } from './handoff.js';
|
|
6
|
+
import { createHumanResultView, HumanResultViewSchema } from './results.js';
|
|
7
|
+
import { LearningDomainError } from '../learning/errors.js';
|
|
8
|
+
const Hash = z.string().regex(/^[a-f0-9]{64}$/), Id = z.string().min(1).max(240);
|
|
9
|
+
const Base = { feedbackKey: Id, criterionId: Id };
|
|
10
|
+
export const HumanComparisonRuleSchema = z.discriminatedUnion('kind', [
|
|
11
|
+
z.object({ ...Base, kind: z.literal('normalized_score'), minimum: z.number().finite(), maximum: z.number().finite(), passThreshold: z.number().min(0).max(1).optional() }).strict(),
|
|
12
|
+
z.object({ ...Base, kind: z.literal('boolean') }).strict(),
|
|
13
|
+
z.object({ ...Base, kind: z.literal('category') }).strict(),
|
|
14
|
+
z.object({ ...Base, kind: z.literal('preference_score'), tieScore: z.number().min(0).max(1).nullable() }).strict(),
|
|
15
|
+
]);
|
|
16
|
+
const Lane = z.object({ execution: ImmutableReleaseRefSchema, resultHash: Hash, reviews: z.array(z.object({ review: ReviewReleaseSchema, decision: ReviewReleaseSchema }).strict()).min(1).max(10000) }).strict();
|
|
17
|
+
export const HumanComparisonSelectionsSchema = z.object({ rules: z.array(HumanComparisonRuleSchema).min(1).max(100), baseline: Lane, candidate: Lane }).strict().superRefine((v, c) => { if (new Set(v.rules.map(r => r.feedbackKey)).size !== v.rules.length)
|
|
18
|
+
c.addIssue({ code: 'custom', message: 'Select each Human feedback key once.' }); });
|
|
19
|
+
const Evidence = z.object({ manifest: ExperimentManifestSchema, result: ExperimentResultSchema }).strict();
|
|
20
|
+
const Source = z.object({ id: Id, manifestHash: Hash, resultHash: Hash }).strict();
|
|
21
|
+
const ReceiptContent = z.object({ schemaVersion: z.literal('openpond.humanComparisonProjection.v1'), teamId: Id, projectId: Id, selections: HumanComparisonSelectionsSchema, original: z.object({ baseline: Source, candidate: Source }).strict(), humanResults: z.object({ baseline: HumanResultViewSchema, candidate: HumanResultViewSchema }).strict(), baseline: Evidence, candidate: Evidence }).strict();
|
|
22
|
+
export const HumanComparisonProjectionSchema = ReceiptContent.extend({ contentHash: Hash }).strict();
|
|
23
|
+
function fail(code) { throw new LearningDomainError(code, 409); }
|
|
24
|
+
/** The host supplies current resource/subject owners and real retained receipt
|
|
25
|
+
* identities. This creates another evidence view; it never changes a pass. */
|
|
26
|
+
export async function projectAcceptedHumanComparison(input) {
|
|
27
|
+
const selections = HumanComparisonSelectionsSchema.parse(input.selections);
|
|
28
|
+
await input.authorize();
|
|
29
|
+
const sources = { baseline: verifyExperimentEvidence(input.baseline), candidate: verifyExperimentEvidence(input.candidate) };
|
|
30
|
+
const expectedExecutions = new Set([selections.baseline.execution.id, selections.candidate.execution.id]);
|
|
31
|
+
if (expectedExecutions.size !== 2)
|
|
32
|
+
fail("human_comparison_distinct_executions_required");
|
|
33
|
+
let acceptedBytes = 0;
|
|
34
|
+
async function lane(kind) {
|
|
35
|
+
const source = sources[kind], selection = selections[kind], execution = source.manifest.lineage?.execution;
|
|
36
|
+
if (source.manifest.lineage?.humanProjection)
|
|
37
|
+
fail('human_comparison_recursive_projection');
|
|
38
|
+
if (!execution || contentHash(execution) !== contentHash(selection.execution) || source.manifest.teamId !== input.teamId)
|
|
39
|
+
fail('human_comparison_execution_changed');
|
|
40
|
+
const reviews = [];
|
|
41
|
+
const projected = new Map();
|
|
42
|
+
for (const selected of selection.reviews) {
|
|
43
|
+
const review = await input.readReview(selected.review);
|
|
44
|
+
if (review.scope !== input.teamId || review.projectId !== input.projectId || review.kind !== 'grade' || review.revision !== selected.review.revision || review.id !== selected.review.id)
|
|
45
|
+
fail('human_comparison_review_scope_changed');
|
|
46
|
+
const accepted = acceptedHumanEvidence(review, { reviewHash: selected.review.contentHash, decisionHash: selected.decision.contentHash });
|
|
47
|
+
acceptedBytes += new TextEncoder().encode(JSON.stringify({ answers: accepted.answers, form: review.form })).byteLength;
|
|
48
|
+
if (acceptedBytes > 4_194_304)
|
|
49
|
+
fail('human_comparison_projection_too_large');
|
|
50
|
+
if (contentHash(accepted.decision) !== contentHash(selected.decision))
|
|
51
|
+
fail('human_comparison_decision_changed');
|
|
52
|
+
if (review.evidence.attempts.some(a => !expectedExecutions.has(a.experimentId)))
|
|
53
|
+
fail('human_comparison_foreign_attempt');
|
|
54
|
+
const attempts = review.evidence.attempts.filter(a => a.experimentId === execution.id);
|
|
55
|
+
if (attempts.length !== 1)
|
|
56
|
+
fail('human_comparison_attempt_ambiguous');
|
|
57
|
+
const attempt = attempts[0], identity = await input.caseIdentity(attempt), row = source.result.cases.find(r => experimentCaseKey(r.identity) === experimentCaseKey(identity));
|
|
58
|
+
if (!row || identity.caseId !== attempt.taskId || !row.traceRef || row.traceRef.contentHash !== attempt.trace?.contentHash || row.output === null)
|
|
59
|
+
fail('human_comparison_receipt_changed');
|
|
60
|
+
const key = experimentCaseKey(identity) + ':' + review.evidence.grader.id;
|
|
61
|
+
if (projected.has(key))
|
|
62
|
+
fail('human_comparison_duplicate_assessment');
|
|
63
|
+
projected.set(key, { review, accepted, attempt });
|
|
64
|
+
reviews.push(review);
|
|
65
|
+
}
|
|
66
|
+
const resultView = createHumanResultView({ executionId: execution.id, executionHash: execution.contentHash, reviews });
|
|
67
|
+
if (resultView.contentHash !== selection.resultHash || resultView.coverage.accepted !== reviews.length)
|
|
68
|
+
fail('human_comparison_result_changed');
|
|
69
|
+
const evaluatorRules = new Map(selections.rules.map(rule => [rule.feedbackKey, rule]));
|
|
70
|
+
const evaluators = source.manifest.evaluators.map(evaluator => {
|
|
71
|
+
const rule = evaluatorRules.get(evaluator.feedbackKey);
|
|
72
|
+
if (!rule)
|
|
73
|
+
return evaluator;
|
|
74
|
+
const forms = reviews.filter(r => r.evidence.grader.id === evaluator.release.id && String(r.evidence.grader.revision) === String(evaluator.release.revision) && r.evidence.grader.contentHash === evaluator.release.contentHash).map(r => r.form);
|
|
75
|
+
if (!forms.length || new Set(forms.map(contentHash)).size !== 1)
|
|
76
|
+
fail('human_comparison_form_changed');
|
|
77
|
+
const criterion = forms[0].criteria.find(c => c.id === rule.criterionId);
|
|
78
|
+
if (!criterion)
|
|
79
|
+
fail('human_comparison_criterion_unavailable');
|
|
80
|
+
if (rule.kind === 'normalized_score' && (criterion.kind !== 'score' || rule.minimum !== criterion.minimum || rule.maximum !== criterion.maximum || rule.maximum <= rule.minimum) || rule.kind === 'boolean' && criterion.kind !== 'boolean' || rule.kind === 'category' && criterion.kind !== 'category' || rule.kind === 'preference_score' && (criterion.kind !== 'preference' || forms[0].mode !== 'pairwise'))
|
|
81
|
+
fail('human_comparison_rule_changed');
|
|
82
|
+
return { ...evaluator, output: rule.kind === 'boolean' ? 'boolean' : rule.kind === 'category' ? 'category' : 'score', categories: criterion.kind === 'category' ? criterion.options : [], configurationHash: contentHash({ original: evaluator.configurationHash, rule, formHash: contentHash(forms[0]) }) };
|
|
83
|
+
});
|
|
84
|
+
if (selections.rules.some(rule => !source.manifest.evaluators.some(e => e.feedbackKey === rule.feedbackKey)))
|
|
85
|
+
fail('human_comparison_grader_unavailable');
|
|
86
|
+
const { contentHash: _manifestHash, ...manifestBody } = source.manifest;
|
|
87
|
+
void _manifestHash;
|
|
88
|
+
const projectionLineage = { originalManifest: { id: source.manifest.id, contentHash: source.manifest.contentHash }, selectionHash: contentHash(selection), rulesHash: contentHash(selections.rules) };
|
|
89
|
+
const manifest = createExperimentManifest({ ...manifestBody, id: humanProjectionViewId(projectionLineage.originalManifest, projectionLineage.selectionHash, projectionLineage.rulesHash), lineage: { ...source.manifest.lineage, humanProjection: projectionLineage }, evaluators });
|
|
90
|
+
const cases = source.result.cases.map(row => {
|
|
91
|
+
const feedback = row.feedback.map(original => {
|
|
92
|
+
const rule = evaluatorRules.get(original.feedbackKey);
|
|
93
|
+
if (!rule)
|
|
94
|
+
return original;
|
|
95
|
+
const found = projected.get(experimentCaseKey(row.identity) + ':' + original.evaluator.id);
|
|
96
|
+
if (!found || found.review.evidence.grader.contentHash !== original.evaluator.contentHash)
|
|
97
|
+
fail('human_comparison_assessment_missing');
|
|
98
|
+
const answer = found.accepted.answers.find(a => a.criterionId === rule.criterionId);
|
|
99
|
+
let value = null;
|
|
100
|
+
if (answer && !answer.abstain) {
|
|
101
|
+
if (rule.kind === 'normalized_score' && typeof answer.value === 'number')
|
|
102
|
+
value = (answer.value - rule.minimum) / (rule.maximum - rule.minimum);
|
|
103
|
+
if (rule.kind === 'boolean' && typeof answer.value === 'boolean')
|
|
104
|
+
value = answer.value;
|
|
105
|
+
if (rule.kind === 'category' && typeof answer.value === 'string')
|
|
106
|
+
value = answer.value;
|
|
107
|
+
if (rule.kind === 'preference_score') {
|
|
108
|
+
const slot = found.review.evidence.attempts.findIndex(a => a.experimentId === execution.id && a.attemptId === found.attempt.attemptId);
|
|
109
|
+
value = answer.value === 'tie' ? rule.tieScore : answer.value === 'A' || answer.value === 'B' ? (answer.value === (slot === 0 ? 'A' : 'B') ? 1 : 0) : null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { ...original, status: value === null ? 'unavailable' : 'scored', value, passed: value === null ? null : rule.kind === 'boolean' ? value : rule.kind === 'normalized_score' && rule.passThreshold !== undefined ? Number(value) >= rule.passThreshold : null, reasoning: answer?.note ?? null, evidenceRefs: [{ id: found.accepted.review.id, contentHash: found.accepted.review.contentHash, mediaType: 'application/vnd.openpond.human-review+json', sizeBytes: null }, { id: found.accepted.decision.id, contentHash: found.accepted.decision.contentHash, mediaType: 'application/vnd.openpond.human-decision+json', sizeBytes: null }] };
|
|
113
|
+
});
|
|
114
|
+
const complete = row.output !== null && feedback.every(f => f.status === 'scored');
|
|
115
|
+
return { ...row, feedback, status: complete ? 'completed' : row.status, error: complete ? null : row.error };
|
|
116
|
+
});
|
|
117
|
+
const { contentHash: _resultHash, ...resultBody } = source.result;
|
|
118
|
+
void _resultHash;
|
|
119
|
+
const result = createExperimentResult({ ...resultBody, manifest: { id: manifest.id, contentHash: manifest.contentHash }, cases }, manifest);
|
|
120
|
+
// After all private reads, re-read every exact accepted revision through the
|
|
121
|
+
// current host authority before returning a projection usable by adoption.
|
|
122
|
+
for (const review of reviews) {
|
|
123
|
+
const current = await input.readReview({ id: review.id, revision: review.revision, contentHash: review.contentHash });
|
|
124
|
+
if (current.contentHash !== review.contentHash || current.status !== 'accepted')
|
|
125
|
+
fail('human_comparison_review_changed');
|
|
126
|
+
}
|
|
127
|
+
await input.authorize();
|
|
128
|
+
return { evidence: { manifest, result }, humanResult: resultView };
|
|
129
|
+
}
|
|
130
|
+
const baseline = await lane('baseline'), candidate = await lane('candidate');
|
|
131
|
+
for (const ref of [...selections.baseline.reviews, ...selections.candidate.reviews]) {
|
|
132
|
+
const current = await input.readReview(ref.review);
|
|
133
|
+
if (current.contentHash !== ref.review.contentHash || current.status !== 'accepted' || current.decisions.at(-1)?.contentHash !== ref.decision.contentHash)
|
|
134
|
+
fail('human_comparison_review_changed');
|
|
135
|
+
}
|
|
136
|
+
await input.authorize();
|
|
137
|
+
const original = { baseline: { id: sources.baseline.manifest.id, manifestHash: sources.baseline.manifest.contentHash, resultHash: sources.baseline.result.contentHash }, candidate: { id: sources.candidate.manifest.id, manifestHash: sources.candidate.manifest.contentHash, resultHash: sources.candidate.result.contentHash } };
|
|
138
|
+
const body = ReceiptContent.parse({ schemaVersion: 'openpond.humanComparisonProjection.v1', teamId: input.teamId, projectId: input.projectId, selections, original, humanResults: { baseline: baseline.humanResult, candidate: candidate.humanResult }, baseline: baseline.evidence, candidate: candidate.evidence });
|
|
139
|
+
if (new TextEncoder().encode(JSON.stringify(body)).byteLength > 8_388_608)
|
|
140
|
+
fail('human_comparison_projection_too_large');
|
|
141
|
+
return HumanComparisonProjectionSchema.parse({ ...body, contentHash: contentHash(body) });
|
|
142
|
+
}
|
|
143
|
+
export function verifyHumanComparisonProjection(raw, originals) {
|
|
144
|
+
const value = HumanComparisonProjectionSchema.parse(raw), { contentHash: hash, ...body } = value;
|
|
145
|
+
if (hash !== contentHash(body))
|
|
146
|
+
fail('human_comparison_projection_invalid');
|
|
147
|
+
for (const kind of ['baseline', 'candidate']) {
|
|
148
|
+
const evidence = verifyExperimentEvidence(value[kind]), lineage = evidence.manifest.lineage, projection = lineage?.humanProjection, original = value.original[kind], selection = value.selections[kind];
|
|
149
|
+
if (!lineage || !projection || projection.originalManifest.id !== original.id || projection.originalManifest.contentHash !== original.manifestHash || projection.selectionHash !== contentHash(selection) || projection.rulesHash !== contentHash(value.selections.rules) || contentHash(lineage.execution) !== contentHash(selection.execution) || evidence.manifest.teamId !== value.teamId || value.humanResults[kind].contentHash !== selection.resultHash)
|
|
150
|
+
fail('human_comparison_projection_lineage_changed');
|
|
151
|
+
const { contentHash: humanHash, ...humanBody } = value.humanResults[kind];
|
|
152
|
+
if (contentHash(humanBody) !== humanHash || humanBody.executionId !== selection.execution.id || humanBody.executionHash !== selection.execution.contentHash || contentHash(humanBody.selection) !== contentHash(selection.reviews.map(ref => ref.review)))
|
|
153
|
+
fail('human_comparison_human_result_changed');
|
|
154
|
+
if (originals) {
|
|
155
|
+
const actual = verifyExperimentEvidence(originals[kind]);
|
|
156
|
+
if (actual.manifest.lineage?.humanProjection || actual.manifest.id !== original.id || actual.manifest.contentHash !== original.manifestHash || actual.result.contentHash !== original.resultHash || contentHash(actual.manifest.lineage?.execution) !== contentHash(selection.execution))
|
|
157
|
+
fail('human_comparison_original_changed');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { HumanRevokeLocalPublicationSchema, HumanLocalPublicationsReadSchema, HumanNativeAttemptOriginSchema, HumanLocalAttemptOriginSchema, HumanExportLocalRequestSchema, HumanPublishLocalRequestSchema, HumanShareLocalRequestSchema } from "./local-publication.js";
|
|
2
|
+
import { HumanTaskProposalSchema, HumanTaskProposalViewSchema } from "./task-proposals.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { contentHash, ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema } from "@openpond/harness";
|
|
5
|
+
const Id = ReleaseIdSchema;
|
|
6
|
+
export const ReviewReleaseSchema = z.object({ id: Id, revision: z.number().int().positive(), contentHash: ReleaseHashSchema }).strict();
|
|
7
|
+
export const HumanCriterionSchema = z.discriminatedUnion("kind", [
|
|
8
|
+
z.object({ id: Id, label: z.string().trim().min(1).max(500), instructions: z.string().max(20_000), required: z.boolean(), allowAbstain: z.boolean(), requireRationale: z.boolean().optional(), anchors: z.array(z.object({ value: z.union([z.number(), z.string(), z.boolean()]), description: z.string().min(1).max(2000) }).strict()).max(100).optional(), kind: z.literal("score"), minimum: z.number().finite(), maximum: z.number().finite(), step: z.number().positive().finite() }).strict().refine(c => c.minimum < c.maximum && c.step <= c.maximum - c.minimum, "Use an increasing raw score range and a step within that range."),
|
|
9
|
+
z.object({ id: Id, label: z.string().trim().min(1).max(500), instructions: z.string().max(20_000), required: z.boolean(), allowAbstain: z.boolean(), requireRationale: z.boolean().optional(), anchors: z.array(z.object({ value: z.union([z.number(), z.string(), z.boolean()]), description: z.string().min(1).max(2000) }).strict()).max(100).optional(), kind: z.literal("category"), options: z.array(z.string().trim().min(1).max(500)).min(2).max(100) }).strict().refine(c => new Set(c.options).size === c.options.length, "Category options must be unique."),
|
|
10
|
+
z.object({ id: Id, label: z.string().trim().min(1).max(500), instructions: z.string().max(20_000), required: z.boolean(), allowAbstain: z.boolean(), requireRationale: z.boolean().optional(), anchors: z.array(z.object({ value: z.union([z.number(), z.string(), z.boolean()]), description: z.string().min(1).max(2000) }).strict()).max(100).optional(), kind: z.literal("boolean") }).strict(),
|
|
11
|
+
z.object({ id: Id, label: z.string().trim().min(1).max(500), instructions: z.string().max(20_000), required: z.boolean(), allowAbstain: z.boolean(), requireRationale: z.boolean().optional(), anchors: z.array(z.object({ value: z.union([z.number(), z.string(), z.boolean()]), description: z.string().min(1).max(2000) }).strict()).max(100).optional(), kind: z.literal("text"), minimumLength: z.number().int().nonnegative().max(20_000).optional(), maximumLength: z.number().int().positive().max(20_000) }).strict(),
|
|
12
|
+
z.object({ id: Id, label: z.string().trim().min(1).max(500), instructions: z.string().max(20_000), required: z.boolean(), allowAbstain: z.boolean(), requireRationale: z.boolean().optional(), anchors: z.array(z.object({ value: z.union([z.number(), z.string(), z.boolean()]), description: z.string().min(1).max(2000) }).strict()).max(100).optional(), kind: z.literal("preference"), requireStrength: z.boolean().optional(), allowTie: z.boolean() }).strict(),
|
|
13
|
+
]);
|
|
14
|
+
export const HumanFormSchema = z.object({ schemaVersion: z.literal("openpond.humanForm.v1"), mode: z.enum(["individual", "pairwise"]), instructions: z.string().max(20_000), criteria: z.array(HumanCriterionSchema).min(1).max(100) }).strict().superRefine((form, ctx) => {
|
|
15
|
+
form.criteria.forEach((criterion, index) => {
|
|
16
|
+
if (criterion.kind === "text" && (criterion.minimumLength ?? 0) > criterion.maximumLength)
|
|
17
|
+
ctx.addIssue({ code: "custom", path: ["criteria", index, "minimumLength"], message: "Minimum text length must not exceed maximum length." });
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
criterion.anchors?.forEach((anchor, anchorIndex) => { const value = anchor.value; const valid = criterion.kind === "score" ? typeof value === "number" && Number.isFinite(value) && value >= criterion.minimum && value <= criterion.maximum && Math.abs((value - criterion.minimum) / criterion.step - Math.round((value - criterion.minimum) / criterion.step)) < 1e-8 : criterion.kind === "category" ? typeof value === "string" && criterion.options.includes(value) : criterion.kind === "boolean" ? typeof value === "boolean" : criterion.kind === "preference" ? ["A", "B", ...(criterion.allowTie ? ["tie"] : [])].includes(String(value)) : typeof value === "string"; const key = JSON.stringify(value); if (!valid || seen.has(key))
|
|
20
|
+
ctx.addIssue({ code: "custom", path: ["criteria", index, "anchors", anchorIndex, "value"], message: "Each anchor must describe a unique valid raw value." }); seen.add(key); });
|
|
21
|
+
});
|
|
22
|
+
if (new Set(form.criteria.map(c => c.id)).size !== form.criteria.length)
|
|
23
|
+
ctx.addIssue({ code: "custom", path: ["criteria"], message: "Criterion IDs must be unique." });
|
|
24
|
+
if (form.mode === "individual" && form.criteria.some(c => c.kind === "preference"))
|
|
25
|
+
ctx.addIssue({ code: "custom", path: ["criteria"], message: "Preferences require an explicit pair." });
|
|
26
|
+
});
|
|
27
|
+
export const HumanAttemptRefSchema = z.object({ localSource: z.union([HumanLocalAttemptOriginSchema, HumanNativeAttemptOriginSchema]).optional(), recordedSource: z.object({ id: Id, snapshotHash: ReleaseHashSchema, boundaryId: Id, boundaryRevisionHash: ReleaseHashSchema }).strict().optional(), experimentId: Id, attemptId: Id, taskId: Id, targetId: Id, output: ReviewReleaseSchema, trace: ReviewReleaseSchema.nullable(), familyKey: Id, split: z.enum(["train", "validation", "test", "frozen_eval"]) }).strict();
|
|
28
|
+
export const HumanEvidenceSchema = z.object({ publication: ReviewReleaseSchema.optional(), dataset: ReviewReleaseSchema, grader: ReviewReleaseSchema, graderBinding: z.object({ id: Id, version: z.string().min(1).max(100), contentHash: ReleaseHashSchema }).strict(), rubric: ReviewReleaseSchema, attempts: z.array(HumanAttemptRefSchema).max(2), taskIds: z.array(Id).min(1).max(1000).optional() }).strict().superRefine((e, ctx) => {
|
|
29
|
+
if (!e.attempts.length && !e.taskIds?.length)
|
|
30
|
+
ctx.addIssue({ code: "custom", message: "Select retained attempts or exact tasks." });
|
|
31
|
+
if (e.taskIds && new Set(e.taskIds).size !== e.taskIds.length)
|
|
32
|
+
ctx.addIssue({ code: "custom", path: ["taskIds"], message: "Select each task once." });
|
|
33
|
+
if (e.attempts.length === 2 && (e.attempts[0].attemptId === e.attempts[1].attemptId || e.attempts[0].taskId !== e.attempts[1].taskId || e.attempts[0].familyKey !== e.attempts[1].familyKey || e.attempts[0].split !== e.attempts[1].split))
|
|
34
|
+
ctx.addIssue({ code: "custom", path: ["attempts"], message: "A/B review requires distinct attempts of the same task, family and split." });
|
|
35
|
+
});
|
|
36
|
+
export const HumanAnswerSchema = z.object({ criterionId: Id, value: z.union([z.number().finite(), z.boolean(), z.string().max(20_000), z.null()]), abstain: z.boolean(), note: z.string().max(20_000), strength: z.enum(["slight", "moderate", "strong"]).optional() }).strict();
|
|
37
|
+
export const HumanPolicySchema = z.object({ approval: z.enum(["none", "manager"]), minimumRaters: z.number().int().min(1).max(100), managerIds: z.array(Id).max(100), reviewerIds: z.array(Id).max(100), queueClaim: z.boolean(), teamVisible: z.boolean(), distinctManager: z.boolean().optional(), independentRaters: z.boolean().optional(), blindPair: z.boolean().optional() }).strict().superRefine((p, c) => {
|
|
38
|
+
if (p.approval === "none" && p.minimumRaters !== 1)
|
|
39
|
+
c.addIssue({ code: "custom", path: ["minimumRaters"], message: "Multi-rater review requires explicit adjudication." });
|
|
40
|
+
});
|
|
41
|
+
const SubmissionSchema = z.object({ taskProposal: HumanTaskProposalSchema.optional(), id: Id, revision: z.number().int().positive(), actorId: Id, generation: z.number().int().positive(), evidenceHash: ReleaseHashSchema, formHash: ReleaseHashSchema, answers: z.array(HumanAnswerSchema).max(100), note: z.string().max(20_000), submittedAt: ReleaseTimestampSchema, supersedes: Id.nullable(), contentHash: ReleaseHashSchema }).strict();
|
|
42
|
+
const DecisionSchema = z.object({ taskProposalHash: ReleaseHashSchema.optional(), id: Id, revision: z.number().int().positive(), actorId: Id, outcome: z.enum(["accept", "reject", "request_changes"]), submissions: z.array(z.object({ id: Id, contentHash: ReleaseHashSchema }).strict()).min(1).max(100), answers: z.array(HumanAnswerSchema).max(100), note: z.string().max(20_000), decidedAt: ReleaseTimestampSchema, contentHash: ReleaseHashSchema }).strict();
|
|
43
|
+
export const HumanReviewSchema = z.object({ schemaVersion: z.literal("openpond.humanReview.v1"), id: Id, revision: z.number().int().positive(), scope: Id, projectId: Id, createdBy: Id, kind: z.enum(["author", "execute", "grade"]), dueAt: ReleaseTimestampSchema.optional(), title: z.string().trim().min(1).max(500), authorPublication: z.object({ proposalHash: ReleaseHashSchema, dataset: ReviewReleaseSchema, operationId: Id, publishedBy: Id, publishedAt: ReleaseTimestampSchema }).strict().nullable(), evidence: HumanEvidenceSchema, form: HumanFormSchema, policy: HumanPolicySchema, status: z.enum(["queued", "assigned", "in_progress", "submitted", "returned", "accepted", "rejected", "cancelled"]), assigneeId: Id.nullable(), generation: z.number().int().positive(), drafts: z.array(z.object({ taskProposal: HumanTaskProposalSchema.optional(), actorId: Id, generation: z.number().int().positive(), answers: z.array(HumanAnswerSchema).max(100), note: z.string().max(20_000), updatedAt: ReleaseTimestampSchema }).strict()).max(100), submissions: z.array(SubmissionSchema).max(1000), decisions: z.array(DecisionSchema).max(1000), audit: z.array(z.object({ actorId: Id, action: z.enum(["claim", "reassign", "cancel"]), priorAssigneeId: Id.nullable(), assigneeId: Id.nullable(), generation: z.number().int().positive(), note: z.string().max(20_000), occurredAt: ReleaseTimestampSchema }).strict()).max(1000), execution: z.object({ executionId: Id.nullable(), operationId: Id, boundBy: Id, configurationHash: ReleaseHashSchema, generation: z.number().int().positive(), result: ReviewReleaseSchema.nullable() }).strict().nullable(), createdAt: ReleaseTimestampSchema, updatedAt: ReleaseTimestampSchema, contentHash: ReleaseHashSchema }).strict();
|
|
44
|
+
const Mutation = { operationId: Id, id: Id, expectedRevision: z.number().int().nonnegative() };
|
|
45
|
+
export const HumanReviewCommandSchema = z.discriminatedUnion("action", [
|
|
46
|
+
z.object({ ...Mutation, action: z.literal("create"), projectId: Id, kind: HumanReviewSchema.shape.kind, title: HumanReviewSchema.shape.title, dueAt: ReleaseTimestampSchema.optional(), evidence: HumanEvidenceSchema, form: HumanFormSchema, policy: HumanPolicySchema, assigneeId: Id.nullable() }).strict(),
|
|
47
|
+
z.object({ ...Mutation, action: z.literal("claim") }).strict(),
|
|
48
|
+
z.object({ ...Mutation, action: z.literal("bind_author_publication"), proposalHash: ReleaseHashSchema, dataset: ReviewReleaseSchema }).strict(),
|
|
49
|
+
z.object({ ...Mutation, action: z.literal("reserve_execution"), generation: z.number().int().positive(), runOperationId: Id, configurationHash: ReleaseHashSchema }).strict(),
|
|
50
|
+
z.object({ ...Mutation, action: z.literal("bind_execution"), generation: z.number().int().positive(), executionId: Id, runOperationId: Id, configurationHash: ReleaseHashSchema }).strict(),
|
|
51
|
+
z.object({ ...Mutation, action: z.literal("reassign"), assigneeId: Id.nullable(), note: z.string().trim().min(1).max(20_000) }).strict(),
|
|
52
|
+
z.object({ ...Mutation, action: z.literal("cancel"), note: z.string().max(20_000) }).strict(),
|
|
53
|
+
z.object({ ...Mutation, action: z.literal("save_draft"), taskProposal: HumanTaskProposalSchema.optional(), generation: z.number().int().positive(), answers: z.array(HumanAnswerSchema).max(100), note: z.string().max(20_000) }).strict(),
|
|
54
|
+
z.object({ ...Mutation, action: z.literal("submit"), taskProposal: HumanTaskProposalSchema.optional(), generation: z.number().int().positive(), evidenceHash: ReleaseHashSchema, formHash: ReleaseHashSchema, executionResult: ReviewReleaseSchema.optional(), answers: z.array(HumanAnswerSchema).max(100), note: z.string().max(20_000) }).strict(),
|
|
55
|
+
z.object({ ...Mutation, action: z.literal("decide"), taskProposalHash: ReleaseHashSchema.optional(), outcome: DecisionSchema.shape.outcome, submissions: DecisionSchema.shape.submissions, answers: DecisionSchema.shape.answers, note: DecisionSchema.shape.note }).strict(),
|
|
56
|
+
]);
|
|
57
|
+
export const HumanReviewRequestSchema = z.discriminatedUnion("endpoint", [
|
|
58
|
+
z.object({ endpoint: z.literal("command"), scope: Id, command: HumanReviewCommandSchema }).strict(),
|
|
59
|
+
z.object({ endpoint: z.literal("get"), scope: Id, id: Id, revision: z.number().int().positive().optional() }).strict(),
|
|
60
|
+
z.object({ endpoint: z.literal("inbox"), scope: Id, projectId: Id.optional(), view: z.enum(["mine", "team", "approval"]), status: z.enum(["active", "history"]), afterId: Id.optional(), limit: z.number().int().min(1).max(100) }).strict(),
|
|
61
|
+
]);
|
|
62
|
+
export function sealHumanRecord(record) { return { ...record, contentHash: contentHash(record) }; }
|
|
63
|
+
export function assertHumanAnswers(form, answers, complete) {
|
|
64
|
+
if (new Set(answers.map(a => a.criterionId)).size !== answers.length)
|
|
65
|
+
throw new Error("human_duplicate_criterion");
|
|
66
|
+
for (const answer of answers) {
|
|
67
|
+
const criterion = form.criteria.find(c => c.id === answer.criterionId);
|
|
68
|
+
if (!criterion)
|
|
69
|
+
throw new Error("human_unknown_criterion");
|
|
70
|
+
if (complete && criterion.requireRationale && !answer.note.trim())
|
|
71
|
+
throw new Error("human_rationale_required");
|
|
72
|
+
if (complete && criterion.kind === "preference" && criterion.requireStrength && !answer.strength && !answer.abstain)
|
|
73
|
+
throw new Error("human_preference_strength_required");
|
|
74
|
+
if (answer.abstain) {
|
|
75
|
+
if (!criterion.allowAbstain || answer.value !== null || !answer.note.trim())
|
|
76
|
+
throw new Error("human_abstention_invalid");
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const v = answer.value;
|
|
80
|
+
if (!complete && v === null)
|
|
81
|
+
continue;
|
|
82
|
+
const valid = criterion.kind === "score" ? typeof v === "number" && v >= criterion.minimum && v <= criterion.maximum && Math.abs((v - criterion.minimum) / criterion.step - Math.round((v - criterion.minimum) / criterion.step)) < 1e-8
|
|
83
|
+
: criterion.kind === "category" ? typeof v === "string" && criterion.options.includes(v)
|
|
84
|
+
: criterion.kind === "boolean" ? typeof v === "boolean"
|
|
85
|
+
: criterion.kind === "preference" ? ["A", "B", ...(criterion.allowTie ? ["tie"] : [])].includes(String(v))
|
|
86
|
+
: typeof v === "string" && v.length >= (criterion.minimumLength ?? 0) && v.length <= criterion.maximumLength && (!criterion.required || Boolean(v.trim()));
|
|
87
|
+
if (!valid)
|
|
88
|
+
throw new Error("human_answer_invalid");
|
|
89
|
+
}
|
|
90
|
+
if (complete && form.criteria.some(c => c.required && !answers.some(a => a.criterionId === c.id)))
|
|
91
|
+
throw new Error("human_required_answer_missing");
|
|
92
|
+
}
|
|
93
|
+
export const HumanReviewViewSchema = HumanReviewSchema.omit({ evidence: true, drafts: true, submissions: true }).extend({ drafts: z.array(HumanReviewSchema.shape.drafts.element.omit({ taskProposal: true }).extend({ taskProposal: HumanTaskProposalViewSchema.optional() })).max(100), submissions: z.array(SubmissionSchema.omit({ taskProposal: true }).extend({ taskProposal: HumanTaskProposalViewSchema.optional() })).max(1000), evidence: HumanEvidenceSchema.nullable(), evidenceHash: ReleaseHashSchema, formHash: ReleaseHashSchema, permissions: z.object({ owner: z.boolean(), manager: z.boolean(), reviewer: z.boolean(), claim: z.boolean() }).strict() }).strict();
|
|
94
|
+
export const HumanInboxSchema = z.object({ items: z.array(HumanReviewViewSchema).max(100), nextCursor: Id.nullable(), actionCount: z.number().int().nonnegative(), approvalAvailable: z.boolean() }).strict();
|
|
95
|
+
export const HumanSnapshotRequestSchema = z.object({ endpoint: z.literal("snapshot"), scope: Id, projectId: Id, graderId: Id, selections: z.array(z.object({ executionId: Id, receiptId: Id }).strict()).min(1).max(2) }).strict();
|
|
96
|
+
export const HumanTasksSnapshotRequestSchema = z.object({ endpoint: z.literal("snapshot_tasks"), scope: Id, projectId: Id, graderId: Id, dataset: ReviewReleaseSchema, taskIds: z.array(Id).min(1).max(1000).optional(), all: z.literal(true).optional() }).strict().refine(value => Boolean(value.all) !== Boolean(value.taskIds), "Choose an exact selected task list or the server-frozen full Dataset.");
|
|
97
|
+
export const HumanInspectRequestSchema = z.object({ endpoint: z.literal("inspect"), scope: Id, id: Id, slot: z.number().int().min(0).max(1).optional(), afterId: z.string().min(1).max(500).optional() }).strict();
|
|
98
|
+
export const HumanResultsReadRequestSchema = z.object({ endpoint: z.literal("results"), scope: Id, executionId: Id, selection: z.array(ReviewReleaseSchema).max(10000).optional() }).strict();
|
|
99
|
+
export const HumanMembersRequestSchema = z.object({ endpoint: z.literal("members"), scope: Id }).strict();
|
|
100
|
+
export const HumanExecuteRequestSchema = z.object({ endpoint: z.literal("execute"), scope: Id, id: Id, expectedRevision: z.number().int().positive(), generation: z.number().int().positive(), operationId: Id, configuration: z.unknown() }).strict();
|
|
101
|
+
export const HumanExecutionReadRequestSchema = z.object({ endpoint: z.literal("execution"), scope: Id, id: Id, action: z.enum(["status", "result", "cancel"]) }).strict();
|
|
102
|
+
export const HumanPublishEvidenceRequestSchema = HumanSnapshotRequestSchema.omit({ endpoint: true }).extend({ endpoint: z.literal("publish_evidence"), operationId: Id, audience: z.array(Id).max(100), workspaceVisible: z.boolean() }).strict();
|
|
103
|
+
export const HumanPublicationAccessRequestSchema = z.object({ endpoint: z.literal("publication_access"), scope: Id, projectId: Id }).strict();
|
|
104
|
+
export const HumanRevokePublicationRequestSchema = z.object({ endpoint: z.literal("revoke_publication"), scope: Id, publication: ReviewReleaseSchema }).strict();
|
|
105
|
+
export const HumanPublishAuthorRequestSchema = z.object({ endpoint: z.literal("publish_author"), scope: Id, id: Id, expectedRevision: z.number().int().positive(), operationId: Id, proposalHash: ReleaseHashSchema, workspaceId: Id.optional(), expectedWorkspaceRevision: z.number().int().positive().optional() }).strict();
|
|
106
|
+
export const HumanAuthorTasksRequestSchema = z.object({ endpoint: z.literal("author_tasks"), scope: Id, id: Id, includeReference: z.literal(true).optional() }).strict();
|
|
107
|
+
export const HumanGraderChoicesRequestSchema = z.object({ endpoint: z.literal("graders"), scope: Id, projectId: Id, dataset: ReviewReleaseSchema }).strict();
|
|
108
|
+
export const HumanGraderChoicesSchema = z.array(z.object({ id: Id, name: z.string(), version: z.string(), contentHash: ReleaseHashSchema, mode: z.enum(["individual", "pairwise"]) }).strict()).max(1000);
|
|
109
|
+
export const HumanReviewTransportSchema = z.union([HumanRevokeLocalPublicationSchema, HumanLocalPublicationsReadSchema, HumanExportLocalRequestSchema, HumanPublishLocalRequestSchema, HumanShareLocalRequestSchema, HumanPublicationAccessRequestSchema, HumanPublishAuthorRequestSchema, HumanAuthorTasksRequestSchema, HumanGraderChoicesRequestSchema, HumanPublishEvidenceRequestSchema, HumanRevokePublicationRequestSchema, HumanExecuteRequestSchema, HumanExecutionReadRequestSchema, HumanReviewRequestSchema, HumanSnapshotRequestSchema, HumanTasksSnapshotRequestSchema, HumanInspectRequestSchema, HumanResultsReadRequestSchema, HumanMembersRequestSchema]);
|
|
110
|
+
export const HumanInspectionSchema = z.array(z.object({ origin: z.literal("owner_attested_local").optional(), coverage: z.object({ sourceEventCount: z.number().int().nonnegative(), publishedEventCount: z.number().int().nonnegative(), traceLimited: z.boolean(), artifactsIncluded: z.literal(false) }).strict().optional(), reviewId: Id, reviewRevision: z.number().int().positive(), evidenceHash: ReleaseHashSchema, slot: z.string(), input: z.unknown(), output: z.unknown(), error: z.string().nullable(), events: z.array(z.record(z.string(), z.unknown())), eventCount: z.number().int().nonnegative(), evidenceLimited: z.boolean(), nextEventCursor: z.string().nullable(), slotIndex: z.number().int().nonnegative(), executionId: Id.optional(), receiptId: Id.optional() }).strict()).max(1000);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { contentHash } from "@openpond/harness";
|
|
2
|
+
import { LearningDomainError } from "../learning/errors.js";
|
|
3
|
+
/** Reserve before I/O; recovery reads the original owner's durable admission.
|
|
4
|
+
* A reservation with unknown admission never becomes a second dispatch. */
|
|
5
|
+
export async function dispatchAssignedHumanExecution(input) {
|
|
6
|
+
const { service, context, request } = input, opened = await service.get(context, request.id);
|
|
7
|
+
if (opened.kind !== "execute" || opened.generation !== request.generation)
|
|
8
|
+
throw new LearningDomainError("human_execution_assignment_stale", 409);
|
|
9
|
+
const retained = opened.execution;
|
|
10
|
+
if (retained && retained.boundBy !== context.actorId)
|
|
11
|
+
throw new LearningDomainError("human_execution_executor_changed", 403);
|
|
12
|
+
const runOperationId = retained?.operationId ?? `human-run-${contentHash([context.actorId, request.id, request.operationId])}`;
|
|
13
|
+
const raw = input.configuration;
|
|
14
|
+
const configuration = { ...raw, operationId: runOperationId, request: { ...raw.request, operationId: runOperationId } }, configurationHash = contentHash(configuration);
|
|
15
|
+
const reserved = retained ? opened : await service.command(context, { action: "reserve_execution", id: request.id, expectedRevision: request.expectedRevision, operationId: `reserve-${request.operationId}`, generation: request.generation, runOperationId, configurationHash });
|
|
16
|
+
if (!reserved.execution || reserved.execution.operationId !== runOperationId || reserved.execution.configurationHash !== configurationHash)
|
|
17
|
+
throw new LearningDomainError("human_execution_reservation_conflict", 409);
|
|
18
|
+
const current = await service.get(context, request.id);
|
|
19
|
+
if (current.execution?.executionId) {
|
|
20
|
+
if (current.status === "cancelled")
|
|
21
|
+
await input.cancel(current);
|
|
22
|
+
return current;
|
|
23
|
+
}
|
|
24
|
+
let dispatched;
|
|
25
|
+
if (retained)
|
|
26
|
+
dispatched = await input.recover(current);
|
|
27
|
+
else {
|
|
28
|
+
if (current.generation !== reserved.generation || ["accepted", "rejected", "cancelled"].includes(current.status))
|
|
29
|
+
throw new LearningDomainError("human_execution_assignment_stale", 409);
|
|
30
|
+
try {
|
|
31
|
+
dispatched = await input.dispatch(configuration, current);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
dispatched = await input.recover(await service.get(context, request.id));
|
|
35
|
+
if (!dispatched)
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!dispatched)
|
|
40
|
+
throw new LearningDomainError("human_execution_admission_recovery_pending", 409, "The original execution admission is unavailable. Recover its retained owner outcome before retrying this assignment.");
|
|
41
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
42
|
+
const latest = await service.get(context, request.id);
|
|
43
|
+
if (latest.generation !== reserved.generation || !latest.execution || latest.execution.operationId !== runOperationId)
|
|
44
|
+
throw new LearningDomainError("human_execution_assignment_stale", 409);
|
|
45
|
+
let bound;
|
|
46
|
+
try {
|
|
47
|
+
bound = latest.execution.executionId ? latest : await service.command(context, { action: "bind_execution", id: request.id, expectedRevision: latest.revision, operationId: `bind-${runOperationId}`, generation: request.generation, runOperationId, configurationHash, executionId: dispatched.id });
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error instanceof LearningDomainError && error.code === "human_revision_conflict")
|
|
51
|
+
continue;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
if (bound.execution?.executionId !== dispatched.id)
|
|
55
|
+
throw new LearningDomainError("human_execution_binding_conflict", 409);
|
|
56
|
+
if (bound.status === "cancelled")
|
|
57
|
+
await input.cancel(bound);
|
|
58
|
+
return bound;
|
|
59
|
+
}
|
|
60
|
+
throw new LearningDomainError("human_execution_binding_recovery_pending", 409, "The assignment changed while retaining its execution. Retry the original operation to recover that exact run.");
|
|
61
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { contentHash } from "@openpond/harness";
|
|
2
|
+
import { HumanReviewSchema } from "./contracts.js";
|
|
3
|
+
import { LearningDomainError } from "../learning/errors.js";
|
|
4
|
+
/** Call only after current host authority/evidence checks. Acceptance grants no learning use or adoption. */
|
|
5
|
+
export function acceptedHumanEvidence(raw, expected) {
|
|
6
|
+
const review = HumanReviewSchema.parse(raw);
|
|
7
|
+
const { contentHash: hash, ...content } = review;
|
|
8
|
+
const decision = review.decisions.at(-1);
|
|
9
|
+
if (hash !== contentHash(content) || hash !== expected.reviewHash || review.status !== "accepted" || !decision || decision.outcome !== "accept" || decision.contentHash !== expected.decisionHash)
|
|
10
|
+
throw new LearningDomainError("human_accepted_revision_unavailable", 409);
|
|
11
|
+
const submissions = decision.submissions.map(ref => {
|
|
12
|
+
const submission = review.submissions.find(s => s.id === ref.id && s.contentHash === ref.contentHash);
|
|
13
|
+
if (!submission || submission.evidenceHash !== contentHash(review.evidence) || submission.formHash !== contentHash(review.form))
|
|
14
|
+
throw new LearningDomainError("human_accepted_lineage_invalid", 409);
|
|
15
|
+
return { id: submission.id, revision: submission.revision, contentHash: submission.contentHash };
|
|
16
|
+
});
|
|
17
|
+
return { schemaVersion: "openpond.acceptedHumanEvidence.v1", scope: review.scope, projectId: review.projectId, review: { id: review.id, revision: review.revision, contentHash: hash }, sourceOrigins: review.evidence.attempts.map(attempt => attempt.localSource ? attempt.localSource.kind : attempt.recordedSource ? "recorded_evidence" : "hosted_retained"), decision: { id: decision.id, revision: decision.revision, contentHash: decision.contentHash }, submissions, evidence: review.evidence, answers: decision.answers, ...(review.execution?.result ? { execution: { id: review.execution.executionId, result: review.execution.result, configurationHash: review.execution.configurationHash } } : {}), ...(decision.taskProposalHash ? { taskProposal: review.submissions.find(s => s.taskProposal?.contentHash === decision.taskProposalHash).taskProposal } : {}) };
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./contracts.js";
|
|
2
|
+
export * from "./repository.js";
|
|
3
|
+
export * from "./service.js";
|
|
4
|
+
export * from "./handoff.js";
|
|
5
|
+
export * from "./results.js";
|
|
6
|
+
export * from "./execution.js";
|
|
7
|
+
export * from "./task-proposals.js";
|
|
8
|
+
export * from "./local-publication.js";
|
|
9
|
+
export * from "./comparison.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { contentHash, ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema } from "@openpond/harness";
|
|
3
|
+
const Id = ReleaseIdSchema, Hash = ReleaseHashSchema;
|
|
4
|
+
const Release = z.object({ id: Id, revision: z.number().int().positive(), contentHash: Hash }).strict();
|
|
5
|
+
export const HumanLocalAttemptOriginSchema = z.object({ kind: z.literal("owner_attested_local"), ownerActorId: Id, teamId: Id, executionHash: Hash, configurationHash: Hash, retainedAttemptHash: Hash, traceHash: Hash, publicationCutoffHash: Hash }).strict();
|
|
6
|
+
export const HumanNativeAttemptOriginSchema = HumanLocalAttemptOriginSchema.omit({ publicationCutoffHash: true, kind: true }).extend({ kind: z.literal("native_local") }).strict();
|
|
7
|
+
const Attempt = z.object({ origin: HumanLocalAttemptOriginSchema, executionId: Id, receiptId: Id, taskId: Id, targetId: Id, familyKey: Id, split: z.enum(["train", "validation", "test", "frozen_eval"]), input: z.record(z.string(), z.unknown()), policyVisibleContext: z.record(z.string(), z.unknown()), output: z.string().nullable(), error: z.string().nullable(), events: z.array(z.record(z.string(), z.unknown())).max(1000), coverage: z.object({ sourceEventCount: z.number().int().nonnegative().max(20000), publishedEventCount: z.number().int().nonnegative().max(1000), traceLimited: z.boolean(), artifactsIncluded: z.literal(false) }).strict() }).strict().superRefine((value, context) => { if (value.events.length !== value.coverage.publishedEventCount || value.coverage.sourceEventCount < value.events.length || value.coverage.traceLimited !== (value.coverage.sourceEventCount > value.events.length))
|
|
8
|
+
context.addIssue({ code: "custom", message: "Publication coverage must describe its actual retained event population." }); if (contentHash(value.events) !== value.origin.publicationCutoffHash)
|
|
9
|
+
context.addIssue({ code: "custom", message: "Published trace cutoff changed." }); });
|
|
10
|
+
const Content = z.object({ schemaVersion: z.literal("openpond.humanLocalEvidenceExport.v1"), actorId: Id, scope: Id, projectId: Id, dataset: Release, graderId: Id, compiledGraderHash: Hash, attempts: z.array(Attempt).min(1).max(2), sealedAt: ReleaseTimestampSchema }).strict().superRefine((value, context) => { if (value.attempts.some(attempt => attempt.origin.ownerActorId !== value.actorId || attempt.origin.teamId !== value.scope))
|
|
11
|
+
context.addIssue({ code: "custom", message: "Local publication belongs to another actor/workspace." }); });
|
|
12
|
+
export const HumanLocalEvidenceExportSchema = Content.safeExtend({ contentHash: Hash }).strict().superRefine((value, context) => { const { contentHash: hash, ...content } = value; if (contentHash(content) !== hash)
|
|
13
|
+
context.addIssue({ code: "custom", message: "Local evidence export bytes changed." }); });
|
|
14
|
+
export const HumanExportLocalRequestSchema = z.object({ endpoint: z.literal("export_local"), scope: Id, projectId: Id, graderId: Id, selections: z.array(z.object({ executionId: Id, receiptId: Id }).strict()).min(1).max(2) }).strict();
|
|
15
|
+
export const HumanPublishLocalRequestSchema = z.object({ endpoint: z.literal("publish_local"), scope: Id, projectId: Id, operationId: Id, audience: z.array(Id).max(100), workspaceVisible: z.boolean(), evidence: HumanLocalEvidenceExportSchema }).strict();
|
|
16
|
+
export const HumanShareLocalRequestSchema = HumanExportLocalRequestSchema.omit({ endpoint: true }).extend({ endpoint: z.literal("share_local"), operationId: Id, audience: z.array(Id).max(100), workspaceVisible: z.boolean() }).strict();
|
|
17
|
+
export const HumanLocalPublicationsReadSchema = z.object({ endpoint: z.literal("local_publications"), scope: Id, executionId: Id }).strict();
|
|
18
|
+
export const HumanRevokeLocalPublicationSchema = z.object({ endpoint: z.literal("revoke_local"), scope: Id, publication: Release }).strict();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { contentHash } from "@openpond/harness";
|
|
3
|
+
import { HumanAnswerSchema, HumanReviewSchema, ReviewReleaseSchema, sealHumanRecord } from "./contracts.js";
|
|
4
|
+
import { LearningDomainError } from "../learning/errors.js";
|
|
5
|
+
const Content = z.object({ schemaVersion: z.literal("openpond.humanResultView.v1"), executionId: z.string().min(1), executionHash: z.string().regex(/^[a-f0-9]{64}$/), selection: z.array(ReviewReleaseSchema).max(10000), coverage: z.object({ pending: z.number().int().nonnegative(), submitted: z.number().int().nonnegative(), accepted: z.number().int().nonnegative(), rejected: z.number().int().nonnegative(), cancelled: z.number().int().nonnegative() }).strict(), results: z.array(z.object({ review: ReviewReleaseSchema, decision: ReviewReleaseSchema.nullable(), attemptIds: z.array(z.string().min(1)).min(1).max(2), mode: z.enum(["individual", "pairwise"]), origin: z.enum(["hosted_retained", "recorded_evidence", "owner_attested_local", "native_local"]), status: z.enum(["pending", "submitted", "accepted", "rejected", "cancelled"]), grader: ReviewReleaseSchema, answers: z.array(HumanAnswerSchema).max(100) }).strict()).max(10000) }).strict();
|
|
6
|
+
export const HumanResultViewSchema = Content.extend({ contentHash: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
|
|
7
|
+
/** A content-addressed view retains selected review revisions; automatic execution receipts remain untouched. */
|
|
8
|
+
export function createHumanResultView(input) {
|
|
9
|
+
const selected = input.reviews.map(raw => { const r = HumanReviewSchema.parse(raw), { contentHash: hash, ...body } = r; if (contentHash(body) !== hash || !r.evidence.attempts.some(a => a.experimentId === input.executionId))
|
|
10
|
+
throw new LearningDomainError("human_result_selection_mismatch", 409); return r; });
|
|
11
|
+
if (new Set(selected.map(r => r.id)).size !== selected.length)
|
|
12
|
+
throw new LearningDomainError("human_result_duplicate_review", 422);
|
|
13
|
+
const results = selected.map(r => { const d = r.decisions.at(-1); return { review: { id: r.id, revision: r.revision, contentHash: r.contentHash }, decision: d ? { id: d.id, revision: d.revision, contentHash: d.contentHash } : null, attemptIds: r.evidence.attempts.map(a => a.attemptId), mode: r.form.mode, origin: r.evidence.attempts.some(attempt => attempt.localSource?.kind === "owner_attested_local") ? "owner_attested_local" : r.evidence.attempts.some(attempt => attempt.localSource?.kind === "native_local") ? "native_local" : r.evidence.attempts.some(attempt => attempt.recordedSource) ? "recorded_evidence" : "hosted_retained", status: r.status === "accepted" ? "accepted" : r.status === "rejected" ? "rejected" : r.status === "cancelled" ? "cancelled" : r.status === "submitted" ? "submitted" : "pending", grader: r.evidence.grader, answers: r.status === "accepted" && d?.outcome === "accept" ? d.answers : [] }; });
|
|
14
|
+
const coverage = { pending: 0, submitted: 0, accepted: 0, rejected: 0, cancelled: 0 };
|
|
15
|
+
for (const r of results)
|
|
16
|
+
coverage[r.status]++;
|
|
17
|
+
return HumanResultViewSchema.parse(sealHumanRecord(Content.parse({ schemaVersion: "openpond.humanResultView.v1", executionId: input.executionId, executionHash: input.executionHash, selection: results.map(r => r.review), coverage, results })));
|
|
18
|
+
}
|