@evo-dev/core 0.0.1-alpha.1 → 0.0.1-alpha.10
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/assets/skills/coding/knowledge-distillation/SKILL.md +117 -114
- package/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +11 -7
- package/assets/team/agents/code-reviewer.md +48 -0
- package/assets/team/agents/docs-maintainer.md +51 -0
- package/assets/team/agents/implementation-engineer.md +51 -0
- package/assets/team/agents/product-scope-analyst.md +58 -0
- package/assets/team/agents/release-engineer.md +55 -0
- package/assets/team/agents/security-boundary-reviewer.md +50 -0
- package/assets/team/agents/solution-architect.md +51 -0
- package/assets/team/agents/verification-engineer.md +51 -0
- package/assets/team/team.md +102 -0
- package/dist/config/index.js +925 -97
- package/dist/index.js +13107 -5618
- package/package.json +5 -1
- package/src/agents/index.ts +56 -264
- package/src/code-agent-traces/index.ts +520 -0
- package/src/config/index.ts +5 -0
- package/src/config/paths.ts +1 -1
- package/src/config/settings.ts +149 -0
- package/src/config/store.ts +2 -0
- package/src/daemon/index.ts +99 -50
- package/src/evolution/candidates/index.ts +564 -0
- package/src/evolution/control/index.ts +20 -0
- package/src/evolution/evidence/analysis.ts +533 -0
- package/src/evolution/evidence/index.ts +3 -0
- package/src/evolution/evidence/session-memory/analysis.ts +281 -0
- package/src/evolution/evidence/session-memory/constants.ts +9 -0
- package/src/evolution/evidence/session-memory/index.ts +7 -0
- package/src/evolution/evidence/session-memory/paths.ts +29 -0
- package/src/evolution/evidence/session-memory/policy.ts +39 -0
- package/src/evolution/evidence/session-memory/segment.ts +202 -0
- package/src/evolution/evidence/session-memory/sensitivity.ts +335 -0
- package/src/evolution/evidence/session-memory/state-machine.ts +249 -0
- package/src/evolution/evidence/session-memory/storage.ts +379 -0
- package/src/evolution/evidence/session-memory/types.ts +221 -0
- package/src/evolution/evidence/session-memory/updater.ts +191 -0
- package/src/evolution/formatters.ts +169 -0
- package/src/evolution/index.ts +16 -2356
- package/src/evolution/knowledge/index.ts +5427 -0
- package/src/evolution/paths.ts +44 -0
- package/src/evolution/processor/distillation.ts +518 -0
- package/src/evolution/processor/index.ts +3 -0
- package/src/evolution/processor/process.ts +528 -0
- package/src/{learning → evolution/review}/index.ts +10 -14
- package/src/evolution/schema.ts +568 -0
- package/src/evolution/shared.ts +758 -0
- package/src/evolution/triggers/classification.ts +102 -0
- package/src/evolution/triggers/index.ts +295 -0
- package/src/hooks/index.ts +438 -179
- package/src/index.ts +12 -3
- package/src/projects/index.ts +453 -0
- package/src/runtime-logs/index.ts +490 -24
- package/src/team/index.ts +1429 -185
- package/src/team/mcp.ts +9 -5
- package/src/team/prompts.ts +141 -0
- package/src/utils/errors.ts +13 -0
- package/src/utils/fs.ts +40 -0
- package/src/utils/hash.ts +9 -0
- package/src/utils/ids.ts +12 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/parsing.ts +11 -0
- package/src/utils/text.ts +18 -0
- package/src/utils/time.ts +5 -0
- package/src/workflow/index.ts +3 -21
- package/src/project/index.ts +0 -507
- package/src/task/index.ts +0 -840
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { pathExists } from "../utils/index.ts";
|
|
4
|
+
import {
|
|
5
|
+
detectSessionMemorySensitivity,
|
|
6
|
+
redactSessionMemoryCredentialText,
|
|
7
|
+
} from "./evidence/session-memory/sensitivity.ts";
|
|
8
|
+
import type {
|
|
9
|
+
EvolutionDistillationBatch,
|
|
10
|
+
EvolutionEpisodeKind,
|
|
11
|
+
EvolutionEpisodeStatus,
|
|
12
|
+
EvolutionEvidenceEventKind,
|
|
13
|
+
EvolutionEvidenceSourceKind,
|
|
14
|
+
EvolutionEvidenceWindow,
|
|
15
|
+
EvolutionEvosCase,
|
|
16
|
+
EvolutionKnowledgeKind,
|
|
17
|
+
EvolutionKnowledgeRecord,
|
|
18
|
+
EvolutionNormalizedEventType,
|
|
19
|
+
EvolutionPrivacyFields,
|
|
20
|
+
EvolutionProposalKind,
|
|
21
|
+
EvolutionRepoProposal,
|
|
22
|
+
EvolutionReviewCandidate,
|
|
23
|
+
EvolutionReviewState,
|
|
24
|
+
EvolutionTriggerReason,
|
|
25
|
+
EvolutionTriggerRecord,
|
|
26
|
+
EvolutionTriggerStatus,
|
|
27
|
+
EvolutionTriggerStrength,
|
|
28
|
+
SegmentEvolutionTriggerReason,
|
|
29
|
+
SegmentEvolutionTriggerRecord,
|
|
30
|
+
SegmentEvolutionTriggerStrength,
|
|
31
|
+
} from "./schema.ts";
|
|
32
|
+
|
|
33
|
+
export const MAX_EVIDENCE_EVENTS = 200;
|
|
34
|
+
export const MAX_TEXT_LENGTH = 600;
|
|
35
|
+
export const MAX_PROPOSED_CHANGE_LENGTH = 16 * 1024;
|
|
36
|
+
export const PROCESS_LOCK_STALE_MS = 5 * 60 * 1000;
|
|
37
|
+
export const EVIDENCE_SOURCE_KINDS: readonly EvolutionEvidenceSourceKind[] = [
|
|
38
|
+
"evodev-execution-event",
|
|
39
|
+
"team-agent-trace",
|
|
40
|
+
"session-trace",
|
|
41
|
+
"session-memory-segment",
|
|
42
|
+
"verification",
|
|
43
|
+
"review-summary",
|
|
44
|
+
"user-feedback",
|
|
45
|
+
"code-agent-trace-ref",
|
|
46
|
+
];
|
|
47
|
+
export const REVIEW_STATES: readonly EvolutionReviewState[] = [
|
|
48
|
+
"auto-accepted",
|
|
49
|
+
"auto-stored/unreviewed",
|
|
50
|
+
"needs-human",
|
|
51
|
+
"accepted",
|
|
52
|
+
"rejected",
|
|
53
|
+
"deferred",
|
|
54
|
+
"stale",
|
|
55
|
+
"deprecated",
|
|
56
|
+
"superseded",
|
|
57
|
+
"revoked",
|
|
58
|
+
];
|
|
59
|
+
export const KNOWLEDGE_KINDS: readonly EvolutionKnowledgeKind[] = [
|
|
60
|
+
"run-summary",
|
|
61
|
+
"lesson",
|
|
62
|
+
"rule",
|
|
63
|
+
"workflow-hint",
|
|
64
|
+
"skill-gap",
|
|
65
|
+
"role-note",
|
|
66
|
+
"verification-pattern",
|
|
67
|
+
];
|
|
68
|
+
export const PROPOSAL_KINDS: readonly EvolutionProposalKind[] = [
|
|
69
|
+
"skill",
|
|
70
|
+
"rule",
|
|
71
|
+
"role-agent",
|
|
72
|
+
"team",
|
|
73
|
+
"ci",
|
|
74
|
+
"test",
|
|
75
|
+
"docs",
|
|
76
|
+
"engineering-practice",
|
|
77
|
+
];
|
|
78
|
+
export const EVENT_KINDS: readonly EvolutionEvidenceEventKind[] = [
|
|
79
|
+
"trace",
|
|
80
|
+
"tool-call",
|
|
81
|
+
"skill-call",
|
|
82
|
+
"subagent",
|
|
83
|
+
"verification",
|
|
84
|
+
"review",
|
|
85
|
+
"user-feedback",
|
|
86
|
+
"error",
|
|
87
|
+
"run-state",
|
|
88
|
+
];
|
|
89
|
+
export const NORMALIZED_EVENT_TYPES: readonly EvolutionNormalizedEventType[] = [
|
|
90
|
+
"SessionStart",
|
|
91
|
+
"UserPromptSubmit",
|
|
92
|
+
"UserPromptExpansion",
|
|
93
|
+
"PreToolUse",
|
|
94
|
+
"PermissionRequest",
|
|
95
|
+
"PostToolUse",
|
|
96
|
+
"PostToolUseFailure",
|
|
97
|
+
"PostToolBatch",
|
|
98
|
+
"PermissionDenied",
|
|
99
|
+
"SubagentStart",
|
|
100
|
+
"Stop",
|
|
101
|
+
"StopFailure",
|
|
102
|
+
"TeammateIdle",
|
|
103
|
+
"SubagentStop",
|
|
104
|
+
"TaskCreated",
|
|
105
|
+
"TaskCompleted",
|
|
106
|
+
"PreCompact",
|
|
107
|
+
"PostCompact",
|
|
108
|
+
"SessionEnd",
|
|
109
|
+
"ConfigChange",
|
|
110
|
+
"CwdChanged",
|
|
111
|
+
"FileChanged",
|
|
112
|
+
"WorktreeCreate",
|
|
113
|
+
"WorktreeRemove",
|
|
114
|
+
"unknown",
|
|
115
|
+
];
|
|
116
|
+
export const TRIGGER_STRENGTHS: readonly EvolutionTriggerStrength[] = [
|
|
117
|
+
"none",
|
|
118
|
+
"conditional",
|
|
119
|
+
"strong",
|
|
120
|
+
];
|
|
121
|
+
export const TRIGGER_REASONS: readonly EvolutionTriggerReason[] = [
|
|
122
|
+
"none",
|
|
123
|
+
"turn-completed",
|
|
124
|
+
"task-completed",
|
|
125
|
+
"session-ended",
|
|
126
|
+
"role-completed",
|
|
127
|
+
"stop-failure",
|
|
128
|
+
"tool-failure",
|
|
129
|
+
"permission-denied",
|
|
130
|
+
"explicit-command",
|
|
131
|
+
"failure-signal",
|
|
132
|
+
];
|
|
133
|
+
export const EPISODE_KINDS: readonly EvolutionEpisodeKind[] = ["session", "turn", "task", "role"];
|
|
134
|
+
export const EPISODE_STATUSES: readonly EvolutionEpisodeStatus[] = ["open", "closed", "failed"];
|
|
135
|
+
export const TRIGGER_STATUSES: readonly EvolutionTriggerStatus[] = [
|
|
136
|
+
"pending",
|
|
137
|
+
"processing",
|
|
138
|
+
"consumed",
|
|
139
|
+
"failed",
|
|
140
|
+
"skipped",
|
|
141
|
+
];
|
|
142
|
+
export const SEGMENT_TRIGGER_REASONS: readonly SegmentEvolutionTriggerReason[] = [
|
|
143
|
+
"session-memory-init",
|
|
144
|
+
"session-memory-threshold",
|
|
145
|
+
"intent-refinement",
|
|
146
|
+
"user-interruption",
|
|
147
|
+
"failure-signal",
|
|
148
|
+
"permission-denied",
|
|
149
|
+
"verification-after-fix",
|
|
150
|
+
"explicit-memory-intent",
|
|
151
|
+
];
|
|
152
|
+
export const SEGMENT_TRIGGER_STRENGTHS: readonly SegmentEvolutionTriggerStrength[] = [
|
|
153
|
+
"normal",
|
|
154
|
+
"strong",
|
|
155
|
+
];
|
|
156
|
+
export const FORBIDDEN_RAW_KEYS = new Set([
|
|
157
|
+
"commandhistory",
|
|
158
|
+
"commandoutput",
|
|
159
|
+
"credential",
|
|
160
|
+
"credentials",
|
|
161
|
+
"env",
|
|
162
|
+
"fullsource",
|
|
163
|
+
"memorybody",
|
|
164
|
+
"password",
|
|
165
|
+
"privatekey",
|
|
166
|
+
"prompt",
|
|
167
|
+
"promptbody",
|
|
168
|
+
"prompttext",
|
|
169
|
+
"rawcommand",
|
|
170
|
+
"rawcommandoutput",
|
|
171
|
+
"rawlog",
|
|
172
|
+
"rawlogs",
|
|
173
|
+
"rawoutput",
|
|
174
|
+
"rawpayload",
|
|
175
|
+
"rawprompt",
|
|
176
|
+
"secret",
|
|
177
|
+
"secretvalue",
|
|
178
|
+
"source",
|
|
179
|
+
"sourcebody",
|
|
180
|
+
"sourcecode",
|
|
181
|
+
"sourcecontent",
|
|
182
|
+
"sourcetext",
|
|
183
|
+
"stderr",
|
|
184
|
+
"stdout",
|
|
185
|
+
"token",
|
|
186
|
+
"transcript",
|
|
187
|
+
"transcriptbody",
|
|
188
|
+
"transcripttext",
|
|
189
|
+
]);
|
|
190
|
+
export function validateEvolutionEvidenceWindow(window: EvolutionEvidenceWindow): void {
|
|
191
|
+
if (!isRecord(window)) throw new Error("Evidence window must be an object.");
|
|
192
|
+
if (window.schemaVersion !== 1) throw new Error("Evidence window schemaVersion must be 1.");
|
|
193
|
+
if (window.kind !== "evidence-window") throw new Error("Evidence window kind is invalid.");
|
|
194
|
+
assertString("projectKey", window.projectKey);
|
|
195
|
+
assertString("runId", window.runId);
|
|
196
|
+
assertString("id", window.id);
|
|
197
|
+
assertString("createdAt", window.createdAt);
|
|
198
|
+
assertPrivacy(window.privacy);
|
|
199
|
+
if (!Array.isArray(window.sourceRefs)) throw new Error("sourceRefs must be an array.");
|
|
200
|
+
for (const sourceRef of window.sourceRefs) {
|
|
201
|
+
assertString("sourceRefs.id", sourceRef.id);
|
|
202
|
+
assertEnum("sourceRefs.kind", sourceRef.kind, EVIDENCE_SOURCE_KINDS);
|
|
203
|
+
assertString("sourceRefs.path", sourceRef.path);
|
|
204
|
+
if (sourceRef.rawContentStored !== false)
|
|
205
|
+
throw new Error("sourceRefs.rawContentStored must be false.");
|
|
206
|
+
if (sourceRef.externalContentCopied !== false) {
|
|
207
|
+
throw new Error("sourceRefs.externalContentCopied must be false.");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!Array.isArray(window.events)) throw new Error("events must be an array.");
|
|
211
|
+
for (const event of window.events) {
|
|
212
|
+
assertString("events.id", event.id);
|
|
213
|
+
assertEnum("events.kind", event.kind, EVENT_KINDS);
|
|
214
|
+
assertEnum("events.eventType", event.eventType, NORMALIZED_EVENT_TYPES);
|
|
215
|
+
assertEnum("events.triggerStrength", event.triggerStrength, TRIGGER_STRENGTHS);
|
|
216
|
+
assertEnum("events.triggerReason", event.triggerReason, TRIGGER_REASONS);
|
|
217
|
+
assertString("events.summary", event.summary);
|
|
218
|
+
assertString("events.evidenceRef", event.evidenceRef);
|
|
219
|
+
if (!Array.isArray(event.episodeIds)) throw new Error("events.episodeIds must be an array.");
|
|
220
|
+
for (const episodeId of event.episodeIds) assertString("events.episodeIds", episodeId);
|
|
221
|
+
if (event.rawContentStored !== false) throw new Error("events.rawContentStored must be false.");
|
|
222
|
+
}
|
|
223
|
+
if (!Array.isArray(window.episodes)) throw new Error("episodes must be an array.");
|
|
224
|
+
for (const episode of window.episodes) {
|
|
225
|
+
assertString("episodes.id", episode.id);
|
|
226
|
+
assertEnum("episodes.kind", episode.kind, EPISODE_KINDS);
|
|
227
|
+
assertEnum("episodes.status", episode.status, EPISODE_STATUSES);
|
|
228
|
+
assertEnum("episodes.triggerStrength", episode.triggerStrength, TRIGGER_STRENGTHS);
|
|
229
|
+
assertEnum("episodes.triggerReason", episode.triggerReason, TRIGGER_REASONS);
|
|
230
|
+
assertString("episodes.summary", episode.summary);
|
|
231
|
+
if (!Array.isArray(episode.eventIds)) throw new Error("episodes.eventIds must be an array.");
|
|
232
|
+
for (const eventId of episode.eventIds) assertString("episodes.eventIds", eventId);
|
|
233
|
+
if (episode.rawContentStored !== false)
|
|
234
|
+
throw new Error("episodes.rawContentStored must be false.");
|
|
235
|
+
}
|
|
236
|
+
assertEnum("triggerPolicy.strongest", window.triggerPolicy.strongest, TRIGGER_STRENGTHS);
|
|
237
|
+
if (!Array.isArray(window.triggerPolicy.reasons))
|
|
238
|
+
throw new Error("triggerPolicy.reasons must be an array.");
|
|
239
|
+
for (const reason of window.triggerPolicy.reasons) {
|
|
240
|
+
assertEnum("triggerPolicy.reasons", reason, TRIGGER_REASONS);
|
|
241
|
+
}
|
|
242
|
+
assertNoForbiddenRawFields(window);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function validateEvolutionKnowledgeRecord(record: EvolutionKnowledgeRecord): void {
|
|
246
|
+
if (!isRecord(record)) throw new Error("Knowledge record must be an object.");
|
|
247
|
+
if (record.schemaVersion !== 1) throw new Error("Knowledge record schemaVersion must be 1.");
|
|
248
|
+
assertEnum("kind", record.kind, KNOWLEDGE_KINDS);
|
|
249
|
+
assertEnum("reviewState", record.reviewState, REVIEW_STATES);
|
|
250
|
+
assertEnum("authority", record.authority, ["contextual", "reviewed"]);
|
|
251
|
+
if (record.reviewState === "accepted" && record.authority !== "reviewed") {
|
|
252
|
+
throw new Error("Accepted knowledge must use reviewed authority.");
|
|
253
|
+
}
|
|
254
|
+
if (
|
|
255
|
+
(record.reviewState === "auto-stored/unreviewed" ||
|
|
256
|
+
record.reviewState === "auto-accepted" ||
|
|
257
|
+
record.reviewState === "needs-human") &&
|
|
258
|
+
record.authority !== "contextual"
|
|
259
|
+
) {
|
|
260
|
+
throw new Error("Unreviewed knowledge must remain contextual.");
|
|
261
|
+
}
|
|
262
|
+
if (record.runtime.hardBlocking !== false) {
|
|
263
|
+
throw new Error("Knowledge runtime.hardBlocking must be false.");
|
|
264
|
+
}
|
|
265
|
+
assertPrivacy(record.privacy);
|
|
266
|
+
assertProvenance(record.provenance);
|
|
267
|
+
assertNoForbiddenRawFields(record);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function validateEvolutionDistillationBatch(batch: EvolutionDistillationBatch): void {
|
|
271
|
+
if (!isRecord(batch)) throw new Error("Distillation batch must be an object.");
|
|
272
|
+
if (batch.schemaVersion !== 1) throw new Error("Distillation batch schemaVersion must be 1.");
|
|
273
|
+
validateEvolutionEvidenceWindow(batch.evidenceWindow);
|
|
274
|
+
for (const record of batch.knowledgeRecords) validateEvolutionKnowledgeRecord(record);
|
|
275
|
+
for (const evosCase of batch.evosCases) validateEvolutionEvosCase(evosCase);
|
|
276
|
+
for (const proposal of batch.repoProposals) validateEvolutionRepoProposal(proposal);
|
|
277
|
+
assertNoForbiddenRawFields(batch);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function createEvolutionKnowledgeRecord(
|
|
281
|
+
input: Omit<EvolutionKnowledgeRecord, "schemaVersion">,
|
|
282
|
+
): EvolutionKnowledgeRecord {
|
|
283
|
+
const record: EvolutionKnowledgeRecord = {
|
|
284
|
+
...input,
|
|
285
|
+
schemaVersion: 1,
|
|
286
|
+
id: sanitizeId(input.id),
|
|
287
|
+
title: sanitizeText(input.title),
|
|
288
|
+
projectKey: sanitizeId(input.projectKey),
|
|
289
|
+
summary: sanitizeText(input.summary),
|
|
290
|
+
body: sanitizeText(input.body),
|
|
291
|
+
roleTags: uniqueSanitizedIds(input.roleTags),
|
|
292
|
+
tags: uniqueSanitizedIds(input.tags),
|
|
293
|
+
privacy: { ...input.privacy },
|
|
294
|
+
};
|
|
295
|
+
record.privacy.internalLinksStored =
|
|
296
|
+
detectSessionMemorySensitivity(record).reasons.includes("url");
|
|
297
|
+
validateEvolutionKnowledgeRecord(record);
|
|
298
|
+
return record;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function createEvolutionEvosCase(
|
|
302
|
+
input: Omit<EvolutionEvosCase, "schemaVersion" | "kind">,
|
|
303
|
+
): EvolutionEvosCase {
|
|
304
|
+
const evosCase: EvolutionEvosCase = {
|
|
305
|
+
...input,
|
|
306
|
+
schemaVersion: 1,
|
|
307
|
+
kind: "evos-case",
|
|
308
|
+
id: sanitizeId(input.id),
|
|
309
|
+
projectKey: sanitizeId(input.projectKey),
|
|
310
|
+
title: sanitizeText(input.title),
|
|
311
|
+
roleTags: uniqueSanitizedIds(input.roleTags),
|
|
312
|
+
tags: uniqueSanitizedIds(input.tags),
|
|
313
|
+
trigger: {
|
|
314
|
+
kind: input.trigger.kind,
|
|
315
|
+
summary: sanitizeText(input.trigger.summary),
|
|
316
|
+
},
|
|
317
|
+
intervention: {
|
|
318
|
+
summary: sanitizeText(input.intervention.summary),
|
|
319
|
+
roleIds: uniqueSanitizedIds(input.intervention.roleIds),
|
|
320
|
+
},
|
|
321
|
+
result: {
|
|
322
|
+
summary: sanitizeText(input.result.summary),
|
|
323
|
+
verificationSignals: input.result.verificationSignals.map(sanitizeText),
|
|
324
|
+
},
|
|
325
|
+
expectedFutureBehavior: sanitizeText(input.expectedFutureBehavior),
|
|
326
|
+
privacy: { ...input.privacy },
|
|
327
|
+
};
|
|
328
|
+
evosCase.privacy.internalLinksStored =
|
|
329
|
+
detectSessionMemorySensitivity(evosCase).reasons.includes("url");
|
|
330
|
+
validateEvolutionEvosCase(evosCase);
|
|
331
|
+
return evosCase;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function createEvolutionRepoProposal(
|
|
335
|
+
input: Omit<EvolutionRepoProposal, "schemaVersion">,
|
|
336
|
+
): EvolutionRepoProposal {
|
|
337
|
+
const proposal: EvolutionRepoProposal = {
|
|
338
|
+
...input,
|
|
339
|
+
schemaVersion: 1,
|
|
340
|
+
id: sanitizeId(input.id),
|
|
341
|
+
projectKey: sanitizeId(input.projectKey),
|
|
342
|
+
title: sanitizeText(input.title),
|
|
343
|
+
summary: sanitizeText(input.summary),
|
|
344
|
+
rationale: sanitizeText(input.rationale),
|
|
345
|
+
roleTags: uniqueSanitizedIds(input.roleTags),
|
|
346
|
+
tags: uniqueSanitizedIds(input.tags),
|
|
347
|
+
plannedFiles: input.plannedFiles.map((file) => ({
|
|
348
|
+
relativePath: sanitizeRelativePath(file.relativePath),
|
|
349
|
+
action: file.action,
|
|
350
|
+
reason: sanitizeText(file.reason),
|
|
351
|
+
...(file.proposedChange === undefined
|
|
352
|
+
? {}
|
|
353
|
+
: { proposedChange: sanitizeProposedChange(file.proposedChange) }),
|
|
354
|
+
})),
|
|
355
|
+
privacy: { ...input.privacy },
|
|
356
|
+
};
|
|
357
|
+
proposal.privacy.internalLinksStored =
|
|
358
|
+
detectSessionMemorySensitivity(proposal).reasons.includes("url");
|
|
359
|
+
validateEvolutionRepoProposal(proposal);
|
|
360
|
+
return proposal;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function validateEvolutionEvosCase(evosCase: EvolutionEvosCase): void {
|
|
364
|
+
if (!isRecord(evosCase)) throw new Error("Evos case must be an object.");
|
|
365
|
+
if (evosCase.schemaVersion !== 1) throw new Error("Evos case schemaVersion must be 1.");
|
|
366
|
+
if (evosCase.kind !== "evos-case") throw new Error("Evos case kind is invalid.");
|
|
367
|
+
assertEnum("reviewState", evosCase.reviewState, REVIEW_STATES);
|
|
368
|
+
assertPrivacy(evosCase.privacy);
|
|
369
|
+
assertProvenance(evosCase.provenance);
|
|
370
|
+
assertNoForbiddenRawFields(evosCase);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function validateEvolutionRepoProposal(proposal: EvolutionRepoProposal): void {
|
|
374
|
+
if (!isRecord(proposal)) throw new Error("Repo proposal must be an object.");
|
|
375
|
+
if (proposal.schemaVersion !== 1) throw new Error("Repo proposal schemaVersion must be 1.");
|
|
376
|
+
assertEnum("kind", proposal.kind, PROPOSAL_KINDS);
|
|
377
|
+
assertEnum("reviewState", proposal.reviewState, [
|
|
378
|
+
"pending",
|
|
379
|
+
"accepted",
|
|
380
|
+
"rejected",
|
|
381
|
+
"deferred",
|
|
382
|
+
"applied",
|
|
383
|
+
]);
|
|
384
|
+
if (
|
|
385
|
+
proposal.reviewStateChangedAt !== undefined &&
|
|
386
|
+
Number.isNaN(new Date(proposal.reviewStateChangedAt).getTime())
|
|
387
|
+
) {
|
|
388
|
+
throw new Error("Repo proposal reviewStateChangedAt must be a valid timestamp.");
|
|
389
|
+
}
|
|
390
|
+
for (const file of proposal.plannedFiles) {
|
|
391
|
+
if (file.proposedChange !== undefined && file.proposedChange.trim() === "") {
|
|
392
|
+
throw new Error("Repo proposal proposedChange must not be empty.");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (proposal.lastDecision !== undefined) {
|
|
396
|
+
assertEnum("lastDecision.state", proposal.lastDecision.state, [
|
|
397
|
+
"accepted",
|
|
398
|
+
"rejected",
|
|
399
|
+
"deferred",
|
|
400
|
+
]);
|
|
401
|
+
if (
|
|
402
|
+
proposal.lastDecision.reason !== null &&
|
|
403
|
+
(proposal.lastDecision.reason.trim() === "" || proposal.lastDecision.reason.length > 500)
|
|
404
|
+
) {
|
|
405
|
+
throw new Error("Repo proposal decision reason must be 1-500 characters.");
|
|
406
|
+
}
|
|
407
|
+
if (Number.isNaN(new Date(proposal.lastDecision.decidedAt).getTime())) {
|
|
408
|
+
throw new Error("Repo proposal decision timestamp must be valid.");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (proposal.apply.autoApply !== false || proposal.apply.requiresExplicitCommand !== true) {
|
|
412
|
+
throw new Error("Repo proposal must require explicit apply.");
|
|
413
|
+
}
|
|
414
|
+
assertPrivacy(proposal.privacy);
|
|
415
|
+
assertProvenance(proposal.provenance);
|
|
416
|
+
assertNoForbiddenRawFields(proposal);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function hasConcreteRepoProposalChanges(proposal: EvolutionRepoProposal): boolean {
|
|
420
|
+
return (
|
|
421
|
+
proposal.targetRepoPath !== null &&
|
|
422
|
+
proposal.targetRepoPath.trim() !== "" &&
|
|
423
|
+
proposal.plannedFiles.length > 0 &&
|
|
424
|
+
proposal.plannedFiles.every(
|
|
425
|
+
(file) => file.proposedChange !== undefined && file.proposedChange.trim() !== "",
|
|
426
|
+
)
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function validateEvolutionReviewCandidate(candidate: EvolutionReviewCandidate): void {
|
|
431
|
+
if (!isRecord(candidate)) throw new Error("Review candidate must be an object.");
|
|
432
|
+
if (candidate.schemaVersion !== 1) {
|
|
433
|
+
throw new Error("Review candidate schemaVersion must be 1.");
|
|
434
|
+
}
|
|
435
|
+
if (candidate.kind !== "evolution-review-candidate") {
|
|
436
|
+
throw new Error("Review candidate kind is invalid.");
|
|
437
|
+
}
|
|
438
|
+
assertString("reviewCandidate.id", candidate.id);
|
|
439
|
+
assertString("reviewCandidate.projectKey", candidate.projectKey);
|
|
440
|
+
assertString("reviewCandidate.runId", candidate.runId);
|
|
441
|
+
assertString("reviewCandidate.createdAt", candidate.createdAt);
|
|
442
|
+
assertString("reviewCandidate.candidateKind", candidate.candidateKind);
|
|
443
|
+
assertString("reviewCandidate.title", candidate.title);
|
|
444
|
+
assertString("reviewCandidate.targetStore", candidate.targetStore);
|
|
445
|
+
assertString("reviewCandidate.targetPath", candidate.targetPath);
|
|
446
|
+
assertString("reviewCandidate.stableKey", candidate.stableKey);
|
|
447
|
+
assertEnum("reviewCandidate.reviewState", candidate.reviewState, [
|
|
448
|
+
"needs-human",
|
|
449
|
+
"accepted",
|
|
450
|
+
"rejected",
|
|
451
|
+
"deferred",
|
|
452
|
+
]);
|
|
453
|
+
if (
|
|
454
|
+
candidate.reviewStateChangedAt !== undefined &&
|
|
455
|
+
Number.isNaN(new Date(candidate.reviewStateChangedAt).getTime())
|
|
456
|
+
) {
|
|
457
|
+
throw new Error("Review candidate reviewStateChangedAt must be a valid timestamp.");
|
|
458
|
+
}
|
|
459
|
+
if (!Array.isArray(candidate.reasons))
|
|
460
|
+
throw new Error("Review candidate reasons must be an array.");
|
|
461
|
+
for (const reason of candidate.reasons) assertString("reviewCandidate.reasons", reason);
|
|
462
|
+
if (!isRecord(candidate.provenance))
|
|
463
|
+
throw new Error("Review candidate provenance must be an object.");
|
|
464
|
+
assertString("reviewCandidate.provenance.runId", candidate.provenance.runId);
|
|
465
|
+
assertString(
|
|
466
|
+
"reviewCandidate.provenance.evidenceWindowId",
|
|
467
|
+
candidate.provenance.evidenceWindowId,
|
|
468
|
+
);
|
|
469
|
+
if (candidate.provenance.createdBy !== "evodev") {
|
|
470
|
+
throw new Error("Review candidate provenance.createdBy must be evodev.");
|
|
471
|
+
}
|
|
472
|
+
if (!Array.isArray(candidate.provenance.evidenceRefs)) {
|
|
473
|
+
throw new Error("Review candidate evidenceRefs must be an array.");
|
|
474
|
+
}
|
|
475
|
+
for (const evidenceRef of candidate.provenance.evidenceRefs) {
|
|
476
|
+
assertString("reviewCandidate.provenance.evidenceRefs", evidenceRef);
|
|
477
|
+
}
|
|
478
|
+
if (
|
|
479
|
+
candidate.provenance.rawLogsStored !== false ||
|
|
480
|
+
candidate.provenance.rawPromptsStored !== false ||
|
|
481
|
+
candidate.provenance.sourceDumpsStored !== false ||
|
|
482
|
+
candidate.provenance.rawCommandOutputStored !== false
|
|
483
|
+
) {
|
|
484
|
+
throw new Error("Review candidate provenance must remain metadata-only.");
|
|
485
|
+
}
|
|
486
|
+
assertPrivacy(candidate.privacy);
|
|
487
|
+
assertNoForbiddenRawFields(candidate);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function validateEvolutionTriggerRecord(trigger: EvolutionTriggerRecord): void {
|
|
491
|
+
if (!isRecord(trigger)) throw new Error("Evolution trigger must be an object.");
|
|
492
|
+
if (trigger.schemaVersion !== 1) throw new Error("Evolution trigger schemaVersion must be 1.");
|
|
493
|
+
if (trigger.kind !== "evolution-trigger") throw new Error("Evolution trigger kind is invalid.");
|
|
494
|
+
assertString("trigger.id", trigger.id);
|
|
495
|
+
assertString("trigger.projectKey", trigger.projectKey);
|
|
496
|
+
assertString("trigger.runId", trigger.runId);
|
|
497
|
+
assertEnum("trigger.eventType", trigger.eventType, NORMALIZED_EVENT_TYPES);
|
|
498
|
+
assertEnum("trigger.triggerStrength", trigger.triggerStrength, TRIGGER_STRENGTHS);
|
|
499
|
+
assertEnum("trigger.triggerReason", trigger.triggerReason, TRIGGER_REASONS);
|
|
500
|
+
assertEnum("trigger.status", trigger.status, TRIGGER_STATUSES);
|
|
501
|
+
assertString("trigger.summary", trigger.summary);
|
|
502
|
+
assertString("trigger.createdAt", trigger.createdAt);
|
|
503
|
+
assertString("trigger.updatedAt", trigger.updatedAt);
|
|
504
|
+
if (typeof trigger.attempts !== "number" || trigger.attempts < 0) {
|
|
505
|
+
throw new Error("trigger.attempts must be a non-negative number.");
|
|
506
|
+
}
|
|
507
|
+
if (trigger.rawContentStored !== false)
|
|
508
|
+
throw new Error("trigger.rawContentStored must be false.");
|
|
509
|
+
assertPrivacy(trigger.privacy);
|
|
510
|
+
assertNoForbiddenRawFields(trigger);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function validateSegmentEvolutionTriggerRecord(
|
|
514
|
+
trigger: SegmentEvolutionTriggerRecord,
|
|
515
|
+
): void {
|
|
516
|
+
if (!isRecord(trigger)) throw new Error("Segment evolution trigger must be an object.");
|
|
517
|
+
if (trigger.schemaVersion !== 1)
|
|
518
|
+
throw new Error("Segment evolution trigger schemaVersion must be 1.");
|
|
519
|
+
if (trigger.kind !== "segment-evolution-trigger") {
|
|
520
|
+
throw new Error("Segment evolution trigger kind is invalid.");
|
|
521
|
+
}
|
|
522
|
+
assertString("segmentTrigger.id", trigger.id);
|
|
523
|
+
assertString("segmentTrigger.projectKey", trigger.projectKey);
|
|
524
|
+
assertString("segmentTrigger.sessionKey", trigger.sessionKey);
|
|
525
|
+
assertString("segmentTrigger.segmentId", trigger.segmentId);
|
|
526
|
+
assertString("segmentTrigger.segmentPath", trigger.segmentPath);
|
|
527
|
+
assertEnum("segmentTrigger.strength", trigger.strength, SEGMENT_TRIGGER_STRENGTHS);
|
|
528
|
+
assertEnum("segmentTrigger.reason", trigger.reason, SEGMENT_TRIGGER_REASONS);
|
|
529
|
+
assertEnum("segmentTrigger.status", trigger.status, TRIGGER_STATUSES);
|
|
530
|
+
assertString("segmentTrigger.summary", trigger.summary);
|
|
531
|
+
assertString("segmentTrigger.createdAt", trigger.createdAt);
|
|
532
|
+
assertString("segmentTrigger.updatedAt", trigger.updatedAt);
|
|
533
|
+
if (trigger.runId !== null) assertString("segmentTrigger.runId", trigger.runId);
|
|
534
|
+
if (trigger.roleId !== null) assertString("segmentTrigger.roleId", trigger.roleId);
|
|
535
|
+
if (typeof trigger.attempts !== "number" || trigger.attempts < 0) {
|
|
536
|
+
throw new Error("segmentTrigger.attempts must be a non-negative number.");
|
|
537
|
+
}
|
|
538
|
+
if (trigger.rawContentStored !== false) {
|
|
539
|
+
throw new Error("segmentTrigger.rawContentStored must be false.");
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function parseKnowledgeRecord(value: unknown): EvolutionKnowledgeRecord {
|
|
544
|
+
validateEvolutionKnowledgeRecord(value as EvolutionKnowledgeRecord);
|
|
545
|
+
return value as EvolutionKnowledgeRecord;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function parseEvosCase(value: unknown): EvolutionEvosCase {
|
|
549
|
+
validateEvolutionEvosCase(value as EvolutionEvosCase);
|
|
550
|
+
return value as EvolutionEvosCase;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export function parseRepoProposal(value: unknown): EvolutionRepoProposal {
|
|
554
|
+
validateEvolutionRepoProposal(value as EvolutionRepoProposal);
|
|
555
|
+
return value as EvolutionRepoProposal;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function parseReviewCandidate(value: unknown): EvolutionReviewCandidate {
|
|
559
|
+
validateEvolutionReviewCandidate(value as EvolutionReviewCandidate);
|
|
560
|
+
return value as EvolutionReviewCandidate;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function parseTrigger(value: unknown): EvolutionTriggerRecord {
|
|
564
|
+
validateEvolutionTriggerRecord(value as EvolutionTriggerRecord);
|
|
565
|
+
return value as EvolutionTriggerRecord;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function parseSegmentTrigger(value: unknown): SegmentEvolutionTriggerRecord {
|
|
569
|
+
validateSegmentEvolutionTriggerRecord(value as SegmentEvolutionTriggerRecord);
|
|
570
|
+
return value as SegmentEvolutionTriggerRecord;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export async function listDirectoryNames(dir: string): Promise<string[]> {
|
|
574
|
+
if (!(await pathExists(dir))) return [];
|
|
575
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
576
|
+
return entries
|
|
577
|
+
.filter((entry) => entry.isDirectory())
|
|
578
|
+
.map((entry) => entry.name)
|
|
579
|
+
.sort();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function uniqueSorted(values: string[]): string[] {
|
|
583
|
+
return [...new Set(values)].sort();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function collectRoleIds(evidenceWindow: EvolutionEvidenceWindow): string[] {
|
|
587
|
+
return uniqueSanitizedIds([
|
|
588
|
+
...evidenceWindow.sourceRefs.flatMap((sourceRef) =>
|
|
589
|
+
sourceRef.roleId === null ? [] : [sourceRef.roleId],
|
|
590
|
+
),
|
|
591
|
+
...evidenceWindow.events.flatMap((event) => (event.roleId === null ? [] : [event.roleId])),
|
|
592
|
+
]);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export function createPrivacyFields(value?: unknown): EvolutionPrivacyFields {
|
|
596
|
+
return {
|
|
597
|
+
classification: "local-private",
|
|
598
|
+
rawPromptsStored: false,
|
|
599
|
+
rawLogsStored: false,
|
|
600
|
+
sourceDumpsStored: false,
|
|
601
|
+
rawCommandOutputStored: false,
|
|
602
|
+
secretsStored: false,
|
|
603
|
+
internalLinksStored:
|
|
604
|
+
value !== undefined && detectSessionMemorySensitivity(value).reasons.includes("url"),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export function assertPrivacy(privacy: EvolutionPrivacyFields): void {
|
|
609
|
+
if (!isRecord(privacy)) throw new Error("privacy must be an object.");
|
|
610
|
+
if (privacy.classification !== "local-private") {
|
|
611
|
+
throw new Error("privacy.classification must be local-private.");
|
|
612
|
+
}
|
|
613
|
+
if (
|
|
614
|
+
privacy.rawPromptsStored !== false ||
|
|
615
|
+
privacy.rawLogsStored !== false ||
|
|
616
|
+
privacy.sourceDumpsStored !== false ||
|
|
617
|
+
privacy.rawCommandOutputStored !== false ||
|
|
618
|
+
privacy.secretsStored !== false
|
|
619
|
+
) {
|
|
620
|
+
throw new Error("Evolution privacy fields must forbid raw content and credentials.");
|
|
621
|
+
}
|
|
622
|
+
if (typeof privacy.internalLinksStored !== "boolean") {
|
|
623
|
+
throw new Error("privacy.internalLinksStored must be boolean.");
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function assertProvenance(provenance: EvolutionKnowledgeRecord["provenance"]): void {
|
|
628
|
+
if (!isRecord(provenance)) throw new Error("provenance must be an object.");
|
|
629
|
+
assertString("provenance.runId", provenance.runId);
|
|
630
|
+
assertString("provenance.evidenceWindowId", provenance.evidenceWindowId);
|
|
631
|
+
assertString("provenance.createdAt", provenance.createdAt);
|
|
632
|
+
if (provenance.createdBy !== "evodev") throw new Error("provenance.createdBy must be evodev.");
|
|
633
|
+
if (
|
|
634
|
+
provenance.rawLogsStored !== false ||
|
|
635
|
+
provenance.rawPromptsStored !== false ||
|
|
636
|
+
provenance.sourceDumpsStored !== false ||
|
|
637
|
+
provenance.rawCommandOutputStored !== false
|
|
638
|
+
) {
|
|
639
|
+
throw new Error(
|
|
640
|
+
"Evolution provenance cannot store raw logs, prompts, source, or command output.",
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function assertEnum<T extends string>(
|
|
646
|
+
field: string,
|
|
647
|
+
value: unknown,
|
|
648
|
+
allowed: readonly T[],
|
|
649
|
+
): asserts value is T {
|
|
650
|
+
if (typeof value !== "string" || !allowed.includes(value as T)) {
|
|
651
|
+
throw new Error(`${field} must be one of: ${allowed.join(", ")}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export function assertString(field: string, value: unknown): asserts value is string {
|
|
656
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
657
|
+
throw new Error(`${field} must be a non-empty string.`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function assertNoForbiddenRawFields(value: unknown, path = ""): void {
|
|
662
|
+
if (value === null || value === undefined) return;
|
|
663
|
+
if (typeof value === "string") {
|
|
664
|
+
if (detectSessionMemorySensitivity(value).classification === "credential") {
|
|
665
|
+
throw new Error(`Evolution record contains a credential at ${path || "value"}.`);
|
|
666
|
+
}
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (typeof value !== "object") return;
|
|
670
|
+
if (Array.isArray(value)) {
|
|
671
|
+
value.forEach((item, index) => assertNoForbiddenRawFields(item, `${path}[${index}]`));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
for (const [key, child] of Object.entries(value)) {
|
|
675
|
+
const normalizedKey = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
676
|
+
if (FORBIDDEN_RAW_KEYS.has(normalizedKey)) {
|
|
677
|
+
throw new Error(
|
|
678
|
+
`Evolution record contains forbidden raw field: ${path ? `${path}.` : ""}${key}`,
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
assertNoForbiddenRawFields(child, path ? `${path}.${key}` : key);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export function sanitizeText(value: string): string {
|
|
686
|
+
return redactSessionMemoryCredentialText(value)
|
|
687
|
+
.value.replace(/\s+/g, " ")
|
|
688
|
+
.trim()
|
|
689
|
+
.slice(0, MAX_TEXT_LENGTH);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export function sanitizeProposedChange(value: string): string {
|
|
693
|
+
return redactSessionMemoryCredentialText(value)
|
|
694
|
+
.value.replaceAll("\r\n", "\n")
|
|
695
|
+
.replaceAll("\r", "\n")
|
|
696
|
+
.trim()
|
|
697
|
+
.slice(0, MAX_PROPOSED_CHANGE_LENGTH);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function sanitizeId(value: string): string {
|
|
701
|
+
return (
|
|
702
|
+
value
|
|
703
|
+
.replace(/[^a-zA-Z0-9._/-]/g, "-")
|
|
704
|
+
.replace(/[\\/]+/g, "-")
|
|
705
|
+
.slice(0, 160) || "local"
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export function sanitizeStorageId(field: string, value: string): string {
|
|
710
|
+
const sanitized = sanitizeId(value);
|
|
711
|
+
if (sanitized === "." || sanitized === "..") {
|
|
712
|
+
throw new Error(`Invalid evolution ${field}: ${value}`);
|
|
713
|
+
}
|
|
714
|
+
if (sanitized.includes("/") || sanitized.includes("\\")) {
|
|
715
|
+
throw new Error(`Invalid evolution ${field}: ${value}`);
|
|
716
|
+
}
|
|
717
|
+
return sanitized;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
export function assertPathDescendant(root: string, candidate: string, field: string): void {
|
|
721
|
+
const normalizedRoot = resolve(root);
|
|
722
|
+
const normalizedCandidate = resolve(candidate);
|
|
723
|
+
const relativePath = relative(normalizedRoot, normalizedCandidate);
|
|
724
|
+
if (relativePath === "" || relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
725
|
+
throw new Error(`Evolution path ${field} escaped expected root.`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function sanitizeOptionalId(value: unknown): string | null {
|
|
730
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
731
|
+
return sanitizeId(value);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export function uniqueSanitizedIds(values: string[]): string[] {
|
|
735
|
+
return [...new Set(values.map(sanitizeId).filter((value) => value !== ""))];
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
export function sanitizeRelativePath(value: string): string {
|
|
739
|
+
const sanitized = value.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
740
|
+
if (sanitized === "" || sanitized.startsWith("../") || sanitized.includes("/../")) {
|
|
741
|
+
throw new Error(`Invalid relative path in repo proposal: ${value}`);
|
|
742
|
+
}
|
|
743
|
+
return sanitized;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export function displayPath(homeDir: string, path: string): string {
|
|
747
|
+
const relativePath = relative(homeDir, path);
|
|
748
|
+
if (relativePath !== "" && !relativePath.startsWith("..")) return `~/${relativePath}`;
|
|
749
|
+
return path;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
export function optionalString(value: unknown): string | null {
|
|
753
|
+
return typeof value === "string" && value.trim() !== "" ? value : null;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
757
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
758
|
+
}
|