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

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.
@@ -0,0 +1,472 @@
1
+ import { sha256Hex } from "../../utils/index.ts";
2
+ import {
3
+ type CanonicalImportRecordType,
4
+ type CanonicalImportSnapshot,
5
+ type CanonicalImportSourceIdentityKind,
6
+ type CreateTrajectoryImportPreviewInput,
7
+ TRAJECTORY_IMPORT_DIAGNOSTIC_POLICY_VERSION,
8
+ TRAJECTORY_IMPORT_PREVIEW_SCHEMA_VERSION,
9
+ type TrajectoryImportDiagnosticSeverity,
10
+ type TrajectoryImportDownstreamPreviewV1,
11
+ type TrajectoryImportGenerationContractV1,
12
+ type TrajectoryImportIndexRecordV1,
13
+ type TrajectoryImportPreviewBuildResult,
14
+ type TrajectoryImportPreviewV1,
15
+ } from "./types.ts";
16
+
17
+ const SOURCE_KEY_HASH_LENGTH = 24;
18
+ const STATE_ID_HASH_LENGTH = 24;
19
+ const PREVIEW_ID_HASH_LENGTH = 24;
20
+ const MAX_SCOPE_ID_LENGTH = 120;
21
+ const MAX_EXPLICIT_SOURCE_ID_LENGTH = 256;
22
+
23
+ const DIAGNOSTIC_POLICY = {
24
+ invalid_json_line: "blocker",
25
+ non_object_json_line: "blocker",
26
+ injected_context_dropped: "info",
27
+ noise_record_dropped: "info",
28
+ sidechain_record_dropped: "info",
29
+ tool_call_id_synthesized: "warning",
30
+ duplicate_tool_call_id: "blocker",
31
+ orphan_tool_result: "warning",
32
+ duplicate_tool_result: "blocker",
33
+ unknown_tool_name: "warning",
34
+ tool_arguments_reshaped: "warning",
35
+ tool_arguments_truncated: "warning",
36
+ tool_result_truncated: "warning",
37
+ timestamps_synthesized: "info",
38
+ timestamps_interpolated: "info",
39
+ } as const satisfies Record<string, TrajectoryImportDiagnosticSeverity>;
40
+
41
+ const EMPTY_MATERIALIZER_CONFIG_HASH = sha256Hex(
42
+ stableTrajectoryImportJson({ implemented: false }),
43
+ );
44
+
45
+ const DEFAULT_DOWNSTREAM: TrajectoryImportDownstreamPreviewV1 = {
46
+ semanticProcessing: "queue-only",
47
+ automationKnowledge: false,
48
+ automationSemanticKnowledge: false,
49
+ mayInvokeConfiguredProviderAfterApply: false,
50
+ };
51
+
52
+ export function createTrajectoryImportPreview(
53
+ input: CreateTrajectoryImportPreviewInput,
54
+ ): TrajectoryImportPreviewBuildResult {
55
+ const projectKey = normalizeScopeId(input.projectKey, "projectKey");
56
+ const roleId = normalizeScopeId(input.roleId, "roleId");
57
+ if (roleId !== null && projectKey === null) {
58
+ throw new Error("Trajectory import --role requires --project.");
59
+ }
60
+ validateSnapshot(input.snapshot);
61
+
62
+ const blockers = new Set<string>();
63
+ const sourceKey = resolveSourceKey(input.snapshot, blockers);
64
+ const generationContract = createGenerationContract(input.snapshot);
65
+ const previous = input.previous ?? null;
66
+
67
+ if (previous !== null) {
68
+ if (previous.sourceKey !== sourceKey) blockers.add("source-key-mismatch");
69
+ if (!sameGenerationContract(previous.generationContract, generationContract)) {
70
+ blockers.add("normalizer-contract-changed");
71
+ }
72
+ }
73
+
74
+ const indexed = createIndex(input.snapshot, sourceKey, blockers);
75
+ const diff = compareIndexes(indexed, previous?.records ?? [], blockers);
76
+ const generationId =
77
+ previous?.generationId ??
78
+ createStateId("gen", [
79
+ sourceKey,
80
+ stableTrajectoryImportJson(generationContract),
81
+ input.snapshot.inputFingerprint,
82
+ ]);
83
+ const snapshotId = createTrajectoryImportSnapshotId(generationId, indexed);
84
+ const diagnostics = summarizeDiagnostics(input.snapshot.diagnostics, blockers);
85
+ const previewWithoutId: Omit<TrajectoryImportPreviewV1, "id"> = {
86
+ schemaVersion: TRAJECTORY_IMPORT_PREVIEW_SCHEMA_VERSION,
87
+ kind: "trajectory-import-preview",
88
+ source: input.snapshot.source,
89
+ sourceKey,
90
+ projectKey,
91
+ scope: {
92
+ roleId,
93
+ },
94
+ input: {
95
+ bytes: input.snapshot.inputBytes,
96
+ fingerprint: input.snapshot.inputFingerprint,
97
+ pathHash: input.pathHash,
98
+ },
99
+ normalizer: {
100
+ version: input.snapshot.normalizer.version,
101
+ canonicalSchemaVersion: input.snapshot.normalizer.canonicalSchemaVersion,
102
+ canonicalConfigHash: input.snapshot.normalizer.canonicalConfigHash,
103
+ },
104
+ contracts: {
105
+ diagnosticPolicyVersion: TRAJECTORY_IMPORT_DIAGNOSTIC_POLICY_VERSION,
106
+ materializerVersion: 0,
107
+ materializerConfigHash: EMPTY_MATERIALIZER_CONFIG_HASH,
108
+ },
109
+ generationId,
110
+ snapshotId,
111
+ previousSnapshotId: previous?.snapshotId ?? null,
112
+ records: {
113
+ total: indexed.length,
114
+ ...diff,
115
+ recordTypes: countRecordTypes(indexed),
116
+ identityKinds: countIdentityKinds(indexed),
117
+ },
118
+ diagnostics,
119
+ materialization: null,
120
+ downstream: DEFAULT_DOWNSTREAM,
121
+ blockers: [],
122
+ canApply: false,
123
+ rawContentStored: false,
124
+ };
125
+ previewWithoutId.blockers = [...blockers].sort();
126
+
127
+ const previewId = createTrajectoryImportPreviewId(previewWithoutId);
128
+ const { schemaVersion, kind, ...previewFields } = previewWithoutId;
129
+ const preview: TrajectoryImportPreviewV1 = {
130
+ schemaVersion,
131
+ kind,
132
+ id: previewId,
133
+ ...previewFields,
134
+ };
135
+
136
+ return {
137
+ preview,
138
+ comparisonSnapshot: {
139
+ sourceKey,
140
+ generationId,
141
+ initialInputFingerprint: previous?.initialInputFingerprint ?? input.snapshot.inputFingerprint,
142
+ generationContract,
143
+ snapshotId,
144
+ records: indexed,
145
+ },
146
+ };
147
+ }
148
+
149
+ export function createTrajectoryImportConfigHash(value: unknown): string {
150
+ return sha256Hex(stableTrajectoryImportJson(value));
151
+ }
152
+
153
+ export function createTrajectoryImportIndexDigest(
154
+ records: TrajectoryImportIndexRecordV1[],
155
+ ): string {
156
+ return sha256Hex(
157
+ stableTrajectoryImportJson(
158
+ records.map((record) => ({
159
+ schemaVersion: record.schemaVersion,
160
+ kind: record.kind,
161
+ stableRecordKeyHash: record.stableRecordKeyHash,
162
+ contentDigest: record.contentDigest,
163
+ ordinal: record.ordinal,
164
+ sourceIdentityKind: record.sourceIdentityKind,
165
+ recordType: record.recordType,
166
+ })),
167
+ ),
168
+ );
169
+ }
170
+
171
+ export function createTrajectoryImportSnapshotId(
172
+ generationId: string,
173
+ records: TrajectoryImportIndexRecordV1[],
174
+ ): string {
175
+ return createStateId("snap", [generationId, createTrajectoryImportIndexDigest(records)]);
176
+ }
177
+
178
+ export function createTrajectoryImportStableRecordKeyHash(
179
+ sourceKey: string,
180
+ recordId: string,
181
+ ): string {
182
+ return sha256Hex(`${sourceKey}\0${recordId}`);
183
+ }
184
+
185
+ export function createTrajectoryImportContentDigest(
186
+ sourceKey: string,
187
+ contentHash: string,
188
+ ): string {
189
+ return sha256Hex(`${sourceKey}\0${contentHash}`);
190
+ }
191
+
192
+ export function createTrajectoryImportPreviewId(
193
+ preview: Omit<TrajectoryImportPreviewV1, "id">,
194
+ ): string {
195
+ const previewIdentity = {
196
+ ...preview,
197
+ input: {
198
+ bytes: preview.input.bytes,
199
+ fingerprint: preview.input.fingerprint,
200
+ },
201
+ };
202
+ return `preview-${sha256Hex(stableTrajectoryImportJson(previewIdentity)).slice(
203
+ 0,
204
+ PREVIEW_ID_HASH_LENGTH,
205
+ )}`;
206
+ }
207
+
208
+ export function stableTrajectoryImportJson(value: unknown): string {
209
+ if (value === null || typeof value !== "object") {
210
+ const serialized = JSON.stringify(value);
211
+ return serialized === undefined ? "null" : serialized;
212
+ }
213
+ if (Array.isArray(value)) {
214
+ return `[${value.map(stableTrajectoryImportJson).join(",")}]`;
215
+ }
216
+ const entries = Object.entries(value as Record<string, unknown>)
217
+ .filter(([, child]) => child !== undefined)
218
+ .sort(([left], [right]) => left.localeCompare(right));
219
+ return `{${entries
220
+ .map(([key, child]) => `${JSON.stringify(key)}:${stableTrajectoryImportJson(child)}`)
221
+ .join(",")}}`;
222
+ }
223
+
224
+ function validateSnapshot(snapshot: CanonicalImportSnapshot): void {
225
+ if (snapshot.inputBytes < 0 || !Number.isSafeInteger(snapshot.inputBytes)) {
226
+ throw new Error("Trajectory import inputBytes must be a non-negative safe integer.");
227
+ }
228
+ assertNonEmpty("inputFingerprint", snapshot.inputFingerprint);
229
+ assertNonEmpty("normalizer.version", snapshot.normalizer.version);
230
+ assertNonEmpty("normalizer.canonicalConfigHash", snapshot.normalizer.canonicalConfigHash);
231
+ if (
232
+ !Number.isInteger(snapshot.normalizer.canonicalSchemaVersion) ||
233
+ snapshot.normalizer.canonicalSchemaVersion < 1
234
+ ) {
235
+ throw new Error("Trajectory import canonicalSchemaVersion must be a positive integer.");
236
+ }
237
+ if (snapshot.explicitSourceId !== null) {
238
+ const sourceId = snapshot.explicitSourceId;
239
+ if (
240
+ sourceId.trim() === "" ||
241
+ sourceId.length > MAX_EXPLICIT_SOURCE_ID_LENGTH ||
242
+ containsControlCharacter(sourceId)
243
+ ) {
244
+ throw new Error("Trajectory import --source-id must be a bounded printable value.");
245
+ }
246
+ }
247
+ for (const group of snapshot.groups) {
248
+ assertNonEmpty("sourceGroupId", group.sourceGroupId);
249
+ for (const record of group.records) {
250
+ assertNonEmpty("recordId", record.recordId);
251
+ assertNonEmpty("contentHash", record.contentHash);
252
+ assertNonEmpty("sourceOrderId", record.sourceOrderId);
253
+ if (!Number.isSafeInteger(record.componentIndex) || record.componentIndex < 0) {
254
+ throw new Error("Trajectory import componentIndex must be a non-negative safe integer.");
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ function resolveSourceKey(snapshot: CanonicalImportSnapshot, blockers: Set<string>): string {
261
+ if (snapshot.groups.length !== 1) {
262
+ blockers.add(snapshot.groups.length === 0 ? "source-group-missing" : "multiple-source-groups");
263
+ return `src-unresolved-${sha256Hex(`${snapshot.source}\0${snapshot.inputFingerprint}`).slice(
264
+ 0,
265
+ SOURCE_KEY_HASH_LENGTH,
266
+ )}`;
267
+ }
268
+ const groupId = snapshot.groups[0]?.sourceGroupId ?? "";
269
+ let locator: string;
270
+ if (groupId === "default") {
271
+ if (snapshot.explicitSourceId === null) {
272
+ blockers.add("explicit-source-id-required");
273
+ locator = `unresolved:${snapshot.inputFingerprint}`;
274
+ } else {
275
+ locator = `explicit:${snapshot.explicitSourceId}`;
276
+ }
277
+ } else {
278
+ locator = `group:${groupId}`;
279
+ }
280
+ return `src-${sha256Hex(`${snapshot.source}\0${locator}`).slice(0, SOURCE_KEY_HASH_LENGTH)}`;
281
+ }
282
+
283
+ function createGenerationContract(
284
+ snapshot: CanonicalImportSnapshot,
285
+ ): TrajectoryImportGenerationContractV1 {
286
+ return {
287
+ normalizerVersion: snapshot.normalizer.version,
288
+ canonicalSchemaVersion: snapshot.normalizer.canonicalSchemaVersion,
289
+ canonicalConfigHash: snapshot.normalizer.canonicalConfigHash,
290
+ };
291
+ }
292
+
293
+ function createIndex(
294
+ snapshot: CanonicalImportSnapshot,
295
+ sourceKey: string,
296
+ blockers: Set<string>,
297
+ ): TrajectoryImportIndexRecordV1[] {
298
+ const candidates = snapshot.groups.flatMap((group) =>
299
+ group.records.map((record) => ({
300
+ stableRecordKeyHash: createTrajectoryImportStableRecordKeyHash(sourceKey, record.recordId),
301
+ contentDigest: createTrajectoryImportContentDigest(sourceKey, record.contentHash),
302
+ sourceOrderId: record.sourceOrderId,
303
+ componentIndex: record.componentIndex,
304
+ sourceIdentityKind: record.sourceIdentityKind,
305
+ recordType: record.recordType,
306
+ })),
307
+ );
308
+ candidates.sort(
309
+ (left, right) =>
310
+ left.sourceOrderId.localeCompare(right.sourceOrderId) ||
311
+ left.componentIndex - right.componentIndex ||
312
+ left.stableRecordKeyHash.localeCompare(right.stableRecordKeyHash) ||
313
+ left.contentDigest.localeCompare(right.contentDigest) ||
314
+ left.sourceIdentityKind.localeCompare(right.sourceIdentityKind) ||
315
+ left.recordType.localeCompare(right.recordType),
316
+ );
317
+
318
+ const unique = new Map<string, (typeof candidates)[number]>();
319
+ for (const candidate of candidates) {
320
+ const existing = unique.get(candidate.stableRecordKeyHash);
321
+ if (existing === undefined) {
322
+ unique.set(candidate.stableRecordKeyHash, candidate);
323
+ continue;
324
+ }
325
+ if (
326
+ existing.contentDigest !== candidate.contentDigest ||
327
+ existing.sourceIdentityKind !== candidate.sourceIdentityKind ||
328
+ existing.recordType !== candidate.recordType
329
+ ) {
330
+ blockers.add("duplicate-record-identity-conflict");
331
+ }
332
+ }
333
+
334
+ return [...unique.values()].map((record, ordinal) => ({
335
+ schemaVersion: 1,
336
+ kind: "trajectory-import-index-record",
337
+ stableRecordKeyHash: record.stableRecordKeyHash,
338
+ contentDigest: record.contentDigest,
339
+ ordinal,
340
+ sourceIdentityKind: record.sourceIdentityKind,
341
+ recordType: record.recordType,
342
+ }));
343
+ }
344
+
345
+ function compareIndexes(
346
+ current: TrajectoryImportIndexRecordV1[],
347
+ previous: TrajectoryImportIndexRecordV1[],
348
+ blockers: Set<string>,
349
+ ): Pick<TrajectoryImportPreviewV1["records"], "new" | "unchanged" | "changed" | "missing"> {
350
+ const currentByKey = new Map(current.map((record) => [record.stableRecordKeyHash, record]));
351
+ const previousByKey = new Map(previous.map((record) => [record.stableRecordKeyHash, record]));
352
+ let newRecords = 0;
353
+ let unchanged = 0;
354
+ let changed = 0;
355
+
356
+ for (const record of current) {
357
+ const old = previousByKey.get(record.stableRecordKeyHash);
358
+ if (old === undefined) {
359
+ newRecords += 1;
360
+ continue;
361
+ }
362
+ if (old.sourceIdentityKind !== "content" && record.sourceIdentityKind === "content") {
363
+ blockers.add("source-identity-confidence-regressed");
364
+ }
365
+ if (old.contentDigest === record.contentDigest) unchanged += 1;
366
+ else changed += 1;
367
+ }
368
+
369
+ let missing = 0;
370
+ for (const record of previous) {
371
+ if (!currentByKey.has(record.stableRecordKeyHash)) missing += 1;
372
+ }
373
+ if (changed > 0) blockers.add("changed-records");
374
+ if (missing > 0) blockers.add("missing-records");
375
+ return { new: newRecords, unchanged, changed, missing };
376
+ }
377
+
378
+ function summarizeDiagnostics(
379
+ diagnostics: Array<{ code: string }>,
380
+ blockers: Set<string>,
381
+ ): TrajectoryImportPreviewV1["diagnostics"] {
382
+ const counts = new Map<string, number>();
383
+ for (const diagnostic of diagnostics) {
384
+ const code = normalizeDiagnosticCode(diagnostic.code);
385
+ counts.set(code, (counts.get(code) ?? 0) + 1);
386
+ }
387
+ return [...counts.entries()]
388
+ .sort(([left], [right]) => left.localeCompare(right))
389
+ .map(([code, count]) => {
390
+ const severity = diagnosticSeverity(code);
391
+ if (severity === "blocker") blockers.add(`diagnostic:${code}`);
392
+ return { code, severity, count };
393
+ });
394
+ }
395
+
396
+ function diagnosticSeverity(code: string): TrajectoryImportDiagnosticSeverity {
397
+ return DIAGNOSTIC_POLICY[code as keyof typeof DIAGNOSTIC_POLICY] ?? "blocker";
398
+ }
399
+
400
+ function normalizeDiagnosticCode(code: string): string {
401
+ return /^[a-z][a-z0-9_-]{0,79}$/u.test(code)
402
+ ? code
403
+ : `unrecognized-${sha256Hex(code).slice(0, 16)}`;
404
+ }
405
+
406
+ function countRecordTypes(
407
+ records: TrajectoryImportIndexRecordV1[],
408
+ ): Record<CanonicalImportRecordType, number> {
409
+ const counts: Record<CanonicalImportRecordType, number> = {
410
+ meta: 0,
411
+ user: 0,
412
+ reasoning: 0,
413
+ assistant: 0,
414
+ "assistant-tool-call": 0,
415
+ tool: 0,
416
+ };
417
+ for (const record of records) counts[record.recordType] += 1;
418
+ return counts;
419
+ }
420
+
421
+ function countIdentityKinds(
422
+ records: TrajectoryImportIndexRecordV1[],
423
+ ): Record<CanonicalImportSourceIdentityKind, number> {
424
+ const counts: Record<CanonicalImportSourceIdentityKind, number> = {
425
+ native: 0,
426
+ location: 0,
427
+ content: 0,
428
+ synthetic: 0,
429
+ };
430
+ for (const record of records) counts[record.sourceIdentityKind] += 1;
431
+ return counts;
432
+ }
433
+
434
+ function sameGenerationContract(
435
+ left: TrajectoryImportGenerationContractV1,
436
+ right: TrajectoryImportGenerationContractV1,
437
+ ): boolean {
438
+ return (
439
+ left.normalizerVersion === right.normalizerVersion &&
440
+ left.canonicalSchemaVersion === right.canonicalSchemaVersion &&
441
+ left.canonicalConfigHash === right.canonicalConfigHash
442
+ );
443
+ }
444
+
445
+ function normalizeScopeId(value: string | null | undefined, field: string): string | null {
446
+ if (value === null || value === undefined) return null;
447
+ if (
448
+ value.length > MAX_SCOPE_ID_LENGTH ||
449
+ !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/u.test(value) ||
450
+ value === "." ||
451
+ value === ".."
452
+ ) {
453
+ throw new Error(`Invalid trajectory import ${field}.`);
454
+ }
455
+ return value;
456
+ }
457
+
458
+ function containsControlCharacter(value: string): boolean {
459
+ for (const character of value) {
460
+ const code = character.codePointAt(0) ?? 0;
461
+ if (code <= 31 || code === 127) return true;
462
+ }
463
+ return false;
464
+ }
465
+
466
+ function createStateId(prefix: string, parts: string[]): string {
467
+ return `${prefix}-${sha256Hex(parts.join("\0")).slice(0, STATE_ID_HASH_LENGTH)}`;
468
+ }
469
+
470
+ function assertNonEmpty(field: string, value: string): void {
471
+ if (value.trim() === "") throw new Error(`Trajectory import ${field} must not be empty.`);
472
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./types.ts";
2
+ export * from "./diff.ts";
3
+ export * from "./paths.ts";
4
+ export * from "./storage.ts";
5
+ export * from "./stage.ts";
6
+ export * from "./materialize.ts";
7
+ export * from "./apply.ts";