@contractspec/lib.product-intent-utils 3.7.6 → 3.7.10

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/index.js CHANGED
@@ -1,4 +1,205 @@
1
1
  // @bun
2
+ // src/impact-engine.ts
3
+ var SURFACE_MAP = {
4
+ add_field: ["api", "db", "ui", "docs", "tests"],
5
+ remove_field: ["api", "db", "ui", "docs", "tests"],
6
+ rename_field: ["api", "db", "ui", "docs", "tests"],
7
+ add_event: ["api", "workflows", "docs", "tests"],
8
+ update_event: ["api", "workflows", "docs", "tests"],
9
+ add_operation: ["api", "ui", "workflows", "docs", "tests"],
10
+ update_operation: ["api", "ui", "workflows", "docs", "tests"],
11
+ update_form: ["ui", "docs", "tests"],
12
+ update_policy: ["policy", "api", "workflows", "docs", "tests"],
13
+ add_enum_value: ["api", "db", "ui", "docs", "tests"],
14
+ remove_enum_value: ["api", "db", "ui", "docs", "tests"],
15
+ other: ["docs", "tests"]
16
+ };
17
+ var BUCKET_MAP = {
18
+ remove_field: "breaks",
19
+ rename_field: "breaks",
20
+ remove_enum_value: "breaks",
21
+ update_operation: "mustChange",
22
+ update_event: "mustChange",
23
+ update_policy: "mustChange",
24
+ update_form: "risky",
25
+ add_field: "risky",
26
+ add_event: "risky",
27
+ add_operation: "risky",
28
+ add_enum_value: "risky",
29
+ other: "risky"
30
+ };
31
+ function slugify(value) {
32
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)+/g, "");
33
+ }
34
+ function buildTokens(change) {
35
+ const combined = `${change.type} ${change.target} ${change.detail}`;
36
+ const tokens = combined.split(/[^a-zA-Z0-9]+/).map((token) => token.trim()).filter((token) => token.length >= 3);
37
+ return Array.from(new Set(tokens.map((token) => token.toLowerCase()))).slice(0, 8);
38
+ }
39
+ function scanTokens(tokens, files, maxHits) {
40
+ const hits = [];
41
+ const lowerTokens = tokens.map((token) => token.toLowerCase());
42
+ for (const file of files) {
43
+ const haystack = file.content.toLowerCase();
44
+ if (lowerTokens.some((token) => haystack.includes(token))) {
45
+ hits.push(file.path);
46
+ }
47
+ if (hits.length >= maxHits)
48
+ break;
49
+ }
50
+ return hits;
51
+ }
52
+ function formatRefs(tokens, repoFiles, maxHits = 3) {
53
+ if (!repoFiles || repoFiles.length === 0) {
54
+ return "refs: (no repo scan)";
55
+ }
56
+ const hits = scanTokens(tokens, repoFiles, maxHits);
57
+ if (!hits.length)
58
+ return "refs: none";
59
+ return `refs: ${hits.join(", ")}`;
60
+ }
61
+ function humanizeChange(change) {
62
+ const label = change.type.replace(/_/g, " ");
63
+ return `${label} ${change.target}`;
64
+ }
65
+ function buildStatement(change, refs, surfaces) {
66
+ const reason = change.detail || `touches ${surfaces.join(", ")}`;
67
+ return `${humanizeChange(change)} because ${reason} (${refs})`;
68
+ }
69
+ function impactEngine(intent, options = {}) {
70
+ const reportId = options.reportId ?? `impact-${slugify(intent.featureKey)}`;
71
+ const patchId = options.patchId ?? `patch-${slugify(intent.featureKey)}`;
72
+ const maxHitsPerChange = options.maxHitsPerChange ?? 3;
73
+ const breaks = [];
74
+ const mustChange = [];
75
+ const risky = [];
76
+ const surfaces = {
77
+ api: [],
78
+ db: [],
79
+ ui: [],
80
+ workflows: [],
81
+ policy: [],
82
+ docs: [],
83
+ tests: []
84
+ };
85
+ for (const change of intent.changes) {
86
+ const bucket = BUCKET_MAP[change.type] ?? "risky";
87
+ const surfaceTargets = SURFACE_MAP[change.type] ?? ["docs", "tests"];
88
+ const tokens = buildTokens(change);
89
+ const refs = formatRefs(tokens, options.repoFiles, maxHitsPerChange);
90
+ const statement = buildStatement(change, refs, surfaceTargets);
91
+ if (bucket === "breaks")
92
+ breaks.push(statement);
93
+ if (bucket === "mustChange")
94
+ mustChange.push(statement);
95
+ if (bucket === "risky")
96
+ risky.push(statement);
97
+ for (const surface of surfaceTargets) {
98
+ const list = surfaces[surface];
99
+ if (Array.isArray(list)) {
100
+ list.push(statement);
101
+ }
102
+ }
103
+ }
104
+ const summary = [
105
+ `Analyzed ${intent.changes.length} change(s).`,
106
+ `Breaks: ${breaks.length}.`,
107
+ `Must change: ${mustChange.length}.`,
108
+ `Risky: ${risky.length}.`
109
+ ].join(" ");
110
+ return {
111
+ reportId,
112
+ patchId,
113
+ summary,
114
+ breaks,
115
+ mustChange,
116
+ risky,
117
+ surfaces
118
+ };
119
+ }
120
+ // src/project-management-sync.ts
121
+ function buildProjectManagementSyncPayload(params) {
122
+ const options = params.options ?? {};
123
+ const items = buildWorkItemsFromTickets(params.tickets, options);
124
+ const summary = options.includeSummary ? buildSummaryWorkItem({
125
+ question: params.question,
126
+ tickets: params.tickets,
127
+ patchIntent: params.patchIntent,
128
+ impact: params.impact,
129
+ title: options.summaryTitle,
130
+ baseTags: options.baseTags
131
+ }) : undefined;
132
+ return { summary, items };
133
+ }
134
+ function buildWorkItemsFromTickets(tickets, options = {}) {
135
+ return tickets.map((ticket) => ({
136
+ title: ticket.title,
137
+ description: renderTicketDescription(ticket),
138
+ type: "task",
139
+ priority: mapPriority(ticket.priority, options.defaultPriority),
140
+ tags: mergeTags(options.baseTags, ticket.tags),
141
+ externalId: ticket.ticketId
142
+ }));
143
+ }
144
+ function buildSummaryWorkItem(params) {
145
+ return {
146
+ title: params.title ?? "Product Intent Summary",
147
+ description: renderSummaryMarkdown(params),
148
+ type: "summary",
149
+ tags: mergeTags(params.baseTags, ["product-intent", "summary"])
150
+ };
151
+ }
152
+ function renderTicketDescription(ticket) {
153
+ const lines = [
154
+ ticket.summary,
155
+ "",
156
+ "Acceptance Criteria:",
157
+ ...ticket.acceptanceCriteria.map((criterion) => `- ${criterion}`)
158
+ ];
159
+ if (ticket.evidenceIds.length > 0) {
160
+ lines.push("", `Evidence: ${ticket.evidenceIds.join(", ")}`);
161
+ }
162
+ return lines.join(`
163
+ `);
164
+ }
165
+ function renderSummaryMarkdown(params) {
166
+ const lines = [`# ${params.question}`, "", "## Top Tickets"];
167
+ for (const ticket of params.tickets) {
168
+ lines.push(`- ${ticket.title}`);
169
+ }
170
+ if (params.patchIntent) {
171
+ lines.push("", "## Patch Intent", `Feature: ${params.patchIntent.featureKey}`);
172
+ params.patchIntent.changes.forEach((change) => {
173
+ lines.push(`- ${change.type}: ${change.target}`);
174
+ });
175
+ }
176
+ if (params.impact) {
177
+ lines.push("", "## Impact Summary", params.impact.summary);
178
+ }
179
+ return lines.join(`
180
+ `);
181
+ }
182
+ function mapPriority(priority, fallback) {
183
+ if (!priority)
184
+ return fallback;
185
+ switch (priority) {
186
+ case "high":
187
+ return "high";
188
+ case "medium":
189
+ return "medium";
190
+ case "low":
191
+ return "low";
192
+ default:
193
+ return fallback;
194
+ }
195
+ }
196
+ function mergeTags(baseTags, tags) {
197
+ const merged = new Set;
198
+ (baseTags ?? []).forEach((tag) => merged.add(tag));
199
+ (tags ?? []).forEach((tag) => merged.add(tag));
200
+ const result = [...merged];
201
+ return result.length > 0 ? result : undefined;
202
+ }
2
203
  // src/prompts.ts
