@openpond/evals 0.4.2 → 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.
Files changed (64) hide show
  1. package/CONTRACT.md +27 -1
  2. package/README.md +31 -2
  3. package/conformance/telemetry/v1/invalid-batch.json +17 -0
  4. package/conformance/telemetry/v1/valid-batch.json +58 -0
  5. package/dist/artifact-verification.js +172 -0
  6. package/dist/builtin-benchmarks/harness-refiner.js +469 -24
  7. package/dist/execution-contracts.js +153 -0
  8. package/dist/execution-receipts.js +185 -0
  9. package/dist/graders.js +8 -2
  10. package/dist/index.js +10 -0
  11. package/dist/learned-preference.js +334 -0
  12. package/dist/preferences.js +803 -0
  13. package/dist/rollouts.js +241 -0
  14. package/dist/tasksets.js +24 -0
  15. package/dist/telemetry/index.js +4 -0
  16. package/dist/telemetry-analysis.js +183 -0
  17. package/dist/telemetry-bundle.js +60 -0
  18. package/dist/telemetry-catalog.js +50 -0
  19. package/dist/telemetry.js +112 -0
  20. package/dist/types/artifact-verification.d.ts +31 -0
  21. package/dist/types/artifact-verification.d.ts.map +1 -0
  22. package/dist/types/builtin-benchmarks/harness-refiner.d.ts +22 -0
  23. package/dist/types/builtin-benchmarks/harness-refiner.d.ts.map +1 -1
  24. package/dist/types/conformance.d.ts +44 -0
  25. package/dist/types/conformance.d.ts.map +1 -1
  26. package/dist/types/evidence/conformance.d.ts +24 -24
  27. package/dist/types/evidence/contracts.d.ts +42 -42
  28. package/dist/types/execution-contracts.d.ts +806 -0
  29. package/dist/types/execution-contracts.d.ts.map +1 -0
  30. package/dist/types/execution-receipts.d.ts +55 -0
  31. package/dist/types/execution-receipts.d.ts.map +1 -0
  32. package/dist/types/graders.d.ts.map +1 -1
  33. package/dist/types/index.d.ts +10 -0
  34. package/dist/types/index.d.ts.map +1 -1
  35. package/dist/types/learned-preference.d.ts +282 -0
  36. package/dist/types/learned-preference.d.ts.map +1 -0
  37. package/dist/types/preferences.d.ts +583 -0
  38. package/dist/types/preferences.d.ts.map +1 -0
  39. package/dist/types/review-conformance.d.ts +20 -15
  40. package/dist/types/review-conformance.d.ts.map +1 -1
  41. package/dist/types/rollouts.d.ts +221 -0
  42. package/dist/types/rollouts.d.ts.map +1 -0
  43. package/dist/types/tasksets.d.ts +89 -0
  44. package/dist/types/tasksets.d.ts.map +1 -1
  45. package/dist/types/telemetry/index.d.ts +5 -0
  46. package/dist/types/telemetry/index.d.ts.map +1 -0
  47. package/dist/types/telemetry-analysis.d.ts +116 -0
  48. package/dist/types/telemetry-analysis.d.ts.map +1 -0
  49. package/dist/types/telemetry-bundle.d.ts +309 -0
  50. package/dist/types/telemetry-bundle.d.ts.map +1 -0
  51. package/dist/types/telemetry-catalog.d.ts +269 -0
  52. package/dist/types/telemetry-catalog.d.ts.map +1 -0
  53. package/dist/types/telemetry.d.ts +266 -0
  54. package/dist/types/telemetry.d.ts.map +1 -0
  55. package/package.json +20 -2
  56. package/schemas/telemetry/v1/evidence-completeness.schema.json +82 -0
  57. package/schemas/telemetry/v1/evidence-reference.schema.json +41 -0
  58. package/schemas/telemetry/v1/metric-definition.schema.json +96 -0
  59. package/schemas/telemetry/v1/metric-observation.schema.json +182 -0
  60. package/schemas/telemetry/v1/run-metric-summary.schema.json +98 -0
  61. package/schemas/telemetry/v1/run-telemetry-batch.schema.json +405 -0
  62. package/schemas/telemetry/v1/run-telemetry-event.schema.json +201 -0
  63. package/schemas/telemetry/v1/telemetry-cohort.schema.json +100 -0
  64. 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, and Work-evidence eligibility.
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
@@ -17,6 +18,8 @@ runtime processes.
17
18
  | `HarnessRunTrace` | `HarnessTrace` | Preserve ordered actions, observations, lifecycle events, terminal state, failure class, and trace hash. Learning-signal envelopes remain a training projection of the trace and receipt. |
