@evo-dev/core 0.0.1-alpha.1 → 0.0.1-alpha.11

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.
Files changed (66) hide show
  1. package/assets/skills/coding/knowledge-distillation/SKILL.md +117 -114
  2. package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +11 -7
  3. package/assets/team/agents/code-reviewer.md +48 -0
  4. package/assets/team/agents/docs-maintainer.md +51 -0
  5. package/assets/team/agents/implementation-engineer.md +51 -0
  6. package/assets/team/agents/product-scope-analyst.md +58 -0
  7. package/assets/team/agents/release-engineer.md +55 -0
  8. package/assets/team/agents/security-boundary-reviewer.md +50 -0
  9. package/assets/team/agents/solution-architect.md +51 -0
  10. package/assets/team/agents/verification-engineer.md +51 -0
  11. package/assets/team/team.md +102 -0
  12. package/dist/config/index.js +925 -97
  13. package/dist/index.js +13107 -5618
  14. package/package.json +5 -1
  15. package/src/agents/index.ts +56 -264
  16. package/src/code-agent-traces/index.ts +520 -0
  17. package/src/config/index.ts +5 -0
  18. package/src/config/paths.ts +1 -1
  19. package/src/config/settings.ts +149 -0
  20. package/src/config/store.ts +2 -0
  21. package/src/daemon/index.ts +99 -50
  22. package/src/evolution/candidates/index.ts +564 -0
  23. package/src/evolution/control/index.ts +20 -0
  24. package/src/evolution/evidence/analysis.ts +533 -0
  25. package/src/evolution/evidence/index.ts +3 -0
  26. package/src/evolution/evidence/session-memory/analysis.ts +281 -0
  27. package/src/evolution/evidence/session-memory/constants.ts +9 -0
  28. package/src/evolution/evidence/session-memory/index.ts +7 -0
  29. package/src/evolution/evidence/session-memory/paths.ts +29 -0
  30. package/src/evolution/evidence/session-memory/policy.ts +39 -0
  31. package/src/evolution/evidence/session-memory/segment.ts +202 -0
  32. package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
  33. package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
  34. package/src/evolution/evidence/session-memory/storage.ts +379 -0
  35. package/src/evolution/evidence/session-memory/types.ts +221 -0
  36. package/src/evolution/evidence/session-memory/updater.ts +191 -0
  37. package/src/evolution/formatters.ts +169 -0
  38. package/src/evolution/index.ts +16 -2356
  39. package/src/evolution/knowledge/index.ts +5427 -0
  40. package/src/evolution/paths.ts +44 -0
  41. package/src/evolution/processor/distillation.ts +518 -0
  42. package/src/evolution/processor/index.ts +3 -0
  43. package/src/evolution/processor/process.ts +528 -0
  44. package/src/{learning → evolution/review}/index.ts +10 -14
  45. package/src/evolution/schema.ts +568 -0
  46. package/src/evolution/shared.ts +758 -0
  47. package/src/evolution/triggers/classification.ts +102 -0
  48. package/src/evolution/triggers/index.ts +295 -0
  49. package/src/hooks/index.ts +438 -179
  50. package/src/index.ts +12 -3
  51. package/src/projects/index.ts +453 -0
  52. package/src/runtime-logs/index.ts +490 -24
  53. package/src/team/index.ts +1429 -185
  54. package/src/team/mcp.ts +9 -5
  55. package/src/team/prompts.ts +141 -0
  56. package/src/utils/errors.ts +13 -0
  57. package/src/utils/fs.ts +40 -0
  58. package/src/utils/hash.ts +9 -0
  59. package/src/utils/ids.ts +12 -0
  60. package/src/utils/index.ts +7 -0
  61. package/src/utils/parsing.ts +11 -0
  62. package/src/utils/text.ts +18 -0
  63. package/src/utils/time.ts +5 -0
  64. package/src/workflow/index.ts +3 -21
  65. package/src/project/index.ts +0 -507
  66. package/src/task/index.ts +0 -840
