@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evo-dev/core",
3
- "version": "0.0.1-alpha.10",
3
+ "version": "0.0.1-alpha.12",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,6 +7,8 @@ import { resolveEvolutionPaths } from "../paths.ts";
7
7
  import type {
8
8
  EvolutionEvosCase,
9
9
  EvolutionEvosCaseQueryResult,
10
+ EvolutionImprovementEvalResult,
11
+ EvolutionImprovementEvalStatus,
10
12
  EvolutionKnowledgeRecord,
11
13
  EvolutionKnowledgeReviewHistoryRecord,
12
14
  EvolutionRepoProposal,
@@ -17,7 +19,9 @@ import type {
17
19
  } from "../schema.ts";
18
20
  import {
19
21
  REVIEW_STATES,
22
+ createEvolutionImprovementEval,
20
23
  hasConcreteRepoProposalChanges,
24
+ isEvolutionImprovementEvalRecommended,
21
25
  listDirectoryNames,
22
26
  parseEvosCase,
23
27
  parseKnowledgeRecord,
@@ -368,6 +372,7 @@ export async function updateEvolutionRepoProposalReviewState(input: {
368
372
  projectKey?: string;
369
373
  reviewState: "accepted" | "rejected" | "deferred";
370
374
  expectedReviewState?: EvolutionRepoProposal["reviewState"];
375
+ improvementEvalRequested?: boolean;
371
376
  reason?: string;
372
377
  now?: string | Date;
373
378
  }): Promise<{ path: string; record: EvolutionRepoProposal; changed: boolean }> {
@@ -400,10 +405,25 @@ export async function updateEvolutionRepoProposalReviewState(input: {
400
405
  if (record.reviewState === input.reviewState) return { path, record, changed: false };
401
406
 
402
407
  const changedAt = normalizeTimestamp(input.now);
408
+ const improvementEval =
409
+ input.reviewState === "accepted"
410
+ ? createEvolutionImprovementEval({
411
+ kind: record.kind,
412
+ requested:
413
+ input.improvementEvalRequested ??
414
+ isEvolutionImprovementEvalRecommended(record.kind),
415
+ now: changedAt,
416
+ })
417
+ : createEvolutionImprovementEval({
418
+ kind: record.kind,
419
+ requested: false,
420
+ now: changedAt,
421
+ });
403
422
  const next: EvolutionRepoProposal = {
404
423
  ...record,
405
424
  reviewState: input.reviewState,
406
425
  reviewStateChangedAt: changedAt,
426
+ improvementEval,
407
427
  lastDecision: {
408
428
  state: input.reviewState,
409
429
  reason: reason === undefined ? null : sanitizeText(reason),
@@ -417,6 +437,101 @@ export async function updateEvolutionRepoProposalReviewState(input: {
417
437
  );
418
438
  }
419
439
 
440
+ export async function updateEvolutionRepoProposalImprovementEval(input: {
441
+ homeDir: string;
442
+ proposalId: string;
443
+ projectKey?: string;
444
+ status: Exclude<EvolutionImprovementEvalStatus, "not-requested" | "awaiting-eval">;
445
+ expectedStatus?: EvolutionImprovementEvalStatus;
446
+ evidenceRef?: string | null;
447
+ result?: Omit<EvolutionImprovementEvalResult, "retention">;
448
+ now?: string | Date;
449
+ }): Promise<{ path: string; record: EvolutionRepoProposal; changed: boolean }> {
450
+ return await withEvolutionReviewDecisionLock(
451
+ { homeDir: input.homeDir, kind: "repo-proposal", itemId: input.proposalId },
452
+ async () => {
453
+ const record = await readEvolutionRepoProposalById(input);
454
+ if (record.reviewState !== "accepted") {
455
+ throw new Error("Only accepted repo proposals can be evaluated.");
456
+ }
457
+ const current = record.improvementEval;
458
+ if (current === undefined || !current.requested) {
459
+ throw new Error("Repo proposal improvement evaluation was not requested.");
460
+ }
461
+ if (input.expectedStatus !== undefined && current.status !== input.expectedStatus) {
462
+ throw new Error("Repo proposal improvement evaluation state changed before update.");
463
+ }
464
+ if (
465
+ input.status !== "evaluating" &&
466
+ input.status !== "effective" &&
467
+ input.status !== "ineffective" &&
468
+ input.status !== "inconclusive"
469
+ ) {
470
+ throw new Error("Repo proposal improvement evaluation state is invalid.");
471
+ }
472
+ if (
473
+ input.status === "evaluating" &&
474
+ current.status !== "awaiting-eval" &&
475
+ current.status !== "evaluating"
476
+ ) {
477
+ throw new Error("Only an awaiting improvement evaluation can start.");
478
+ }
479
+ if (input.status !== "evaluating" && current.status !== "evaluating") {
480
+ throw new Error("Only a running improvement evaluation can complete.");
481
+ }
482
+ const completed = input.status !== "evaluating";
483
+ if (completed !== (input.result !== undefined)) {
484
+ throw new Error("Completed improvement evaluation requires a result.");
485
+ }
486
+ const changedAt = normalizeTimestamp(input.now);
487
+ const result: EvolutionImprovementEvalResult | null =
488
+ input.result === undefined
489
+ ? null
490
+ : {
491
+ targetImproved: input.result.targetImproved,
492
+ hardRegression: input.result.hardRegression,
493
+ summary: sanitizeText(input.result.summary),
494
+ assertions: input.result.assertions.slice(0, 8).map((assertion) => ({
495
+ label: sanitizeText(assertion.label),
496
+ status: assertion.status,
497
+ })),
498
+ evaluatedBy: input.result.evaluatedBy,
499
+ metrics: { ...input.result.metrics },
500
+ retention: {
501
+ metadataOnly: true,
502
+ replayInputStored: false,
503
+ replayOutputStored: false,
504
+ },
505
+ };
506
+ const evidenceRef =
507
+ input.evidenceRef === undefined
508
+ ? current.evidenceRef
509
+ : input.evidenceRef === null
510
+ ? null
511
+ : sanitizeId(input.evidenceRef);
512
+ const next: EvolutionRepoProposal = {
513
+ ...record,
514
+ improvementEval: {
515
+ ...current,
516
+ status: input.status,
517
+ evaluatedAt: completed ? changedAt : null,
518
+ evidenceRef,
519
+ result,
520
+ },
521
+ };
522
+ validateEvolutionRepoProposal(next);
523
+ const paths = resolveEvolutionPaths({
524
+ homeDir: input.homeDir,
525
+ projectKey: record.projectKey,
526
+ runId: record.provenance.runId,
527
+ });
528
+ const path = join(paths.repoProposalsDir, `${record.id}.json`);
529
+ await writeRepoProposalAndIndex({ homeDir: input.homeDir, proposal: next, changedAt });
530
+ return { path, record: next, changed: true };
531
+ },
532
+ );
533
+ }
534
+
420
535
  export async function markEvolutionRepoProposalApplied(input: {
421
536
  homeDir: string;
422
537
  proposalId: string;
@@ -487,6 +602,7 @@ async function writeRepoProposalAndIndex(input: {
487
602
  kind: proposal.kind,
488
603
  title: proposal.title,
489
604
  reviewState: proposal.reviewState,
605
+ improvementEvalStatus: proposal.improvementEval?.status ?? "not-requested",
490
606
  })),
491
607
  },
492
608
  { overwrite: true },
@@ -34,6 +34,7 @@ import type {
34
34
  export type SessionSegmentDistillationSkipReason =
35
35
  | "sensitive-segment"
36
36
  | "segment-deleted"
37
+ | "segment-raw-expired"
37
38
  | "segment-ignored"
38
39
  | "segment-rejected"
39
40
  | "no-distillation-signal";
@@ -55,6 +56,7 @@ export function analyzeSessionEvidenceSegment(input: {
55
56
  }): SessionSegmentEvidenceAnalysis {
56
57
  const { segment } = input;
57
58
  if (segment.lifecycle.status === "deleted") return skipped("segment-deleted");
59
+ if (segment.lifecycle.status === "raw-expired") return skipped("segment-raw-expired");
58
60
  if (segment.lifecycle.status === "ignored") return skipped("segment-ignored");
59
61
  if (segment.lifecycle.reviewState === "rejected") return skipped("segment-rejected");
60
62
 
@@ -241,6 +243,9 @@ function classifySegmentTrigger(
241
243
  ): { strength: EvolutionTriggerStrength; reason: EvolutionTriggerReason } {
242
244
  if (reason === "failure-signal") return { strength: "strong", reason: "failure-signal" };
243
245
  if (reason === "permission-denied") return { strength: "strong", reason: "permission-denied" };
246
+ if (reason === "historical-import") {
247
+ return { strength: "strong", reason: "explicit-command" };
248
+ }
244
249
  if (reason === "session-memory-init") return { strength: "none", reason: "none" };
245
250
  return {
246
251
  strength: strength === "strong" ? "strong" : "conditional",
@@ -258,7 +263,8 @@ function isSegmentEligibleForDistillation(
258
263
  segment.reason === "failure-signal" ||
259
264
  segment.reason === "permission-denied" ||
260
265
  segment.reason === "verification-after-fix" ||
261
- segment.reason === "explicit-memory-intent"
266
+ segment.reason === "explicit-memory-intent" ||
267
+ segment.reason === "historical-import"
262
268
  ) {
263
269
  return true;
264
270
  }
@@ -2,6 +2,8 @@ export * from "./analysis.ts";
2
2
  export * from "./paths.ts";
3
3
  export * from "./policy.ts";
4
4
  export * from "./sensitivity.ts";
5
+ export * from "./semantic-packet.ts";
6
+ export * from "./retention.ts";
5
7
  export * from "./storage.ts";
6
8
  export * from "./types.ts";
7
9
  export * from "./updater.ts";
@@ -0,0 +1,361 @@
1
+ import { createStableId, normalizeTimestamp, sha256Hex } from "../../../utils/index.ts";
2
+ import { readEvolutionReviewSnapshot } from "../../candidates/index.ts";
3
+ import type { EvolutionReviewSnapshot, SegmentEvolutionTriggerRecord } from "../../schema.ts";
4
+ import { listSegmentEvolutionTriggers } from "../../triggers/index.ts";
5
+ import {
6
+ listSessionEvidenceSegments,
7
+ readSessionEvidenceSegment,
8
+ writeSessionEvidenceSegmentAtomic,
9
+ } from "./storage.ts";
10
+ import type { SessionEvidenceSegmentV1 } from "./types.ts";
11
+
12
+ const DEFAULT_RETENTION_LIMIT = 20;
13
+ const MAX_RETENTION_LIMIT = 20;
14
+
15
+ export type HistoricalSessionEvidenceRetentionProtectionReason =
16
+ | "trigger-missing"
17
+ | "trigger-not-terminal"
18
+ | "segment-review-pending"
19
+ | "derived-review-pending"
20
+ | "retention-invalid"
21
+ | "segment-changed";
22
+
23
+ export interface PurgeExpiredHistoricalSessionEvidenceResult {
24
+ scanned: number;
25
+ due: number;
26
+ purged: number;
27
+ alreadyPurged: number;
28
+ protected: Array<{
29
+ projectKey: string;
30
+ segmentId: string;
31
+ reason: HistoricalSessionEvidenceRetentionProtectionReason;
32
+ }>;
33
+ }
34
+
35
+ export interface HistoricalSessionEvidenceRetentionInspection {
36
+ available: number;
37
+ purged: number;
38
+ overdue: number;
39
+ purgeEligible: number;
40
+ protected: number;
41
+ protectionReasons: Partial<Record<HistoricalSessionEvidenceRetentionProtectionReason, number>>;
42
+ }
43
+
44
+ export async function inspectHistoricalSessionEvidenceRetention(input: {
45
+ homeDir: string;
46
+ projectKey?: string;
47
+ now?: string | Date;
48
+ }): Promise<HistoricalSessionEvidenceRetentionInspection> {
49
+ const now = normalizeTimestamp(input.now);
50
+ const historical = (
51
+ await listSessionEvidenceSegments({
52
+ homeDir: input.homeDir,
53
+ projectKey: input.projectKey,
54
+ })
55
+ ).filter(
56
+ (segment) => segment.origin?.kind === "historical-import" && segment.retention !== undefined,
57
+ );
58
+ const result: HistoricalSessionEvidenceRetentionInspection = {
59
+ available: 0,
60
+ purged: 0,
61
+ overdue: 0,
62
+ purgeEligible: 0,
63
+ protected: 0,
64
+ protectionReasons: {},
65
+ };
66
+ const reviewSnapshots = new Map<string, Promise<EvolutionReviewSnapshot>>();
67
+ for (const segment of historical) {
68
+ if (segment.retention?.rawState === "purged") {
69
+ result.purged += 1;
70
+ continue;
71
+ }
72
+ result.available += 1;
73
+ if (!hasValidAvailableRetention(segment)) {
74
+ result.overdue += 1;
75
+ result.protected += 1;
76
+ result.protectionReasons["retention-invalid"] =
77
+ (result.protectionReasons["retention-invalid"] ?? 0) + 1;
78
+ continue;
79
+ }
80
+ if (
81
+ segment.retention === undefined ||
82
+ Date.parse(segment.retention.expiresAt) > Date.parse(now)
83
+ ) {
84
+ continue;
85
+ }
86
+ result.overdue += 1;
87
+ const protection = await findRetentionProtection({
88
+ homeDir: input.homeDir,
89
+ segment,
90
+ reviewSnapshots,
91
+ });
92
+ if (protection === null) {
93
+ result.purgeEligible += 1;
94
+ continue;
95
+ }
96
+ result.protected += 1;
97
+ result.protectionReasons[protection] = (result.protectionReasons[protection] ?? 0) + 1;
98
+ }
99
+ return result;
100
+ }
101
+
102
+ export async function purgeExpiredHistoricalSessionEvidence(input: {
103
+ homeDir: string;
104
+ projectKey?: string;
105
+ now?: string | Date;
106
+ limit?: number;
107
+ }): Promise<PurgeExpiredHistoricalSessionEvidenceResult> {
108
+ const now = normalizeTimestamp(input.now);
109
+ const limit = normalizeLimit(input.limit);
110
+ const historical = (
111
+ await listSessionEvidenceSegments({
112
+ homeDir: input.homeDir,
113
+ projectKey: input.projectKey,
114
+ })
115
+ )
116
+ .filter(
117
+ (segment) => segment.origin?.kind === "historical-import" && segment.retention !== undefined,
118
+ )
119
+ .sort(
120
+ (left, right) =>
121
+ (left.retention?.expiresAt ?? "").localeCompare(right.retention?.expiresAt ?? "") ||
122
+ left.id.localeCompare(right.id),
123
+ );
124
+ const result: PurgeExpiredHistoricalSessionEvidenceResult = {
125
+ scanned: 0,
126
+ due: 0,
127
+ purged: 0,
128
+ alreadyPurged: 0,
129
+ protected: [],
130
+ };
131
+ const reviewSnapshots = new Map<string, Promise<EvolutionReviewSnapshot>>();
132
+
133
+ result.alreadyPurged = historical.filter(
134
+ (segment) => segment.retention?.rawState === "purged",
135
+ ).length;
136
+ for (const segment of historical) {
137
+ if (result.scanned >= limit) break;
138
+ if (segment.retention === undefined || segment.retention.rawState === "purged") continue;
139
+ result.scanned += 1;
140
+ if (!hasValidAvailableRetention(segment)) {
141
+ result.due += 1;
142
+ result.protected.push({
143
+ projectKey: segment.projectKey,
144
+ segmentId: segment.id,
145
+ reason: "retention-invalid",
146
+ });
147
+ continue;
148
+ }
149
+ if (Date.parse(segment.retention.expiresAt) > Date.parse(now)) continue;
150
+ result.due += 1;
151
+
152
+ const protection = await findRetentionProtection({
153
+ homeDir: input.homeDir,
154
+ segment,
155
+ reviewSnapshots,
156
+ });
157
+ if (protection !== null) {
158
+ result.protected.push({
159
+ projectKey: segment.projectKey,
160
+ segmentId: segment.id,
161
+ reason: protection,
162
+ });
163
+ continue;
164
+ }
165
+
166
+ const current = await readSessionEvidenceSegment({
167
+ homeDir: input.homeDir,
168
+ projectKey: segment.projectKey,
169
+ sessionKey: segment.sessionKey,
170
+ segmentId: segment.id,
171
+ });
172
+ const currentTrigger = await findSegmentTrigger(input.homeDir, current);
173
+ if (
174
+ !hasValidAvailableRetention(current) ||
175
+ current.retention?.rawState !== "available" ||
176
+ current.retention.expiresAt !== segment.retention.expiresAt ||
177
+ !current.rawExcerpt.stored ||
178
+ current.rawExcerpt.sha256 !== segment.rawExcerpt.sha256 ||
179
+ current.lifecycle.reviewState !== segment.lifecycle.reviewState ||
180
+ currentTrigger === null ||
181
+ !isTerminalTrigger(currentTrigger)
182
+ ) {
183
+ result.protected.push({
184
+ projectKey: segment.projectKey,
185
+ segmentId: segment.id,
186
+ reason: "segment-changed",
187
+ });
188
+ continue;
189
+ }
190
+ const freshSnapshot = await readEvolutionReviewSnapshot({
191
+ homeDir: input.homeDir,
192
+ projectKey: current.projectKey,
193
+ });
194
+ if (hasPendingDerivedReview(freshSnapshot, current)) {
195
+ result.protected.push({
196
+ projectKey: segment.projectKey,
197
+ segmentId: segment.id,
198
+ reason: "derived-review-pending",
199
+ });
200
+ continue;
201
+ }
202
+
203
+ await writeSessionEvidenceSegmentAtomic({
204
+ homeDir: input.homeDir,
205
+ segment: createPurgedHistoricalSegment(current, now),
206
+ });
207
+ result.purged += 1;
208
+ }
209
+
210
+ return result;
211
+ }
212
+
213
+ function normalizeLimit(value: number | undefined): number {
214
+ const limit = value ?? DEFAULT_RETENTION_LIMIT;
215
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_RETENTION_LIMIT) {
216
+ throw new Error(
217
+ `Historical Session Evidence retention limit must be between 1 and ${MAX_RETENTION_LIMIT}.`,
218
+ );
219
+ }
220
+ return limit;
221
+ }
222
+
223
+ function hasValidAvailableRetention(segment: SessionEvidenceSegmentV1): boolean {
224
+ const retention = segment.retention;
225
+ return (
226
+ retention !== undefined &&
227
+ retention.rawState === "available" &&
228
+ retention.rawPurgedAt === null &&
229
+ Number.isFinite(Date.parse(retention.expiresAt)) &&
230
+ /^[a-f0-9]{64}$/u.test(retention.originalRawSha256) &&
231
+ segment.rawExcerpt.stored &&
232
+ segment.rawExcerpt.byteLength === Buffer.byteLength(segment.rawExcerpt.content, "utf8") &&
233
+ segment.rawExcerpt.sha256 === sha256Hex(segment.rawExcerpt.content) &&
234
+ retention.originalRawSha256 === segment.rawExcerpt.sha256
235
+ );
236
+ }
237
+
238
+ async function findRetentionProtection(input: {
239
+ homeDir: string;
240
+ segment: SessionEvidenceSegmentV1;
241
+ reviewSnapshots: Map<string, Promise<EvolutionReviewSnapshot>>;
242
+ }): Promise<HistoricalSessionEvidenceRetentionProtectionReason | null> {
243
+ if (
244
+ input.segment.lifecycle.reviewState === "unreviewed" ||
245
+ input.segment.lifecycle.reviewState === "deferred"
246
+ ) {
247
+ return "segment-review-pending";
248
+ }
249
+ const trigger = await findSegmentTrigger(input.homeDir, input.segment);
250
+ if (trigger === null) return "trigger-missing";
251
+ if (!isTerminalTrigger(trigger)) return "trigger-not-terminal";
252
+
253
+ let snapshot = input.reviewSnapshots.get(input.segment.projectKey);
254
+ if (snapshot === undefined) {
255
+ snapshot = readEvolutionReviewSnapshot({
256
+ homeDir: input.homeDir,
257
+ projectKey: input.segment.projectKey,
258
+ });
259
+ input.reviewSnapshots.set(input.segment.projectKey, snapshot);
260
+ }
261
+ return hasPendingDerivedReview(await snapshot, input.segment) ? "derived-review-pending" : null;
262
+ }
263
+
264
+ async function findSegmentTrigger(
265
+ homeDir: string,
266
+ segment: SessionEvidenceSegmentV1,
267
+ ): Promise<SegmentEvolutionTriggerRecord | null> {
268
+ const matches = (
269
+ await listSegmentEvolutionTriggers({
270
+ homeDir,
271
+ projectKey: segment.projectKey,
272
+ })
273
+ ).filter(
274
+ (trigger) => trigger.segmentId === segment.id && trigger.sessionKey === segment.sessionKey,
275
+ );
276
+ return matches.length === 1 ? (matches[0] ?? null) : null;
277
+ }
278
+
279
+ function isTerminalTrigger(trigger: SegmentEvolutionTriggerRecord): boolean {
280
+ return trigger.status === "consumed" || trigger.status === "skipped";
281
+ }
282
+
283
+ function hasPendingDerivedReview(
284
+ snapshot: EvolutionReviewSnapshot,
285
+ segment: SessionEvidenceSegmentV1,
286
+ ): boolean {
287
+ const runId = createStableId("segment-run", [segment.projectKey, segment.sessionKey, segment.id]);
288
+ if (
289
+ snapshot.knowledgeRecords.some(
290
+ (record) =>
291
+ record.provenance.runId === runId &&
292
+ !["auto-accepted", "accepted", "rejected", "deprecated", "superseded", "revoked"].includes(
293
+ record.reviewState,
294
+ ),
295
+ )
296
+ ) {
297
+ return true;
298
+ }
299
+ if (
300
+ snapshot.evosCases.some(
301
+ (record) =>
302
+ record.provenance.runId === runId &&
303
+ !["auto-accepted", "accepted", "rejected", "deprecated", "superseded", "revoked"].includes(
304
+ record.reviewState,
305
+ ),
306
+ )
307
+ ) {
308
+ return true;
309
+ }
310
+ if (
311
+ snapshot.repoProposals.some(
312
+ (proposal) =>
313
+ proposal.provenance.runId === runId &&
314
+ proposal.reviewState !== "rejected" &&
315
+ proposal.reviewState !== "applied",
316
+ )
317
+ ) {
318
+ return true;
319
+ }
320
+ return snapshot.reviewCandidates.some(
321
+ (candidate) =>
322
+ candidate.runId === runId &&
323
+ candidate.reviewState !== "accepted" &&
324
+ candidate.reviewState !== "rejected",
325
+ );
326
+ }
327
+
328
+ function createPurgedHistoricalSegment(
329
+ segment: SessionEvidenceSegmentV1,
330
+ now: string,
331
+ ): SessionEvidenceSegmentV1 {
332
+ const retention = segment.retention;
333
+ if (retention === undefined || retention.rawState !== "available") {
334
+ throw new Error("Historical Session Evidence raw body is not available for purge.");
335
+ }
336
+ return {
337
+ ...segment,
338
+ retention: {
339
+ ...retention,
340
+ rawState: "purged",
341
+ rawPurgedAt: now,
342
+ },
343
+ rawExcerpt: {
344
+ ...segment.rawExcerpt,
345
+ stored: false,
346
+ content: "",
347
+ byteLength: 0,
348
+ sha256: sha256Hex(""),
349
+ },
350
+ privacy: {
351
+ ...segment.privacy,
352
+ rawPromptStored: false,
353
+ rawOutputStored: false,
354
+ sourceContentStored: false,
355
+ },
356
+ lifecycle: {
357
+ ...segment.lifecycle,
358
+ status: "raw-expired",
359
+ },
360
+ };
361
+ }