@contractspec/example.product-intent 3.7.6 → 3.7.7

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.
@@ -1,316 +1,316 @@
1
+ import { PosthogAnalyticsProvider } from '@contractspec/integration.providers-impls/impls/posthog';
1
2
  import type {
2
- AnalyticsReader,
3
- AnalyticsQueryResult,
4
- DateRangeInput,
5
- } from '@contractspec/lib.contracts-integrations';
6
- import type { EvidenceChunk } from '@contractspec/lib.contracts-spec/product-intent/types';
3
+ AnalyticsEvent,
4
+ FunnelDefinition,
5
+ } from '@contractspec/lib.analytics';
7
6
  import { FunnelAnalyzer } from '@contractspec/lib.analytics/funnel';
8
7
  import type {
9
- AnalyticsEvent,
10
- FunnelDefinition,
11
- } from '@contractspec/lib.analytics';
12
- import { PosthogAnalyticsProvider } from '@contractspec/integration.providers-impls/impls/posthog';
8
+ AnalyticsQueryResult,
9
+ AnalyticsReader,
10
+ DateRangeInput,
11
+ } from '@contractspec/lib.contracts-integrations';
12
+ import type { EvidenceChunk } from '@contractspec/lib.contracts-spec/product-intent/types';
13
13
 
14
14
  export interface PosthogEvidenceOptions {
15
- reader: AnalyticsReader;
16
- dateRange?: DateRangeInput;
17
- eventNames?: string[];
18
- limit?: number;
19
- funnel?: FunnelDefinition;
20
- includeFeatureFlags?: boolean;
15
+ reader: AnalyticsReader;
16
+ dateRange?: DateRangeInput;
17
+ eventNames?: string[];
18
+ limit?: number;
19
+ funnel?: FunnelDefinition;
20
+ includeFeatureFlags?: boolean;
21
21
  }
22
22
 
23
23
  export interface PosthogEvidenceEnvOptions {
24
- defaultLookbackDays?: number;
25
- defaultLimit?: number;
24
+ defaultLookbackDays?: number;
25
+ defaultLimit?: number;
26
26
  }
27
27
 
28
28
  export async function loadPosthogEvidenceChunks(
29
- options: PosthogEvidenceOptions
29
+ options: PosthogEvidenceOptions
30
30
  ): Promise<EvidenceChunk[]> {
31
- const chunks: EvidenceChunk[] = [];
32
- const range = resolveRange(options.dateRange);
31
+ const chunks: EvidenceChunk[] = [];
32
+ const range = resolveRange(options.dateRange);
33
33
 
34
- const eventSummary = await loadEventSummary(options, range);
35
- if (eventSummary) {
36
- chunks.push(eventSummary);
37
- }
34
+ const eventSummary = await loadEventSummary(options, range);
35
+ if (eventSummary) {
36
+ chunks.push(eventSummary);
37
+ }
38
38
 
39
- const funnelEvidence = await loadFunnelEvidence(options, range);
40
- if (funnelEvidence) {
41
- chunks.push(funnelEvidence);
42
- }
39
+ const funnelEvidence = await loadFunnelEvidence(options, range);
40
+ if (funnelEvidence) {
41
+ chunks.push(funnelEvidence);
42
+ }
43
43
 
44
- const featureFlags = await loadFeatureFlagEvidence(options);
45
- if (featureFlags) {
46
- chunks.push(featureFlags);
47
- }
44
+ const featureFlags = await loadFeatureFlagEvidence(options);
45
+ if (featureFlags) {
46
+ chunks.push(featureFlags);
47
+ }
48
48
 
49
- return chunks;
49
+ return chunks;
50
50
  }
51
51
 
