@openpond/evals 0.10.9 → 0.11.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 (101) hide show
  1. package/LEARNING.md +35 -0
  2. package/dist/graders.js +19 -4
  3. package/dist/learning/budgeted-reward-fixtures.js +34 -0
  4. package/dist/learning/contracts.js +5 -0
  5. package/dist/learning/grade-judge-budget.js +26 -0
  6. package/dist/learning/index.js +8 -0
  7. package/dist/learning/intake-history.js +2 -1
  8. package/dist/learning/intake-text.js +14 -0
  9. package/dist/learning/intake.js +5 -2
  10. package/dist/learning/iteration-eligibility.js +5 -2
  11. package/dist/learning/iteration-reservation-contracts.js +1 -0
  12. package/dist/learning/iteration-reservation-service.js +11 -2
  13. package/dist/learning/judge-budget.js +59 -0
  14. package/dist/learning/model-judge.js +49 -0
  15. package/dist/learning/nightly-schedule.js +69 -0
  16. package/dist/learning/operations.js +3 -1
  17. package/dist/learning/reward-authoring.js +4 -0
  18. package/dist/learning/reward-calibration.js +66 -0
  19. package/dist/learning/reward-check-budget.js +27 -0
  20. package/dist/learning/reward-check-service.js +2 -2
  21. package/dist/learning/reward-check-worker.js +10 -2
  22. package/dist/learning/reward-checks.js +4 -1
  23. package/dist/learning/reward-fixture-executor.js +3 -2
  24. package/dist/learning/schedule-contracts.js +5 -1
  25. package/dist/learning/schedule-service.js +9 -5
  26. package/dist/learning/schedule-worker.js +22 -7
  27. package/dist/learning/service.js +40 -6
  28. package/dist/learning/task-queue-inspection.js +48 -0
  29. package/dist/learning/transport.js +1 -0
  30. package/dist/rewards.js +15 -3
  31. package/dist/rollouts.js +2 -2
  32. package/dist/types/graders.d.ts +51 -0
  33. package/dist/types/graders.d.ts.map +1 -1
  34. package/dist/types/learning/admission.d.ts +5 -0
  35. package/dist/types/learning/admission.d.ts.map +1 -1
  36. package/dist/types/learning/budgeted-reward-fixtures.d.ts +27 -0
  37. package/dist/types/learning/budgeted-reward-fixtures.d.ts.map +1 -0
  38. package/dist/types/learning/contracts.d.ts +313 -0
  39. package/dist/types/learning/contracts.d.ts.map +1 -1
  40. package/dist/types/learning/grade-judge-budget.d.ts +10 -0
  41. package/dist/types/learning/grade-judge-budget.d.ts.map +1 -0
  42. package/dist/types/learning/index.d.ts +8 -0
  43. package/dist/types/learning/index.d.ts.map +1 -1
  44. package/dist/types/learning/intake-history.d.ts.map +1 -1
  45. package/dist/types/learning/intake-text.d.ts +6 -0
  46. package/dist/types/learning/intake-text.d.ts.map +1 -0
  47. package/dist/types/learning/intake.d.ts +1 -0
  48. package/dist/types/learning/intake.d.ts.map +1 -1
  49. package/dist/types/learning/iteration-dispatch-contracts.d.ts +1 -1
  50. package/dist/types/learning/iteration-eligibility.d.ts +8 -1
  51. package/dist/types/learning/iteration-eligibility.d.ts.map +1 -1
  52. package/dist/types/learning/iteration-reservation-contracts.d.ts +7 -1
  53. package/dist/types/learning/iteration-reservation-contracts.d.ts.map +1 -1
  54. package/dist/types/learning/iteration-reservation-service.d.ts.map +1 -1
  55. package/dist/types/learning/judge-budget.d.ts +43 -0
  56. package/dist/types/learning/judge-budget.d.ts.map +1 -0
  57. package/dist/types/learning/model-judge.d.ts +29 -0
  58. package/dist/types/learning/model-judge.d.ts.map +1 -0
  59. package/dist/types/learning/nightly-schedule.d.ts +13 -0
  60. package/dist/types/learning/nightly-schedule.d.ts.map +1 -0
  61. package/dist/types/learning/operations.d.ts +63 -0
  62. package/dist/types/learning/operations.d.ts.map +1 -1
  63. package/dist/types/learning/repository.d.ts +188 -4
  64. package/dist/types/learning/repository.d.ts.map +1 -1
  65. package/dist/types/learning/reward-authoring.d.ts.map +1 -1
  66. package/dist/types/learning/reward-calibration.d.ts +129 -0
  67. package/dist/types/learning/reward-calibration.d.ts.map +1 -0
  68. package/dist/types/learning/reward-check-budget.d.ts +12 -0
  69. package/dist/types/learning/reward-check-budget.d.ts.map +1 -0
  70. package/dist/types/learning/reward-check-service.d.ts +1 -1
  71. package/dist/types/learning/reward-check-service.d.ts.map +1 -1
  72. package/dist/types/learning/reward-check-worker.d.ts.map +1 -1
  73. package/dist/types/learning/reward-checks.d.ts +84 -0
  74. package/dist/types/learning/reward-checks.d.ts.map +1 -1
  75. package/dist/types/learning/reward-fixture-executor.d.ts +3 -0
  76. package/dist/types/learning/reward-fixture-executor.d.ts.map +1 -1
  77. package/dist/types/learning/schedule-contracts.d.ts +5 -1
  78. package/dist/types/learning/schedule-contracts.d.ts.map +1 -1
  79. package/dist/types/learning/schedule-service.d.ts +4 -0
  80. package/dist/types/learning/schedule-service.d.ts.map +1 -1
  81. package/dist/types/learning/schedule-worker.d.ts.map +1 -1
  82. package/dist/types/learning/service.d.ts +10 -0
  83. package/dist/types/learning/service.d.ts.map +1 -1
  84. package/dist/types/learning/task-queue-inspection.d.ts +25 -0
  85. package/dist/types/learning/task-queue-inspection.d.ts.map +1 -0
  86. package/dist/types/learning/transport.d.ts +224 -5
  87. package/dist/types/learning/transport.d.ts.map +1 -1
  88. package/dist/types/rewards.d.ts +107 -0
  89. package/dist/types/rewards.d.ts.map +1 -1
  90. package/dist/types/rollouts.d.ts.map +1 -1
  91. package/package.json +1 -1
  92. package/schemas/learning/v1/command-request.schema.json +266 -169
  93. package/schemas/learning/v1/decision.schema.json +192 -0
  94. package/schemas/learning/v1/grade.schema.json +335 -3
  95. package/schemas/learning/v1/policy.schema.json +24 -0
  96. package/schemas/learning/v1/read-request.schema.json +28 -0
  97. package/schemas/learning/v1/reservation.schema.json +17 -0
  98. package/schemas/learning/v1/reward.schema.json +22 -0
  99. package/schemas/learning/v1/reward_check.schema.json +335 -3
  100. package/schemas/learning/v1/schedule.schema.json +44 -13
  101. package/schemas/learning/v1/task-queue-inspection.schema.json +72 -0