18
19
  | `TaskAttemptResult` | `AttemptReceipt` | Preserve the old record for application persistence while adding a lossless receipt reference. Output becomes `outputHash`; trace and artifacts are separately hash-bound. |
19
20
  | `GradeResult` | `GraderEvidence[]` | Preserve component score, pass, reward eligibility, failure class, feedback, and visible/private evidence references. Aggregate UI results remain host projections. |
21
+ | Human or model comparison of attempts | `PreferenceComparisonRelease` + `ComparisonAssignment` + `PreferenceReceipt` | Keep release policy, blinded candidate ordering, exact attempt/artifact references, reviewer identity, and ranking immutable. Human and model reviewers emit the same receipt; provider sessions, identity details, queue state, and artifact bytes remain host-owned. |
22
+ | Comparative reward | `PreferenceAggregationReceipt` + standard `RewardComponentReceipt` | Aggregate only with the release's named quorum algorithm, calibrate automated reviewers against non-frozen human receipts, then project the named pairwise-win fraction into the existing per-attempt reward component boundary. |
20
23
  | managed-RL local receipt | `AttemptReceipt` | Submit canonical manifest/task/trace/artifact/grader identities; policy token responses and provider request IDs remain host-private trace data. |
21
24
  | resolved training bundle | `HarnessRelease` + host training binding | Agent snapshot, program, lifecycle, tool declarations, files, and grader interface belong to the Harness. Taskset environment/policy/graders and dataset/evidence, recipe, compute, engine, approval, and opaque leases remain explicit host-side bindings. |
22
25
  | completed Work or Development turn | `WorkEvidenceReceipt` | Project the authoritative terminal turn, immutable Agent snapshot when available, model/runtime identity, sanitized trace reference, exact output revisions, validation evidence, interventions, timing, usage, and explicit consent provenance. Keep the raw source and trace host-private. |
@@ -24,6 +27,8 @@ runtime processes.
24
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. |
25
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. |
26
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. |
27
32
 
28
33
  ## Compatibility policy
29
34
 
@@ -46,11 +51,32 @@ runtime processes.
46
51
  immutable objects and rejects lifecycle, tool, grader-interface, or required
47
52
  Environment-tool drift before issuing the receipt.
48
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.
49
63
  - Portable paths are relative and at most 2,000 characters. Individual assets
50
64
  are at most 250 MB. Tasksets, traces, and evidence arrays have schema-level
51
65
  upper bounds.
52
66
  - Immutable content never contains secret values, opaque lease values, mutable
53
67
  provider resource IDs, database keys, UI state, or process handles.
68
+ - Preference comparisons are an additive group-evaluation protocol, not a new
69
+ per-attempt grader kind and not a replacement for benchmark comparisons.
70
+ Releases contain two through four candidates, an ordered-tie-group result
71
+ policy, a frozen rubric artifact, presentation policy, aggregation policy,
72
+ reward projection, and calibration thresholds. Assignments bind every visible
73
+ artifact to an existing attempt and manifest under one Taskset, Harness,
74
+ Environment, Verifier, tool, and policy lineage.
75
+ - A model preference receipt is reward-eligible only when a passed calibration
76
+ report binds the exact comparison release and immutable model-reviewer
77
+ release. Frozen-evaluation assignments cannot contribute calibration or
78
+ training evidence. Invalid or unrenderable candidates remain unscorable;
79
+ they are not silently converted into aesthetic losses.
54
80
  - The Work evidence schemas first ship as additive `0.2.x` package exports. The
55
81
  package version does not replace the `openpond.workEvidenceReceipt.v1`,
56
82
  `openpond.workProcessTrace.v1`, `openpond.workFeedbackReceipt.v1`, or
package/README.md CHANGED
@@ -3,11 +3,17 @@
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, and no-training/SFT/preference/RL qualification receipts. The package depends on
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.
10
11
 
12
+ The `@openpond/evals/preferences` subpath adds a bounded, artifact-backed
13
+ comparison protocol: two to four attempts can be ranked by a human or model
14
+ reviewer, calibrated against held-out human receipts, then projected into the
15
+ existing reward-component boundary without encoding a specific visual domain.
16
+
11
17
  ```ts
12
18
  import {
13
19
  AttemptReceiptSchema,
@@ -29,7 +35,7 @@ import {
29
35
  ```