52
52
  async function loadEventSummary(
53
- options: PosthogEvidenceOptions,
54
- range: { from: Date; to: Date }
53
+ options: PosthogEvidenceOptions,
54
+ range: { from: Date; to: Date }
55
55
  ): Promise<EvidenceChunk | null> {
56
- if (!options.reader.queryHogQL) return null;
57
- const eventFilter = buildEventFilter(options.eventNames);
58
- const limit = options.limit ?? 10;
59
- const result = await options.reader.queryHogQL({
60
- query: [
61
- 'select',
62
- ' event as eventName,',
63
- ' count() as total',
64
- 'from events',
65
- 'where timestamp >= {dateFrom} and timestamp < {dateTo}',
66
- eventFilter.clause ? `and ${eventFilter.clause}` : '',
67
- 'group by eventName',
68
- 'order by total desc',
69
- `limit ${limit}`,
70
- ]
71
- .filter(Boolean)
72
- .join('\n'),
73
- values: {
74
- dateFrom: range.from.toISOString(),
75
- dateTo: range.to.toISOString(),
76
- ...eventFilter.values,
77
- },
78
- });
79
- const rows = mapRows(result);
80
- if (rows.length === 0) return null;
81
- const lines = rows.map((row) => {
82
- const name = asString(row.eventName) ?? 'unknown';
83
- const total = asNumber(row.total);
84
- return `- ${name}: ${total}`;
85
- });
86
- return {
87
- chunkId: `posthog:event_summary:${range.from.toISOString()}`,
88
- text: [
89
- `PostHog event summary (${range.from.toISOString()} → ${range.to.toISOString()}):`,
90
- ...lines,
91
- ].join('\n'),
92
- meta: {
93
- source: 'posthog',
94
- kind: 'event_summary',
95
- dateFrom: range.from.toISOString(),
96
- dateTo: range.to.toISOString(),
97
- },
98
- };
56
+ if (!options.reader.queryHogQL) return null;
57
+ const eventFilter = buildEventFilter(options.eventNames);
58
+ const limit = options.limit ?? 10;
59
+ const result = await options.reader.queryHogQL({
60
+ query: [
61
+ 'select',
62
+ ' event as eventName,',
63
+ ' count() as total',
64
+ 'from events',
65
+ 'where timestamp >= {dateFrom} and timestamp < {dateTo}',
66
+ eventFilter.clause ? `and ${eventFilter.clause}` : '',
67
+ 'group by eventName',
68
+ 'order by total desc',
69
+ `limit ${limit}`,
70
+ ]
71
+ .filter(Boolean)
72
+ .join('\n'),
73
+ values: {
74
+ dateFrom: range.from.toISOString(),
75
+ dateTo: range.to.toISOString(),
76
+ ...eventFilter.values,
77
+ },
78
+ });
79
+ const rows = mapRows(result);
80
+ if (rows.length === 0) return null;
81
+ const lines = rows.map((row) => {
82
+ const name = asString(row.eventName) ?? 'unknown';
83
+ const total = asNumber(row.total);
84
+ return `- ${name}: ${total}`;
85
+ });
86
+ return {
87
+ chunkId: `posthog:event_summary:${range.from.toISOString()}`,
88
+ text: [
89
+ `PostHog event summary (${range.from.toISOString()} → ${range.to.toISOString()}):`,
90
+ ...lines,
91
+ ].join('\n'),
92
+ meta: {
93
+ source: 'posthog',
94
+ kind: 'event_summary',
95
+ dateFrom: range.from.toISOString(),
96
+ dateTo: range.to.toISOString(),
97
+ },
98
+ };
99
99
  }
100
100
 
