@evo-dev/core 0.0.1-alpha.4 → 0.0.1-alpha.5
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/config/index.js +3 -3
- package/dist/index.js +860 -612
- package/package.json +1 -1
- package/src/evolution/candidates/index.ts +314 -44
- package/src/evolution/evidence/session-memory/index.ts +1 -0
- package/src/evolution/evidence/session-memory/segment.ts +2 -2
- package/src/evolution/evidence/session-memory/storage.ts +123 -2
- package/src/evolution/evidence/session-memory/types.ts +3 -3
- package/src/evolution/index.ts +4 -0
- package/src/evolution/knowledge/index.ts +9 -74
- package/src/evolution/paths.ts +3 -0
- package/src/evolution/processor/distillation.ts +42 -33
- package/src/evolution/processor/process.ts +3 -74
- package/src/evolution/schema.ts +23 -1
- package/src/evolution/shared.ts +62 -2
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
3
4
|
import { resolveEvoDevPaths } from "../../config/paths.ts";
|
|
4
5
|
import { pathExists, readJsonFiles, writeJsonFile as writeJson } from "../../utils/index.ts";
|
|
5
6
|
import { resolveEvolutionPaths } from "../paths.ts";
|
|
@@ -7,6 +8,7 @@ import type {
|
|
|
7
8
|
EvolutionEvosCase,
|
|
8
9
|
EvolutionEvosCaseQueryResult,
|
|
9
10
|
EvolutionKnowledgeRecord,
|
|
11
|
+
EvolutionKnowledgeReviewHistoryRecord,
|
|
10
12
|
EvolutionRepoProposal,
|
|
11
13
|
EvolutionReviewCandidate,
|
|
12
14
|
EvolutionReviewSnapshot,
|
|
@@ -14,6 +16,8 @@ import type {
|
|
|
14
16
|
EvolutionTriggerRecord,
|
|
15
17
|
} from "../schema.ts";
|
|
16
18
|
import {
|
|
19
|
+
REVIEW_STATES,
|
|
20
|
+
hasConcreteRepoProposalChanges,
|
|
17
21
|
listDirectoryNames,
|
|
18
22
|
parseEvosCase,
|
|
19
23
|
parseKnowledgeRecord,
|
|
@@ -22,10 +26,51 @@ import {
|
|
|
22
26
|
parseTrigger,
|
|
23
27
|
sanitizeId,
|
|
24
28
|
sanitizeStorageId,
|
|
29
|
+
sanitizeText,
|
|
25
30
|
uniqueSorted,
|
|
26
31
|
validateEvolutionKnowledgeRecord,
|
|
32
|
+
validateEvolutionRepoProposal,
|
|
27
33
|
} from "../shared.ts";
|
|
28
34
|
|
|
35
|
+
const REVIEW_LOCK_WAIT_MS = 2_000;
|
|
36
|
+
const REVIEW_LOCK_RETRY_MS = 10;
|
|
37
|
+
|
|
38
|
+
export async function withEvolutionReviewDecisionLock<T>(
|
|
39
|
+
input: {
|
|
40
|
+
homeDir: string;
|
|
41
|
+
kind: "learning" | "knowledge" | "repo-proposal";
|
|
42
|
+
itemId: string;
|
|
43
|
+
},
|
|
44
|
+
operation: () => Promise<T>,
|
|
45
|
+
): Promise<T> {
|
|
46
|
+
const paths = resolveEvoDevPaths(input.homeDir);
|
|
47
|
+
const key = createHash("sha256").update(`${input.kind}\0${input.itemId}`).digest("hex");
|
|
48
|
+
const lockPath = join(paths.stateDir, "evolution", ".review-locks", `${key}.lock`);
|
|
49
|
+
const deadline = Date.now() + REVIEW_LOCK_WAIT_MS;
|
|
50
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
51
|
+
|
|
52
|
+
while (true) {
|
|
53
|
+
try {
|
|
54
|
+
await writeFile(
|
|
55
|
+
lockPath,
|
|
56
|
+
`${JSON.stringify({ version: 1, kind: input.kind, acquiredAt: new Date().toISOString(), pid: process.pid })}\n`,
|
|
57
|
+
{ encoding: "utf8", flag: "wx" },
|
|
58
|
+
);
|
|
59
|
+
break;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (!isAlreadyExistsError(error)) throw error;
|
|
62
|
+
if (Date.now() >= deadline) throw new Error("Review decision lock is busy or stale.");
|
|
63
|
+
await sleep(REVIEW_LOCK_RETRY_MS);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
return await operation();
|
|
69
|
+
} finally {
|
|
70
|
+
await rm(lockPath, { force: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
29
74
|
export async function readEvolutionReviewSnapshot(input: {
|
|
30
75
|
homeDir: string;
|
|
31
76
|
projectKey?: string;
|
|
@@ -113,6 +158,37 @@ export async function listEvolutionKnowledgeRecords(input: {
|
|
|
113
158
|
).knowledgeRecords.sort((left, right) => left.id.localeCompare(right.id));
|
|
114
159
|
}
|
|
115
160
|
|
|
161
|
+
export async function listEvolutionKnowledgeReviewHistory(input: {
|
|
162
|
+
homeDir: string;
|
|
163
|
+
projectKey?: string;
|
|
164
|
+
}): Promise<EvolutionKnowledgeReviewHistoryRecord[]> {
|
|
165
|
+
const paths = resolveEvoDevPaths(input.homeDir);
|
|
166
|
+
const projectKeys =
|
|
167
|
+
input.projectKey === undefined
|
|
168
|
+
? await listDirectoryNames(join(paths.stateDir, "evolution"))
|
|
169
|
+
: [sanitizeStorageId("projectKey", input.projectKey)];
|
|
170
|
+
const records: EvolutionKnowledgeReviewHistoryRecord[] = [];
|
|
171
|
+
|
|
172
|
+
for (const projectKey of projectKeys) {
|
|
173
|
+
const historyPath = resolveEvolutionPaths({
|
|
174
|
+
homeDir: input.homeDir,
|
|
175
|
+
projectKey,
|
|
176
|
+
runId: "review",
|
|
177
|
+
}).knowledgeReviewHistoryPath;
|
|
178
|
+
if (!(await pathExists(historyPath))) continue;
|
|
179
|
+
for (const line of (await readFile(historyPath, "utf8")).split("\n")) {
|
|
180
|
+
if (line.trim() === "") continue;
|
|
181
|
+
records.push(parseKnowledgeReviewHistoryRecord(JSON.parse(line) as unknown));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return records.sort((left, right) => {
|
|
186
|
+
const changedAt = left.changedAt.localeCompare(right.changedAt);
|
|
187
|
+
if (changedAt !== 0) return changedAt;
|
|
188
|
+
return left.knowledgeId.localeCompare(right.knowledgeId);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
116
192
|
export async function listEvolutionEvosCases(input: {
|
|
117
193
|
homeDir: string;
|
|
118
194
|
projectKey?: string;
|
|
@@ -186,50 +262,244 @@ export async function updateEvolutionKnowledgeReviewState(input: {
|
|
|
186
262
|
knowledgeId: string;
|
|
187
263
|
projectKey?: string;
|
|
188
264
|
reviewState: "accepted" | "rejected" | "deferred";
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
265
|
+
expectedReviewState?: EvolutionReviewState;
|
|
266
|
+
now?: string | Date;
|
|
267
|
+
}): Promise<{ path: string; record: EvolutionKnowledgeRecord; changed: boolean }> {
|
|
268
|
+
return await withEvolutionReviewDecisionLock(
|
|
269
|
+
{ homeDir: input.homeDir, kind: "knowledge", itemId: input.knowledgeId },
|
|
270
|
+
async () => {
|
|
271
|
+
const record = await readEvolutionKnowledgeRecordById(input);
|
|
272
|
+
if (
|
|
273
|
+
input.expectedReviewState !== undefined &&
|
|
274
|
+
record.reviewState !== input.expectedReviewState
|
|
275
|
+
) {
|
|
276
|
+
throw new Error("Knowledge review state changed before the decision was applied.");
|
|
277
|
+
}
|
|
278
|
+
const paths = resolveEvolutionPaths({
|
|
279
|
+
homeDir: input.homeDir,
|
|
280
|
+
projectKey: record.projectKey,
|
|
281
|
+
runId: "review",
|
|
282
|
+
});
|
|
283
|
+
const path = join(paths.knowledgeRecordsDir, `${record.id}.json`);
|
|
284
|
+
if (record.reviewState === input.reviewState) return { path, record, changed: false };
|
|
285
|
+
|
|
286
|
+
const changedAt = normalizeTimestamp(input.now);
|
|
287
|
+
const next: EvolutionKnowledgeRecord = {
|
|
288
|
+
...record,
|
|
289
|
+
reviewState: input.reviewState,
|
|
290
|
+
authority: input.reviewState === "accepted" ? "reviewed" : "contextual",
|
|
291
|
+
runtime: {
|
|
292
|
+
...record.runtime,
|
|
293
|
+
// Legacy JSON review records are not active OKF concepts.
|
|
294
|
+
canLoad: false,
|
|
295
|
+
hardBlocking: false,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
validateEvolutionKnowledgeRecord(next);
|
|
299
|
+
await writeJson(path, next, { overwrite: true });
|
|
300
|
+
const allProjectKnowledgeRecords = await readJsonFiles(
|
|
301
|
+
paths.knowledgeRecordsDir,
|
|
302
|
+
parseKnowledgeRecord,
|
|
303
|
+
);
|
|
304
|
+
await writeJson(
|
|
305
|
+
paths.knowledgeIndexPath,
|
|
306
|
+
{
|
|
307
|
+
version: 1,
|
|
308
|
+
kind: "evolution-knowledge-index",
|
|
309
|
+
projectKey: next.projectKey,
|
|
310
|
+
updatedAt: changedAt,
|
|
311
|
+
records: allProjectKnowledgeRecords.map((item) => ({
|
|
312
|
+
id: item.id,
|
|
313
|
+
kind: item.kind,
|
|
314
|
+
title: item.title,
|
|
315
|
+
roleTags: item.roleTags,
|
|
316
|
+
tags: item.tags,
|
|
317
|
+
reviewState: item.reviewState,
|
|
318
|
+
authority: item.authority,
|
|
319
|
+
confidence: item.confidence,
|
|
320
|
+
runtime: item.runtime,
|
|
321
|
+
})),
|
|
322
|
+
},
|
|
323
|
+
{ overwrite: true },
|
|
324
|
+
);
|
|
325
|
+
const history: EvolutionKnowledgeReviewHistoryRecord = {
|
|
326
|
+
version: 1,
|
|
327
|
+
kind: "evolution-knowledge-review-state-changed",
|
|
328
|
+
knowledgeId: next.id,
|
|
329
|
+
projectKey: next.projectKey,
|
|
330
|
+
roleTags: uniqueSorted(next.roleTags),
|
|
331
|
+
artifactCreatedAt: next.provenance.createdAt,
|
|
332
|
+
previousReviewState: record.reviewState,
|
|
333
|
+
nextReviewState: next.reviewState,
|
|
334
|
+
changedAt,
|
|
335
|
+
metadataOnly: true,
|
|
336
|
+
};
|
|
337
|
+
await mkdir(dirname(paths.knowledgeReviewHistoryPath), { recursive: true });
|
|
338
|
+
await writeFile(paths.knowledgeReviewHistoryPath, `${JSON.stringify(history)}\n`, {
|
|
339
|
+
encoding: "utf8",
|
|
340
|
+
flag: "a",
|
|
341
|
+
});
|
|
342
|
+
return { path, record: next, changed: true };
|
|
199
343
|
},
|
|
200
|
-
};
|
|
201
|
-
validateEvolutionKnowledgeRecord(next);
|
|
202
|
-
const paths = resolveEvolutionPaths({
|
|
203
|
-
homeDir: input.homeDir,
|
|
204
|
-
projectKey: next.projectKey,
|
|
205
|
-
runId: "review",
|
|
206
|
-
});
|
|
207
|
-
const path = join(paths.knowledgeRecordsDir, `${next.id}.json`);
|
|
208
|
-
await writeJson(path, next, { overwrite: true });
|
|
209
|
-
const allProjectKnowledgeRecords = await readJsonFiles(
|
|
210
|
-
paths.knowledgeRecordsDir,
|
|
211
|
-
parseKnowledgeRecord,
|
|
212
344
|
);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function readEvolutionRepoProposalById(input: {
|
|
348
|
+
homeDir: string;
|
|
349
|
+
proposalId: string;
|
|
350
|
+
projectKey?: string;
|
|
351
|
+
}): Promise<EvolutionRepoProposal> {
|
|
352
|
+
const matches = (
|
|
353
|
+
await readEvolutionReviewSnapshot({
|
|
354
|
+
homeDir: input.homeDir,
|
|
355
|
+
projectKey: input.projectKey,
|
|
356
|
+
})
|
|
357
|
+
).repoProposals.filter((proposal) => proposal.id === input.proposalId);
|
|
358
|
+
if (matches.length === 0) throw new Error(`Repo proposal not found: ${input.proposalId}`);
|
|
359
|
+
if (matches.length > 1) {
|
|
360
|
+
throw new Error(`Repo proposal id is ambiguous across runs: ${input.proposalId}`);
|
|
361
|
+
}
|
|
362
|
+
return matches[0] as EvolutionRepoProposal;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export async function updateEvolutionRepoProposalReviewState(input: {
|
|
366
|
+
homeDir: string;
|
|
367
|
+
proposalId: string;
|
|
368
|
+
projectKey?: string;
|
|
369
|
+
reviewState: "accepted" | "rejected" | "deferred";
|
|
370
|
+
expectedReviewState?: EvolutionRepoProposal["reviewState"];
|
|
371
|
+
reason?: string;
|
|
372
|
+
now?: string | Date;
|
|
373
|
+
}): Promise<{ path: string; record: EvolutionRepoProposal; changed: boolean }> {
|
|
374
|
+
return await withEvolutionReviewDecisionLock(
|
|
375
|
+
{ homeDir: input.homeDir, kind: "repo-proposal", itemId: input.proposalId },
|
|
376
|
+
async () => {
|
|
377
|
+
const record = await readEvolutionRepoProposalById(input);
|
|
378
|
+
if (!hasConcreteRepoProposalChanges(record)) {
|
|
379
|
+
throw new Error("Repo proposal has no concrete repository changes to review.");
|
|
380
|
+
}
|
|
381
|
+
const reason = input.reason?.trim();
|
|
382
|
+
if (input.reviewState === "rejected" && reason === undefined) {
|
|
383
|
+
throw new Error("Rejecting a repo proposal requires a reason.");
|
|
384
|
+
}
|
|
385
|
+
if (reason !== undefined && (reason === "" || reason.length > 500)) {
|
|
386
|
+
throw new Error("Repo proposal review reason must be 1-500 characters.");
|
|
387
|
+
}
|
|
388
|
+
if (
|
|
389
|
+
input.expectedReviewState !== undefined &&
|
|
390
|
+
record.reviewState !== input.expectedReviewState
|
|
391
|
+
) {
|
|
392
|
+
throw new Error("Repo proposal review state changed before the decision was applied.");
|
|
393
|
+
}
|
|
394
|
+
const paths = resolveEvolutionPaths({
|
|
395
|
+
homeDir: input.homeDir,
|
|
396
|
+
projectKey: record.projectKey,
|
|
397
|
+
runId: record.provenance.runId,
|
|
398
|
+
});
|
|
399
|
+
const path = join(paths.repoProposalsDir, `${record.id}.json`);
|
|
400
|
+
if (record.reviewState === input.reviewState) return { path, record, changed: false };
|
|
401
|
+
|
|
402
|
+
const changedAt = normalizeTimestamp(input.now);
|
|
403
|
+
const next: EvolutionRepoProposal = {
|
|
404
|
+
...record,
|
|
405
|
+
reviewState: input.reviewState,
|
|
406
|
+
reviewStateChangedAt: changedAt,
|
|
407
|
+
lastDecision: {
|
|
408
|
+
state: input.reviewState,
|
|
409
|
+
reason: reason === undefined ? null : sanitizeText(reason),
|
|
410
|
+
decidedAt: changedAt,
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
validateEvolutionRepoProposal(next);
|
|
414
|
+
await writeJson(path, next, { overwrite: true });
|
|
415
|
+
const allRunProposals = await readJsonFiles(paths.repoProposalsDir, parseRepoProposal);
|
|
416
|
+
await writeJson(
|
|
417
|
+
paths.repoProposalsIndexPath,
|
|
418
|
+
{
|
|
419
|
+
schemaVersion: 1,
|
|
420
|
+
projectKey: next.projectKey,
|
|
421
|
+
runId: next.provenance.runId,
|
|
422
|
+
updatedAt: changedAt,
|
|
423
|
+
proposals: allRunProposals.map((proposal) => ({
|
|
424
|
+
id: proposal.id,
|
|
425
|
+
kind: proposal.kind,
|
|
426
|
+
title: proposal.title,
|
|
427
|
+
reviewState: proposal.reviewState,
|
|
428
|
+
})),
|
|
429
|
+
},
|
|
430
|
+
{ overwrite: true },
|
|
431
|
+
);
|
|
432
|
+
return { path, record: next, changed: true };
|
|
231
433
|
},
|
|
232
|
-
{ overwrite: true },
|
|
233
434
|
);
|
|
234
|
-
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function parseKnowledgeReviewHistoryRecord(value: unknown): EvolutionKnowledgeReviewHistoryRecord {
|
|
438
|
+
if (!isRecord(value)) throw new Error("Knowledge review history record must be an object.");
|
|
439
|
+
const allowedKeys = new Set([
|
|
440
|
+
"version",
|
|
441
|
+
"kind",
|
|
442
|
+
"knowledgeId",
|
|
443
|
+
"projectKey",
|
|
444
|
+
"roleTags",
|
|
445
|
+
"artifactCreatedAt",
|
|
446
|
+
"previousReviewState",
|
|
447
|
+
"nextReviewState",
|
|
448
|
+
"changedAt",
|
|
449
|
+
"metadataOnly",
|
|
450
|
+
]);
|
|
451
|
+
if (Object.keys(value).some((key) => !allowedKeys.has(key))) {
|
|
452
|
+
throw new Error("Knowledge review history record contains unsupported fields.");
|
|
453
|
+
}
|
|
454
|
+
if (
|
|
455
|
+
value.version !== 1 ||
|
|
456
|
+
value.kind !== "evolution-knowledge-review-state-changed" ||
|
|
457
|
+
typeof value.knowledgeId !== "string" ||
|
|
458
|
+
typeof value.projectKey !== "string" ||
|
|
459
|
+
!Array.isArray(value.roleTags) ||
|
|
460
|
+
!value.roleTags.every((roleId) => typeof roleId === "string") ||
|
|
461
|
+
typeof value.artifactCreatedAt !== "string" ||
|
|
462
|
+
typeof value.previousReviewState !== "string" ||
|
|
463
|
+
!REVIEW_STATES.includes(value.previousReviewState as EvolutionReviewState) ||
|
|
464
|
+
typeof value.nextReviewState !== "string" ||
|
|
465
|
+
!REVIEW_STATES.includes(value.nextReviewState as EvolutionReviewState) ||
|
|
466
|
+
typeof value.changedAt !== "string" ||
|
|
467
|
+
value.metadataOnly !== true
|
|
468
|
+
) {
|
|
469
|
+
throw new Error("Invalid knowledge review history record.");
|
|
470
|
+
}
|
|
471
|
+
normalizeTimestamp(value.artifactCreatedAt);
|
|
472
|
+
normalizeTimestamp(value.changedAt);
|
|
473
|
+
return {
|
|
474
|
+
version: 1,
|
|
475
|
+
kind: "evolution-knowledge-review-state-changed",
|
|
476
|
+
knowledgeId: sanitizeId(value.knowledgeId),
|
|
477
|
+
projectKey: sanitizeStorageId("projectKey", value.projectKey),
|
|
478
|
+
roleTags: uniqueSorted(value.roleTags.map(sanitizeId)),
|
|
479
|
+
artifactCreatedAt: value.artifactCreatedAt,
|
|
480
|
+
previousReviewState: value.previousReviewState as EvolutionReviewState,
|
|
481
|
+
nextReviewState: value.nextReviewState as EvolutionReviewState,
|
|
482
|
+
changedAt: value.changedAt,
|
|
483
|
+
metadataOnly: true,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function normalizeTimestamp(value: string | Date | undefined): string {
|
|
488
|
+
const date = value === undefined ? new Date() : value instanceof Date ? value : new Date(value);
|
|
489
|
+
if (Number.isNaN(date.getTime())) throw new Error("Invalid knowledge review history timestamp.");
|
|
490
|
+
return date.toISOString();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function isAlreadyExistsError(error: unknown): boolean {
|
|
494
|
+
return (
|
|
495
|
+
error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "EEXIST"
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function sleep(milliseconds: number): Promise<void> {
|
|
500
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
504
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
235
505
|
}
|
|
@@ -144,8 +144,8 @@ export async function createSegment(input: {
|
|
|
144
144
|
externalUploadAllowed: false,
|
|
145
145
|
},
|
|
146
146
|
lifecycle: {
|
|
147
|
-
status: "
|
|
148
|
-
reviewState: "
|
|
147
|
+
status: "captured",
|
|
148
|
+
reviewState: "not-required",
|
|
149
149
|
consumedByBatchIds: [],
|
|
150
150
|
},
|
|
151
151
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname } from "node:path";
|
|
1
|
+
import { appendFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { resolveEvoDevPaths } from "../../../config/paths.ts";
|
|
3
4
|
import { isNotFoundError } from "../../../utils/index.ts";
|
|
4
5
|
import { SENSITIVE_TEXT_PATTERN } from "./constants.ts";
|
|
6
|
+
import { resolveSessionMemoryPaths } from "./paths.ts";
|
|
5
7
|
import type {
|
|
6
8
|
SessionEvidenceSegmentV1,
|
|
7
9
|
SessionMemoryCursorV1,
|
|
@@ -11,6 +13,79 @@ import type {
|
|
|
11
13
|
SessionMemoryStateV1,
|
|
12
14
|
} from "./types.ts";
|
|
13
15
|
|
|
16
|
+
export class SessionMemoryEvidenceError extends Error {
|
|
17
|
+
readonly code: "not-found" | "invalid";
|
|
18
|
+
|
|
19
|
+
constructor(code: "not-found" | "invalid", message: string) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "SessionMemoryEvidenceError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function listSessionEvidenceSegments(input: {
|
|
27
|
+
homeDir: string;
|
|
28
|
+
projectKey?: string;
|
|
29
|
+
}): Promise<SessionEvidenceSegmentV1[]> {
|
|
30
|
+
const rootDir = join(resolveEvoDevPaths(input.homeDir).stateDir, "session-memory");
|
|
31
|
+
const projectDirs =
|
|
32
|
+
input.projectKey === undefined ? await listDirectoryNames(rootDir) : [input.projectKey];
|
|
33
|
+
const segments: SessionEvidenceSegmentV1[] = [];
|
|
34
|
+
for (const projectKey of projectDirs) {
|
|
35
|
+
const paths = resolveSessionMemoryPaths({
|
|
36
|
+
homeDir: input.homeDir,
|
|
37
|
+
projectKey,
|
|
38
|
+
sessionKey: "list",
|
|
39
|
+
});
|
|
40
|
+
for (const sessionKey of await listDirectoryNames(paths.projectDir)) {
|
|
41
|
+
const sessionPaths = resolveSessionMemoryPaths({
|
|
42
|
+
homeDir: input.homeDir,
|
|
43
|
+
projectKey,
|
|
44
|
+
sessionKey,
|
|
45
|
+
});
|
|
46
|
+
for (const entry of await listJsonFiles(sessionPaths.segmentsDir)) {
|
|
47
|
+
try {
|
|
48
|
+
segments.push(parseSessionEvidenceSegment(JSON.parse(await readFile(entry, "utf8"))));
|
|
49
|
+
} catch {
|
|
50
|
+
// Invalid local evidence is omitted from listings and remains available for diagnostics.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return segments.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function readSessionEvidenceSegment(input: {
|
|
59
|
+
homeDir: string;
|
|
60
|
+
projectKey: string;
|
|
61
|
+
sessionKey: string;
|
|
62
|
+
segmentId: string;
|
|
63
|
+
}): Promise<SessionEvidenceSegmentV1> {
|
|
64
|
+
const paths = resolveSessionMemoryPaths(input);
|
|
65
|
+
try {
|
|
66
|
+
const segment = parseSessionEvidenceSegment(
|
|
67
|
+
JSON.parse(await readFile(paths.segmentPath(input.segmentId), "utf8")) as unknown,
|
|
68
|
+
);
|
|
69
|
+
if (
|
|
70
|
+
segment.id !== input.segmentId ||
|
|
71
|
+
segment.projectKey !== input.projectKey ||
|
|
72
|
+
segment.sessionKey !== input.sessionKey
|
|
73
|
+
) {
|
|
74
|
+
throw new SessionMemoryEvidenceError(
|
|
75
|
+
"invalid",
|
|
76
|
+
"Session evidence identity does not match its storage path.",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return segment;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error instanceof SessionMemoryEvidenceError) throw error;
|
|
82
|
+
if (isNotFoundError(error)) {
|
|
83
|
+
throw new SessionMemoryEvidenceError("not-found", "Session evidence segment was not found.");
|
|
84
|
+
}
|
|
85
|
+
throw new SessionMemoryEvidenceError("invalid", "Session evidence segment is invalid.");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
14
89
|
export async function readSessionState(path: string): Promise<SessionMemoryStateV1 | null> {
|
|
15
90
|
try {
|
|
16
91
|
return JSON.parse(await readFile(path, "utf8")) as SessionMemoryStateV1;
|
|
@@ -143,3 +218,49 @@ function isSessionIndexSegment(value: unknown): value is SessionMemoryIndexSegme
|
|
|
143
218
|
typeof item.reviewState === "string"
|
|
144
219
|
);
|
|
145
220
|
}
|
|
221
|
+
|
|
222
|
+
function parseSessionEvidenceSegment(value: unknown): SessionEvidenceSegmentV1 {
|
|
223
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
224
|
+
throw new SessionMemoryEvidenceError("invalid", "Session evidence must be an object.");
|
|
225
|
+
}
|
|
226
|
+
const segment = value as Partial<SessionEvidenceSegmentV1>;
|
|
227
|
+
if (
|
|
228
|
+
segment.schemaVersion !== 1 ||
|
|
229
|
+
segment.kind !== "session-evidence-segment" ||
|
|
230
|
+
typeof segment.id !== "string" ||
|
|
231
|
+
typeof segment.projectKey !== "string" ||
|
|
232
|
+
typeof segment.sessionKey !== "string" ||
|
|
233
|
+
typeof segment.createdAt !== "string" ||
|
|
234
|
+
typeof segment.normalized !== "object" ||
|
|
235
|
+
segment.normalized === null ||
|
|
236
|
+
typeof segment.rawExcerpt !== "object" ||
|
|
237
|
+
segment.rawExcerpt === null
|
|
238
|
+
) {
|
|
239
|
+
throw new SessionMemoryEvidenceError("invalid", "Session evidence fields are invalid.");
|
|
240
|
+
}
|
|
241
|
+
return segment as SessionEvidenceSegmentV1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function listDirectoryNames(path: string): Promise<string[]> {
|
|
245
|
+
try {
|
|
246
|
+
return (await readdir(path, { withFileTypes: true }))
|
|
247
|
+
.filter((entry) => entry.isDirectory())
|
|
248
|
+
.map((entry) => entry.name)
|
|
249
|
+
.sort();
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (isNotFoundError(error)) return [];
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function listJsonFiles(path: string): Promise<string[]> {
|
|
257
|
+
try {
|
|
258
|
+
return (await readdir(path, { withFileTypes: true }))
|
|
259
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
260
|
+
.map((entry) => join(path, entry.name))
|
|
261
|
+
.sort();
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (isNotFoundError(error)) return [];
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -133,8 +133,8 @@ export interface SessionEvidenceSegmentV1 {
|
|
|
133
133
|
externalUploadAllowed: false;
|
|
134
134
|
};
|
|
135
135
|
lifecycle: {
|
|
136
|
-
status: "pending-review" | "reviewed" | "distilled" | "ignored" | "deleted";
|
|
137
|
-
reviewState: "unreviewed" | "accepted" | "rejected" | "deferred";
|
|
136
|
+
status: "captured" | "pending-review" | "reviewed" | "distilled" | "ignored" | "deleted";
|
|
137
|
+
reviewState: "not-required" | "unreviewed" | "accepted" | "rejected" | "deferred";
|
|
138
138
|
consumedByBatchIds: string[];
|
|
139
139
|
};
|
|
140
140
|
}
|
|
@@ -203,5 +203,5 @@ export interface SessionMemoryIndexSegment {
|
|
|
203
203
|
reason: SessionMemorySegmentReason;
|
|
204
204
|
strength: SessionMemorySignalStrength;
|
|
205
205
|
createdAt: string;
|
|
206
|
-
reviewState: "unreviewed" | "accepted" | "rejected" | "deferred";
|
|
206
|
+
reviewState: "not-required" | "unreviewed" | "accepted" | "rejected" | "deferred";
|
|
207
207
|
}
|
package/src/evolution/index.ts
CHANGED
|
@@ -6,7 +6,11 @@ export * from "./candidates/index.ts";
|
|
|
6
6
|
export * from "./triggers/index.ts";
|
|
7
7
|
export * from "./formatters.ts";
|
|
8
8
|
export {
|
|
9
|
+
createEvolutionRepoProposal,
|
|
10
|
+
hasConcreteRepoProposalChanges,
|
|
11
|
+
sanitizeProposedChange,
|
|
9
12
|
validateEvolutionDistillationBatch,
|
|
10
13
|
validateEvolutionEvidenceWindow,
|
|
11
14
|
validateEvolutionKnowledgeRecord,
|
|
15
|
+
validateEvolutionRepoProposal,
|
|
12
16
|
} from "./shared.ts";
|