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