101
101
  async function loadFunnelEvidence(
102
- options: PosthogEvidenceOptions,
103
- range: { from: Date; to: Date }
102
+ options: PosthogEvidenceOptions,
103
+ range: { from: Date; to: Date }
104
104
  ): Promise<EvidenceChunk | null> {
105
- if (!options.funnel) return null;
106
- if (!options.reader.getEvents) return null;
107
- const events: AnalyticsEvent[] = [];
108
- const eventNames = options.funnel.steps.map((step) => step.eventName);
109
- for (const eventName of eventNames) {
110
- const response = await options.reader.getEvents({
111
- event: eventName,
112
- dateRange: {
113
- from: range.from,
114
- to: range.to,
115
- },
116
- limit: options.limit ?? 500,
117
- });
118
- response.results.forEach((event) => {
119
- events.push({
120
- name: event.event,
121
- userId: event.distinctId,
122
- tenantId:
123
- typeof event.properties?.tenantId === 'string'
124
- ? event.properties.tenantId
125
- : undefined,
126
- timestamp: event.timestamp,
127
- properties: event.properties,
128
- });
129
- });
130
- }
131
- if (events.length === 0) return null;
132
- const analyzer = new FunnelAnalyzer();
133
- const analysis = analyzer.analyze(events, options.funnel);
134
- const lines = analysis.steps.map((step) => {
135
- return `- ${step.step.eventName}: ${step.count} (conversion ${step.conversionRate}, drop-off ${step.dropOffRate})`;
136
- });
137
- return {
138
- chunkId: `posthog:funnel:${options.funnel.name}`,
139
- text: [`PostHog funnel analysis — ${options.funnel.name}:`, ...lines].join(
140
- '\n'
141
- ),
142
- meta: {
143
- source: 'posthog',
144
- kind: 'funnel',
145
- funnelName: options.funnel.name,
146
- dateFrom: range.from.toISOString(),
147
- dateTo: range.to.toISOString(),
148
- },
149
- };
105
+ if (!options.funnel) return null;
106
+ if (!options.reader.getEvents) return null;
107
+ const events: AnalyticsEvent[] = [];
108
+ const eventNames = options.funnel.steps.map((step) => step.eventName);
109
+ for (const eventName of eventNames) {
110
+ const response = await options.reader.getEvents({
111
+ event: eventName,
112
+ dateRange: {
113
+ from: range.from,
114
+ to: range.to,
115
+ },
116
+ limit: options.limit ?? 500,
117
+ });
118
+ response.results.forEach((event) => {
119
+ events.push({
120
+ name: event.event,
121
+ userId: event.distinctId,
122
+ tenantId:
123
+ typeof event.properties?.tenantId === 'string'
124
+ ? event.properties.tenantId
125
+ : undefined,
126
+ timestamp: event.timestamp,
127
+ properties: event.properties,
128
+ });
129
+ });
130
+ }
131
+ if (events.length === 0) return null;
132
+ const analyzer = new FunnelAnalyzer();
133
+ const analysis = analyzer.analyze(events, options.funnel);
134
+ const lines = analysis.steps.map((step) => {
135
+ return `- ${step.step.eventName}: ${step.count} (conversion ${step.conversionRate}, drop-off ${step.dropOffRate})`;
136
+ });
137
+ return {
138
+ chunkId: `posthog:funnel:${options.funnel.name}`,
139
+ text: [`PostHog funnel analysis — ${options.funnel.name}:`, ...lines].join(
140
+ '\n'
141
+ ),
142
+ meta: {
143
+ source: 'posthog',
144
+ kind: 'funnel',
145
+ funnelName: options.funnel.name,
146
+ dateFrom: range.from.toISOString(),
147
+ dateTo: range.to.toISOString(),
148
+ },
149
+ };
150
150
  }
151
151
 
