@evo-dev/core 0.0.1-alpha.10 → 0.0.1-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +9229 -6005
- package/package.json +1 -1
- package/src/evolution/candidates/index.ts +116 -0
- package/src/evolution/evidence/session-memory/analysis.ts +7 -1
- package/src/evolution/evidence/session-memory/index.ts +2 -0
- package/src/evolution/evidence/session-memory/retention.ts +361 -0
- package/src/evolution/evidence/session-memory/semantic-packet.ts +408 -0
- package/src/evolution/evidence/session-memory/storage.ts +336 -2
- package/src/evolution/evidence/session-memory/types.ts +75 -3
- package/src/evolution/imports/apply.ts +435 -0
- package/src/evolution/imports/diff.ts +472 -0
- package/src/evolution/imports/index.ts +7 -0
- package/src/evolution/imports/materialize.ts +640 -0
- package/src/evolution/imports/paths.ts +129 -0
- package/src/evolution/imports/stage.ts +414 -0
- package/src/evolution/imports/storage.ts +952 -0
- package/src/evolution/imports/types.ts +226 -0
- package/src/evolution/index.ts +3 -0
- package/src/evolution/schema.ts +44 -1
- package/src/evolution/shared.ts +187 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
export const TRAJECTORY_IMPORT_PREVIEW_SCHEMA_VERSION = 1 as const;
|
|
2
|
+
export const TRAJECTORY_IMPORT_DIAGNOSTIC_POLICY_VERSION = 1 as const;
|
|
3
|
+
|
|
4
|
+
export type TrajectoryImportSource =
|
|
5
|
+
| "claude-code"
|
|
6
|
+
| "codex"
|
|
7
|
+
| "hermes"
|
|
8
|
+
| "letta-code"
|
|
9
|
+
| "openclaw"
|
|
10
|
+
| "openhands";
|
|
11
|
+
|
|
12
|
+
export type CanonicalImportRecordType =
|
|
13
|
+
| "meta"
|
|
14
|
+
| "user"
|
|
15
|
+
| "reasoning"
|
|
16
|
+
| "assistant"
|
|
17
|
+
| "assistant-tool-call"
|
|
18
|
+
| "tool";
|
|
19
|
+
|
|
20
|
+
export type CanonicalImportSourceIdentityKind = "native" | "location" | "content" | "synthetic";
|
|
21
|
+
|
|
22
|
+
export interface CanonicalImportSnapshot {
|
|
23
|
+
source: TrajectoryImportSource;
|
|
24
|
+
explicitSourceId: string | null;
|
|
25
|
+
inputBytes: number;
|
|
26
|
+
inputFingerprint: string;
|
|
27
|
+
normalizer: {
|
|
28
|
+
package: "@letta-ai/trajectory";
|
|
29
|
+
version: string;
|
|
30
|
+
canonicalSchemaVersion: number;
|
|
31
|
+
canonicalConfigHash: string;
|
|
32
|
+
};
|
|
33
|
+
groups: Array<{
|
|
34
|
+
sourceGroupId: string;
|
|
35
|
+
records: Array<{
|
|
36
|
+
recordId: string;
|
|
37
|
+
contentHash: string;
|
|
38
|
+
sourceOrderId: string;
|
|
39
|
+
componentIndex: number;
|
|
40
|
+
sourceIdentityKind: CanonicalImportSourceIdentityKind;
|
|
41
|
+
recordType: CanonicalImportRecordType;
|
|
42
|
+
recordJson: string;
|
|
43
|
+
}>;
|
|
44
|
+
}>;
|
|
45
|
+
diagnostics: Array<{ code: string }>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type TrajectoryImportDiagnosticSeverity = "info" | "warning" | "blocker";
|
|
49
|
+
|
|
50
|
+
export interface TrajectoryImportIndexRecordV1 {
|
|
51
|
+
schemaVersion: 1;
|
|
52
|
+
kind: "trajectory-import-index-record";
|
|
53
|
+
stableRecordKeyHash: string;
|
|
54
|
+
contentDigest: string;
|
|
55
|
+
ordinal: number;
|
|
56
|
+
sourceIdentityKind: CanonicalImportSourceIdentityKind;
|
|
57
|
+
recordType: CanonicalImportRecordType;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TrajectoryImportGenerationContractV1 {
|
|
61
|
+
normalizerVersion: string;
|
|
62
|
+
canonicalSchemaVersion: number;
|
|
63
|
+
canonicalConfigHash: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface TrajectoryImportPreviousSnapshotV1 {
|
|
67
|
+
sourceKey: string;
|
|
68
|
+
generationId: string;
|
|
69
|
+
initialInputFingerprint: string;
|
|
70
|
+
generationContract: TrajectoryImportGenerationContractV1;
|
|
71
|
+
snapshotId: string;
|
|
72
|
+
records: TrajectoryImportIndexRecordV1[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TrajectoryImportMaterializationPreviewV1 {
|
|
76
|
+
selectedRecords: number;
|
|
77
|
+
estimatedSegments: number;
|
|
78
|
+
truncatedSegments: number;
|
|
79
|
+
credentialRedactions: number;
|
|
80
|
+
estimatedTriggers: number;
|
|
81
|
+
retentionDays: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface TrajectoryImportDownstreamPreviewV1 {
|
|
85
|
+
semanticProcessing: "queue-only" | "automation-eligible";
|
|
86
|
+
automationKnowledge: boolean;
|
|
87
|
+
automationSemanticKnowledge: boolean;
|
|
88
|
+
mayInvokeConfiguredProviderAfterApply: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TrajectoryImportPreviewV1 {
|
|
92
|
+
schemaVersion: 1;
|
|
93
|
+
kind: "trajectory-import-preview";
|
|
94
|
+
id: string;
|
|
95
|
+
source: TrajectoryImportSource;
|
|
96
|
+
sourceKey: string;
|
|
97
|
+
projectKey: string | null;
|
|
98
|
+
scope: {
|
|
99
|
+
roleId: string | null;
|
|
100
|
+
};
|
|
101
|
+
input: {
|
|
102
|
+
bytes: number;
|
|
103
|
+
fingerprint: string;
|
|
104
|
+
pathHash: string;
|
|
105
|
+
};
|
|
106
|
+
normalizer: {
|
|
107
|
+
version: string;
|
|
108
|
+
canonicalSchemaVersion: number;
|
|
109
|
+
canonicalConfigHash: string;
|
|
110
|
+
};
|
|
111
|
+
contracts: {
|
|
112
|
+
diagnosticPolicyVersion: number;
|
|
113
|
+
materializerVersion: number;
|
|
114
|
+
materializerConfigHash: string;
|
|
115
|
+
};
|
|
116
|
+
generationId: string;
|
|
117
|
+
snapshotId: string;
|
|
118
|
+
previousSnapshotId: string | null;
|
|
119
|
+
records: {
|
|
120
|
+
total: number;
|
|
121
|
+
new: number;
|
|
122
|
+
unchanged: number;
|
|
123
|
+
changed: number;
|
|
124
|
+
missing: number;
|
|
125
|
+
recordTypes: Record<CanonicalImportRecordType, number>;
|
|
126
|
+
identityKinds: Record<CanonicalImportSourceIdentityKind, number>;
|
|
127
|
+
};
|
|
128
|
+
diagnostics: Array<{
|
|
129
|
+
code: string;
|
|
130
|
+
severity: TrajectoryImportDiagnosticSeverity;
|
|
131
|
+
count: number;
|
|
132
|
+
}>;
|
|
133
|
+
materialization: TrajectoryImportMaterializationPreviewV1 | null;
|
|
134
|
+
downstream: TrajectoryImportDownstreamPreviewV1;
|
|
135
|
+
blockers: string[];
|
|
136
|
+
canApply: boolean;
|
|
137
|
+
rawContentStored: false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface CreateTrajectoryImportPreviewInput {
|
|
141
|
+
snapshot: CanonicalImportSnapshot;
|
|
142
|
+
projectKey?: string | null;
|
|
143
|
+
roleId?: string | null;
|
|
144
|
+
pathHash: string;
|
|
145
|
+
previous?: TrajectoryImportPreviousSnapshotV1 | null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface TrajectoryImportPreviewBuildResult {
|
|
149
|
+
preview: TrajectoryImportPreviewV1;
|
|
150
|
+
comparisonSnapshot: TrajectoryImportPreviousSnapshotV1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type TrajectoryImportApplyState = "staged" | "materializing" | "completed";
|
|
154
|
+
|
|
155
|
+
export interface TrajectoryImportManifestV1 {
|
|
156
|
+
schemaVersion: 1;
|
|
157
|
+
kind: "trajectory-import-manifest";
|
|
158
|
+
source: TrajectoryImportSource;
|
|
159
|
+
sourceKey: string;
|
|
160
|
+
projectKey: string;
|
|
161
|
+
scope: {
|
|
162
|
+
roleId: string | null;
|
|
163
|
+
};
|
|
164
|
+
currentGenerationId: string;
|
|
165
|
+
initialInputFingerprint: string;
|
|
166
|
+
generationContract: TrajectoryImportGenerationContractV1;
|
|
167
|
+
currentSnapshotId: string;
|
|
168
|
+
previousSnapshotId: string | null;
|
|
169
|
+
currentPreviewId: string;
|
|
170
|
+
currentInputFingerprint: string;
|
|
171
|
+
currentInputBytes: number;
|
|
172
|
+
currentIndexDigest: string;
|
|
173
|
+
stagedAt: string;
|
|
174
|
+
applyState: TrajectoryImportApplyState;
|
|
175
|
+
latestMaterializedSnapshotId: string | null;
|
|
176
|
+
rawContentStored: false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface TrajectoryImportReceiptV1 {
|
|
180
|
+
schemaVersion: 1;
|
|
181
|
+
kind: "trajectory-import-receipt";
|
|
182
|
+
previewId: string;
|
|
183
|
+
sourceKey: string;
|
|
184
|
+
generationId: string;
|
|
185
|
+
snapshotId: string;
|
|
186
|
+
operation: "stage" | "materialize";
|
|
187
|
+
outcome: "staged" | "completed";
|
|
188
|
+
counts: {
|
|
189
|
+
total: number;
|
|
190
|
+
new: number;
|
|
191
|
+
unchanged: number;
|
|
192
|
+
changed: number;
|
|
193
|
+
missing: number;
|
|
194
|
+
};
|
|
195
|
+
createdAt: string;
|
|
196
|
+
rawContentStored: false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface StageTrajectoryImportInput {
|
|
200
|
+
homeDir: string;
|
|
201
|
+
preview: TrajectoryImportPreviewV1;
|
|
202
|
+
comparisonSnapshot: TrajectoryImportPreviousSnapshotV1;
|
|
203
|
+
now?: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface StageTrajectoryImportResult {
|
|
207
|
+
status: "staged" | "unchanged";
|
|
208
|
+
manifest: TrajectoryImportManifestV1;
|
|
209
|
+
receipt: TrajectoryImportReceiptV1;
|
|
210
|
+
stateWrites: string[];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface TrajectoryImportMaterializationProgressV1 {
|
|
214
|
+
schemaVersion: 1;
|
|
215
|
+
kind: "trajectory-import-materialization-progress";
|
|
216
|
+
previewId: string;
|
|
217
|
+
sourceKey: string;
|
|
218
|
+
generationId: string;
|
|
219
|
+
snapshotId: string;
|
|
220
|
+
expectedSegmentIds: string[];
|
|
221
|
+
completedSegmentIds: string[];
|
|
222
|
+
expectedTriggerIds: string[];
|
|
223
|
+
completedTriggerIds: string[];
|
|
224
|
+
updatedAt: string;
|
|
225
|
+
rawContentStored: false;
|
|
226
|
+
}
|
package/src/evolution/index.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
export * from "./schema.ts";
|
|
2
2
|
export * from "./paths.ts";
|
|
3
|
+
export * from "./imports/index.ts";
|
|
3
4
|
export * from "./evidence/analysis.ts";
|
|
4
5
|
export * from "./processor/index.ts";
|
|
5
6
|
export * from "./candidates/index.ts";
|
|
6
7
|
export * from "./triggers/index.ts";
|
|
7
8
|
export * from "./formatters.ts";
|
|
8
9
|
export {
|
|
10
|
+
createEvolutionImprovementEval,
|
|
9
11
|
createEvolutionRepoProposal,
|
|
10
12
|
hasConcreteRepoProposalChanges,
|
|
13
|
+
isEvolutionImprovementEvalRecommended,
|
|
11
14
|
sanitizeProposedChange,
|
|
12
15
|
validateEvolutionDistillationBatch,
|
|
13
16
|
validateEvolutionEvidenceWindow,
|
package/src/evolution/schema.ts
CHANGED
|
@@ -73,7 +73,8 @@ export type SegmentEvolutionTriggerReason =
|
|
|
73
73
|
| "failure-signal"
|
|
74
74
|
| "permission-denied"
|
|
75
75
|
| "verification-after-fix"
|
|
76
|
-
| "explicit-memory-intent"
|
|
76
|
+
| "explicit-memory-intent"
|
|
77
|
+
| "historical-import";
|
|
77
78
|
export type SegmentEvolutionTriggerStrength = "normal" | "strong";
|
|
78
79
|
|
|
79
80
|
export type EvolutionKnowledgeKind =
|
|
@@ -110,6 +111,47 @@ export type EvolutionProposalKind =
|
|
|
110
111
|
| "docs"
|
|
111
112
|
| "engineering-practice";
|
|
112
113
|
|
|
114
|
+
export type EvolutionImprovementEvalStatus =
|
|
115
|
+
| "not-requested"
|
|
116
|
+
| "awaiting-eval"
|
|
117
|
+
| "evaluating"
|
|
118
|
+
| "effective"
|
|
119
|
+
| "ineffective"
|
|
120
|
+
| "inconclusive";
|
|
121
|
+
|
|
122
|
+
export interface EvolutionImprovementEvalResult {
|
|
123
|
+
targetImproved: boolean | null;
|
|
124
|
+
hardRegression: boolean | null;
|
|
125
|
+
summary: string;
|
|
126
|
+
assertions: Array<{
|
|
127
|
+
label: string;
|
|
128
|
+
status: "pass" | "fail" | "unknown";
|
|
129
|
+
}>;
|
|
130
|
+
evaluatedBy: "local-code-agent" | "local-eval-orchestrator";
|
|
131
|
+
metrics: {
|
|
132
|
+
durationMs: number;
|
|
133
|
+
modelCalls: number;
|
|
134
|
+
inputTokens: number | null;
|
|
135
|
+
outputTokens: number | null;
|
|
136
|
+
toolCalls: number | null;
|
|
137
|
+
};
|
|
138
|
+
retention: {
|
|
139
|
+
metadataOnly: true;
|
|
140
|
+
replayInputStored: false;
|
|
141
|
+
replayOutputStored: false;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface EvolutionImprovementEval {
|
|
146
|
+
recommended: boolean;
|
|
147
|
+
requested: boolean;
|
|
148
|
+
status: EvolutionImprovementEvalStatus;
|
|
149
|
+
requestedAt: string | null;
|
|
150
|
+
evaluatedAt: string | null;
|
|
151
|
+
evidenceRef: string | null;
|
|
152
|
+
result: EvolutionImprovementEvalResult | null;
|
|
153
|
+
}
|
|
154
|
+
|
|
113
155
|
export interface EvolutionPrivacyFields {
|
|
114
156
|
classification: "local-private";
|
|
115
157
|
rawPromptsStored: false;
|
|
@@ -355,6 +397,7 @@ export interface EvolutionRepoProposal {
|
|
|
355
397
|
reason: string | null;
|
|
356
398
|
decidedAt: string;
|
|
357
399
|
};
|
|
400
|
+
improvementEval?: EvolutionImprovementEval;
|
|
358
401
|
apply: {
|
|
359
402
|
autoApply: false;
|
|
360
403
|
requiresExplicitCommand: true;
|
package/src/evolution/shared.ts
CHANGED
|
@@ -13,6 +13,8 @@ import type {
|
|
|
13
13
|
EvolutionEvidenceSourceKind,
|
|
14
14
|
EvolutionEvidenceWindow,
|
|
15
15
|
EvolutionEvosCase,
|
|
16
|
+
EvolutionImprovementEval,
|
|
17
|
+
EvolutionImprovementEvalStatus,
|
|
16
18
|
EvolutionKnowledgeKind,
|
|
17
19
|
EvolutionKnowledgeRecord,
|
|
18
20
|
EvolutionNormalizedEventType,
|
|
@@ -75,6 +77,14 @@ export const PROPOSAL_KINDS: readonly EvolutionProposalKind[] = [
|
|
|
75
77
|
"docs",
|
|
76
78
|
"engineering-practice",
|
|
77
79
|
];
|
|
80
|
+
export const IMPROVEMENT_EVAL_STATUSES: readonly EvolutionImprovementEvalStatus[] = [
|
|
81
|
+
"not-requested",
|
|
82
|
+
"awaiting-eval",
|
|
83
|
+
"evaluating",
|
|
84
|
+
"effective",
|
|
85
|
+
"ineffective",
|
|
86
|
+
"inconclusive",
|
|
87
|
+
];
|
|
78
88
|
export const EVENT_KINDS: readonly EvolutionEvidenceEventKind[] = [
|
|
79
89
|
"trace",
|
|
80
90
|
"tool-call",
|
|
@@ -148,6 +158,7 @@ export const SEGMENT_TRIGGER_REASONS: readonly SegmentEvolutionTriggerReason[] =
|
|
|
148
158
|
"permission-denied",
|
|
149
159
|
"verification-after-fix",
|
|
150
160
|
"explicit-memory-intent",
|
|
161
|
+
"historical-import",
|
|
151
162
|
];
|
|
152
163
|
export const SEGMENT_TRIGGER_STRENGTHS: readonly SegmentEvolutionTriggerStrength[] = [
|
|
153
164
|
"normal",
|
|
@@ -352,6 +363,9 @@ export function createEvolutionRepoProposal(
|
|
|
352
363
|
? {}
|
|
353
364
|
: { proposedChange: sanitizeProposedChange(file.proposedChange) }),
|
|
354
365
|
})),
|
|
366
|
+
...(input.improvementEval === undefined
|
|
367
|
+
? {}
|
|
368
|
+
: { improvementEval: sanitizeEvolutionImprovementEval(input.improvementEval) }),
|
|
355
369
|
privacy: { ...input.privacy },
|
|
356
370
|
};
|
|
357
371
|
proposal.privacy.internalLinksStored =
|
|
@@ -408,6 +422,9 @@ export function validateEvolutionRepoProposal(proposal: EvolutionRepoProposal):
|
|
|
408
422
|
throw new Error("Repo proposal decision timestamp must be valid.");
|
|
409
423
|
}
|
|
410
424
|
}
|
|
425
|
+
if (proposal.improvementEval !== undefined) {
|
|
426
|
+
validateEvolutionImprovementEval(proposal.improvementEval);
|
|
427
|
+
}
|
|
411
428
|
if (proposal.apply.autoApply !== false || proposal.apply.requiresExplicitCommand !== true) {
|
|
412
429
|
throw new Error("Repo proposal must require explicit apply.");
|
|
413
430
|
}
|
|
@@ -416,6 +433,28 @@ export function validateEvolutionRepoProposal(proposal: EvolutionRepoProposal):
|
|
|
416
433
|
assertNoForbiddenRawFields(proposal);
|
|
417
434
|
}
|
|
418
435
|
|
|
436
|
+
export function isEvolutionImprovementEvalRecommended(kind: EvolutionProposalKind): boolean {
|
|
437
|
+
return !["ci", "test", "docs"].includes(kind);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function createEvolutionImprovementEval(input: {
|
|
441
|
+
kind: EvolutionProposalKind;
|
|
442
|
+
requested?: boolean;
|
|
443
|
+
now?: string | Date;
|
|
444
|
+
}): EvolutionImprovementEval {
|
|
445
|
+
const requested = input.requested ?? false;
|
|
446
|
+
const requestedAt = requested ? normalizeTimestampValue(input.now) : null;
|
|
447
|
+
return {
|
|
448
|
+
recommended: isEvolutionImprovementEvalRecommended(input.kind),
|
|
449
|
+
requested,
|
|
450
|
+
status: requested ? "awaiting-eval" : "not-requested",
|
|
451
|
+
requestedAt,
|
|
452
|
+
evaluatedAt: null,
|
|
453
|
+
evidenceRef: null,
|
|
454
|
+
result: null,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
419
458
|
export function hasConcreteRepoProposalChanges(proposal: EvolutionRepoProposal): boolean {
|
|
420
459
|
return (
|
|
421
460
|
proposal.targetRepoPath !== null &&
|
|
@@ -427,6 +466,154 @@ export function hasConcreteRepoProposalChanges(proposal: EvolutionRepoProposal):
|
|
|
427
466
|
);
|
|
428
467
|
}
|
|
429
468
|
|
|
469
|
+
function validateEvolutionImprovementEval(value: EvolutionImprovementEval): void {
|
|
470
|
+
if (!isRecord(value)) throw new Error("Repo proposal improvementEval must be an object.");
|
|
471
|
+
if (typeof value.recommended !== "boolean" || typeof value.requested !== "boolean") {
|
|
472
|
+
throw new Error("Repo proposal improvementEval flags must be booleans.");
|
|
473
|
+
}
|
|
474
|
+
assertEnum("improvementEval.status", value.status, IMPROVEMENT_EVAL_STATUSES);
|
|
475
|
+
assertNullableTimestamp("improvementEval.requestedAt", value.requestedAt);
|
|
476
|
+
assertNullableTimestamp("improvementEval.evaluatedAt", value.evaluatedAt);
|
|
477
|
+
if (value.evidenceRef !== null) assertString("improvementEval.evidenceRef", value.evidenceRef);
|
|
478
|
+
if (!value.requested) {
|
|
479
|
+
if (
|
|
480
|
+
value.status !== "not-requested" ||
|
|
481
|
+
value.requestedAt !== null ||
|
|
482
|
+
value.evaluatedAt !== null ||
|
|
483
|
+
value.result !== null
|
|
484
|
+
) {
|
|
485
|
+
throw new Error("A non-requested improvement eval cannot have evaluation state.");
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (value.status === "not-requested" || value.requestedAt === null) {
|
|
490
|
+
throw new Error("A requested improvement eval must be awaiting or completed.");
|
|
491
|
+
}
|
|
492
|
+
const completed = ["effective", "ineffective", "inconclusive"].includes(value.status);
|
|
493
|
+
if (completed !== (value.result !== null && value.evaluatedAt !== null)) {
|
|
494
|
+
throw new Error("Completed improvement eval state and result must be stored together.");
|
|
495
|
+
}
|
|
496
|
+
if (value.result === null) return;
|
|
497
|
+
if (value.result.targetImproved !== null && typeof value.result.targetImproved !== "boolean") {
|
|
498
|
+
throw new Error("Improvement eval targetImproved must be boolean or null.");
|
|
499
|
+
}
|
|
500
|
+
if (value.result.hardRegression !== null && typeof value.result.hardRegression !== "boolean") {
|
|
501
|
+
throw new Error("Improvement eval hardRegression must be boolean or null.");
|
|
502
|
+
}
|
|
503
|
+
if (
|
|
504
|
+
value.status === "effective" &&
|
|
505
|
+
(value.result.targetImproved !== true || value.result.hardRegression !== false)
|
|
506
|
+
) {
|
|
507
|
+
throw new Error("An effective improvement eval must improve the target without regression.");
|
|
508
|
+
}
|
|
509
|
+
if (
|
|
510
|
+
value.status === "ineffective" &&
|
|
511
|
+
value.result.targetImproved !== false &&
|
|
512
|
+
value.result.hardRegression !== true
|
|
513
|
+
) {
|
|
514
|
+
throw new Error("An ineffective improvement eval must fail the target or a hard gate.");
|
|
515
|
+
}
|
|
516
|
+
if (
|
|
517
|
+
value.status === "inconclusive" &&
|
|
518
|
+
(value.result.targetImproved === false ||
|
|
519
|
+
value.result.hardRegression === true ||
|
|
520
|
+
(value.result.targetImproved === true && value.result.hardRegression === false))
|
|
521
|
+
) {
|
|
522
|
+
throw new Error("An inconclusive improvement eval cannot contain a definitive gate result.");
|
|
523
|
+
}
|
|
524
|
+
assertString("improvementEval.result.summary", value.result.summary);
|
|
525
|
+
if (!Array.isArray(value.result.assertions) || value.result.assertions.length > 8) {
|
|
526
|
+
throw new Error("Improvement eval assertions must contain at most 8 items.");
|
|
527
|
+
}
|
|
528
|
+
for (const assertion of value.result.assertions) {
|
|
529
|
+
assertString("improvementEval.result.assertions.label", assertion.label);
|
|
530
|
+
assertEnum("improvementEval.result.assertions.status", assertion.status, [
|
|
531
|
+
"pass",
|
|
532
|
+
"fail",
|
|
533
|
+
"unknown",
|
|
534
|
+
]);
|
|
535
|
+
}
|
|
536
|
+
if (
|
|
537
|
+
value.result.evaluatedBy !== "local-code-agent" &&
|
|
538
|
+
value.result.evaluatedBy !== "local-eval-orchestrator"
|
|
539
|
+
) {
|
|
540
|
+
throw new Error("Improvement eval evaluatedBy is invalid.");
|
|
541
|
+
}
|
|
542
|
+
if (!isRecord(value.result.metrics)) {
|
|
543
|
+
throw new Error("Improvement eval metrics must be an object.");
|
|
544
|
+
}
|
|
545
|
+
if (
|
|
546
|
+
!isNonNegativeInteger(value.result.metrics.durationMs) ||
|
|
547
|
+
!isNonNegativeInteger(value.result.metrics.modelCalls)
|
|
548
|
+
) {
|
|
549
|
+
throw new Error("Improvement eval duration and model call count are required.");
|
|
550
|
+
}
|
|
551
|
+
if (
|
|
552
|
+
(value.result.evaluatedBy === "local-code-agent" && value.result.metrics.modelCalls !== 1) ||
|
|
553
|
+
(value.result.evaluatedBy === "local-eval-orchestrator" &&
|
|
554
|
+
value.result.metrics.modelCalls !== 0)
|
|
555
|
+
) {
|
|
556
|
+
throw new Error("Improvement eval model call metadata is inconsistent.");
|
|
557
|
+
}
|
|
558
|
+
for (const [field, metric] of [
|
|
559
|
+
["inputTokens", value.result.metrics.inputTokens],
|
|
560
|
+
["outputTokens", value.result.metrics.outputTokens],
|
|
561
|
+
["toolCalls", value.result.metrics.toolCalls],
|
|
562
|
+
] as const) {
|
|
563
|
+
if (metric !== null && !isNonNegativeInteger(metric)) {
|
|
564
|
+
throw new Error(`Improvement eval metric ${field} must be a non-negative integer or null.`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (
|
|
568
|
+
value.result.retention.metadataOnly !== true ||
|
|
569
|
+
value.result.retention.replayInputStored !== false ||
|
|
570
|
+
value.result.retention.replayOutputStored !== false
|
|
571
|
+
) {
|
|
572
|
+
throw new Error("Improvement eval must retain metadata-only results.");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function sanitizeEvolutionImprovementEval(
|
|
577
|
+
value: EvolutionImprovementEval,
|
|
578
|
+
): EvolutionImprovementEval {
|
|
579
|
+
return {
|
|
580
|
+
...value,
|
|
581
|
+
evidenceRef: value.evidenceRef === null ? null : sanitizeId(value.evidenceRef),
|
|
582
|
+
result:
|
|
583
|
+
value.result === null
|
|
584
|
+
? null
|
|
585
|
+
: {
|
|
586
|
+
...value.result,
|
|
587
|
+
summary: sanitizeText(value.result.summary),
|
|
588
|
+
assertions: value.result.assertions.slice(0, 8).map((assertion) => ({
|
|
589
|
+
label: sanitizeText(assertion.label),
|
|
590
|
+
status: assertion.status,
|
|
591
|
+
})),
|
|
592
|
+
metrics: { ...value.result.metrics },
|
|
593
|
+
retention: { ...value.result.retention },
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function assertNullableTimestamp(field: string, value: string | null): void {
|
|
599
|
+
if (value !== null && Number.isNaN(new Date(value).getTime())) {
|
|
600
|
+
throw new Error(`${field} must be null or a valid timestamp.`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function isNonNegativeInteger(value: unknown): value is number {
|
|
605
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function normalizeTimestampValue(value: string | Date | undefined): string {
|
|
609
|
+
const timestamp =
|
|
610
|
+
value instanceof Date ? value.toISOString() : (value ?? new Date().toISOString());
|
|
611
|
+
if (Number.isNaN(new Date(timestamp).getTime())) {
|
|
612
|
+
throw new Error("Improvement eval timestamp must be valid.");
|
|
613
|
+
}
|
|
614
|
+
return timestamp;
|
|
615
|
+
}
|
|
616
|
+
|
|
430
617
|
export function validateEvolutionReviewCandidate(candidate: EvolutionReviewCandidate): void {
|
|
431
618
|
if (!isRecord(candidate)) throw new Error("Review candidate must be an object.");
|
|
432
619
|
if (candidate.schemaVersion !== 1) {
|