@evo-dev/core 0.0.1-alpha.11 → 0.0.1-alpha.13
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
package/package.json
CHANGED
|
@@ -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
|
+
}
|