152
152
  async function loadFeatureFlagEvidence(
153
- options: PosthogEvidenceOptions
153
+ options: PosthogEvidenceOptions
154
154
  ): Promise<EvidenceChunk | null> {
155
- if (!options.includeFeatureFlags) return null;
156
- if (!options.reader.getFeatureFlags) return null;
157
- const response = await options.reader.getFeatureFlags({ limit: 10 });
158
- if (!response.results.length) return null;
159
- const lines = response.results.map((flag) => {
160
- const key = flag.key ?? 'unknown';
161
- const active = flag.active ? 'active' : 'inactive';
162
- return `- ${key}: ${active}`;
163
- });
164
- return {
165
- chunkId: 'posthog:feature_flags',
166
- text: ['PostHog feature flags:', ...lines].join('\n'),
167
- meta: {
168
- source: 'posthog',
169
- kind: 'feature_flags',
170
- },
171
- };
155
+ if (!options.includeFeatureFlags) return null;
156
+ if (!options.reader.getFeatureFlags) return null;
157
+ const response = await options.reader.getFeatureFlags({ limit: 10 });
158
+ if (!response.results.length) return null;
159
+ const lines = response.results.map((flag) => {
160
+ const key = flag.key ?? 'unknown';
161
+ const active = flag.active ? 'active' : 'inactive';
162
+ return `- ${key}: ${active}`;
163
+ });
164
+ return {
165
+ chunkId: 'posthog:feature_flags',
166
+ text: ['PostHog feature flags:', ...lines].join('\n'),
167
+ meta: {
168
+ source: 'posthog',
169
+ kind: 'feature_flags',
170
+ },
171
+ };
172
172
  }
173
173
 
174
174
  function resolveRange(dateRange: DateRangeInput | undefined): {
175
- from: Date;
176
- to: Date;
175
+ from: Date;
176
+ to: Date;
177
177
  } {
178
- const now = new Date();
179
- const from =
180
- dateRange?.from instanceof Date
181
- ? dateRange.from
182
- : dateRange?.from
183
- ? new Date(dateRange.from)
184
- : new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
185
- const to =
186
- dateRange?.to instanceof Date
187
- ? dateRange.to
188
- : dateRange?.to
189
- ? new Date(dateRange.to)
190
- : now;
191
- return { from, to };
178
+ const now = new Date();
179
+ const from =
180
+ dateRange?.from instanceof Date
181
+ ? dateRange.from
182
+ : dateRange?.from
183
+ ? new Date(dateRange.from)
184
+ : new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
185
+ const to =
186
+ dateRange?.to instanceof Date
187
+ ? dateRange.to
188
+ : dateRange?.to
189
+ ? new Date(dateRange.to)
190
+ : now;
191
+ return { from, to };
192
192
  }
193
193
 
194
194
  function buildEventFilter(eventNames?: string[]): {
195
- clause?: string;
196
- values?: Record<string, unknown>;
195
+ clause?: string;
196
+ values?: Record<string, unknown>;
197
197
  } {
198
- if (!eventNames || eventNames.length === 0) return {};
199
- if (eventNames.length === 1) {
200
- return {
201
- clause: 'event = {event0}',
202
- values: { event0: eventNames[0] },
203
- };
204
- }
205
- const values: Record<string, unknown> = {};
206
- const clauses = eventNames.map((eventName, index) => {
207
- values[`event${index}`] = eventName;
208
- return `event = {event${index}}`;
209
- });
210
- return {
211
- clause: `(${clauses.join(' or ')})`,
212
- values,
213
- };
198
+ if (!eventNames || eventNames.length === 0) return {};
199
+ if (eventNames.length === 1) {
200
+ return {
201
+ clause: 'event = {event0}',
202
+ values: { event0: eventNames[0] },
203
+ };
204
+ }
205
+ const values: Record<string, unknown> = {};
206
+ const clauses = eventNames.map((eventName, index) => {
207
+ values[`event${index}`] = eventName;
208
+ return `event = {event${index}}`;
209
+ });
210
+ return {
211
+ clause: `(${clauses.join(' or ')})`,
212
+ values,
213
+ };
214
214
  }
215
215
 
216
216
  function mapRows(result: AnalyticsQueryResult): Record<string, unknown>[] {
217
- if (!Array.isArray(result.results) || !Array.isArray(result.columns)) {
218
- return [];
219
- }
220
- const columns = result.columns;
221
- return result.results.flatMap((row) => {
222
- if (!Array.isArray(row)) return [];
223
- const record: Record<string, unknown> = {};
224
- columns.forEach((column, index) => {
225
- record[column] = row[index];
226
- });
227
- return [record];
228
- });
217
+ if (!Array.isArray(result.results) || !Array.isArray(result.columns)) {
218
+ return [];
219
+ }
220
+ const columns = result.columns;
221
+ return result.results.flatMap((row) => {
222
+ if (!Array.isArray(row)) return [];
223
+ const record: Record<string, unknown> = {};
224
+ columns.forEach((column, index) => {
225
+ record[column] = row[index];
226
+ });
227
+ return [record];
228
+ });
229
229
  }
