@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evo-dev/core",
3
- "version": "0.0.1-alpha.4",
3
+ "version": "0.0.1-alpha.5",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,6 @@
1
- import { readFile, readdir } from "node:fs/promises";
2
- import { join } from "node:path";
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
- }): Promise<{ path: string; record: EvolutionKnowledgeRecord }> {
190
- const record = await readEvolutionKnowledgeRecordById(input);
191
- const next: EvolutionKnowledgeRecord = {
192
- ...record,
193
- reviewState: input.reviewState,
194
- authority: input.reviewState === "accepted" ? "reviewed" : "contextual",
195
- runtime: {
196
- ...record.runtime,
197
- canLoad: input.reviewState === "accepted",
198
- hardBlocking: false,
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
- await writeJson(
214
- paths.knowledgeIndexPath,
215
- {
216
- version: 1,
217
- kind: "evolution-knowledge-index",
218
- projectKey: next.projectKey,
219
- updatedAt: new Date().toISOString(),
220
- records: allProjectKnowledgeRecords.map((item) => ({
221
- id: item.id,
222
- kind: item.kind,
223
- title: item.title,
224
- roleTags: item.roleTags,
225
- tags: item.tags,
226
- reviewState: item.reviewState,
227
- authority: item.authority,
228
- confidence: item.confidence,
229
- runtime: item.runtime,
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
- return { path, record: next };
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
  }
@@ -1,4 +1,5 @@
1
1
  export * from "./paths.ts";
2
2
  export * from "./policy.ts";
3
+ export * from "./storage.ts";
3
4
  export * from "./types.ts";
4
5
  export * from "./updater.ts";
@@ -144,8 +144,8 @@ export async function createSegment(input: {
144
144
  externalUploadAllowed: false,
145
145
  },
146
146
  lifecycle: {
147
- status: "pending-review",
148
- reviewState: "unreviewed",
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
  }
@@ -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";