@@ -0,0 +1,564 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { resolveEvoDevPaths } from "../../config/paths.ts";
5
+ import { pathExists, readJsonFiles, writeJsonFile as writeJson } from "../../utils/index.ts";
6
+ import { resolveEvolutionPaths } from "../paths.ts";
7
+ import type {
8
+ EvolutionEvosCase,
9
+ EvolutionEvosCaseQueryResult,
10
+ EvolutionKnowledgeRecord,
11
+ EvolutionKnowledgeReviewHistoryRecord,
12
+ EvolutionRepoProposal,
13
+ EvolutionReviewCandidate,
14
+ EvolutionReviewSnapshot,
15
+ EvolutionReviewState,
16
+ EvolutionTriggerRecord,
17
+ } from "../schema.ts";
18
+ import {
19
+ REVIEW_STATES,
20
+ hasConcreteRepoProposalChanges,
21
+ listDirectoryNames,
22
+ parseEvosCase,
23
+ parseKnowledgeRecord,
24
+ parseRepoProposal,
25
+ parseReviewCandidate,
26
+ parseTrigger,
27
+ sanitizeId,
28
+ sanitizeStorageId,
29
+ sanitizeText,
30
+ uniqueSorted,
31
+ validateEvolutionKnowledgeRecord,
32
+ validateEvolutionRepoProposal,
33
+ } from "../shared.ts";
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
+
74
+ export async function readEvolutionReviewSnapshot(input: {
75
+ homeDir: string;
76
+ projectKey?: string;
77
+ }): Promise<EvolutionReviewSnapshot> {
78
+ const paths = resolveEvoDevPaths(input.homeDir);
79
+ const projectKeys =
80
+ input.projectKey === undefined
81
+ ? await listEvolutionReviewProjectKeys(paths)
82
+ : [sanitizeStorageId("projectKey", input.projectKey)];
83
+ const knowledgeRecords: EvolutionKnowledgeRecord[] = [];
84
+ const evosCases: EvolutionEvosCase[] = [];
85
+ const repoProposals: EvolutionRepoProposal[] = [];
86
+ const reviewCandidates: EvolutionReviewCandidate[] = [];
87
+ const triggers: EvolutionTriggerRecord[] = [];
88
+
89
+ for (const projectKey of projectKeys) {
90
+ const resolved = resolveEvolutionPaths({
91
+ homeDir: input.homeDir,
92
+ projectKey,
93
+ runId: "review",
94
+ });
95
+ knowledgeRecords.push(
96
+ ...(await readJsonFiles(resolved.knowledgeRecordsDir, parseKnowledgeRecord)),
97
+ );
98
+ evosCases.push(...(await readJsonFiles(resolved.evosCasesProjectDir, parseEvosCase)));
99
+ }
100
+
101
+ const evolutionStateDir = join(paths.stateDir, "evolution");
102
+ const stateProjectKeys =
103
+ input.projectKey === undefined
104
+ ? await listDirectoryNames(evolutionStateDir)
105
+ : [sanitizeStorageId("projectKey", input.projectKey)];
106
+ for (const projectKey of stateProjectKeys) {
107
+ const projectStateDir = join(evolutionStateDir, projectKey);
108
+ const runIds = await listDirectoryNames(projectStateDir);
109
+ for (const runId of runIds) {
110
+ const proposalsDir = join(projectStateDir, runId, "proposals");
111
+ const reviewCandidatesDir = join(projectStateDir, runId, "review-candidates");
112
+ repoProposals.push(...(await readJsonFiles(proposalsDir, parseRepoProposal)));
113
+ reviewCandidates.push(...(await readJsonFiles(reviewCandidatesDir, parseReviewCandidate)));
114
+ triggers.push(
115
+ ...(await readJsonFiles(join(projectStateDir, runId, "triggers"), parseTrigger)),
116
+ );
117
+ }
118
+ }
119
+
120
+ return {
121
+ projectKey:
122
+ input.projectKey === undefined ? null : sanitizeStorageId("projectKey", input.projectKey),
123
+ knowledgeRecords,
124
+ evosCases,
125
+ repoProposals,
126
+ reviewCandidates,
127
+ triggers,
128
+ };
129
+ }
130
+
131
+ async function listEvolutionReviewProjectKeys(
132
+ paths: ReturnType<typeof resolveEvoDevPaths>,
133
+ ): Promise<string[]> {
134
+ const legacyKnowledgeProjectKeys = (
135
+ await Promise.all(
136
+ (
137
+ await listDirectoryNames(paths.knowledgeDir)
138
+ ).map(async (projectKey) =>
139
+ (await pathExists(join(paths.knowledgeDir, projectKey, "records"))) ? projectKey : null,
140
+ ),
141
+ )
142
+ ).filter((projectKey): projectKey is string => projectKey !== null);
143
+ return uniqueSorted([
144
+ ...legacyKnowledgeProjectKeys,
145
+ ...(await listDirectoryNames(paths.evosCasesDir)),
146
+ ]);
147
+ }
148
+
149
+ export async function listEvolutionKnowledgeRecords(input: {
150
+ homeDir: string;
151
+ projectKey?: string;
152
+ }): Promise<EvolutionKnowledgeRecord[]> {
153
+ return (
154
+ await readEvolutionReviewSnapshot({
155
+ homeDir: input.homeDir,
156
+ projectKey: input.projectKey,
157
+ })
158
+ ).knowledgeRecords.sort((left, right) => left.id.localeCompare(right.id));
159
+ }
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
+
192
+ export async function listEvolutionEvosCases(input: {
193
+ homeDir: string;
194
+ projectKey?: string;
195
+ roleId?: string;
196
+ reviewStates?: EvolutionReviewState[];
197
+ }): Promise<EvolutionEvosCaseQueryResult> {
198
+ const paths = resolveEvoDevPaths(input.homeDir);
199
+ const projectKeys =
200
+ input.projectKey === undefined
201
+ ? await listDirectoryNames(paths.evosCasesDir)
202
+ : [sanitizeStorageId("projectKey", input.projectKey)];
203
+ const roleId = input.roleId === undefined ? null : sanitizeId(input.roleId);
204
+ const reviewStates = input.reviewStates ?? ["accepted", "auto-accepted"];
205
+ const cases: EvolutionEvosCase[] = [];
206
+ const warnings: string[] = [];
207
+
208
+ for (const projectKey of projectKeys) {
209
+ const projectDir = join(paths.evosCasesDir, projectKey);
210
+ if (!(await pathExists(projectDir))) continue;
211
+ const entries = await readdir(projectDir, { withFileTypes: true });
212
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
213
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
214
+ const sourceLink = `cases/${projectKey}/${entry.name}`;
215
+ try {
216
+ const value = JSON.parse(await readFile(join(projectDir, entry.name), "utf8")) as unknown;
217
+ const evosCase = parseEvosCase(value);
218
+ if (!reviewStates.includes(evosCase.reviewState)) continue;
219
+ if (
220
+ roleId !== null &&
221
+ evosCase.roleTags.length > 0 &&
222
+ !evosCase.roleTags.includes(roleId)
223
+ ) {
224
+ continue;
225
+ }
226
+ cases.push(evosCase);
227
+ } catch {
228
+ warnings.push(
229
+ `Omitted unsafe or invalid evos case: ${sourceLink}. Run evodev knowledge lint.`,
230
+ );
231
+ }
232
+ }
233
+ }
234
+
235
+ return {
236
+ cases: cases.sort((left, right) => {
237
+ const project = left.projectKey.localeCompare(right.projectKey);
238
+ if (project !== 0) return project;
239
+ return left.id.localeCompare(right.id);
240
+ }),
241
+ warnings,
242
+ };
243
+ }
244
+
245
+ export async function readEvolutionKnowledgeRecordById(input: {
246
+ homeDir: string;
247
+ knowledgeId: string;
248
+ projectKey?: string;
249
+ }): Promise<EvolutionKnowledgeRecord> {
250
+ const matches = (await listEvolutionKnowledgeRecords(input)).filter(
251
+ (record) => record.id === input.knowledgeId,
252
+ );
253
+ if (matches.length === 0) throw new Error(`Knowledge record not found: ${input.knowledgeId}`);
254
+ if (matches.length > 1) {
255
+ throw new Error(`Knowledge record id is ambiguous across projects: ${input.knowledgeId}`);
256
+ }
257
+ return matches[0] as EvolutionKnowledgeRecord;
258
+ }
259
+
260
+ export async function updateEvolutionKnowledgeReviewState(input: {
261
+ homeDir: string;
262
+ knowledgeId: string;
263
+ projectKey?: string;
264
+ reviewState: "accepted" | "rejected" | "deferred";
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 };
343
+ },
344
+ );
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 writeRepoProposalAndIndex({ homeDir: input.homeDir, proposal: next, changedAt });
415
+ return { path, record: next, changed: true };
416
+ },
417
+ );
418
+ }
419
+
420
+ export async function markEvolutionRepoProposalApplied(input: {
421
+ homeDir: string;
422
+ proposalId: string;
423
+ projectKey?: string;
424
+ expectedReviewState?: EvolutionRepoProposal["reviewState"];
425
+ now?: string | Date;
426
+ }): Promise<{ path: string; record: EvolutionRepoProposal; changed: boolean }> {
427
+ return await withEvolutionReviewDecisionLock(
428
+ { homeDir: input.homeDir, kind: "repo-proposal", itemId: input.proposalId },
429
+ async () => {
430
+ const record = await readEvolutionRepoProposalById(input);
431
+ if (!hasConcreteRepoProposalChanges(record)) {
432
+ throw new Error("Repo proposal has no concrete repository changes to apply.");
433
+ }
434
+ if (
435
+ input.expectedReviewState !== undefined &&
436
+ record.reviewState !== input.expectedReviewState
437
+ ) {
438
+ throw new Error("Repo proposal review state changed before it was marked applied.");
439
+ }
440
+ const paths = resolveEvolutionPaths({
441
+ homeDir: input.homeDir,
442
+ projectKey: record.projectKey,
443
+ runId: record.provenance.runId,
444
+ });
445
+ const path = join(paths.repoProposalsDir, `${record.id}.json`);
446
+ if (record.reviewState === "applied") return { path, record, changed: false };
447
+ if (record.reviewState !== "accepted") {
448
+ throw new Error("Only accepted repo proposals can be marked applied.");
449
+ }
450
+
451
+ const changedAt = normalizeTimestamp(input.now);
452
+ const next: EvolutionRepoProposal = {
453
+ ...record,
454
+ reviewState: "applied",
455
+ reviewStateChangedAt: changedAt,
456
+ };
457
+ validateEvolutionRepoProposal(next);
458
+ await writeRepoProposalAndIndex({ homeDir: input.homeDir, proposal: next, changedAt });
459
+ return { path, record: next, changed: true };
460
+ },
461
+ );
462
+ }
463
+
464
+ async function writeRepoProposalAndIndex(input: {
465
+ homeDir: string;
466
+ proposal: EvolutionRepoProposal;
467
+ changedAt: string;
468
+ }): Promise<void> {
469
+ const paths = resolveEvolutionPaths({
470
+ homeDir: input.homeDir,
471
+ projectKey: input.proposal.projectKey,
472
+ runId: input.proposal.provenance.runId,
473
+ });
474
+ await writeJson(join(paths.repoProposalsDir, `${input.proposal.id}.json`), input.proposal, {
475
+ overwrite: true,
476
+ });
477
+ const allRunProposals = await readJsonFiles(paths.repoProposalsDir, parseRepoProposal);
478
+ await writeJson(
479
+ paths.repoProposalsIndexPath,
480
+ {
481
+ schemaVersion: 1,
482
+ projectKey: input.proposal.projectKey,
483
+ runId: input.proposal.provenance.runId,
484
+ updatedAt: input.changedAt,
485
+ proposals: allRunProposals.map((proposal) => ({
486
+ id: proposal.id,
487
+ kind: proposal.kind,
488
+ title: proposal.title,
489
+ reviewState: proposal.reviewState,
490
+ })),
491
+ },
492
+ { overwrite: true },
493
+ );
494
+ }
495
+
496
+ function parseKnowledgeReviewHistoryRecord(value: unknown): EvolutionKnowledgeReviewHistoryRecord {
497
+ if (!isRecord(value)) throw new Error("Knowledge review history record must be an object.");
498
+ const allowedKeys = new Set([
499
+ "version",
500
+ "kind",
501
+ "knowledgeId",
502
+ "projectKey",
503
+ "roleTags",
504
+ "artifactCreatedAt",
505
+ "previousReviewState",
506
+ "nextReviewState",
507
+ "changedAt",
508
+ "metadataOnly",
509
+ ]);
510
+ if (Object.keys(value).some((key) => !allowedKeys.has(key))) {
511
+ throw new Error("Knowledge review history record contains unsupported fields.");
512
+ }
513
+ if (
514
+ value.version !== 1 ||
515
+ value.kind !== "evolution-knowledge-review-state-changed" ||
516
+ typeof value.knowledgeId !== "string" ||
517
+ typeof value.projectKey !== "string" ||
518
+ !Array.isArray(value.roleTags) ||
519
+ !value.roleTags.every((roleId) => typeof roleId === "string") ||
520
+ typeof value.artifactCreatedAt !== "string" ||
521
+ typeof value.previousReviewState !== "string" ||
522
+ !REVIEW_STATES.includes(value.previousReviewState as EvolutionReviewState) ||
523
+ typeof value.nextReviewState !== "string" ||
524
+ !REVIEW_STATES.includes(value.nextReviewState as EvolutionReviewState) ||
525
+ typeof value.changedAt !== "string" ||
526
+ value.metadataOnly !== true
527
+ ) {
528
+ throw new Error("Invalid knowledge review history record.");
529
+ }
530
+ normalizeTimestamp(value.artifactCreatedAt);
531
+ normalizeTimestamp(value.changedAt);
532
+ return {
533
+ version: 1,
534
+ kind: "evolution-knowledge-review-state-changed",
535
+ knowledgeId: sanitizeId(value.knowledgeId),
536
+ projectKey: sanitizeStorageId("projectKey", value.projectKey),
537
+ roleTags: uniqueSorted(value.roleTags.map(sanitizeId)),
538
+ artifactCreatedAt: value.artifactCreatedAt,
539
+ previousReviewState: value.previousReviewState as EvolutionReviewState,
540
+ nextReviewState: value.nextReviewState as EvolutionReviewState,
541
+ changedAt: value.changedAt,
542
+ metadataOnly: true,
543
+ };
544
+ }
545
+
546
+ function normalizeTimestamp(value: string | Date | undefined): string {
547
+ const date = value === undefined ? new Date() : value instanceof Date ? value : new Date(value);
548
+ if (Number.isNaN(date.getTime())) throw new Error("Invalid knowledge review history timestamp.");
549
+ return date.toISOString();
550
+ }
551
+
552
+ function isAlreadyExistsError(error: unknown): boolean {
553
+ return (
554
+ error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "EEXIST"
555
+ );
556
+ }
557
+
558
+ async function sleep(milliseconds: number): Promise<void> {
559
+ await new Promise((resolve) => setTimeout(resolve, milliseconds));
560
+ }
561
+
562
+ function isRecord(value: unknown): value is Record<string, unknown> {
563
+ return typeof value === "object" && value !== null && !Array.isArray(value);
564
+ }
@@ -0,0 +1,20 @@
1
+ export {
2
+ formatEvolutionProcessResult,
3
+ formatEvolutionReviewSnapshot,
4
+ } from "../formatters.ts";
5
+ export { processEvolutionTriggers } from "../processor/index.ts";
6
+ export { readEvolutionReviewSnapshot } from "../candidates/index.ts";
7
+ export {
8
+ listEvolutionTriggers,
9
+ listSegmentEvolutionTriggers,
10
+ } from "../triggers/index.ts";
11
+ export type {
12
+ EvolutionProcessInput,
13
+ EvolutionProcessProgress,
14
+ EvolutionProcessResult,
15
+ EvolutionReviewSnapshot,
16
+ EvolutionTriggerListInput,
17
+ EvolutionTriggerRecord,
18
+ SegmentEvolutionTriggerListInput,
19
+ SegmentEvolutionTriggerRecord,
20
+ } from "../schema.ts";