230
230
 
231
231
  function asString(value: unknown): string | null {
232
- if (typeof value === 'string' && value.trim()) return value;
233
- if (typeof value === 'number') return String(value);
234
- return null;
232
+ if (typeof value === 'string' && value.trim()) return value;
233
+ if (typeof value === 'number') return String(value);
234
+ return null;
235
235
  }
236
236
 
237
237
  function asNumber(value: unknown): number {
238
- if (typeof value === 'number' && Number.isFinite(value)) return value;
239
- if (typeof value === 'string' && value.trim()) {
240
- const parsed = Number(value);
241
- if (Number.isFinite(parsed)) return parsed;
242
- }
243
- return 0;
238
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
239
+ if (typeof value === 'string' && value.trim()) {
240
+ const parsed = Number(value);
241
+ if (Number.isFinite(parsed)) return parsed;
242
+ }
243
+ return 0;
244
244
  }
245
245
 
246
246
  export function resolvePosthogEvidenceOptionsFromEnv(
247
- options: PosthogEvidenceEnvOptions = {}
247
+ options: PosthogEvidenceEnvOptions = {}
248
248
  ): PosthogEvidenceOptions | null {
249
- const projectId = process.env.POSTHOG_PROJECT_ID;
250
- const personalApiKey = process.env.POSTHOG_PERSONAL_API_KEY;
251
- if (!projectId || !personalApiKey) return null;
249
+ const projectId = process.env.POSTHOG_PROJECT_ID;
250
+ const personalApiKey = process.env.POSTHOG_PERSONAL_API_KEY;
251
+ if (!projectId || !personalApiKey) return null;
252
252
 
253
- const lookbackDays = resolveNumberEnv(
254
- 'POSTHOG_EVIDENCE_LOOKBACK_DAYS',
255
- options.defaultLookbackDays ?? 30
256
- );
257
- const limit = resolveNumberEnv(
258
- 'POSTHOG_EVIDENCE_LIMIT',
259
- options.defaultLimit ?? 10
260
- );
253
+ const lookbackDays = resolveNumberEnv(
254
+ 'POSTHOG_EVIDENCE_LOOKBACK_DAYS',
255
+ options.defaultLookbackDays ?? 30
256
+ );
257
+ const limit = resolveNumberEnv(
258
+ 'POSTHOG_EVIDENCE_LIMIT',
259
+ options.defaultLimit ?? 10
260
+ );
261
261
 
262
- const now = new Date();
263
- const from = new Date(now.getTime() - lookbackDays * 24 * 60 * 60 * 1000);
264
- const eventNames = resolveCsvEnv('POSTHOG_EVIDENCE_EVENTS');
265
- const funnelSteps = resolveCsvEnv('POSTHOG_EVIDENCE_FUNNEL_STEPS');
266
- const funnel =
267
- funnelSteps && funnelSteps.length
268
- ? {
269
- name: 'posthog-evidence-funnel',
270
- steps: funnelSteps.map((eventName, index) => ({
271
- id: `step_${index + 1}`,
272
- eventName,
273
- })),
274
- }
275
- : undefined;
262
+ const now = new Date();
263
+ const from = new Date(now.getTime() - lookbackDays * 24 * 60 * 60 * 1000);
264
+ const eventNames = resolveCsvEnv('POSTHOG_EVIDENCE_EVENTS');
265
+ const funnelSteps = resolveCsvEnv('POSTHOG_EVIDENCE_FUNNEL_STEPS');
266
+ const funnel =
267
+ funnelSteps && funnelSteps.length
268
+ ? {
269
+ name: 'posthog-evidence-funnel',
270
+ steps: funnelSteps.map((eventName, index) => ({
271
+ id: `step_${index + 1}`,
272
+ eventName,
273
+ })),
274
+ }
275
+ : undefined;
276
276
 
277
- const reader = new PosthogAnalyticsProvider({
278
- host: process.env.POSTHOG_HOST,
279
- projectId,
280
- personalApiKey,
281
- });
277
+ const reader = new PosthogAnalyticsProvider({
278
+ host: process.env.POSTHOG_HOST,
279
+ projectId,
280
+ personalApiKey,
281
+ });
282
282
 
283
- return {
284
- reader,
285
- dateRange: { from, to: now },
286
- eventNames,
287
- limit,
288
- funnel,
289
- includeFeatureFlags: resolveBooleanEnv(
290
- 'POSTHOG_EVIDENCE_FEATURE_FLAGS',
291
- true
292
- ),
293
- };
283
+ return {
284
+ reader,
285
+ dateRange: { from, to: now },
286
+ eventNames,
287
+ limit,
288
+ funnel,
289
+ includeFeatureFlags: resolveBooleanEnv(
290
+ 'POSTHOG_EVIDENCE_FEATURE_FLAGS',
291
+ true
292
+ ),
293
+ };
294
294
  }
