@contractspec/lib.product-intent-utils 1.57.0 → 1.59.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.
- package/dist/browser/index.js +1592 -0
- package/dist/impact-engine.d.ts +10 -14
- package/dist/impact-engine.d.ts.map +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1593 -10
- package/dist/node/index.js +1592 -0
- package/dist/project-management-sync.d.ts +17 -21
- package/dist/project-management-sync.d.ts.map +1 -1
- package/dist/prompts.d.ts +41 -45
- package/dist/prompts.d.ts.map +1 -1
- package/dist/ticket-pipeline-runner.d.ts +22 -25
- package/dist/ticket-pipeline-runner.d.ts.map +1 -1
- package/dist/ticket-pipeline.d.ts +29 -32
- package/dist/ticket-pipeline.d.ts.map +1 -1
- package/dist/ticket-prompts.d.ts +13 -16
- package/dist/ticket-prompts.d.ts.map +1 -1
- package/dist/ticket-validators.d.ts +4 -8
- package/dist/ticket-validators.d.ts.map +1 -1
- package/dist/validators.d.ts +24 -28
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.test.d.ts +2 -0
- package/dist/validators.test.d.ts.map +1 -0
- package/package.json +20 -15
- package/dist/impact-engine.js +0 -168
- package/dist/impact-engine.js.map +0 -1
- package/dist/project-management-sync.js +0 -80
- package/dist/project-management-sync.js.map +0 -1
- package/dist/prompts.js +0 -372
- package/dist/prompts.js.map +0 -1
- package/dist/schema/dist/SchemaModelType.d.ts +0 -50
- package/dist/schema/dist/SchemaModelType.d.ts.map +0 -1
- package/dist/ticket-pipeline-runner.js +0 -97
- package/dist/ticket-pipeline-runner.js.map +0 -1
- package/dist/ticket-pipeline.js +0 -425
- package/dist/ticket-pipeline.js.map +0 -1
- package/dist/ticket-prompts.js +0 -131
- package/dist/ticket-prompts.js.map +0 -1
- package/dist/ticket-validators.js +0 -106
- package/dist/ticket-validators.js.map +0 -1
- package/dist/validators.js +0 -277
- package/dist/validators.js.map +0 -1
|
@@ -0,0 +1,1592 @@
|
|
|
1
|
+
// src/prompts.ts
|
|
2
|
+
function formatEvidenceForModel(chunks, maxChars = 900) {
|
|
3
|
+
const safe = chunks.map((chunk) => ({
|
|
4
|
+
chunkId: chunk.chunkId,
|
|
5
|
+
text: chunk.text.length > maxChars ? `${chunk.text.slice(0, maxChars)}...` : chunk.text,
|
|
6
|
+
meta: chunk.meta ?? {}
|
|
7
|
+
}));
|
|
8
|
+
return JSON.stringify({ evidenceChunks: safe }, null, 2);
|
|
9
|
+
}
|
|
10
|
+
var JSON_ONLY_RULES = `
|
|
11
|
+
You MUST output valid JSON ONLY.
|
|
12
|
+
- Do not wrap in markdown fences.
|
|
13
|
+
- Do not include any commentary.
|
|
14
|
+
- Do not include trailing commas.
|
|
15
|
+
- Use double quotes for all keys and string values.
|
|
16
|
+
`;
|
|
17
|
+
var CITATION_RULES = `
|
|
18
|
+
CITATION RULES (strict):
|
|
19
|
+
- You may ONLY cite from the provided evidenceChunks.
|
|
20
|
+
- Each citation must include:
|
|
21
|
+
- "chunkId": exactly one of the provided chunkId values
|
|
22
|
+
- "quote": an exact substring copied from that chunk's text
|
|
23
|
+
- Do NOT invent quotes.
|
|
24
|
+
- Keep quotes short (<= 240 chars).
|
|
25
|
+
- If you cannot support a claim with evidence, do not make the claim.
|
|
26
|
+
`;
|
|
27
|
+
function promptExtractInsights(params) {
|
|
28
|
+
return `
|
|
29
|
+
You are extracting ATOMIC, EVIDENCE-GROUNDED insights to answer a product discovery question.
|
|
30
|
+
|
|
31
|
+
Question:
|
|
32
|
+
${params.question}
|
|
33
|
+
|
|
34
|
+
Evidence:
|
|
35
|
+
${params.evidenceJSON}
|
|
36
|
+
|
|
37
|
+
Task:
|
|
38
|
+
Return JSON with:
|
|
39
|
+
{
|
|
40
|
+
"insights": [
|
|
41
|
+
{
|
|
42
|
+
"insightId": "ins_001",
|
|
43
|
+
"claim": "...",
|
|
44
|
+
"tags": ["..."],
|
|
45
|
+
"segment": "...",
|
|
46
|
+
"confidence": 0.0,
|
|
47
|
+
"citations": [{ "chunkId": "...", "quote": "..." }]
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Guidelines:
|
|
53
|
+
- Produce 8 to 16 insights.
|
|
54
|
+
- Each insight must be supported by 1 to 3 citations.
|
|
55
|
+
- Prefer user pain, blockers, confusions, workarounds, requests, and measurable outcomes.
|
|
56
|
+
- If evidence conflicts, include both sides as separate insights.
|
|
57
|
+
${CITATION_RULES}
|
|
58
|
+
${JSON_ONLY_RULES}
|
|
59
|
+
`.trim();
|
|
60
|
+
}
|
|
61
|
+
function promptSynthesizeBrief(params) {
|
|
62
|
+
const allowed = JSON.stringify({ allowedChunkIds: params.allowedChunkIds }, null, 2);
|
|
63
|
+
return `
|
|
64
|
+
You are synthesizing a product opportunity brief that is STRICTLY grounded in evidence.
|
|
65
|
+
|
|
66
|
+
Question:
|
|
67
|
+
${params.question}
|
|
68
|
+
|
|
69
|
+
Extracted insights (already grounded):
|
|
70
|
+
${params.insightsJSON}
|
|
71
|
+
|
|
72
|
+
Allowed citations:
|
|
73
|
+
${allowed}
|
|
74
|
+
|
|
75
|
+
Return JSON with exactly this shape:
|
|
76
|
+
{
|
|
77
|
+
"opportunityId": "opp_001",
|
|
78
|
+
"title": "short title",
|
|
79
|
+
"problem": { "text": "...", "citations": [ { "chunkId": "...", "quote": "..." } ] },
|
|
80
|
+
"who": { "text": "...", "citations": [ ... ] },
|
|
81
|
+
"proposedChange": { "text": "...", "citations": [ ... ] },
|
|
82
|
+
"expectedImpact": { "metric": "activation_rate", "direction": "up", "magnitudeHint": "...", "timeframeHint": "..." },
|
|
83
|
+
"confidence": "low|medium|high",
|
|
84
|
+
"risks": [ { "text": "...", "citations": [ ... ] } ]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
Rules:
|
|
88
|
+
- The fields problem/who/proposedChange MUST each have >=1 citation.
|
|
89
|
+
- All citations must use allowedChunkIds and include exact quotes.
|
|
90
|
+
- Keep the brief concise and specific.
|
|
91
|
+
${CITATION_RULES}
|
|
92
|
+
${JSON_ONLY_RULES}
|
|
93
|
+
`.trim();
|
|
94
|
+
}
|
|
95
|
+
function promptSkepticCheck(params) {
|
|
96
|
+
return `
|
|
97
|
+
You are auditing a brief for unsupported claims and citation misuse.
|
|
98
|
+
|
|
99
|
+
Brief:
|
|
100
|
+
${params.briefJSON}
|
|
101
|
+
|
|
102
|
+
Evidence:
|
|
103
|
+
${params.evidenceJSON}
|
|
104
|
+
|
|
105
|
+
Return JSON:
|
|
106
|
+
{
|
|
107
|
+
"issues": [
|
|
108
|
+
{
|
|
109
|
+
"path": "problem|who|proposedChange|risks[i]|...",
|
|
110
|
+
"reason": "unsupported|quote_not_exact|wrong_chunk|too_vague|overreach",
|
|
111
|
+
"fix": "what to change"
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Rules:
|
|
117
|
+
- If everything is supported, return {"issues": []}.
|
|
118
|
+
- Be strict. If a statement is not clearly supported by citations, flag it.
|
|
119
|
+
${JSON_ONLY_RULES}
|
|
120
|
+
`.trim();
|
|
121
|
+
}
|
|
122
|
+
function promptGeneratePatchIntent(params) {
|
|
123
|
+
return `
|
|
124
|
+
You are generating a ContractPatchIntent from an OpportunityBrief.
|
|
125
|
+
|
|
126
|
+
OpportunityBrief:
|
|
127
|
+
${params.briefJSON}
|
|
128
|
+
|
|
129
|
+
Return JSON:
|
|
130
|
+
{
|
|
131
|
+
"featureKey": "activation_onboarding",
|
|
132
|
+
"changes": [
|
|
133
|
+
{ "type": "add_event|update_form|update_operation|add_field|...", "target": "string", "detail": "string" }
|
|
134
|
+
],
|
|
135
|
+
"acceptanceCriteria": ["..."]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
Rules:
|
|
139
|
+
- Keep changes <= 12.
|
|
140
|
+
- Detail should be minimal and explicit.
|
|
141
|
+
- Acceptance criteria must be testable and verifiable.
|
|
142
|
+
${JSON_ONLY_RULES}
|
|
143
|
+
`.trim();
|
|
144
|
+
}
|
|
145
|
+
function promptGenerateGenericSpecOverlay(params) {
|
|
146
|
+
return `
|
|
147
|
+
You are generating a GENERIC spec overlay patch based on PatchIntent.
|
|
148
|
+
You must respect the base spec snippet.
|
|
149
|
+
|
|
150
|
+
Base spec snippet (context):
|
|
151
|
+
${params.baseSpecSnippet}
|
|
152
|
+
|
|
153
|
+
PatchIntent:
|
|
154
|
+
${params.patchIntentJSON}
|
|
155
|
+
|
|
156
|
+
Return JSON:
|
|
157
|
+
{
|
|
158
|
+
"overlay": {
|
|
159
|
+
"adds": [ { "path": "dot.path", "value": {} } ],
|
|
160
|
+
"updates": [ { "path": "dot.path", "value": {} } ],
|
|
161
|
+
"removes": [ { "path": "dot.path" } ]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
Rules:
|
|
166
|
+
- Only reference paths that plausibly exist in the base spec snippet or add new ones under reasonable roots.
|
|
167
|
+
- Keep values small. Avoid massive blobs.
|
|
168
|
+
${JSON_ONLY_RULES}
|
|
169
|
+
`.trim();
|
|
170
|
+
}
|
|
171
|
+
function promptGenerateImpactReport(params) {
|
|
172
|
+
return `
|
|
173
|
+
You are generating an Impact Report for a spec patch.
|
|
174
|
+
|
|
175
|
+
PatchIntent:
|
|
176
|
+
${params.patchIntentJSON}
|
|
177
|
+
|
|
178
|
+
Overlay:
|
|
179
|
+
${params.overlayJSON}
|
|
180
|
+
|
|
181
|
+
Compiler output (if present):
|
|
182
|
+
${params.compilerOutputText ?? "(none)"}
|
|
183
|
+
|
|
184
|
+
Return JSON:
|
|
185
|
+
{
|
|
186
|
+
"reportId": "impact_001",
|
|
187
|
+
"patchId": "patch_001",
|
|
188
|
+
"summary": "...",
|
|
189
|
+
"breaks": ["..."],
|
|
190
|
+
"mustChange": ["..."],
|
|
191
|
+
"risky": ["..."],
|
|
192
|
+
"surfaces": {
|
|
193
|
+
"api": ["..."],
|
|
194
|
+
"db": ["..."],
|
|
195
|
+
"ui": ["..."],
|
|
196
|
+
"workflows": ["..."],
|
|
197
|
+
"policy": ["..."],
|
|
198
|
+
"docs": ["..."],
|
|
199
|
+
"tests": ["..."]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
Rules:
|
|
204
|
+
- Be concrete: name what changes and why.
|
|
205
|
+
- If unsure, put it under "risky" not "breaks".
|
|
206
|
+
- Keep each item short.
|
|
207
|
+
${JSON_ONLY_RULES}
|
|
208
|
+
`.trim();
|
|
209
|
+
}
|
|
210
|
+
function promptGenerateTaskPack(params) {
|
|
211
|
+
return `
|
|
212
|
+
You are generating an agent-ready Task Pack to implement a product change safely.
|
|
213
|
+
|
|
214
|
+
Repo context:
|
|
215
|
+
${params.repoContext ?? "(none)"}
|
|
216
|
+
|
|
217
|
+
OpportunityBrief:
|
|
218
|
+
${params.briefJSON}
|
|
219
|
+
|
|
220
|
+
PatchIntent:
|
|
221
|
+
${params.patchIntentJSON}
|
|
222
|
+
|
|
223
|
+
Impact report:
|
|
224
|
+
${params.impactJSON}
|
|
225
|
+
|
|
226
|
+
Return JSON:
|
|
227
|
+
{
|
|
228
|
+
"packId": "tasks_001",
|
|
229
|
+
"patchId": "patch_001",
|
|
230
|
+
"overview": "...",
|
|
231
|
+
"tasks": [
|
|
232
|
+
{
|
|
233
|
+
"id": "t1",
|
|
234
|
+
"title": "...",
|
|
235
|
+
"surface": ["api", "db", "ui", "tests"],
|
|
236
|
+
"why": "...",
|
|
237
|
+
"acceptance": ["..."],
|
|
238
|
+
"agentPrompt": "...",
|
|
239
|
+
"dependsOn": []
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
Rules:
|
|
245
|
+
- 6 to 12 tasks.
|
|
246
|
+
- Each task must have testable acceptance criteria.
|
|
247
|
+
- Agent prompts must be copy-paste friendly and mention expected files/surfaces.
|
|
248
|
+
- Include at least one tests task.
|
|
249
|
+
${JSON_ONLY_RULES}
|
|
250
|
+
`.trim();
|
|
251
|
+
}
|
|
252
|
+
function promptWireframeImage(params) {
|
|
253
|
+
return `
|
|
254
|
+
Create a minimal grayscale wireframe (${params.device}) for screen: "${params.screenName}".
|
|
255
|
+
|
|
256
|
+
Style rules:
|
|
257
|
+
- Wireframe only, grayscale, no brand colors, no gradients
|
|
258
|
+
- Simple boxes, labels, and spacing
|
|
259
|
+
- High readability, minimal detail, like a product spec wireframe
|
|
260
|
+
- No decorative illustrations
|
|
261
|
+
|
|
262
|
+
Current screen summary:
|
|
263
|
+
${params.currentScreenSummary}
|
|
264
|
+
|
|
265
|
+
Proposed changes (must be reflected in the wireframe):
|
|
266
|
+
- ${params.proposedChanges.join(`
|
|
267
|
+
- `)}
|
|
268
|
+
|
|
269
|
+
Output: a single wireframe image that clearly shows the updated layout.
|
|
270
|
+
`.trim();
|
|
271
|
+
}
|
|
272
|
+
function promptWireframeLayoutJSON(params) {
|
|
273
|
+
return `
|
|
274
|
+
You are generating a simple UI wireframe layout JSON (NOT an image).
|
|
275
|
+
Screen: "${params.screenName}" (${params.device})
|
|
276
|
+
|
|
277
|
+
Proposed changes:
|
|
278
|
+
- ${params.proposedChanges.join(`
|
|
279
|
+
- `)}
|
|
280
|
+
|
|
281
|
+
Return JSON:
|
|
282
|
+
{
|
|
283
|
+
"layout": [
|
|
284
|
+
{ "type": "header", "label": "..." },
|
|
285
|
+
{ "type": "text", "label": "..." },
|
|
286
|
+
{ "type": "input", "label": "..." },
|
|
287
|
+
{ "type": "button", "label": "..." }
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
Rules:
|
|
292
|
+
- 8 to 18 elements.
|
|
293
|
+
- Must reflect proposed changes.
|
|
294
|
+
- Labels should be clear and specific.
|
|
295
|
+
${JSON_ONLY_RULES}
|
|
296
|
+
`.trim();
|
|
297
|
+
}
|
|
298
|
+
function promptGenerateSyntheticInterviews(params) {
|
|
299
|
+
return `
|
|
300
|
+
Generate ${params.count} synthetic customer interview transcripts for this product context:
|
|
301
|
+
${params.productContext}
|
|
302
|
+
|
|
303
|
+
Personas to cover:
|
|
304
|
+
- ${params.personas.join(`
|
|
305
|
+
- `)}
|
|
306
|
+
|
|
307
|
+
Requirements:
|
|
308
|
+
- Each transcript 400-900 words
|
|
309
|
+
- Include realistic friction, objections, and competing priorities
|
|
310
|
+
- Include at least 6 quotable lines per transcript (short, direct)
|
|
311
|
+
- Include at least one conflicting opinion across the set
|
|
312
|
+
- Format as plain text with speaker labels (INTERVIEWER:, CUSTOMER:)
|
|
313
|
+
|
|
314
|
+
Return JSON:
|
|
315
|
+
{
|
|
316
|
+
"items": [
|
|
317
|
+
{ "title": "...", "persona": "...", "segment": "...", "transcript": "..." }
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
${JSON_ONLY_RULES}
|
|
322
|
+
`.trim();
|
|
323
|
+
}
|
|
324
|
+
// src/ticket-prompts.ts
|
|
325
|
+
function promptExtractEvidenceFindings(params) {
|
|
326
|
+
return `
|
|
327
|
+
You are extracting evidence findings grounded in transcript excerpts.
|
|
328
|
+
|
|
329
|
+
Question:
|
|
330
|
+
${params.question}
|
|
331
|
+
|
|
332
|
+
Evidence:
|
|
333
|
+
${params.evidenceJSON}
|
|
334
|
+
|
|
335
|
+
Return JSON:
|
|
336
|
+
{
|
|
337
|
+
"findings": [
|
|
338
|
+
{
|
|
339
|
+
"findingId": "find_001",
|
|
340
|
+
"summary": "...",
|
|
341
|
+
"tags": ["..."],
|
|
342
|
+
"citations": [{ "chunkId": "...", "quote": "..." }]
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
Rules:
|
|
348
|
+
- Produce 8 to 18 findings.
|
|
349
|
+
- Each finding must include at least 1 citation.
|
|
350
|
+
- Summaries must be specific and short.
|
|
351
|
+
- Quotes must be copied character-for-character from the chunk text (no paraphrasing, no ellipses).
|
|
352
|
+
- Preserve punctuation, smart quotes, and special hyphens exactly as shown in the chunk text.
|
|
353
|
+
${CITATION_RULES}
|
|
354
|
+
${JSON_ONLY_RULES}
|
|
355
|
+
`.trim();
|
|
356
|
+
}
|
|
357
|
+
function promptGroupProblems(params) {
|
|
358
|
+
const allowed = JSON.stringify({ findingIds: params.findingIds }, null, 2);
|
|
359
|
+
return `
|
|
360
|
+
You are grouping evidence findings into problem statements.
|
|
361
|
+
|
|
362
|
+
Question:
|
|
363
|
+
${params.question}
|
|
364
|
+
|
|
365
|
+
Findings:
|
|
366
|
+
${params.findingsJSON}
|
|
367
|
+
|
|
368
|
+
Allowed finding IDs:
|
|
369
|
+
${allowed}
|
|
370
|
+
|
|
371
|
+
Return JSON:
|
|
372
|
+
{
|
|
373
|
+
"problems": [
|
|
374
|
+
{
|
|
375
|
+
"problemId": "prob_001",
|
|
376
|
+
"statement": "...",
|
|
377
|
+
"evidenceIds": ["find_001"],
|
|
378
|
+
"tags": ["..."],
|
|
379
|
+
"severity": "low|medium|high"
|
|
380
|
+
}
|
|
381
|
+
]
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
Rules:
|
|
385
|
+
- Each problem must reference 1 to 6 evidenceIds.
|
|
386
|
+
- evidenceIds must be drawn from the allowed finding IDs.
|
|
387
|
+
- Keep statements short and actionable.
|
|
388
|
+
${JSON_ONLY_RULES}
|
|
389
|
+
`.trim();
|
|
390
|
+
}
|
|
391
|
+
function promptGenerateTickets(params) {
|
|
392
|
+
return `
|
|
393
|
+
You are generating implementation tickets grounded in evidence.
|
|
394
|
+
|
|
395
|
+
Question:
|
|
396
|
+
${params.question}
|
|
397
|
+
|
|
398
|
+
Problems:
|
|
399
|
+
${params.problemsJSON}
|
|
400
|
+
|
|
401
|
+
Evidence findings:
|
|
402
|
+
${params.findingsJSON}
|
|
403
|
+
|
|
404
|
+
Return JSON:
|
|
405
|
+
{
|
|
406
|
+
"tickets": [
|
|
407
|
+
{
|
|
408
|
+
"ticketId": "t_001",
|
|
409
|
+
"title": "...",
|
|
410
|
+
"summary": "...",
|
|
411
|
+
"evidenceIds": ["find_001"],
|
|
412
|
+
"acceptanceCriteria": ["..."]
|
|
413
|
+
}
|
|
414
|
+
]
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
Rules:
|
|
418
|
+
- 1 to 2 tickets per problem.
|
|
419
|
+
- Every ticket must include evidenceIds and acceptanceCriteria.
|
|
420
|
+
- Acceptance criteria must be testable.
|
|
421
|
+
- Each acceptanceCriteria item must be <= 160 characters.
|
|
422
|
+
${JSON_ONLY_RULES}
|
|
423
|
+
`.trim();
|
|
424
|
+
}
|
|
425
|
+
function promptSuggestPatchIntent(params) {
|
|
426
|
+
return `
|
|
427
|
+
You are generating a ContractPatchIntent from an evidence-backed ticket.
|
|
428
|
+
|
|
429
|
+
Ticket:
|
|
430
|
+
${params.ticketJSON}
|
|
431
|
+
|
|
432
|
+
Return JSON:
|
|
433
|
+
{
|
|
434
|
+
"featureKey": "feature_slug",
|
|
435
|
+
"changes": [
|
|
436
|
+
{ "type": "add_field|remove_field|rename_field|add_event|update_event|add_operation|update_operation|update_form|update_policy|add_enum_value|remove_enum_value|other", "target": "string", "detail": "string" }
|
|
437
|
+
],
|
|
438
|
+
"acceptanceCriteria": ["..."]
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
Rules:
|
|
442
|
+
- Keep changes <= 8.
|
|
443
|
+
- Each change must be concrete and scoped.
|
|
444
|
+
- Acceptance criteria must be testable and derived from the ticket.
|
|
445
|
+
- Each acceptanceCriteria item must be <= 140 characters.
|
|
446
|
+
${JSON_ONLY_RULES}
|
|
447
|
+
`.trim();
|
|
448
|
+
}
|
|
449
|
+
// src/validators.ts
|
|
450
|
+
import {
|
|
451
|
+
CitationModel,
|
|
452
|
+
ContractPatchIntentModel,
|
|
453
|
+
ImpactReportModel,
|
|
454
|
+
InsightExtractionModel,
|
|
455
|
+
OpportunityBriefModel,
|
|
456
|
+
TaskPackModel
|
|
457
|
+
} from "@contractspec/lib.contracts/product-intent/types";
|
|
458
|
+
function assertStringLength(value, path, bounds) {
|
|
459
|
+
if (bounds.min !== undefined && value.length < bounds.min) {
|
|
460
|
+
throw new Error(`Expected ${path} to be at least ${bounds.min} characters, got ${value.length}`);
|
|
461
|
+
}
|
|
462
|
+
if (bounds.max !== undefined && value.length > bounds.max) {
|
|
463
|
+
throw new Error(`Expected ${path} to be at most ${bounds.max} characters, got ${value.length}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function assertArrayLength(value, path, bounds) {
|
|
467
|
+
if (bounds.min !== undefined && value.length < bounds.min) {
|
|
468
|
+
throw new Error(`Expected ${path} to have at least ${bounds.min} items, got ${value.length}`);
|
|
469
|
+
}
|
|
470
|
+
if (bounds.max !== undefined && value.length > bounds.max) {
|
|
471
|
+
throw new Error(`Expected ${path} to have at most ${bounds.max} items, got ${value.length}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function assertNumberRange(value, path, bounds) {
|
|
475
|
+
if (bounds.min !== undefined && value < bounds.min) {
|
|
476
|
+
throw new Error(`Expected ${path} to be >= ${bounds.min}, got ${value}`);
|
|
477
|
+
}
|
|
478
|
+
if (bounds.max !== undefined && value > bounds.max) {
|
|
479
|
+
throw new Error(`Expected ${path} to be <= ${bounds.max}, got ${value}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function parseStrictJSON(schema, raw) {
|
|
483
|
+
const trimmed = raw.trim();
|
|
484
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
485
|
+
throw new Error("Model did not return JSON (missing leading { or [)");
|
|
486
|
+
}
|
|
487
|
+
let parsed;
|
|
488
|
+
try {
|
|
489
|
+
parsed = JSON.parse(trimmed);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
492
|
+
throw new Error(`Invalid JSON: ${message}`);
|
|
493
|
+
}
|
|
494
|
+
return schema.getZod().parse(parsed);
|
|
495
|
+
}
|
|
496
|
+
function buildChunkIndex(chunks) {
|
|
497
|
+
const map = new Map;
|
|
498
|
+
for (const chunk of chunks) {
|
|
499
|
+
map.set(chunk.chunkId, chunk);
|
|
500
|
+
}
|
|
501
|
+
return map;
|
|
502
|
+
}
|
|
503
|
+
function validateCitation(citation, chunkIndex, opts) {
|
|
504
|
+
const maxQuoteLen = opts?.maxQuoteLen ?? 240;
|
|
505
|
+
const requireExactSubstring = opts?.requireExactSubstring ?? true;
|
|
506
|
+
const parsed = CitationModel.getZod().parse(citation);
|
|
507
|
+
assertStringLength(parsed.quote, "citation.quote", {
|
|
508
|
+
min: 1,
|
|
509
|
+
max: maxQuoteLen
|
|
510
|
+
});
|
|
511
|
+
const chunk = chunkIndex.get(parsed.chunkId);
|
|
512
|
+
if (!chunk) {
|
|
513
|
+
throw new Error(`Citation references unknown chunkId: ${parsed.chunkId}`);
|
|
514
|
+
}
|
|
515
|
+
if (requireExactSubstring && !chunk.text.includes(parsed.quote)) {
|
|
516
|
+
throw new Error(`Citation quote is not an exact substring of chunk ${parsed.chunkId}`);
|
|
517
|
+
}
|
|
518
|
+
return parsed;
|
|
519
|
+
}
|
|
520
|
+
function validateCitationsInTextBlock(block, chunkIndex) {
|
|
521
|
+
assertStringLength(block.text, "textBlock.text", { min: 1 });
|
|
522
|
+
if (!block.citations?.length) {
|
|
523
|
+
throw new Error("Missing required citations");
|
|
524
|
+
}
|
|
525
|
+
const citations = block.citations.map((c) => validateCitation(c, chunkIndex));
|
|
526
|
+
return { text: block.text, citations };
|
|
527
|
+
}
|
|
528
|
+
function validateOpportunityBrief(raw, chunks) {
|
|
529
|
+
const chunkIndex = buildChunkIndex(chunks);
|
|
530
|
+
const brief = parseStrictJSON(OpportunityBriefModel, raw);
|
|
531
|
+
assertStringLength(brief.opportunityId, "opportunityId", { min: 1 });
|
|
532
|
+
assertStringLength(brief.title, "title", { min: 1, max: 120 });
|
|
533
|
+
validateCitationsInTextBlock(brief.problem, chunkIndex);
|
|
534
|
+
validateCitationsInTextBlock(brief.who, chunkIndex);
|
|
535
|
+
validateCitationsInTextBlock(brief.proposedChange, chunkIndex);
|
|
536
|
+
assertStringLength(brief.expectedImpact.metric, "expectedImpact.metric", {
|
|
537
|
+
min: 1,
|
|
538
|
+
max: 64
|
|
539
|
+
});
|
|
540
|
+
if (brief.expectedImpact.magnitudeHint) {
|
|
541
|
+
assertStringLength(brief.expectedImpact.magnitudeHint, "expectedImpact.magnitudeHint", { max: 64 });
|
|
542
|
+
}
|
|
543
|
+
if (brief.expectedImpact.timeframeHint) {
|
|
544
|
+
assertStringLength(brief.expectedImpact.timeframeHint, "expectedImpact.timeframeHint", { max: 64 });
|
|
545
|
+
}
|
|
546
|
+
if (brief.risks) {
|
|
547
|
+
for (const risk of brief.risks) {
|
|
548
|
+
assertStringLength(risk.text, "risks[].text", { min: 1, max: 240 });
|
|
549
|
+
if (risk.citations) {
|
|
550
|
+
for (const c of risk.citations) {
|
|
551
|
+
validateCitation(c, chunkIndex);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return brief;
|
|
557
|
+
}
|
|
558
|
+
function validateInsightExtraction(raw, chunks) {
|
|
559
|
+
const chunkIndex = buildChunkIndex(chunks);
|
|
560
|
+
const data = parseStrictJSON(InsightExtractionModel, raw);
|
|
561
|
+
assertArrayLength(data.insights, "insights", { min: 1, max: 30 });
|
|
562
|
+
for (const insight of data.insights) {
|
|
563
|
+
assertStringLength(insight.insightId, "insights[].insightId", { min: 1 });
|
|
564
|
+
assertStringLength(insight.claim, "insights[].claim", {
|
|
565
|
+
min: 1,
|
|
566
|
+
max: 320
|
|
567
|
+
});
|
|
568
|
+
if (insight.tags) {
|
|
569
|
+
for (const tag of insight.tags) {
|
|
570
|
+
assertStringLength(tag, "insights[].tags[]", { min: 1 });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (insight.confidence !== undefined) {
|
|
574
|
+
assertNumberRange(insight.confidence, "insights[].confidence", {
|
|
575
|
+
min: 0,
|
|
576
|
+
max: 1
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
assertArrayLength(insight.citations, "insights[].citations", { min: 1 });
|
|
580
|
+
for (const c of insight.citations) {
|
|
581
|
+
validateCitation(c, chunkIndex);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return data;
|
|
585
|
+
}
|
|
586
|
+
function validatePatchIntent(raw) {
|
|
587
|
+
const data = parseStrictJSON(ContractPatchIntentModel, raw);
|
|
588
|
+
assertStringLength(data.featureKey, "featureKey", { min: 1, max: 80 });
|
|
589
|
+
assertArrayLength(data.changes, "changes", { min: 1, max: 25 });
|
|
590
|
+
for (const change of data.changes) {
|
|
591
|
+
assertStringLength(change.target, "changes[].target", { min: 1 });
|
|
592
|
+
assertStringLength(change.detail, "changes[].detail", { min: 1 });
|
|
593
|
+
}
|
|
594
|
+
assertArrayLength(data.acceptanceCriteria, "acceptanceCriteria", {
|
|
595
|
+
min: 1,
|
|
596
|
+
max: 12
|
|
597
|
+
});
|
|
598
|
+
for (const item of data.acceptanceCriteria) {
|
|
599
|
+
assertStringLength(item, "acceptanceCriteria[]", { min: 1, max: 140 });
|
|
600
|
+
}
|
|
601
|
+
return data;
|
|
602
|
+
}
|
|
603
|
+
function validateImpactReport(raw) {
|
|
604
|
+
const data = parseStrictJSON(ImpactReportModel, raw);
|
|
605
|
+
assertStringLength(data.reportId, "reportId", { min: 1 });
|
|
606
|
+
assertStringLength(data.patchId, "patchId", { min: 1 });
|
|
607
|
+
assertStringLength(data.summary, "summary", { min: 1, max: 200 });
|
|
608
|
+
if (data.breaks) {
|
|
609
|
+
for (const item of data.breaks) {
|
|
610
|
+
assertStringLength(item, "breaks[]", { min: 1, max: 160 });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (data.mustChange) {
|
|
614
|
+
for (const item of data.mustChange) {
|
|
615
|
+
assertStringLength(item, "mustChange[]", { min: 1, max: 160 });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (data.risky) {
|
|
619
|
+
for (const item of data.risky) {
|
|
620
|
+
assertStringLength(item, "risky[]", { min: 1, max: 160 });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const surfaces = data.surfaces;
|
|
624
|
+
if (surfaces.api) {
|
|
625
|
+
for (const item of surfaces.api) {
|
|
626
|
+
assertStringLength(item, "surfaces.api[]", { min: 1, max: 140 });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (surfaces.db) {
|
|
630
|
+
for (const item of surfaces.db) {
|
|
631
|
+
assertStringLength(item, "surfaces.db[]", { min: 1, max: 140 });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (surfaces.ui) {
|
|
635
|
+
for (const item of surfaces.ui) {
|
|
636
|
+
assertStringLength(item, "surfaces.ui[]", { min: 1, max: 140 });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (surfaces.workflows) {
|
|
640
|
+
for (const item of surfaces.workflows) {
|
|
641
|
+
assertStringLength(item, "surfaces.workflows[]", { min: 1, max: 140 });
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (surfaces.policy) {
|
|
645
|
+
for (const item of surfaces.policy) {
|
|
646
|
+
assertStringLength(item, "surfaces.policy[]", { min: 1, max: 140 });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (surfaces.docs) {
|
|
650
|
+
for (const item of surfaces.docs) {
|
|
651
|
+
assertStringLength(item, "surfaces.docs[]", { min: 1, max: 140 });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (surfaces.tests) {
|
|
655
|
+
for (const item of surfaces.tests) {
|
|
656
|
+
assertStringLength(item, "surfaces.tests[]", { min: 1, max: 140 });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return data;
|
|
660
|
+
}
|
|
661
|
+
function validateTaskPack(raw) {
|
|
662
|
+
const data = parseStrictJSON(TaskPackModel, raw);
|
|
663
|
+
assertStringLength(data.packId, "packId", { min: 1 });
|
|
664
|
+
assertStringLength(data.patchId, "patchId", { min: 1 });
|
|
665
|
+
assertStringLength(data.overview, "overview", { min: 1, max: 240 });
|
|
666
|
+
assertArrayLength(data.tasks, "tasks", { min: 3, max: 14 });
|
|
667
|
+
for (const task of data.tasks) {
|
|
668
|
+
assertStringLength(task.id, "tasks[].id", { min: 1 });
|
|
669
|
+
assertStringLength(task.title, "tasks[].title", { min: 1, max: 120 });
|
|
670
|
+
assertArrayLength(task.surface, "tasks[].surface", { min: 1 });
|
|
671
|
+
assertStringLength(task.why, "tasks[].why", { min: 1, max: 200 });
|
|
672
|
+
assertArrayLength(task.acceptance, "tasks[].acceptance", {
|
|
673
|
+
min: 1,
|
|
674
|
+
max: 10
|
|
675
|
+
});
|
|
676
|
+
for (const criterion of task.acceptance) {
|
|
677
|
+
assertStringLength(criterion, "tasks[].acceptance[]", {
|
|
678
|
+
min: 1,
|
|
679
|
+
max: 160
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
assertStringLength(task.agentPrompt, "tasks[].agentPrompt", {
|
|
683
|
+
min: 1,
|
|
684
|
+
max: 1400
|
|
685
|
+
});
|
|
686
|
+
if (task.dependsOn) {
|
|
687
|
+
for (const dep of task.dependsOn) {
|
|
688
|
+
assertStringLength(dep, "tasks[].dependsOn[]", { min: 1 });
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return data;
|
|
693
|
+
}
|
|
694
|
+
function buildRepairPrompt(error) {
|
|
695
|
+
return [
|
|
696
|
+
"Your previous output failed validation.",
|
|
697
|
+
"Fix the output and return JSON ONLY (no markdown, no commentary).",
|
|
698
|
+
"Validation error:",
|
|
699
|
+
error
|
|
700
|
+
].join(`
|
|
701
|
+
`);
|
|
702
|
+
}
|
|
703
|
+
function truncateText(value, maxChars) {
|
|
704
|
+
if (value.length <= maxChars)
|
|
705
|
+
return value;
|
|
706
|
+
return `${value.slice(0, maxChars)}
|
|
707
|
+
...(truncated)`;
|
|
708
|
+
}
|
|
709
|
+
function buildRepairPromptWithOutput(error, previousOutput, maxOutputChars = 4000) {
|
|
710
|
+
return [
|
|
711
|
+
"Your previous output failed validation.",
|
|
712
|
+
"Fix the output and return JSON ONLY (no markdown, no commentary).",
|
|
713
|
+
"Do not change the JSON shape or rename fields.",
|
|
714
|
+
"If a citation quote is invalid, replace it with an exact substring from the referenced chunk.",
|
|
715
|
+
"Validation error:",
|
|
716
|
+
error,
|
|
717
|
+
"Previous output:",
|
|
718
|
+
truncateText(previousOutput, maxOutputChars)
|
|
719
|
+
].join(`
|
|
720
|
+
`);
|
|
721
|
+
}
|
|
722
|
+
// src/ticket-validators.ts
|
|
723
|
+
import {
|
|
724
|
+
EvidenceFindingExtractionModel,
|
|
725
|
+
ProblemGroupingModel,
|
|
726
|
+
TicketCollectionModel
|
|
727
|
+
} from "@contractspec/lib.contracts/product-intent/types";
|
|
728
|
+
function assertStringLength2(value, path, bounds) {
|
|
729
|
+
if (bounds.min !== undefined && value.length < bounds.min) {
|
|
730
|
+
throw new Error(`Expected ${path} to be at least ${bounds.min} characters, got ${value.length}`);
|
|
731
|
+
}
|
|
732
|
+
if (bounds.max !== undefined && value.length > bounds.max) {
|
|
733
|
+
throw new Error(`Expected ${path} to be at most ${bounds.max} characters, got ${value.length}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function assertArrayLength2(value, path, bounds) {
|
|
737
|
+
if (bounds.min !== undefined && value.length < bounds.min) {
|
|
738
|
+
throw new Error(`Expected ${path} to have at least ${bounds.min} items, got ${value.length}`);
|
|
739
|
+
}
|
|
740
|
+
if (bounds.max !== undefined && value.length > bounds.max) {
|
|
741
|
+
throw new Error(`Expected ${path} to have at most ${bounds.max} items, got ${value.length}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function assertIdsExist(ids, allowed, path) {
|
|
745
|
+
for (const id of ids) {
|
|
746
|
+
if (!allowed.has(id)) {
|
|
747
|
+
throw new Error(`Unknown ${path} reference: ${id}`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function parseJSON(schema, raw) {
|
|
752
|
+
return parseStrictJSON(schema, raw);
|
|
753
|
+
}
|
|
754
|
+
function validateEvidenceFindingExtraction(raw, chunks) {
|
|
755
|
+
const chunkIndex = buildChunkIndex(chunks);
|
|
756
|
+
const data = parseJSON(EvidenceFindingExtractionModel, raw);
|
|
757
|
+
assertArrayLength2(data.findings, "findings", { min: 1, max: 40 });
|
|
758
|
+
for (const finding of data.findings) {
|
|
759
|
+
assertStringLength2(finding.findingId, "findings[].findingId", { min: 1 });
|
|
760
|
+
assertStringLength2(finding.summary, "findings[].summary", {
|
|
761
|
+
min: 1,
|
|
762
|
+
max: 320
|
|
763
|
+
});
|
|
764
|
+
if (finding.tags) {
|
|
765
|
+
for (const tag of finding.tags) {
|
|
766
|
+
assertStringLength2(tag, "findings[].tags[]", { min: 1, max: 48 });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
assertArrayLength2(finding.citations, "findings[].citations", { min: 1 });
|
|
770
|
+
for (const citation of finding.citations) {
|
|
771
|
+
validateCitation(citation, chunkIndex);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return data;
|
|
775
|
+
}
|
|
776
|
+
function validateProblemGrouping(raw, findings) {
|
|
777
|
+
const data = parseJSON(ProblemGroupingModel, raw);
|
|
778
|
+
const allowedIds = new Set(findings.map((finding) => finding.findingId));
|
|
779
|
+
assertArrayLength2(data.problems, "problems", { min: 1, max: 20 });
|
|
780
|
+
for (const problem of data.problems) {
|
|
781
|
+
assertStringLength2(problem.problemId, "problems[].problemId", { min: 1 });
|
|
782
|
+
assertStringLength2(problem.statement, "problems[].statement", {
|
|
783
|
+
min: 1,
|
|
784
|
+
max: 320
|
|
785
|
+
});
|
|
786
|
+
assertArrayLength2(problem.evidenceIds, "problems[].evidenceIds", {
|
|
787
|
+
min: 1,
|
|
788
|
+
max: 8
|
|
789
|
+
});
|
|
790
|
+
assertIdsExist(problem.evidenceIds, allowedIds, "evidenceId");
|
|
791
|
+
if (problem.tags) {
|
|
792
|
+
for (const tag of problem.tags) {
|
|
793
|
+
assertStringLength2(tag, "problems[].tags[]", { min: 1, max: 48 });
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return data;
|
|
798
|
+
}
|
|
799
|
+
function validateTicketCollection(raw, findings) {
|
|
800
|
+
const data = parseJSON(TicketCollectionModel, raw);
|
|
801
|
+
const allowedIds = new Set(findings.map((finding) => finding.findingId));
|
|
802
|
+
assertArrayLength2(data.tickets, "tickets", { min: 1, max: 30 });
|
|
803
|
+
for (const ticket of data.tickets) {
|
|
804
|
+
assertStringLength2(ticket.ticketId, "tickets[].ticketId", { min: 1 });
|
|
805
|
+
assertStringLength2(ticket.title, "tickets[].title", { min: 1, max: 120 });
|
|
806
|
+
assertStringLength2(ticket.summary, "tickets[].summary", {
|
|
807
|
+
min: 1,
|
|
808
|
+
max: 320
|
|
809
|
+
});
|
|
810
|
+
assertArrayLength2(ticket.evidenceIds, "tickets[].evidenceIds", {
|
|
811
|
+
min: 1,
|
|
812
|
+
max: 8
|
|
813
|
+
});
|
|
814
|
+
assertIdsExist(ticket.evidenceIds, allowedIds, "evidenceId");
|
|
815
|
+
assertArrayLength2(ticket.acceptanceCriteria, "tickets[].acceptanceCriteria", {
|
|
816
|
+
min: 1,
|
|
817
|
+
max: 8
|
|
818
|
+
});
|
|
819
|
+
for (const criterion of ticket.acceptanceCriteria) {
|
|
820
|
+
assertStringLength2(criterion, "tickets[].acceptanceCriteria[]", {
|
|
821
|
+
min: 1,
|
|
822
|
+
max: 280
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
if (ticket.tags) {
|
|
826
|
+
for (const tag of ticket.tags) {
|
|
827
|
+
assertStringLength2(tag, "tickets[].tags[]", { min: 1, max: 48 });
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return data;
|
|
832
|
+
}
|
|
833
|
+
// src/ticket-pipeline.ts
|
|
834
|
+
import {
|
|
835
|
+
ContractPatchIntentModel as ContractPatchIntentModel2,
|
|
836
|
+
EvidenceFindingExtractionModel as EvidenceFindingExtractionModel2,
|
|
837
|
+
ProblemGroupingModel as ProblemGroupingModel2,
|
|
838
|
+
TicketCollectionModel as TicketCollectionModel2
|
|
839
|
+
} from "@contractspec/lib.contracts/product-intent/types";
|
|
840
|
+
|
|
841
|
+
// src/ticket-pipeline-runner.ts
|
|
842
|
+
var DEFAULT_MAX_ATTEMPTS = 2;
|
|
843
|
+
function timestamp() {
|
|
844
|
+
return new Date().toISOString();
|
|
845
|
+
}
|
|
846
|
+
function toErrorMessage(error) {
|
|
847
|
+
return error instanceof Error ? error.message : String(error);
|
|
848
|
+
}
|
|
849
|
+
async function safeLog(logger, entry) {
|
|
850
|
+
if (!logger)
|
|
851
|
+
return;
|
|
852
|
+
try {
|
|
853
|
+
await logger.log(entry);
|
|
854
|
+
} catch {}
|
|
855
|
+
}
|
|
856
|
+
async function runWithValidation(options) {
|
|
857
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
|
|
858
|
+
let attempt = 0;
|
|
859
|
+
let lastError;
|
|
860
|
+
let lastRaw = "";
|
|
861
|
+
let currentPrompt = options.prompt;
|
|
862
|
+
while (attempt < maxAttempts) {
|
|
863
|
+
attempt += 1;
|
|
864
|
+
await safeLog(options.logger, {
|
|
865
|
+
stage: options.stage,
|
|
866
|
+
phase: "request",
|
|
867
|
+
attempt,
|
|
868
|
+
prompt: currentPrompt,
|
|
869
|
+
timestamp: timestamp()
|
|
870
|
+
});
|
|
871
|
+
let raw;
|
|
872
|
+
try {
|
|
873
|
+
raw = await options.modelRunner.generateJson(currentPrompt);
|
|
874
|
+
} catch (error) {
|
|
875
|
+
lastError = toErrorMessage(error);
|
|
876
|
+
await safeLog(options.logger, {
|
|
877
|
+
stage: options.stage,
|
|
878
|
+
phase: "model_error",
|
|
879
|
+
attempt,
|
|
880
|
+
prompt: currentPrompt,
|
|
881
|
+
error: lastError,
|
|
882
|
+
timestamp: timestamp()
|
|
883
|
+
});
|
|
884
|
+
throw new Error(`[${options.stage}] Model error: ${lastError}`);
|
|
885
|
+
}
|
|
886
|
+
await safeLog(options.logger, {
|
|
887
|
+
stage: options.stage,
|
|
888
|
+
phase: "response",
|
|
889
|
+
attempt,
|
|
890
|
+
prompt: currentPrompt,
|
|
891
|
+
response: raw,
|
|
892
|
+
timestamp: timestamp()
|
|
893
|
+
});
|
|
894
|
+
try {
|
|
895
|
+
return options.validate(raw);
|
|
896
|
+
} catch (error) {
|
|
897
|
+
lastError = toErrorMessage(error);
|
|
898
|
+
lastRaw = raw;
|
|
899
|
+
if (options.repair) {
|
|
900
|
+
const repaired = options.repair(raw, lastError);
|
|
901
|
+
if (repaired && repaired !== raw) {
|
|
902
|
+
await safeLog(options.logger, {
|
|
903
|
+
stage: options.stage,
|
|
904
|
+
phase: "repair",
|
|
905
|
+
attempt,
|
|
906
|
+
prompt: currentPrompt,
|
|
907
|
+
response: repaired,
|
|
908
|
+
error: lastError,
|
|
909
|
+
timestamp: timestamp()
|
|
910
|
+
});
|
|
911
|
+
try {
|
|
912
|
+
return options.validate(repaired);
|
|
913
|
+
} catch (repairError) {
|
|
914
|
+
lastError = toErrorMessage(repairError);
|
|
915
|
+
lastRaw = repaired;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
await safeLog(options.logger, {
|
|
920
|
+
stage: options.stage,
|
|
921
|
+
phase: "validation_error",
|
|
922
|
+
attempt,
|
|
923
|
+
prompt: currentPrompt,
|
|
924
|
+
response: lastRaw,
|
|
925
|
+
error: lastError,
|
|
926
|
+
timestamp: timestamp()
|
|
927
|
+
});
|
|
928
|
+
currentPrompt = [
|
|
929
|
+
options.prompt,
|
|
930
|
+
buildRepairPromptWithOutput(lastError, lastRaw)
|
|
931
|
+
].join(`
|
|
932
|
+
|
|
933
|
+
`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
throw new Error(`[${options.stage}] Validation failed after ${maxAttempts} attempt(s): ${lastError ?? "unknown error"}`);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// src/ticket-pipeline.ts
|
|
940
|
+
var TAG_HINTS = {
|
|
941
|
+
onboarding: ["onboarding", "setup", "activation"],
|
|
942
|
+
pricing: ["pricing", "cost", "billing"],
|
|
943
|
+
security: ["security", "compliance", "audit"],
|
|
944
|
+
support: ["support", "ticket", "helpdesk"],
|
|
945
|
+
analytics: ["analytics", "report", "dashboard"],
|
|
946
|
+
performance: ["slow", "latency", "performance"],
|
|
947
|
+
integrations: ["integration", "api", "webhook"]
|
|
948
|
+
};
|
|
949
|
+
function slugify(value) {
|
|
950
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)+/g, "");
|
|
951
|
+
}
|
|
952
|
+
function pickQuote(text, maxLen = 220) {
|
|
953
|
+
const trimmed = text.trim();
|
|
954
|
+
const sentenceEnd = trimmed.search(/[.!?]\s/);
|
|
955
|
+
const sentence = sentenceEnd === -1 ? trimmed : trimmed.slice(0, sentenceEnd + 1);
|
|
956
|
+
const quote = sentence.length > maxLen ? sentence.slice(0, maxLen) : sentence;
|
|
957
|
+
return quote.trim();
|
|
958
|
+
}
|
|
959
|
+
function deriveTags(text) {
|
|
960
|
+
const lower = text.toLowerCase();
|
|
961
|
+
const tags = Object.entries(TAG_HINTS).filter(([, hints]) => hints.some((hint) => lower.includes(hint))).map(([tag]) => tag);
|
|
962
|
+
return tags.slice(0, 3);
|
|
963
|
+
}
|
|
964
|
+
function truncateToMax(value, maxChars) {
|
|
965
|
+
if (value.length <= maxChars)
|
|
966
|
+
return value;
|
|
967
|
+
if (maxChars <= 3)
|
|
968
|
+
return value.slice(0, maxChars);
|
|
969
|
+
return `${value.slice(0, maxChars - 3).trimEnd()}...`;
|
|
970
|
+
}
|
|
971
|
+
var QUOTE_HYPHENS = new Set(["-", "‐", "‑", "‒", "–", "—"]);
|
|
972
|
+
var QUOTE_SINGLE = new Set(["'", "’", "‘"]);
|
|
973
|
+
var QUOTE_DOUBLE = new Set(['"', "“", "”"]);
|
|
974
|
+
function escapeRegex(value) {
|
|
975
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
976
|
+
}
|
|
977
|
+
function buildLooseQuotePattern(quote) {
|
|
978
|
+
let pattern = "";
|
|
979
|
+
for (let i = 0;i < quote.length; i += 1) {
|
|
980
|
+
const char = quote[i] ?? "";
|
|
981
|
+
if (char === "." && quote.slice(i, i + 3) === "...") {
|
|
982
|
+
pattern += "(?:\\.\\.\\.|…)";
|
|
983
|
+
i += 2;
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (char === "…") {
|
|
987
|
+
pattern += "(?:\\.\\.\\.|…)";
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (/\s/.test(char)) {
|
|
991
|
+
pattern += "\\s+";
|
|
992
|
+
while (i + 1 < quote.length && /\s/.test(quote[i + 1] ?? "")) {
|
|
993
|
+
i += 1;
|
|
994
|
+
}
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (QUOTE_HYPHENS.has(char)) {
|
|
998
|
+
pattern += "[-‐‑‒–—]";
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
if (QUOTE_SINGLE.has(char)) {
|
|
1002
|
+
pattern += "['‘’]";
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
if (QUOTE_DOUBLE.has(char)) {
|
|
1006
|
+
pattern += '["“”]';
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
pattern += escapeRegex(char);
|
|
1010
|
+
}
|
|
1011
|
+
return pattern;
|
|
1012
|
+
}
|
|
1013
|
+
function findQuoteInChunk(quote, chunkText) {
|
|
1014
|
+
if (chunkText.includes(quote))
|
|
1015
|
+
return quote;
|
|
1016
|
+
const pattern = buildLooseQuotePattern(quote);
|
|
1017
|
+
const match = chunkText.match(new RegExp(pattern));
|
|
1018
|
+
return match?.[0] ?? null;
|
|
1019
|
+
}
|
|
1020
|
+
function normalizeForTokens(value) {
|
|
1021
|
+
return value.replace(/[“”]/g, '"').replace(/[‘’]/g, "'").replace(/[‐‑‒–—]/g, "-").replace(/\s+/g, " ").trim();
|
|
1022
|
+
}
|
|
1023
|
+
function tokenize(value) {
|
|
1024
|
+
const normalized = normalizeForTokens(value).toLowerCase();
|
|
1025
|
+
return normalized.match(/[a-z0-9]+/g) ?? [];
|
|
1026
|
+
}
|
|
1027
|
+
function splitIntoSegments(text) {
|
|
1028
|
+
const matches = text.match(/[^.!?\n]+[.!?]?/g);
|
|
1029
|
+
if (!matches)
|
|
1030
|
+
return [text];
|
|
1031
|
+
return matches.map((segment) => segment.trim()).filter(Boolean);
|
|
1032
|
+
}
|
|
1033
|
+
function selectBestQuoteFromChunk(quote, chunkText, maxLen = 240) {
|
|
1034
|
+
const quoteTokens = tokenize(quote);
|
|
1035
|
+
if (!quoteTokens.length)
|
|
1036
|
+
return null;
|
|
1037
|
+
const quoteTokenSet = new Set(quoteTokens);
|
|
1038
|
+
let best = null;
|
|
1039
|
+
for (const segment of splitIntoSegments(chunkText)) {
|
|
1040
|
+
if (!segment)
|
|
1041
|
+
continue;
|
|
1042
|
+
const segmentTokens = new Set(tokenize(segment));
|
|
1043
|
+
if (!segmentTokens.size)
|
|
1044
|
+
continue;
|
|
1045
|
+
let overlap = 0;
|
|
1046
|
+
for (const token of quoteTokenSet) {
|
|
1047
|
+
if (segmentTokens.has(token))
|
|
1048
|
+
overlap += 1;
|
|
1049
|
+
}
|
|
1050
|
+
if (!overlap)
|
|
1051
|
+
continue;
|
|
1052
|
+
const score = overlap / quoteTokenSet.size;
|
|
1053
|
+
if (!best || score > best.score) {
|
|
1054
|
+
best = { segment, score, overlap };
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (!best)
|
|
1058
|
+
return null;
|
|
1059
|
+
if (best.overlap < 2 && quoteTokens.length > 2)
|
|
1060
|
+
return null;
|
|
1061
|
+
const trimmed = best.segment.trim();
|
|
1062
|
+
return trimmed.length > maxLen ? trimmed.slice(0, maxLen).trimEnd() : trimmed;
|
|
1063
|
+
}
|
|
1064
|
+
function fallbackQuoteFromChunk(chunkText, maxLen = 240) {
|
|
1065
|
+
const trimmed = chunkText.trim();
|
|
1066
|
+
if (!trimmed)
|
|
1067
|
+
return null;
|
|
1068
|
+
const slice = trimmed.length > maxLen ? trimmed.slice(0, maxLen) : trimmed;
|
|
1069
|
+
return slice.trimEnd();
|
|
1070
|
+
}
|
|
1071
|
+
function findQuoteAcrossChunks(quote, chunkIndex) {
|
|
1072
|
+
for (const [chunkId, chunk] of chunkIndex.entries()) {
|
|
1073
|
+
if (chunk.text.includes(quote)) {
|
|
1074
|
+
return { chunkId, quote };
|
|
1075
|
+
}
|
|
1076
|
+
const repaired = findQuoteInChunk(quote, chunk.text);
|
|
1077
|
+
if (repaired) {
|
|
1078
|
+
return { chunkId, quote: repaired };
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
function repairEvidenceFindingExtraction(raw, chunks) {
|
|
1084
|
+
let data;
|
|
1085
|
+
try {
|
|
1086
|
+
data = parseStrictJSON(EvidenceFindingExtractionModel2, raw);
|
|
1087
|
+
} catch {
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
const chunkIndex = buildChunkIndex(chunks);
|
|
1091
|
+
let updated = false;
|
|
1092
|
+
for (const finding of data.findings) {
|
|
1093
|
+
for (const citation of finding.citations) {
|
|
1094
|
+
const chunk = chunkIndex.get(citation.chunkId);
|
|
1095
|
+
if (chunk) {
|
|
1096
|
+
if (chunk.text.includes(citation.quote))
|
|
1097
|
+
continue;
|
|
1098
|
+
const repaired = findQuoteInChunk(citation.quote, chunk.text);
|
|
1099
|
+
if (repaired) {
|
|
1100
|
+
citation.quote = repaired;
|
|
1101
|
+
updated = true;
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
const other = findQuoteAcrossChunks(citation.quote, chunkIndex);
|
|
1106
|
+
if (other) {
|
|
1107
|
+
citation.chunkId = other.chunkId;
|
|
1108
|
+
citation.quote = other.quote;
|
|
1109
|
+
updated = true;
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
if (chunk) {
|
|
1113
|
+
const best = selectBestQuoteFromChunk(citation.quote, chunk.text);
|
|
1114
|
+
if (best) {
|
|
1115
|
+
citation.quote = best;
|
|
1116
|
+
updated = true;
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
const fallback = fallbackQuoteFromChunk(chunk.text);
|
|
1120
|
+
if (fallback) {
|
|
1121
|
+
citation.quote = fallback;
|
|
1122
|
+
updated = true;
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return updated ? JSON.stringify(data, null, 2) : null;
|
|
1129
|
+
}
|
|
1130
|
+
function repairProblemGrouping(raw) {
|
|
1131
|
+
let data;
|
|
1132
|
+
try {
|
|
1133
|
+
data = parseStrictJSON(ProblemGroupingModel2, raw);
|
|
1134
|
+
} catch {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
let updated = false;
|
|
1138
|
+
for (const problem of data.problems) {
|
|
1139
|
+
const statement = truncateToMax(problem.statement, 320);
|
|
1140
|
+
if (statement !== problem.statement) {
|
|
1141
|
+
problem.statement = statement;
|
|
1142
|
+
updated = true;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return updated ? JSON.stringify(data, null, 2) : null;
|
|
1146
|
+
}
|
|
1147
|
+
function repairTicketCollection(raw) {
|
|
1148
|
+
let data;
|
|
1149
|
+
try {
|
|
1150
|
+
data = parseStrictJSON(TicketCollectionModel2, raw);
|
|
1151
|
+
} catch {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
let updated = false;
|
|
1155
|
+
for (const ticket of data.tickets) {
|
|
1156
|
+
const title = truncateToMax(ticket.title, 120);
|
|
1157
|
+
const summary = truncateToMax(ticket.summary, 320);
|
|
1158
|
+
if (title !== ticket.title) {
|
|
1159
|
+
ticket.title = title;
|
|
1160
|
+
updated = true;
|
|
1161
|
+
}
|
|
1162
|
+
if (summary !== ticket.summary) {
|
|
1163
|
+
ticket.summary = summary;
|
|
1164
|
+
updated = true;
|
|
1165
|
+
}
|
|
1166
|
+
ticket.acceptanceCriteria = ticket.acceptanceCriteria.map((criterion) => {
|
|
1167
|
+
const next = truncateToMax(criterion, 160);
|
|
1168
|
+
if (next !== criterion)
|
|
1169
|
+
updated = true;
|
|
1170
|
+
return next;
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
return updated ? JSON.stringify(data, null, 2) : null;
|
|
1174
|
+
}
|
|
1175
|
+
function repairPatchIntent(raw) {
|
|
1176
|
+
let data;
|
|
1177
|
+
try {
|
|
1178
|
+
data = parseStrictJSON(ContractPatchIntentModel2, raw);
|
|
1179
|
+
} catch {
|
|
1180
|
+
return null;
|
|
1181
|
+
}
|
|
1182
|
+
let updated = false;
|
|
1183
|
+
const featureKey = truncateToMax(data.featureKey, 80);
|
|
1184
|
+
if (featureKey !== data.featureKey) {
|
|
1185
|
+
data.featureKey = featureKey;
|
|
1186
|
+
updated = true;
|
|
1187
|
+
}
|
|
1188
|
+
data.acceptanceCriteria = data.acceptanceCriteria.map((criterion) => {
|
|
1189
|
+
const next = truncateToMax(criterion, 140);
|
|
1190
|
+
if (next !== criterion)
|
|
1191
|
+
updated = true;
|
|
1192
|
+
return next;
|
|
1193
|
+
});
|
|
1194
|
+
return updated ? JSON.stringify(data, null, 2) : null;
|
|
1195
|
+
}
|
|
1196
|
+
function retrieveChunks(transcript, question, options = {}) {
|
|
1197
|
+
const chunkSize = options.chunkSize ?? 800;
|
|
1198
|
+
const sourceId = options.sourceId ?? slugify(question || "transcript");
|
|
1199
|
+
const clean = transcript.trim();
|
|
1200
|
+
const chunks = [];
|
|
1201
|
+
for (let offset = 0, idx = 0;offset < clean.length; idx += 1) {
|
|
1202
|
+
const slice = clean.slice(offset, offset + chunkSize);
|
|
1203
|
+
chunks.push({
|
|
1204
|
+
chunkId: `${sourceId}#c_${String(idx).padStart(2, "0")}`,
|
|
1205
|
+
text: slice,
|
|
1206
|
+
meta: { sourceId, ...options.meta }
|
|
1207
|
+
});
|
|
1208
|
+
offset += chunkSize;
|
|
1209
|
+
}
|
|
1210
|
+
return chunks;
|
|
1211
|
+
}
|
|
1212
|
+
async function extractEvidence(chunks, question, options = {}) {
|
|
1213
|
+
if (options.modelRunner) {
|
|
1214
|
+
const evidenceJSON = formatEvidenceForModel(chunks, 900);
|
|
1215
|
+
const prompt = promptExtractEvidenceFindings({ question, evidenceJSON });
|
|
1216
|
+
return runWithValidation({
|
|
1217
|
+
stage: "extractEvidence",
|
|
1218
|
+
prompt,
|
|
1219
|
+
modelRunner: options.modelRunner,
|
|
1220
|
+
logger: options.logger,
|
|
1221
|
+
maxAttempts: options.maxAttempts,
|
|
1222
|
+
repair: (raw2) => repairEvidenceFindingExtraction(raw2, chunks),
|
|
1223
|
+
validate: (raw2) => validateEvidenceFindingExtraction(raw2, chunks).findings
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
const maxFindings = options.maxFindings ?? 12;
|
|
1227
|
+
const findings = [];
|
|
1228
|
+
for (const chunk of chunks) {
|
|
1229
|
+
if (findings.length >= maxFindings)
|
|
1230
|
+
break;
|
|
1231
|
+
const quote = pickQuote(chunk.text);
|
|
1232
|
+
findings.push({
|
|
1233
|
+
findingId: `find_${String(findings.length + 1).padStart(3, "0")}`,
|
|
1234
|
+
summary: quote.length > 160 ? `${quote.slice(0, 160)}...` : quote,
|
|
1235
|
+
tags: deriveTags(chunk.text),
|
|
1236
|
+
citations: [{ chunkId: chunk.chunkId, quote }]
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
const raw = JSON.stringify({ findings }, null, 2);
|
|
1240
|
+
return validateEvidenceFindingExtraction(raw, chunks).findings;
|
|
1241
|
+
}
|
|
1242
|
+
async function groupProblems(findings, question, options = {}) {
|
|
1243
|
+
if (options.modelRunner) {
|
|
1244
|
+
const findingsJSON = JSON.stringify({ findings }, null, 2);
|
|
1245
|
+
const prompt = promptGroupProblems({
|
|
1246
|
+
question,
|
|
1247
|
+
findingsJSON,
|
|
1248
|
+
findingIds: findings.map((finding) => finding.findingId)
|
|
1249
|
+
});
|
|
1250
|
+
return runWithValidation({
|
|
1251
|
+
stage: "groupProblems",
|
|
1252
|
+
prompt,
|
|
1253
|
+
modelRunner: options.modelRunner,
|
|
1254
|
+
logger: options.logger,
|
|
1255
|
+
maxAttempts: options.maxAttempts,
|
|
1256
|
+
repair: (raw2) => repairProblemGrouping(raw2),
|
|
1257
|
+
validate: (raw2) => validateProblemGrouping(raw2, findings).problems
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
const grouped = new Map;
|
|
1261
|
+
for (const finding of findings) {
|
|
1262
|
+
const tag = finding.tags?.[0] ?? "general";
|
|
1263
|
+
if (!grouped.has(tag))
|
|
1264
|
+
grouped.set(tag, []);
|
|
1265
|
+
grouped.get(tag)?.push(finding);
|
|
1266
|
+
}
|
|
1267
|
+
const problems = [];
|
|
1268
|
+
for (const [tag, items] of grouped.entries()) {
|
|
1269
|
+
const count = items.length;
|
|
1270
|
+
const severity = count >= 4 ? "high" : count >= 2 ? "medium" : "low";
|
|
1271
|
+
const statement = tag === "general" ? "Users report friction that slows adoption." : `Users report ${tag} friction that blocks progress.`;
|
|
1272
|
+
problems.push({
|
|
1273
|
+
problemId: `prob_${String(problems.length + 1).padStart(3, "0")}`,
|
|
1274
|
+
statement,
|
|
1275
|
+
evidenceIds: items.map((item) => item.findingId),
|
|
1276
|
+
tags: tag === "general" ? undefined : [tag],
|
|
1277
|
+
severity
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
const raw = JSON.stringify({ problems }, null, 2);
|
|
1281
|
+
return validateProblemGrouping(raw, findings).problems;
|
|
1282
|
+
}
|
|
1283
|
+
async function generateTickets(problems, findings, question, options = {}) {
|
|
1284
|
+
if (options.modelRunner) {
|
|
1285
|
+
const problemsJSON = JSON.stringify({ problems }, null, 2);
|
|
1286
|
+
const findingsJSON = JSON.stringify({ findings }, null, 2);
|
|
1287
|
+
const prompt = promptGenerateTickets({
|
|
1288
|
+
question,
|
|
1289
|
+
problemsJSON,
|
|
1290
|
+
findingsJSON
|
|
1291
|
+
});
|
|
1292
|
+
return runWithValidation({
|
|
1293
|
+
stage: "generateTickets",
|
|
1294
|
+
prompt,
|
|
1295
|
+
modelRunner: options.modelRunner,
|
|
1296
|
+
logger: options.logger,
|
|
1297
|
+
maxAttempts: options.maxAttempts,
|
|
1298
|
+
repair: (raw2) => repairTicketCollection(raw2),
|
|
1299
|
+
validate: (raw2) => validateTicketCollection(raw2, findings).tickets
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
const tickets = problems.map((problem, idx) => {
|
|
1303
|
+
const tag = problem.tags?.[0];
|
|
1304
|
+
const title = tag ? `Improve ${tag} flow` : "Reduce user friction";
|
|
1305
|
+
const summary = problem.statement;
|
|
1306
|
+
return {
|
|
1307
|
+
ticketId: `t_${String(idx + 1).padStart(3, "0")}`,
|
|
1308
|
+
title,
|
|
1309
|
+
summary,
|
|
1310
|
+
evidenceIds: problem.evidenceIds.slice(0, 4),
|
|
1311
|
+
acceptanceCriteria: [
|
|
1312
|
+
"Acceptance criteria maps to the evidence findings",
|
|
1313
|
+
"Success metrics are tracked for the change"
|
|
1314
|
+
],
|
|
1315
|
+
tags: problem.tags,
|
|
1316
|
+
priority: problem.severity === "high" ? "high" : "medium"
|
|
1317
|
+
};
|
|
1318
|
+
});
|
|
1319
|
+
const raw = JSON.stringify({ tickets }, null, 2);
|
|
1320
|
+
return validateTicketCollection(raw, findings).tickets;
|
|
1321
|
+
}
|
|
1322
|
+
async function suggestPatch(ticket, options = {}) {
|
|
1323
|
+
if (options.modelRunner) {
|
|
1324
|
+
const ticketJSON = JSON.stringify(ticket, null, 2);
|
|
1325
|
+
const prompt = promptSuggestPatchIntent({ ticketJSON });
|
|
1326
|
+
return runWithValidation({
|
|
1327
|
+
stage: "suggestPatch",
|
|
1328
|
+
prompt,
|
|
1329
|
+
modelRunner: options.modelRunner,
|
|
1330
|
+
logger: options.logger,
|
|
1331
|
+
maxAttempts: options.maxAttempts,
|
|
1332
|
+
repair: (raw) => repairPatchIntent(raw),
|
|
1333
|
+
validate: (raw) => validatePatchIntent(raw)
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
const featureKey = slugify(ticket.title) || "product_intent_ticket";
|
|
1337
|
+
const intent = {
|
|
1338
|
+
featureKey,
|
|
1339
|
+
changes: [
|
|
1340
|
+
{
|
|
1341
|
+
type: "update_operation",
|
|
1342
|
+
target: `productIntent.${featureKey}`,
|
|
1343
|
+
detail: ticket.summary
|
|
1344
|
+
}
|
|
1345
|
+
],
|
|
1346
|
+
acceptanceCriteria: ticket.acceptanceCriteria
|
|
1347
|
+
};
|
|
1348
|
+
return validatePatchIntent(JSON.stringify(intent, null, 2));
|
|
1349
|
+
}
|
|
1350
|
+
// src/impact-engine.ts
|
|
1351
|
+
var SURFACE_MAP = {
|
|
1352
|
+
add_field: ["api", "db", "ui", "docs", "tests"],
|
|
1353
|
+
remove_field: ["api", "db", "ui", "docs", "tests"],
|
|
1354
|
+
rename_field: ["api", "db", "ui", "docs", "tests"],
|
|
1355
|
+
add_event: ["api", "workflows", "docs", "tests"],
|
|
1356
|
+
update_event: ["api", "workflows", "docs", "tests"],
|
|
1357
|
+
add_operation: ["api", "ui", "workflows", "docs", "tests"],
|
|
1358
|
+
update_operation: ["api", "ui", "workflows", "docs", "tests"],
|
|
1359
|
+
update_form: ["ui", "docs", "tests"],
|
|
1360
|
+
update_policy: ["policy", "api", "workflows", "docs", "tests"],
|
|
1361
|
+
add_enum_value: ["api", "db", "ui", "docs", "tests"],
|
|
1362
|
+
remove_enum_value: ["api", "db", "ui", "docs", "tests"],
|
|
1363
|
+
other: ["docs", "tests"]
|
|
1364
|
+
};
|
|
1365
|
+
var BUCKET_MAP = {
|
|
1366
|
+
remove_field: "breaks",
|
|
1367
|
+
rename_field: "breaks",
|
|
1368
|
+
remove_enum_value: "breaks",
|
|
1369
|
+
update_operation: "mustChange",
|
|
1370
|
+
update_event: "mustChange",
|
|
1371
|
+
update_policy: "mustChange",
|
|
1372
|
+
update_form: "risky",
|
|
1373
|
+
add_field: "risky",
|
|
1374
|
+
add_event: "risky",
|
|
1375
|
+
add_operation: "risky",
|
|
1376
|
+
add_enum_value: "risky",
|
|
1377
|
+
other: "risky"
|
|
1378
|
+
};
|
|
1379
|
+
function slugify2(value) {
|
|
1380
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)+/g, "");
|
|
1381
|
+
}
|
|
1382
|
+
function buildTokens(change) {
|
|
1383
|
+
const combined = `${change.type} ${change.target} ${change.detail}`;
|
|
1384
|
+
const tokens = combined.split(/[^a-zA-Z0-9]+/).map((token) => token.trim()).filter((token) => token.length >= 3);
|
|
1385
|
+
return Array.from(new Set(tokens.map((token) => token.toLowerCase()))).slice(0, 8);
|
|
1386
|
+
}
|
|
1387
|
+
function scanTokens(tokens, files, maxHits) {
|
|
1388
|
+
const hits = [];
|
|
1389
|
+
const lowerTokens = tokens.map((token) => token.toLowerCase());
|
|
1390
|
+
for (const file of files) {
|
|
1391
|
+
const haystack = file.content.toLowerCase();
|
|
1392
|
+
if (lowerTokens.some((token) => haystack.includes(token))) {
|
|
1393
|
+
hits.push(file.path);
|
|
1394
|
+
}
|
|
1395
|
+
if (hits.length >= maxHits)
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
return hits;
|
|
1399
|
+
}
|
|
1400
|
+
function formatRefs(tokens, repoFiles, maxHits = 3) {
|
|
1401
|
+
if (!repoFiles || repoFiles.length === 0) {
|
|
1402
|
+
return "refs: (no repo scan)";
|
|
1403
|
+
}
|
|
1404
|
+
const hits = scanTokens(tokens, repoFiles, maxHits);
|
|
1405
|
+
if (!hits.length)
|
|
1406
|
+
return "refs: none";
|
|
1407
|
+
return `refs: ${hits.join(", ")}`;
|
|
1408
|
+
}
|
|
1409
|
+
function humanizeChange(change) {
|
|
1410
|
+
const label = change.type.replace(/_/g, " ");
|
|
1411
|
+
return `${label} ${change.target}`;
|
|
1412
|
+
}
|
|
1413
|
+
function buildStatement(change, refs, surfaces) {
|
|
1414
|
+
const reason = change.detail || `touches ${surfaces.join(", ")}`;
|
|
1415
|
+
return `${humanizeChange(change)} because ${reason} (${refs})`;
|
|
1416
|
+
}
|
|
1417
|
+
function impactEngine(intent, options = {}) {
|
|
1418
|
+
const reportId = options.reportId ?? `impact-${slugify2(intent.featureKey)}`;
|
|
1419
|
+
const patchId = options.patchId ?? `patch-${slugify2(intent.featureKey)}`;
|
|
1420
|
+
const maxHitsPerChange = options.maxHitsPerChange ?? 3;
|
|
1421
|
+
const breaks = [];
|
|
1422
|
+
const mustChange = [];
|
|
1423
|
+
const risky = [];
|
|
1424
|
+
const surfaces = {
|
|
1425
|
+
api: [],
|
|
1426
|
+
db: [],
|
|
1427
|
+
ui: [],
|
|
1428
|
+
workflows: [],
|
|
1429
|
+
policy: [],
|
|
1430
|
+
docs: [],
|
|
1431
|
+
tests: []
|
|
1432
|
+
};
|
|
1433
|
+
for (const change of intent.changes) {
|
|
1434
|
+
const bucket = BUCKET_MAP[change.type] ?? "risky";
|
|
1435
|
+
const surfaceTargets = SURFACE_MAP[change.type] ?? ["docs", "tests"];
|
|
1436
|
+
const tokens = buildTokens(change);
|
|
1437
|
+
const refs = formatRefs(tokens, options.repoFiles, maxHitsPerChange);
|
|
1438
|
+
const statement = buildStatement(change, refs, surfaceTargets);
|
|
1439
|
+
if (bucket === "breaks")
|
|
1440
|
+
breaks.push(statement);
|
|
1441
|
+
if (bucket === "mustChange")
|
|
1442
|
+
mustChange.push(statement);
|
|
1443
|
+
if (bucket === "risky")
|
|
1444
|
+
risky.push(statement);
|
|
1445
|
+
for (const surface of surfaceTargets) {
|
|
1446
|
+
const list = surfaces[surface];
|
|
1447
|
+
if (Array.isArray(list)) {
|
|
1448
|
+
list.push(statement);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
const summary = [
|
|
1453
|
+
`Analyzed ${intent.changes.length} change(s).`,
|
|
1454
|
+
`Breaks: ${breaks.length}.`,
|
|
1455
|
+
`Must change: ${mustChange.length}.`,
|
|
1456
|
+
`Risky: ${risky.length}.`
|
|
1457
|
+
].join(" ");
|
|
1458
|
+
return {
|
|
1459
|
+
reportId,
|
|
1460
|
+
patchId,
|
|
1461
|
+
summary,
|
|
1462
|
+
breaks,
|
|
1463
|
+
mustChange,
|
|
1464
|
+
risky,
|
|
1465
|
+
surfaces
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
// src/project-management-sync.ts
|
|
1469
|
+
function buildProjectManagementSyncPayload(params) {
|
|
1470
|
+
const options = params.options ?? {};
|
|
1471
|
+
const items = buildWorkItemsFromTickets(params.tickets, options);
|
|
1472
|
+
const summary = options.includeSummary ? buildSummaryWorkItem({
|
|
1473
|
+
question: params.question,
|
|
1474
|
+
tickets: params.tickets,
|
|
1475
|
+
patchIntent: params.patchIntent,
|
|
1476
|
+
impact: params.impact,
|
|
1477
|
+
title: options.summaryTitle,
|
|
1478
|
+
baseTags: options.baseTags
|
|
1479
|
+
}) : undefined;
|
|
1480
|
+
return { summary, items };
|
|
1481
|
+
}
|
|
1482
|
+
function buildWorkItemsFromTickets(tickets, options = {}) {
|
|
1483
|
+
return tickets.map((ticket) => ({
|
|
1484
|
+
title: ticket.title,
|
|
1485
|
+
description: renderTicketDescription(ticket),
|
|
1486
|
+
type: "task",
|
|
1487
|
+
priority: mapPriority(ticket.priority, options.defaultPriority),
|
|
1488
|
+
tags: mergeTags(options.baseTags, ticket.tags),
|
|
1489
|
+
externalId: ticket.ticketId
|
|
1490
|
+
}));
|
|
1491
|
+
}
|
|
1492
|
+
function buildSummaryWorkItem(params) {
|
|
1493
|
+
return {
|
|
1494
|
+
title: params.title ?? "Product Intent Summary",
|
|
1495
|
+
description: renderSummaryMarkdown(params),
|
|
1496
|
+
type: "summary",
|
|
1497
|
+
tags: mergeTags(params.baseTags, ["product-intent", "summary"])
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
function renderTicketDescription(ticket) {
|
|
1501
|
+
const lines = [
|
|
1502
|
+
ticket.summary,
|
|
1503
|
+
"",
|
|
1504
|
+
"Acceptance Criteria:",
|
|
1505
|
+
...ticket.acceptanceCriteria.map((criterion) => `- ${criterion}`)
|
|
1506
|
+
];
|
|
1507
|
+
if (ticket.evidenceIds.length > 0) {
|
|
1508
|
+
lines.push("", `Evidence: ${ticket.evidenceIds.join(", ")}`);
|
|
1509
|
+
}
|
|
1510
|
+
return lines.join(`
|
|
1511
|
+
`);
|
|
1512
|
+
}
|
|
1513
|
+
function renderSummaryMarkdown(params) {
|
|
1514
|
+
const lines = [`# ${params.question}`, "", "## Top Tickets"];
|
|
1515
|
+
for (const ticket of params.tickets) {
|
|
1516
|
+
lines.push(`- ${ticket.title}`);
|
|
1517
|
+
}
|
|
1518
|
+
if (params.patchIntent) {
|
|
1519
|
+
lines.push("", "## Patch Intent", `Feature: ${params.patchIntent.featureKey}`);
|
|
1520
|
+
params.patchIntent.changes.forEach((change) => {
|
|
1521
|
+
lines.push(`- ${change.type}: ${change.target}`);
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
if (params.impact) {
|
|
1525
|
+
lines.push("", "## Impact Summary", params.impact.summary);
|
|
1526
|
+
}
|
|
1527
|
+
return lines.join(`
|
|
1528
|
+
`);
|
|
1529
|
+
}
|
|
1530
|
+
function mapPriority(priority, fallback) {
|
|
1531
|
+
if (!priority)
|
|
1532
|
+
return fallback;
|
|
1533
|
+
switch (priority) {
|
|
1534
|
+
case "high":
|
|
1535
|
+
return "high";
|
|
1536
|
+
case "medium":
|
|
1537
|
+
return "medium";
|
|
1538
|
+
case "low":
|
|
1539
|
+
return "low";
|
|
1540
|
+
default:
|
|
1541
|
+
return fallback;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
function mergeTags(baseTags, tags) {
|
|
1545
|
+
const merged = new Set;
|
|
1546
|
+
(baseTags ?? []).forEach((tag) => merged.add(tag));
|
|
1547
|
+
(tags ?? []).forEach((tag) => merged.add(tag));
|
|
1548
|
+
const result = [...merged];
|
|
1549
|
+
return result.length > 0 ? result : undefined;
|
|
1550
|
+
}
|
|
1551
|
+
export {
|
|
1552
|
+
validateTicketCollection,
|
|
1553
|
+
validateTaskPack,
|
|
1554
|
+
validateProblemGrouping,
|
|
1555
|
+
validatePatchIntent,
|
|
1556
|
+
validateOpportunityBrief,
|
|
1557
|
+
validateInsightExtraction,
|
|
1558
|
+
validateImpactReport,
|
|
1559
|
+
validateEvidenceFindingExtraction,
|
|
1560
|
+
validateCitationsInTextBlock,
|
|
1561
|
+
validateCitation,
|
|
1562
|
+
suggestPatch,
|
|
1563
|
+
runWithValidation,
|
|
1564
|
+
retrieveChunks,
|
|
1565
|
+
promptWireframeLayoutJSON,
|
|
1566
|
+
promptWireframeImage,
|
|
1567
|
+
promptSynthesizeBrief,
|
|
1568
|
+
promptSuggestPatchIntent,
|
|
1569
|
+
promptSkepticCheck,
|
|
1570
|
+
promptGroupProblems,
|
|
1571
|
+
promptGenerateTickets,
|
|
1572
|
+
promptGenerateTaskPack,
|
|
1573
|
+
promptGenerateSyntheticInterviews,
|
|
1574
|
+
promptGeneratePatchIntent,
|
|
1575
|
+
promptGenerateImpactReport,
|
|
1576
|
+
promptGenerateGenericSpecOverlay,
|
|
1577
|
+
promptExtractInsights,
|
|
1578
|
+
promptExtractEvidenceFindings,
|
|
1579
|
+
parseStrictJSON,
|
|
1580
|
+
impactEngine,
|
|
1581
|
+
groupProblems,
|
|
1582
|
+
generateTickets,
|
|
1583
|
+
formatEvidenceForModel,
|
|
1584
|
+
extractEvidence,
|
|
1585
|
+
buildWorkItemsFromTickets,
|
|
1586
|
+
buildRepairPromptWithOutput,
|
|
1587
|
+
buildRepairPrompt,
|
|
1588
|
+
buildProjectManagementSyncPayload,
|
|
1589
|
+
buildChunkIndex,
|
|
1590
|
+
JSON_ONLY_RULES,
|
|
1591
|
+
CITATION_RULES
|
|
1592
|
+
};
|