3
204
  function formatEvidenceForModel(chunks, maxChars = 900) {
4
205
  const safe = chunks.map((chunk) => ({
@@ -322,147 +523,30 @@ Return JSON:
322
523
  ${JSON_ONLY_RULES}
323
524
  `.trim();
324
525
  }
325
- // src/ticket-prompts.ts
326
- function promptExtractEvidenceFindings(params) {
327
- return `
328
- You are extracting evidence findings grounded in transcript excerpts.
329
-
330
- Question:
331
- ${params.question}
332
-
333
- Evidence:
334
- ${params.evidenceJSON}
335
-
336
- Return JSON:
337
- {
338
- "findings": [
339
- {
340
- "findingId": "find_001",
341
- "summary": "...",
342
- "tags": ["..."],
343
- "citations": [{ "chunkId": "...", "quote": "..." }]
344
- }
345
- ]
346
- }
526
+ // src/ticket-pipeline.ts
527
+ import {
528
+ ContractPatchIntentModel as ContractPatchIntentModel2,
529
+ EvidenceFindingExtractionModel as EvidenceFindingExtractionModel2,
530
+ ProblemGroupingModel as ProblemGroupingModel2,
531
+ TicketCollectionModel as TicketCollectionModel2
532
+ } from "@contractspec/lib.contracts-spec/product-intent/types";
347
533
 
348
- Rules:
349
- - Produce 8 to 18 findings.
350
- - Each finding must include at least 1 citation.
351
- - Summaries must be specific and short.
352
- - Quotes must be copied character-for-character from the chunk text (no paraphrasing, no ellipses).
353
- - Preserve punctuation, smart quotes, and special hyphens exactly as shown in the chunk text.
354
- ${CITATION_RULES}
355
- ${JSON_ONLY_RULES}
356
- `.trim();
357
- }
358
- function promptGroupProblems(params) {
359
- const allowed = JSON.stringify({ findingIds: params.findingIds }, null, 2);
360
- return `
361
- You are grouping evidence findings into problem statements.
362
-
363
- Question:
364
- ${params.question}
365
-
366
- Findings:
367
- ${params.findingsJSON}
368
-
369
- Allowed finding IDs:
370
- ${allowed}
371
-
372
- Return JSON:
373
- {
374
- "problems": [
375
- {
376
- "problemId": "prob_001",
377
- "statement": "...",
378
- "evidenceIds": ["find_001"],
379
- "tags": ["..."],
380
- "severity": "low|medium|high"
381
- }
382
- ]
383
- }
384
-
385
- Rules:
386
- - Each problem must reference 1 to 6 evidenceIds.
387
- - evidenceIds must be drawn from the allowed finding IDs.
388
- - Keep statements short and actionable.
389
- ${JSON_ONLY_RULES}
390
- `.trim();
391
- }
392
- function promptGenerateTickets(params) {
393
- return `
394
- You are generating implementation tickets grounded in evidence.
395
-
396
- Question:
397
- ${params.question}
398
-
399
- Problems:
400
- ${params.problemsJSON}
401
-
402
- Evidence findings:
403
- ${params.findingsJSON}
404
-
405
- Return JSON:
406
- {
407
- "tickets": [
408
- {
409
- "ticketId": "t_001",
410
- "title": "...",
411
- "summary": "...",
412
- "evidenceIds": ["find_001"],
413
- "acceptanceCriteria": ["..."]
414
- }
415
- ]
416
- }
417
-
418
- Rules:
419
- - 1 to 2 tickets per problem.
420
- - Every ticket must include evidenceIds and acceptanceCriteria.
421
- - Acceptance criteria must be testable.
422
- - Each acceptanceCriteria item must be <= 160 characters.
423
- ${JSON_ONLY_RULES}
424
- `.trim();
425
- }
426
- function promptSuggestPatchIntent(params) {
427
- return `
428
- You are generating a ContractPatchIntent from an evidence-backed ticket.
429
-
430
- Ticket:
431
- ${params.ticketJSON}
432
-
433
- Return JSON:
434
- {
435
- "featureKey": "feature_slug",
436
- "changes": [
437
- { "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" }
438
- ],
439
- "acceptanceCriteria": ["..."]
440
- }
441
-
442
- Rules:
443
- - Keep changes <= 8.
444
- - Each change must be concrete and scoped.
445
- - Acceptance criteria must be testable and derived from the ticket.
446
- - Each acceptanceCriteria item must be <= 140 characters.
447
- ${JSON_ONLY_RULES}
448
- `.trim();
449
- }
450
- // src/validators.ts
451
- import {
452
- CitationModel,
453
- ContractPatchIntentModel,
454
- ImpactReportModel,
455
- InsightExtractionModel,
456
- OpportunityBriefModel,
457
- TaskPackModel
458
- } from "@contractspec/lib.contracts-spec/product-intent/types";
459
- function assertStringLength(value, path, bounds) {
460
- if (bounds.min !== undefined && value.length < bounds.min) {
461
- throw new Error(`Expected ${path} to be at least ${bounds.min} characters, got ${value.length}`);
462
- }
463
- if (bounds.max !== undefined && value.length > bounds.max) {
464
- throw new Error(`Expected ${path} to be at most ${bounds.max} characters, got ${value.length}`);
465
- }
534
+ // src/validators.ts
535
+ import {
536
+ CitationModel,
537
+ ContractPatchIntentModel,
538
+ ImpactReportModel,
539
+ InsightExtractionModel,
540
+ OpportunityBriefModel,
541
+ TaskPackModel
542
+ } from "@contractspec/lib.contracts-spec/product-intent/types";
543
+ function assertStringLength(value, path, bounds) {
544
+ if (bounds.min !== undefined && value.length < bounds.min) {
545
+ throw new Error(`Expected ${path} to be at least ${bounds.min} characters, got ${value.length}`);
546
+ }
547
+ if (bounds.max !== undefined && value.length > bounds.max) {
548
+ throw new Error(`Expected ${path} to be at most ${bounds.max} characters, got ${value.length}`);
549
+ }
466
550
  }
467
551
  function assertArrayLength(value, path, bounds) {
468
552
  if (bounds.min !== undefined && value.length < bounds.min) {
@@ -720,6 +804,231 @@ function buildRepairPromptWithOutput(error, previousOutput, maxOutputChars = 400
720
804
  ].join(`
721
805
  `);
722
806
  }
807
+
808
+ // src/ticket-pipeline-runner.ts
809
+ var DEFAULT_MAX_ATTEMPTS = 2;
810
+ function timestamp() {
811
+ return new Date().toISOString();
812
+ }
813
+ function toErrorMessage(error) {
814
+ return error instanceof Error ? error.message : String(error);
815
+ }
816
+ async function safeLog(logger, entry) {
817
+ if (!logger)
818
+ return;
819
+ try {
820
+ await logger.log(entry);
821
+ } catch {}
822
+ }
823
+ async function runWithValidation(options) {
824
+ const maxAttempts = Math.max(1, options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
825
+ let attempt = 0;
826
+ let lastError;
827
+ let lastRaw = "";
828
+ let currentPrompt = options.prompt;
829
+ while (attempt < maxAttempts) {
830
+ attempt += 1;
831
+ await safeLog(options.logger, {
832
+ stage: options.stage,
833
+ phase: "request",
834
+ attempt,
835
+ prompt: currentPrompt,
836
+ timestamp: timestamp()
837
+ });
838
+ let raw;
839
+ try {
840
+ raw = await options.modelRunner.generateJson(currentPrompt);
841
+ } catch (error) {
842
+ lastError = toErrorMessage(error);
843
+ await safeLog(options.logger, {
844
+ stage: options.stage,
845
+ phase: "model_error",
846
+ attempt,
847
+ prompt: currentPrompt,
848
+ error: lastError,
849
+ timestamp: timestamp()
850
+ });
851
+ throw new Error(`[${options.stage}] Model error: ${lastError}`);
852
+ }
853
+ await safeLog(options.logger, {
854
+ stage: options.stage,
855
+ phase: "response",
856
+ attempt,
857
+ prompt: currentPrompt,
858
+ response: raw,
859
+ timestamp: timestamp()
860
+ });
861
+ try {
862
+ return options.validate(raw);
863
+ } catch (error) {
864
+ lastError = toErrorMessage(error);
865
+ lastRaw = raw;
866
+ if (options.repair) {
867
+ const repaired = options.repair(raw, lastError);
868
+ if (repaired && repaired !== raw) {
869
+ await safeLog(options.logger, {
870
+ stage: options.stage,
871
+ phase: "repair",
872
+ attempt,
873
+ prompt: currentPrompt,
874
+ response: repaired,
875
+ error: lastError,
876
+ timestamp: timestamp()
877
+ });
878
+ try {
879
+ return options.validate(repaired);
880
+ } catch (repairError) {
881
+ lastError = toErrorMessage(repairError);
882
+ lastRaw = repaired;
883
+ }
884
+ }
885
+ }
886
+ await safeLog(options.logger, {
887
+ stage: options.stage,
888
+ phase: "validation_error",
889
+ attempt,
890
+ prompt: currentPrompt,
891
+ response: lastRaw,
892
+ error: lastError,
893
+ timestamp: timestamp()
894
+ });
895
+ currentPrompt = [
896
+ options.prompt,
897
+ buildRepairPromptWithOutput(lastError, lastRaw)
898
+ ].join(`
899
+
900
+ `);
901
+ }
902
+ }
903
+ throw new Error(`[${options.stage}] Validation failed after ${maxAttempts} attempt(s): ${lastError ?? "unknown error"}`);
904
+ }
905
+
906
+ // src/ticket-prompts.ts
907
+ function promptExtractEvidenceFindings(params) {
908
+ return `
909
+ You are extracting evidence findings grounded in transcript excerpts.
910
+
911
+ Question:
912
+ ${params.question}
913
+
914
+ Evidence:
915
+ ${params.evidenceJSON}
916
+
917
+ Return JSON:
918
+ {
919
+ "findings": [
920
+ {
921
+ "findingId": "find_001",
922
+ "summary": "...",
923
+ "tags": ["..."],
924
+ "citations": [{ "chunkId": "...", "quote": "..." }]
925
+ }
926
+ ]
927
+ }
928
+
929
+ Rules:
930
+ - Produce 8 to 18 findings.
931
+ - Each finding must include at least 1 citation.
932
+ - Summaries must be specific and short.
933
+ - Quotes must be copied character-for-character from the chunk text (no paraphrasing, no ellipses).
934
+ - Preserve punctuation, smart quotes, and special hyphens exactly as shown in the chunk text.
935
+ ${CITATION_RULES}
936
+ ${JSON_ONLY_RULES}
937
+ `.trim();
938
+ }
939
+ function promptGroupProblems(params) {
940
+ const allowed = JSON.stringify({ findingIds: params.findingIds }, null, 2);
941
+ return `
942
+ You are grouping evidence findings into problem statements.
943
+
944
+ Question:
945
+ ${params.question}
946
+
947
+ Findings:
948
+ ${params.findingsJSON}
949
+
950
+ Allowed finding IDs:
951
+ ${allowed}
952
+
953
+ Return JSON:
954
+ {
955
+ "problems": [
956
+ {
957
+ "problemId": "prob_001",
958
+ "statement": "...",
959
+ "evidenceIds": ["find_001"],
960
+ "tags": ["..."],
961
+ "severity": "low|medium|high"
962
+ }
963
+ ]
964
+ }
965
+
966
+ Rules:
967
+ - Each problem must reference 1 to 6 evidenceIds.
968
+ - evidenceIds must be drawn from the allowed finding IDs.
969
+ - Keep statements short and actionable.
970
+ ${JSON_ONLY_RULES}
971
+ `.trim();
972
+ }
973
+ function promptGenerateTickets(params) {
974
+ return `
975
+ You are generating implementation tickets grounded in evidence.
976
+
977
+ Question:
978
+ ${params.question}
979
+
980
+ Problems:
981
+ ${params.problemsJSON}
982
+
983
+ Evidence findings:
984
+ ${params.findingsJSON}
985
+
986
+ Return JSON:
987
+ {
988
+ "tickets": [
989
+ {
990
+ "ticketId": "t_001",
991
+ "title": "...",
992
+ "summary": "...",
993
+ "evidenceIds": ["find_001"],
994
+ "acceptanceCriteria": ["..."]
995
+ }
996
+ ]
997
+ }
998
+
999
+ Rules:
1000
+ - 1 to 2 tickets per problem.
1001
+ - Every ticket must include evidenceIds and acceptanceCriteria.
1002
+ - Acceptance criteria must be testable.
1003
+ - Each acceptanceCriteria item must be <= 160 characters.
1004
+ ${JSON_ONLY_RULES}
1005
+ `.trim();
1006
+ }
1007
+ function promptSuggestPatchIntent(params) {
1008
+ return `
1009
+ You are generating a ContractPatchIntent from an evidence-backed ticket.
1010
+
1011
+ Ticket:
1012
+ ${params.ticketJSON}
1013
+
1014
+ Return JSON:
1015
+ {
1016
+ "featureKey": "feature_slug",
1017
+ "changes": [
1018
+ { "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" }
1019
+ ],
1020
+ "acceptanceCriteria": ["..."]
1021
+ }
1022
+
1023
+ Rules:
1024
+ - Keep changes <= 8.
1025
+ - Each change must be concrete and scoped.
1026
+ - Acceptance criteria must be testable and derived from the ticket.
1027
+ - Each acceptanceCriteria item must be <= 140 characters.
1028
+ ${JSON_ONLY_RULES}
1029
+ `.trim();
1030
+ }
1031
+
723
1032
  // src/ticket-validators.ts
724
1033
  import {
725
1034
  EvidenceFindingExtractionModel,
@@ -819,122 +1128,17 @@ function validateTicketCollection(raw, findings) {
819
1128
  });
820
1129
  for (const criterion of ticket.acceptanceCriteria) {
821
1130
  assertStringLength2(criterion, "tickets[].acceptanceCriteria[]", {
822
- min: 1,
823
- max: 280
824
- });
825
- }
826
- if (ticket.tags) {
827
- for (const tag of ticket.tags) {
828
- assertStringLength2(tag, "tickets[].tags[]", { min: 1, max: 48 });
829
- }
830
- }
831
- }
832
- return data;
833
- }
834
- // src/ticket-pipeline.ts
835
- import {
836
- ContractPatchIntentModel as ContractPatchIntentModel2,
837
- EvidenceFindingExtractionModel as EvidenceFindingExtractionModel2,
838
- ProblemGroupingModel as ProblemGroupingModel2,
839
- TicketCollectionModel as TicketCollectionModel2
840
- } from "@contractspec/lib.contracts-spec/product-intent/types";
841
-
842
- // src/ticket-pipeline-runner.ts
843
- var DEFAULT_MAX_ATTEMPTS = 2;
844
- function timestamp() {
845
- return new Date().toISOString();
846
- }
847
- function toErrorMessage(error) {
848
- return error instanceof Error ? error.message : String(error);
849
- }
850
- async function safeLog(logger, entry) {
851
- if (!logger)
852
- return;
853
- try {
854
- await logger.log(entry);
855
- } catch {}
856
- }
857
- async function runWithValidation(options) {
858
- const maxAttempts = Math.max(1, options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS);
859
- let attempt = 0;
860
- let lastError;
861
- let lastRaw = "";
862
- let currentPrompt = options.prompt;
863
- while (attempt < maxAttempts) {
864
- attempt += 1;
865
- await safeLog(options.logger, {
866
- stage: options.stage,
867
- phase: "request",
868
- attempt,
869
- prompt: currentPrompt,
870
- timestamp: timestamp()
871
- });
872
- let raw;
873
- try {
874
- raw = await options.modelRunner.generateJson(currentPrompt);
875
- } catch (error) {
876
- lastError = toErrorMessage(error);
877
- await safeLog(options.logger, {
878
- stage: options.stage,
879
- phase: "model_error",
880
- attempt,
881
- prompt: currentPrompt,
882
- error: lastError,
883
- timestamp: timestamp()
1131
+ min: 1,
1132
+ max: 280
884
1133
  });
885
- throw new Error(`[${options.stage}] Model error: ${lastError}`);
886
1134
  }
887
- await safeLog(options.logger, {
888
- stage: options.stage,
889
- phase: "response",
890
- attempt,
891
- prompt: currentPrompt,
892
- response: raw,
893
- timestamp: timestamp()
894
- });
895
- try {
896
- return options.validate(raw);
897
- } catch (error) {
898
- lastError = toErrorMessage(error);
899
- lastRaw = raw;
900
- if (options.repair) {
901
- const repaired = options.repair(raw, lastError);
902
- if (repaired && repaired !== raw) {
903
- await safeLog(options.logger, {
904
- stage: options.stage,
905
- phase: "repair",
906
- attempt,
907
- prompt: currentPrompt,
908
- response: repaired,
909
- error: lastError,
910
- timestamp: timestamp()
911
- });
912
- try {
913
- return options.validate(repaired);
914
- } catch (repairError) {
915
- lastError = toErrorMessage(repairError);
916
- lastRaw = repaired;
917
- }
918
- }
1135
+ if (ticket.tags) {
1136
+ for (const tag of ticket.tags) {
1137
+ assertStringLength2(tag, "tickets[].tags[]", { min: 1, max: 48 });
919
1138
  }
920
- await safeLog(options.logger, {
921
- stage: options.stage,
922
- phase: "validation_error",
923
- attempt,
924
- prompt: currentPrompt,
925
- response: lastRaw,
926
- error: lastError,
927
- timestamp: timestamp()
928
- });
929
- currentPrompt = [
930
- options.prompt,
931
- buildRepairPromptWithOutput(lastError, lastRaw)
932
- ].join(`
933
-
934
- `);
935
1139
  }
936
1140
  }
937
- throw new Error(`[${options.stage}] Validation failed after ${maxAttempts} attempt(s): ${lastError ?? "unknown error"}`);
1141
+ return data;
938
1142
  }
939
1143
 
940
1144
  // src/ticket-pipeline.ts
@@ -947,7 +1151,7 @@ var TAG_HINTS = {
947
1151
  performance: ["slow", "latency", "performance"],
948
1152
  integrations: ["integration", "api", "webhook"]
949
1153
  };
950
- function slugify(value) {
1154
+ function slugify2(value) {
951
1155
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)+/g, "");
952
1156
  }
953
1157
  function pickQuote(text, maxLen = 220) {
@@ -1196,7 +1400,7 @@ function repairPatchIntent(raw) {
1196
1400
  }
1197
1401
  function retrieveChunks(transcript, question, options = {}) {
1198
1402
  const chunkSize = options.chunkSize ?? 800;
1199
- const sourceId = options.sourceId ?? slugify(question || "transcript");
1403
+ const sourceId = options.sourceId ?? slugify2(question || "transcript");
1200
1404
  const clean = transcript.trim();
1201
1405
  const chunks = [];
1202
1406
  for (let offset = 0, idx = 0;offset < clean.length; idx += 1) {
@@ -1334,7 +1538,7 @@ async function suggestPatch(ticket, options = {}) {
1334
1538
  validate: (raw) => validatePatchIntent(raw)
1335
1539
  });
1336
1540
  }
1337
- const featureKey = slugify(ticket.title) || "product_intent_ticket";
1541
+ const featureKey = slugify2(ticket.title) || "product_intent_ticket";
1338
1542
  const intent = {
1339
1543
  featureKey,
1340
1544
  changes: [
@@ -1348,207 +1552,6 @@ async function suggestPatch(ticket, options = {}) {
1348
1552
  };
1349
1553
  return validatePatchIntent(JSON.stringify(intent, null, 2));
1350
1554
  }
1351
- // src/impact-engine.ts
1352
- var SURFACE_MAP = {
1353
- add_field: ["api", "db", "ui", "docs", "tests"],
1354
- remove_field: ["api", "db", "ui", "docs", "tests"],
1355
- rename_field: ["api", "db", "ui", "docs", "tests"],
1356
- add_event: ["api", "workflows", "docs", "tests"],
1357
- update_event: ["api", "workflows", "docs", "tests"],
1358
- add_operation: ["api", "ui", "workflows", "docs", "tests"],
1359
- update_operation: ["api", "ui", "workflows", "docs", "tests"],
1360
- update_form: ["ui", "docs", "tests"],
1361
- update_policy: ["policy", "api", "workflows", "docs", "tests"],
1362
- add_enum_value: ["api", "db", "ui", "docs", "tests"],
1363
- remove_enum_value: ["api", "db", "ui", "docs", "tests"],
1364
- other: ["docs", "tests"]
1365
- };
1366
- var BUCKET_MAP = {
1367
- remove_field: "breaks",
1368
- rename_field: "breaks",
1369
- remove_enum_value: "breaks",
1370
- update_operation: "mustChange",
1371
- update_event: "mustChange",
1372
- update_policy: "mustChange",
1373
- update_form: "risky",
1374
- add_field: "risky",
1375
- add_event: "risky",
1376
- add_operation: "risky",
1377
- add_enum_value: "risky",
1378
- other: "risky"
1379
- };
1380
- function slugify2(value) {
1381
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)+/g, "");
1382
- }
1383
- function buildTokens(change) {
1384
- const combined = `${change.type} ${change.target} ${change.detail}`;
1385
- const tokens = combined.split(/[^a-zA-Z0-9]+/).map((token) => token.trim()).filter((token) => token.length >= 3);
1386
- return Array.from(new Set(tokens.map((token) => token.toLowerCase()))).slice(0, 8);
1387
- }
1388
- function scanTokens(tokens, files, maxHits) {
1389
- const hits = [];
1390
- const lowerTokens = tokens.map((token) => token.toLowerCase());
1391
- for (const file of files) {
1392
- const haystack = file.content.toLowerCase();
1393
- if (lowerTokens.some((token) => haystack.includes(token))) {
1394
- hits.push(file.path);
1395
- }
1396
- if (hits.length >= maxHits)
1397
- break;
1398
- }
1399
- return hits;
1400
- }
1401
- function formatRefs(tokens, repoFiles, maxHits = 3) {
1402
- if (!repoFiles || repoFiles.length === 0) {
1403
- return "refs: (no repo scan)";
1404
- }
1405
- const hits = scanTokens(tokens, repoFiles, maxHits);
1406
- if (!hits.length)
1407
- return "refs: none";
1408
- return `refs: ${hits.join(", ")}`;
1409
- }
1410
- function humanizeChange(change) {
1411
- const label = change.type.replace(/_/g, " ");
1412
- return `${label} ${change.target}`;
1413
- }
1414
- function buildStatement(change, refs, surfaces) {
1415
- const reason = change.detail || `touches ${surfaces.join(", ")}`;
1416
- return `${humanizeChange(change)} because ${reason} (${refs})`;
1417
- }
1418
- function impactEngine(intent, options = {}) {
1419
- const reportId = options.reportId ?? `impact-${slugify2(intent.featureKey)}`;
1420
- const patchId = options.patchId ?? `patch-${slugify2(intent.featureKey)}`;
1421
- const maxHitsPerChange = options.maxHitsPerChange ?? 3;
1422
- const breaks = [];
1423
- const mustChange = [];
1424
- const risky = [];
1425
- const surfaces = {
1426
- api: [],
1427
- db: [],
1428
- ui: [],
1429
- workflows: [],
1430
- policy: [],
1431
- docs: [],
1432
- tests: []
1433
- };
1434
- for (const change of intent.changes) {
1435
- const bucket = BUCKET_MAP[change.type] ?? "risky";
1436
- const surfaceTargets = SURFACE_MAP[change.type] ?? ["docs", "tests"];
1437
- const tokens = buildTokens(change);
1438
- const refs = formatRefs(tokens, options.repoFiles, maxHitsPerChange);
1439
- const statement = buildStatement(change, refs, surfaceTargets);
1440
- if (bucket === "breaks")
1441
- breaks.push(statement);
1442
- if (bucket === "mustChange")
1443
- mustChange.push(statement);
1444
- if (bucket === "risky")
1445
- risky.push(statement);
1446
- for (const surface of surfaceTargets) {
1447
- const list = surfaces[surface];
1448
- if (Array.isArray(list)) {
1449
- list.push(statement);
1450
- }
1451
- }
1452
- }
1453
- const summary = [
1454
- `Analyzed ${intent.changes.length} change(s).`,
1455
- `Breaks: ${breaks.length}.`,
1456
- `Must change: ${mustChange.length}.`,
1457
- `Risky: ${risky.length}.`
1458
- ].join(" ");
1459
- return {
1460
- reportId,
1461
- patchId,
1462
- summary,
1463
- breaks,
1464
- mustChange,
1465
- risky,
1466
- surfaces
1467
- };
1468
- }
1469
- // src/project-management-sync.ts
1470
- function buildProjectManagementSyncPayload(params) {
1471
- const options = params.options ?? {};
1472
- const items = buildWorkItemsFromTickets(params.tickets, options);
1473
- const summary = options.includeSummary ? buildSummaryWorkItem({
1474
- question: params.question,
1475
- tickets: params.tickets,
1476
- patchIntent: params.patchIntent,
1477
- impact: params.impact,
1478
- title: options.summaryTitle,
1479
- baseTags: options.baseTags
1480
- }) : undefined;
1481
- return { summary, items };
1482
- }
1483
- function buildWorkItemsFromTickets(tickets, options = {}) {
1484
- return tickets.map((ticket) => ({
1485
- title: ticket.title,
1486
- description: renderTicketDescription(ticket),
1487
- type: "task",
1488
- priority: mapPriority(ticket.priority, options.defaultPriority),
1489
- tags: mergeTags(options.baseTags, ticket.tags),
1490
- externalId: ticket.ticketId
1491
- }));
1492
- }
1493
- function buildSummaryWorkItem(params) {
1494
- return {
1495
- title: params.title ?? "Product Intent Summary",
1496
- description: renderSummaryMarkdown(params),
1497
- type: "summary",
1498
- tags: mergeTags(params.baseTags, ["product-intent", "summary"])
1499
- };
1500
- }
1501
- function renderTicketDescription(ticket) {
1502
- const lines = [
1503
- ticket.summary,
1504
- "",
1505
- "Acceptance Criteria:",
1506
- ...ticket.acceptanceCriteria.map((criterion) => `- ${criterion}`)
1507
- ];
1508
- if (ticket.evidenceIds.length > 0) {
1509
- lines.push("", `Evidence: ${ticket.evidenceIds.join(", ")}`);
1510
- }
1511
- return lines.join(`
1512
- `);
1513
- }
1514
- function renderSummaryMarkdown(params) {
1515
- const lines = [`# ${params.question}`, "", "## Top Tickets"];
1516
- for (const ticket of params.tickets) {
1517
- lines.push(`- ${ticket.title}`);
1518
- }
1519
- if (params.patchIntent) {
1520
- lines.push("", "## Patch Intent", `Feature: ${params.patchIntent.featureKey}`);
1521
- params.patchIntent.changes.forEach((change) => {
1522
- lines.push(`- ${change.type}: ${change.target}`);
1523
- });
1524
- }
1525
- if (params.impact) {
1526
- lines.push("", "## Impact Summary", params.impact.summary);
1527
- }
1528
- return lines.join(`
1529
- `);
1530
- }
1531
- function mapPriority(priority, fallback) {
1532
- if (!priority)
1533
- return fallback;
1534
- switch (priority) {
1535
- case "high":
1536
- return "high";
1537
- case "medium":
1538
- return "medium";
1539
- case "low":
1540
- return "low";
1541
- default:
1542
- return fallback;
1543
- }
1544
- }
1545
- function mergeTags(baseTags, tags) {
1546
- const merged = new Set;
1547
- (baseTags ?? []).forEach((tag) => merged.add(tag));
1548
- (tags ?? []).forEach((tag) => merged.add(tag));
1549
- const result = [...merged];
1550
- return result.length > 0 ? result : undefined;
1551
- }
1552
1555
  export {
1553
1556
  validateTicketCollection,
1554
1557
  validateTaskPack,