295
295
 
296
296
  function resolveCsvEnv(key: string): string[] | undefined {
297
- const value = process.env[key];
298
- if (!value) return undefined;
299
- return value
300
- .split(',')
301
- .map((item) => item.trim())
302
- .filter(Boolean);
297
+ const value = process.env[key];
298
+ if (!value) return undefined;
299
+ return value
300
+ .split(',')
301
+ .map((item) => item.trim())
302
+ .filter(Boolean);
303
303
  }
304
304
 
305
305
  function resolveNumberEnv(key: string, fallback: number): number {
306
- const value = process.env[key];
307
- if (!value) return fallback;
308
- const parsed = Number(value);
309
- return Number.isFinite(parsed) ? parsed : fallback;
306
+ const value = process.env[key];
307
+ if (!value) return fallback;
308
+ const parsed = Number(value);
309
+ return Number.isFinite(parsed) ? parsed : fallback;
310
310
  }
311
311
 
312
312
  function resolveBooleanEnv(key: string, fallback: boolean): boolean {
313
- const value = process.env[key];
314
- if (value === undefined) return fallback;
315
- return value.toLowerCase() === 'true';
313
+ const value = process.env[key];
314
+ if (value === undefined) return fallback;
315
+ return value.toLowerCase() === 'true';
316
316
  }
@@ -1,19 +1,19 @@
1
1
  import { defineFeature } from '@contractspec/lib.contracts-spec';
2
2
 
3
3
  export const ProductIntentFeature = defineFeature({
4
- meta: {
5
- key: 'product-intent',
6
- version: '1.0.0',
7
- title: 'Product Intent',
8
- description:
9
- 'Evidence ingestion, PostHog signals, and transcript-to-tickets discovery workflow',
10
- domain: 'product',
11
- owners: ['@examples'],
12
- tags: ['product', 'intent', 'evidence', 'posthog'],
13
- stability: 'experimental',
14
- },
4
+ meta: {
5
+ key: 'product-intent',
6
+ version: '1.0.0',
7
+ title: 'Product Intent',
8
+ description:
9
+ 'Evidence ingestion, PostHog signals, and transcript-to-tickets discovery workflow',
10
+ domain: 'product',
11
+ owners: ['@examples'],
12
+ tags: ['product', 'intent', 'evidence', 'posthog'],
13
+ stability: 'experimental',
14
+ },
15
15
 
16
- telemetry: [{ key: 'product-intent.telemetry', version: '1.0.0' }],
16
+ telemetry: [{ key: 'product-intent.telemetry', version: '1.0.0' }],
17
17
 
18
- docs: [],
18
+ docs: [],
19
19
  });