@@ -0,0 +1,66 @@
1
+ import { contentHash } from "@openpond/harness";
2
+ import { createRewardRelease } from "../rewards.js";
3
+ import { learningRef, sameLearningRef } from "./contracts.js";
4
+ import { LearningDomainError } from "./errors.js";
5
+ import { requireLearningRelease, requireLearningResource } from "./repository.js";
6
+ import { compileRewardCheck, matchRewardFixture } from "./reward-checks.js";
7
+ /** Qualification is tied to the exact private draft, fixtures and worker-owned
8
+ * provider receipts. Publishing does not rewrite or re-run those checks. */
9
+ export function qualifyRewardCheck(draft, base, check) {
10
+ const compiled = compileRewardCheck(draft, base);
11
+ const implementation = compiled.reward.implementation;
12
+ if (implementation.kind !== "model_judge" || !implementation.model)
13
+ throw new LearningDomainError("reward_calibration_model_required", 422);
14
+ if (!sameLearningRef(check.draft, learningRef(draft)) || !sameLearningRef(check.reward, learningRef(compiled.reward))
15
+ || check.snapshotHash !== compiled.snapshotHash || contentHash(check.fixtureRefs) !== contentHash(compiled.fixtureRefs))
16
+ throw new LearningDomainError("reward_calibration_snapshot_mismatch", 422);
17
+ if (check.status !== "completed" || !check.runtime || check.matchesExpectations !== true || check.failure
18
+ || check.results.length !== compiled.fixtures.length)
19
+ throw new LearningDomainError("reward_calibration_check_incomplete", 422);
20
+ if (!compiled.fixtures.some(fixture => fixture.expected.status === "scored" && fixture.expected.passed === true)
21
+ || !compiled.fixtures.some(fixture => fixture.expected.status === "scored" && fixture.expected.passed === false)) {
22
+ throw new LearningDomainError("reward_calibration_coverage_required", 422, "Check both a passing and a failing example before publishing a calibrated judge.");
23
+ }
24
+ for (const fixture of compiled.fixtures) {
25
+ const results = check.results.filter(result => result.fixture.id === fixture.id);
26
+ const retained = results[0];
27
+ if (results.length !== 1 || !retained || retained.fixture.contentHash !== contentHash(fixture)
28
+ || !sameLearningRef(retained.result.reward, check.reward) || !matchRewardFixture(fixture, retained.result).matchesExpectation)
29
+ throw new LearningDomainError("reward_calibration_fixture_mismatch", 422);
30
+ if (fixture.expected.status !== "scored")
31
+ continue;
32
+ const receipt = retained.result.graderEvidence?.modelJudgeReceipt;
33
+ const call = check.judgeCalls?.find(call => call.id === `fixture-${contentHash([fixture, compiled.reward.contentHash])}`);
34
+ if (!receipt || receipt.providerId !== implementation.model.providerId || receipt.modelId !== implementation.model.modelId
35
+ || (implementation.model.revision !== null && receipt.modelRevision !== implementation.model.revision)
36
+ || call?.status !== "settled" || !call.response || call.requestHash !== receipt.requestHash
37
+ || contentHash(call.response) !== receipt.responseHash)
38
+ throw new LearningDomainError("reward_calibration_provider_receipt_mismatch", 422);
39
+ }
40
+ const { contentHash: _hash, ...content } = compiled.reward;
41
+ return { ...compiled, reward: createRewardRelease({ ...content, implementation: { ...implementation, calibrationStatus: "passed" },
42
+ calibrationCheckRef: { id: check.id, revision: check.revision, contentHash: contentHash(check) } }) };
43
+ }
44
+ /** Generic publication cannot manufacture a passed flag, including by copying
45
+ * a check from a different rubric, model, temperature or fixture set. */
46
+ export async function assertRewardCalibration(tx, reward) {
47
+ if (reward.implementation.kind !== "model_judge" || reward.implementation.calibrationStatus !== "passed") {
48
+ if (reward.calibrationCheckRef)
49
+ throw new LearningDomainError("reward_calibration_reference_invalid", 422);
50
+ return;
51
+ }
52
+ const reference = reward.calibrationCheckRef;
53
+ if (!reference)
54
+ throw new LearningDomainError("reward_calibration_check_required", 422);
55
+ const check = await requireLearningResource(tx, "reward_check", reference.id, reference.revision);
56
+ if (contentHash(check) !== reference.contentHash)
57
+ throw new LearningDomainError("reward_calibration_check_changed", 422);
58
+ const draft = await requireLearningRelease(tx, "draft", check.draft);
59
+ if (draft.targetKind !== "reward")
60
+ throw new LearningDomainError("reward_calibration_draft_kind_invalid", 422);
61
+ const base = draft.baseRelease ? await requireLearningRelease(tx, "reward", draft.baseRelease) : null;
62
+ const qualified = qualifyRewardCheck(draft, base, check).reward;
63
+ const scoring = (value) => contentHash([value.implementation, value.rawScore, value.assets, value.fixtureSetRef ?? null]);
64
+ if (reward.id !== qualified.id || scoring(reward) !== scoring(qualified))
65
+ throw new LearningDomainError("reward_calibration_configuration_changed", 422);
66
+ }
@@ -0,0 +1,27 @@
1
+ import { contentHash } from "@openpond/harness";
2
+ import { requireLearningResource } from "./repository.js";
3
+ import { RewardCheckRunSchema } from "./reward-checks.js";
4
+ /** The same persisted run owns fixture results and budget reservations. A late
5
+ * provider settlement is retained even after its worker loses the lease. */
6
+ export function createRewardCheckJudgeBudgetStore(input) {
7
+ const now = input.now ?? (() => new Date().toISOString());
8
+ return {
9
+ transaction(intent, update) {
10
+ return input.repository.transaction(input.scope, async (tx) => {
11
+ const current = await requireLearningResource(tx, "reward_check", input.run.id);
12
+ if (current.snapshotHash !== input.run.snapshotHash)
13
+ throw new Error("model_judge_budget_snapshot_changed");
14
+ if (intent === "dispatch" && (current.status !== "running" || !current.leaseOwner || current.leaseOwner !== input.run.leaseOwner
15
+ || current.attemptCount !== input.run.attemptCount || !current.leaseExpiresAt || Date.parse(current.leaseExpiresAt) <= Date.parse(now())))
16
+ throw new Error("model_judge_budget_owner_inactive");
17
+ const calls = current.judgeCalls ?? [];
18
+ const next = update({ maximumSpendUsd: current.maximumSpendUsd, calls });
19
+ if (contentHash(next.calls) !== contentHash(calls)) {
20
+ const updated = RewardCheckRunSchema.parse({ ...current, revision: current.revision + 1, judgeCalls: next.calls, updatedAt: now() });
21
+ await tx.put("reward_check", updated, current.revision, { parentId: current.reward.id, status: current.status });
22
+ }
23
+ return next.result;
24
+ });
25
+ },
26
+ };
27
+ }
@@ -3,7 +3,7 @@ import { currentDraft } from "./authoring-service.js";
3
3
  import { learningRef } from "./contracts.js";
