@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.
- package/.turbo/turbo-build.log +2 -2
- package/AGENTS.md +44 -20
- package/README.md +60 -44
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/load-evidence.js +1 -1
- package/dist/node/index.js +1 -1
- package/dist/node/load-evidence.js +1 -1
- package/dist/node/posthog-signals.js +1 -1
- package/dist/node/script.js +1 -1
- package/dist/node/sync-actions.js +4 -4
- package/dist/posthog-signals.d.ts +1 -1
- package/dist/posthog-signals.js +1 -1
- package/dist/script.js +1 -1
- package/dist/sync-actions.js +4 -4
- package/package.json +9 -8
- package/src/docs/product-intent.docblock.ts +21 -21
- package/src/example.ts +26 -26
- package/src/index.ts +12 -12
- package/src/load-evidence.test.ts +6 -6
- package/src/load-evidence.ts +49 -49
- package/src/posthog-signals.ts +253 -253
- package/src/product-intent.feature.ts +13 -13
- package/src/script.ts +191 -191
- package/src/sync-actions.ts +185 -185
- package/tsconfig.json +7 -7
- package/tsdown.config.js +7 -13
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
117
|
+
logDir: string,
|
|
118
|
+
runId: string
|
|
119
119
|
): TicketPipelineLogger {
|
|
120
|
-
|
|
120
|
+
const tracePath = path.join(logDir, 'trace.jsonl');
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
fs.appendFileSync(tracePath, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
async function main() {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
234
|
+
if (!tickets[0]) {
|
|
235
|
+
console.log('\nNo tickets generated.');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
258
|
-
|
|
257
|
+
console.error(error);
|
|
258
|
+
process.exitCode = 1;
|
|
259
259
|
});
|