@taodigital/agents 0.0.0-stage → 1.2.1

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 (76) hide show
  1. package/README.md +99 -2
  2. package/dist/api-test-agent.d.ts +126 -0
  3. package/dist/api-test-agent.js +267 -0
  4. package/dist/artifact-intel-agent.d.ts +11 -0
  5. package/dist/artifact-intel-agent.js +152 -0
  6. package/dist/coverage-gap-agent.d.ts +25 -0
  7. package/dist/coverage-gap-agent.js +269 -0
  8. package/dist/coverage-map-agent.d.ts +15 -0
  9. package/dist/coverage-map-agent.js +255 -0
  10. package/dist/crawl-diff-agent.d.ts +27 -0
  11. package/dist/crawl-diff-agent.js +412 -0
  12. package/dist/data-agent.d.ts +101 -0
  13. package/dist/data-agent.js +2284 -0
  14. package/dist/document-agent.d.ts +10 -0
  15. package/dist/document-agent.js +32 -0
  16. package/dist/enhancement-agent.d.ts +48 -0
  17. package/dist/enhancement-agent.js +342 -0
  18. package/dist/field-detector-agent.d.ts +35 -0
  19. package/dist/field-detector-agent.js +220 -0
  20. package/dist/flow-agent.d.ts +130 -0
  21. package/dist/flow-agent.js +169 -0
  22. package/dist/github-agent.d.ts +18 -0
  23. package/dist/github-agent.js +39 -0
  24. package/dist/github-write-agent.d.ts +67 -0
  25. package/dist/github-write-agent.js +249 -0
  26. package/dist/grouping-agent.d.ts +18 -0
  27. package/dist/grouping-agent.js +338 -0
  28. package/dist/healing-agent.d.ts +73 -0
  29. package/dist/healing-agent.js +137 -0
  30. package/dist/index.d.ts +36 -0
  31. package/dist/index.js +67 -0
  32. package/dist/integration-test-agent.d.ts +118 -0
  33. package/dist/integration-test-agent.js +165 -0
  34. package/dist/label-assignment-agent.d.ts +15 -0
  35. package/dist/label-assignment-agent.js +97 -0
  36. package/dist/llm-config.d.ts +38 -0
  37. package/dist/llm-config.js +287 -0
  38. package/dist/llm-mock.d.ts +77 -0
  39. package/dist/llm-mock.js +192 -0
  40. package/dist/llm.d.ts +58 -0
  41. package/dist/llm.js +438 -0
  42. package/dist/nlp-agent.d.ts +208 -0
  43. package/dist/nlp-agent.js +340 -0
  44. package/dist/orchestrator.d.ts +285 -0
  45. package/dist/orchestrator.js +141 -0
  46. package/dist/plan-agent.d.ts +4 -0
  47. package/dist/plan-agent.js +200 -0
  48. package/dist/repo-scan-agent.d.ts +189 -0
  49. package/dist/repo-scan-agent.js +2032 -0
  50. package/dist/requirements-agent.d.ts +59 -0
  51. package/dist/requirements-agent.js +641 -0
  52. package/dist/scenario-agent.d.ts +20 -0
  53. package/dist/scenario-agent.js +123 -0
  54. package/dist/script-agent.d.ts +447 -0
  55. package/dist/script-agent.js +11813 -0
  56. package/dist/security-test-agent.d.ts +111 -0
  57. package/dist/security-test-agent.js +141 -0
  58. package/dist/skill-file-agent.d.ts +264 -0
  59. package/dist/skill-file-agent.js +3470 -0
  60. package/dist/step-coverage.d.ts +46 -0
  61. package/dist/step-coverage.js +288 -0
  62. package/dist/step-utils.d.ts +2 -0
  63. package/dist/step-utils.js +62 -0
  64. package/dist/test-case-agent.d.ts +206 -0
  65. package/dist/test-case-agent.js +2141 -0
  66. package/dist/test-import-agent.d.ts +148 -0
  67. package/dist/test-import-agent.js +3235 -0
  68. package/dist/test-script-agent.d.ts +16 -0
  69. package/dist/test-script-agent.js +48 -0
  70. package/dist/token-pricing.d.ts +14 -0
  71. package/dist/token-pricing.js +62 -0
  72. package/dist/types.d.ts +232 -0
  73. package/dist/types.js +7 -0
  74. package/dist/visual-agent.d.ts +114 -0
  75. package/dist/visual-agent.js +261 -0
  76. package/package.json +26 -4
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ /**
3
+ * ZeTa-AI Field Detector Agent
4
+ *
5
+ * Drafts an AppField catalog from one of three inputs:
6
+ * (1) Rendered HTML — DOM detector reads form controls
7
+ * (2) OpenAPI/Swagger JSON/YAML — schema detector reads request bodies
8
+ * (3) Sample JSON payload — shape detector infers types from values
9
+ *
10
+ * Output is always FieldSchema[] in "unconfirmed" status.
11
+ * Persist via AppBrain.upsertAppField() after user review.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.detectFieldsFromHtml = detectFieldsFromHtml;
15
+ exports.detectFieldsFromApi = detectFieldsFromApi;
16
+ exports.mergeIntoCatalog = mergeIntoCatalog;
17
+ const app_brain_1 = require("@taodigital/app-brain");
18
+ const llm_js_1 = require("./llm.js");
19
+ const llm_config_js_1 = require("./llm-config.js");
20
+ const core_1 = require("@taodigital/core");
21
+ // ─── DOM mapping table (deterministic — no LLM needed for mapping, only for label inference) ──
22
+ const DOM_ELEMENT_ROLE_MAP = {
23
+ 'input:text': { elementRole: 'INPUT_TEXT', dataType: 'string', dataBearing: true },
24
+ 'input:email': { elementRole: 'INPUT_EMAIL', dataType: 'email', dataBearing: true },
25
+ 'input:password': { elementRole: 'INPUT_PASSWORD', dataType: 'string', dataBearing: true },
26
+ 'input:number': { elementRole: 'INPUT_NUMBER', dataType: 'integer', dataBearing: true },
27
+ 'input:tel': { elementRole: 'INPUT_TEXT', dataType: 'phone', dataBearing: true },
28
+ 'input:url': { elementRole: 'INPUT_TEXT', dataType: 'url', dataBearing: true },
29
+ 'input:date': { elementRole: 'DATE', dataType: 'date', dataBearing: true },
30
+ 'input:datetime-local': { elementRole: 'DATE', dataType: 'datetime', dataBearing: true },
31
+ 'input:checkbox': { elementRole: 'CHECKBOX', dataType: 'boolean', dataBearing: true },
32
+ 'input:radio': { elementRole: 'RADIO', dataType: 'enum', dataBearing: true },
33
+ 'input:file': { elementRole: 'FILE', dataType: 'file', dataBearing: true },
34
+ 'input:search': { elementRole: 'INPUT_TEXT', dataType: 'string', dataBearing: true },
35
+ 'input:color': { elementRole: 'INPUT_TEXT', dataType: 'string', dataBearing: true },
36
+ 'textarea': { elementRole: 'TEXTAREA', dataType: 'string', dataBearing: true },
37
+ 'select': { elementRole: 'SELECT', dataType: 'enum', dataBearing: true },
38
+ 'button': { elementRole: 'BUTTON', dataType: 'string', dataBearing: false },
39
+ 'a': { elementRole: 'LINK', dataType: 'string', dataBearing: false },
40
+ 'toggle': { elementRole: 'TOGGLE', dataType: 'boolean', dataBearing: false },
41
+ };
42
+ function inferPiiClass(label, fieldKey) {
43
+ const s = (label + ' ' + fieldKey).toLowerCase();
44
+ if (/password|ssn|aadhaar|pan|card.?(number|num|no)|cvv|cvc|secret|token|private/.test(s))
45
+ return 'high';
46
+ if (/email|phone|mobile|name|address|dob|birth|gender|salary|income/.test(s))
47
+ return 'low';
48
+ return 'none';
49
+ }
50
+ const HTML_DETECTION_SYSTEM_DEFAULT = `You extract a test-data field catalog from HTML.
51
+
52
+ For every form control, emit one FieldSchema JSON object.
53
+
54
+ MAPPING RULES:
55
+ - dataBearing = false for <button>, <a>, role="button", toggles/switches, submit buttons.
56
+ These still get a row (cases grounded on them need a catalog entry) but have NO datasets.
57
+ - Skip hidden, disabled, and aria-hidden elements entirely.
58
+ - Pull constraints ONLY from real HTML attributes: min, max, minlength, maxlength, pattern,
59
+ step, required, accept, <option> values. Never invent constraints.
60
+ - Group <input type="radio"> sharing the same name into ONE enum field; enumValues = the radio values.
61
+ - Derive label (priority): <label for=> text → aria-label → placeholder → name attribute.
62
+ - Derive fieldKey: name → id → snake_case of label. Unique per form; append _2, _3 on collision.
63
+ - piiClass: infer from name/label keywords (password/ssn/card → high; email/phone/name → low; else none).
64
+ - exampleValue: one realistic value satisfying the constraints. For piiClass=high, use obviously-synthetic
65
+ (e.g. "Test User", "4111111111111111") — never real-looking sensitive data.
66
+ - If type is uncertain, use dataType:"string" and add notes:"type inferred, confirm".
67
+
68
+ OUTPUT: valid JSON only — { "fields": FieldSchema[] }. No preamble, no markdown.
69
+
70
+ Each FieldSchema has: fieldKey, label, elementRole, dataBearing, dataType, required,
71
+ and any applicable: minLength, maxLength, minValue, maxValue, precision, regex, enumValues,
72
+ piiClass, exampleValue, relationGroup, notes.`;
73
+ const API_DETECTION_SYSTEM_DEFAULT = `You extract a test-data field catalog from an API contract or sample payload.
74
+
75
+ For OpenAPI/Swagger:
76
+ - type:string + format:email → dataType:email; format:date/date-time → date/datetime;
77
+ format:uri → url; format:phone → phone; type:integer → integer; type:number → decimal
78
+ (precision from multipleOf if present); type:boolean → boolean.
79
+ - Constraints: minLength/maxLength, minimum/maximum → minValue/maxValue, pattern → regex,
80
+ required[] → required:true, enum → dataType:enum + enumValues.
81
+ - All payload fields: dataBearing = true.
82
+
83
+ For raw JSON sample:
84
+ - Infer dataType from the value: email-pattern → email, ISO-date → date,
85
+ number → integer (if no decimal) or decimal, true/false → boolean, else string.
86
+ - No structural constraints available → add notes:"inferred from sample — confirm constraints".
87
+
88
+ Common rules:
89
+ - Nested objects: flatten with dot keys (e.g. "beneficiary.ifsc_code") as fieldKey.
90
+ - Arrays: emit the element field once, notes:"repeats in array".
91
+ - piiClass by keyword inference; synthetic exampleValue for piiClass=high.
92
+
93
+ OUTPUT: valid JSON only — { "fields": FieldSchema[] }. No preamble, no markdown.`;
94
+ async function detectFieldsFromHtml(opts) {
95
+ const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(opts.tenantId, 'testGeneration');
96
+ const user = `HTML TO ANALYZE:\n\`\`\`html\n${opts.html.slice(0, 40_000)}\n\`\`\``;
97
+ const t0 = Date.now();
98
+ const seed = (0, llm_js_1.deterministicSeed)(`${opts.tenantId}:${opts.projectId}:html-fields:${opts.html.slice(0, 500)}`);
99
+ const htmlSystem = await (0, core_1.getAgentPrompt)('field-detector-html', undefined, opts.tenantId) ?? HTML_DETECTION_SYSTEM_DEFAULT;
100
+ const { text: raw, usage } = await (0, llm_js_1.askTracked)(htmlSystem, user, llmCfg, undefined, undefined, { temperature: 0, seed });
101
+ const latencyMs = Date.now() - t0;
102
+ (0, app_brain_1.recordLLMCall)({
103
+ tenantId: opts.tenantId,
104
+ projectId: opts.projectId,
105
+ agentType: 'data',
106
+ label: 'field_detection_html',
107
+ entityId: opts.projectId,
108
+ entityType: 'field_catalog',
109
+ usage,
110
+ latencyMs,
111
+ }).catch(console.error);
112
+ return parseFieldsFromLlmOutput(raw);
113
+ }
114
+ async function detectFieldsFromApi(opts) {
115
+ const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(opts.tenantId, 'testGeneration');
116
+ const user = `API CONTRACT / SAMPLE PAYLOAD:\n\`\`\`\n${opts.input.slice(0, 40_000)}\n\`\`\``;
117
+ const t0 = Date.now();
118
+ const seed = (0, llm_js_1.deterministicSeed)(`${opts.tenantId}:${opts.projectId}:api-fields:${opts.input.slice(0, 500)}`);
119
+ const apiSystem = await (0, core_1.getAgentPrompt)('field-detector-api', undefined, opts.tenantId) ?? API_DETECTION_SYSTEM_DEFAULT;
120
+ const { text: raw, usage } = await (0, llm_js_1.askTracked)(apiSystem, user, llmCfg, undefined, undefined, { temperature: 0, seed });
121
+ const latencyMs = Date.now() - t0;
122
+ (0, app_brain_1.recordLLMCall)({
123
+ tenantId: opts.tenantId,
124
+ projectId: opts.projectId,
125
+ agentType: 'data',
126
+ label: 'field_detection_api',
127
+ entityId: opts.projectId,
128
+ entityType: 'field_catalog',
129
+ usage,
130
+ latencyMs,
131
+ }).catch(console.error);
132
+ return parseFieldsFromLlmOutput(raw);
133
+ }
134
+ function parseFieldsFromLlmOutput(raw) {
135
+ const jsonStart = raw.indexOf('{');
136
+ const jsonEnd = raw.lastIndexOf('}');
137
+ if (jsonStart === -1 || jsonEnd === -1)
138
+ return [];
139
+ try {
140
+ const parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
141
+ const fields = (parsed.fields ?? []).map((f) => ({
142
+ fieldKey: String(f.fieldKey ?? '').slice(0, 80),
143
+ label: String(f.label ?? f.fieldKey ?? ''),
144
+ elementRole: f.elementRole ?? 'OTHER',
145
+ dataBearing: f.dataBearing !== false,
146
+ dataType: f.dataType ?? 'string',
147
+ required: f.required === true,
148
+ minLength: f.minLength != null ? Number(f.minLength) : undefined,
149
+ maxLength: f.maxLength != null ? Number(f.maxLength) : undefined,
150
+ minValue: f.minValue != null ? Number(f.minValue) : undefined,
151
+ maxValue: f.maxValue != null ? Number(f.maxValue) : undefined,
152
+ precision: f.precision != null ? Number(f.precision) : undefined,
153
+ regex: f.regex ?? undefined,
154
+ enumValues: Array.isArray(f.enumValues) ? f.enumValues.map(String) : undefined,
155
+ piiClass: f.piiClass ?? inferPiiClass(f.label ?? '', f.fieldKey ?? ''),
156
+ exampleValue: f.exampleValue ?? undefined,
157
+ relationGroup: f.relationGroup ?? undefined,
158
+ notes: f.notes ?? undefined,
159
+ status: 'unconfirmed',
160
+ }));
161
+ // Dedup by fieldKey (keep first occurrence)
162
+ const seen = new Set();
163
+ return fields.filter((f) => {
164
+ if (!f.fieldKey || seen.has(f.fieldKey))
165
+ return false;
166
+ seen.add(f.fieldKey);
167
+ return true;
168
+ });
169
+ }
170
+ catch {
171
+ return [];
172
+ }
173
+ }
174
+ /**
175
+ * Merge auto-detected fields into the project catalog.
176
+ * Preserves human-edited piiClass, notes, and status on existing rows.
177
+ */
178
+ async function mergeIntoCatalog(opts) {
179
+ const existing = await app_brain_1.AppBrain.listAppFields(opts.tenantId, opts.projectId);
180
+ const existingByKey = new Map(existing.map((r) => [r.fieldKey, r]));
181
+ let added = 0, updated = 0, unchanged = 0;
182
+ for (const field of opts.detected) {
183
+ const current = existingByKey.get(field.fieldKey);
184
+ if (!current) {
185
+ await app_brain_1.AppBrain.upsertAppField(opts.tenantId, opts.projectId, { ...field, status: 'unconfirmed' });
186
+ added++;
187
+ }
188
+ else {
189
+ // Never overwrite human-edited fields
190
+ const merged = {
191
+ ...field,
192
+ piiClass: current.piiClass ?? field.piiClass,
193
+ notes: current.notes ?? field.notes,
194
+ status: current.status ?? 'unconfirmed',
195
+ };
196
+ // Only update if structural fields changed
197
+ const changed = current.elementRole !== merged.elementRole ||
198
+ current.dataBearing !== merged.dataBearing ||
199
+ current.dataType !== merged.dataType ||
200
+ current.required !== merged.required ||
201
+ current.minLength !== (merged.minLength ?? null) ||
202
+ current.maxLength !== (merged.maxLength ?? null);
203
+ if (changed) {
204
+ await app_brain_1.AppBrain.upsertAppField(opts.tenantId, opts.projectId, merged);
205
+ updated++;
206
+ }
207
+ else {
208
+ unchanged++;
209
+ }
210
+ }
211
+ }
212
+ // Mark fields no longer seen in the page as stale (but do NOT delete)
213
+ const detectedKeys = new Set(opts.detected.map((f) => f.fieldKey));
214
+ for (const row of existing) {
215
+ if (!detectedKeys.has(row.fieldKey) && row.status !== 'stale') {
216
+ await app_brain_1.AppBrain.confirmAppField(opts.tenantId, opts.projectId, row.fieldKey, { status: 'stale' });
217
+ }
218
+ }
219
+ return { added, updated, unchanged };
220
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Flow Agent — KAG + CAG Journey Test Generator
3
+ *
4
+ * Generates cross-screen E2E journey test cases by combining:
5
+ * KAG: the navigation graph path (which elements link which screens)
6
+ * CAG: per-screen grounding context (elements, skill files)
7
+ *
8
+ * A journey test case spans multiple screens:
9
+ * Given I am on LoginScreen
10
+ * When I enter valid credentials and click "Sign in"
11
+ * Then I navigate to DashboardScreen
12
+ * When I click "New Project"
13
+ * Then I navigate to ProjectSetupScreen
14
+ * And the form shows required fields...
15
+ *
16
+ * Test kinds generated: E2E, REGRESSION, SMOKE, SANITY
17
+ */
18
+ export interface FlowEdge {
19
+ fromScreenId: string;
20
+ toScreenId: string;
21
+ viaElementMeaning: string;
22
+ }
23
+ export interface FlowTestCase {
24
+ title: string;
25
+ kind: 'E2E' | 'REGRESSION' | 'SMOKE' | 'SANITY';
26
+ priority: 'CRITICAL' | 'HIGH' | 'MEDIUM';
27
+ steps: string[];
28
+ expectedResult: string;
29
+ preconditions: string;
30
+ groundedOn: string[];
31
+ tags: string[];
32
+ }
33
+ export declare function generateFlowTestCases(opts: {
34
+ tenantId: string;
35
+ projectId: string;
36
+ startScreenId: string;
37
+ endScreenId: string;
38
+ path: FlowEdge[];
39
+ }): Promise<{
40
+ status: string;
41
+ cases: never[];
42
+ saved: null;
43
+ screenCount?: undefined;
44
+ suiteName?: undefined;
45
+ } | {
46
+ status: string;
47
+ cases: never[];
48
+ saved: null;
49
+ screenCount: number;
50
+ suiteName?: undefined;
51
+ } | {
52
+ status: string;
53
+ cases: FlowTestCase[];
54
+ saved: {
55
+ suite: {
56
+ tenantId: string;
57
+ id: string;
58
+ projectId: string;
59
+ name: string;
60
+ createdAt: Date;
61
+ genModel: string | null;
62
+ category: import(".prisma/client").$Enums.SuiteCategory | null;
63
+ parentId: string | null;
64
+ ownerUserId: string | null;
65
+ genTotalTokens: number | null;
66
+ genTotalCostUsd: number | null;
67
+ genAt: Date | null;
68
+ genScreenCount: number | null;
69
+ coverageReport: import("@prisma/client/runtime/library").JsonValue | null;
70
+ };
71
+ testCases: {
72
+ tenantId: string;
73
+ screenId: string | null;
74
+ id: string;
75
+ projectId: string;
76
+ source: string;
77
+ status: string;
78
+ createdAt: Date;
79
+ updatedAt: Date;
80
+ steps: import("@prisma/client/runtime/library").JsonValue;
81
+ kind: string;
82
+ tags: import("@prisma/client/runtime/library").JsonValue;
83
+ approvedBy: string | null;
84
+ suiteId: string;
85
+ ref: string | null;
86
+ title: string;
87
+ priority: string;
88
+ preconditions: string | null;
89
+ expectedResult: string | null;
90
+ gherkin: string | null;
91
+ approvedAt: Date | null;
92
+ groundedOn: import("@prisma/client/runtime/library").JsonValue;
93
+ caseRefs: import("@prisma/client/runtime/library").JsonValue;
94
+ viewports: import("@prisma/client/runtime/library").JsonValue;
95
+ viewportConstraint: string;
96
+ techniqueOfRecord: string | null;
97
+ traceTo: string | null;
98
+ traceToExtra: import("@prisma/client/runtime/library").JsonValue;
99
+ testDataInline: import("@prisma/client/runtime/library").JsonValue | null;
100
+ dataNote: string | null;
101
+ scriptNote: string | null;
102
+ structuredSteps: import("@prisma/client/runtime/library").JsonValue | null;
103
+ artifactId: string | null;
104
+ scenarioId: string | null;
105
+ suspect: boolean;
106
+ suspectReason: string | null;
107
+ flakyScore: number;
108
+ consecutiveFlakes: number;
109
+ lastFlakyAt: Date | null;
110
+ quarantined: boolean;
111
+ quarantinedAt: Date | null;
112
+ quarantineReason: string | null;
113
+ genPromptTokens: number | null;
114
+ genCompletionTokens: number | null;
115
+ genCostUsd: number | null;
116
+ genModel: string | null;
117
+ dataSetId: string | null;
118
+ repoTargetPath: string | null;
119
+ stepsKey: string | null;
120
+ gherkinKey: string | null;
121
+ contentHash: string | null;
122
+ groundingHash: string | null;
123
+ locatorsJson: import("@prisma/client/runtime/library").JsonValue | null;
124
+ locatorsExtractedAt: Date | null;
125
+ captureStepScreenshots: import("@prisma/client/runtime/library").JsonValue | null;
126
+ }[];
127
+ };
128
+ screenCount: number;
129
+ suiteName: string;
130
+ }>;
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ /**
3
+ * Flow Agent — KAG + CAG Journey Test Generator
4
+ *
5
+ * Generates cross-screen E2E journey test cases by combining:
6
+ * KAG: the navigation graph path (which elements link which screens)
7
+ * CAG: per-screen grounding context (elements, skill files)
8
+ *
9
+ * A journey test case spans multiple screens:
10
+ * Given I am on LoginScreen
11
+ * When I enter valid credentials and click "Sign in"
12
+ * Then I navigate to DashboardScreen
13
+ * When I click "New Project"
14
+ * Then I navigate to ProjectSetupScreen
15
+ * And the form shows required fields...
16
+ *
17
+ * Test kinds generated: E2E, REGRESSION, SMOKE, SANITY
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.generateFlowTestCases = generateFlowTestCases;
21
+ const app_brain_1 = require("@taodigital/app-brain");
22
+ const core_1 = require("@taodigital/core");
23
+ const llm_js_1 = require("./llm.js");
24
+ const llm_config_js_1 = require("./llm-config.js");
25
+ async function generateFlowTestCases(opts) {
26
+ if (opts.path.length === 0) {
27
+ return { status: 'NO_PATH', cases: [], saved: null };
28
+ }
29
+ const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(opts.tenantId, 'appBrain');
30
+ // Build ordered list of screen IDs in the journey
31
+ const screenIds = [opts.startScreenId, ...opts.path.map(e => e.toScreenId)];
32
+ const uniqueScreenIds = [...new Set(screenIds)];
33
+ // Load CAG context for each screen in the path
34
+ const grounding = (0, app_brain_1.groundingFor)(false);
35
+ const screenContexts = [];
36
+ for (const edge of [{ fromScreenId: '', toScreenId: opts.startScreenId, viaElementMeaning: '' }, ...opts.path]) {
37
+ const screenId = edge.toScreenId;
38
+ let ctx;
39
+ try {
40
+ const groundCtx = await grounding.ground(opts.tenantId, opts.projectId, screenId);
41
+ ctx = {
42
+ screenId,
43
+ screenName: groundCtx.screenName,
44
+ url: groundCtx.url,
45
+ elements: groundCtx.elements,
46
+ viaElement: edge.viaElementMeaning || undefined,
47
+ };
48
+ }
49
+ catch {
50
+ // Screen might not have detailed data — use minimal context
51
+ ctx = {
52
+ screenId,
53
+ screenName: `Screen ${screenId.slice(-6)}`,
54
+ elements: [],
55
+ viaElement: edge.viaElementMeaning || undefined,
56
+ };
57
+ }
58
+ screenContexts.push(ctx);
59
+ }
60
+ // Load global skill files once (not per-screen)
61
+ const GLOBAL_KINDS = ['TECH_STACK', 'AUTH_SESSION', 'BUSINESS_DOMAIN', 'INTERACTION_PATTERNS', 'ERROR_CATALOGUE'];
62
+ const globalFiles = await app_brain_1.AppBrain.listSkillFiles(opts.tenantId, opts.projectId, GLOBAL_KINDS);
63
+ const skillFileCtx = globalFiles.map(sf => `### ${sf.kind}\n${sf.content}`).join('\n\n');
64
+ // Build journey description for the LLM
65
+ const ELEMENT_LIMIT = parseInt(process.env.FLOW_ELEMENT_LIMIT ?? '8', 10);
66
+ const journeyDesc = screenContexts
67
+ .map((sc, i) => {
68
+ const nav = sc.viaElement ? ` Navigation: via "${sc.viaElement}"` : '';
69
+ const elList = sc.elements.slice(0, ELEMENT_LIMIT).map(e => ` - ${e.role}: "${e.meaning}"${e.expectedData ? ` (${e.expectedData})` : ''}`).join('\n');
70
+ return `Screen ${i + 1}: ${sc.screenName}${sc.url ? ` (${sc.url})` : ''}\n${nav}\n Elements:\n${elList || ' (no elements extracted)'}`;
71
+ })
72
+ .join('\n\n');
73
+ const SYSTEM_DEFAULT = `You are an expert QA engineer generating cross-screen E2E journey test cases.
74
+ You generate tests for user journeys that span multiple screens.
75
+
76
+ GROUNDING RULES:
77
+ - Only reference elements that exist in the listed screens
78
+ - Each step must name the screen it's on: "On [ScreenName]: ..."
79
+ - Preconditions must state the starting URL or state
80
+ - Expected results must be specific and verifiable
81
+ - Do NOT invent elements, features, or behaviors not listed
82
+
83
+ OUTPUT FORMAT — return valid JSON array only, no markdown:
84
+ [
85
+ {
86
+ "title": "string (concise journey title)",
87
+ "kind": "E2E|REGRESSION|SMOKE|SANITY",
88
+ "priority": "CRITICAL|HIGH|MEDIUM",
89
+ "preconditions": "string",
90
+ "steps": ["step1", "step2", ...],
91
+ "expectedResult": "string",
92
+ "groundedOn": ["ScreenName.ElementMeaning", ...],
93
+ "tags": ["journey", ...]
94
+ }
95
+ ]`;
96
+ const user = `Generate E2E journey test cases for this ${screenContexts.length}-screen user journey:
97
+
98
+ ${journeyDesc}
99
+
100
+ SKILL FILE CONTEXT (business rules, tech stack, auth patterns):
101
+ ${skillFileCtx.slice(0, 3000)}
102
+
103
+ Generate these test kinds for this journey:
104
+ 1. SMOKE (1 TC): Happy path — user completes the full journey successfully
105
+ 2. E2E (2-3 TCs): Full journey variations — valid data, edge cases, required field combos
106
+ 3. REGRESSION (1-2 TCs): Critical regression scenarios that must pass on every release
107
+ 4. SANITY (1 TC): Quick sanity — confirm navigation between screens works at all
108
+
109
+ Rules:
110
+ - Steps must reference ACTUAL screen names and ACTUAL element names from the journey above
111
+ - Each step format: "On [ScreenName]: [action] [element] [with/to value]"
112
+ - Never invent screen names or element names
113
+ - groundedOn: list of "ScreenName.ElementMeaning" pairs that this test exercises`;
114
+ const pathKey = opts.path.map((e) => `${e.fromScreenId}->${e.toScreenId}`).join('|');
115
+ const seed = (0, llm_js_1.deterministicSeed)(`${opts.tenantId}:${opts.projectId}:flow:${opts.startScreenId}:${opts.endScreenId}:${pathKey}`);
116
+ const system = await (0, core_1.getAgentPrompt)('flow-e2e-generation', undefined, opts.tenantId) ?? SYSTEM_DEFAULT;
117
+ let parsed = [];
118
+ const t0 = Date.now();
119
+ try {
120
+ const { text: raw, usage } = await (0, llm_js_1.askTracked)(system, user, llmCfg, undefined, undefined, { temperature: 0, seed });
121
+ const latencyMs = Date.now() - t0;
122
+ (0, app_brain_1.recordLLMCall)({
123
+ tenantId: opts.tenantId,
124
+ projectId: opts.projectId,
125
+ agentType: 'flow',
126
+ label: `flow_journey_${screenContexts.length}screens`,
127
+ entityType: 'flow',
128
+ usage,
129
+ latencyMs,
130
+ }).catch(console.error);
131
+ const jsonMatch = raw.match(/\[[\s\S]*\]/);
132
+ if (jsonMatch) {
133
+ const items = JSON.parse(jsonMatch[0]);
134
+ parsed = items.filter(tc => tc.title && Array.isArray(tc.steps) && tc.steps.length >= 2).map(tc => ({
135
+ title: String(tc.title).slice(0, 200),
136
+ kind: ['E2E', 'REGRESSION', 'SMOKE', 'SANITY'].includes(tc.kind) ? tc.kind : 'E2E',
137
+ priority: ['CRITICAL', 'HIGH', 'MEDIUM'].includes(tc.priority) ? tc.priority : 'HIGH',
138
+ steps: tc.steps.map(s => String(s).slice(0, 400)),
139
+ expectedResult: String(tc.expectedResult || '').slice(0, 500),
140
+ preconditions: String(tc.preconditions || '').slice(0, 300),
141
+ groundedOn: Array.isArray(tc.groundedOn) ? tc.groundedOn.map(String) : [],
142
+ tags: ['journey', `${screenContexts.length}-screen`, ...(Array.isArray(tc.tags) ? tc.tags.map(String) : [])],
143
+ }));
144
+ }
145
+ }
146
+ catch (e) {
147
+ console.error('[flow-agent] LLM parse error:', e?.message);
148
+ }
149
+ if (parsed.length === 0) {
150
+ return { status: 'LLM_EMPTY', cases: [], saved: null, screenCount: screenContexts.length };
151
+ }
152
+ // Save under a "Journey" suite — named after start→end screens
153
+ const startName = screenContexts[0]?.screenName ?? 'Start';
154
+ const endName = screenContexts[screenContexts.length - 1]?.screenName ?? 'End';
155
+ const suiteName = `Journey: ${startName} → ${endName}`;
156
+ const saved = await app_brain_1.AppBrain.saveGeneratedTestCases(opts.tenantId, opts.projectId, suiteName, parsed.map(tc => ({
157
+ ...tc,
158
+ screenId: opts.startScreenId,
159
+ source: 'AI_GENERATED',
160
+ })));
161
+ console.log(`[flow-agent] ${suiteName}: ${parsed.length} journey TCs across ${screenContexts.length} screens`);
162
+ return {
163
+ status: 'GENERATED',
164
+ cases: parsed,
165
+ saved,
166
+ screenCount: screenContexts.length,
167
+ suiteName,
168
+ };
169
+ }
@@ -0,0 +1,18 @@
1
+ import { type LLMConfig } from './llm.js';
2
+ /**
3
+ * Analyzes Git diffs and PR descriptions to map code changes
4
+ * to potentially impacted screens or test suites.
5
+ */
6
+ export declare function analyzeCodeImpact(prTitle: string, prBody: string, diffStats: {
7
+ file: string;
8
+ additions: number;
9
+ deletions: number;
10
+ }[], knownScreens: {
11
+ id: string;
12
+ name: string;
13
+ url: string;
14
+ }[], llmCfg?: LLMConfig): Promise<{
15
+ impactedScreenIds: string[];
16
+ reasoning: string;
17
+ suggestedTestKinds: string[];
18
+ }>;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeCodeImpact = analyzeCodeImpact;
4
+ const core_1 = require("@taodigital/core");
5
+ const llm_js_1 = require("./llm.js");
6
+ /**
7
+ * Analyzes Git diffs and PR descriptions to map code changes
8
+ * to potentially impacted screens or test suites.
9
+ */
10
+ async function analyzeCodeImpact(prTitle, prBody, diffStats, knownScreens, llmCfg) {
11
+ const SYSTEM_DEFAULT = [
12
+ 'You are a Test Impact Analyzer.',
13
+ 'Review the PR title, description, and files changed.',
14
+ 'Map the changes to the provided list of known application screens.',
15
+ 'Return valid JSON: { "impactedScreenIds": string[], "reasoning": string, "suggestedTestKinds": string[] }',
16
+ 'suggestedTestKinds should be an array of: "POSITIVE", "NEGATIVE", "EDGE", "SECURITY", "ACCESSIBILITY", "INTEGRATION", "API"'
17
+ ].join('\n');
18
+ const system = await (0, core_1.getAgentPrompt)('github-impact-analysis') ?? SYSTEM_DEFAULT;
19
+ const user = [
20
+ `PR: ${prTitle}`,
21
+ `Body:\n${prBody.slice(0, 1000)}`,
22
+ '',
23
+ 'Files Changed:',
24
+ ...diffStats.slice(0, 50).map(f => `- ${f.file} (+${f.additions} -${f.deletions})`),
25
+ '',
26
+ 'Known Screens:',
27
+ ...knownScreens.map(s => `[ID: ${s.id}] ${s.name} (${s.url})`),
28
+ ].join('\n');
29
+ try {
30
+ const seed = (0, llm_js_1.deterministicSeed)(`code-impact:${prTitle.slice(0, 100)}:${diffStats.slice(0, 5).map(f => f.file).join('|')}`);
31
+ const { text: raw } = await (0, llm_js_1.askTracked)(system, user, llmCfg, undefined, undefined, { temperature: 0, seed });
32
+ const cleaned = raw.replace(/^```[\w-]*\n?/m, '').replace(/\n?```$/m, '').trim();
33
+ return JSON.parse(cleaned);
34
+ }
35
+ catch (err) {
36
+ console.error('[github-agent] Failed to analyze code impact', err);
37
+ return { impactedScreenIds: [], reasoning: 'Analysis failed', suggestedTestKinds: [] };
38
+ }
39
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * GitHub write operations — branch, commit, PR creation.
3
+ * Uses GitHub REST API v3 (no SDK dependency — just fetch).
4
+ * Works with PAT, fine-grained PAT, and GitHub App installation tokens.
5
+ */
6
+ export declare function getRefSha(token: string, repo: string, ref: string): Promise<string>;
7
+ export declare function createBranch(token: string, repo: string, branchName: string, fromSha: string): Promise<void>;
8
+ export declare function branchExists(token: string, repo: string, branchName: string): Promise<boolean>;
9
+ /**
10
+ * Fetch the current content of a file from GitHub (from the default branch).
11
+ * Returns the decoded text content, or null if the file doesn't exist.
12
+ */
13
+ export declare function getFileContent(token: string, repo: string, path: string, ref?: string): Promise<string | null>;
14
+ /**
15
+ * Merge new spec content into existing spec content.
16
+ * Strategy: append new test() / it() / @Test blocks inside the outermost describe/class wrapper.
17
+ * If no wrapper found, append at end.
18
+ * Returns merged content.
19
+ */
20
+ export declare function mergeSpecContent(existing: string, incoming: string): string;
21
+ export interface FileToCommit {
22
+ path: string;
23
+ content: string;
24
+ }
25
+ export declare function createCommitWithFiles(token: string, repo: string, baseSha: string, files: FileToCommit[], message: string): Promise<{
26
+ commitSha: string;
27
+ treeSha: string;
28
+ }>;
29
+ export declare function updateBranchRef(token: string, repo: string, branchName: string, commitSha: string): Promise<void>;
30
+ export interface CreatePROptions {
31
+ token: string;
32
+ repo: string;
33
+ title: string;
34
+ body: string;
35
+ head: string;
36
+ base: string;
37
+ draft?: boolean;
38
+ }
39
+ export interface PRResult {
40
+ prNumber: number;
41
+ prUrl: string;
42
+ headSha: string;
43
+ branch: string;
44
+ }
45
+ export declare function createPullRequest(opts: CreatePROptions): Promise<PRResult>;
46
+ export interface PushScriptsPROptions {
47
+ token: string;
48
+ repo: string;
49
+ baseBranch: string;
50
+ branchName: string;
51
+ files: FileToCommit[];
52
+ prTitle: string;
53
+ prBody: string;
54
+ draft?: boolean;
55
+ }
56
+ export declare function pushScriptsAsPR(opts: PushScriptsPROptions): Promise<PRResult>;
57
+ /**
58
+ * Generate a short-lived JWT for GitHub App authentication.
59
+ * APP_ID and privateKeyPem come from env / DB. JWT expires in 10 min.
60
+ */
61
+ export declare function buildGitHubAppJWT(appId: string, privateKeyPem: string): string;
62
+ /**
63
+ * Exchange a GitHub App JWT for an installation access token (1-hour expiry).
64
+ */
65
+ export declare function getGitHubAppInstallationToken(appId: string, privateKeyPem: string, installationId: number): Promise<string>;
66
+ export declare function addPRComment(token: string, repo: string, prNumber: number, body: string): Promise<void>;
67
+ export declare function verifyGitHubWebhookSignature(rawBody: string | Buffer, signatureHeader: string, secret: string): boolean;