@contractspec/example.product-intent 3.7.5 → 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.
package/src/script.ts CHANGED
@@ -3,18 +3,18 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { createAgentJsonRunner } from '@contractspec/lib.ai-agent/agent/json-runner';
5
5
  import {
6
- extractEvidence,
7
- generateTickets,
8
- groupProblems,
9
- impactEngine,
10
- type RepoScanFile,
11
- suggestPatch,
6
+ extractEvidence,
7
+ generateTickets,
8
+ groupProblems,
9
+ impactEngine,
10
+ type RepoScanFile,
11
+ suggestPatch,
12
12
  } from '@contractspec/lib.product-intent-utils';
13
13
  import { loadEvidenceChunksWithSignals } from './load-evidence';
14
14
  import { resolvePosthogEvidenceOptionsFromEnv } from './posthog-signals';
15
15
 
16
16
  const QUESTION =
17
- 'Which activation and onboarding friction should we prioritize next?';
17
+ 'Which activation and onboarding friction should we prioritize next?';
18
18
  const DEFAULT_PROVIDER = 'openai';
19
19
  const DEFAULT_MODEL = 'gpt-5.2';
20
20
  const DEFAULT_TEMPERATURE = 0;
@@ -23,237 +23,237 @@ const DEFAULT_MAX_ATTEMPTS = 2;
23
23
  const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
24
24
  const REPO_ROOT = path.resolve(MODULE_DIR, '../../../..');
25
25
  const REPO_SCAN_FILES = [
26
- 'packages/examples/product-intent/src/load-evidence.ts',
27
- 'packages/examples/product-intent/src/script.ts',
28
- 'packages/libs/contracts-spec/src/product-intent/contract-patch-intent.ts',
29
- 'packages/libs/contracts-spec/src/product-intent/spec.ts',
30
- 'packages/libs/product-intent-utils/src/impact-engine.ts',
26
+ 'packages/examples/product-intent/src/load-evidence.ts',
27
+ 'packages/examples/product-intent/src/script.ts',
28
+ 'packages/libs/contracts-spec/src/product-intent/contract-patch-intent.ts',
29
+ 'packages/libs/contracts-spec/src/product-intent/spec.ts',
30
+ 'packages/libs/product-intent-utils/src/impact-engine.ts',
31
31
  ];
32
32
 
33
33
  function collectRepoFiles(root: string, files: string[]): RepoScanFile[] {
34
- const collected: RepoScanFile[] = [];
35
- for (const relativePath of files) {
36
- const fullPath = path.join(root, relativePath);
37
- if (!fs.existsSync(fullPath)) continue;
38
- const content = fs.readFileSync(fullPath, 'utf8');
39
- collected.push({ path: relativePath, content });
40
- }
41
- return collected;
34
+ const collected: RepoScanFile[] = [];
35
+ for (const relativePath of files) {
36
+ const fullPath = path.join(root, relativePath);
37
+ if (!fs.existsSync(fullPath)) continue;
38
+ const content = fs.readFileSync(fullPath, 'utf8');
39
+ collected.push({ path: relativePath, content });
40
+ }
41
+ return collected;
42
42
  }
43
43
 
44
44
  interface TicketPipelineLogger {
45
- log: (entry: {
46
- stage: string;
47
- phase: string;
48
- attempt: number;
49
- prompt: string;
50
- response?: string;
51
- error?: string;
52
- timestamp: string;
53
- }) => void | Promise<void>;
45
+ log: (entry: {
46
+ stage: string;
47
+ phase: string;
48
+ attempt: number;
49
+ prompt: string;
50
+ response?: string;
51
+ error?: string;
52
+ timestamp: string;
53
+ }) => void | Promise<void>;
54
54
  }
55
55
 
56
56
  type ProviderName = 'openai' | 'anthropic' | 'mistral' | 'gemini' | 'ollama';
57
57
 
