@unpolarize/code-sessions-schema 0.1.0

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,120 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { SCHEMA_VERSIONS } from './schemas';
3
+ import {
4
+ insightsJsonSchema,
5
+ parseInsights,
6
+ parseSession,
7
+ parseTurn,
8
+ safeParseTurn,
9
+ sessionJsonSchema,
10
+ turnJsonSchema,
11
+ } from './validators';
12
+
13
+ describe('TurnSchema', () => {
14
+ it('applies defaults for optional fields', () => {
15
+ const turn = parseTurn({
16
+ schema: SCHEMA_VERSIONS.turn,
17
+ session_id: 's',
18
+ host: 'h',
19
+ agent: 'claude-code',
20
+ turn_index: 0,
21
+ ts: '2026-06-20T00:00:00Z',
22
+ role: 'assistant',
23
+ });
24
+ expect(turn.text).toBe('');
25
+ expect(turn.tool_calls).toEqual([]);
26
+ expect(turn.usage).toEqual({
27
+ input_tokens: 0,
28
+ output_tokens: 0,
29
+ cache_read_tokens: 0,
30
+ cache_write_tokens: 0,
31
+ });
32
+ expect(turn.scrubbed).toBe(false);
33
+ expect(turn.raw_ref).toBeNull();
34
+ });
35
+
36
+ it('rejects unknown top-level keys (strict)', () => {
37
+ const res = safeParseTurn({
38
+ schema: SCHEMA_VERSIONS.turn,
39
+ session_id: 's',
40
+ host: 'h',
41
+ agent: 'claude-code',
42
+ turn_index: 0,
43
+ ts: 't',
44
+ role: 'user',
45
+ bogus: true,
46
+ });
47
+ expect(res.success).toBe(false);
48
+ });
49
+
50
+ it('rejects an invalid agent', () => {
51
+ expect(
52
+ safeParseTurn({
53
+ schema: SCHEMA_VERSIONS.turn,
54
+ session_id: 's',
55
+ host: 'h',
56
+ agent: 'not-an-agent',
57
+ turn_index: 0,
58
+ ts: 't',
59
+ role: 'user',
60
+ }).success,
61
+ ).toBe(false);
62
+ });
63
+
64
+ it('round-trips through JSON', () => {
65
+ const turn = parseTurn({
66
+ schema: SCHEMA_VERSIONS.turn,
67
+ session_id: 's',
68
+ host: 'h',
69
+ agent: 'claude-code',
70
+ turn_index: 3,
71
+ ts: 't',
72
+ role: 'assistant',
73
+ text: 'hi',
74
+ tool_calls: [{ name: 'Edit', input: { file_path: 'x' }, id: 'a' }],
75
+ });
76
+ const restored = parseTurn(JSON.parse(JSON.stringify(turn)));
77
+ expect(restored).toEqual(turn);
78
+ });
79
+ });
80
+
81
+ describe('SessionSchema', () => {
82
+ it('parses a minimal envelope with defaults', () => {
83
+ const s = parseSession({
84
+ schema: SCHEMA_VERSIONS.session,
85
+ session_id: 's',
86
+ host: 'h',
87
+ agent: 'claude-code',
88
+ native_ref: { format: 'claude-jsonl', uuid: 's' },
89
+ });
90
+ expect(s.turn_count).toBe(0);
91
+ expect(s.labels).toEqual([]);
92
+ expect(s.totals.cost_usd).toBe(0);
93
+ });
94
+ });
95
+
96
+ describe('InsightsSchema', () => {
97
+ it('parses insights with signals', () => {
98
+ const i = parseInsights({
99
+ schema: SCHEMA_VERSIONS.insights,
100
+ session_id: 's',
101
+ host: 'h',
102
+ generated_at: 't',
103
+ provider: 'fake',
104
+ topic: 'debugging',
105
+ tags: ['bug', 'foo.ts'],
106
+ signals: [{ kind: 'error-recovery', severity: 'warn', turn_index: 2 }],
107
+ });
108
+ expect(i.signals[0]!.kind).toBe('error-recovery');
109
+ expect(i.tags).toContain('bug');
110
+ });
111
+ });
112
+
113
+ describe('JSON Schema exports', () => {
114
+ it('exports object schemas for external consumers', () => {
115
+ for (const s of [turnJsonSchema, sessionJsonSchema, insightsJsonSchema]) {
116
+ expect(typeof s).toBe('object');
117
+ expect(s).not.toBeNull();
118
+ }
119
+ });
120
+ });
package/src/schemas.ts ADDED
@@ -0,0 +1,148 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Canonical, agent-neutral session record schemas.
5
+ *
6
+ * Every record carries a versioned `schema` tag so consumers can migrate the
7
+ * way the SQLite `user_version` pattern does. Native records are adapted INTO
8
+ * these shapes while the verbatim `raw` event is preserved for lossless resume.
9
+ */
10
+
11
+ export const AGENTS = ['claude-code', 'codex', 'grok', 'unknown'] as const;
12
+ export const ROLES = ['user', 'assistant', 'tool', 'system'] as const;
13
+
14
+ export const UsageSchema = z
15
+ .object({
16
+ input_tokens: z.number().int().nonnegative().default(0),
17
+ output_tokens: z.number().int().nonnegative().default(0),
18
+ cache_read_tokens: z.number().int().nonnegative().default(0),
19
+ cache_write_tokens: z.number().int().nonnegative().default(0),
20
+ })
21
+ .strict();
22
+
23
+ export const ToolCallSchema = z
24
+ .object({
25
+ name: z.string(),
26
+ input: z.unknown().optional(),
27
+ id: z.string().optional(),
28
+ })
29
+ .strict();
30
+
31
+ export const TelemetrySchema = z
32
+ .object({
33
+ latency_ms: z.number().nonnegative().optional(),
34
+ cost_usd: z.number().nonnegative().optional(),
35
+ })
36
+ .strict();
37
+
38
+ /** Immutable, write-once per-turn record: turns/NNNNNN.json */
39
+ export const TurnSchema = z
40
+ .object({
41
+ schema: z.literal('session-store/turn@1'),
42
+ session_id: z.string().min(1),
43
+ host: z.string().min(1),
44
+ agent: z.enum(AGENTS),
45
+ turn_index: z.number().int().nonnegative(),
46
+ ts: z.string().min(1),
47
+ role: z.enum(ROLES),
48
+ text: z.string().default(''),
49
+ tool_calls: z.array(ToolCallSchema).default([]),
50
+ usage: UsageSchema.default({}),
51
+ telemetry: TelemetrySchema.optional(),
52
+ /** true when secret-scrubbing redacted content in this turn */
53
+ scrubbed: z.boolean().default(false),
54
+ /** sha256 pointer when a large tool output was externalized to raw/ */
55
+ raw_ref: z.string().nullable().default(null),
56
+ /** verbatim native event for lossless tier-1 resume */
57
+ raw: z.unknown().optional(),
58
+ })
59
+ .strict();
60
+
61
+ export const TotalsSchema = z
62
+ .object({
63
+ input_tokens: z.number().int().nonnegative().default(0),
64
+ output_tokens: z.number().int().nonnegative().default(0),
65
+ cost_usd: z.number().nonnegative().default(0),
66
+ })
67
+ .strict();
68
+
69
+ export const NativeRefSchema = z
70
+ .object({
71
+ format: z.string(),
72
+ uuid: z.string(),
73
+ })
74
+ .strict();
75
+
76
+ /** Derived, rebuildable aggregate: session.json */
77
+ export const SessionSchema = z
78
+ .object({
79
+ schema: z.literal('session-store/session@1'),
80
+ session_id: z.string().min(1),
81
+ host: z.string().min(1),
82
+ agent: z.enum(AGENTS),
83
+ project_path: z.string().default(''),
84
+ git_branch: z.string().optional(),
85
+ model: z.string().optional(),
86
+ started_at: z.string().optional(),
87
+ ended_at: z.string().optional(),
88
+ turn_count: z.number().int().nonnegative().default(0),
89
+ tool_call_count: z.number().int().nonnegative().default(0),
90
+ totals: TotalsSchema.default({}),
91
+ title: z.string().optional(),
92
+ labels: z.array(z.string()).default([]),
93
+ native_ref: NativeRefSchema,
94
+ })
95
+ .strict();
96
+
97
+ export const SIGNAL_KINDS = [
98
+ 'stuck-loop',
99
+ 'error-recovery',
100
+ 'high-cost-turn',
101
+ 'long-session',
102
+ 'affect-negative',
103
+ 'affect-positive',
104
+ 'tool-heavy',
105
+ 'other',
106
+ ] as const;
107
+
108
+ export const SignalSchema = z
109
+ .object({
110
+ kind: z.enum(SIGNAL_KINDS),
111
+ severity: z.enum(['info', 'warn', 'critical']).default('info'),
112
+ turn_index: z.number().int().nonnegative().optional(),
113
+ note: z.string().optional(),
114
+ })
115
+ .strict();
116
+
117
+ /** Derived insights: insights/labels.json (MVP-1.1) */
118
+ export const InsightsSchema = z
119
+ .object({
120
+ schema: z.literal('session-store/insights@1'),
121
+ session_id: z.string().min(1),
122
+ host: z.string().min(1),
123
+ generated_at: z.string(),
124
+ provider: z.string(),
125
+ topic: z.string().optional(),
126
+ tags: z.array(z.string()).default([]),
127
+ signals: z.array(SignalSchema).default([]),
128
+ summary: z.string().optional(),
129
+ })
130
+ .strict();
131
+
132
+ export type Usage = z.infer<typeof UsageSchema>;
133
+ export type ToolCall = z.infer<typeof ToolCallSchema>;
134
+ export type Telemetry = z.infer<typeof TelemetrySchema>;
135
+ export type Turn = z.infer<typeof TurnSchema>;
136
+ export type Totals = z.infer<typeof TotalsSchema>;
137
+ export type SessionEnvelope = z.infer<typeof SessionSchema>;
138
+ export type Signal = z.infer<typeof SignalSchema>;
139
+ export type Insights = z.infer<typeof InsightsSchema>;
140
+ export type AgentKind = (typeof AGENTS)[number];
141
+ export type Role = (typeof ROLES)[number];
142
+ export type SignalKind = (typeof SIGNAL_KINDS)[number];
143
+
144
+ export const SCHEMA_VERSIONS = {
145
+ turn: 'session-store/turn@1',
146
+ session: 'session-store/session@1',
147
+ insights: 'session-store/insights@1',
148
+ } as const;
@@ -0,0 +1,17 @@
1
+ import { zodToJsonSchema } from 'zod-to-json-schema';
2
+ import { InsightsSchema, SessionSchema, TurnSchema } from './schemas';
3
+
4
+ /** JSON Schema (draft-07) representations for external (non-TS) consumers. */
5
+ export const turnJsonSchema = zodToJsonSchema(TurnSchema, 'Turn');
6
+ export const sessionJsonSchema = zodToJsonSchema(SessionSchema, 'Session');
7
+ export const insightsJsonSchema = zodToJsonSchema(InsightsSchema, 'Insights');
8
+
9
+ /** Parse + validate (throws on invalid). Applies schema defaults. */
10
+ export const parseTurn = (data: unknown) => TurnSchema.parse(data);
11
+ export const parseSession = (data: unknown) => SessionSchema.parse(data);
12
+ export const parseInsights = (data: unknown) => InsightsSchema.parse(data);
13
+
14
+ /** Non-throwing validation. */
15
+ export const safeParseTurn = (data: unknown) => TurnSchema.safeParse(data);
16
+ export const safeParseSession = (data: unknown) => SessionSchema.safeParse(data);
17
+ export const safeParseInsights = (data: unknown) => InsightsSchema.safeParse(data);