@yasserkhanorg/e2e-agents 0.3.2

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.
Files changed (221) hide show
  1. package/LICENSE +168 -0
  2. package/README.md +620 -0
  3. package/dist/agent/analysis.d.ts +62 -0
  4. package/dist/agent/analysis.d.ts.map +1 -0
  5. package/dist/agent/analysis.js +292 -0
  6. package/dist/agent/blast_radius.d.ts +4 -0
  7. package/dist/agent/blast_radius.d.ts.map +1 -0
  8. package/dist/agent/blast_radius.js +37 -0
  9. package/dist/agent/cache_utils.d.ts +38 -0
  10. package/dist/agent/cache_utils.d.ts.map +1 -0
  11. package/dist/agent/cache_utils.js +67 -0
  12. package/dist/agent/config.d.ts +148 -0
  13. package/dist/agent/config.d.ts.map +1 -0
  14. package/dist/agent/config.js +640 -0
  15. package/dist/agent/dependency_graph.d.ts +14 -0
  16. package/dist/agent/dependency_graph.d.ts.map +1 -0
  17. package/dist/agent/dependency_graph.js +227 -0
  18. package/dist/agent/feedback.d.ts +55 -0
  19. package/dist/agent/feedback.d.ts.map +1 -0
  20. package/dist/agent/feedback.js +257 -0
  21. package/dist/agent/flags.d.ts +23 -0
  22. package/dist/agent/flags.d.ts.map +1 -0
  23. package/dist/agent/flags.js +171 -0
  24. package/dist/agent/flow_catalog.d.ts +25 -0
  25. package/dist/agent/flow_catalog.d.ts.map +1 -0
  26. package/dist/agent/flow_catalog.js +106 -0
  27. package/dist/agent/flow_mapping.d.ts +10 -0
  28. package/dist/agent/flow_mapping.d.ts.map +1 -0
  29. package/dist/agent/flow_mapping.js +84 -0
  30. package/dist/agent/framework.d.ts +13 -0
  31. package/dist/agent/framework.d.ts.map +1 -0
  32. package/dist/agent/framework.js +149 -0
  33. package/dist/agent/gap_suggestions.d.ts +14 -0
  34. package/dist/agent/gap_suggestions.d.ts.map +1 -0
  35. package/dist/agent/gap_suggestions.js +101 -0
  36. package/dist/agent/generator.d.ts +10 -0
  37. package/dist/agent/generator.d.ts.map +1 -0
  38. package/dist/agent/generator.js +115 -0
  39. package/dist/agent/git.d.ts +11 -0
  40. package/dist/agent/git.d.ts.map +1 -0
  41. package/dist/agent/git.js +90 -0
  42. package/dist/agent/handoff.d.ts +22 -0
  43. package/dist/agent/handoff.d.ts.map +1 -0
  44. package/dist/agent/handoff.js +180 -0
  45. package/dist/agent/impact-analyzer.d.ts +114 -0
  46. package/dist/agent/impact-analyzer.d.ts.map +1 -0
  47. package/dist/agent/impact-analyzer.js +557 -0
  48. package/dist/agent/index.d.ts +21 -0
  49. package/dist/agent/index.d.ts.map +1 -0
  50. package/dist/agent/index.js +38 -0
  51. package/dist/agent/model-router.d.ts +57 -0
  52. package/dist/agent/model-router.d.ts.map +1 -0
  53. package/dist/agent/model-router.js +154 -0
  54. package/dist/agent/operational_insights.d.ts +41 -0
  55. package/dist/agent/operational_insights.d.ts.map +1 -0
  56. package/dist/agent/operational_insights.js +126 -0
  57. package/dist/agent/pipeline.d.ts +23 -0
  58. package/dist/agent/pipeline.d.ts.map +1 -0
  59. package/dist/agent/pipeline.js +609 -0
  60. package/dist/agent/plan.d.ts +91 -0
  61. package/dist/agent/plan.d.ts.map +1 -0
  62. package/dist/agent/plan.js +331 -0
  63. package/dist/agent/playwright_report.d.ts +8 -0
  64. package/dist/agent/playwright_report.d.ts.map +1 -0
  65. package/dist/agent/playwright_report.js +126 -0
  66. package/dist/agent/report-generator.d.ts +24 -0
  67. package/dist/agent/report-generator.d.ts.map +1 -0
  68. package/dist/agent/report-generator.js +250 -0
  69. package/dist/agent/report.d.ts +81 -0
  70. package/dist/agent/report.d.ts.map +1 -0
  71. package/dist/agent/report.js +147 -0
  72. package/dist/agent/runner.d.ts +7 -0
  73. package/dist/agent/runner.d.ts.map +1 -0
  74. package/dist/agent/runner.js +576 -0
  75. package/dist/agent/selectors.d.ts +10 -0
  76. package/dist/agent/selectors.d.ts.map +1 -0
  77. package/dist/agent/selectors.js +75 -0
  78. package/dist/agent/spec-bridge.d.ts +101 -0
  79. package/dist/agent/spec-bridge.d.ts.map +1 -0
  80. package/dist/agent/spec-bridge.js +273 -0
  81. package/dist/agent/spec-builder.d.ts +102 -0
  82. package/dist/agent/spec-builder.d.ts.map +1 -0
  83. package/dist/agent/spec-builder.js +273 -0
  84. package/dist/agent/subsystem_risk.d.ts +23 -0
  85. package/dist/agent/subsystem_risk.d.ts.map +1 -0
  86. package/dist/agent/subsystem_risk.js +207 -0
  87. package/dist/agent/telemetry.d.ts +84 -0
  88. package/dist/agent/telemetry.d.ts.map +1 -0
  89. package/dist/agent/telemetry.js +220 -0
  90. package/dist/agent/test_path.d.ts +2 -0
  91. package/dist/agent/test_path.d.ts.map +1 -0
  92. package/dist/agent/test_path.js +23 -0
  93. package/dist/agent/tests.d.ts +18 -0
  94. package/dist/agent/tests.d.ts.map +1 -0
  95. package/dist/agent/tests.js +106 -0
  96. package/dist/agent/traceability.d.ts +22 -0
  97. package/dist/agent/traceability.d.ts.map +1 -0
  98. package/dist/agent/traceability.js +183 -0
  99. package/dist/agent/traceability_capture.d.ts +18 -0
  100. package/dist/agent/traceability_capture.d.ts.map +1 -0
  101. package/dist/agent/traceability_capture.js +313 -0
  102. package/dist/agent/traceability_ingest.d.ts +21 -0
  103. package/dist/agent/traceability_ingest.d.ts.map +1 -0
  104. package/dist/agent/traceability_ingest.js +237 -0
  105. package/dist/agent/utils.d.ts +13 -0
  106. package/dist/agent/utils.d.ts.map +1 -0
  107. package/dist/agent/utils.js +152 -0
  108. package/dist/agent/validators/selector-validator.d.ts +74 -0
  109. package/dist/agent/validators/selector-validator.d.ts.map +1 -0
  110. package/dist/agent/validators/selector-validator.js +165 -0
  111. package/dist/anthropic_provider.d.ts +65 -0
  112. package/dist/anthropic_provider.d.ts.map +1 -0
  113. package/dist/anthropic_provider.js +332 -0
  114. package/dist/api.d.ts +48 -0
  115. package/dist/api.d.ts.map +1 -0
  116. package/dist/api.js +113 -0
  117. package/dist/base_provider.d.ts +53 -0
  118. package/dist/base_provider.d.ts.map +1 -0
  119. package/dist/base_provider.js +81 -0
  120. package/dist/cli.d.ts +3 -0
  121. package/dist/cli.d.ts.map +1 -0
  122. package/dist/cli.js +843 -0
  123. package/dist/custom_provider.d.ts +20 -0
  124. package/dist/custom_provider.d.ts.map +1 -0
  125. package/dist/custom_provider.js +276 -0
  126. package/dist/e2e-test-gen/index.d.ts +51 -0
  127. package/dist/e2e-test-gen/index.d.ts.map +1 -0
  128. package/dist/e2e-test-gen/index.js +57 -0
  129. package/dist/e2e-test-gen/spec_parser.d.ts +142 -0
  130. package/dist/e2e-test-gen/spec_parser.d.ts.map +1 -0
  131. package/dist/e2e-test-gen/spec_parser.js +786 -0
  132. package/dist/e2e-test-gen/types.d.ts +185 -0
  133. package/dist/e2e-test-gen/types.d.ts.map +1 -0
  134. package/dist/e2e-test-gen/types.js +4 -0
  135. package/dist/esm/agent/analysis.js +287 -0
  136. package/dist/esm/agent/blast_radius.js +34 -0
  137. package/dist/esm/agent/cache_utils.js +63 -0
  138. package/dist/esm/agent/config.js +637 -0
  139. package/dist/esm/agent/dependency_graph.js +224 -0
  140. package/dist/esm/agent/feedback.js +253 -0
  141. package/dist/esm/agent/flags.js +160 -0
  142. package/dist/esm/agent/flow_catalog.js +103 -0
  143. package/dist/esm/agent/flow_mapping.js +81 -0
  144. package/dist/esm/agent/framework.js +145 -0
  145. package/dist/esm/agent/gap_suggestions.js +98 -0
  146. package/dist/esm/agent/generator.js +112 -0
  147. package/dist/esm/agent/git.js +87 -0
  148. package/dist/esm/agent/handoff.js +177 -0
  149. package/dist/esm/agent/impact-analyzer.js +548 -0
  150. package/dist/esm/agent/index.js +22 -0
  151. package/dist/esm/agent/model-router.js +150 -0
  152. package/dist/esm/agent/operational_insights.js +123 -0
  153. package/dist/esm/agent/pipeline.js +605 -0
  154. package/dist/esm/agent/plan.js +324 -0
  155. package/dist/esm/agent/playwright_report.js +123 -0
  156. package/dist/esm/agent/report-generator.js +247 -0
  157. package/dist/esm/agent/report.js +144 -0
  158. package/dist/esm/agent/runner.js +572 -0
  159. package/dist/esm/agent/selectors.js +71 -0
  160. package/dist/esm/agent/spec-bridge.js +267 -0
  161. package/dist/esm/agent/spec-builder.js +267 -0
  162. package/dist/esm/agent/subsystem_risk.js +204 -0
  163. package/dist/esm/agent/telemetry.js +216 -0
  164. package/dist/esm/agent/test_path.js +20 -0
  165. package/dist/esm/agent/tests.js +101 -0
  166. package/dist/esm/agent/traceability.js +180 -0
  167. package/dist/esm/agent/traceability_capture.js +310 -0
  168. package/dist/esm/agent/traceability_ingest.js +234 -0
  169. package/dist/esm/agent/utils.js +138 -0
  170. package/dist/esm/agent/validators/selector-validator.js +160 -0
  171. package/dist/esm/anthropic_provider.js +324 -0
  172. package/dist/esm/api.js +105 -0
  173. package/dist/esm/base_provider.js +77 -0
  174. package/dist/esm/cli.js +841 -0
  175. package/dist/esm/custom_provider.js +272 -0
  176. package/dist/esm/e2e-test-gen/index.js +50 -0
  177. package/dist/esm/e2e-test-gen/spec_parser.js +782 -0
  178. package/dist/esm/e2e-test-gen/types.js +3 -0
  179. package/dist/esm/index.js +16 -0
  180. package/dist/esm/logger.js +89 -0
  181. package/dist/esm/mcp-server.js +465 -0
  182. package/dist/esm/ollama_provider.js +300 -0
  183. package/dist/esm/openai_provider.js +242 -0
  184. package/dist/esm/package.json +3 -0
  185. package/dist/esm/plan-and-test-constants.js +126 -0
  186. package/dist/esm/provider_factory.js +336 -0
  187. package/dist/esm/provider_interface.js +23 -0
  188. package/dist/esm/provider_utils.js +96 -0
  189. package/dist/index.d.ts +31 -0
  190. package/dist/index.d.ts.map +1 -0
  191. package/dist/index.js +41 -0
  192. package/dist/logger.d.ts +23 -0
  193. package/dist/logger.d.ts.map +1 -0
  194. package/dist/logger.js +93 -0
  195. package/dist/mcp-server.d.ts +35 -0
  196. package/dist/mcp-server.d.ts.map +1 -0
  197. package/dist/mcp-server.js +469 -0
  198. package/dist/ollama_provider.d.ts +65 -0
  199. package/dist/ollama_provider.d.ts.map +1 -0
  200. package/dist/ollama_provider.js +308 -0
  201. package/dist/openai_provider.d.ts +23 -0
  202. package/dist/openai_provider.d.ts.map +1 -0
  203. package/dist/openai_provider.js +250 -0
  204. package/dist/plan-and-test-constants.d.ts +110 -0
  205. package/dist/plan-and-test-constants.d.ts.map +1 -0
  206. package/dist/plan-and-test-constants.js +132 -0
  207. package/dist/provider_factory.d.ts +99 -0
  208. package/dist/provider_factory.d.ts.map +1 -0
  209. package/dist/provider_factory.js +341 -0
  210. package/dist/provider_interface.d.ts +358 -0
  211. package/dist/provider_interface.d.ts.map +1 -0
  212. package/dist/provider_interface.js +28 -0
  213. package/dist/provider_utils.d.ts +39 -0
  214. package/dist/provider_utils.d.ts.map +1 -0
  215. package/dist/provider_utils.js +103 -0
  216. package/package.json +101 -0
  217. package/schemas/gap.schema.json +18 -0
  218. package/schemas/impact.schema.json +418 -0
  219. package/schemas/plan.schema.json +285 -0
  220. package/schemas/subsystem-risk-map.schema.json +62 -0
  221. package/schemas/traceability-input.schema.json +122 -0
