@openpond/evals 0.5.0 → 0.6.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.
- package/CONTRACT.md +13 -1
- package/README.md +26 -2
- package/conformance/telemetry/v1/invalid-batch.json +17 -0
- package/conformance/telemetry/v1/valid-batch.json +58 -0
- package/dist/index.js +5 -0
- package/dist/learned-preference.js +334 -0
- package/dist/preferences.js +77 -11
- package/dist/telemetry/index.js +4 -0
- package/dist/telemetry-analysis.js +183 -0
- package/dist/telemetry-bundle.js +60 -0
- package/dist/telemetry-catalog.js +50 -0
- package/dist/telemetry.js +112 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/learned-preference.d.ts +282 -0
- package/dist/types/learned-preference.d.ts.map +1 -0
- package/dist/types/preferences.d.ts +52 -4
- package/dist/types/preferences.d.ts.map +1 -1
- package/dist/types/telemetry/index.d.ts +5 -0
- package/dist/types/telemetry/index.d.ts.map +1 -0
- package/dist/types/telemetry-analysis.d.ts +116 -0
- package/dist/types/telemetry-analysis.d.ts.map +1 -0
- package/dist/types/telemetry-bundle.d.ts +309 -0
- package/dist/types/telemetry-bundle.d.ts.map +1 -0
- package/dist/types/telemetry-catalog.d.ts +269 -0
- package/dist/types/telemetry-catalog.d.ts.map +1 -0
- package/dist/types/telemetry.d.ts +266 -0
- package/dist/types/telemetry.d.ts.map +1 -0
- package/package.json +12 -2
- package/schemas/telemetry/v1/evidence-completeness.schema.json +82 -0
- package/schemas/telemetry/v1/evidence-reference.schema.json +41 -0
- package/schemas/telemetry/v1/metric-definition.schema.json +96 -0
- package/schemas/telemetry/v1/metric-observation.schema.json +182 -0
- package/schemas/telemetry/v1/run-metric-summary.schema.json +98 -0
- package/schemas/telemetry/v1/run-telemetry-batch.schema.json +405 -0
- package/schemas/telemetry/v1/run-telemetry-event.schema.json +201 -0
- package/schemas/telemetry/v1/telemetry-cohort.schema.json +100 -0
- package/schemas/telemetry/v1/telemetry-export-bundle.schema.json +655 -0
package/CONTRACT.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
model-driven refinement/review policy, improvements, traces, tools, model
|
|
5
5
|
identities, and shared hashing.
|
|
6
6
|
`@openpond/evals` owns Tasksets, graders, evaluation runs and receipts,
|
|
7
|
-
execution orchestration, conformance fixtures,
|
|
7
|
+
execution orchestration, conformance fixtures, Work-evidence eligibility, and
|
|
8
|
+
portable Run telemetry/metric semantics.
|
|
8
9
|
Evals depends on Harness for exact release identities but does not re-export
|
|
9
10
|
Harness APIs; Harness never depends on Evals. Host applications own persistence, provider sessions, secret
|
|
10
11
|
leases, connected-app authorization, model streaming, artifact bytes, and
|
|
@@ -26,6 +27,8 @@ runtime processes.
|
|
|
26
27
|
| user feedback on Work output | `WorkFeedbackReceipt` | Append a new receipt bound to the evidence receipt and, when selected, the exact content-addressed output-revision descriptor. Corrections are separate artifacts and never mutate prior receipts. |
|
|
27
28
|
| bounded cross-Work review | `HarnessEvaluationReviewReceipt` | Select only currently authorized immutable evidence, advance one watermark, group one stable claim, route to the smallest correct layer, and name the next authority without performing downstream effects. |
|
|
28
29
|
| model-improvement qualification | `ModelImprovementQualificationReceipt` | Bind the originating review, exact Harness, Taskset, real baseline Evaluation, Model, Environment/tool/permission/policy hashes, Verifier, source policies, privacy, budget, and non-frozen signal. Weak or confounded evidence emits `no_training`; training and activation remain host effects. |
|
|
30
|
+
| trainer/runtime telemetry | `RunTelemetryEvent` + `MetricObservation` | Emit compact ordered events and observations with exact Run lineage, bounded attributes/dimensions, source authority, visibility, and stable idempotency identity. Tenant identity, provider credentials, storage, retention, billing, and durable indexes remain host projections. |
|
|
31
|
+
| local/hosted Run investigation export | `TelemetryExportBundle` | Export metric definitions, ordered evidence, bounded references, completeness, and a content hash. Apply visibility and redaction before crossing authority boundaries; never include raw privileged trace bytes. |
|
|
29
32
|
|
|
30
33
|
## Compatibility policy
|
|
31
34
|
|
|
@@ -48,6 +51,15 @@ runtime processes.
|
|
|
48
51
|
immutable objects and rejects lifecycle, tool, grader-interface, or required
|
|
49
52
|
Environment-tool drift before issuing the receipt.
|
|
50
53
|
- The initial support target is Node.js ESM on Node 22.14 through Node 24.
|
|
54
|
+
- Telemetry schema literals are shared across `@openpond/evals/telemetry` and
|
|
55
|
+
the `openpond-evals` Python distribution. Generated JSON Schemas and positive
|
|
56
|
+
and negative fixtures are the cross-language conformance authority.
|
|
57
|
+
- Telemetry producer sequence is monotonic within a Run. Receivers deduplicate
|
|
58
|
+
exact retries, accept late delivery, and reject conflicting reuse of an
|
|
59
|
+
idempotency key or `(runId, sequence)` pair.
|
|
60
|
+
- Core metrics reject unknown dimensions. Taskset- or Environment-specific
|
|
61
|
+
extensions require an explicit `MetricDefinition`; arbitrary metric names or
|
|
62
|
+
unbounded labels are not portable telemetry.
|
|
51
63
|
- Portable paths are relative and at most 2,000 characters. Individual assets
|
|
52
64
|
are at most 250 MB. Tasksets, traces, and evidence arrays have schema-level
|
|
53
65
|
upper bounds.
|
package/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Portable evaluation and benchmark contracts plus pure helpers for Tasksets,
|
|
4
4
|
graders, run manifests, attempt and evaluation receipts, paired benchmark
|
|
5
5
|
comparisons, execution adapters, conformance fixtures, Work-evidence
|
|
6
|
-
eligibility,
|
|
6
|
+
eligibility, no-training/SFT/preference/RL qualification receipts, and portable
|
|
7
|
+
training telemetry. The package depends on
|
|
7
8
|
[`@openpond/harness`](../harness/README.md) for exact Harness identities but
|
|
8
9
|
does not re-export Harness APIs. Applications import the two packages directly,
|
|
9
10
|
which keeps refinement and evaluation authority visibly separate.
|
|
@@ -34,7 +35,7 @@ import {
|
|
|
34
35
|
```
|
|
35
36
|
|
|
36
37
|
Subpath exports are available at `/harness`, `/tasksets`, `/benchmarks`, `/graders`, `/runs`,
|
|
37
|
-
`/conformance`, `/evidence`, `/preferences`, `/review`, and
|
|
38
|
+
`/conformance`, `/evidence`, `/telemetry`, `/preferences`, `/review`, and
|
|
38
39
|
`/model-improvement-qualification`. The package is an evaluation protocol library,
|
|
39
40
|
not a hosted client. It does not execute OpenPond Desktop or Sandbox sessions,
|
|
40
41
|
resolve credentials, or persist artifacts.
|
|
@@ -122,6 +123,29 @@ semantics. Infrastructure failures must remain reward-ineligible.
|
|
|
122
123
|
Package semver and schema literals are independent. See [CONTRACT.md](./CONTRACT.md)
|
|
123
124
|
for compatibility aliases, migration rules, size limits, and the field map.
|
|
124
125
|
|
|
126
|
+
## Training telemetry
|
|
127
|
+
|
|
128
|
+
The `@openpond/evals/telemetry` subpath defines the cross-runtime Run event,
|
|
129
|
+
metric, cohort, evidence-completeness, and export-bundle protocol. It includes a
|
|
130
|
+
bounded core metric catalog, deterministic builders, duplicate and late-delivery
|
|
131
|
+
merge semantics, chart-ready aggregation, cohort filtering, and
|
|
132
|
+
visibility-aware export helpers. The sibling `openpond-evals` Python
|
|
133
|
+
distribution implements the producer-facing models, builders, and asynchronous
|
|
134
|
+
buffered emitter used by GPU workers and external trainers; both languages
|
|
135
|
+
validate the same fixtures and schema literals.
|
|
136
|
+
|
|
137
|
+
Telemetry records what an admitted Run did; it does not configure the Taskset
|
|
138
|
+
or execute training. Core observations are accepted only when their metric ID
|
|
139
|
+
and bounded dimensions match the catalog. Custom metrics require an explicit
|
|
140
|
+
versioned definition. Events carry immutable Run/Model/Harness/Taskset lineage,
|
|
141
|
+
producer sequence, source authority, and evidence visibility.
|
|
142
|
+
|
|
143
|
+
Portable export bundles contain definitions, events, observations, bounded
|
|
144
|
+
evidence references, completeness state, and a content hash. Raw trace bytes,
|
|
145
|
+
credentials, provider handles, tenant identity, billing policy, and hosted
|
|
146
|
+
indexes remain host-owned. The package contains no trainer, optimizer,
|
|
147
|
+
provisioner, persistence client, or diagnostic agent.
|
|
148
|
+
|
|
125
149
|
## Release preparation
|
|
126
150
|
|
|
127
151
|
```bash
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "openpond.runTelemetryBatch.v1",
|
|
3
|
+
"events": [
|
|
4
|
+
{
|
|
5
|
+
"schemaVersion": "openpond.runTelemetryEvent.v1",
|
|
6
|
+
"eventId": "event-invalid",
|
|
7
|
+
"sequence": -1,
|
|
8
|
+
"occurredAt": "not-a-timestamp",
|
|
9
|
+
"source": "unknown_source",
|
|
10
|
+
"type": "optimizer_step_completed",
|
|
11
|
+
"visibility": "team_visible",
|
|
12
|
+
"lineage": {},
|
|
13
|
+
"attributes": {}
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"observations": []
|
|
17
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "openpond.runTelemetryBatch.v1",
|
|
3
|
+
"events": [
|
|
4
|
+
{
|
|
5
|
+
"schemaVersion": "openpond.runTelemetryEvent.v1",
|
|
6
|
+
"eventId": "event-optimizer-step-1",
|
|
7
|
+
"sequence": 1,
|
|
8
|
+
"occurredAt": "2026-08-25T20:00:00.000Z",
|
|
9
|
+
"source": "optimizer",
|
|
10
|
+
"type": "optimizer_step_completed",
|
|
11
|
+
"visibility": "team_visible",
|
|
12
|
+
"lineage": {
|
|
13
|
+
"modelProjectId": "project-1",
|
|
14
|
+
"runId": "run-1",
|
|
15
|
+
"modelVersionId": "version-1",
|
|
16
|
+
"harnessReleaseHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
17
|
+
"tasksetReleaseHash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
18
|
+
"environmentReleaseHash": null,
|
|
19
|
+
"checkpointId": null,
|
|
20
|
+
"step": 1,
|
|
21
|
+
"rolloutGroupId": "group-1",
|
|
22
|
+
"attemptId": null,
|
|
23
|
+
"scenarioId": "scenario-1"
|
|
24
|
+
},
|
|
25
|
+
"attributes": {
|
|
26
|
+
"learningRate": 0.00001,
|
|
27
|
+
"runner": "openpond_direct_grpo_v1"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"observations": [
|
|
32
|
+
{
|
|
33
|
+
"schemaVersion": "openpond.metricObservation.v1",
|
|
34
|
+
"observationId": "observation-learning-rate-1",
|
|
35
|
+
"metricId": "optimizer.learning_rate",
|
|
36
|
+
"eventId": "event-optimizer-step-1",
|
|
37
|
+
"sequence": 2,
|
|
38
|
+
"observedAt": "2026-08-25T20:00:00.000Z",
|
|
39
|
+
"value": 0.00001,
|
|
40
|
+
"lineage": {
|
|
41
|
+
"modelProjectId": "project-1",
|
|
42
|
+
"runId": "run-1",
|
|
43
|
+
"modelVersionId": "version-1",
|
|
44
|
+
"harnessReleaseHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
45
|
+
"tasksetReleaseHash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
46
|
+
"environmentReleaseHash": null,
|
|
47
|
+
"checkpointId": null,
|
|
48
|
+
"step": 1,
|
|
49
|
+
"rolloutGroupId": "group-1",
|
|
50
|
+
"attemptId": null,
|
|
51
|
+
"scenarioId": "scenario-1"
|
|
52
|
+
},
|
|
53
|
+
"dimensions": {
|
|
54
|
+
"split": "train"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,9 +6,14 @@ export * from "./execution-contracts.js";
|
|
|
6
6
|
export * from "./execution-receipts.js";
|
|
7
7
|
export * from "./graders.js";
|
|
8
8
|
export * from "./harness.js";
|
|
9
|
+
export * from "./learned-preference.js";
|
|
9
10
|
export * from "./runs.js";
|
|
10
11
|
export * from "./model-improvement-qualification.js";
|
|
11
12
|
export * from "./preferences.js";
|
|
12
13
|
export * from "./review-conformance.js";
|
|
13
14
|
export * from "./rollouts.js";
|
|
15
|
+
export * from "./telemetry.js";
|
|
16
|
+
export * from "./telemetry-catalog.js";
|
|
17
|
+
export * from "./telemetry-analysis.js";
|
|
18
|
+
export * from "./telemetry-bundle.js";
|
|
14
19
|
export * from "./tasksets.js";
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ImmutableReleaseRefSchema, MetadataSchema, ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema, contentHash, } from "@openpond/harness";
|
|
3
|
+
import { verifyComparisonAssignment, verifyPreferenceReceipt, } from "./preferences.js";
|
|
4
|
+
const PreferenceDatasetPartitionSchema = z.enum([
|
|
5
|
+
"reward_train",
|
|
6
|
+
"reward_validation",
|
|
7
|
+
"reward_qualification",
|
|
8
|
+
]);
|
|
9
|
+
export const PreferenceEvidenceAuthoritySchema = z.enum([
|
|
10
|
+
"human",
|
|
11
|
+
"synthetic_fixture",
|
|
12
|
+
]);
|
|
13
|
+
export const RewardModelQualificationKindSchema = z.enum([
|
|
14
|
+
"synthetic_smoke",
|
|
15
|
+
"human_heldout",
|
|
16
|
+
]);
|
|
17
|
+
const PreferenceDatasetGroupSchema = z.object({
|
|
18
|
+
id: ReleaseIdSchema,
|
|
19
|
+
assignmentRef: ImmutableReleaseRefSchema,
|
|
20
|
+
scenarioRef: ImmutableReleaseRefSchema,
|
|
21
|
+
scenarioSplit: z.enum(["train", "validation"]),
|
|
22
|
+
preferenceResultRef: ImmutableReleaseRefSchema,
|
|
23
|
+
receiptRefs: z.array(ImmutableReleaseRefSchema).min(1).max(100),
|
|
24
|
+
attemptRefs: z.array(ImmutableReleaseRefSchema).min(2).max(4),
|
|
25
|
+
artifactManifestRefs: z.array(ImmutableReleaseRefSchema).min(2).max(4),
|
|
26
|
+
orderedBuckets: z.array(z.array(ReleaseIdSchema).min(1).max(4)).max(4),
|
|
27
|
+
rejectAll: z.boolean(),
|
|
28
|
+
partition: PreferenceDatasetPartitionSchema,
|
|
29
|
+
metadata: MetadataSchema,
|
|
30
|
+
}).strict().superRefine((group, context) => {
|
|
31
|
+
const attemptIds = group.attemptRefs.map((attempt) => attempt.id);
|
|
32
|
+
if (new Set(attemptIds).size !== attemptIds.length) {
|
|
33
|
+
context.addIssue({
|
|
34
|
+
code: "custom",
|
|
35
|
+
path: ["attemptRefs"],
|
|
36
|
+
message: "Preference dataset group Attempt refs must be unique.",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (group.artifactManifestRefs.length !== group.attemptRefs.length) {
|
|
40
|
+
context.addIssue({
|
|
41
|
+
code: "custom",
|
|
42
|
+
path: ["artifactManifestRefs"],
|
|
43
|
+
message: "Every preference dataset Attempt requires one Artifact Manifest ref.",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const bucketIds = group.orderedBuckets.flat();
|
|
47
|
+
if (group.rejectAll && bucketIds.length > 0) {
|
|
48
|
+
context.addIssue({
|
|
49
|
+
code: "custom",
|
|
50
|
+
path: ["orderedBuckets"],
|
|
51
|
+
message: "A reject-all preference dataset group cannot contain ordered buckets.",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (!group.rejectAll
|
|
55
|
+
&& (bucketIds.length !== attemptIds.length
|
|
56
|
+
|| new Set(bucketIds).size !== bucketIds.length
|
|
57
|
+
|| bucketIds.some((attemptId) => !attemptIds.includes(attemptId)))) {
|
|
58
|
+
context.addIssue({
|
|
59
|
+
code: "custom",
|
|
60
|
+
path: ["orderedBuckets"],
|
|
61
|
+
message: "Ordered buckets must contain every group Attempt exactly once.",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const PreferenceDatasetPairSchema = z.object({
|
|
66
|
+
groupId: ReleaseIdSchema,
|
|
67
|
+
preferredAttemptRef: ImmutableReleaseRefSchema,
|
|
68
|
+
dispreferredAttemptRef: ImmutableReleaseRefSchema,
|
|
69
|
+
relation: z.enum(["preferred", "tie"]),
|
|
70
|
+
}).strict().superRefine((pair, context) => {
|
|
71
|
+
if (pair.preferredAttemptRef.id === pair.dispreferredAttemptRef.id
|
|
72
|
+
&& pair.preferredAttemptRef.contentHash === pair.dispreferredAttemptRef.contentHash) {
|
|
73
|
+
context.addIssue({
|
|
74
|
+
code: "custom",
|
|
75
|
+
message: "A derived preference pair must reference two different Attempts.",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
export const PreferenceDatasetReleaseContentSchema = z.object({
|
|
80
|
+
schemaVersion: z.literal("openpond.preferenceDatasetRelease.v1"),
|
|
81
|
+
id: ReleaseIdSchema,
|
|
82
|
+
revision: z.number().int().positive(),
|
|
83
|
+
tasksetRelease: ImmutableReleaseRefSchema,
|
|
84
|
+
comparisonRelease: ImmutableReleaseRefSchema,
|
|
85
|
+
authority: PreferenceEvidenceAuthoritySchema,
|
|
86
|
+
qualificationEligibility: z.enum(["smoke_only", "human_heldout"]),
|
|
87
|
+
fixtureRelease: ImmutableReleaseRefSchema.nullable(),
|
|
88
|
+
groups: z.array(PreferenceDatasetGroupSchema).min(1).max(100_000),
|
|
89
|
+
derivedPairs: z.array(PreferenceDatasetPairSchema).max(1_000_000),
|
|
90
|
+
createdAt: ReleaseTimestampSchema,
|
|
91
|
+
metadata: MetadataSchema,
|
|
92
|
+
}).strict().superRefine((release, context) => {
|
|
93
|
+
if (release.authority === "synthetic_fixture"
|
|
94
|
+
&& (release.qualificationEligibility !== "smoke_only"
|
|
95
|
+
|| release.fixtureRelease === null)) {
|
|
96
|
+
context.addIssue({
|
|
97
|
+
code: "custom",
|
|
98
|
+
path: ["qualificationEligibility"],
|
|
99
|
+
message: "Synthetic preference datasets require a fixture release and are smoke-only.",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (release.authority === "human"
|
|
103
|
+
&& (release.qualificationEligibility !== "human_heldout"
|
|
104
|
+
|| release.fixtureRelease !== null)) {
|
|
105
|
+
context.addIssue({
|
|
106
|
+
code: "custom",
|
|
107
|
+
path: ["authority"],
|
|
108
|
+
message: "Human preference datasets must be human-heldout eligible and cannot bind a fixture release.",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const groupIds = release.groups.map((group) => group.id);
|
|
112
|
+
if (new Set(groupIds).size !== groupIds.length) {
|
|
113
|
+
context.addIssue({
|
|
114
|
+
code: "custom",
|
|
115
|
+
path: ["groups"],
|
|
116
|
+
message: "Preference dataset group IDs must be unique.",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const groups = new Map(release.groups.map((group) => [group.id, group]));
|
|
120
|
+
for (const [index, pair] of release.derivedPairs.entries()) {
|
|
121
|
+
const group = groups.get(pair.groupId);
|
|
122
|
+
if (!group) {
|
|
123
|
+
context.addIssue({
|
|
124
|
+
code: "custom",
|
|
125
|
+
path: ["derivedPairs", index, "groupId"],
|
|
126
|
+
message: "Derived preference pairs must reference a group in the same release.",
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const attemptIds = new Set(group.attemptRefs.map((attempt) => attempt.id));
|
|
131
|
+
if (!attemptIds.has(pair.preferredAttemptRef.id)
|
|
132
|
+
|| !attemptIds.has(pair.dispreferredAttemptRef.id)) {
|
|
133
|
+
context.addIssue({
|
|
134
|
+
code: "custom",
|
|
135
|
+
path: ["derivedPairs", index],
|
|
136
|
+
message: "Derived preference pairs must reference Attempts in their source group.",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
export const PreferenceDatasetReleaseSchema = PreferenceDatasetReleaseContentSchema
|
|
142
|
+
.extend({ contentHash: ReleaseHashSchema })
|
|
143
|
+
.strict();
|
|
144
|
+
const RewardModelQualificationMetricsSchema = z.object({
|
|
145
|
+
sampleCount: z.number().int().positive(),
|
|
146
|
+
finiteScoreRate: z.number().min(0).max(1),
|
|
147
|
+
scoreVariance: z.number().nonnegative(),
|
|
148
|
+
checkpointReloadPassed: z.boolean(),
|
|
149
|
+
processorCompatibilityPassed: z.boolean(),
|
|
150
|
+
invalidAttemptExclusionPassed: z.boolean(),
|
|
151
|
+
orderedPairAccuracy: z.number().min(0).max(1).nullable(),
|
|
152
|
+
bucketAccuracy: z.number().min(0).max(1).nullable(),
|
|
153
|
+
tieAgreement: z.number().min(0).max(1).nullable(),
|
|
154
|
+
}).strict();
|
|
155
|
+
export const RewardModelQualificationReportContentSchema = z.object({
|
|
156
|
+
schemaVersion: z.literal("openpond.rewardModelQualificationReport.v1"),
|
|
157
|
+
id: ReleaseIdSchema,
|
|
158
|
+
kind: RewardModelQualificationKindSchema,
|
|
159
|
+
rewardModelVersion: ImmutableReleaseRefSchema,
|
|
160
|
+
preferenceDatasetRelease: ImmutableReleaseRefSchema,
|
|
161
|
+
tasksetRelease: ImmutableReleaseRefSchema,
|
|
162
|
+
processorRelease: ImmutableReleaseRefSchema,
|
|
163
|
+
metrics: RewardModelQualificationMetricsSchema,
|
|
164
|
+
passed: z.boolean(),
|
|
165
|
+
productionRewardEligible: z.boolean(),
|
|
166
|
+
createdAt: ReleaseTimestampSchema,
|
|
167
|
+
metadata: MetadataSchema,
|
|
168
|
+
}).strict().superRefine((report, context) => {
|
|
169
|
+
if (report.kind === "synthetic_smoke" && report.productionRewardEligible) {
|
|
170
|
+
context.addIssue({
|
|
171
|
+
code: "custom",
|
|
172
|
+
path: ["productionRewardEligible"],
|
|
173
|
+
message: "Synthetic-smoke Reward Model qualification can never grant production reward eligibility.",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (report.kind === "human_heldout"
|
|
177
|
+
&& report.productionRewardEligible !== report.passed) {
|
|
178
|
+
context.addIssue({
|
|
179
|
+
code: "custom",
|
|
180
|
+
path: ["productionRewardEligible"],
|
|
181
|
+
message: "Human-heldout reward eligibility must match the frozen qualification outcome.",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
export const RewardModelQualificationReportSchema = RewardModelQualificationReportContentSchema
|
|
186
|
+
.extend({ contentHash: ReleaseHashSchema })
|
|
187
|
+
.strict();
|
|
188
|
+
export function createPreferenceDatasetRelease(input) {
|
|
189
|
+
const content = PreferenceDatasetReleaseContentSchema.parse(input);
|
|
190
|
+
return PreferenceDatasetReleaseSchema.parse({
|
|
191
|
+
...content,
|
|
192
|
+
contentHash: contentHash(content),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
export function materializePreferenceDatasetRelease(input) {
|
|
196
|
+
if (input.groups.length === 0) {
|
|
197
|
+
throw new Error("Preference dataset materialization requires at least one reviewed group.");
|
|
198
|
+
}
|
|
199
|
+
const tasksetRef = releaseRef(input.tasksetRelease);
|
|
200
|
+
const comparisonRef = releaseRef(input.comparisonRelease);
|
|
201
|
+
const fixtureRefs = new Map();
|
|
202
|
+
const groups = input.groups.map(({ assignment, receipt, partition }) => {
|
|
203
|
+
if (!verifyComparisonAssignment(assignment)) {
|
|
204
|
+
throw new Error("Preference dataset assignment failed immutable verification.");
|
|
205
|
+
}
|
|
206
|
+
if (!verifyPreferenceReceipt(receipt)) {
|
|
207
|
+
throw new Error("Preference dataset receipt failed immutable verification.");
|
|
208
|
+
}
|
|
209
|
+
if (!sameReleaseRef(assignment.lineage.tasksetRelease, tasksetRef)) {
|
|
210
|
+
throw new Error("Preference dataset assignment belongs to a different Taskset release.");
|
|
211
|
+
}
|
|
212
|
+
if (!sameReleaseRef(assignment.comparisonRelease, comparisonRef)) {
|
|
213
|
+
throw new Error("Preference dataset assignment belongs to a different comparison release.");
|
|
214
|
+
}
|
|
215
|
+
if (!sameReleaseRef(receipt.assignmentRef, releaseRef(assignment))) {
|
|
216
|
+
throw new Error("Preference dataset receipt belongs to a different assignment.");
|
|
217
|
+
}
|
|
218
|
+
if (input.authority === "synthetic_fixture") {
|
|
219
|
+
if (receipt.reviewer.kind !== "fixture") {
|
|
220
|
+
throw new Error("Synthetic preference datasets accept only fixture receipts.");
|
|
221
|
+
}
|
|
222
|
+
fixtureRefs.set(receipt.reviewer.fixtureRelease.contentHash, receipt.reviewer.fixtureRelease);
|
|
223
|
+
}
|
|
224
|
+
else if (receipt.reviewer.kind !== "human") {
|
|
225
|
+
throw new Error("Human preference datasets accept only human receipts.");
|
|
226
|
+
}
|
|
227
|
+
const scenario = input.tasksetRelease.tasks.find((task) => task.id === assignment.taskRef.id);
|
|
228
|
+
if (!scenario || (scenario.split !== "train" && scenario.split !== "validation")) {
|
|
229
|
+
throw new Error("Preference model datasets require train or validation scenarios.");
|
|
230
|
+
}
|
|
231
|
+
if (partition === "reward_train" && scenario.split !== "train") {
|
|
232
|
+
throw new Error("Reward training groups must use train scenarios.");
|
|
233
|
+
}
|
|
234
|
+
if (partition !== "reward_train" && scenario.split !== "validation") {
|
|
235
|
+
throw new Error("Reward validation and qualification groups must use validation scenarios.");
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
id: `preference-group:${assignment.id}`,
|
|
239
|
+
assignmentRef: releaseRef(assignment),
|
|
240
|
+
scenarioRef: assignment.taskRef,
|
|
241
|
+
scenarioSplit: scenario.split,
|
|
242
|
+
preferenceResultRef: releaseRef(receipt),
|
|
243
|
+
receiptRefs: [releaseRef(receipt)],
|
|
244
|
+
attemptRefs: assignment.candidates.map((candidate) => candidate.attemptRef),
|
|
245
|
+
artifactManifestRefs: assignment.candidates.map((candidate) => candidate.artifactManifestRef),
|
|
246
|
+
orderedBuckets: receipt.order,
|
|
247
|
+
rejectAll: receipt.rejectAll,
|
|
248
|
+
partition,
|
|
249
|
+
metadata: { purpose: assignment.purpose, ...assignment.metadata },
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
if (input.authority === "synthetic_fixture" && fixtureRefs.size !== 1) {
|
|
253
|
+
throw new Error("Synthetic preference dataset groups must share one immutable fixture release.");
|
|
254
|
+
}
|
|
255
|
+
const fixtureRelease = input.authority === "synthetic_fixture"
|
|
256
|
+
? [...fixtureRefs.values()][0]
|
|
257
|
+
: null;
|
|
258
|
+
return createPreferenceDatasetRelease({
|
|
259
|
+
schemaVersion: "openpond.preferenceDatasetRelease.v1",
|
|
260
|
+
id: input.id,
|
|
261
|
+
revision: input.revision,
|
|
262
|
+
tasksetRelease: tasksetRef,
|
|
263
|
+
comparisonRelease: comparisonRef,
|
|
264
|
+
authority: input.authority,
|
|
265
|
+
qualificationEligibility: input.authority === "synthetic_fixture" ? "smoke_only" : "human_heldout",
|
|
266
|
+
fixtureRelease,
|
|
267
|
+
groups,
|
|
268
|
+
derivedPairs: groups.flatMap(derivePairs),
|
|
269
|
+
createdAt: input.createdAt,
|
|
270
|
+
metadata: input.metadata ?? {},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
export function verifyPreferenceDatasetRelease(value) {
|
|
274
|
+
return verifyHashed(value, PreferenceDatasetReleaseContentSchema, PreferenceDatasetReleaseSchema);
|
|
275
|
+
}
|
|
276
|
+
export function createRewardModelQualificationReport(input) {
|
|
277
|
+
const content = RewardModelQualificationReportContentSchema.parse(input);
|
|
278
|
+
return RewardModelQualificationReportSchema.parse({
|
|
279
|
+
...content,
|
|
280
|
+
contentHash: contentHash(content),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
export function verifyRewardModelQualificationReport(value) {
|
|
284
|
+
return verifyHashed(value, RewardModelQualificationReportContentSchema, RewardModelQualificationReportSchema);
|
|
285
|
+
}
|
|
286
|
+
function verifyHashed(value, contentSchema, fullSchema) {
|
|
287
|
+
const parsed = fullSchema.safeParse(value);
|
|
288
|
+
if (!parsed.success || !parsed.data)
|
|
289
|
+
return false;
|
|
290
|
+
const { contentHash: actual, ...content } = parsed.data;
|
|
291
|
+
try {
|
|
292
|
+
return contentHash(contentSchema.parse(content)) === actual;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function derivePairs(group) {
|
|
299
|
+
if (group.rejectAll)
|
|
300
|
+
return [];
|
|
301
|
+
const attemptById = new Map(group.attemptRefs.map((attempt) => [attempt.id, attempt]));
|
|
302
|
+
const pairs = [];
|
|
303
|
+
for (const [bucketIndex, bucket] of group.orderedBuckets.entries()) {
|
|
304
|
+
for (let left = 0; left < bucket.length; left += 1) {
|
|
305
|
+
for (let right = left + 1; right < bucket.length; right += 1) {
|
|
306
|
+
pairs.push({
|
|
307
|
+
groupId: group.id,
|
|
308
|
+
preferredAttemptRef: attemptById.get(bucket[left]),
|
|
309
|
+
dispreferredAttemptRef: attemptById.get(bucket[right]),
|
|
310
|
+
relation: "tie",
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const lowerBucket of group.orderedBuckets.slice(bucketIndex + 1)) {
|
|
315
|
+
for (const preferredId of bucket) {
|
|
316
|
+
for (const dispreferredId of lowerBucket) {
|
|
317
|
+
pairs.push({
|
|
318
|
+
groupId: group.id,
|
|
319
|
+
preferredAttemptRef: attemptById.get(preferredId),
|
|
320
|
+
dispreferredAttemptRef: attemptById.get(dispreferredId),
|
|
321
|
+
relation: "preferred",
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return pairs;
|
|
328
|
+
}
|
|
329
|
+
function releaseRef(value) {
|
|
330
|
+
return { id: value.id, contentHash: value.contentHash };
|
|
331
|
+
}
|
|
332
|
+
function sameReleaseRef(left, right) {
|
|
333
|
+
return left.id === right.id && left.contentHash === right.contentHash;
|
|
334
|
+
}
|
package/dist/preferences.js
CHANGED
|
@@ -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).
|
|
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:
|
|
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
|
|
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
|
|
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 &&
|
|
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
|
|
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
|
|
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)
|