@openpond/evals 0.11.9 → 0.11.10
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/LEARNING.md +35 -0
- package/dist/learning/acceptance-group.js +199 -0
- package/dist/learning/acceptance-plan.js +7 -1
- package/dist/learning/index.js +1 -0
- package/dist/types/learning/acceptance-group.d.ts +540 -0
- package/dist/types/learning/acceptance-group.d.ts.map +1 -0
- package/dist/types/learning/acceptance-plan.d.ts +39 -0
- package/dist/types/learning/acceptance-plan.d.ts.map +1 -1
- package/dist/types/learning/index.d.ts +1 -0
- package/dist/types/learning/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/schemas/learning/v1/acceptance-group-manifest.schema.json +261 -0
- package/schemas/learning/v1/acceptance-group-snapshot.schema.json +585 -0
- package/schemas/learning/v1/acceptance-plan.schema.json +205 -0
package/LEARNING.md
CHANGED
|
@@ -359,3 +359,38 @@ unique eligible, unconsumed training attempts across current policies. It
|
|
|
359
359
|
deduplicates shared sources and excludes held-out family reservations. Paused
|
|
360
360
|
policies retain pending tasks. An invalid policy yields a null count and explicit
|
|
361
361
|
issues rather than a misleading zero. Source credentials cannot read the queue.
|
|
362
|
+
|
|
363
|
+
## Independent acceptance groups
|
|
364
|
+
|
|
365
|
+
`createAcceptanceGroupManifest` pins a training Job, exact baseline/candidate
|
|
366
|
+
artifacts, an acceptance plan and bounded concurrency. Each durable check declares
|
|
367
|
+
its role and score unit. Required quality/retention gates remain separate;
|
|
368
|
+
diagnostic checks cannot authorize acceptance. Pin the plan before training
|
|
369
|
+
admission, then create the group when the exact candidate exists. A missing
|
|
370
|
+
required group must block acceptance.
|
|
371
|
+
|
|
372
|
+
`createAcceptanceGroupSnapshot` verifies plan/manifest/snapshot integrity and
|
|
373
|
+
attempt identity. `assertAcceptanceGroupUpdate` enforces sequential revisions,
|
|
374
|
+
append-only retries, immutable terminal evidence, legal lifecycle transitions,
|
|
375
|
+
concurrency and dispatch budgets including outstanding reservations and prior
|
|
376
|
+
attempt costs. New attempts enter queued; an explicitly reused baseline may enter
|
|
377
|
+
completed. A terminal attempt requires cleanup, settled accounting and no remaining
|
|
378
|
+
reservation. Deliberate reruns create a new immutable group.
|
|
379
|
+
|
|
380
|
+
`assertAcceptanceBaselineReuse` accepts only fresh, exact, same-team measurements
|
|
381
|
+
matching the artifact, check, qualification reference and execution configuration.
|
|
382
|
+
It rejects recursively reused measurements. The owner must resolve the retained
|
|
383
|
+
source group and current qualification/revocation/access state; hashes alone are
|
|
384
|
+
not authorization or proof of execution.
|
|
385
|
+
|
|
386
|
+
`acceptanceGroupVerdict` preserves per-check results and blocks acceptance until
|
|
387
|
+
the group is complete, all required attempts have comparable measurements, cleanup
|
|
388
|
+
and accounting are settled, and recorded spend stays within all budgets. Failed
|
|
389
|
+
attempt costs count even if a later retry passes. It never averages unrelated
|
|
390
|
+
metrics into one quality score.
|
|
391
|
+
|
|
392
|
+
These contracts do not dispatch work or implement a database. Hosts must serialize
|
|
393
|
+
revision changes and budget reservations, resolve authoritative execution/grading
|
|
394
|
+
receipts, isolate holdouts from training, bind the exact candidate at runtime,
|
|
395
|
+
verify cleanup, and enforce the verdict inside the candidate-decision transaction.
|
|
396
|
+
No customer serving is activated by an acceptance group.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { assertContentHash, contentHash, ImmutableReleaseRefSchema } from "@openpond/harness";
|
|
3
|
+
import { AcceptanceMeasurementSchema, AcceptancePlanSchema, evaluateAcceptancePlan } from "./acceptance-plan.js";
|
|
4
|
+
const Id = z.string().trim().min(1).max(240);
|
|
5
|
+
const Hash = z.string().regex(/^[a-f0-9]{64}$/);
|
|
6
|
+
const Timestamp = z.iso.datetime();
|
|
7
|
+
const Money = z.number().finite().nonnegative();
|
|
8
|
+
const terminalAttempts = new Set(["completed", "failed", "cancelled", "budget_exhausted"]);
|
|
9
|
+
const terminalGroups = new Set(["completed", "failed", "cancelled", "budget_exhausted"]);
|
|
10
|
+
const groupTransitions = {
|
|
11
|
+
queued: ["queued", "running", "blocked", "cancelling", "failed", "budget_exhausted"],
|
|
12
|
+
running: ["running", "blocked", "cancelling", "cleaning", "completed", "failed", "budget_exhausted"],
|
|
13
|
+
blocked: ["blocked", "running", "cancelling", "failed", "budget_exhausted"],
|
|
14
|
+
cancelling: ["cancelling", "cleaning", "cancelled", "budget_exhausted"],
|
|
15
|
+
cleaning: ["cleaning", "completed", "failed", "cancelled", "budget_exhausted"],
|
|
16
|
+
};
|
|
17
|
+
const attemptTransitions = {
|
|
18
|
+
queued: ["queued", "dispatching", "failed", "cancelled", "budget_exhausted"],
|
|
19
|
+
dispatching: ["dispatching", "running", "cleaning", "failed", "cancelled", "budget_exhausted"],
|
|
20
|
+
running: ["running", "grading", "cleaning", "failed", "cancelled", "budget_exhausted"],
|
|
21
|
+
grading: ["grading", "cleaning", "completed", "failed", "cancelled", "budget_exhausted"],
|
|
22
|
+
cleaning: ["cleaning", "completed", "failed", "cancelled", "budget_exhausted"],
|
|
23
|
+
};
|
|
24
|
+
/** A group starts only after the exact candidate is available. The host must pin
|
|
25
|
+
* its acceptance plan before training admission so a missing group blocks review. */
|
|
26
|
+
export const AcceptanceGroupManifestContentSchema = z.object({
|
|
27
|
+
schemaVersion: z.literal("openpond.acceptanceGroupManifest.v1"),
|
|
28
|
+
id: Id, teamId: Id, trainingJob: ImmutableReleaseRefSchema,
|
|
29
|
+
baseline: ImmutableReleaseRefSchema, candidate: ImmutableReleaseRefSchema,
|
|
30
|
+
plan: AcceptancePlanSchema, maximumParallelAttempts: z.number().int().min(1).max(8),
|
|
31
|
+
createdAt: Timestamp,
|
|
32
|
+
}).strict().superRefine((manifest, ctx) => {
|
|
33
|
+
if (manifest.plan.checks.some(check => !check.role || !check.unit))
|
|
34
|
+
ctx.addIssue({ code: "custom", message: "Durable acceptance checks require explicit roles and score units." });
|
|
35
|
+
});
|
|
36
|
+
export const AcceptanceGroupManifestSchema = AcceptanceGroupManifestContentSchema.safeExtend({ contentHash: Hash });
|
|
37
|
+
export const AcceptanceAttemptSchema = z.object({
|
|
38
|
+
id: Id, checkId: Id, subject: z.enum(["baseline", "candidate"]),
|
|
39
|
+
ordinal: z.number().int().positive(), dispatchId: Id,
|
|
40
|
+
state: z.enum(["queued", "dispatching", "running", "grading", "cleaning", "completed", "failed", "cancelled", "budget_exhausted"]),
|
|
41
|
+
artifact: ImmutableReleaseRefSchema, checkHash: Hash,
|
|
42
|
+
measurement: AcceptanceMeasurementSchema.nullable(),
|
|
43
|
+
executionReceipt: ImmutableReleaseRefSchema.nullable(),
|
|
44
|
+
reusedFrom: z.object({ group: ImmutableReleaseRefSchema, attemptId: Id }).strict().nullable(),
|
|
45
|
+
spendUsd: Money, reservedUsd: Money,
|
|
46
|
+
cleanupComplete: z.boolean(), accountingComplete: z.boolean(),
|
|
47
|
+
failureCode: Id.nullable(), createdAt: Timestamp, updatedAt: Timestamp,
|
|
48
|
+
}).strict().superRefine((attempt, ctx) => {
|
|
49
|
+
if (Date.parse(attempt.updatedAt) < Date.parse(attempt.createdAt))
|
|
50
|
+
ctx.addIssue({ code: "custom", message: "Attempt timestamps are reversed." });
|
|
51
|
+
if (terminalAttempts.has(attempt.state) && (!attempt.cleanupComplete || !attempt.accountingComplete || attempt.reservedUsd !== 0))
|
|
52
|
+
ctx.addIssue({ code: "custom", message: "Terminal attempts require cleanup and settled accounting." });
|
|
53
|
+
if (attempt.state === "completed" && (attempt.measurement?.status !== "completed" || !attempt.executionReceipt))
|
|
54
|
+
ctx.addIssue({ code: "custom", message: "Completed attempts require retained measurement and execution receipts." });
|
|
55
|
+
if (attempt.reusedFrom && (attempt.subject !== "baseline" || attempt.state !== "completed" || attempt.spendUsd !== 0))
|
|
56
|
+
ctx.addIssue({ code: "custom", message: "Only completed baselines may reuse retained evidence without new spend." });
|
|
57
|
+
});
|
|
58
|
+
export const AcceptanceGroupSnapshotContentSchema = z.object({
|
|
59
|
+
schemaVersion: z.literal("openpond.acceptanceGroupSnapshot.v1"),
|
|
60
|
+
manifest: AcceptanceGroupManifestSchema,
|
|
61
|
+
revision: z.number().int().positive(),
|
|
62
|
+
state: z.enum(["queued", "running", "blocked", "cancelling", "cleaning", "completed", "failed", "cancelled", "budget_exhausted"]),
|
|
63
|
+
attempts: z.array(AcceptanceAttemptSchema).max(2_000),
|
|
64
|
+
failureCode: Id.nullable(), updatedAt: Timestamp,
|
|
65
|
+
}).strict();
|
|
66
|
+
export const AcceptanceGroupSnapshotSchema = AcceptanceGroupSnapshotContentSchema.extend({ contentHash: Hash });
|
|
67
|
+
export function createAcceptanceGroupManifest(raw) {
|
|
68
|
+
const content = AcceptanceGroupManifestContentSchema.parse(raw);
|
|
69
|
+
assertContentHash(content.plan, "Acceptance plan");
|
|
70
|
+
return { ...content, contentHash: contentHash(content) };
|
|
71
|
+
}
|
|
72
|
+
export function createAcceptanceGroupSnapshot(raw) {
|
|
73
|
+
const content = AcceptanceGroupSnapshotContentSchema.parse(raw);
|
|
74
|
+
return assertAcceptanceGroupSnapshot({ ...content, contentHash: contentHash(content) });
|
|
75
|
+
}
|
|
76
|
+
/** Structural integrity does not replace receipt retrieval, resource access,
|
|
77
|
+
* qualification/revocation checks, or authoritative ledger verification. */
|
|
78
|
+
export function assertAcceptanceGroupSnapshot(raw) {
|
|
79
|
+
const snapshot = AcceptanceGroupSnapshotSchema.parse(raw);
|
|
80
|
+
assertContentHash(snapshot, "Acceptance group snapshot");
|
|
81
|
+
assertContentHash(snapshot.manifest, "Acceptance group manifest");
|
|
82
|
+
assertContentHash(snapshot.manifest.plan, "Acceptance plan");
|
|
83
|
+
const checks = new Map(snapshot.manifest.plan.checks.map(check => [check.id, check]));
|
|
84
|
+
const identities = new Set(), dispatches = new Set(), positions = new Set();
|
|
85
|
+
for (const attempt of snapshot.attempts) {
|
|
86
|
+
const check = checks.get(attempt.checkId);
|
|
87
|
+
if (!check || attempt.checkHash !== contentHash(check) || contentHash(attempt.artifact) !== contentHash(snapshot.manifest[attempt.subject]))
|
|
88
|
+
throw new Error("Acceptance attempt does not match its pinned check and artifact.");
|
|
89
|
+
const position = JSON.stringify([attempt.checkId, attempt.subject, attempt.ordinal]);
|
|
90
|
+
if (identities.has(attempt.id) || dispatches.has(attempt.dispatchId) || positions.has(position))
|
|
91
|
+
throw new Error("Acceptance attempts require unique identities, dispatches and positions.");
|
|
92
|
+
identities.add(attempt.id);
|
|
93
|
+
dispatches.add(attempt.dispatchId);
|
|
94
|
+
positions.add(position);
|
|
95
|
+
if (attempt.measurement && (attempt.measurement.checkHash !== attempt.checkHash || contentHash(attempt.measurement.artifact) !== contentHash(attempt.artifact)))
|
|
96
|
+
throw new Error("Acceptance measurement does not match its attempt.");
|
|
97
|
+
if (attempt.reusedFrom && !check.baselineReuse)
|
|
98
|
+
throw new Error("This check does not permit baseline reuse.");
|
|
99
|
+
}
|
|
100
|
+
for (const check of checks.values())
|
|
101
|
+
for (const subject of ["baseline", "candidate"]) {
|
|
102
|
+
const attempts = snapshot.attempts.filter(attempt => attempt.checkId === check.id && attempt.subject === subject).sort((a, b) => a.ordinal - b.ordinal);
|
|
103
|
+
attempts.forEach((attempt, index) => {
|
|
104
|
+
if (attempt.ordinal !== index + 1 || (index > 0 && (!terminalAttempts.has(attempts[index - 1].state) || attempts[index - 1].state === "completed")))
|
|
105
|
+
throw new Error("Retries must follow a settled failed attempt; completed evidence requires a new group to rerun.");
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (terminalGroups.has(snapshot.state) && snapshot.attempts.some(attempt => !terminalAttempts.has(attempt.state)))
|
|
109
|
+
throw new Error("Terminal groups cannot retain unsettled attempts.");
|
|
110
|
+
if (snapshot.attempts.filter(attempt => !terminalAttempts.has(attempt.state) && attempt.state !== "queued").length > snapshot.manifest.maximumParallelAttempts)
|
|
111
|
+
throw new Error("Acceptance attempts exceed the declared concurrency limit.");
|
|
112
|
+
if (Date.parse(snapshot.updatedAt) < Date.parse(snapshot.manifest.createdAt) || snapshot.attempts.some(attempt => Date.parse(attempt.createdAt) < Date.parse(snapshot.manifest.createdAt) || Date.parse(attempt.updatedAt) > Date.parse(snapshot.updatedAt)))
|
|
113
|
+
throw new Error("Acceptance group timestamps do not contain their attempts.");
|
|
114
|
+
if (snapshot.state === "completed" && snapshot.manifest.plan.checks.some(check => ["baseline", "candidate"].some(subject => !snapshot.attempts.some(attempt => attempt.checkId === check.id && attempt.subject === subject))))
|
|
115
|
+
throw new Error("Completed groups require an attempt for each declared check and subject.");
|
|
116
|
+
return snapshot;
|
|
117
|
+
}
|
|
118
|
+
/** Runtime retries append history. A terminal group is immutable; deliberate
|
|
119
|
+
* reruns create a new manifest and cannot silently replace reviewed evidence. */
|
|
120
|
+
export function assertAcceptanceGroupUpdate(previousRaw, nextRaw) {
|
|
121
|
+
const previous = assertAcceptanceGroupSnapshot(previousRaw), next = assertAcceptanceGroupSnapshot(nextRaw);
|
|
122
|
+
if (previous.manifest.contentHash !== next.manifest.contentHash || next.revision !== previous.revision + 1 || terminalGroups.has(previous.state))
|
|
123
|
+
throw new Error("Acceptance group revision or immutable identity changed.");
|
|
124
|
+
if (!groupTransitions[previous.state]?.includes(next.state))
|
|
125
|
+
throw new Error("Invalid acceptance group transition.");
|
|
126
|
+
if (["cancelling", "cleaning"].includes(previous.state) && next.attempts.length !== previous.attempts.length)
|
|
127
|
+
throw new Error("Stopping groups cannot dispatch additional attempts.");
|
|
128
|
+
if (Date.parse(next.updatedAt) < Date.parse(previous.updatedAt))
|
|
129
|
+
throw new Error("Acceptance group time moved backwards.");
|
|
130
|
+
const priorIds = new Set(previous.attempts.map(attempt => attempt.id));
|
|
131
|
+
for (const added of next.attempts.filter(attempt => !priorIds.has(attempt.id))) {
|
|
132
|
+
if (added.state !== "queued" && !(added.state === "completed" && added.reusedFrom))
|
|
133
|
+
throw new Error("New attempts must enter the queue or retain an explicitly reused baseline.");
|
|
134
|
+
if (Date.parse(added.createdAt) < Date.parse(previous.updatedAt))
|
|
135
|
+
throw new Error("New attempts cannot predate the preceding group revision.");
|
|
136
|
+
}
|
|
137
|
+
for (const before of previous.attempts) {
|
|
138
|
+
const after = next.attempts.find(attempt => attempt.id === before.id);
|
|
139
|
+
if (!after || after.checkId !== before.checkId || after.subject !== before.subject || after.ordinal !== before.ordinal || after.dispatchId !== before.dispatchId || after.createdAt !== before.createdAt || after.checkHash !== before.checkHash || contentHash(after.reusedFrom) !== contentHash(before.reusedFrom) || contentHash(after.artifact) !== contentHash(before.artifact))
|
|
140
|
+
throw new Error("Acceptance attempt identity or history changed.");
|
|
141
|
+
if (after.spendUsd < before.spendUsd || Date.parse(after.updatedAt) < Date.parse(before.updatedAt) || (terminalAttempts.has(before.state) && contentHash(before) !== contentHash(after)))
|
|
142
|
+
throw new Error("Settled acceptance evidence and spend cannot be rewritten.");
|
|
143
|
+
if (!terminalAttempts.has(before.state) && !attemptTransitions[before.state]?.includes(after.state))
|
|
144
|
+
throw new Error("Invalid acceptance attempt transition.");
|
|
145
|
+
}
|
|
146
|
+
const admitsWork = next.attempts.some(after => after.state === "dispatching" && previous.attempts.find(before => before.id === after.id)?.state === "queued");
|
|
147
|
+
if (admitsWork) {
|
|
148
|
+
const committed = (attempt) => attempt.spendUsd + attempt.reservedUsd;
|
|
149
|
+
if (next.attempts.reduce((sum, attempt) => sum + committed(attempt), 0) > next.manifest.plan.maximumSpendUsd + 1e-9 ||
|
|
150
|
+
next.manifest.plan.checks.some(check => next.attempts.filter(attempt => attempt.checkId === check.id).reduce((sum, attempt) => sum + committed(attempt), 0) > check.maximumSpendUsd + 1e-9))
|
|
151
|
+
throw new Error("Dispatch exceeds the pinned acceptance budget, including outstanding reservations and retry spend.");
|
|
152
|
+
}
|
|
153
|
+
return next;
|
|
154
|
+
}
|
|
155
|
+
/** Hosts additionally resolve the qualification reference and check current
|
|
156
|
+
* revocation/access. This verifier rejects stale or differently configured
|
|
157
|
+
* cached measurements without requiring the source candidate to have passed. */
|
|
158
|
+
export function assertAcceptanceBaselineReuse(input) {
|
|
159
|
+
const source = assertAcceptanceGroupSnapshot(input.sourceGroup);
|
|
160
|
+
const target = AcceptanceGroupManifestSchema.parse(input.targetManifest);
|
|
161
|
+
assertContentHash(target, "Acceptance group manifest");
|
|
162
|
+
assertContentHash(target.plan, "Acceptance plan");
|
|
163
|
+
const check = target.plan.checks.find(value => value.id === input.checkId);
|
|
164
|
+
const attempt = source.attempts.find(value => value.id === input.attemptId);
|
|
165
|
+
const now = Date.parse(Timestamp.parse(input.now));
|
|
166
|
+
if (!check?.baselineReuse || !attempt || !terminalGroups.has(source.state) || source.manifest.id === target.id || source.manifest.teamId !== target.teamId || attempt.state !== "completed" || !attempt.measurement || attempt.reusedFrom || !attempt.cleanupComplete || !attempt.accountingComplete)
|
|
167
|
+
throw new Error("This retained attempt is not eligible for baseline reuse.");
|
|
168
|
+
if (attempt.checkId !== check.id || attempt.checkHash !== contentHash(check) || contentHash(attempt.artifact) !== contentHash(target.baseline) || now < Date.parse(attempt.updatedAt) || now - Date.parse(attempt.updatedAt) > check.baselineReuse.maximumAgeSeconds * 1_000)
|
|
169
|
+
throw new Error("Baseline reuse requires the exact artifact, check, qualification and freshness window.");
|
|
170
|
+
const measurement = attempt.measurement;
|
|
171
|
+
if (measurement.status !== "completed" || measurement.score === null || measurement.scoredCount / measurement.expectedCount < check.minimumCoverage || measurement.populationHash !== check.populationHash || measurement.executionHash !== check.executionHash || contentHash(measurement.evaluator) !== contentHash(check.evaluator) || measurement.metric !== check.metric)
|
|
172
|
+
throw new Error("Baseline reuse requires complete compatible retained measurements.");
|
|
173
|
+
return attempt;
|
|
174
|
+
}
|
|
175
|
+
export function acceptanceGroupVerdict(raw) {
|
|
176
|
+
const snapshot = assertAcceptanceGroupSnapshot(raw);
|
|
177
|
+
const { manifest } = snapshot;
|
|
178
|
+
const latest = (checkId, subject) => snapshot.attempts
|
|
179
|
+
.filter(attempt => attempt.checkId === checkId && attempt.subject === subject)
|
|
180
|
+
.sort((a, b) => b.ordinal - a.ordinal)[0];
|
|
181
|
+
const measurements = manifest.plan.checks.map(check => ({
|
|
182
|
+
checkId: check.id,
|
|
183
|
+
baseline: latest(check.id, "baseline")?.measurement ?? null,
|
|
184
|
+
candidate: latest(check.id, "candidate")?.measurement ?? null,
|
|
185
|
+
}));
|
|
186
|
+
const verdict = evaluateAcceptancePlan({ plan: manifest.plan, baseline: manifest.baseline, candidate: manifest.candidate, measurements });
|
|
187
|
+
const reasons = [];
|
|
188
|
+
if (snapshot.state !== "completed")
|
|
189
|
+
reasons.push("group_not_completed");
|
|
190
|
+
if (snapshot.attempts.some(attempt => !attempt.cleanupComplete || !attempt.accountingComplete || attempt.reservedUsd !== 0))
|
|
191
|
+
reasons.push("cleanup_or_accounting_incomplete");
|
|
192
|
+
const spendUsd = snapshot.attempts.reduce((sum, attempt) => sum + attempt.spendUsd, 0);
|
|
193
|
+
if (spendUsd > manifest.plan.maximumSpendUsd + 1e-9 || manifest.plan.checks.some(check => snapshot.attempts.filter(attempt => attempt.checkId === check.id).reduce((sum, attempt) => sum + attempt.spendUsd, 0) > check.maximumSpendUsd + 1e-9))
|
|
194
|
+
reasons.push("acceptance_budget_exceeded");
|
|
195
|
+
for (const check of manifest.plan.checks.filter(check => check.required))
|
|
196
|
+
if (["baseline", "candidate"].some(subject => latest(check.id, subject)?.state !== "completed"))
|
|
197
|
+
reasons.push("required_attempt_incomplete");
|
|
198
|
+
return { ...verdict, passed: verdict.passed && reasons.length === 0, reasons: [...new Set(reasons)], spendUsd };
|
|
199
|
+
}
|
|
@@ -4,11 +4,17 @@ const Id = z.string().trim().min(1).max(240);
|
|
|
4
4
|
const Hash = z.string().regex(/^[a-f0-9]{64}$/);
|
|
5
5
|
export const AcceptanceCheckSchema = z.object({
|
|
6
6
|
id: Id, name: z.string().trim().min(1).max(200), required: z.boolean(),
|
|
7
|
+
role: z.enum(["quality", "retention", "diagnostic"]).optional(),
|
|
8
|
+
unit: z.string().trim().min(1).max(80).optional(),
|
|
9
|
+
baselineReuse: z.object({ maximumAgeSeconds: z.number().int().positive().max(31_536_000), qualification: ImmutableReleaseRefSchema }).strict().optional(),
|
|
7
10
|
dataset: ImmutableReleaseRefSchema, evaluator: ImmutableReleaseRefSchema,
|
|
8
11
|
executionHash: Hash, populationHash: Hash, metric: Id, direction: z.enum(["higher", "lower"]),
|
|
9
12
|
minimumCoverage: z.number().positive().max(1), threshold: z.number().finite(),
|
|
10
13
|
maximumRegression: z.number().finite().nonnegative(), maximumSpendUsd: z.number().finite().positive(),
|
|
11
|
-
}).strict()
|
|
14
|
+
}).strict().superRefine((check, ctx) => {
|
|
15
|
+
if (check.role === "diagnostic" && check.required)
|
|
16
|
+
ctx.addIssue({ code: "custom", message: "Diagnostic checks cannot be required acceptance gates." });
|
|
17
|
+
});
|
|
12
18
|
export const AcceptancePlanContentSchema = z.object({
|
|
13
19
|
schemaVersion: z.literal("openpond.acceptancePlan.v1"), id: Id, revision: z.number().int().positive(),
|
|
14
20
|
checks: z.array(AcceptanceCheckSchema).min(1).max(100),
|
package/dist/learning/index.js
CHANGED