teleportation-cli 1.4.0 → 1.4.1

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,240 @@
1
+ /**
2
+ * Transcript intelligence benchmark aggregation.
3
+ *
4
+ * Computes comparative performance slices by task class + provider + model.
5
+ */
6
+
7
+ function toNumberOrNull(value) {
8
+ if (value == null) return null;
9
+ const num = Number(value);
10
+ return Number.isFinite(num) ? num : null;
11
+ }
12
+
13
+ function toBoolOrNull(value) {
14
+ if (typeof value === 'boolean') return value;
15
+ return null;
16
+ }
17
+
18
+ function initSession(event) {
19
+ return {
20
+ session_id: event.session_id || 'unknown-session',
21
+ task_category: event.task_category || 'unknown',
22
+ provider: event.provider || 'unknown',
23
+ model: event.model || 'unknown',
24
+ success_seen: false,
25
+ failure_seen: false,
26
+ cost_usd: 0,
27
+ duration_ms: 0,
28
+ tokens_used: 0,
29
+ approval_prompts: 0,
30
+ approval_escalations: 0,
31
+ tool_events: 0,
32
+ };
33
+ }
34
+
35
+ function updateSessionSummary(summary, event) {
36
+ if (event.event_type === 'approval_request') {
37
+ summary.approval_prompts += 1;
38
+ }
39
+ if (event.event_type === 'approval_decision' && event.approval?.decision === 'escalate') {
40
+ summary.approval_escalations += 1;
41
+ }
42
+ if (event.event_type === 'tool_call') {
43
+ summary.tool_events += 1;
44
+ }
45
+
46
+ const success = toBoolOrNull(event.execution?.success);
47
+ if (success === true) summary.success_seen = true;
48
+ if (success === false) summary.failure_seen = true;
49
+
50
+ const cost = toNumberOrNull(event.execution?.cost_usd);
51
+ if (cost != null) summary.cost_usd += cost;
52
+
53
+ const duration = toNumberOrNull(event.execution?.duration_ms);
54
+ if (duration != null) summary.duration_ms += duration;
55
+
56
+ const tokens = toNumberOrNull(event.execution?.tokens_used);
57
+ if (tokens != null) summary.tokens_used += tokens;
58
+ }
59
+
60
+ function finalizeSession(summary) {
61
+ let completion_state = 'partial';
62
+ if (summary.success_seen && !summary.failure_seen) completion_state = 'success';
63
+ if (summary.failure_seen && !summary.success_seen) completion_state = 'failed';
64
+
65
+ return {
66
+ ...summary,
67
+ completion_state,
68
+ };
69
+ }
70
+
71
+ function average(values) {
72
+ if (values.length === 0) return null;
73
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
74
+ }
75
+
76
+ function computeQualityProxy(stats) {
77
+ // Bounded [0..1]: favor success and penalize failures/escalations.
78
+ const escalationPenalty = Math.min(0.25, stats.escalation_rate * 0.5);
79
+ const failurePenalty = stats.failure_rate * 0.35;
80
+ const quality = stats.success_rate - escalationPenalty - failurePenalty;
81
+ return Math.max(0, Number(quality.toFixed(4)));
82
+ }
83
+
84
+ function normalizeHigherIsWorse(value, min, max) {
85
+ if (value == null || min == null || max == null) return 0;
86
+ const span = max - min;
87
+ if (span <= 0) return 0;
88
+ return Math.min(1, Math.max(0, (value - min) / span));
89
+ }
90
+
91
+ function proportionConfidenceInterval(rate, sampleSize) {
92
+ if (!Number.isFinite(rate) || !Number.isFinite(sampleSize) || sampleSize <= 0) {
93
+ return { low: 0, high: 0, margin: 0 };
94
+ }
95
+
96
+ const margin = 1.96 * Math.sqrt((rate * (1 - rate)) / sampleSize);
97
+ return {
98
+ low: Number(Math.max(0, rate - margin).toFixed(4)),
99
+ high: Number(Math.min(1, rate + margin).toFixed(4)),
100
+ margin: Number(margin.toFixed(4)),
101
+ };
102
+ }
103
+
104
+ export function buildSessionBenchmarks(events) {
105
+ if (!Array.isArray(events) || events.length === 0) return [];
106
+
107
+ const sessions = new Map();
108
+
109
+ for (const event of events) {
110
+ const key = event.session_id || 'unknown-session';
111
+ const summary = sessions.get(key) || initSession(event);
112
+ updateSessionSummary(summary, event);
113
+ sessions.set(key, summary);
114
+ }
115
+
116
+ return Array.from(sessions.values()).map(finalizeSession);
117
+ }
118
+
119
+ export function aggregateBenchmarksByTaskClass(events, options = {}) {
120
+ const minSessions = Number.isInteger(options.minSessions) ? Math.max(1, options.minSessions) : 1;
121
+ const sessionRows = buildSessionBenchmarks(events);
122
+ if (sessionRows.length === 0) return [];
123
+
124
+ const slices = new Map();
125
+
126
+ for (const row of sessionRows) {
127
+ const key = `${row.task_category}::${row.provider}::${row.model}`;
128
+ const existing = slices.get(key) || {
129
+ task_category: row.task_category,
130
+ provider: row.provider,
131
+ model: row.model,
132
+ sessions: [],
133
+ };
134
+ existing.sessions.push(row);
135
+ slices.set(key, existing);
136
+ }
137
+
138
+ return Array.from(slices.values())
139
+ .filter((slice) => slice.sessions.length >= minSessions)
140
+ .map((slice) => {
141
+ const total = slice.sessions.length;
142
+ const successCount = slice.sessions.filter((row) => row.completion_state === 'success').length;
143
+ const failureCount = slice.sessions.filter((row) => row.completion_state === 'failed').length;
144
+
145
+ const escalationPrompts = slice.sessions.reduce((sum, row) => sum + row.approval_prompts, 0);
146
+ const escalations = slice.sessions.reduce((sum, row) => sum + row.approval_escalations, 0);
147
+
148
+ const stats = {
149
+ success_rate: Number((successCount / total).toFixed(4)),
150
+ failure_rate: Number((failureCount / total).toFixed(4)),
151
+ escalation_rate: escalationPrompts === 0
152
+ ? 0
153
+ : Number((escalations / escalationPrompts).toFixed(4)),
154
+ };
155
+
156
+ return {
157
+ task_category: slice.task_category,
158
+ provider: slice.provider,
159
+ model: slice.model,
160
+ sample_size: total,
161
+ ...stats,
162
+ avg_cost_usd: average(slice.sessions.map((row) => row.cost_usd)),
163
+ avg_duration_ms: average(slice.sessions.map((row) => row.duration_ms)),
164
+ avg_tokens_used: average(slice.sessions.map((row) => row.tokens_used)),
165
+ completion_reliability: Number((1 - stats.failure_rate).toFixed(4)),
166
+ quality_proxy: computeQualityProxy(stats),
167
+ };
168
+ })
169
+ .sort((a, b) => {
170
+ if (b.quality_proxy !== a.quality_proxy) return b.quality_proxy - a.quality_proxy;
171
+ if (b.success_rate !== a.success_rate) return b.success_rate - a.success_rate;
172
+ return a.avg_cost_usd - b.avg_cost_usd;
173
+ });
174
+ }
175
+
176
+ export function scoreHarnessModelBenchmark(events, options = {}) {
177
+ const provider = options.harness || options.provider;
178
+ const model = options.model;
179
+ const taskCategory = options.taskCategory || options.task_category;
180
+ const minSessions = Number.isInteger(options.minSessions) ? Math.max(1, options.minSessions) : 30;
181
+
182
+ const allSlices = aggregateBenchmarksByTaskClass(events, { minSessions: 1 });
183
+ if (allSlices.length === 0) {
184
+ return { eligible: false, reason: 'no_benchmark_data' };
185
+ }
186
+
187
+ const comparable = taskCategory
188
+ ? allSlices.filter((slice) => slice.task_category === taskCategory)
189
+ : allSlices;
190
+
191
+ const target = comparable.find((slice) =>
192
+ slice.provider === provider && slice.model === model && (!taskCategory || slice.task_category === taskCategory),
193
+ );
194
+
195
+ if (!target) {
196
+ return { eligible: false, reason: 'no_matching_slice' };
197
+ }
198
+
199
+ if (target.sample_size < minSessions) {
200
+ return {
201
+ eligible: false,
202
+ reason: 'insufficient_evidence',
203
+ sample_size: target.sample_size,
204
+ required_sample_size: minSessions,
205
+ };
206
+ }
207
+
208
+ const costs = comparable.map((slice) => slice.avg_cost_usd).filter((value) => Number.isFinite(value));
209
+ const latencies = comparable.map((slice) => slice.avg_duration_ms).filter((value) => Number.isFinite(value));
210
+
211
+ const costMin = costs.length > 0 ? Math.min(...costs) : null;
212
+ const costMax = costs.length > 0 ? Math.max(...costs) : null;
213
+ const latencyMin = latencies.length > 0 ? Math.min(...latencies) : null;
214
+ const latencyMax = latencies.length > 0 ? Math.max(...latencies) : null;
215
+
216
+ const normalizedCostPenalty = normalizeHigherIsWorse(target.avg_cost_usd, costMin, costMax);
217
+ const normalizedLatencyPenalty = normalizeHigherIsWorse(target.avg_duration_ms, latencyMin, latencyMax);
218
+
219
+ const score = Number((
220
+ (0.35 * target.success_rate) +
221
+ (0.2 * target.quality_proxy) +
222
+ (0.15 * (1 - normalizedCostPenalty)) +
223
+ (0.1 * (1 - normalizedLatencyPenalty)) +
224
+ (0.1 * target.completion_reliability) +
225
+ (0.1 * (1 - target.escalation_rate))
226
+ ).toFixed(4));
227
+
228
+ return {
229
+ eligible: true,
230
+ task_category: target.task_category,
231
+ provider: target.provider,
232
+ model: target.model,
233
+ sample_size: target.sample_size,
234
+ score,
235
+ metrics: target,
236
+ confidence_interval: proportionConfidenceInterval(target.success_rate, target.sample_size),
237
+ };
238
+ }
239
+
240
+ export { computeQualityProxy, proportionConfidenceInterval };
@@ -0,0 +1,29 @@
1
+ export {
2
+ normalizeTranscriptEvent,
3
+ normalizeTranscriptEvents,
4
+ isValidNormalizedEvent,
5
+ normalizeTranscriptEntry,
6
+ validateNormalizedTranscriptEntry,
7
+ TASK_CATEGORIES,
8
+ EVENT_TYPES,
9
+ APPROVAL_DECISIONS,
10
+ APPROVAL_SOURCES,
11
+ ENTRY_SCHEMA_VERSION,
12
+ } from './schema.js';
13
+
14
+ export {
15
+ mineRequestPatterns,
16
+ extractRequestText,
17
+ tokenize,
18
+ classifyIntent,
19
+ } from './transcript-mine.js';
20
+
21
+ export {
22
+ buildSessionBenchmarks,
23
+ aggregateBenchmarksByTaskClass,
24
+ scoreHarnessModelBenchmark,
25
+ computeQualityProxy,
26
+ proportionConfidenceInterval,
27
+ } from './benchmark.js';
28
+
29
+ export { rebuildPolicyArtifacts } from './rebuild-policies.js';
@@ -0,0 +1,169 @@
1
+ import { aggregateBenchmarksByTaskClass, scoreHarnessModelBenchmark } from './benchmark.js';
2
+ import { mineRequestPatterns } from './transcript-mine.js';
3
+
4
+ function collectOperationalTelemetry(events) {
5
+ const telemetry = {
6
+ request_interrupted: 0,
7
+ request_interrupted_tool_use: 0,
8
+ local_command_caveat: 0,
9
+ local_command_stdout: 0,
10
+ local_command_stderr: 0,
11
+ };
12
+
13
+ if (!Array.isArray(events) || events.length === 0) {
14
+ return {
15
+ ...telemetry,
16
+ interrupted_total: 0,
17
+ interruption_rate: 0,
18
+ wrapper_marker_total: 0,
19
+ };
20
+ }
21
+
22
+ for (const event of events) {
23
+ const metadata = event?.metadata || {};
24
+ const fields = [
25
+ metadata.user_message,
26
+ metadata.user_prompt,
27
+ metadata.prompt,
28
+ metadata.message,
29
+ metadata.instruction,
30
+ metadata.query,
31
+ metadata.task_prompt,
32
+ ];
33
+
34
+ for (const value of fields) {
35
+ if (typeof value !== 'string') continue;
36
+ const text = value.toLowerCase();
37
+ if (text.includes('[request interrupted by user for tool use]')) {
38
+ telemetry.request_interrupted_tool_use += 1;
39
+ } else if (text.includes('[request interrupted by user]')) {
40
+ telemetry.request_interrupted += 1;
41
+ }
42
+ if (text.includes('<local-command-caveat>')) telemetry.local_command_caveat += 1;
43
+ if (text.includes('<local-command-stdout>')) telemetry.local_command_stdout += 1;
44
+ if (text.includes('<local-command-stderr>')) telemetry.local_command_stderr += 1;
45
+ }
46
+ }
47
+
48
+ const interruptedTotal = telemetry.request_interrupted + telemetry.request_interrupted_tool_use;
49
+ const wrapperTotal =
50
+ telemetry.local_command_caveat +
51
+ telemetry.local_command_stdout +
52
+ telemetry.local_command_stderr;
53
+
54
+ return {
55
+ ...telemetry,
56
+ interrupted_total: interruptedTotal,
57
+ interruption_rate: Number((interruptedTotal / events.length).toFixed(4)),
58
+ wrapper_marker_total: wrapperTotal,
59
+ };
60
+ }
61
+
62
+ function buildLeaderboardAndNoRouteSet(events, slices, options = {}) {
63
+ const minSessionsForRouting = Number.isInteger(options.minSessionsForRouting)
64
+ ? options.minSessionsForRouting
65
+ : 30;
66
+ const minRouteScore = typeof options.minRouteScore === 'number' ? options.minRouteScore : 0.45;
67
+ const maxEscalationRate = typeof options.maxEscalationRate === 'number' ? options.maxEscalationRate : 0.5;
68
+
69
+ const scored = slices.map((slice) => {
70
+ const result = scoreHarnessModelBenchmark(events, {
71
+ harness: slice.provider,
72
+ model: slice.model,
73
+ taskCategory: slice.task_category,
74
+ minSessions: minSessionsForRouting,
75
+ });
76
+ return { slice, result };
77
+ });
78
+
79
+ const noRouteSet = scored
80
+ .filter(({ slice, result }) => {
81
+ if (!result.eligible) return true;
82
+ if (result.score < minRouteScore) return true;
83
+ if (slice.escalation_rate > maxEscalationRate) return true;
84
+ return false;
85
+ })
86
+ .map(({ slice, result }) => ({
87
+ task_category: slice.task_category,
88
+ provider: slice.provider,
89
+ model: slice.model,
90
+ reason: !result.eligible
91
+ ? result.reason
92
+ : result.score < minRouteScore
93
+ ? 'low_route_score'
94
+ : 'high_escalation_rate',
95
+ sample_size: slice.sample_size,
96
+ score: result.score ?? null,
97
+ }));
98
+
99
+ const eligible = scored
100
+ .filter(({ slice, result }) =>
101
+ result.eligible &&
102
+ result.score >= minRouteScore &&
103
+ slice.escalation_rate <= maxEscalationRate,
104
+ )
105
+ .map(({ slice, result }) => ({
106
+ task_category: slice.task_category,
107
+ provider: slice.provider,
108
+ model: slice.model,
109
+ score: result.score,
110
+ sample_size: result.sample_size,
111
+ confidence_interval: result.confidence_interval,
112
+ }));
113
+
114
+ const byTaskCategory = new Map();
115
+ for (const row of eligible) {
116
+ const existing = byTaskCategory.get(row.task_category);
117
+ if (!existing || row.score > existing.score) {
118
+ byTaskCategory.set(row.task_category, row);
119
+ }
120
+ }
121
+
122
+ return {
123
+ leaderboard: Array.from(byTaskCategory.values()).sort((a, b) => b.score - a.score),
124
+ no_route_set: noRouteSet.sort((a, b) => a.task_category.localeCompare(b.task_category)),
125
+ };
126
+ }
127
+
128
+ export function rebuildPolicyArtifacts(events, options = {}) {
129
+ const benchmarkMinSessions = Number.isInteger(options.benchmarkMinSessions)
130
+ ? options.benchmarkMinSessions
131
+ : 1;
132
+ const patternMinOccurrences = Number.isInteger(options.patternMinOccurrences)
133
+ ? options.patternMinOccurrences
134
+ : 2;
135
+ const topPatterns = Number.isInteger(options.topPatterns) ? options.topPatterns : 25;
136
+
137
+ const patterns = mineRequestPatterns(events, {
138
+ minOccurrences: patternMinOccurrences,
139
+ topK: topPatterns,
140
+ });
141
+
142
+ const benchmarkSlices = aggregateBenchmarksByTaskClass(events, {
143
+ minSessions: benchmarkMinSessions,
144
+ });
145
+ const { leaderboard, no_route_set } = buildLeaderboardAndNoRouteSet(events, benchmarkSlices, options);
146
+ const operationalTelemetry = collectOperationalTelemetry(events);
147
+
148
+ return {
149
+ generated_at: new Date().toISOString(),
150
+ scope: options.scope || 'global',
151
+ summary: {
152
+ events_processed: Array.isArray(events) ? events.length : 0,
153
+ pattern_count: patterns.length,
154
+ benchmark_slice_count: benchmarkSlices.length,
155
+ leaderboard_count: leaderboard.length,
156
+ no_route_count: no_route_set.length,
157
+ interrupted_total: operationalTelemetry.interrupted_total,
158
+ interruption_rate: operationalTelemetry.interruption_rate,
159
+ wrapper_marker_total: operationalTelemetry.wrapper_marker_total,
160
+ },
161
+ artifacts: {
162
+ request_patterns: patterns,
163
+ benchmark_slices: benchmarkSlices,
164
+ benchmark_leaderboard: leaderboard,
165
+ no_route_set,
166
+ operational_telemetry: operationalTelemetry,
167
+ },
168
+ };
169
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Transcript Intelligence Schema
3
+ *
4
+ * Canonical event normalization for cross-provider transcript data.
5
+ */
6
+
7
+ const TASK_CATEGORIES = new Set([
8
+ 'codebase-modification',
9
+ 'api-orchestration',
10
+ 'data-processing',
11
+ 'web-research',
12
+ 'code-review',
13
+ 'multi-tool-workflow',
14
+ 'unknown',
15
+ ]);
16
+
17
+ const EVENT_TYPES = new Set([
18
+ 'task_start',
19
+ 'task_end',
20
+ 'tool_call',
21
+ 'approval_request',
22
+ 'approval_decision',
23
+ 'override',
24
+ 'route_decision',
25
+ ]);
26
+
27
+ const APPROVAL_DECISIONS = new Set(['approve', 'reject', 'escalate', 'deny', 'none']);
28
+ const APPROVAL_SOURCES = new Set(['user', 'policy', 'fallback']);
29
+
30
+ function toIsoTimestamp(value) {
31
+ if (!value) return new Date().toISOString();
32
+ if (typeof value === 'number') return new Date(value).toISOString();
33
+ if (typeof value === 'string' && /^\d+$/.test(value)) {
34
+ return new Date(Number(value)).toISOString();
35
+ }
36
+ const parsed = new Date(value);
37
+ return Number.isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString();
38
+ }
39
+
40
+ function normalizeTaskCategory(value) {
41
+ if (typeof value !== 'string') return 'unknown';
42
+ const lowered = value.toLowerCase();
43
+ return TASK_CATEGORIES.has(lowered) ? lowered : 'unknown';
44
+ }
45
+
46
+ function normalizeApprovalDecision(value) {
47
+ if (typeof value !== 'string') return 'none';
48
+ const lowered = value.toLowerCase();
49
+ return APPROVAL_DECISIONS.has(lowered) ? lowered : 'none';
50
+ }
51
+
52
+ function normalizeApprovalSource(value) {
53
+ if (typeof value !== 'string') return 'fallback';
54
+ const lowered = value.toLowerCase();
55
+ return APPROVAL_SOURCES.has(lowered) ? lowered : 'fallback';
56
+ }
57
+
58
+ function mapTimelineTypeToEventType(type) {
59
+ switch (type) {
60
+ case 'tool_use':
61
+ case 'tool_executed':
62
+ case 'tool_completed':
63
+ case 'tool_failed':
64
+ return 'tool_call';
65
+ case 'approval_requested':
66
+ return 'approval_request';
67
+ case 'approval_decided':
68
+ case 'approval_decision':
69
+ return 'approval_decision';
70
+ case 'route_decision':
71
+ return 'route_decision';
72
+ case 'override':
73
+ return 'override';
74
+ case 'task_started':
75
+ return 'task_start';
76
+ case 'task_completed':
77
+ return 'task_end';
78
+ case 'assistant_response':
79
+ case 'session_registered':
80
+ case 'compact_summary':
81
+ // These relay event types do not map to a canonical intelligence event type.
82
+ return null;
83
+ default:
84
+ // Unknown types: return null so callers can explicitly filter or flag them.
85
+ return null;
86
+ }
87
+ }
88
+
89
+ function mapActionClass(event) {
90
+ if (event.type === 'tool_use') return 'tool_execute';
91
+ if (event.type === 'tool_failed') return 'tool_failed';
92
+ if (event.type === 'tool_completed') return 'tool_completed';
93
+ if (event.type === 'assistant_response') return 'assistant_response';
94
+ if (event.type === 'approval_requested') return 'approval_gate';
95
+ if (event.type === 'approval_decision') return 'approval_gate';
96
+ return 'unknown';
97
+ }
98
+
99
+ function toNumberOrNull(value) {
100
+ if (value == null) return null;
101
+ const numeric = Number(value);
102
+ return Number.isFinite(numeric) ? numeric : null;
103
+ }
104
+
105
+ export function normalizeTranscriptEvent(event, context = {}) {
106
+ const eventType = mapTimelineTypeToEventType(event.type);
107
+ const normalized = {
108
+ event_id: event.id || null,
109
+ occurred_at: toIsoTimestamp(event.timestamp ?? event.occurred_at),
110
+ user_id: context.user_id ?? null,
111
+ project_id: context.project_id ?? null,
112
+ session_id: context.session_id ?? null,
113
+ task_id: context.task_id ?? null,
114
+ task_category: normalizeTaskCategory(context.task_category),
115
+ provider: context.provider ?? null,
116
+ model: context.model ?? null,
117
+ event_type: eventType,
118
+ action_class: mapActionClass(event),
119
+ tool_name: event.meta?.tool_name ?? null,
120
+ approval: {
121
+ requested: eventType === 'approval_request' || eventType === 'approval_decision',
122
+ decision: normalizeApprovalDecision(event.meta?.decision),
123
+ decision_source: normalizeApprovalSource(event.meta?.decision_source),
124
+ confidence: toNumberOrNull(event.meta?.confidence),
125
+ },
126
+ execution: {
127
+ success: typeof event.meta?.success === 'boolean' ? event.meta.success : null,
128
+ duration_ms: toNumberOrNull(event.meta?.duration_ms),
129
+ tokens_used: toNumberOrNull(event.meta?.tokens_used),
130
+ cost_usd: toNumberOrNull(event.meta?.cost_usd),
131
+ loop_detected: typeof event.meta?.loop_detected === 'boolean' ? event.meta.loop_detected : null,
132
+ },
133
+ metadata: {
134
+ // Only include safe, non-sensitive fields from event.meta.
135
+ // Do NOT spread event.meta directly — it may contain tool arguments,
136
+ // file contents, or credentials that must not leave the ingestion layer.
137
+ source: event.source ?? null,
138
+ raw_type: event.type ?? null,
139
+ },
140
+ };
141
+
142
+ return normalized;
143
+ }
144
+
145
+ export function normalizeTranscriptEvents(events, context = {}) {
146
+ if (!Array.isArray(events)) return [];
147
+ return events.map((event) => normalizeTranscriptEvent(event, context));
148
+ }
149
+
150
+ export function isValidNormalizedEvent(event) {
151
+ if (!event || typeof event !== 'object') return false;
152
+ if (!EVENT_TYPES.has(event.event_type)) return false;
153
+ if (!TASK_CATEGORIES.has(event.task_category)) return false;
154
+ if (!event.occurred_at || Number.isNaN(new Date(event.occurred_at).getTime())) return false;
155
+ if (!event.approval || !APPROVAL_DECISIONS.has(event.approval.decision)) return false;
156
+ if (!APPROVAL_SOURCES.has(event.approval.decision_source)) return false;
157
+ return true;
158
+ }
159
+
160
+ // ============================================================================
161
+ // Raw Transcript Entry Normalizer (per-message, pre-timeline-event)
162
+ // ============================================================================
163
+
164
+ const ENTRY_SCHEMA_VERSION = 'v1';
165
+ const DEFAULT_HARNESS = 'claude-code';
166
+ const TEXT_EXCERPT_LIMIT = 500;
167
+
168
+ function toTimestampMs(value) {
169
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
170
+ if (typeof value === 'string') {
171
+ if (/^\d+$/.test(value)) return Number(value);
172
+ const parsed = Date.parse(value);
173
+ if (!Number.isNaN(parsed)) return parsed;
174
+ }
175
+ return Date.now();
176
+ }
177
+
178
+ function toMessage(entry) {
179
+ if (!entry || typeof entry !== 'object') return {};
180
+ if (entry.message && typeof entry.message === 'object') return entry.message;
181
+ return entry;
182
+ }
183
+
184
+ function extractTextContent(content) {
185
+ if (typeof content === 'string') return content.trim();
186
+ if (!Array.isArray(content)) return '';
187
+ return content
188
+ .filter(block => block?.type === 'text' && typeof block.text === 'string')
189
+ .map(block => block.text.trim())
190
+ .filter(Boolean)
191
+ .join('\n')
192
+ .trim();
193
+ }
194
+
195
+ /**
196
+ * Normalize a raw transcript entry (message) into a stable canonical shape
197
+ * for the intelligence pipeline. Operates on the raw transcript format, not
198
+ * on timeline events.
199
+ */
200
+ export function normalizeTranscriptEntry(entry, options = {}) {
201
+ const message = toMessage(entry);
202
+ const content = message.content;
203
+ const blocks = Array.isArray(content) ? content : [];
204
+ const textContent = extractTextContent(content);
205
+
206
+ const toolUses = blocks.filter(block => block?.type === 'tool_use');
207
+ const toolResults = blocks.filter(block => block?.type === 'tool_result');
208
+
209
+ const sessionId = typeof options.sessionId === 'string' ? options.sessionId : '';
210
+ const messageIndex = Number.isInteger(options.messageIndex) ? options.messageIndex : 0;
211
+ const harness = typeof options.harness === 'string' && options.harness
212
+ ? options.harness
213
+ : DEFAULT_HARNESS;
214
+
215
+ return {
216
+ schema_version: ENTRY_SCHEMA_VERSION,
217
+ session_id: sessionId,
218
+ message_index: messageIndex,
219
+ harness,
220
+ message_id: entry?.uuid || message?.uuid || `${sessionId}:${messageIndex}`,
221
+ role: typeof message.role === 'string' ? message.role : 'unknown',
222
+ model: typeof message.model === 'string' ? message.model : null,
223
+ timestamp_ms: toTimestampMs(entry?.timestamp ?? message?.timestamp),
224
+ has_content: Boolean(content),
225
+ text_excerpt: textContent ? textContent.slice(0, TEXT_EXCERPT_LIMIT) : null,
226
+ tool_uses_count: toolUses.length,
227
+ tool_results_count: toolResults.length,
228
+ approval_candidate: toolUses.length > 0,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Validate a normalized transcript entry shape.
234
+ */
235
+ export function validateNormalizedTranscriptEntry(entry) {
236
+ const errors = [];
237
+ if (!entry || typeof entry !== 'object') return { valid: false, errors: ['entry must be an object'] };
238
+ if (entry.schema_version !== ENTRY_SCHEMA_VERSION) errors.push(`schema_version must be ${ENTRY_SCHEMA_VERSION}`);
239
+ if (typeof entry.session_id !== 'string') errors.push('session_id must be a string');
240
+ if (!Number.isInteger(entry.message_index) || entry.message_index < 0) errors.push('message_index must be a non-negative integer');
241
+ if (typeof entry.harness !== 'string' || !entry.harness) errors.push('harness must be a non-empty string');
242
+ if (typeof entry.message_id !== 'string' || !entry.message_id) errors.push('message_id must be a non-empty string');
243
+ if (typeof entry.role !== 'string' || !entry.role) errors.push('role must be a non-empty string');
244
+ if (typeof entry.timestamp_ms !== 'number' || !Number.isFinite(entry.timestamp_ms)) errors.push('timestamp_ms must be a finite number');
245
+ if (typeof entry.tool_uses_count !== 'number' || entry.tool_uses_count < 0) errors.push('tool_uses_count must be a non-negative number');
246
+ if (typeof entry.tool_results_count !== 'number' || entry.tool_results_count < 0) errors.push('tool_results_count must be a non-negative number');
247
+ if (typeof entry.approval_candidate !== 'boolean') errors.push('approval_candidate must be a boolean');
248
+ return { valid: errors.length === 0, errors };
249
+ }
250
+
251
+ export {
252
+ TASK_CATEGORIES,
253
+ EVENT_TYPES,
254
+ APPROVAL_DECISIONS,
255
+ APPROVAL_SOURCES,
256
+ ENTRY_SCHEMA_VERSION,
257
+ DEFAULT_HARNESS,
258
+ TEXT_EXCERPT_LIMIT,
259
+ };