@@ -0,0 +1,786 @@
1
+ "use strict";
2
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
+ // See LICENSE.txt for license information.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.SpecificationParser = void 0;
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ const crypto_1 = require("crypto");
9
+ /**
10
+ * Specification Parser
11
+ *
12
+ * Parses user-provided specification documents to guide autonomous testing.
13
+ * Supports multiple formats:
14
+ * - Markdown (.md) with embedded screenshots and Given-When-Then scenarios
15
+ * - JSON (.json) with structured feature definitions
16
+ * - PDF (.pdf) with text, diagrams, and screenshots extracted via LLM
17
+ * - Plain text focus strings (natural language)
18
+ *
19
+ * Extracts:
20
+ * - Feature name, description, priority
21
+ * - Target URLs to prioritize in crawling
22
+ * - Business scenarios (Given-When-Then)
23
+ * - Acceptance criteria
24
+ * - Reference screenshots for visual comparison
25
+ * - UI mockups and wireframes from PDFs
26
+ *
27
+ * The parsed specifications guide:
28
+ * - Crawler URL prioritization
29
+ * - Test scenario generation
30
+ * - Visual regression comparison
31
+ * - Coverage gap detection
32
+ */
33
+ class SpecificationParser {
34
+ constructor(llmProvider, cache) {
35
+ this.llmProvider = llmProvider;
36
+ this.cache = cache;
37
+ }
38
+ /**
39
+ * Parse specification from file or string
40
+ */
41
+ async parse(source, sourceType = 'file') {
42
+ let content;
43
+ let sourcePath;
44
+ if (sourceType === 'file') {
45
+ if (!(0, fs_1.existsSync)(source)) {
46
+ throw new Error(`Specification file not found: ${source}`);
47
+ }
48
+ sourcePath = source;
49
+ // Determine format from extension
50
+ if (source.endsWith('.md')) {
51
+ content = (0, fs_1.readFileSync)(source, 'utf-8');
52
+ return this.parseMarkdown(content, sourcePath);
53
+ }
54
+ else if (source.endsWith('.json')) {
55
+ content = (0, fs_1.readFileSync)(source, 'utf-8');
56
+ return this.parseJSON(content, sourcePath);
57
+ }
58
+ else if (source.endsWith('.pdf')) {
59
+ return this.parsePDF(sourcePath);
60
+ }
61
+ else {
62
+ throw new Error(`Unsupported specification format: ${source}. Use .md, .json, or .pdf`);
63
+ }
64
+ }
65
+ else {
66
+ // Plain text focus string - use LLM to interpret
67
+ return this.parseFocusString(source);
68
+ }
69
+ }
70
+ /**
71
+ * Parse Markdown specification
72
+ *
73
+ * Expected format:
74
+ * # Feature: Feature Name
75
+ * **Priority**: High
76
+ * **Target URLs**: /path1, /path2
77
+ *
78
+ * ## Description
79
+ * Feature description...
80
+ *
81
+ * ## Business Scenarios
82
+ * ### Scenario 1: Name
83
+ * - **Given**: Precondition
84
+ * - **When**: Action
85
+ * - **Then**: Expected outcome
86
+ *
87
+ * ## Acceptance Criteria
88
+ * - Criterion 1
89
+ * - Criterion 2
90
+ *
91
+ * ## Screenshots
92
+ * ![Description](path/to/image.png)
93
+ */
94
+ async parseMarkdown(content, sourcePath) {
95
+ // Dynamic import to handle ESM marked module in CommonJS context
96
+ const { marked } = await import('marked');
97
+ const tokens = marked.lexer(content);
98
+ const specs = [];
99
+ let currentSpec = null;
100
+ let currentSection = null;
101
+ let currentScenario = null;
102
+ for (let i = 0; i < tokens.length; i++) {
103
+ const token = tokens[i];
104
+ if (token.type === 'heading') {
105
+ const headingToken = token;
106
+ const text = headingToken.text;
107
+ if (headingToken.depth === 1) {
108
+ // New feature
109
+ if (currentSpec) {
110
+ specs.push(this.finalizeSpec(currentSpec, sourcePath));
111
+ }
112
+ currentSpec = {
113
+ name: text.replace(/^Feature:\s*/i, '').trim(),
114
+ targetUrls: [],
115
+ scenarios: [],
116
+ screenshots: [],
117
+ acceptanceCriteria: [],
118
+ };
119
+ currentSection = null;
120
+ }
121
+ else if (headingToken.depth === 2) {
122
+ // Section
123
+ currentSection = text.toLowerCase();
124
+ }
125
+ else if (headingToken.depth === 3 && currentSection === 'business scenarios') {
126
+ // New scenario
127
+ if (currentScenario && currentSpec) {
128
+ currentSpec.scenarios.push(this.finalizeScenario(currentScenario));
129
+ }
130
+ currentScenario = {
131
+ name: text.replace(/^Scenario \d+:\s*/i, '').trim(),
132
+ priority: 'should-have',
133
+ };
134
+ }
135
+ }
136
+ else if (token.type === 'paragraph' && currentSpec) {
137
+ const paragraphToken = token;
138
+ const text = paragraphToken.text;
139
+ // Extract metadata from bold markers
140
+ const priorityMatch = text.match(/\*\*Priority\*\*:\s*(\w+)/i);
141
+ if (priorityMatch) {
142
+ currentSpec.priority = priorityMatch[1].toLowerCase();
143
+ }
144
+ const urlsMatch = text.match(/\*\*Target URLs?\*\*:\s*(.+)/i);
145
+ if (urlsMatch) {
146
+ currentSpec.targetUrls = urlsMatch[1]
147
+ .split(',')
148
+ .map((url) => url.trim())
149
+ .filter(Boolean);
150
+ }
151
+ // Extract scenario details
152
+ if (currentScenario) {
153
+ const givenMatch = text.match(/\*\*Given\*\*:\s*(.+)/i);
154
+ const whenMatch = text.match(/\*\*When\*\*:\s*(.+)/i);
155
+ const thenMatch = text.match(/\*\*Then\*\*:\s*(.+)/i);
156
+ const priorityMatch = text.match(/\*\*Priority\*\*:\s*(\w+)/i);
157
+ if (givenMatch)
158
+ currentScenario.given = givenMatch[1].trim();
159
+ if (whenMatch)
160
+ currentScenario.when = whenMatch[1].trim();
161
+ if (thenMatch)
162
+ currentScenario.then = thenMatch[1].trim();
163
+ if (priorityMatch) {
164
+ const priority = priorityMatch[1].toLowerCase();
165
+ if (priority.includes('must'))
166
+ currentScenario.priority = 'must-have';
167
+ else if (priority.includes('should'))
168
+ currentScenario.priority = 'should-have';
169
+ else
170
+ currentScenario.priority = 'nice-to-have';
171
+ }
172
+ }
173
+ // Description section
174
+ if (currentSection === 'description') {
175
+ currentSpec.description = (currentSpec.description || '') + text + ' ';
176
+ }
177
+ }
178
+ else if (token.type === 'list' && currentSpec) {
179
+ const listToken = token;
180
+ if (currentSection === 'acceptance criteria') {
181
+ // Extract acceptance criteria
182
+ for (const item of listToken.items) {
183
+ const itemToken = item;
184
+ currentSpec.acceptanceCriteria.push(itemToken.text);
185
+ }
186
+ }
187
+ else if (currentSection === 'business scenarios' && currentScenario) {
188
+ // Extract Given-When-Then from list items
189
+ for (const item of listToken.items) {
190
+ const itemToken = item;
191
+ const text = itemToken.text;
192
+ const givenMatch = text.match(/\*\*Given\*\*:\s*(.+)/i);
193
+ const whenMatch = text.match(/\*\*When\*\*:\s*(.+)/i);
194
+ const thenMatch = text.match(/\*\*Then\*\*:\s*(.+)/i);
195
+ if (givenMatch)
196
+ currentScenario.given = givenMatch[1].trim();
197
+ if (whenMatch)
198
+ currentScenario.when = whenMatch[1].trim();
199
+ if (thenMatch)
200
+ currentScenario.then = thenMatch[1].trim();
201
+ }
202
+ }
203
+ }
204
+ else if (token.type === 'image' && currentSpec && currentSection === 'screenshots') {
205
+ const imageToken = token;
206
+ // Resolve image path relative to spec file
207
+ const imagePath = this.resolveImagePath(imageToken.href, sourcePath);
208
+ currentSpec.screenshots.push({
209
+ path: imagePath,
210
+ description: imageToken.text || 'Screenshot',
211
+ });
212
+ }
213
+ }
214
+ // Finalize last scenario and spec
215
+ if (currentScenario && currentSpec) {
216
+ currentSpec.scenarios.push(this.finalizeScenario(currentScenario));
217
+ }
218
+ if (currentSpec) {
219
+ specs.push(this.finalizeSpec(currentSpec, sourcePath));
220
+ }
221
+ // Load screenshot data
222
+ await this.loadScreenshots(specs);
223
+ return specs;
224
+ }
225
+ /**
226
+ * Parse JSON specification
227
+ */
228
+ parseJSON(content, sourcePath) {
229
+ try {
230
+ let data;
231
+ try {
232
+ data = JSON.parse(content);
233
+ }
234
+ catch (parseError) {
235
+ throw new Error(`Invalid JSON syntax: ${parseError.message}`);
236
+ }
237
+ if (!data) {
238
+ throw new Error('JSON is empty or null');
239
+ }
240
+ // Support both single spec and array of specs
241
+ const specsData = Array.isArray(data) ? data : [data];
242
+ if (specsData.length === 0) {
243
+ throw new Error('No specifications found in JSON');
244
+ }
245
+ const specs = specsData.map((specData, index) => {
246
+ // Validate required fields
247
+ if (!specData.name && !specData.feature) {
248
+ throw new Error(`Specification ${index + 1} missing required "name" or "feature" field`);
249
+ }
250
+ if (!specData.scenarios || !Array.isArray(specData.scenarios)) {
251
+ // eslint-disable-next-line no-console
252
+ console.warn(`Specification "${specData.name || specData.feature}" has no scenarios`);
253
+ }
254
+ const scenarios = (specData.scenarios || []).map((s, sIndex) => {
255
+ if (!s.name) {
256
+ throw new Error(`Scenario ${sIndex + 1} in "${specData.name || specData.feature}" missing "name" field`);
257
+ }
258
+ return {
259
+ name: s.name,
260
+ given: s.given || '',
261
+ when: s.when || '',
262
+ then: s.then || '',
263
+ priority: s.priority || 'should-have',
264
+ };
265
+ });
266
+ const screenshots = (specData.screenshots || []).map((s) => {
267
+ if (typeof s === 'string') {
268
+ return { path: this.resolveImagePath(s, sourcePath), description: 'Screenshot' };
269
+ }
270
+ return {
271
+ path: this.resolveImagePath(s.path, sourcePath),
272
+ description: s.description || 'Screenshot',
273
+ };
274
+ });
275
+ return {
276
+ id: this.generateSpecId(specData.feature || specData.name),
277
+ name: specData.feature || specData.name,
278
+ description: specData.description || '',
279
+ priority: specData.priority || 'medium',
280
+ targetUrls: specData.targetUrls || specData.urls || [],
281
+ scenarios,
282
+ screenshots,
283
+ acceptanceCriteria: specData.acceptanceCriteria || [],
284
+ sourcePath,
285
+ };
286
+ });
287
+ // Load screenshot data
288
+ this.loadScreenshots(specs);
289
+ return specs;
290
+ }
291
+ catch (error) {
292
+ throw new Error(`Failed to parse JSON specification: ${error instanceof Error ? error.message : String(error)}`);
293
+ }
294
+ }
295
+ /**
296
+ * Parse PDF specification using LLM vision
297
+ *
298
+ * PDFs can contain:
299
+ * - Product requirement documents (PRDs)
300
+ * - Feature specifications with screenshots
301
+ * - UI mockups and wireframes
302
+ * - User flow diagrams
303
+ * - Acceptance criteria and test plans
304
+ *
305
+ * The LLM will extract structured information including:
306
+ * - Feature name and description
307
+ * - Business scenarios
308
+ * - Target URLs (inferred from screenshots/mockups)
309
+ * - Acceptance criteria
310
+ * - Screenshots/mockups for visual comparison
311
+ */
312
+ async parsePDF(pdfPath) {
313
+ /* eslint-disable no-console */
314
+ // Console output is expected for PDF parsing progress
315
+ console.log(`📄 Parsing PDF specification: ${pdfPath}`);
316
+ // Read PDF file first to calculate hash
317
+ const pdfBuffer = (0, fs_1.readFileSync)(pdfPath);
318
+ const pdfHash = (0, crypto_1.createHash)('sha256').update(pdfBuffer).digest('hex');
319
+ // Check if already cached
320
+ if (this.cache && this.cache.isSpecificationCached(pdfPath, pdfHash)) {
321
+ console.log(` ✓ Using cached PDF specification (hash: ${pdfHash.substring(0, 8)}...)`);
322
+ const cachedSpecs = this.cache.getCachedSpecifications(pdfPath, pdfHash);
323
+ console.log(`✅ Loaded ${cachedSpecs.length} feature(s) from cache`);
324
+ // Print scenario count for each feature
325
+ for (const spec of cachedSpecs) {
326
+ const priority = spec.priority === 'critical' ? 'critical' : spec.priority;
327
+ console.log(` ✓ Loaded 1 specifications`);
328
+ console.log(` - ${spec.name} (${priority}): ${spec.scenarios.length} scenarios`);
329
+ }
330
+ return cachedSpecs;
331
+ }
332
+ // Check if provider supports vision (required for PDF parsing)
333
+ if (!this.llmProvider.capabilities.vision) {
334
+ throw new Error('PDF parsing requires a vision-capable LLM provider (e.g., Anthropic Claude). ' +
335
+ 'Current provider does not support vision. ' +
336
+ 'Use --llm-provider anthropic or --llm-provider hybrid');
337
+ }
338
+ // SECURITY WARNING: PDF will be sent to external LLM
339
+ console.warn('');
340
+ console.warn('⚠️ SECURITY WARNING: PDF Document Transmission ⚠️');
341
+ console.warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
342
+ console.warn('The PDF specification will be sent to an external LLM provider.');
343
+ console.warn('');
344
+ console.warn('Ensure the PDF does NOT contain:');
345
+ console.warn(' • Internal API keys or credentials');
346
+ console.warn(' • Sensitive architecture details');
347
+ console.warn(' • Production URLs or IP addresses');
348
+ console.warn(' • Personal Identifying Information (PII)');
349
+ console.warn(' • Proprietary business information');
350
+ console.warn('');
351
+ console.warn('The LLM provider may retain this data per their policy.');
352
+ console.warn('For Anthropic: https://www.anthropic.com/legal/privacy');
353
+ console.warn('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
354
+ console.warn('');
355
+ // Check if user has consented (via environment variable)
356
+ if (!process.env.AUTONOMOUS_ALLOW_PDF_UPLOAD) {
357
+ console.error('❌ PDF upload blocked for security.');
358
+ console.error('❌ To proceed, set environment variable:');
359
+ console.error('❌ AUTONOMOUS_ALLOW_PDF_UPLOAD=true');
360
+ console.error('❌ Only set this if you have reviewed the PDF for sensitive data.');
361
+ throw new Error('PDF upload requires explicit consent via AUTONOMOUS_ALLOW_PDF_UPLOAD=true');
362
+ }
363
+ try {
364
+ console.log(`📄 Extracting text from PDF... (${(pdfBuffer.length / 1024).toFixed(2)} KB)`);
365
+ // Parse PDF to extract text content (dynamic import with default export)
366
+ const pdfParse = (await import('pdf-parse')).default;
367
+ const pdfData = await pdfParse(pdfBuffer);
368
+ const pdfText = pdfData.text;
369
+ console.log(` ✓ Extracted ${pdfText.length} characters, ${pdfData.numpages} pages`);
370
+ // Use LLM for semantic parsing (text-only, no vision)
371
+ const prompt = `
372
+ You are a test specification parser. Parse this UX/product specification document and extract structured, machine-operable testing information.
373
+
374
+ The document describes features, user flows, permissions, edge cases, and acceptance criteria.
375
+
376
+ Extract the following in JSON format (this schema is optimized for test automation):
377
+
378
+ {
379
+ "features": [
380
+ {
381
+ "name": "Feature Name (e.g., Auto-translation)",
382
+ "scope": "MVP|v1|v2",
383
+ "description": "What this feature does",
384
+ "priority": "critical|high|medium|low",
385
+
386
+ "roles": {
387
+ "role_name": {
388
+ "permissions": ["permission1", "permission2"]
389
+ }
390
+ },
391
+
392
+ "enablement": {
393
+ "location_type": {
394
+ "enabled_by": "who can enable",
395
+ "default_state": "on|off",
396
+ "side_effects": ["system_message", "label_visible"]
397
+ }
398
+ },
399
+
400
+ "scenarios": [
401
+ {
402
+ "name": "Scenario name",
403
+ "given": "Precondition",
404
+ "when": "Action",
405
+ "then": "Expected outcome",
406
+ "priority": "must-have|should-have|nice-to-have"
407
+ }
408
+ ],
409
+
410
+ "acceptanceCriteria": [
411
+ "Testable criterion"
412
+ ],
413
+
414
+ "stateMachines": {
415
+ "message_lifecycle": {
416
+ "states": ["translating", "translated", "failed"],
417
+ "transitions": ["new_message -> translating", "translating -> translated"]
418
+ }
419
+ },
420
+
421
+ "uiIndicators": {
422
+ "location": "visual_indicator"
423
+ },
424
+
425
+ "edgeCases": [
426
+ "What happens when X"
427
+ ],
428
+
429
+ "platformDifferences": {
430
+ "desktop": "behavior",
431
+ "mobile": "behavior"
432
+ },
433
+
434
+ "nonGoals": [
435
+ "Out of scope items"
436
+ ]
437
+ }
438
+ ]
439
+ }
440
+
441
+ IMPORTANT:
442
+ - Extract ALL features, flows, roles, and permissions
443
+ - Map state machines if described
444
+ - Identify edge cases and failure modes
445
+ - List platform differences
446
+ - Capture non-goals/out-of-scope items
447
+ - DO NOT infer or generate target URLs - feature specs describe behavior, not routes
448
+ - Be precise with permissions and access control
449
+ - For MVP features or P0 features, set priority to "critical"
450
+ - For must-have scenarios in MVP features, set scenario priority to "must-have"
451
+
452
+ Respond with ONLY valid JSON, no markdown formatting.
453
+
454
+ DOCUMENT TEXT:
455
+ ${pdfText}
456
+ `.trim();
457
+ // Call LLM with text (no vision)
458
+ // Use higher maxTokens for complex documents (16K for large specs)
459
+ const response = await this.llmProvider.generateText(prompt, {
460
+ maxTokens: 16000,
461
+ temperature: 0.1, // Very low temperature for structured extraction
462
+ });
463
+ // Parse LLM response
464
+ let jsonText = response.text.trim();
465
+ // Remove markdown code blocks if present (handle various formats)
466
+ // Try multiple patterns to be more robust
467
+ const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]+?)\s*```/);
468
+ if (jsonMatch) {
469
+ jsonText = jsonMatch[1].trim();
470
+ }
471
+ else if (jsonText.startsWith('```')) {
472
+ // Fallback: remove all triple backticks
473
+ jsonText = jsonText
474
+ .replace(/```[^\n]*\n?/g, '')
475
+ .replace(/```\s*$/g, '')
476
+ .trim();
477
+ }
478
+ let data;
479
+ try {
480
+ data = JSON.parse(jsonText);
481
+ }
482
+ catch (parseError) {
483
+ // Log the problematic JSON for debugging
484
+ console.error('Failed to parse LLM response as JSON');
485
+ console.error('First 500 chars:', jsonText.substring(0, 500));
486
+ console.error('Last 500 chars:', jsonText.substring(Math.max(0, jsonText.length - 500)));
487
+ // Try to repair common JSON issues
488
+ console.log('Attempting to repair JSON...');
489
+ let repairedJson = jsonText;
490
+ // Common fixes:
491
+ // 1. Fix unquoted property names (e.g., {name: "value"} -> {"name": "value"})
492
+ repairedJson = repairedJson.replace(/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":');
493
+ // 2. Fix single quotes to double quotes
494
+ repairedJson = repairedJson.replace(/'/g, '"');
495
+ // 3. Remove trailing commas
496
+ repairedJson = repairedJson.replace(/,(\s*[}\]])/g, '$1');
497
+ // 4. Fix missing quotes around string values (this is tricky, skip for now)
498
+ try {
499
+ data = JSON.parse(repairedJson);
500
+ console.log('✓ JSON repair successful!');
501
+ }
502
+ catch {
503
+ // If repair failed, ask LLM to fix it
504
+ console.log('JSON repair failed, requesting LLM to fix the JSON...');
505
+ const fixPrompt = `
506
+ The following JSON is malformed. Please fix it and return ONLY valid JSON with no markdown formatting:
507
+
508
+ ${jsonText}
509
+
510
+ Error: ${parseError instanceof Error ? parseError.message : String(parseError)}
511
+
512
+ Return the corrected JSON:`.trim();
513
+ const fixResponse = await this.llmProvider.generateText(fixPrompt, {
514
+ maxTokens: 16000,
515
+ temperature: 0,
516
+ });
517
+ let fixedJson = fixResponse.text.trim();
518
+ // Remove markdown code blocks again
519
+ const fixedMatch = fixedJson.match(/```(?:json)?\s*([\s\S]+?)\s*```/);
520
+ if (fixedMatch) {
521
+ fixedJson = fixedMatch[1].trim();
522
+ }
523
+ else if (fixedJson.startsWith('```')) {
524
+ fixedJson = fixedJson
525
+ .replace(/```[^\n]*\n?/g, '')
526
+ .replace(/```\s*$/g, '')
527
+ .trim();
528
+ }
529
+ try {
530
+ data = JSON.parse(fixedJson);
531
+ console.log('✓ LLM successfully fixed the JSON!');
532
+ }
533
+ catch (finalError) {
534
+ console.error('Failed to parse even after LLM repair');
535
+ console.error('Original error:', parseError);
536
+ console.error('Repair error:', finalError);
537
+ throw parseError; // Throw original error
538
+ }
539
+ }
540
+ }
541
+ if (!data.features || !Array.isArray(data.features)) {
542
+ throw new Error('Invalid PDF parse response: missing features array');
543
+ }
544
+ console.log(`✅ Extracted ${data.features.length} feature(s) from PDF`);
545
+ // Convert to FeatureSpecification format
546
+ const specs = data.features.map((feature) => {
547
+ const scenarios = (feature.scenarios || []).map((s) => ({
548
+ name: s.name,
549
+ given: s.given || '',
550
+ when: s.when || '',
551
+ then: s.then || '',
552
+ priority: s.priority || 'should-have',
553
+ }));
554
+ // Store screenshot metadata (page numbers for extraction)
555
+ const screenshots = (feature.screenshots || []).map((s) => ({
556
+ path: `${pdfPath}#page=${s.pageNumber}`,
557
+ description: s.description || `Page ${s.pageNumber}`,
558
+ pageNumber: s.pageNumber,
559
+ }));
560
+ const spec = {
561
+ id: this.generateSpecId(feature.name),
562
+ name: feature.name,
563
+ description: feature.description || '',
564
+ priority: feature.priority || 'medium',
565
+ targetUrls: feature.targetUrls || [],
566
+ scenarios,
567
+ screenshots,
568
+ acceptanceCriteria: feature.acceptanceCriteria || [],
569
+ sourcePath: pdfPath,
570
+ sourceHash: pdfHash,
571
+ metadata: {
572
+ uiElements: feature.uiElements || [],
573
+ userFlows: feature.userFlows || [],
574
+ extractedFrom: 'pdf',
575
+ },
576
+ };
577
+ return spec;
578
+ });
579
+ // Save to cache if knowledge base is available
580
+ if (this.cache) {
581
+ for (const spec of specs) {
582
+ this.cache.saveSpecification(spec);
583
+ }
584
+ }
585
+ /* eslint-enable no-console */
586
+ return specs;
587
+ }
588
+ catch (error) {
589
+ if (error instanceof Error && error.message.includes('vision')) {
590
+ throw error; // Re-throw vision capability errors
591
+ }
592
+ throw new Error(`Failed to parse PDF specification: ${error instanceof Error ? error.message : String(error)}. ` +
593
+ 'Ensure the PDF contains readable text and images. ' +
594
+ 'For scanned PDFs, ensure they have been OCR processed.');
595
+ }
596
+ }
597
+ /**
598
+ * Parse natural language focus string using LLM
599
+ */
600
+ async parseFocusString(focusString) {
601
+ const prompt = `
602
+ You are a test specification generator. Given a natural language focus string, extract:
603
+ 1. Feature name (infer from the focus string)
604
+ 2. Target URLs (guess likely URLs based on feature names)
605
+ 3. Business scenarios (generate 2-3 test scenarios)
606
+ 4. Priority (infer from words like "critical", "thoroughly", etc.)
607
+
608
+ Focus string: "${focusString}"
609
+
610
+ Respond with valid JSON in this format:
611
+ {
612
+ "feature": "Feature Name",
613
+ "priority": "high|medium|low",
614
+ "targetUrls": ["/url1", "/url2"],
615
+ "scenarios": [
616
+ {
617
+ "name": "Scenario name",
618
+ "given": "Precondition",
619
+ "when": "Action",
620
+ "then": "Expected result",
621
+ "priority": "must-have|should-have|nice-to-have"
622
+ }
623
+ ],
624
+ "acceptanceCriteria": ["criterion 1", "criterion 2"]
625
+ }
626
+ `.trim();
627
+ try {
628
+ const response = await this.llmProvider.generateText(prompt, {
629
+ maxTokens: 1000,
630
+ temperature: 0.3,
631
+ });
632
+ // Extract JSON from response (might have markdown code blocks)
633
+ let jsonText = response.text.trim();
634
+ // Try multiple patterns to be more robust
635
+ const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]+?)\s*```/);
636
+ if (jsonMatch) {
637
+ jsonText = jsonMatch[1].trim();
638
+ }
639
+ else if (jsonText.startsWith('```')) {
640
+ // Fallback: remove all triple backticks
641
+ jsonText = jsonText
642
+ .replace(/```[^\n]*\n?/g, '')
643
+ .replace(/```\s*$/g, '')
644
+ .trim();
645
+ }
646
+ const data = JSON.parse(jsonText);
647
+ const scenarios = (data.scenarios || []).map((s) => ({
648
+ name: s.name,
649
+ given: s.given,
650
+ when: s.when,
651
+ then: s.then,
652
+ priority: s.priority || 'should-have',
653
+ }));
654
+ return [
655
+ {
656
+ id: this.generateSpecId(data.feature),
657
+ name: data.feature,
658
+ description: `Generated from focus string: ${focusString}`,
659
+ priority: data.priority || 'medium',
660
+ targetUrls: data.targetUrls || [],
661
+ scenarios,
662
+ screenshots: [],
663
+ acceptanceCriteria: data.acceptanceCriteria || [],
664
+ sourcePath: 'focus-string',
665
+ },
666
+ ];
667
+ }
668
+ catch (error) {
669
+ throw new Error(`Failed to parse focus string with LLM: ${error instanceof Error ? error.message : String(error)}`);
670
+ }
671
+ }
672
+ /**
673
+ * Finalize partial spec
674
+ */
675
+ finalizeSpec(partial, sourcePath) {
676
+ return {
677
+ id: this.generateSpecId(partial.name || 'unknown'),
678
+ name: partial.name || 'Unnamed Feature',
679
+ description: (partial.description || '').trim(),
680
+ priority: partial.priority || 'medium',
681
+ targetUrls: partial.targetUrls || [],
682
+ scenarios: partial.scenarios || [],
683
+ screenshots: partial.screenshots || [],
684
+ acceptanceCriteria: partial.acceptanceCriteria || [],
685
+ sourcePath,
686
+ };
687
+ }
688
+ /**
689
+ * Finalize partial scenario
690
+ */
691
+ finalizeScenario(partial) {
692
+ return {
693
+ name: partial.name || 'Unnamed Scenario',
694
+ given: partial.given || '',
695
+ when: partial.when || '',
696
+ then: partial.then || '',
697
+ priority: partial.priority || 'should-have',
698
+ };
699
+ }
700
+ /**
701
+ * Generate spec ID from name
702
+ */
703
+ generateSpecId(name) {
704
+ return name
705
+ .toLowerCase()
706
+ .replace(/[^a-z0-9]+/g, '-')
707
+ .replace(/^-|-$/g, '');
708
+ }
709
+ /**
710
+ * Resolve image path relative to spec file
711
+ */
712
+ resolveImagePath(imagePath, specPath) {
713
+ if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
714
+ return imagePath; // Absolute URL
715
+ }
716
+ if (imagePath.startsWith('/')) {
717
+ return imagePath; // Absolute path
718
+ }
719
+ // Relative path - resolve relative to spec file
720
+ const specDir = (0, path_1.dirname)(specPath);
721
+ return (0, path_1.join)(specDir, imagePath);
722
+ }
723
+ /**
724
+ * Load screenshot data from files
725
+ */
726
+ async loadScreenshots(specs) {
727
+ for (const spec of specs) {
728
+ for (const screenshot of spec.screenshots) {
729
+ try {
730
+ if ((0, fs_1.existsSync)(screenshot.path)) {
731
+ const imageData = (0, fs_1.readFileSync)(screenshot.path);
732
+ screenshot.data = imageData.toString('base64');
733
+ }
734
+ else {
735
+ // eslint-disable-next-line no-console
736
+ console.warn(`Screenshot not found: ${screenshot.path}`);
737
+ }
738
+ }
739
+ catch (error) {
740
+ // eslint-disable-next-line no-console
741
+ console.warn(`Failed to load screenshot ${screenshot.path}:`, error);
742
+ }
743
+ }
744
+ }
745
+ }
746
+ /**
747
+ * Validate parsed specification
748
+ */
749
+ validateSpec(spec) {
750
+ const errors = [];
751
+ if (!spec.name || spec.name.trim().length === 0) {
752
+ errors.push('Feature name is required');
753
+ }
754
+ if (spec.scenarios.length === 0) {
755
+ errors.push('At least one scenario is required');
756
+ }
757
+ for (let i = 0; i < spec.scenarios.length; i++) {
758
+ const scenario = spec.scenarios[i];
759
+ if (!scenario.given || !scenario.when || !scenario.then) {
760
+ errors.push(`Scenario ${i + 1} is incomplete (missing given/when/then)`);
761
+ }
762
+ }
763
+ return {
764
+ valid: errors.length === 0,
765
+ errors,
766
+ };
767
+ }
768
+ /**
769
+ * Get spec coverage summary
770
+ */
771
+ getSpecSummary(spec) {
772
+ return {
773
+ id: spec.id,
774
+ name: spec.name,
775
+ priority: spec.priority,
776
+ targetUrlCount: spec.targetUrls.length,
777
+ scenarioCount: spec.scenarios.length,
778
+ mustHaveScenarios: spec.scenarios.filter((s) => s.priority === 'must-have').length,
779
+ shouldHaveScenarios: spec.scenarios.filter((s) => s.priority === 'should-have').length,
780
+ niceToHaveScenarios: spec.scenarios.filter((s) => s.priority === 'nice-to-have').length,
781
+ acceptanceCriteriaCount: spec.acceptanceCriteria.length,
782
+ hasScreenshots: spec.screenshots.length > 0,
783
+ };
784
+ }
785
+ }
786
+ exports.SpecificationParser = SpecificationParser;