@contractspec/lib.product-intent-utils 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/README.md +53 -30
- package/dist/browser/index.js +458 -455
- package/dist/index.d.ts +5 -5
- package/dist/index.js +458 -455
- package/dist/node/index.js +458 -455
- package/dist/project-management-sync.d.ts +1 -1
- package/dist/validators.d.ts +1 -1
- package/package.json +6 -5
package/dist/browser/index.js
CHANGED
|
@@ -1,3 +1,204 @@
|
|
|
1
|
+
// src/impact-engine.ts
|
|
2
|
+
var SURFACE_MAP = {
|
|
3
|
+
add_field: ["api", "db", "ui", "docs", "tests"],
|
|
4
|
+
remove_field: ["api", "db", "ui", "docs", "tests"],
|
|
5
|
+
rename_field: ["api", "db", "ui", "docs", "tests"],
|
|
6
|
+
add_event: ["api", "workflows", "docs", "tests"],
|
|
7
|
+
update_event: ["api", "workflows", "docs", "tests"],
|
|
8
|
+
add_operation: ["api", "ui", "workflows", "docs", "tests"],
|
|
9
|
+
update_operation: ["api", "ui", "workflows", "docs", "tests"],
|
|
10
|
+
update_form: ["ui", "docs", "tests"],
|
|
11
|
+
update_policy: ["policy", "api", "workflows", "docs", "tests"],
|
|
12
|
+
add_enum_value: ["api", "db", "ui", "docs", "tests"],
|
|
13
|
+
remove_enum_value: ["api", "db", "ui", "docs", "tests"],
|
|
14
|
+
other: ["docs", "tests"]
|
|
15
|
+
};
|
|
16
|
+
var BUCKET_MAP = {
|
|
17
|
+
remove_field: "breaks",
|
|
18
|
+
rename_field: "breaks",
|
|
19
|
+
remove_enum_value: "breaks",
|
|
20
|
+
update_operation: "mustChange",
|
|
21
|
+
update_event: "mustChange",
|
|
22
|
+
update_policy: "mustChange",
|
|
23
|
+
update_form: "risky",
|
|
24
|
+
add_field: "risky",
|
|
25
|
+
add_event: "risky",
|
|
26
|
+
add_operation: "risky",
|
|
27
|
+
add_enum_value: "risky",
|
|
28
|
+
other: "risky"
|
|
29
|
+
};
|
|
30
|
+
function slugify(value) {
|
|
31
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)+/g, "");
|
|
32
|
+
}
|
|
33
|
+
function buildTokens(change) {
|
|
34
|
+
const combined = `${change.type} ${change.target} ${change.detail}`;
|
|
35
|
+
const tokens = combined.split(/[^a-zA-Z0-9]+/).map((token) => token.trim()).filter((token) => token.length >= 3);
|
|
36
|
+
return Array.from(new Set(tokens.map((token) => token.toLowerCase()))).slice(0, 8);
|
|
37
|
+
}
|
|
38
|
+
function scanTokens(tokens, files, maxHits) {
|
|
39
|
+
const hits = [];
|
|
40
|
+
const lowerTokens = tokens.map((token) => token.toLowerCase());
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
const haystack = file.content.toLowerCase();
|
|
43
|
+
if (lowerTokens.some((token) => haystack.includes(token))) {
|
|
44
|
+
hits.push(file.path);
|
|
45
|
+
}
|
|
46
|
+
if (hits.length >= maxHits)
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
return hits;
|
|
50
|
+
}
|
|
51
|
+
function formatRefs(tokens, repoFiles, maxHits = 3) {
|
|
52
|
+
if (!repoFiles || repoFiles.length === 0) {
|
|
53
|
+
return "refs: (no repo scan)";
|
|
54
|
+
}
|
|
55
|
+
const hits = scanTokens(tokens, repoFiles, maxHits);
|
|
56
|
+
if (!hits.length)
|
|
57
|
+
return "refs: none";
|
|
58
|
+
return `refs: ${hits.join(", ")}`;
|
|
59
|
+
}
|
|
60
|
+
function humanizeChange(change) {
|
|
61
|
+
const label = change.type.replace(/_/g, " ");
|
|
62
|
+
return `${label} ${change.target}`;
|
|
63
|
+
}
|
|
64
|
+
function buildStatement(change, refs, surfaces) {
|
|
65
|
+
const reason = change.detail || `touches ${surfaces.join(", ")}`;
|
|
66
|
+
return `${humanizeChange(change)} because ${reason} (${refs})`;
|
|
67
|
+
}
|
|
68
|
+
function impactEngine(intent, options = {}) {
|
|
69
|
+
const reportId = options.reportId ?? `impact-${slugify(intent.featureKey)}`;
|
|
70
|
+
const patchId = options.patchId ?? `patch-${slugify(intent.featureKey)}`;
|
|
71
|
+
const maxHitsPerChange = options.maxHitsPerChange ?? 3;
|
|
72
|
+
const breaks = [];
|
|
73
|
+
const mustChange = [];
|
|
74
|
+
const risky = [];
|
|
75
|
+
const surfaces = {
|
|
76
|
+
api: [],
|
|
77
|
+
db: [],
|
|
78
|
+
ui: [],
|
|
79
|
+
workflows: [],
|
|
80
|
+
policy: [],
|
|
81
|
+
docs: [],
|
|
82
|
+
tests: []
|
|
83
|
+
};
|
|
84
|
+
for (const change of intent.changes) {
|
|
85
|
+
const bucket = BUCKET_MAP[change.type] ?? "risky";
|
|
86
|
+
const surfaceTargets = SURFACE_MAP[change.type] ?? ["docs", "tests"];
|
|
87
|
+
const tokens = buildTokens(change);
|
|
88
|
+
const refs = formatRefs(tokens, options.repoFiles, maxHitsPerChange);
|
|
89
|
+
const statement = buildStatement(change, refs, surfaceTargets);
|
|
90
|
+
if (bucket === "breaks")
|
|
91
|
+
breaks.push(statement);
|
|
92
|
+
if (bucket === "mustChange")
|
|
93
|
+
mustChange.push(statement);
|
|
94
|
+
if (bucket === "risky")
|
|
95
|
+
risky.push(statement);
|
|
96
|
+
for (const surface of surfaceTargets) {
|
|
97
|
+
const list = surfaces[surface];
|
|
98
|
+
if (Array.isArray(list)) {
|
|
99
|
+
list.push(statement);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const summary = [
|
|
104
|
+
`Analyzed ${intent.changes.length} change(s).`,
|
|
105
|
+
`Breaks: ${breaks.length}.`,
|
|
106
|
+
`Must change: ${mustChange.length}.`,
|
|
107
|
+
`Risky: ${risky.length}.`
|
|
108
|
+
].join(" ");
|
|
109
|
+
return {
|
|
110
|
+
reportId,
|
|
111
|
+
patchId,
|
|
112
|
+
summary,
|
|
113
|
+
breaks,
|
|
114
|
+
mustChange,
|
|
115
|
+
risky,
|
|
116
|
+
surfaces
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// src/project-management-sync.ts
|
|
120
|
+
function buildProjectManagementSyncPayload(params) {
|
|
121
|
+
const options = params.options ?? {};
|
|
122
|
+
const items = buildWorkItemsFromTickets(params.tickets, options);
|
|
123
|
+
const summary = options.includeSummary ? buildSummaryWorkItem({
|
|
124
|
+
question: params.question,
|
|
125
|
+
tickets: params.tickets,
|
|
126
|
+
patchIntent: params.patchIntent,
|
|
127
|
+
impact: params.impact,
|
|
128
|
+
title: options.summaryTitle,
|
|
129
|
+
baseTags: options.baseTags
|
|
130
|
+
}) : undefined;
|
|
131
|
+
return { summary, items };
|
|
132
|
+
}
|
|
133
|
+
function buildWorkItemsFromTickets(tickets, options = {}) {
|
|
134
|
+
return tickets.map((ticket) => ({
|
|
135
|
+
title: ticket.title,
|
|
136
|
+
description: renderTicketDescription(ticket),
|
|
137
|
+
type: "task",
|
|
138
|
+
priority: mapPriority(ticket.priority, options.defaultPriority),
|
|
139
|
+
tags: mergeTags(options.baseTags, ticket.tags),
|
|
140
|
+
externalId: ticket.ticketId
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
function buildSummaryWorkItem(params) {
|
|
144
|
+
return {
|
|
145
|
+
title: params.title ?? "Product Intent Summary",
|
|
146
|
+
description: renderSummaryMarkdown(params),
|
|
147
|
+
type: "summary",
|
|
148
|
+
tags: mergeTags(params.baseTags, ["product-intent", "summary"])
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function renderTicketDescription(ticket) {
|
|
152
|
+
const lines = [
|
|
153
|
+
ticket.summary,
|
|
154
|
+
"",
|
|
155
|
+
"Acceptance Criteria:",
|
|
156
|
+
...ticket.acceptanceCriteria.map((criterion) => `- ${criterion}`)
|
|
157
|
+
];
|
|
158
|
+
if (ticket.evidenceIds.length > 0) {
|
|
159
|
+
lines.push("", `Evidence: ${ticket.evidenceIds.join(", ")}`);
|
|
160
|
+
}
|
|
161
|
+
return lines.join(`
|
|
162
|
+
`);
|
|
163
|
+
}
|
|
164
|
+
function renderSummaryMarkdown(params) {
|
|
165
|
+
const lines = [`# ${params.question}`, "", "## Top Tickets"];
|
|
166
|
+
for (const ticket of params.tickets) {
|
|
167
|
+
lines.push(`- ${ticket.title}`);
|
|
168
|
+
}
|
|
169
|
+
if (params.patchIntent) {
|
|
170
|
+
lines.push("", "## Patch Intent", `Feature: ${params.patchIntent.featureKey}`);
|
|
171
|
+
params.patchIntent.changes.forEach((change) => {
|
|
172
|
+
lines.push(`- ${change.type}: ${change.target}`);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (params.impact) {
|
|
176
|
+
lines.push("", "## Impact Summary", params.impact.summary);
|
|
177
|
+
}
|
|
178
|
+
return lines.join(`
|
|
179
|
+
`);
|
|
180
|
+
}
|
|
181
|
+
function mapPriority(priority, fallback) {
|
|
182
|
+
if (!priority)
|
|
183
|
+
return fallback;
|
|
184
|
+
switch (priority) {
|
|
185
|
+
case "high":
|
|
186
|
+
return "high";
|
|
187
|
+
case "medium":
|
|
188
|
+
return "medium";
|
|
189
|
+
case "low":
|
|
190
|
+
return "low";
|
|
191
|
+
default:
|
|
192
|
+
return fallback;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function mergeTags(baseTags, tags) {
|
|
196
|
+
const merged = new Set;
|
|
197
|
+
(baseTags ?? []).forEach((tag) => merged.add(tag));
|
|
198
|
+
(tags ?? []).forEach((tag) => merged.add(tag));
|
|
199
|
+
const result = [...merged];
|
|
200
|
+
return result.length > 0 ? result : undefined;
|
|
201
|
+
}
|
|
1
202
|
// src/prompts.ts
|
|
2
203
|
function formatEvidenceForModel(chunks, maxChars = 900) {
|
|
3
204
|
const safe = chunks.map((chunk) => ({
|
|
@@ -321,147 +522,30 @@ Return JSON:
|
|
|
321
522
|
${JSON_ONLY_RULES}
|
|
322
523
|
`.trim();
|
|
323
524
|
}
|
|
324
|
-
// src/ticket-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
}
|
|
525
|
+
// src/ticket-pipeline.ts
|
|
526
|
+
import {
|
|
527
|
+
ContractPatchIntentModel as ContractPatchIntentModel2,
|
|
528
|
+
EvidenceFindingExtractionModel as EvidenceFindingExtractionModel2,
|
|
529
|
+
ProblemGroupingModel as ProblemGroupingModel2,
|
|
530
|
+
TicketCollectionModel as TicketCollectionModel2
|
|
531
|
+
} from "@contractspec/lib.contracts-spec/product-intent/types";
|
|
346
532
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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-spec/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
|
-
}
|
|
533
|
+
// src/validators.ts
|
|
534
|
+
import {
|
|
535
|
+
CitationModel,
|
|
536
|
+
ContractPatchIntentModel,
|
|
537
|
+
ImpactReportModel,
|
|
538
|
+
InsightExtractionModel,
|
|
539
|
+
OpportunityBriefModel,
|
|
540
|
+
TaskPackModel
|
|
541
|
+
} from "@contractspec/lib.contracts-spec/product-intent/types";
|
|
542
|
+
function assertStringLength(value, path, bounds) {
|
|
543
|
+
if (bounds.min !== undefined && value.length < bounds.min) {
|
|
544
|
+
throw new Error(`Expected ${path} to be at least ${bounds.min} characters, got ${value.length}`);
|
|
545
|
+
}
|
|
546
|
+
if (bounds.max !== undefined && value.length > bounds.max) {
|
|
547
|
+
throw new Error(`Expected ${path} to be at most ${bounds.max} characters, got ${value.length}`);
|
|
548
|
+
}
|
|
465
549
|
}
|
|
466
550
|
function assertArrayLength(value, path, bounds) {
|
|
467
551
|
if (bounds.min !== undefined && value.length < bounds.min) {
|
|
@@ -719,6 +803,231 @@ function buildRepairPromptWithOutput(error, previousOutput, maxOutputChars = 400
|
|
|
719
803
|
].join(`
|
|
720
804
|
`);
|
|
721
805
|
}
|
|
806
|
+
|
|
807
|
+
// src/ticket-pipeline-runner.ts
|
|
808
|
+
var DEFAULT_MAX_ATTEMPTS = 2;
|
|
809
|
+
function timestamp() {
|
|
810
|
+
return new Date().toISOString();
|
|
811
|
+
}
|
|
812
|
+
function toErrorMessage(error) {
|
|
813
|
+
return error instanceof Error ? error.message : String(error);
|
|
814
|
+
}
|
|
815
|
+
async function safeLog(logger, entry) {
|
|
816
|
+
if (!logger)
|
|
817
|
+
return;
|
|
818
|
+
try {
|
|
819
|
+
await logger.log(entry);
|
|
820
|
+
} catch {}
|
|
821
|
+
}
|
|
822
|
+
async function runWithValidation(options) {
|
|
823
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
|
|
824
|
+
let attempt = 0;
|
|
825
|
+
let lastError;
|
|
826
|
+
let lastRaw = "";
|
|
827
|
+
let currentPrompt = options.prompt;
|
|
828
|
+
while (attempt < maxAttempts) {
|
|
829
|
+
attempt += 1;
|
|
830
|
+
await safeLog(options.logger, {
|
|
831
|
+
stage: options.stage,
|
|
832
|
+
phase: "request",
|
|
833
|
+
attempt,
|
|
834
|
+
prompt: currentPrompt,
|
|
835
|
+
timestamp: timestamp()
|
|
836
|
+
});
|
|
837
|
+
let raw;
|
|
838
|
+
try {
|
|
839
|
+
raw = await options.modelRunner.generateJson(currentPrompt);
|
|
840
|
+
} catch (error) {
|
|
841
|
+
lastError = toErrorMessage(error);
|
|
842
|
+
await safeLog(options.logger, {
|
|
843
|
+
stage: options.stage,
|
|
844
|
+
phase: "model_error",
|
|
845
|
+
attempt,
|
|
846
|
+
prompt: currentPrompt,
|
|
847
|
+
error: lastError,
|
|
848
|
+
timestamp: timestamp()
|
|
849
|
+
});
|
|
850
|
+
throw new Error(`[${options.stage}] Model error: ${lastError}`);
|
|
851
|
+
}
|
|
852
|
+
await safeLog(options.logger, {
|
|
853
|
+
stage: options.stage,
|
|
854
|
+
phase: "response",
|
|
855
|
+
attempt,
|
|
856
|
+
prompt: currentPrompt,
|
|
857
|
+
response: raw,
|
|
858
|
+
timestamp: timestamp()
|
|
859
|
+
});
|
|
860
|
+
try {
|
|
861
|
+
return options.validate(raw);
|
|
862
|
+
} catch (error) {
|
|
863
|
+
lastError = toErrorMessage(error);
|
|
864
|
+
lastRaw = raw;
|
|
865
|
+
if (options.repair) {
|
|
866
|
+
const repaired = options.repair(raw, lastError);
|
|
867
|
+
if (repaired && repaired !== raw) {
|
|
868
|
+
await safeLog(options.logger, {
|
|
869
|
+
stage: options.stage,
|
|
870
|
+
phase: "repair",
|
|
871
|
+
attempt,
|
|
872
|
+
prompt: currentPrompt,
|
|
873
|
+
response: repaired,
|
|
874
|
+
error: lastError,
|
|
875
|
+
timestamp: timestamp()
|
|
876
|
+
});
|
|
877
|
+
try {
|
|
878
|
+
return options.validate(repaired);
|
|
879
|
+
} catch (repairError) {
|
|
880
|
+
lastError = toErrorMessage(repairError);
|
|
881
|
+
lastRaw = repaired;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
await safeLog(options.logger, {
|
|
886
|
+
stage: options.stage,
|
|
887
|
+
phase: "validation_error",
|
|
888
|
+
attempt,
|
|
889
|
+
prompt: currentPrompt,
|
|
890
|
+
response: lastRaw,
|
|
891
|
+
error: lastError,
|
|
892
|
+
timestamp: timestamp()
|
|
893
|
+
});
|
|
894
|
+
currentPrompt = [
|
|
895
|
+
options.prompt,
|
|
896
|
+
buildRepairPromptWithOutput(lastError, lastRaw)
|
|
897
|
+
].join(`
|
|
898
|
+
|
|
899
|
+
`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
throw new Error(`[${options.stage}] Validation failed after ${maxAttempts} attempt(s): ${lastError ?? "unknown error"}`);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/ticket-prompts.ts
|
|
906
|
+
function promptExtractEvidenceFindings(params) {
|
|
907
|
+
return `
|
|
908
|
+
You are extracting evidence findings grounded in transcript excerpts.
|
|
909
|
+
|
|
910
|
+
Question:
|
|
911
|
+
${params.question}
|
|
912
|
+
|
|
913
|
+
Evidence:
|
|
914
|
+
${params.evidenceJSON}
|
|
915
|
+
|
|
916
|
+
Return JSON:
|
|
917
|
+
{
|
|
918
|
+
"findings": [
|
|
919
|
+
{
|
|
920
|
+
"findingId": "find_001",
|
|
921
|
+
"summary": "...",
|
|
922
|
+
"tags": ["..."],
|
|
923
|
+
"citations": [{ "chunkId": "...", "quote": "..." }]
|
|
924
|
+
}
|
|
925
|
+
]
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
Rules:
|
|
929
|
+
- Produce 8 to 18 findings.
|
|
930
|
+
- Each finding must include at least 1 citation.
|
|
931
|
+
- Summaries must be specific and short.
|
|
932
|
+
- Quotes must be copied character-for-character from the chunk text (no paraphrasing, no ellipses).
|
|
933
|
+
- Preserve punctuation, smart quotes, and special hyphens exactly as shown in the chunk text.
|
|
934
|
+
${CITATION_RULES}
|
|
935
|
+
${JSON_ONLY_RULES}
|
|
936
|
+
`.trim();
|
|
937
|
+
}
|
|
938
|
+
function promptGroupProblems(params) {
|
|
939
|
+
const allowed = JSON.stringify({ findingIds: params.findingIds }, null, 2);
|
|
940
|
+
return `
|
|
941
|
+
You are grouping evidence findings into problem statements.
|
|
942
|
+
|
|
943
|
+
Question:
|
|
944
|
+
${params.question}
|
|
945
|
+
|
|
946
|
+
Findings:
|
|
947
|
+
${params.findingsJSON}
|
|
948
|
+
|
|
949
|
+
Allowed finding IDs:
|
|
950
|
+
${allowed}
|
|
951
|
+
|
|
952
|
+
Return JSON:
|
|
953
|
+
{
|
|
954
|
+
"problems": [
|
|
955
|
+
{
|
|
956
|
+
"problemId": "prob_001",
|
|
957
|
+
"statement": "...",
|
|
958
|
+
"evidenceIds": ["find_001"],
|
|
959
|
+
"tags": ["..."],
|
|
960
|
+
"severity": "low|medium|high"
|
|
961
|
+
}
|
|
962
|
+
]
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
Rules:
|
|
966
|
+
- Each problem must reference 1 to 6 evidenceIds.
|
|
967
|
+
- evidenceIds must be drawn from the allowed finding IDs.
|
|
968
|
+
- Keep statements short and actionable.
|
|
969
|
+
${JSON_ONLY_RULES}
|
|
970
|
+
`.trim();
|
|
971
|
+
}
|
|
972
|
+
function promptGenerateTickets(params) {
|
|
973
|
+
return `
|
|
974
|
+
You are generating implementation tickets grounded in evidence.
|
|
975
|
+
|
|
976
|
+
Question:
|
|
977
|
+
${params.question}
|
|
978
|
+
|
|
979
|
+
Problems:
|
|
980
|
+
${params.problemsJSON}
|
|
981
|
+
|
|
982
|
+
Evidence findings:
|
|
983
|
+
${params.findingsJSON}
|
|
984
|
+
|
|
985
|
+
Return JSON:
|
|
986
|
+
{
|
|
987
|
+
"tickets": [
|
|
988
|
+
{
|
|
989
|
+
"ticketId": "t_001",
|
|
990
|
+
"title": "...",
|
|
991
|
+
"summary": "...",
|
|
992
|
+
"evidenceIds": ["find_001"],
|
|
993
|
+
"acceptanceCriteria": ["..."]
|
|
994
|
+
}
|
|
995
|
+
]
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
Rules:
|
|
999
|
+
- 1 to 2 tickets per problem.
|
|
1000
|
+
- Every ticket must include evidenceIds and acceptanceCriteria.
|
|
1001
|
+
- Acceptance criteria must be testable.
|
|
1002
|
+
- Each acceptanceCriteria item must be <= 160 characters.
|
|
1003
|
+
${JSON_ONLY_RULES}
|
|
1004
|
+
`.trim();
|
|
1005
|
+
}
|
|
1006
|
+
function promptSuggestPatchIntent(params) {
|
|
1007
|
+
return `
|
|
1008
|
+
You are generating a ContractPatchIntent from an evidence-backed ticket.
|
|
1009
|
+
|
|
1010
|
+
Ticket:
|
|
1011
|
+
${params.ticketJSON}
|
|
1012
|
+
|
|
1013
|
+
Return JSON:
|
|
1014
|
+
{
|
|
1015
|
+
"featureKey": "feature_slug",
|
|
1016
|
+
"changes": [
|
|
1017
|
+
{ "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" }
|
|
1018
|
+
],
|
|
1019
|
+
"acceptanceCriteria": ["..."]
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
Rules:
|
|
1023
|
+
- Keep changes <= 8.
|
|
1024
|
+
- Each change must be concrete and scoped.
|
|
1025
|
+
- Acceptance criteria must be testable and derived from the ticket.
|
|
1026
|
+
- Each acceptanceCriteria item must be <= 140 characters.
|
|
1027
|
+
${JSON_ONLY_RULES}
|
|
1028
|
+
`.trim();
|
|
1029
|
+
}
|
|
1030
|
+
|
|
722
1031
|
// src/ticket-validators.ts
|
|
723
1032
|
import {
|
|
724
1033
|
EvidenceFindingExtractionModel,
|
|
@@ -818,122 +1127,17 @@ function validateTicketCollection(raw, findings) {
|
|
|
818
1127
|
});
|
|
819
1128
|
for (const criterion of ticket.acceptanceCriteria) {
|
|
820
1129
|
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-spec/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()
|
|
1130
|
+
min: 1,
|
|
1131
|
+
max: 280
|
|
883
1132
|
});
|
|
884
|
-
throw new Error(`[${options.stage}] Model error: ${lastError}`);
|
|
885
1133
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
}
|
|
1134
|
+
if (ticket.tags) {
|
|
1135
|
+
for (const tag of ticket.tags) {
|
|
1136
|
+
assertStringLength2(tag, "tickets[].tags[]", { min: 1, max: 48 });
|
|
918
1137
|
}
|
|
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
1138
|
}
|
|
935
1139
|
}
|
|
936
|
-
|
|
1140
|
+
return data;
|
|
937
1141
|
}
|
|
938
1142
|
|
|
939
1143
|
// src/ticket-pipeline.ts
|
|
@@ -946,7 +1150,7 @@ var TAG_HINTS = {
|
|
|
946
1150
|
performance: ["slow", "latency", "performance"],
|
|
947
1151
|
integrations: ["integration", "api", "webhook"]
|
|
948
1152
|
};
|
|
949
|
-
function
|
|
1153
|
+
function slugify2(value) {
|
|
950
1154
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)+/g, "");
|
|
951
1155
|
}
|
|
952
1156
|
function pickQuote(text, maxLen = 220) {
|
|
@@ -1195,7 +1399,7 @@ function repairPatchIntent(raw) {
|
|
|
1195
1399
|
}
|
|
1196
1400
|
function retrieveChunks(transcript, question, options = {}) {
|
|
1197
1401
|
const chunkSize = options.chunkSize ?? 800;
|
|
1198
|
-
const sourceId = options.sourceId ??
|
|
1402
|
+
const sourceId = options.sourceId ?? slugify2(question || "transcript");
|
|
1199
1403
|
const clean = transcript.trim();
|
|
1200
1404
|
const chunks = [];
|
|
1201
1405
|
for (let offset = 0, idx = 0;offset < clean.length; idx += 1) {
|
|
@@ -1333,7 +1537,7 @@ async function suggestPatch(ticket, options = {}) {
|
|
|
1333
1537
|
validate: (raw) => validatePatchIntent(raw)
|
|
1334
1538
|
});
|
|
1335
1539
|
}
|
|
1336
|
-
const featureKey =
|
|
1540
|
+
const featureKey = slugify2(ticket.title) || "product_intent_ticket";
|
|
1337
1541
|
const intent = {
|
|
1338
1542
|
featureKey,
|
|
1339
1543
|
changes: [
|
|
@@ -1347,207 +1551,6 @@ async function suggestPatch(ticket, options = {}) {
|
|
|
1347
1551
|
};
|
|
1348
1552
|
return validatePatchIntent(JSON.stringify(intent, null, 2));
|
|
1349
1553
|
}
|
|
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
1554
|
export {
|
|
1552
1555
|
validateTicketCollection,
|
|
1553
1556
|
validateTaskPack,
|