@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.
@@ -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-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
- }
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
- 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-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
- 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
- }
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
- throw new Error(`[${options.stage}] Validation failed after ${maxAttempts} attempt(s): ${lastError ?? "unknown error"}`);
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 slugify(value) {
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 ?? slugify(question || "transcript");
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 = slugify(ticket.title) || "product_intent_ticket";
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,