4
4
  import { compileRewardCheck, RewardCheckRunSchema } from "./reward-checks.js";
5
5
  import { LearningConflictError, requireLearningRelease, requireLearningResource } from "./repository.js";
6
- export async function queueRewardCheck(tx, input, operationId, now) {
6
+ export async function queueRewardCheck(tx, input, operationId, now, requestedBy) {
7
7
  const draft = await currentDraft(tx, input.draft);
8
8
  if (draft.targetKind !== "reward")
9
9
  throw new LearningDomainError("reward_check_draft_kind_invalid", 422);
@@ -13,7 +13,7 @@ export async function queueRewardCheck(tx, input, operationId, now) {
13
13
  schemaVersion: "openpond.rewardCheckRun.v1", id: `reward-check-${operationId}`, revision: 1,
14
14
  draft: learningRef(draft), reward: learningRef(compiled.reward), snapshotHash: compiled.snapshotHash,
15
15
  fixtureRefs: compiled.fixtureRefs, status: "queued", runtime: null, results: [], matchesExpectations: null,
16
- timeoutMs: input.timeoutMs, maximumSpendUsd: input.maximumSpendUsd,
16
+ timeoutMs: input.timeoutMs, maximumSpendUsd: input.maximumSpendUsd, requestedBy,
17
17
  leaseOwner: null, leaseExpiresAt: null, attemptCount: 0, failure: null, createdAt: now, updatedAt: now,
18
18
  });
19
19
  await tx.put("reward_check", check, 0, { parentId: draft.targetId, status: check.status });
@@ -17,7 +17,7 @@ export function createRewardCheckWorker(repository, executor, options) {
17
17
  const claimed = RewardCheckRunSchema.parse({ ...current, revision: current.revision + 1,
18
18
  status: current.status === "cancelling" ? "cancelling" : "running", runtime: current.status === "cancelling" ? current.runtime : runtime,
19
19
  leaseOwner: options.workerId, leaseExpiresAt: new Date(Date.parse(now()) + current.timeoutMs + 30_000).toISOString(),
20
- attemptCount: current.attemptCount + 1, results: current.status === "cancelling" ? current.results : [], matchesExpectations: null, failure: null, updatedAt: now(),
20
+ attemptCount: current.attemptCount + 1, results: current.results, matchesExpectations: null, failure: null, updatedAt: now(),
21
21
  });
22
22
  await tx.put("reward_check", claimed, current.revision, { parentId: claimed.reward.id, status: claimed.status });
23
23
  return { acquired: true, run: claimed };
@@ -44,8 +44,16 @@ export function createRewardCheckWorker(repository, executor, options) {
44
44
  });
45
45
  for (const fixture of compiled.fixtures) {
46
46
  controller.signal.throwIfAborted();
47
+ const previous = claim.run.results.find(result => result.fixture.id === fixture.id);
48
+ if (previous) {
49
+ if (previous.fixture.contentHash !== contentHash(fixture) || !sameLearningRef(previous.result.reward, claim.run.reward)
50
+ || previous.result.graderId !== compiled.reward.id || previous.result.role !== "evaluation")
51
+ throw new Error("reward_check_retained_fixture_mismatch");
52
+ continue;
53
+ }
47
54
  const result = BoundRewardResultSchema.parse(await executor.execute({ ...compiled, scope, run: claim.run, fixture, signal: controller.signal }));
48
- controller.signal.throwIfAborted();
55
+ // Retain a settled execution before observing cancellation. Its provider
56
+ // cost and evidence already exist even if no more fixtures should run.
49
57
  if (result.graderId !== compiled.reward.id || !sameLearningRef(result.reward, claim.run.reward) || result.role !== "evaluation")
50
58
  throw new Error("reward_check_result_identity_mismatch");
51
59
  const retained = await repository.transaction(scope, async (tx) => {
@@ -5,6 +5,7 @@ import { TaskRecordSchema } from "../tasksets.js";
5
5
  import { LearningJsonObjectSchema, LearningRevisionRefSchema, learningRef } from "./contracts.js";
6
6
  import { authoringNumber, compileRewardAuthoring, parseRewardAuthoringObject } from "./reward-authoring.js";
7
7
  import { LearningDomainError } from "./errors.js";
8
+ import { JudgeCallReservationSchema } from "./judge-budget.js";
8
9
  export const RewardFixtureSchema = z.object({
9
10
  id: ReleaseIdSchema, name: z.string().trim().min(1).max(500),
10
11
  input: LearningJsonObjectSchema, output: LearningJsonObjectSchema,
@@ -32,6 +33,8 @@ export const RewardCheckRunSchema = z.object({
32
33
  runtime: RewardCheckRuntimeSchema.nullable(), results: z.array(RewardFixtureCheckResultSchema).max(50),
33
34
  matchesExpectations: z.boolean().nullable(), failure: z.string().max(20_000).nullable(),
34
35
  timeoutMs: z.number().int().min(100).max(300_000), maximumSpendUsd: z.number().nonnegative().max(1_000),
36
+ judgeCalls: z.array(JudgeCallReservationSchema).max(1_000).optional(),
37
+ requestedBy: ReleaseIdSchema.optional(),
35
38
  leaseOwner: ReleaseIdSchema.nullable(), leaseExpiresAt: ReleaseTimestampSchema.nullable(), attemptCount: z.number().int().nonnegative(),
36
39
  createdAt: ReleaseTimestampSchema, updatedAt: ReleaseTimestampSchema,
37
40
  }).strict();
@@ -69,7 +72,7 @@ export async function executeRewardFixture(input) {
69
72
  }, [input.reward]);
70
73
  const composition = await executeRewardBinding({ binding, rewards: [input.reward], task,
71
74
  evidence: { output: fixture.output, artifactRefs: fixture.artifactRefs, runtimeEventRefs: fixture.runtimeEventRefs, infrastructureError: fixture.infrastructureError },
72
- signal: input.signal, customVerifier: input.customVerifier, modelJudge: input.modelJudge,
75
+ signal: input.signal, customVerifier: input.customVerifier, modelJudge: input.modelJudge, purpose: "fixture_calibration",
73
76
  });
74
77
  return composition.results[0];
75
78
  }
@@ -4,9 +4,10 @@ import { executeRewardFixture } from "./reward-checks.js";
4
4
  export function createIsolatedRewardFixtureExecutor(options) {
5
5
  return {
6
6
  runtime: options.runtime,
7
- execute(input) {
7
+ async execute(input) {
8
8
  return executeRewardFixture({
9
9
  reward: input.reward, fixture: input.fixture, signal: input.signal,
10
+ modelJudge: input.reward.implementation.kind === "model_judge" ? await options.createModelJudge?.(input) : undefined,
10
11
  customVerifier: async ({ grader, task, evidence }) => {
11
12
  const asset = input.assets.find(asset => asset.id === grader.verifierRef.id);
12
13
  if (!asset)
@@ -24,6 +25,6 @@ export function createIsolatedRewardFixtureExecutor(options) {
24
25
  });
25
26
  },
26
27
  // No remote allocation; executeJavaScript's settlement contract confirms cleanup.
27
- async cancel() { return true; },
28
+ async cancel(input) { return options.createModelJudge ? options.cancelModelJudge?.(input) ?? false : true; },
28
29
  };
29
30
  }
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { ReleaseHashSchema, ReleaseIdSchema, ReleaseTimestampSchema } from "@openpond/harness";
3
3
  import { LearningRevisionRefSchema } from "./contracts.js";
4
+ import { NightlyScheduleSchema } from "./nightly-schedule.js";
4
5
  /** The durable timer is operational state; every fire retains its own policy. */
5
6
  export const LearningScheduleSchema = z.object({
6
7
  schemaVersion: z.literal("openpond.learningSchedule.v1"),
@@ -11,6 +12,7 @@ export const LearningScheduleSchema = z.object({
11
12
  executionOwner: z.enum(["local", "hosted"]),
12
13
  state: z.enum(["scheduled", "disabled", "blocked"]),
13
14
  intervalSeconds: z.number().int().min(60).max(31_536_000).nullable(),
15
+ calendar: NightlyScheduleSchema.nullable().default(null),
14
16
  nextRunAt: ReleaseTimestampSchema.nullable(),
15
17
  lastFire: LearningRevisionRefSchema.nullable(),
16
18
  consecutiveFailures: z.number().int().nonnegative(),
@@ -19,10 +21,12 @@ export const LearningScheduleSchema = z.object({
19
21
  createdAt: ReleaseTimestampSchema,
20
22
  updatedAt: ReleaseTimestampSchema,
21
23
  }).strict().superRefine((value, context) => {
22
- if (value.state !== "disabled" && (!value.intervalSeconds || !value.nextRunAt))
24
+ if (value.state !== "disabled" && ((!value.intervalSeconds && !value.calendar) || !value.nextRunAt))
23
25
  context.addIssue({ code: "custom", path: ["nextRunAt"], message: "An active timer must retain its cadence and due time." });
24
26
  if (value.state === "disabled" && value.nextRunAt)
25
27
  context.addIssue({ code: "custom", path: ["nextRunAt"], message: "A disabled timer cannot have a pending occurrence." });
28
+ if (value.intervalSeconds && value.calendar)
29
+ context.addIssue({ code: "custom", path: ["calendar"], message: "A timer uses either an interval or a nightly calendar." });
26
30
  });
27
31
  /** Immutable occurrence evidence survives timer edits and coalesced downtime. */
28
32
  export const LearningScheduleFireSchema = z.object({
@@ -2,7 +2,8 @@ import { contentHash } from "@openpond/harness";
2
2
  import { assertLearningContentHash, learningRef, sameLearningRef } from "./contracts.js";
3
3
  import { LearningScheduleSchema } from "./schedule-contracts.js";
4
4
  import { LearningDomainError } from "./errors.js";
5
- import { requireLearningResource } from "./repository.js";
5
+ import { requireLearningRelease, requireLearningResource } from "./repository.js";
6
+ import { nextNightlyOccurrence } from "./nightly-schedule.js";
6
7
  export function learningScheduleId(policyId) { return `schedule-${contentHash(policyId)}`; }
7
8
  /** Called atomically with policy publication. No external timer or Work is made. */
8
9
  export async function synchronizeLearningSchedule(tx, policy, now) {
@@ -14,15 +15,18 @@ export async function synchronizeLearningSchedule(tx, policy, now) {
14
15
  const previous = await tx.get("schedule", id);
15
16
  if (previous && sameLearningRef(previous.policy, learningRef(policy)))
16
17
  return previous;
17
- const intervalSeconds = policy.trigger.kind === "schedule" ? policy.trigger.intervalSeconds : null;
18
- const enabled = policy.enabled && policy.automation.train && intervalSeconds !== null;
18
+ const intervalSeconds = policy.trigger.kind === "schedule" ? policy.trigger.intervalSeconds : policy.trigger.kind === "approved_count" ? 60 : null;
19
+ const calendar = policy.trigger.kind === "nightly" ? { localTime: policy.trigger.localTime, timeZone: policy.trigger.timeZone } : null;
20
+ const enabled = policy.enabled && policy.automation.train && (intervalSeconds !== null || calendar !== null);
21
+ const previousPolicy = previous ? await requireLearningRelease(tx, "policy", previous.policy) : null;
19
22
  const preserveDue = previous?.nextRunAt && previous.intervalSeconds === intervalSeconds
23
+ && JSON.stringify(previous.calendar) === JSON.stringify(calendar) && previousPolicy?.trigger.kind === policy.trigger.kind
20
24
  && previous.executionOwner === policy.executionOwner;
21
25
  const schedule = LearningScheduleSchema.parse({
22
26
  schemaVersion: "openpond.learningSchedule.v1", id, revision: (previous?.revision ?? 0) + 1,
23
27
  policy: learningRef(policy), modelProjectId: policy.modelProjectId, executionOwner: policy.executionOwner,
24
- state: enabled ? "scheduled" : "disabled", intervalSeconds,
25
- nextRunAt: enabled ? preserveDue ? previous.nextRunAt : new Date(Date.parse(now) + intervalSeconds * 1_000).toISOString() : null,
28
+ state: enabled ? "scheduled" : "disabled", intervalSeconds, calendar,
29
+ nextRunAt: enabled ? preserveDue ? previous.nextRunAt : calendar ? nextNightlyOccurrence(calendar, now) : new Date(Date.parse(now) + intervalSeconds * 1_000).toISOString() : null,
26
30
  lastFire: previous?.lastFire ?? null, consecutiveFailures: 0, nextAttemptAt: null, lastError: null,
27
31
  createdAt: previous?.createdAt ?? now, updatedAt: now,
28
32
  });
@@ -4,6 +4,8 @@ import { LearningDomainError } from "./errors.js";
4
4
  import { reserveLearningIteration } from "./iteration-reservation-service.js";
5
5
  import { LearningScheduleFireSchema, LearningScheduleSchema } from "./schedule-contracts.js";
6
6
  import { requireLearningRelease, requireLearningResource } from "./repository.js";
7
+ import { inspectIterationEligibility } from "./iteration-eligibility.js";
8
+ import { coalesceNightlyOccurrences } from "./nightly-schedule.js";
7
9
  const skipReasons = new Map([
8
10
  ["learning_iteration_active", "active_iteration"], ["learning_iteration_cooldown", "cooldown"],
9
11
  ["learning_daily_budget_exhausted", "daily_budget"],
@@ -28,20 +30,33 @@ export function createLearningScheduleWorker(repository, options) {
28
30
  attempt = { schedule, maxRetries: 0 };
29
31
  const policy = await requireLearningRelease(tx, "policy", schedule.policy);
30
32
  attempt.maxRetries = policy.limits.maxRetries;
31
- if (policy.executionOwner !== schedule.executionOwner || policy.trigger.kind !== "schedule"
32
- || policy.trigger.intervalSeconds !== schedule.intervalSeconds)
33
+ const matches = policy.trigger.kind === "nightly" ? schedule.intervalSeconds === null && schedule.calendar?.localTime === policy.trigger.localTime && schedule.calendar.timeZone === policy.trigger.timeZone
34
+ : policy.trigger.kind === "schedule" ? schedule.calendar === null && policy.trigger.intervalSeconds === schedule.intervalSeconds
35
+ : policy.trigger.kind === "approved_count" && schedule.calendar === null && schedule.intervalSeconds === 60;
36
+ if (policy.executionOwner !== schedule.executionOwner || !matches)
33
37
  throw new LearningDomainError("learning_schedule_policy_mismatch", 409);
34
38
  const scheduledAt = schedule.nextRunAt;
35
- const interval = schedule.intervalSeconds * 1_000;
39
+ const interval = (schedule.intervalSeconds ?? 86_400) * 1_000;
36
40
  const due = Date.parse(scheduledAt);
37
41
  const coalescedCount = Math.floor((time - due) / interval);
42
+ const occurrence = schedule.calendar ? coalesceNightlyOccurrences(schedule.calendar, scheduledAt, timestamp) : {
43
+ coalescedCount, coalescedThroughAt: new Date(due + coalescedCount * interval).toISOString(), nextRunAt: new Date(due + (coalescedCount + 1) * interval).toISOString(),
44
+ };
45
+ // Below-threshold count checks advance their timer without filling the
46
+ // Model's history with empty iterations or consuming any evidence.
47
+ if (policy.trigger.kind === "approved_count" && (await inspectIterationEligibility(tx, policy)).counts.eligible < policy.admission.minimumApprovedExamples) {
48
+ const updated = LearningScheduleSchema.parse({ ...schedule, revision: schedule.revision + 1, nextRunAt: occurrence.nextRunAt,
49
+ consecutiveFailures: 0, nextAttemptAt: null, lastError: null, updatedAt: timestamp });
50
+ await tx.put("schedule", updated, schedule.revision, { parentId: schedule.modelProjectId, status: updated.state });
51
+ return updated;
52
+ }
38
53
  const id = `schedule-fire-${contentHash([schedule.id, scheduledAt])}`;
39
54
  let outcome = "skipped";
40
55
  let reason = null;
41
56
  let iteration = null;
42
57
  try {
43
58
  const pointers = await reserveLearningIteration(tx, { action: "reserve_iteration", operationId: id,
44
- policy: schedule.policy, trigger: { kind: "schedule", scheduledAt } }, actorId, timestamp);
59
+ policy: schedule.policy, trigger: policy.trigger.kind === "approved_count" ? { kind: "approved_count", checkedAt: scheduledAt } : { kind: "schedule", scheduledAt } }, actorId, timestamp);
45
60
  const pointer = pointers.find(value => value.kind === "iteration");
46
61
  if (!pointer)
47
62
  throw new LearningDomainError("learning_schedule_reservation_missing", 409);
@@ -59,12 +74,12 @@ export function createLearningScheduleWorker(repository, options) {
59
74
  }
60
75
  const fire = LearningScheduleFireSchema.parse(sealLearningContent({
61
76
  schemaVersion: "openpond.learningScheduleFire.v1", id, revision: 1, scheduleId,
62
- policy: schedule.policy, scheduledAt, coalescedThroughAt: new Date(due + coalescedCount * interval).toISOString(),
63
- coalescedCount, outcome, reason, iteration, createdAt: timestamp,
77
+ policy: schedule.policy, scheduledAt, coalescedThroughAt: occurrence.coalescedThroughAt,
78
+ coalescedCount: occurrence.coalescedCount, outcome, reason, iteration, createdAt: timestamp,
64
79
  }));
65
80
  await tx.put("schedule_fire", fire, 0, { parentId: schedule.id, status: outcome });
66
81
  const updated = LearningScheduleSchema.parse({ ...schedule, revision: schedule.revision + 1,
67
- nextRunAt: new Date(due + (coalescedCount + 1) * interval).toISOString(), lastFire: learningRef(fire),
82
+ nextRunAt: occurrence.nextRunAt, lastFire: learningRef(fire),
68
83
  consecutiveFailures: 0, nextAttemptAt: null, lastError: null, updatedAt: timestamp });
69
84
  await tx.put("schedule", updated, schedule.revision, { parentId: schedule.modelProjectId, status: updated.state });
70
85
  return updated;
@@ -4,10 +4,13 @@ import { reserveLearningIteration } from "./iteration-reservation-service.js";
4
4
  import { synchronizeLearningSchedule } from "./schedule-service.js";
5
5
  import { inspectLearningPolicy } from "./policy-inspection.js";
6
6
  import { requireCurrentLearningEvidence as currentEvidence, sealLearningBatch } from "./batch-service.js";
7
- import { saveAuthoringDraft, archiveAuthoringDraft, finalizeAuthoringDraft } from "./authoring-service.js";
7
+ import { saveAuthoringDraft, archiveAuthoringDraft, finalizeAuthoringDraft, currentDraft } from "./authoring-service.js";
8
+ import { assertRewardCalibration, qualifyRewardCheck } from "./reward-calibration.js";
8
9
  import { queueRewardCheck, cancelRewardCheck } from "./reward-check-service.js";
9
10
  import { LearningDomainError } from "./errors.js";
10
11
  import { contentHash } from "@openpond/harness";
12
+ import { inspectLearningTaskQueue } from "./task-queue-inspection.js";
13
+ import { LearningReadRequestSchema } from "./transport.js";
11
14
  import { createRewardBinding, createRewardRelease } from "../rewards.js";
12
15
  import { assertBoundedTaskJson, validateTaskValue } from "../task-schema.js";
13
16
  import { assertAdmissionDecision, assertGradeIdentity, inspectTaskEvidence } from "./admission.js";
@@ -36,6 +39,29 @@ export function createLearningService(repository, options = {}) {
36
39
  }
37
40
  let pointers;
38
41
  switch (input.action) {
42
+ case "publish_checked_reward": {
43
+ const draft = await currentDraft(transaction, input.draft);
44
+ if (draft.targetKind !== "reward")
45
+ throw new LearningDomainError("reward_calibration_draft_kind_invalid", 422);
46
+ const base = draft.baseRelease ? await requireLearningRelease(transaction, "reward", draft.baseRelease) : null;
47
+ const check = await requireLearningResource(transaction, "reward_check", input.checkId, input.checkRevision);
48
+ const qualified = qualifyRewardCheck(draft, base, check);
49
+ const { contentHash: _rewardHash, ...content } = qualified.reward;
50
+ const publication = { action: "publish_resources", operationId: input.operationId,
51
+ finalizeDraft: { draft: input.draft, targetKind: "reward", release: learningRef(qualified.reward) },
52
+ resources: [
53
+ ...qualified.assets.map(({ contentHash: _assetHash, ...content }) => ({ kind: "asset", expectedRevision: 0, content })),
54
+ { kind: "reward", expectedRevision: base?.revision ?? 0, content },
55
+ ],
56
+ };
57
+ pointers = [];
58
+ for (const resource of publication.resources)
59
+ pointers.push(await publish(transaction, { ...resource, action: "publish", operationId: input.operationId }));
60
+ const finalized = await finalizeAuthoringDraft(transaction, publication, pointers, now());
61
+ if (finalized)
62
+ pointers.push(finalized);
63
+ break;
64
+ }
39
65
  case "import_intake":
40
66
  pointers = await importTaskIntake(transaction, input, now(), submit);
41
67
  break;
@@ -50,7 +76,7 @@ export function createLearningService(repository, options = {}) {
50
76
  pointers = await reserveLearningIteration(transaction, input, context.actor.id, now());
51
77
  break;
52
78
  case "queue_reward_check":
53
- pointers = [await queueRewardCheck(transaction, input, operationId, now())];
79
+ pointers = [await queueRewardCheck(transaction, input, operationId, now(), context.actor.id)];
54
80
  break;
55
81
  case "cancel_reward_check":
56
82
  pointers = [await cancelRewardCheck(transaction, input, now())];
@@ -83,7 +109,7 @@ export function createLearningService(repository, options = {}) {
83
109
  pointers = [await resolveFeedback(transaction, input, context.actor.id)];
84
110
  break;
85
111
  case "queue_grade":
86
- pointers = [await queueGrade(transaction, input, operationId)];
112
+ pointers = [await queueGrade(transaction, input, operationId, context.actor.id)];
87
113
  break;
88
114
  case "cancel_grade":
89
115
  pointers = [await cancelGrade(transaction, input)];
@@ -118,6 +144,7 @@ export function createLearningService(repository, options = {}) {
118
144
  }
119
145
  case "reward": {
120
146
  resource = createRewardRelease(input.content);
147
+ await assertRewardCalibration(transaction, resource);
121
148
  const implementation = input.content.implementation;
122
149
  const references = [
123
150
  ...input.content.assets,
@@ -282,7 +309,7 @@ export function createLearningService(repository, options = {}) {
282
309
  await transaction.put("feedback", resolved, feedback.revision, { parentId: learningEvidenceId(feedback.submission.sourceId, feedback.submission.exampleId, feedback.submission.attemptId), status: resolved.status });
283
310
  return pointer("feedback", resolved);
284
311
  }
285
- async function queueGrade(transaction, input, operationId) {
312
+ async function queueGrade(transaction, input, operationId, requestedBy) {
286
313
  const evidence = await currentEvidence(transaction, input.evidence);
287
314
  const definition = await requireLearningRelease(transaction, "definition", evidence.submission.taskDefinition);
288
315
  if (!inspectTaskEvidence(evidence, definition).taskReady)
@@ -296,7 +323,7 @@ export function createLearningService(repository, options = {}) {
296
323
  schemaVersion: "openpond.taskGradeRun.v1", id: `grade-${operationId}`, revision: 1,
297
324
  evidence: learningRef(evidence), binding: definition.rewardBinding, target: input.target, output,
298
325
  status: "queued", composition: null, leaseOwner: null, leaseExpiresAt: null, attemptCount: 0,
299
- timeoutMs: input.timeoutMs, maximumSpendUsd: input.maximumSpendUsd, failure: null, createdAt: now(), updatedAt: now(),
326
+ timeoutMs: input.timeoutMs, maximumSpendUsd: input.maximumSpendUsd, requestedBy, failure: null, createdAt: now(), updatedAt: now(),
300
327
  });
301
328
  await transaction.put("grade", grade, 0, { parentId: evidence.id, status: grade.status });
302
329
  return pointer("grade", grade);
@@ -335,6 +362,13 @@ export function createLearningService(repository, options = {}) {
335
362
  }
336
363
  return {
337
364
  command,
365
+ async inspectTaskQueue(context, modelProjectId = null) {
366
+ authorizeRead(context);
367
+ const input = LearningReadRequestSchema.parse({ action: "inspect_task_queue", scope: context.scope, modelProjectId });
368
+ if (input.action !== "inspect_task_queue")
369
+ throw new LearningDomainError("learning_request_invalid", 400);
370
+ return repository.transaction(context.scope, transaction => inspectLearningTaskQueue(transaction, input.modelProjectId, now()));
371
+ },
338
372
  async inspectPolicy(context, reference) {
339
373
  authorizeRead(context);
340
374
  const ref = LearningRevisionRefSchema.parse(reference);
@@ -382,7 +416,7 @@ function authorize(context, input) {
382
416
  throw new LearningDomainError("learning_scope_required", 400);
383
417
  const publications = input.action === "publish" ? [input] : input.action === "publish_resources" ? input.resources : [];
384
418
  if (context.actor.role !== "reviewer" && publications.some(value => value.kind === "policy" && value.content.enabled
385
- && value.content.automation.train && value.content.trigger.kind === "schedule"))
419
+ && value.content.automation.train && value.content.trigger.kind !== "manual"))
386
420
  throw new LearningDomainError("learning_review_not_authorized", 403);
387
421
  if (context.actor.role === "source") {
388
422
  const sourceId = input.action === "submit_example" ? input.example.sourceId : input.action === "submit_feedback" ? input.feedback.sourceId : null;
@@ -0,0 +1,48 @@
1
+ import { ReleaseIdSchema, ReleaseTimestampSchema } from "@openpond/harness";
2
+ import { z } from "zod";
3
+ import { LearningDomainError } from "./errors.js";
4
+ import { inspectIterationEligibility } from "./iteration-eligibility.js";
5
+ import { taskFamilyReservations } from "./admission.js";
6
+ import { requireLearningRelease } from "./repository.js";
7
+ export const LearningTaskQueueInspectionSchema = z.object({
8
+ modelProjectId: ReleaseIdSchema.nullable(),
9
+ inspectedAt: ReleaseTimestampSchema,
10
+ pendingTrainingCount: z.number().int().nonnegative().nullable(),
11
+ issues: z.array(z.object({ policyId: ReleaseIdSchema, code: z.string(), message: z.string() }).strict()),
12
+ }).strict();
13
+ /** Count exact eligible, unconsumed attempts once across selected policies.
14
+ * Paused policies still have pending work; review-only and held-out tasks do not. */
15
+ export async function inspectLearningTaskQueue(transaction, modelProjectId, now) {
16
+ const eligible = new Set();
17
+ const issues = [];
18
+ let afterId;
19
+ do {
20
+ const page = await transaction.list("policy", { limit: 100, ...(modelProjectId ? { parentId: modelProjectId } : {}), ...(afterId ? { afterId } : {}) });
21
+ for (const policy of page.items) {
22
+ try {
23
+ const inspection = await inspectIterationEligibility(transaction, policy, { collectEligible: true });
24
+ const definition = await requireLearningRelease(transaction, "definition", policy.taskDefinition);
25
+ for (const reference of inspection.eligibleEvidence) {
26
+ const evidence = await requireLearningRelease(transaction, "evidence", reference);
27
+ let heldOut = false;
28
+ for (const family of taskFamilyReservations(evidence, definition)) {
29
+ const split = await transaction.familySplit(family.namespace, family.kind, family.key);
30
+ if (split !== null && split !== "train") {
31
+ heldOut = true;
32
+ break;
33
+ }
34
+ }
35
+ if (!heldOut)
36
+ eligible.add(JSON.stringify([reference.id, reference.revision, reference.contentHash]));
37
+ }
38
+ }
39
+ catch (error) {
40
+ if (!(error instanceof LearningDomainError))
41
+ throw error;
42
+ issues.push({ policyId: policy.id, code: error.code, message: error.message });
43
+ }
44
+ }
45
+ afterId = page.nextCursor ?? undefined;
46
+ } while (afterId);
47
+ return LearningTaskQueueInspectionSchema.parse({ modelProjectId, inspectedAt: now, pendingTrainingCount: issues.length ? null : eligible.size, issues });
48
+ }
@@ -17,6 +17,7 @@ export function assertLearningRequestJson(raw) {
17
17
  /** Scope selection is never authorization; each host resolves its authenticated owner. */
18
18
  export const LearningCommandRequestSchema = z.object({ scope: ReleaseIdSchema, command: LearningCommandSchema }).strict();
19
19
  export const LearningReadRequestSchema = z.discriminatedUnion("action", [
20
+ z.object({ action: z.literal("inspect_task_queue"), scope: ReleaseIdSchema, modelProjectId: ReleaseIdSchema.nullable() }).strict(),
20
21
  z.object({ action: z.literal("inspect_policy"), scope: ReleaseIdSchema, policy: LearningRevisionRefSchema }).strict(),
21
22
  z.object({ action: z.literal("inspect_evidence"), scope: ReleaseIdSchema, evidence: LearningRevisionRefSchema }).strict(),
22
23
  z.object({ action: z.literal("get"), scope: ReleaseIdSchema, kind: LearningResourceKindSchema, id: ReleaseIdSchema, revision: z.number().int().positive().optional() }).strict(),
package/dist/rewards.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { LearningDomainError } from "./learning/errors.js";
2
2
  import { z } from "zod";
3
3
  import { ImmutableAssetRefSchema, ImmutableReleaseRefSchema, ReleaseHashSchema, ReleaseIdSchema, contentHash } from "@openpond/harness";
4
- import { gradeEvidence } from "./graders.js";
4
+ import { gradeEvidence, GraderEvidenceSchema } from "./graders.js";
5
5
  import { CustomVerifierGraderSpecSchema, DeterministicGraderSpecSchema, GraderSpecSchema, HumanGraderSpecSchema, ModelJudgeGraderSpecSchema } from "./tasksets.js";
6
6
  import { assertBoundedTaskJson, validateTaskSchema } from "./task-schema.js";
7
7
  const bindingFields = { id: true, version: true, weight: true, hardGate: true, rewardEligible: true, privileged: true };
@@ -22,6 +22,7 @@ export const RewardReleaseContentSchema = z.object({
22
22
  rawScore: z.object({ minimum: z.number().finite(), maximum: z.number().finite() }).strict(),
23
23
  assets: z.array(ImmutableAssetRefSchema).max(1_000),
24
24
  fixtureSetRef: ImmutableAssetRefSchema.optional(),
25
+ calibrationCheckRef: ImmutableReleaseRefSchema.extend({ revision: z.number().int().positive() }).optional(),
25
26
  }).strict().superRefine((reward, context) => {
26
27
  if (reward.fixtureSetRef && (reward.fixtureSetRef.visibility !== "verifier" || !reward.assets.some(asset => contentHash(asset) === contentHash(reward.fixtureSetRef))))
27
28
  context.addIssue({ code: "custom", path: ["fixtureSetRef"], message: "Reward fixtures must be included as a private verifier asset." });
@@ -86,6 +87,7 @@ export const BoundRewardResultSchema = z.object({
86
87
  normalizedScore: z.number().min(0).max(1).nullable(),
87
88
  passed: z.boolean().nullable(),
88
89
  evidenceHashes: z.array(ReleaseHashSchema).max(1_000),
90
+ graderEvidence: GraderEvidenceSchema.optional(),
89
91
  message: z.string().max(20_000).nullable(),
90
92
  }).strict();
91
93
  export const RewardCompositionContentSchema = z.object({
@@ -136,6 +138,8 @@ export function compileBoundGraders(binding, rewards) {
136
138
  }
137
139
  export async function executeRewardBinding(input) {
138
140
  const resolved = resolveBoundRewards(input.binding, input.rewards);
141
+ if (input.purpose === "fixture_calibration" && resolved.some(({ source }) => source.role !== "evaluation"))
142
+ throw new LearningDomainError("reward_calibration_requires_evaluation_binding");
139
143
  const results = [];
140
144
  for (const { source, reward } of resolved) {
141
145
  input.signal?.throwIfAborted();
@@ -154,10 +158,10 @@ export async function executeRewardBinding(input) {
154
158
  }
155
159
  }
156
160
  else {
157
- const [evidence] = await gradeEvidence({ task: input.task, evidence: input.evidence, graders: [compileSource(source, reward)], modelJudge: input.modelJudge, customVerifier: input.customVerifier });
161
+ const [evidence] = await gradeEvidence({ task: input.task, evidence: input.evidence, graders: [compileSource(source, reward)], modelJudge: input.modelJudge, customVerifier: input.customVerifier, purpose: input.purpose, signal: input.signal });
158
162
  if (!evidence)
159
163
  throw new LearningDomainError("reward_evidence_missing");
160
- Object.assign(result, { status: evidence.score === null ? "unavailable" : "scored", rawScore: evidence.score, passed: evidence.score === null ? null : evidence.passed, evidenceHashes: [evidence.contentHash], message: evidence.feedback.join("\n") });
164
+ Object.assign(result, { status: evidence.score === null ? "unavailable" : "scored", rawScore: evidence.score, passed: evidence.score === null ? null : evidence.passed, evidenceHashes: [evidence.contentHash], graderEvidence: evidence, message: evidence.feedback.join("\n") });
161
165
  }
162
166
  if (result.status === "scored") {
163
167
  if (result.rawScore === null || !Number.isFinite(result.rawScore) || result.rawScore < reward.rawScore.minimum || result.rawScore > reward.rawScore.maximum)
@@ -186,6 +190,14 @@ export function composeBoundRewards(input) {
186
190
  const result = results.find((candidate) => candidate.graderId === source.graderId);
187
191
  if (!result || contentHash(result.reward) !== contentHash(source.reward) || result.role !== source.role)
188
192
  throw new LearningDomainError("reward_result_binding_mismatch");
193
+ if (result.graderEvidence) {
194
+ const { contentHash: evidenceHash, ...body } = result.graderEvidence;
195
+ if (contentHash(body) !== evidenceHash || !result.evidenceHashes.includes(evidenceHash)
196
+ || body.graderId !== source.graderId || body.graderVersion !== String(source.reward.revision))
197
+ throw new LearningDomainError("reward_grader_evidence_mismatch");
198
+ if (result.status === "scored" && (body.score !== result.rawScore || body.passed !== result.passed))
199
+ throw new LearningDomainError("reward_grader_evidence_score_mismatch");
200
+ }
189
201
  if ((result.status === "scored") !== (result.rawScore !== null && result.normalizedScore !== null && result.passed !== null))
190
202
  throw new LearningDomainError("reward_result_score_state_invalid");
191
203
  if (result.status !== "scored" && (result.rawScore !== null || result.normalizedScore !== null || result.passed !== null))
package/dist/rollouts.js CHANGED
@@ -116,7 +116,7 @@ export const CanonicalRolloutRecordSchema = z.object({
116
116
  ...CanonicalRolloutRecordFieldsSchema.shape,
117
117
  contentHash: ReleaseHashSchema,
118
118
  }).strict().superRefine(validateCanonicalRolloutRecord);
119
- export const RolloutQualificationSchema = z.object({
119
+ export const RolloutQualificationSchema = /* @__PURE__ */ (() => z.object({
120
120
  schemaVersion: z.literal("openpond.rolloutQualification.v1"),
121
121
  rolloutCount: z.number().int().nonnegative(),
122
122
  scoredCount: z.number().int().nonnegative(),
@@ -128,7 +128,7 @@ export const RolloutQualificationSchema = z.object({
128
128
  distinctRewardCount: z.number().int().nonnegative(),
129
129
  eligibleForRl: z.boolean(),
130
130
  reasons: z.array(z.string().trim().min(1).max(1_000)).max(100),
131
- }).strict();
131
+ }).strict())();
132
132
  export function createCanonicalRolloutRecord(input) {
133
133
  const attemptReceipt = AttemptReceiptSchema.parse(input.attemptReceipt);
134
134
  if (!verifyAttemptReceipt(attemptReceipt)) {