58
58
  function resolveProviderName(): ProviderName {
59
- const raw =
60
- process.env.CONTRACTSPEC_AI_PROVIDER ??
61
- process.env.AI_PROVIDER ??
62
- DEFAULT_PROVIDER;
63
- const normalized = raw.toLowerCase();
64
- const allowed: ProviderName[] = [
65
- 'openai',
66
- 'anthropic',
67
- 'mistral',
68
- 'gemini',
69
- 'ollama',
70
- ];
71
- if (!allowed.includes(normalized as ProviderName)) {
72
- throw new Error(
73
- `Unsupported AI provider '${raw}'. Allowed: ${allowed.join(', ')}`
74
- );
75
- }
76
- return normalized as ProviderName;
59
+ const raw =
60
+ process.env.CONTRACTSPEC_AI_PROVIDER ??
61
+ process.env.AI_PROVIDER ??
62
+ DEFAULT_PROVIDER;
63
+ const normalized = raw.toLowerCase();
64
+ const allowed: ProviderName[] = [
65
+ 'openai',
66
+ 'anthropic',
67
+ 'mistral',
68
+ 'gemini',
69
+ 'ollama',
70
+ ];
71
+ if (!allowed.includes(normalized as ProviderName)) {
72
+ throw new Error(
73
+ `Unsupported AI provider '${raw}'. Allowed: ${allowed.join(', ')}`
74
+ );
75
+ }
76
+ return normalized as ProviderName;
77
77
  }
78
78
 
79
79
  function resolveApiKey(provider: ProviderName): string | undefined {
80
- switch (provider) {
81
- case 'openai':
82
- return process.env.OPENAI_API_KEY;
83
- case 'anthropic':
84
- return process.env.ANTHROPIC_API_KEY;
85
- case 'mistral':
86
- return process.env.MISTRAL_API_KEY;
87
- case 'gemini':
88
- return process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
89
- case 'ollama':
90
- return undefined;
91
- }
80
+ switch (provider) {
81
+ case 'openai':
82
+ return process.env.OPENAI_API_KEY;
83
+ case 'anthropic':
84
+ return process.env.ANTHROPIC_API_KEY;
85
+ case 'mistral':
86
+ return process.env.MISTRAL_API_KEY;
87
+ case 'gemini':
88
+ return process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
89
+ case 'ollama':
90
+ return undefined;
91
+ }
92
92
  }
93
93
 
94
94
  function resolveTemperature(): number {
95
- const raw =
96
- process.env.CONTRACTSPEC_AI_TEMPERATURE ?? process.env.AI_TEMPERATURE;
97
- if (!raw) return DEFAULT_TEMPERATURE;
98
- const value = Number.parseFloat(raw);
99
- return Number.isNaN(value) ? DEFAULT_TEMPERATURE : value;
95
+ const raw =
96
+ process.env.CONTRACTSPEC_AI_TEMPERATURE ?? process.env.AI_TEMPERATURE;
97
+ if (!raw) return DEFAULT_TEMPERATURE;
98
+ const value = Number.parseFloat(raw);
99
+ return Number.isNaN(value) ? DEFAULT_TEMPERATURE : value;
100
100
  }
101
101
 
102
102
  function resolveMaxAttempts(): number {
103
- const raw =
104
- process.env.CONTRACTSPEC_AI_MAX_ATTEMPTS ?? process.env.AI_MAX_ATTEMPTS;
105
- if (!raw) return DEFAULT_MAX_ATTEMPTS;
106
- const value = Number.parseInt(raw, 10);
107
- return Number.isNaN(value) ? DEFAULT_MAX_ATTEMPTS : Math.max(1, value);
103
+ const raw =
104
+ process.env.CONTRACTSPEC_AI_MAX_ATTEMPTS ?? process.env.AI_MAX_ATTEMPTS;
105
+ if (!raw) return DEFAULT_MAX_ATTEMPTS;
106
+ const value = Number.parseInt(raw, 10);
107
+ return Number.isNaN(value) ? DEFAULT_MAX_ATTEMPTS : Math.max(1, value);
108
108
  }
