@aiwg/cli 2026.7.21 → 2026.7.23
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/README.md +14 -3
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/cli/handlers/sessions.js +339 -40
- package/dist/src/config/aiwg-config.js +12 -0
- package/dist/src/config/cli.js +16 -3
- package/dist/src/security/threat-assessment-config.js +296 -0
- package/dist/src/sessions/adapters/claude.js +37 -9
- package/dist/src/sessions/adapters/codex.js +38 -11
- package/dist/src/sessions/adapters/cursor.js +166 -10
- package/dist/src/sessions/adapters/factory.js +50 -9
- package/dist/src/sessions/batch-contracts.js +121 -0
- package/dist/src/sessions/batch-import.js +265 -0
- package/dist/src/sessions/contracts.js +32 -5
- package/dist/src/sessions/import-lease.js +152 -0
- package/dist/src/sessions/importer.js +163 -14
- package/dist/src/sessions/index.js +6 -0
- package/dist/src/sessions/origin.js +117 -0
- package/dist/src/sessions/readers.js +1 -1
- package/dist/src/sessions/repository.js +354 -13
- package/dist/src/sessions/timeline.js +148 -0
- package/dist/src/sessions/workspace-discovery.js +319 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { opendir } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, extname, resolve, } from 'node:path';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { SessionContractError, assertSupportedSchemaMajor, } from '../contracts.js';
|
|
4
5
|
import { readBoundedJsonLines, readBoundedText, streamBoundedJsonLines, } from '../readers.js';
|
|
@@ -44,19 +45,52 @@ const CloudEventSchema = z.object({
|
|
|
44
45
|
}).passthrough().refine((value) => value.agent?.id || value.run?.id, {
|
|
45
46
|
message: 'cloud event requires an agent or run identity',
|
|
46
47
|
});
|
|
48
|
+
const AgentTranscriptEventSchema = z.object({
|
|
49
|
+
schemaVersion: z.string().optional(),
|
|
50
|
+
id: z.string().optional(),
|
|
51
|
+
event_id: z.string().optional(),
|
|
52
|
+
request_id: z.string().optional(),
|
|
53
|
+
timestamp: z.string().datetime({ offset: true }).optional(),
|
|
54
|
+
type: z.string().min(1).optional(),
|
|
55
|
+
status: z.string().min(1).optional(),
|
|
56
|
+
role: z.string().min(1).optional(),
|
|
57
|
+
message: z.object({
|
|
58
|
+
id: z.string().optional(),
|
|
59
|
+
role: z.string().optional(),
|
|
60
|
+
content: z.union([z.string(), z.array(z.unknown())]).optional(),
|
|
61
|
+
}).passthrough().optional(),
|
|
62
|
+
}).passthrough().refine((value) => value.role || value.type, {
|
|
63
|
+
message: 'agent transcript event requires role or type',
|
|
64
|
+
});
|
|
47
65
|
export class CursorSessionAdapter {
|
|
48
66
|
limits;
|
|
67
|
+
discoveryLimits;
|
|
49
68
|
provider = 'cursor';
|
|
50
69
|
adapterVersion = CURSOR_ADAPTER_VERSION;
|
|
51
70
|
disposition = 'implemented';
|
|
52
|
-
supportedOperations = ['inspect', 'stream'];
|
|
71
|
+
supportedOperations = ['discover', 'inspect', 'stream'];
|
|
53
72
|
acquisitionModes = ['api', 'jsonl', 'manual-export'];
|
|
54
|
-
constructor(limits) {
|
|
73
|
+
constructor(limits, discoveryLimits = { maxDepth: 8, maxFiles: 10_000 }) {
|
|
55
74
|
this.limits = limits;
|
|
75
|
+
this.discoveryLimits = discoveryLimits;
|
|
56
76
|
}
|
|
57
|
-
async *discover(
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
async *discover(scope) {
|
|
78
|
+
if (scope.allowedRoots.length === 0) {
|
|
79
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'Cursor discovery requires an explicitly authorized agent-transcripts root');
|
|
80
|
+
}
|
|
81
|
+
let emitted = 0;
|
|
82
|
+
for (const root of [...scope.allowedRoots].sort()) {
|
|
83
|
+
for await (const locator of discoverJsonl(resolve(root), this.discoveryLimits.maxDepth)) {
|
|
84
|
+
if (++emitted > this.discoveryLimits.maxFiles) {
|
|
85
|
+
throw new SessionContractError('RESOURCE_LIMIT_EXCEEDED', 'Cursor source discovery exceeded the authorized file limit');
|
|
86
|
+
}
|
|
87
|
+
yield {
|
|
88
|
+
provider: 'cursor',
|
|
89
|
+
locator,
|
|
90
|
+
locatorClass: 'cursor-agent-transcript-jsonl',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
60
94
|
}
|
|
61
95
|
async inspect(source) {
|
|
62
96
|
const parsed = await this.readSource(source);
|
|
@@ -179,9 +213,43 @@ export class CursorSessionAdapter {
|
|
|
179
213
|
records: normalized.records,
|
|
180
214
|
};
|
|
181
215
|
}
|
|
216
|
+
if (source.locatorClass === 'cursor-agent-transcript-jsonl') {
|
|
217
|
+
const normalized = normalizeAgentTranscript(input.records, source.locator);
|
|
218
|
+
return {
|
|
219
|
+
schemaVersion,
|
|
220
|
+
consistency: normalized.complete && !input.incompleteTail ? 'complete' : 'provisional',
|
|
221
|
+
records: normalized.records,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
182
224
|
throw new SessionContractError('UNSUPPORTED_OPERATION', 'unsupported Cursor source class');
|
|
183
225
|
}
|
|
184
226
|
}
|
|
227
|
+
async function* discoverJsonl(root, remainingDepth) {
|
|
228
|
+
if (remainingDepth < 0)
|
|
229
|
+
return;
|
|
230
|
+
let directory;
|
|
231
|
+
try {
|
|
232
|
+
directory = await opendir(root);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const entries = [];
|
|
238
|
+
for await (const entry of directory)
|
|
239
|
+
entries.push(entry);
|
|
240
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
const path = resolve(root, entry.name);
|
|
243
|
+
if (entry.isSymbolicLink())
|
|
244
|
+
continue;
|
|
245
|
+
if (entry.isDirectory()) {
|
|
246
|
+
yield* discoverJsonl(path, remainingDepth - 1);
|
|
247
|
+
}
|
|
248
|
+
else if (entry.isFile() && extname(entry.name) === '.jsonl') {
|
|
249
|
+
yield path;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
185
253
|
function normalizeCli(input) {
|
|
186
254
|
const records = [];
|
|
187
255
|
let sessionId;
|
|
@@ -196,7 +264,10 @@ function normalizeCli(input) {
|
|
|
196
264
|
throw new SessionContractError('DUPLICATE_NATIVE_ID', 'Cursor CLI stream changes session identity');
|
|
197
265
|
}
|
|
198
266
|
sessionId = event.session_id;
|
|
199
|
-
|
|
267
|
+
const terminal = event.type === 'result'
|
|
268
|
+
&& event.subtype === 'success'
|
|
269
|
+
&& event.is_error !== true;
|
|
270
|
+
complete ||= terminal;
|
|
200
271
|
const text = event.type === 'result'
|
|
201
272
|
? (event.result ?? '')
|
|
202
273
|
: (event.message?.content ?? []).flatMap((part) => part.text ?? []).join('');
|
|
@@ -220,7 +291,7 @@ function normalizeCli(input) {
|
|
|
220
291
|
permissionMode: event.permissionMode,
|
|
221
292
|
productVersion: event.cliVersion ?? event.version ?? 'not-reported',
|
|
222
293
|
toolCall: event.tool_call,
|
|
223
|
-
lifecycle:
|
|
294
|
+
...(terminal ? { lifecycle: 'complete' } : {}),
|
|
224
295
|
provenance: { acquisition: 'cursor-cli-stream-json', schema: declaredEventVersion(event) },
|
|
225
296
|
unknownFields: unknownFields(event, CLI_KEYS),
|
|
226
297
|
},
|
|
@@ -250,6 +321,7 @@ function normalizeCloud(input) {
|
|
|
250
321
|
agentId = nextAgentId;
|
|
251
322
|
runId = nextRunId;
|
|
252
323
|
const status = event.run?.status ?? event.agent?.status;
|
|
324
|
+
const lifecycle = cloudLifecycle(event.type, status);
|
|
253
325
|
complete ||= isTerminal(status) || event.type === 'agent.deleted' || event.type === 'agent.archived';
|
|
254
326
|
const sessionId = runId ? `${agentId ?? 'agent'}:${runId}` : agentId;
|
|
255
327
|
records.push({
|
|
@@ -258,13 +330,28 @@ function normalizeCloud(input) {
|
|
|
258
330
|
sequence: line.sequence,
|
|
259
331
|
kind: `cursor.cloud.${event.type}`,
|
|
260
332
|
role: 'system',
|
|
333
|
+
activityBoundary: event.type === 'agent.unarchived'
|
|
334
|
+
? 'resume'
|
|
335
|
+
: event.type === 'agent.archived' || event.type === 'agent.deleted'
|
|
336
|
+
? 'end'
|
|
337
|
+
: undefined,
|
|
338
|
+
activityBoundaryBasis: event.type === 'agent.unarchived'
|
|
339
|
+
|| event.type === 'agent.archived'
|
|
340
|
+
|| event.type === 'agent.deleted'
|
|
341
|
+
? `cursor-cloud:${event.type}`
|
|
342
|
+
: undefined,
|
|
343
|
+
activityBoundaryConfidence: event.type === 'agent.unarchived'
|
|
344
|
+
|| event.type === 'agent.archived'
|
|
345
|
+
|| event.type === 'agent.deleted'
|
|
346
|
+
? 'high'
|
|
347
|
+
: undefined,
|
|
261
348
|
text: extractCloudText(event.data),
|
|
262
349
|
rawReference: { locatorClass: 'cursor-cloud-events-jsonl', offset: line.byteOffset },
|
|
263
350
|
extensions: {
|
|
264
351
|
agent: event.agent,
|
|
265
352
|
run: event.run,
|
|
266
353
|
status,
|
|
267
|
-
lifecycle:
|
|
354
|
+
...(lifecycle ? { lifecycle } : {}),
|
|
268
355
|
reconnect: {
|
|
269
356
|
eventId: event.event_id ?? event.id,
|
|
270
357
|
supported: true,
|
|
@@ -277,6 +364,49 @@ function normalizeCloud(input) {
|
|
|
277
364
|
}
|
|
278
365
|
return { records, complete };
|
|
279
366
|
}
|
|
367
|
+
function normalizeAgentTranscript(input, locator) {
|
|
368
|
+
const nativeSessionId = agentTranscriptSessionId(locator);
|
|
369
|
+
const records = [];
|
|
370
|
+
let complete = false;
|
|
371
|
+
for (const line of input) {
|
|
372
|
+
const parsed = AgentTranscriptEventSchema.safeParse(line.value);
|
|
373
|
+
if (!parsed.success) {
|
|
374
|
+
throw new SessionContractError('MALFORMED_SOURCE', 'Cursor agent transcript event is malformed');
|
|
375
|
+
}
|
|
376
|
+
const event = parsed.data;
|
|
377
|
+
const terminal = event.type === 'turn_ended' && event.status === 'success';
|
|
378
|
+
complete ||= terminal;
|
|
379
|
+
const messageRole = event.message?.role ?? event.role ?? 'system';
|
|
380
|
+
records.push({
|
|
381
|
+
nativeSessionId,
|
|
382
|
+
nativeEventId: event.event_id ?? event.id ?? event.request_id ?? `${messageRole}:${line.sequence}`,
|
|
383
|
+
sequence: line.sequence,
|
|
384
|
+
kind: event.type ? `cursor.agent.${event.type}` : 'message',
|
|
385
|
+
role: messageRole,
|
|
386
|
+
participant: messageRole,
|
|
387
|
+
occurredAt: event.timestamp,
|
|
388
|
+
activityBoundary: event.type === 'turn_ended' ? 'end' : undefined,
|
|
389
|
+
activityBoundaryBasis: event.type === 'turn_ended'
|
|
390
|
+
? `cursor-agent:${event.status ?? 'unknown'}`
|
|
391
|
+
: undefined,
|
|
392
|
+
activityBoundaryConfidence: event.type === 'turn_ended' ? 'high' : undefined,
|
|
393
|
+
text: messageText(event.message?.content),
|
|
394
|
+
rawReference: { locatorClass: 'cursor-agent-transcript-jsonl', offset: line.byteOffset },
|
|
395
|
+
extensions: {
|
|
396
|
+
transcriptRole: event.role,
|
|
397
|
+
status: event.status,
|
|
398
|
+
...(terminal ? { lifecycle: 'complete' } : {}),
|
|
399
|
+
provenance: {
|
|
400
|
+
acquisition: 'cursor-agent-transcript-jsonl',
|
|
401
|
+
schema: declaredEventVersion(event),
|
|
402
|
+
nativeSessionIdDerivedFromPath: true,
|
|
403
|
+
},
|
|
404
|
+
unknownFields: unknownFields(event, AGENT_TRANSCRIPT_KEYS),
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return { records, complete };
|
|
409
|
+
}
|
|
280
410
|
function normalizeMarkdown(value, locator) {
|
|
281
411
|
const heading = /^#{1,3}\s+(User|Assistant|Cursor)\s*$/gim;
|
|
282
412
|
const matches = [...value.matchAll(heading)];
|
|
@@ -312,6 +442,26 @@ function normalizeMarkdown(value, locator) {
|
|
|
312
442
|
};
|
|
313
443
|
});
|
|
314
444
|
}
|
|
445
|
+
function agentTranscriptSessionId(locator) {
|
|
446
|
+
const fileIdentity = basename(locator, extname(locator));
|
|
447
|
+
const directoryIdentity = basename(dirname(locator));
|
|
448
|
+
return directoryIdentity && directoryIdentity === fileIdentity
|
|
449
|
+
? directoryIdentity : fileIdentity;
|
|
450
|
+
}
|
|
451
|
+
function messageText(value) {
|
|
452
|
+
if (typeof value === 'string')
|
|
453
|
+
return value;
|
|
454
|
+
if (!Array.isArray(value))
|
|
455
|
+
return '';
|
|
456
|
+
return value.map((item) => {
|
|
457
|
+
const block = asObject(item);
|
|
458
|
+
if (typeof block.text === 'string')
|
|
459
|
+
return block.text;
|
|
460
|
+
if (typeof block.content === 'string')
|
|
461
|
+
return block.content;
|
|
462
|
+
return '';
|
|
463
|
+
}).filter(Boolean).join('\n');
|
|
464
|
+
}
|
|
315
465
|
function declaredVersion(records) {
|
|
316
466
|
const versions = new Set(records.map((line) => {
|
|
317
467
|
const value = asObject(line.value);
|
|
@@ -342,7 +492,9 @@ function cloudLifecycle(type, status) {
|
|
|
342
492
|
return 'active';
|
|
343
493
|
if (status === 'cancelled' || status === 'canceled')
|
|
344
494
|
return 'cancelled';
|
|
345
|
-
|
|
495
|
+
if (status === 'active')
|
|
496
|
+
return 'active';
|
|
497
|
+
return isTerminal(status) ? 'complete' : undefined;
|
|
346
498
|
}
|
|
347
499
|
function isTerminal(status) {
|
|
348
500
|
return ['completed', 'failed', 'cancelled', 'canceled'].includes(status ?? '');
|
|
@@ -369,4 +521,8 @@ const CLI_KEYS = new Set([
|
|
|
369
521
|
'duration_ms', 'duration_api_ms',
|
|
370
522
|
]);
|
|
371
523
|
const CLOUD_KEYS = new Set(['schemaVersion', 'id', 'event_id', 'type', 'agent', 'run', 'data']);
|
|
524
|
+
const AGENT_TRANSCRIPT_KEYS = new Set([
|
|
525
|
+
'schemaVersion', 'id', 'event_id', 'request_id', 'timestamp', 'type', 'status',
|
|
526
|
+
'role', 'message',
|
|
527
|
+
]);
|
|
372
528
|
//# sourceMappingURL=cursor.js.map
|
|
@@ -18,7 +18,7 @@ const FactoryRecordSchema = z.object({
|
|
|
18
18
|
createdAt: z.number().optional(),
|
|
19
19
|
updatedAt: z.number().optional(),
|
|
20
20
|
cwd: z.string().optional(),
|
|
21
|
-
version: z.string().optional(),
|
|
21
|
+
version: z.union([z.string(), z.number()]).optional(),
|
|
22
22
|
message: z.object({
|
|
23
23
|
id: z.string().optional(),
|
|
24
24
|
role: z.string().optional(),
|
|
@@ -80,7 +80,7 @@ export class FactorySessionAdapter {
|
|
|
80
80
|
sawRecord = true;
|
|
81
81
|
const parsed = FactoryRecordSchema.safeParse(line.value);
|
|
82
82
|
if (!parsed.success) {
|
|
83
|
-
throw
|
|
83
|
+
throw factoryMalformedRecord(source, line, parsed.error);
|
|
84
84
|
}
|
|
85
85
|
const value = parsed.data;
|
|
86
86
|
const currentSchema = value.schemaVersion ?? FACTORY_SOURCE_SCHEMA_VERSION;
|
|
@@ -90,12 +90,12 @@ export class FactorySessionAdapter {
|
|
|
90
90
|
schemaVersion = currentSchema;
|
|
91
91
|
assertSupportedSchemaMajor(schemaVersion);
|
|
92
92
|
const filenameId = basename(source.locator, extname(source.locator));
|
|
93
|
-
const currentId = value
|
|
93
|
+
const currentId = factoryNativeSessionId(value, establishedId, filenameId);
|
|
94
94
|
if (establishedId && establishedId !== currentId) {
|
|
95
95
|
throw new SessionContractError('DUPLICATE_NATIVE_ID', 'Factory source changes session identity');
|
|
96
96
|
}
|
|
97
97
|
establishedId = currentId;
|
|
98
|
-
for (const record of normalize([line], source).records) {
|
|
98
|
+
for (const record of normalize([line], source, establishedId).records) {
|
|
99
99
|
if (outputIndex++ >= start)
|
|
100
100
|
yield record;
|
|
101
101
|
}
|
|
@@ -162,24 +162,28 @@ export class FactorySessionAdapter {
|
|
|
162
162
|
}));
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
-
function normalize(input, source) {
|
|
165
|
+
function normalize(input, source, initialSessionId) {
|
|
166
166
|
const output = [];
|
|
167
167
|
const filenameId = basename(source.locator, extname(source.locator));
|
|
168
168
|
let complete = false;
|
|
169
|
-
let establishedId;
|
|
169
|
+
let establishedId = initialSessionId;
|
|
170
170
|
for (const line of input) {
|
|
171
171
|
const parsed = FactoryRecordSchema.safeParse(line.value);
|
|
172
172
|
if (!parsed.success) {
|
|
173
|
-
throw
|
|
173
|
+
throw factoryMalformedRecord(source, line, parsed.error);
|
|
174
174
|
}
|
|
175
175
|
const value = parsed.data;
|
|
176
|
-
const nativeSessionId = value
|
|
176
|
+
const nativeSessionId = factoryNativeSessionId(value, establishedId, filenameId);
|
|
177
177
|
if (establishedId && nativeSessionId !== establishedId) {
|
|
178
178
|
throw new SessionContractError('DUPLICATE_NATIVE_ID', 'Factory source changes session identity');
|
|
179
179
|
}
|
|
180
180
|
establishedId = nativeSessionId;
|
|
181
181
|
complete ||= ['session_end', 'result'].includes(value.type)
|
|
182
182
|
|| ['completed', 'archived', 'deleted'].includes(value.status ?? '');
|
|
183
|
+
const explicitLifecycle = ['session_end', 'result'].includes(value.type)
|
|
184
|
+
|| ['completed', 'archived', 'deleted'].includes(value.status ?? '')
|
|
185
|
+
? lifecycle(value.status)
|
|
186
|
+
: undefined;
|
|
183
187
|
const blocks = contentBlocks(value);
|
|
184
188
|
if (blocks.length === 0)
|
|
185
189
|
blocks.push({
|
|
@@ -199,13 +203,30 @@ function normalize(input, source) {
|
|
|
199
203
|
kind: block.kind,
|
|
200
204
|
role: value.message?.role,
|
|
201
205
|
occurredAt: timestamp(value.timestamp ?? value.updatedAt ?? value.createdAt),
|
|
206
|
+
activityBoundary: value.type === 'session_end'
|
|
207
|
+
? 'end'
|
|
208
|
+
: value.type === 'session_start' && value.subtype === 'resume'
|
|
209
|
+
? 'resume'
|
|
210
|
+
: value.type === 'session_start' && value.subtype === 'continuation'
|
|
211
|
+
? 'continuation'
|
|
212
|
+
: undefined,
|
|
213
|
+
activityBoundaryBasis: value.type === 'session_end'
|
|
214
|
+
|| (value.type === 'session_start'
|
|
215
|
+
&& (value.subtype === 'resume' || value.subtype === 'continuation'))
|
|
216
|
+
? `factory:${value.type}:${value.subtype ?? 'none'}`
|
|
217
|
+
: undefined,
|
|
218
|
+
activityBoundaryConfidence: value.type === 'session_end'
|
|
219
|
+
|| (value.type === 'session_start'
|
|
220
|
+
&& (value.subtype === 'resume' || value.subtype === 'continuation'))
|
|
221
|
+
? 'high'
|
|
222
|
+
: undefined,
|
|
202
223
|
text: block.text,
|
|
203
224
|
rawReference: { locatorClass: source.locatorClass, offset: line.byteOffset },
|
|
204
225
|
extensions: {
|
|
205
226
|
subtype: value.subtype,
|
|
206
227
|
parentUuid: value.parentUuid,
|
|
207
228
|
productVersion: value.version ?? 'not-reported',
|
|
208
|
-
lifecycle:
|
|
229
|
+
...(explicitLifecycle ? { lifecycle: explicitLifecycle } : {}),
|
|
209
230
|
workspace: { cwdClass: value.cwd ? '<workspace>' : undefined },
|
|
210
231
|
settings: value.sessionSettings ?? value.settings,
|
|
211
232
|
opaque: block.opaque,
|
|
@@ -263,6 +284,15 @@ function contentBlocks(value) {
|
|
|
263
284
|
return { kind: `factory.${type}`, text: '', opaque: true, nativeId, unknownFields: { ...block } };
|
|
264
285
|
});
|
|
265
286
|
}
|
|
287
|
+
function factoryNativeSessionId(value, establishedId, filenameId) {
|
|
288
|
+
if (value.sessionId)
|
|
289
|
+
return value.sessionId;
|
|
290
|
+
if (value.session_id)
|
|
291
|
+
return value.session_id;
|
|
292
|
+
if (value.type === 'session_start' && value.id)
|
|
293
|
+
return value.id;
|
|
294
|
+
return establishedId ?? filenameId;
|
|
295
|
+
}
|
|
266
296
|
async function* discoverJsonl(root, maxDepth) {
|
|
267
297
|
const pending = [{ path: root, depth: 0 }];
|
|
268
298
|
while (pending.length) {
|
|
@@ -291,6 +321,17 @@ async function* discoverJsonl(root, maxDepth) {
|
|
|
291
321
|
pending.push({ path, depth: current.depth + 1 });
|
|
292
322
|
}
|
|
293
323
|
}
|
|
324
|
+
function factoryMalformedRecord(source, record, error) {
|
|
325
|
+
const value = asObject(record.value);
|
|
326
|
+
const rawType = typeof value.type === 'string' ? value.type : '';
|
|
327
|
+
const type = /^[a-zA-Z0-9_.:-]{1,64}$/.test(rawType) ? rawType : '<missing-or-invalid>';
|
|
328
|
+
const issue = error.issues[0];
|
|
329
|
+
const requirement = issue?.path.length
|
|
330
|
+
? issue.path.map(String).join('.')
|
|
331
|
+
: 'record';
|
|
332
|
+
return new SessionContractError('MALFORMED_SOURCE', `Factory source ${basename(source.locator)} record ${record.sequence + 1} `
|
|
333
|
+
+ `type ${type} failed requirement ${requirement} (${issue?.code ?? 'invalid_type'})`);
|
|
334
|
+
}
|
|
294
335
|
function declaredVersion(records) {
|
|
295
336
|
const versions = new Set(records.flatMap((record) => {
|
|
296
337
|
const value = asObject(record.value);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export const BATCH_IMPORT_VERSION = '1.0.0';
|
|
2
|
+
export const COVERAGE_VERSION = '1.0.0';
|
|
3
|
+
export function coverageFromBatchRun(run, now = new Date(), staleAfterMs = 24 * 60 * 60 * 1_000) {
|
|
4
|
+
if (!run)
|
|
5
|
+
return unknownCoverage();
|
|
6
|
+
const coveredStatuses = new Set([
|
|
7
|
+
'committed', 'duplicate', 'previously-committed',
|
|
8
|
+
]);
|
|
9
|
+
const accepted = run.sources.filter((source) => source.status === 'committed').length;
|
|
10
|
+
const rejected = run.sources.filter((source) => source.status === 'rejected').length;
|
|
11
|
+
const skipped = run.sources.filter((source) => source.status === 'skipped').length;
|
|
12
|
+
const duplicated = run.sources.filter((source) => source.status === 'duplicate').length;
|
|
13
|
+
const previouslyCommitted = run.sources.filter((source) => source.status === 'previously-committed').length;
|
|
14
|
+
const pending = run.sources.filter((source) => source.status === 'pending' || source.status === 'running').length;
|
|
15
|
+
const checked = providerNames(run, 'checked');
|
|
16
|
+
const unavailable = providerNames(run, 'unavailable');
|
|
17
|
+
const exportRequired = providerNames(run, 'export-required');
|
|
18
|
+
const notChecked = providerNames(run, 'not-checked');
|
|
19
|
+
const manifestAgeMs = Math.max(0, now.getTime() - Date.parse(run.manifestCreatedAt));
|
|
20
|
+
const isStale = manifestAgeMs > staleAfterMs;
|
|
21
|
+
const incomplete = rejected > 0 || pending > 0 || skipped > 0
|
|
22
|
+
|| exportRequired.length > 0 || notChecked.length > 0;
|
|
23
|
+
const rejectionCounts = {};
|
|
24
|
+
for (const source of run.sources) {
|
|
25
|
+
if (source.status !== 'rejected')
|
|
26
|
+
continue;
|
|
27
|
+
const code = source.errorCode ?? 'UNKNOWN_REJECTION';
|
|
28
|
+
rejectionCounts[code] = (rejectionCounts[code] ?? 0) + 1;
|
|
29
|
+
}
|
|
30
|
+
const sourceTimestamps = run.providers.flatMap((provider) => [
|
|
31
|
+
provider.dateRange.earliest,
|
|
32
|
+
provider.dateRange.latest,
|
|
33
|
+
]).filter((value) => value !== null).sort();
|
|
34
|
+
const importedTimestamps = run.sources
|
|
35
|
+
.filter((source) => coveredStatuses.has(source.status))
|
|
36
|
+
.map((source) => source.updatedAt)
|
|
37
|
+
.sort();
|
|
38
|
+
const remediation = [
|
|
39
|
+
...(rejected > 0
|
|
40
|
+
? ['Run `aiwg sessions import-discovered --resume --confirm` after correcting rejected sources.']
|
|
41
|
+
: []),
|
|
42
|
+
...(exportRequired.length > 0
|
|
43
|
+
? ['Export and explicitly authorize providers marked export-required, then run discovery again.']
|
|
44
|
+
: []),
|
|
45
|
+
...(isStale ? ['Run `aiwg sessions discover` to refresh the stale manifest.'] : []),
|
|
46
|
+
];
|
|
47
|
+
return {
|
|
48
|
+
schemaVersion: COVERAGE_VERSION,
|
|
49
|
+
status: isStale ? 'stale' : incomplete ? 'partial' : 'complete',
|
|
50
|
+
workspaceId: run.workspaceId,
|
|
51
|
+
manifestId: run.manifestId,
|
|
52
|
+
batchRunId: run.runId,
|
|
53
|
+
manifestCreatedAt: run.manifestCreatedAt,
|
|
54
|
+
manifestAgeMs,
|
|
55
|
+
providers: { checked, unavailable, exportRequired, notChecked },
|
|
56
|
+
sources: {
|
|
57
|
+
discovered: run.sources.length,
|
|
58
|
+
accepted,
|
|
59
|
+
rejected,
|
|
60
|
+
skipped,
|
|
61
|
+
duplicated,
|
|
62
|
+
previouslyCommitted,
|
|
63
|
+
pending,
|
|
64
|
+
},
|
|
65
|
+
sessionsAccepted: run.sources.reduce((sum, source) => sum + source.sessionsAccepted, 0),
|
|
66
|
+
eventsAccepted: run.sources.reduce((sum, source) => sum + source.eventsAccepted, 0),
|
|
67
|
+
coverageRatio: run.sources.length === 0
|
|
68
|
+
? null
|
|
69
|
+
: (accepted + duplicated + previouslyCommitted) / run.sources.length,
|
|
70
|
+
rejectionCounts,
|
|
71
|
+
sourceDateRange: {
|
|
72
|
+
earliest: sourceTimestamps.at(0) ?? null,
|
|
73
|
+
latest: sourceTimestamps.at(-1) ?? null,
|
|
74
|
+
},
|
|
75
|
+
importedDateRange: {
|
|
76
|
+
earliest: importedTimestamps.at(0) ?? null,
|
|
77
|
+
latest: importedTimestamps.at(-1) ?? null,
|
|
78
|
+
},
|
|
79
|
+
remediation,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function providerNames(run, status) {
|
|
83
|
+
return run.providers
|
|
84
|
+
.filter((provider) => provider.status === status)
|
|
85
|
+
.map((provider) => provider.provider)
|
|
86
|
+
.sort();
|
|
87
|
+
}
|
|
88
|
+
function unknownCoverage() {
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: COVERAGE_VERSION,
|
|
91
|
+
status: 'unknown',
|
|
92
|
+
workspaceId: '',
|
|
93
|
+
manifestId: null,
|
|
94
|
+
batchRunId: null,
|
|
95
|
+
manifestCreatedAt: null,
|
|
96
|
+
manifestAgeMs: null,
|
|
97
|
+
providers: {
|
|
98
|
+
checked: [],
|
|
99
|
+
unavailable: [],
|
|
100
|
+
exportRequired: [],
|
|
101
|
+
notChecked: [],
|
|
102
|
+
},
|
|
103
|
+
sources: {
|
|
104
|
+
discovered: 0,
|
|
105
|
+
accepted: 0,
|
|
106
|
+
rejected: 0,
|
|
107
|
+
skipped: 0,
|
|
108
|
+
duplicated: 0,
|
|
109
|
+
previouslyCommitted: 0,
|
|
110
|
+
pending: 0,
|
|
111
|
+
},
|
|
112
|
+
sessionsAccepted: 0,
|
|
113
|
+
eventsAccepted: 0,
|
|
114
|
+
coverageRatio: null,
|
|
115
|
+
rejectionCounts: {},
|
|
116
|
+
sourceDateRange: { earliest: null, latest: null },
|
|
117
|
+
importedDateRange: { earliest: null, latest: null },
|
|
118
|
+
remediation: ['Run `aiwg sessions discover --workspace <path>` to establish coverage.'],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=batch-contracts.js.map
|