30
36
 
31
37
  Subpath exports are available at `/harness`, `/tasksets`, `/benchmarks`, `/graders`, `/runs`,
32
- `/conformance`, `/evidence`, `/review`, and
38
+ `/conformance`, `/evidence`, `/telemetry`, `/preferences`, `/review`, and
33
39
  `/model-improvement-qualification`. The package is an evaluation protocol library,
34
40
  not a hosted client. It does not execute OpenPond Desktop or Sandbox sessions,
35
41
  resolve credentials, or persist artifacts.
@@ -117,6 +123,29 @@ semantics. Infrastructure failures must remain reward-ineligible.
117
123
  Package semver and schema literals are independent. See [CONTRACT.md](./CONTRACT.md)
118
124
  for compatibility aliases, migration rules, size limits, and the field map.
119
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
+
120
149
  ## Release preparation
121
150
 
122
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
+ }
@@ -0,0 +1,172 @@
1
+ import { ArtifactManifestEntrySchema, RewardComponentReceiptSchema, } from "./execution-contracts.js";
2
+ import { createArtifactManifest } from "./execution-receipts.js";
3
+ export function buildArtifactManifest(input) {
4
+ const byPath = new Map();
5
+ for (const artifact of input.collectedArtifacts) {
6
+ const matches = byPath.get(artifact.path) ?? [];
7
+ matches.push(artifact);
8
+ byPath.set(artifact.path, matches);
9
+ }
10
+ const consumed = new Set();
11
+ const entries = input.requiredOutputs.map((required) => {
12
+ const matches = byPath.get(required.path) ?? [];
13
+ if (matches.length === 0) {
14
+ return ArtifactManifestEntrySchema.parse({
15
+ requiredOutputPath: required.path,
16
+ collectedPath: null,
17
+ declaredMediaType: required.mediaType,
18
+ detectedMediaType: null,
19
+ artifact: null,
20
+ status: "missing",
21
+ parseStatus: "not_requested",
22
+ schemaStatus: "not_requested",
23
+ errorCode: "required_output_missing",
24
+ failureOwner: "policy",
25
+ evidenceRefs: [],
26
+ metadata: { maxBytes: required.maxBytes, schemaRef: required.schemaRef },
27
+ });
28
+ }
29
+ if (matches.length > 1) {
30
+ for (const match of matches)
31
+ consumed.add(match);
32
+ return ArtifactManifestEntrySchema.parse({
33
+ requiredOutputPath: required.path,
34
+ collectedPath: required.path,
35
+ declaredMediaType: required.mediaType,
36
+ detectedMediaType: null,
37
+ artifact: null,
38
+ status: "failed",
39
+ parseStatus: "not_requested",
40
+ schemaStatus: "not_requested",
41
+ errorCode: "artifact_collection_ambiguous",
42
+ failureOwner: "collector",
43
+ evidenceRefs: matches.flatMap((match) => match.evidenceRefs ?? []),
44
+ metadata: { duplicateCount: matches.length },
45
+ });
46
+ }
47
+ const [collected] = matches;
48
+ consumed.add(collected);
49
+ return manifestEntry(required, collected);
50
+ });
51
+ for (const collected of input.collectedArtifacts) {
52
+ if (consumed.has(collected))
53
+ continue;
54
+ entries.push(ArtifactManifestEntrySchema.parse({
55
+ requiredOutputPath: null,
56
+ collectedPath: collected.path,
57
+ declaredMediaType: null,
58
+ detectedMediaType: collected.detectedMediaType,
59
+ artifact: collected.artifact,
60
+ status: collected.status,
61
+ parseStatus: collected.parseStatus ?? "not_requested",
62
+ schemaStatus: collected.schemaStatus ?? "not_requested",
63
+ errorCode: collected.errorCode ?? null,
64
+ failureOwner: collected.failureOwner ?? null,
65
+ evidenceRefs: collected.evidenceRefs ?? [],
66
+ metadata: collected.metadata ?? {},
67
+ }));
68
+ }
69
+ return createArtifactManifest({
70
+ schemaVersion: "openpond.artifactManifest.v1",
71
+ id: input.id,
72
+ attemptRef: input.attemptRef,
73
+ entries,
74
+ createdAt: input.createdAt,
75
+ metadata: input.metadata ?? {},
76
+ });
77
+ }
78
+ export function verifyRequiredOutputs(input) {
79
+ return input.requiredOutputs.map((required) => {
80
+ const entry = input.manifest.entries.find((candidate) => candidate.requiredOutputPath === required.path);
81
+ if (!entry || entry.status === "missing") {
82
+ return requiredOutputComponent(required, entry ?? null, {
83
+ status: "scored",
84
+ score: 0,
85
+ passed: false,
86
+ rewardEligible: true,
87
+ failureOwner: "policy",
88
+ feedback: `Required output ${required.path} was not collected.`,
89
+ });
90
+ }
91
+ if (entry.status === "failed" && entry.failureOwner !== "policy") {
92
+ return requiredOutputComponent(required, entry, {
93
+ status: "unscorable",
94
+ score: null,
95
+ passed: false,
96
+ rewardEligible: false,
97
+ failureOwner: entry.failureOwner ?? "collector",
98
+ feedback: `Required output ${required.path} could not be collected reliably.`,
99
+ });
100
+ }
101
+ const failures = structuralFailures(required, entry);
102
+ return requiredOutputComponent(required, entry, {
103
+ status: "scored",
104
+ score: failures.length === 0 ? 1 : 0,
105
+ passed: failures.length === 0,
106
+ rewardEligible: true,
107
+ failureOwner: failures.length === 0 ? null : "policy",
108
+ feedback: failures.length === 0
109
+ ? `Required output ${required.path} passed structural verification.`
110
+ : failures.join(" "),
111
+ });
112
+ });
113
+ }
114
+ function manifestEntry(required, collected) {
115
+ return ArtifactManifestEntrySchema.parse({
116
+ requiredOutputPath: required.path,
117
+ collectedPath: collected.path,
118
+ declaredMediaType: required.mediaType,
119
+ detectedMediaType: collected.detectedMediaType,
120
+ artifact: collected.artifact,
121
+ status: collected.status,
122
+ parseStatus: collected.parseStatus ?? "not_requested",
123
+ schemaStatus: collected.schemaStatus ?? "not_requested",
124
+ errorCode: collected.errorCode ?? null,
125
+ failureOwner: collected.failureOwner ?? null,
126
+ evidenceRefs: collected.evidenceRefs ?? [],
127
+ metadata: {
128
+ ...collected.metadata,
129
+ maxBytes: required.maxBytes,
130
+ schemaRef: required.schemaRef,
131
+ },
132
+ });
133
+ }
134
+ function structuralFailures(required, entry) {
135
+ const failures = [];
136
+ if (!entry.artifact)
137
+ failures.push(`Required output ${required.path} has no immutable artifact reference.`);
138
+ if (entry.detectedMediaType !== required.mediaType) {
139
+ failures.push(`Required output ${required.path} has media type ${entry.detectedMediaType ?? "unknown"}; expected ${required.mediaType}.`);
140
+ }
141
+ if (required.maxBytes !== null && entry.artifact?.sizeBytes !== null && entry.artifact && entry.artifact.sizeBytes > required.maxBytes) {
142
+ failures.push(`Required output ${required.path} exceeds ${required.maxBytes} bytes.`);
143
+ }
144
+ if (entry.parseStatus === "failed")
145
+ failures.push(`Required output ${required.path} could not be parsed.`);
146
+ if (entry.schemaStatus === "failed")
147
+ failures.push(`Required output ${required.path} failed schema validation.`);
148
+ return failures;
149
+ }
150
+ function requiredOutputComponent(required, entry, result) {
151
+ const evidenceRefs = [
152
+ ...(entry?.evidenceRefs ?? []),
153
+ ...(entry?.artifact ? [entry.artifact] : []),
154
+ ];
155
+ return RewardComponentReceiptSchema.parse({
156
+ verifierId: `required-output:${required.path}`,
157
+ verifierVersion: "1",
158
+ status: result.status,
159
+ rawScore: result.score,
160
+ normalizedScore: result.score,
161
+ weight: 1,
162
+ passed: result.passed,
163
+ hardGate: true,
164
+ rewardEligible: result.rewardEligible,
165
+ rewardContribution: result.rewardEligible ? result.score : null,
166
+ failureOwner: result.failureOwner,
167
+ feedback: [result.feedback],
168
+ visibleEvidenceRefs: evidenceRefs,
169
+ privilegedEvidenceRefs: [],
170
+ metadata: { requiredOutputPath: required.path },
171
+ });
172
+ }