109
109
 
110
110
  function writeArtifact(dir: string, name: string, contents: string): string {
111
- const filePath = path.join(dir, name);
112
- fs.writeFileSync(filePath, contents, 'utf8');
113
- return filePath;
111
+ const filePath = path.join(dir, name);
112
+ fs.writeFileSync(filePath, contents, 'utf8');
113
+ return filePath;
114
114
  }
115
115
 
116
116
  function createPipelineLogger(
117
- logDir: string,
118
- runId: string
117
+ logDir: string,
118
+ runId: string
119
119
  ): TicketPipelineLogger {
120
- const tracePath = path.join(logDir, 'trace.jsonl');
120
+ const tracePath = path.join(logDir, 'trace.jsonl');
121
121
 
122
- return {
123
- log(entry) {
124
- const baseName = `${entry.stage}-attempt-${entry.attempt}-${entry.phase}`;
125
- const payload: Record<string, unknown> = {
126
- runId,
127
- stage: entry.stage,
128
- phase: entry.phase,
129
- attempt: entry.attempt,
130
- timestamp: entry.timestamp,
131
- };
122
+ return {
123
+ log(entry) {
124
+ const baseName = `${entry.stage}-attempt-${entry.attempt}-${entry.phase}`;
125
+ const payload: Record<string, unknown> = {
126
+ runId,
127
+ stage: entry.stage,
128
+ phase: entry.phase,
129
+ attempt: entry.attempt,
130
+ timestamp: entry.timestamp,
131
+ };
132
132
 
133
- if (entry.prompt) {
134
- payload.promptPath = path.relative(
135
- REPO_ROOT,
136
- writeArtifact(logDir, `${baseName}.prompt.txt`, entry.prompt)
137
- );
138
- }
133
+ if (entry.prompt) {
134
+ payload.promptPath = path.relative(
135
+ REPO_ROOT,
136
+ writeArtifact(logDir, `${baseName}.prompt.txt`, entry.prompt)
137
+ );
138
+ }
139
139
 
140
- if (entry.response) {
141
- payload.responsePath = path.relative(
142
- REPO_ROOT,
143
- writeArtifact(logDir, `${baseName}.response.txt`, entry.response)
144
- );
145
- }
140
+ if (entry.response) {
141
+ payload.responsePath = path.relative(
142
+ REPO_ROOT,
143
+ writeArtifact(logDir, `${baseName}.response.txt`, entry.response)
144
+ );
145
+ }
146
146
 
147
- if (entry.error) {
148
- payload.errorPath = path.relative(
149
- REPO_ROOT,
150
- writeArtifact(logDir, `${baseName}.error.txt`, entry.error)
151
- );
152
- }
147
+ if (entry.error) {
148
+ payload.errorPath = path.relative(
149
+ REPO_ROOT,
150
+ writeArtifact(logDir, `${baseName}.error.txt`, entry.error)
151
+ );
152
+ }
153
153
 
154
- fs.appendFileSync(tracePath, `${JSON.stringify(payload)}\n`, 'utf8');
155
- },
156
- };
154
+ fs.appendFileSync(tracePath, `${JSON.stringify(payload)}\n`, 'utf8');
155
+ },
156
+ };
157
157
  }
158
158
 
159
159
  async function main() {
160
- const provider = resolveProviderName();
161
- const temperature = resolveTemperature();
162
- const maxAttempts = resolveMaxAttempts();
163
- const apiKey = resolveApiKey(provider);
164
- const proxyUrl = process.env.CONTRACTSPEC_AI_PROXY_URL;
165
- const organizationId = process.env.CONTRACTSPEC_ORG_ID;
166
- const baseUrl = process.env.OLLAMA_BASE_URL;
167
- const model =
168
- process.env.CONTRACTSPEC_AI_MODEL ??
169
- process.env.AI_MODEL ??
170
- (provider === 'mistral' ? DEFAULT_MODEL : undefined);
160
+ const provider = resolveProviderName();
161
+ const temperature = resolveTemperature();
162
+ const maxAttempts = resolveMaxAttempts();
163
+ const apiKey = resolveApiKey(provider);
164
+ const proxyUrl = process.env.CONTRACTSPEC_AI_PROXY_URL;
165
+ const organizationId = process.env.CONTRACTSPEC_ORG_ID;
166
+ const baseUrl = process.env.OLLAMA_BASE_URL;
167
+ const model =
168
+ process.env.CONTRACTSPEC_AI_MODEL ??
169
+ process.env.AI_MODEL ??
170
+ (provider === 'mistral' ? DEFAULT_MODEL : undefined);
171
171
 
172
- if (provider !== 'ollama' && !apiKey && !proxyUrl && !organizationId) {
173
- throw new Error(
174
- `Missing API credentials for ${provider}. Set the provider API key or CONTRACTSPEC_AI_PROXY_URL.`
175
- );
176
- }
172
+ if (provider !== 'ollama' && !apiKey && !proxyUrl && !organizationId) {
173
+ throw new Error(
174
+ `Missing API credentials for ${provider}. Set the provider API key or CONTRACTSPEC_AI_PROXY_URL.`
175
+ );
176
+ }
177
177
 
178
- const runId = new Date().toISOString().replace(/[:.]/g, '-');
179
- const logDir = path.join(MODULE_DIR, '../logs', `run-${runId}`);
180
- fs.mkdirSync(logDir, { recursive: true });
181
- const logger = createPipelineLogger(logDir, runId);
178
+ const runId = new Date().toISOString().replace(/[:.]/g, '-');
179
+ const logDir = path.join(MODULE_DIR, '../logs', `run-${runId}`);
180
+ fs.mkdirSync(logDir, { recursive: true });
181
+ const logger = createPipelineLogger(logDir, runId);
182
182
 
183
- const modelRunner = await createAgentJsonRunner({
184
- provider: {
185
- provider,
186
- model,
187
- apiKey,
188
- baseUrl,
189
- proxyUrl,
190
- organizationId,
191
- },
192
- temperature,
193
- system:
194
- 'You are a product discovery analyst. Respond with strict JSON only and use exact quotes for citations.',
195
- });
183
+ const modelRunner = await createAgentJsonRunner({
184
+ provider: {
185
+ provider,
186
+ model,
187
+ apiKey,
188
+ baseUrl,
189
+ proxyUrl,
190
+ organizationId,
191
+ },
192
+ temperature,
193
+ system:
194
+ 'You are a product discovery analyst. Respond with strict JSON only and use exact quotes for citations.',
195
+ });
196
196
 
197
- console.log(`AI provider: ${provider}`);
198
- console.log(`Model: ${model ?? '(provider default)'}`);
199
- console.log(`Temperature: ${temperature}`);
200
- console.log(`Max attempts: ${maxAttempts}`);
201
- console.log(`Trace log: ${path.relative(REPO_ROOT, logDir)}/trace.jsonl`);
197
+ console.log(`AI provider: ${provider}`);
198
+ console.log(`Model: ${model ?? '(provider default)'}`);
199
+ console.log(`Temperature: ${temperature}`);
200
+ console.log(`Max attempts: ${maxAttempts}`);
201
+ console.log(`Trace log: ${path.relative(REPO_ROOT, logDir)}/trace.jsonl`);
202
202
 
203
- const posthogEvidence = resolvePosthogEvidenceOptionsFromEnv();
204
- const evidenceChunks = await loadEvidenceChunksWithSignals({
205
- posthog: posthogEvidence ?? undefined,
206
- });
207
- console.log(`Loaded ${evidenceChunks.length} evidence chunks`);
203
+ const posthogEvidence = resolvePosthogEvidenceOptionsFromEnv();
204
+ const evidenceChunks = await loadEvidenceChunksWithSignals({
205
+ posthog: posthogEvidence ?? undefined,
206
+ });
207
+ console.log(`Loaded ${evidenceChunks.length} evidence chunks`);
208
208
 
209
- const findings = await extractEvidence(evidenceChunks, QUESTION, {
210
- maxFindings: 12,
211
- modelRunner,
212
- logger,
213
- maxAttempts,
214
- });
215
- console.log('\nEvidence findings:\n');
216
- console.log(JSON.stringify(findings, null, 2));
209
+ const findings = await extractEvidence(evidenceChunks, QUESTION, {
210
+ maxFindings: 12,
211
+ modelRunner,
212
+ logger,
213
+ maxAttempts,
214
+ });
215
+ console.log('\nEvidence findings:\n');
216
+ console.log(JSON.stringify(findings, null, 2));
217
217
 
218
- const problems = await groupProblems(findings, QUESTION, {
219
- modelRunner,
220
- logger,
221
- maxAttempts,
222
- });
223
- console.log('\nProblems:\n');
224
- console.log(JSON.stringify(problems, null, 2));
218
+ const problems = await groupProblems(findings, QUESTION, {
219
+ modelRunner,
220
+ logger,
221
+ maxAttempts,
222
+ });
223
+ console.log('\nProblems:\n');
224
+ console.log(JSON.stringify(problems, null, 2));
225
225
 
226
- const tickets = await generateTickets(problems, findings, QUESTION, {
227
- modelRunner,
228
- logger,
229
- maxAttempts,
230
- });
231
- console.log('\nTickets:\n');
232
- console.log(JSON.stringify(tickets, null, 2));
226
+ const tickets = await generateTickets(problems, findings, QUESTION, {
227
+ modelRunner,
228
+ logger,
229
+ maxAttempts,
230
+ });
231
+ console.log('\nTickets:\n');
232
+ console.log(JSON.stringify(tickets, null, 2));
233
233
 
234
- if (!tickets[0]) {
235
- console.log('\nNo tickets generated.');
236
- return;
237
- }
234
+ if (!tickets[0]) {
235
+ console.log('\nNo tickets generated.');
236
+ return;
237
+ }
238
238
 
239
- const patchIntent = await suggestPatch(tickets[0], {
240
- modelRunner,
241
- logger,
242
- maxAttempts,
243
- });
244
- console.log('\nPatch intent:\n');
245
- console.log(JSON.stringify(patchIntent, null, 2));
239
+ const patchIntent = await suggestPatch(tickets[0], {
240
+ modelRunner,
241
+ logger,
242
+ maxAttempts,
243
+ });
244
+ console.log('\nPatch intent:\n');
245
+ console.log(JSON.stringify(patchIntent, null, 2));
246
246
 
247
- const repoFiles = collectRepoFiles(REPO_ROOT, REPO_SCAN_FILES);
248
- const impact = impactEngine(patchIntent, {
249
- repoFiles,
250
- maxHitsPerChange: 3,
251
- });
252
- console.log('\nImpact report (deterministic):\n');
253
- console.log(JSON.stringify(impact, null, 2));
247
+ const repoFiles = collectRepoFiles(REPO_ROOT, REPO_SCAN_FILES);
248
+ const impact = impactEngine(patchIntent, {
249
+ repoFiles,
250
+ maxHitsPerChange: 3,
251
+ });
252
+ console.log('\nImpact report (deterministic):\n');
253
+ console.log(JSON.stringify(impact, null, 2));
254
254
  }
255
255
 
256
256
  main().catch((error) => {
257
- console.error(error);
258
- process.exitCode = 1;
257
+ console.error(error);
258
+ process.exitCode = 1;
259
259
  });