@taodigital/agents 0.0.0-stage → 1.2.0

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,249 @@
1
+ "use strict";
2
+ /**
3
+ * GitHub write operations — branch, commit, PR creation.
4
+ * Uses GitHub REST API v3 (no SDK dependency — just fetch).
5
+ * Works with PAT, fine-grained PAT, and GitHub App installation tokens.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.getRefSha = getRefSha;
9
+ exports.createBranch = createBranch;
10
+ exports.branchExists = branchExists;
11
+ exports.getFileContent = getFileContent;
12
+ exports.mergeSpecContent = mergeSpecContent;
13
+ exports.createCommitWithFiles = createCommitWithFiles;
14
+ exports.updateBranchRef = updateBranchRef;
15
+ exports.createPullRequest = createPullRequest;
16
+ exports.pushScriptsAsPR = pushScriptsAsPR;
17
+ exports.buildGitHubAppJWT = buildGitHubAppJWT;
18
+ exports.getGitHubAppInstallationToken = getGitHubAppInstallationToken;
19
+ exports.addPRComment = addPRComment;
20
+ exports.verifyGitHubWebhookSignature = verifyGitHubWebhookSignature;
21
+ const GITHUB_API_BASE = process.env.GITHUB_API_BASE_URL ?? 'https://api.github.com';
22
+ function githubHeaders(token) {
23
+ return {
24
+ Authorization: `Bearer ${token}`,
25
+ Accept: 'application/vnd.github+json',
26
+ 'X-GitHub-Api-Version': '2022-11-28',
27
+ 'Content-Type': 'application/json',
28
+ };
29
+ }
30
+ async function ghRequest(token, method, path, body) {
31
+ const res = await fetch(`${GITHUB_API_BASE}${path}`, {
32
+ method,
33
+ headers: githubHeaders(token),
34
+ body: body !== undefined ? JSON.stringify(body) : undefined,
35
+ });
36
+ if (!res.ok) {
37
+ const txt = await res.text().catch(() => '');
38
+ throw new Error(`GitHub API ${method} ${path} → ${res.status}: ${txt.slice(0, 400)}`);
39
+ }
40
+ if (res.status === 204)
41
+ return undefined;
42
+ return res.json();
43
+ }
44
+ // ── Ref / branch ────────────────────────────────────────────────────────────
45
+ async function getRefSha(token, repo, ref) {
46
+ const data = await ghRequest(token, 'GET', `/repos/${repo}/git/ref/heads/${ref}`);
47
+ return data.object.sha;
48
+ }
49
+ async function createBranch(token, repo, branchName, fromSha) {
50
+ await ghRequest(token, 'POST', `/repos/${repo}/git/refs`, {
51
+ ref: `refs/heads/${branchName}`,
52
+ sha: fromSha,
53
+ });
54
+ }
55
+ async function branchExists(token, repo, branchName) {
56
+ try {
57
+ await getRefSha(token, repo, branchName);
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ /**
65
+ * Fetch the current content of a file from GitHub (from the default branch).
66
+ * Returns the decoded text content, or null if the file doesn't exist.
67
+ */
68
+ async function getFileContent(token, repo, path, ref) {
69
+ try {
70
+ const query = ref ? `?ref=${encodeURIComponent(ref)}` : '';
71
+ const data = await ghRequest(token, 'GET', `/repos/${repo}/contents/${path}${query}`);
72
+ if (data.content && data.encoding === 'base64') {
73
+ return Buffer.from(data.content.replace(/\n/g, ''), 'base64').toString('utf-8');
74
+ }
75
+ return null;
76
+ }
77
+ catch {
78
+ return null; // file doesn't exist
79
+ }
80
+ }
81
+ /**
82
+ * Merge new spec content into existing spec content.
83
+ * Strategy: append new test() / it() / @Test blocks inside the outermost describe/class wrapper.
84
+ * If no wrapper found, append at end.
85
+ * Returns merged content.
86
+ */
87
+ function mergeSpecContent(existing, incoming) {
88
+ // Extract test() / it() / scenario calls from incoming content
89
+ // Strip imports, describe wrappers — keep only the inner test blocks
90
+ const testBlockRe = /^\s*(test|it|scenario|@Test[^\n]*\n[^\n]*void\s+\w+)\s*[\(\(]/m;
91
+ if (!testBlockRe.test(incoming)) {
92
+ // Can't parse incoming — skip merge, use incoming as-is
93
+ return incoming;
94
+ }
95
+ // Find the closing brace of the outermost describe/class block in existing content
96
+ // Strategy: find the LAST }) or }); or } that closes the file-level block
97
+ const closingPatterns = [
98
+ /\}\s*\)\s*;?\s*$/, // JS: }); at end
99
+ /\}\s*$/, // Java/Python class closing brace at end
100
+ ];
101
+ // Extract raw test blocks from incoming (lines that are test/it calls + their bodies)
102
+ // Simple approach: strip describe wrapper if present
103
+ let innerContent = incoming;
104
+ const describeWrapRe = /^.*?(?:describe|test\.describe)\s*\([^)]*\)\s*,?\s*\(\s*\)\s*=>\s*\{([\s\S]*)\}\s*\)\s*;?\s*$/;
105
+ const describeMatch = innerContent.match(describeWrapRe);
106
+ if (describeMatch) {
107
+ innerContent = describeMatch[1];
108
+ }
109
+ // For Java: strip class declaration wrapper
110
+ const javaClassRe = /^[\s\S]*?class\s+\w+[^{]*\{([\s\S]*)\}\s*$/;
111
+ const javaMatch = innerContent.match(javaClassRe);
112
+ if (javaMatch) {
113
+ innerContent = javaMatch[1];
114
+ }
115
+ // Trim inner content
116
+ innerContent = innerContent.trim();
117
+ // Insert before the last closing } or }); of existing content
118
+ for (const re of closingPatterns) {
119
+ const m = existing.match(re);
120
+ if (m) {
121
+ const insertPoint = existing.lastIndexOf(m[0]);
122
+ if (insertPoint !== -1) {
123
+ return (existing.slice(0, insertPoint) +
124
+ '\n\n // --- ZeTa: appended ---\n' +
125
+ innerContent.split('\n').map(l => ' ' + l).join('\n') +
126
+ '\n\n' +
127
+ existing.slice(insertPoint));
128
+ }
129
+ }
130
+ }
131
+ // Fallback: append at end
132
+ return existing + '\n\n// --- ZeTa: appended ---\n' + innerContent;
133
+ }
134
+ async function createCommitWithFiles(token, repo, baseSha, files, message) {
135
+ // Build tree entries — use base64 for content to handle any encoding
136
+ const treeEntries = files.map((f) => ({
137
+ path: f.path,
138
+ mode: '100644',
139
+ type: 'blob',
140
+ content: f.content,
141
+ }));
142
+ const tree = await ghRequest(token, 'POST', `/repos/${repo}/git/trees`, {
143
+ base_tree: baseSha,
144
+ tree: treeEntries,
145
+ });
146
+ const commit = await ghRequest(token, 'POST', `/repos/${repo}/git/commits`, {
147
+ message,
148
+ tree: tree.sha,
149
+ parents: [baseSha],
150
+ });
151
+ return { commitSha: commit.sha, treeSha: tree.sha };
152
+ }
153
+ async function updateBranchRef(token, repo, branchName, commitSha) {
154
+ await ghRequest(token, 'PATCH', `/repos/${repo}/git/refs/heads/${branchName}`, {
155
+ sha: commitSha,
156
+ force: false,
157
+ });
158
+ }
159
+ async function createPullRequest(opts) {
160
+ const pr = await ghRequest(opts.token, 'POST', `/repos/${opts.repo}/pulls`, {
161
+ title: opts.title,
162
+ body: opts.body,
163
+ head: opts.head,
164
+ base: opts.base,
165
+ draft: opts.draft ?? false,
166
+ });
167
+ return {
168
+ prNumber: pr.number,
169
+ prUrl: pr.html_url,
170
+ headSha: pr.head.sha,
171
+ branch: opts.head,
172
+ };
173
+ }
174
+ async function pushScriptsAsPR(opts) {
175
+ const baseSha = await getRefSha(opts.token, opts.repo, opts.baseBranch);
176
+ // If branch already exists, use it; otherwise create
177
+ const exists = await branchExists(opts.token, opts.repo, opts.branchName);
178
+ const { commitSha } = await createCommitWithFiles(opts.token, opts.repo, baseSha, opts.files, opts.prTitle);
179
+ if (exists) {
180
+ await updateBranchRef(opts.token, opts.repo, opts.branchName, commitSha);
181
+ }
182
+ else {
183
+ // Create branch pointing at new commit
184
+ await ghRequest(opts.token, 'POST', `/repos/${opts.repo}/git/refs`, {
185
+ ref: `refs/heads/${opts.branchName}`,
186
+ sha: commitSha,
187
+ });
188
+ }
189
+ return createPullRequest({
190
+ token: opts.token,
191
+ repo: opts.repo,
192
+ title: opts.prTitle,
193
+ body: opts.prBody,
194
+ head: opts.branchName,
195
+ base: opts.baseBranch,
196
+ draft: opts.draft,
197
+ });
198
+ }
199
+ // ── GitHub App JWT token generation ─────────────────────────────────────────
200
+ /**
201
+ * Generate a short-lived JWT for GitHub App authentication.
202
+ * APP_ID and privateKeyPem come from env / DB. JWT expires in 10 min.
203
+ */
204
+ function buildGitHubAppJWT(appId, privateKeyPem) {
205
+ const { createSign } = require('node:crypto');
206
+ const now = Math.floor(Date.now() / 1000);
207
+ const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
208
+ const payload = Buffer.from(JSON.stringify({ iat: now - 60, exp: now + 540, iss: appId })).toString('base64url');
209
+ const data = `${header}.${payload}`;
210
+ const sign = createSign('RSA-SHA256');
211
+ sign.update(data);
212
+ const sig = sign.sign(privateKeyPem, 'base64url');
213
+ return `${data}.${sig}`;
214
+ }
215
+ /**
216
+ * Exchange a GitHub App JWT for an installation access token (1-hour expiry).
217
+ */
218
+ async function getGitHubAppInstallationToken(appId, privateKeyPem, installationId) {
219
+ const jwt = buildGitHubAppJWT(appId, privateKeyPem);
220
+ const res = await fetch(`${GITHUB_API_BASE}/app/installations/${installationId}/access_tokens`, {
221
+ method: 'POST',
222
+ headers: {
223
+ Authorization: `Bearer ${jwt}`,
224
+ Accept: 'application/vnd.github+json',
225
+ 'X-GitHub-Api-Version': '2022-11-28',
226
+ },
227
+ });
228
+ if (!res.ok)
229
+ throw new Error(`GitHub App token exchange failed: ${res.status} ${await res.text()}`);
230
+ const data = await res.json();
231
+ return data.token;
232
+ }
233
+ // ── PR comment ───────────────────────────────────────────────────────────────
234
+ async function addPRComment(token, repo, prNumber, body) {
235
+ await ghRequest(token, 'POST', `/repos/${repo}/issues/${prNumber}/comments`, { body });
236
+ }
237
+ // ── Verify GitHub webhook signature ─────────────────────────────────────────
238
+ function verifyGitHubWebhookSignature(rawBody, signatureHeader, secret) {
239
+ const { createHmac, timingSafeEqual } = require('node:crypto');
240
+ const hmac = createHmac('sha256', secret);
241
+ hmac.update(rawBody);
242
+ const expected = `sha256=${hmac.digest('hex')}`;
243
+ try {
244
+ return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
245
+ }
246
+ catch {
247
+ return false;
248
+ }
249
+ }
@@ -0,0 +1,18 @@
1
+ interface ScreenInput {
2
+ id: string;
3
+ name: string;
4
+ url?: string | null;
5
+ elementCount?: number;
6
+ elements?: any[];
7
+ }
8
+ /**
9
+ * Post-crawl: groups screens into semantic areas and persists to DB.
10
+ *
11
+ * Incremental strategy (stable across recrawls):
12
+ * - If groups already exist → only assign NEW unassigned screens using URL-pattern
13
+ * matching. Existing assignments are preserved unchanged. Zero LLM cost.
14
+ * - If no groups exist yet → run full AI grouping (first crawl only).
15
+ * - Pass force=true to wipe and re-run AI grouping regardless (explicit user action).
16
+ */
17
+ export declare function groupScreensWithAI(tenantId: string, projectId: string, screens: ScreenInput[], _jobId?: string, _jobStartedAt?: Date, force?: boolean): Promise<void>;
18
+ export {};
@@ -0,0 +1,338 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.groupScreensWithAI = groupScreensWithAI;
4
+ const app_brain_1 = require("@taodigital/app-brain");
5
+ const llm_config_js_1 = require("./llm-config.js");
6
+ const llm_js_1 = require("./llm.js");
7
+ const core_1 = require("@taodigital/core");
8
+ const GROUP_COLORS = [
9
+ '#6FD0E8', // cyan
10
+ '#4ADE80', // green
11
+ '#FBBF24', // amber
12
+ '#F87171', // coral
13
+ '#A78BFA', // violet
14
+ '#FB923C', // orange
15
+ '#38BDF8', // sky
16
+ '#34D399', // emerald
17
+ ];
18
+ // URL pattern → candidate group slugs (first match wins, order matters).
19
+ // Covers ecommerce, banking, fintech, healthcare, pharma, agriculture, saas,
20
+ // legacy-enterprise, and generic SaaS patterns. Aligns with GICS/NAICS/ICB verticals.
21
+ const URL_GROUP_RULES = [
22
+ // ── Auth (universal) ────────────────────────────────────────────────────
23
+ { pattern: /\/(login|register|sign-in|signup|sign-up|auth|forgot-password|reset-password|set-password|verify-email|two-factor|2fa|mfa|otp|sso|oauth)/i, slugs: ['authentication', 'auth', 'sign-in', 'login', 'access'] },
24
+ // ── Home / Landing ───────────────────────────────────────────────────────
25
+ { pattern: /^\/?(home\/?)?$/i, slugs: ['home-navigation', 'home', 'landing', 'dashboard'] },
26
+ // ── E-commerce: cart / checkout / orders ────────────────────────────────
27
+ { pattern: /\/(cart|basket|checkout|payment|order-confirm|order-received|thank-you|place-order|order-success)/i, slugs: ['cart-checkout', 'cart', 'checkout', 'orders'] },
28
+ { pattern: /\/(orders|my-orders|order-history|order-tracking|returns|return-request|refund-request|wishlist|saved-items)/i, slugs: ['user-account', 'orders', 'account'] },
29
+ { pattern: /\/(product\/|\/p\/|\/item\/|\/sku\/|\/dp\/)/i, slugs: ['product-details', 'products', 'product-catalog'] },
30
+ { pattern: /\/(shop|store|products|catalog|collection|category|product-category|browse|listing)/i, slugs: ['product-catalog', 'catalog', 'shop', 'products'] },
31
+ // ── Banking / Fintech: accounts, transactions, cards ────────────────────
32
+ { pattern: /\/(accounts|account-summary|my-account|account-detail|savings|current-account|fixed-deposit)/i, slugs: ['accounts', 'user-account', 'account', 'dashboard'] },
33
+ { pattern: /\/(transactions|transaction-history|ledger|statement|e-statement|passbook)/i, slugs: ['transactions', 'accounts', 'statements'] },
34
+ { pattern: /\/(transfer|fund-transfer|neft|rtgs|imps|wire|send-money|pay-now|beneficiar)/i, slugs: ['transfers', 'payments', 'accounts'] },
35
+ { pattern: /\/(cards|debit-card|credit-card|card-management|card-block|card-limit)/i, slugs: ['cards', 'accounts', 'payments'] },
36
+ { pattern: /\/(kyc|know-your-customer|aml|cdd|onboarding-kyc|identity-verification)/i, slugs: ['kyc-onboarding', 'onboarding', 'compliance'] },
37
+ { pattern: /\/(wallet|e-wallet|prepaid|balance|topup|load-money)/i, slugs: ['wallet', 'payments', 'accounts'] },
38
+ { pattern: /\/(loans|loan-application|emi|repayment|credit|lending|borrowing)/i, slugs: ['loans-credit', 'loans', 'accounts'] },
39
+ { pattern: /\/(invest|investment|portfolio|mutual-fund|stocks|trading|demat|brokerage)/i, slugs: ['investments', 'portfolio', 'accounts'] },
40
+ { pattern: /\/(dispute|chargeback|complaint|grievance)/i, slugs: ['disputes', 'support', 'compliance'] },
41
+ // ── Healthcare ───────────────────────────────────────────────────────────
42
+ { pattern: /\/(patient|patients|patient-lookup|patient-search|patient-profile)/i, slugs: ['patient-management', 'patients', 'records'] },
43
+ { pattern: /\/(appointment|appointments|schedule|booking|book-appointment|visit|consultation)/i, slugs: ['appointments', 'scheduling', 'patient-management'] },
44
+ { pattern: /\/(claims|claim|claim-submission|prior-auth|pre-authorization|insurance-claim)/i, slugs: ['claims', 'billing', 'compliance'] },
45
+ { pattern: /\/(prescription|prescriptions|rx|medication|drug-order|e-prescription)/i, slugs: ['prescriptions', 'clinical', 'patient-management'] },
46
+ { pattern: /\/(health-record|medical-record|ehr|emr|lab-result|lab-report|pathology|radiology|imaging)/i, slugs: ['health-records', 'records', 'clinical'] },
47
+ { pattern: /\/(provider|doctor|physician|specialist|nurse|care-team|referral)/i, slugs: ['providers', 'care-team', 'patient-management'] },
48
+ { pattern: /\/(consent|hipaa|phi|authorization|release-of-information)/i, slugs: ['consent-compliance', 'compliance', 'policies-legal'] },
49
+ { pattern: /\/(telemedicine|telehealth|video-consult|virtual-visit)/i, slugs: ['telemedicine', 'appointments', 'clinical'] },
50
+ // ── Pharma ───────────────────────────────────────────────────────────────
51
+ { pattern: /\/(adverse-event|pharmacovigilance|safety-report|adr|side-effect|signal)/i, slugs: ['pharmacovigilance', 'safety', 'compliance'] },
52
+ { pattern: /\/(regulatory|submission|dossier|ectd|label|labeling|spc|pil)/i, slugs: ['regulatory-affairs', 'regulatory', 'compliance'] },
53
+ { pattern: /\/(batch|lot|manufacturing|gmp|quality-control|deviation|capa|audit-trail)/i, slugs: ['manufacturing-quality', 'batch', 'quality'] },
54
+ { pattern: /\/(clinical-trial|trial|protocol|investigator|site-management|ctms)/i, slugs: ['clinical-trials', 'research', 'clinical'] },
55
+ // ── Agriculture ──────────────────────────────────────────────────────────
56
+ { pattern: /\/(crop|field|farm|plot|season|sowing|harvest|yield|pest|disease)/i, slugs: ['farm-management', 'crops', 'farming'] },
57
+ { pattern: /\/(sensor|iot|weather|soil|irrigation|telemetry|device|alert)/i, slugs: ['sensors-iot', 'monitoring', 'farm-management'] },
58
+ { pattern: /\/(agri-market|mandi|commodity|price|trade|agriculture-marketplace)/i, slugs: ['agricultural-marketplace', 'marketplace', 'trading'] },
59
+ { pattern: /\/(subsidy|scheme|government-scheme|kisan|agri-loan|input)/i, slugs: ['subsidies-schemes', 'government', 'support'] },
60
+ { pattern: /\/(livestock|poultry|dairy|animal-health|veterinary)/i, slugs: ['livestock', 'farm-management', 'farming'] },
61
+ // ── SaaS / Enterprise ────────────────────────────────────────────────────
62
+ { pattern: /\/(workspace|organization|org|tenant|instance|environment)/i, slugs: ['workspace', 'settings', 'admin'] },
63
+ { pattern: /\/(billing|subscription|plan|pricing|invoices|payment-method|upgrade)/i, slugs: ['billing', 'subscription', 'settings'] },
64
+ { pattern: /\/(integrations|integration|connect|webhooks|api-keys|tokens|oauth-apps)/i, slugs: ['integrations', 'settings', 'admin'] },
65
+ { pattern: /\/(team|members|users|roles|permissions|access-control|rbac|groups)/i, slugs: ['team-management', 'users', 'admin'] },
66
+ { pattern: /\/(analytics|metrics|usage|reports|insights|dashboards)/i, slugs: ['analytics-reports', 'analytics', 'dashboard'] },
67
+ { pattern: /\/(audit-log|activity|history|changelog)/i, slugs: ['audit-logs', 'admin', 'compliance'] },
68
+ // ── Legacy enterprise ────────────────────────────────────────────────────
69
+ { pattern: /\/(erp|crm|hris|hrms|payroll|procurement|inventory-mgmt|supply-chain|wms)/i, slugs: ['enterprise-modules', 'admin', 'dashboard'] },
70
+ { pattern: /\/(report|reports|export|download|print|batch-report)/i, slugs: ['reports', 'analytics-reports', 'dashboard'] },
71
+ // ── Generic / cross-vertical ─────────────────────────────────────────────
72
+ { pattern: /\/(profile|my-profile|account-settings|user-settings|preferences|notifications-settings)/i, slugs: ['user-account', 'account', 'profile', 'settings'] },
73
+ { pattern: /\/(terms|privacy|refund|return|shipping-policy|legal|cookie|disclaimer|policies|gdpr|ccpa)/i, slugs: ['policies-legal', 'legal', 'policies', 'compliance'] },
74
+ { pattern: /\/(about|our-story|mission|team|careers|blog|news|press|media)/i, slugs: ['company-info', 'about', 'company'] },
75
+ { pattern: /\/(contact|faq|help|support|customer-service|helpdesk|ticket|raise-ticket)/i, slugs: ['faqs-support', 'support', 'faq', 'help'] },
76
+ { pattern: /\/(search|find|explore|discover)/i, slugs: ['search-filter', 'search', 'explore'] },
77
+ { pattern: /\/(admin|cms|manage|management|back-office|backoffice|ops)/i, slugs: ['admin', 'back-office', 'management'] },
78
+ { pattern: /\/(notification|notifications|inbox|alerts|messages)/i, slugs: ['notifications', 'user-account', 'inbox'] },
79
+ { pattern: /\/(onboarding|getting-started|setup|wizard|tour)/i, slugs: ['onboarding', 'kyc-onboarding', 'settings'] },
80
+ ];
81
+ /**
82
+ * Assign a single screen to the best-fit existing group by URL-pattern matching.
83
+ * Falls back to the largest group if no pattern matches.
84
+ * Returns null if no groups exist at all.
85
+ */
86
+ function pickBestGroup(url, existingGroups) {
87
+ if (existingGroups.length === 0)
88
+ return null;
89
+ const path = (() => { try {
90
+ return new URL(url ?? '').pathname;
91
+ }
92
+ catch {
93
+ return url ?? '';
94
+ } })();
95
+ for (const rule of URL_GROUP_RULES) {
96
+ if (rule.pattern.test(path)) {
97
+ for (const candidateSlug of rule.slugs) {
98
+ const match = existingGroups.find(g => g.slug === candidateSlug || g.slug.includes(candidateSlug) || candidateSlug.includes(g.slug));
99
+ if (match)
100
+ return match.id;
101
+ }
102
+ }
103
+ }
104
+ // No URL pattern matched — assign to the group with the most screens (catch-all)
105
+ return existingGroups.reduce((best, g) => g.screens.length > best.screens.length ? g : best, existingGroups[0]).id;
106
+ }
107
+ const SYSTEM = `You are an expert UX architect who groups app screens into logical feature areas.
108
+ Given a list of screens (name + URL + element hints), produce 3-8 semantic groups that represent distinct product domains or user journeys.
109
+ Detect the app's industry vertical from the URLs and screen names, then apply the matching classification rules below.
110
+
111
+ Rules:
112
+ - Every screen must be assigned to exactly one group
113
+ - Group names are short (2-3 words max)
114
+ - slug: lowercase-hyphenated version of name
115
+ - icon: single emoji that represents the group
116
+
117
+ ═══ UNIVERSAL (all verticals) ════════════════════════════════════════════════
118
+ - /login /register /sign-in /signup /auth/* /forgot-password /reset-password /2fa /mfa /sso → "Authentication" 🔐
119
+ - / /home (root path) → "Home & Navigation" 🏠
120
+ - /search* /find* /explore* → "Search & Filter" 🔍
121
+ - /admin* /cms* /manage* /back-office* → "Admin" ⚙️
122
+ - /terms* /privacy* /legal* /cookie* /gdpr* /policies* → "Policies & Legal" 📋
123
+ - /about* /team* /careers* /blog* /news* → "Company & Info" ℹ️
124
+ - /contact* /faq* /help* /support* /ticket* → "FAQs & Support" ❓
125
+ - /profile* /account-settings* /preferences* /notifications-settings* → "User Account" 👤
126
+ - /notifications* /inbox* /alerts* → "Notifications" 🔔
127
+ - /onboarding* /getting-started* /setup* /wizard* → "Onboarding" 🚀
128
+ - /audit-log* /activity* /changelog* → "Audit Logs" 📜
129
+
130
+ ═══ E-COMMERCE ════════════════════════════════════════════════════════════════
131
+ - /cart* /basket* /checkout* /payment* /order-confirm* /thank-you* → "Cart & Checkout" 🛒
132
+ - /orders* /my-orders* /order-history* /returns* /refund-request* /wishlist* → "Orders & Returns" 📦
133
+ - /product/* /p/* /item/* /sku/* /dp/* → "Product Details" 🛍️
134
+ - /shop* /store* /catalog* /collection* /category* /product-category* /listing* → "Product Catalog" 📦
135
+
136
+ ═══ BANKING / FINTECH ═════════════════════════════════════════════════════════
137
+ - /accounts* /account-summary* /savings* /current-account* /fixed-deposit* → "Accounts" 🏦
138
+ - /transactions* /transaction-history* /ledger* /statement* /passbook* → "Transactions" 💳
139
+ - /transfer* /fund-transfer* /neft* /rtgs* /send-money* /beneficiar* → "Transfers & Payments" 💸
140
+ - /cards* /debit-card* /credit-card* /card-management* → "Cards" 💳
141
+ - /kyc* /identity-verification* /onboarding-kyc* /aml* → "KYC & Onboarding" 🪪
142
+ - /wallet* /e-wallet* /prepaid* /topup* → "Wallet" 👛
143
+ - /loans* /loan-application* /emi* /repayment* /credit* → "Loans & Credit" 📊
144
+ - /invest* /portfolio* /mutual-fund* /stocks* /trading* → "Investments" 📈
145
+ - /dispute* /chargeback* /complaint* → "Disputes" ⚠️
146
+
147
+ ═══ HEALTHCARE ═════════════════════════════════════════════════════════════════
148
+ - /patient* /patients/* /patient-lookup* → "Patient Management" 🏥
149
+ - /appointment* /schedule* /booking* /book-appointment* /consultation* → "Appointments" 📅
150
+ - /claims* /claim-submission* /prior-auth* /insurance-claim* → "Claims & Billing" 🧾
151
+ - /prescription* /rx/* /medication* /drug-order* → "Prescriptions" 💊
152
+ - /health-record* /medical-record* /ehr* /emr* /lab-result* /imaging* → "Health Records" 📋
153
+ - /provider* /doctor* /physician* /specialist* /care-team* → "Providers" 👩‍⚕️
154
+ - /consent* /hipaa* /phi* /release-of-information* → "Consent & Compliance" 🔒
155
+ - /telemedicine* /telehealth* /video-consult* /virtual-visit* → "Telemedicine" 📹
156
+
157
+ ═══ PHARMA ═════════════════════════════════════════════════════════════════════
158
+ - /adverse-event* /pharmacovigilance* /safety-report* /adr* → "Pharmacovigilance" ⚕️
159
+ - /regulatory* /submission* /dossier* /ectd* /label* /spc* → "Regulatory Affairs" 📑
160
+ - /batch* /lot* /manufacturing* /gmp* /quality-control* /capa* → "Manufacturing & Quality" 🔬
161
+ - /clinical-trial* /trial* /protocol* /investigator* /ctms* → "Clinical Trials" 🧪
162
+
163
+ ═══ AGRICULTURE ════════════════════════════════════════════════════════════════
164
+ - /crop* /field* /farm* /plot* /harvest* /yield* /pest* → "Farm Management" 🌾
165
+ - /sensor* /iot* /weather* /soil* /irrigation* /telemetry* → "Sensors & IoT" 📡
166
+ - /agri-market* /mandi* /commodity* /agriculture-marketplace* → "Agricultural Marketplace" 🏪
167
+ - /subsidy* /scheme* /government-scheme* /kisan* → "Subsidies & Schemes" 🏛️
168
+ - /livestock* /poultry* /dairy* /animal-health* → "Livestock" 🐄
169
+
170
+ ═══ SAAS / ENTERPRISE ══════════════════════════════════════════════════════════
171
+ - /workspace* /organization* /org* /tenant* → "Workspace" 🏢
172
+ - /billing* /subscription* /plan* /invoices* /upgrade* → "Billing & Plans" 💰
173
+ - /integrations* /integration/* /webhooks* /api-keys* → "Integrations" 🔗
174
+ - /team* /members* /users* /roles* /permissions* /rbac* → "Team Management" 👥
175
+ - /analytics* /metrics* /usage* /reports* /insights* → "Analytics & Reports" 📊
176
+ - /erp* /crm* /hris* /payroll* /procurement* /supply-chain* → "Enterprise Modules" 🗄️
177
+
178
+ Element-hint overrides (when URL pattern is ambiguous):
179
+ - hasCheckoutForm=true → "Cart & Checkout"
180
+ - hasPriceTag=true AND hasAddToCart=true → "Product Details"
181
+ - hasProductGrid=true → "Product Catalog"
182
+ - hasAuthForm=true → "Authentication"
183
+ - hasPatientForm=true → "Patient Management"
184
+ - hasLabResult=true → "Health Records"
185
+
186
+ Critical rules:
187
+ - Legal/policy pages NEVER belong to "User Account" — always "Policies & Legal"
188
+ - The site root ("/") always belongs to "Home & Navigation"
189
+ - Product listing pages → "Product Catalog", NOT "Cart & Checkout"
190
+ - Banking account pages → "Accounts", NOT "User Account"
191
+ - Healthcare patient pages → "Patient Management", NOT "User Account"
192
+ - If a group would have only 1 screen AND a semantically close group exists, merge them
193
+ - NEVER create a group named "Other", "Miscellaneous", "Misc", "General", or "Uncategorized" — assign those screens to the closest matching group
194
+ - Group names must NOT start with "@", "#", or any special character — use plain words only (e.g. "Workspace" not "@Workspace")
195
+ - Return ONLY valid JSON, no markdown fences`;
196
+ /**
197
+ * Post-crawl: groups screens into semantic areas and persists to DB.
198
+ *
199
+ * Incremental strategy (stable across recrawls):
200
+ * - If groups already exist → only assign NEW unassigned screens using URL-pattern
201
+ * matching. Existing assignments are preserved unchanged. Zero LLM cost.
202
+ * - If no groups exist yet → run full AI grouping (first crawl only).
203
+ * - Pass force=true to wipe and re-run AI grouping regardless (explicit user action).
204
+ */
205
+ async function groupScreensWithAI(tenantId, projectId, screens, _jobId, _jobStartedAt, force = false) {
206
+ if (screens.length === 0)
207
+ return;
208
+ const existingGroups = await app_brain_1.AppBrain.getScreenGroups(tenantId, projectId).catch(() => []);
209
+ // ── Incremental path: groups exist and force-regroup not requested ────────────
210
+ if (existingGroups.length > 0 && !force) {
211
+ const assignedIds = new Set(existingGroups.flatMap(g => g.screens.map(s => s.id)));
212
+ const unassigned = screens.filter(s => !assignedIds.has(s.id));
213
+ if (unassigned.length === 0) {
214
+ console.log(`[grouping-agent] all ${screens.length} screens already grouped — skipping`);
215
+ return;
216
+ }
217
+ console.log(`[grouping-agent] incremental: assigning ${unassigned.length} new screen(s) to existing groups`);
218
+ for (const screen of unassigned) {
219
+ const groupId = pickBestGroup(screen.url, existingGroups);
220
+ if (groupId) {
221
+ await app_brain_1.AppBrain.assignScreensToGroup(tenantId, [screen.id], groupId).catch(() => { });
222
+ }
223
+ }
224
+ return;
225
+ }
226
+ // ── Full AI grouping path: first crawl or force=true ─────────────────────────
227
+ const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(tenantId, 'screenNaming');
228
+ if (!llmCfg) {
229
+ console.warn('[grouping-agent] No AI provider configured — skipping screen grouping. Configure one in Workspace → Settings → AI Provider.');
230
+ return;
231
+ }
232
+ function getElementHints(screen) {
233
+ const els = screen.elements ?? [];
234
+ const texts = els.map((e) => (e.text ?? e.label ?? '').toLowerCase());
235
+ const hasAddToCart = texts.some((t) => t.includes('add to cart') || t.includes('add to bag'));
236
+ const hasPriceTag = els.some((e) => e.type === 'price' || /₹|\$|€|£/.test(e.text ?? ''));
237
+ const hasProductGrid = els.filter((e) => e.type === 'product-card' || e.type === 'image').length > 4;
238
+ const hasCheckoutForm = texts.some((t) => t.includes('place order') || t.includes('proceed to checkout') || t.includes('payment'));
239
+ const hasAuthForm = texts.some((t) => t.includes('sign in') || t.includes('log in') || t.includes('create account'));
240
+ const hints = [];
241
+ if (hasAddToCart)
242
+ hints.push('hasAddToCart=true');
243
+ if (hasPriceTag)
244
+ hints.push('hasPriceTag=true');
245
+ if (hasProductGrid)
246
+ hints.push('hasProductGrid=true');
247
+ if (hasCheckoutForm)
248
+ hints.push('hasCheckoutForm=true');
249
+ if (hasAuthForm)
250
+ hints.push('hasAuthForm=true');
251
+ return hints.length ? ` [${hints.join(', ')}]` : '';
252
+ }
253
+ const screenList = screens
254
+ .map((s) => `${s.id}: ${s.name} | ${s.url ?? ''}${getElementHints(s)}`)
255
+ .join('\n');
256
+ const user = `Group these ${screens.length} screens into 3-8 semantic feature areas.
257
+
258
+ ${screenList}
259
+
260
+ Return JSON exactly like this (no other text):
261
+ {
262
+ "groups": [
263
+ {
264
+ "name": "Authentication",
265
+ "slug": "authentication",
266
+ "icon": "🔐",
267
+ "description": "Login, registration, password flows",
268
+ "screenIds": ["clxxx1abc", "clxxx2def"]
269
+ }
270
+ ]
271
+ }`;
272
+ try {
273
+ const seed = (0, llm_js_1.deterministicSeed)(`${tenantId}:group-screens:${screens.map((s) => s.id).sort().join('|').slice(0, 500)}`);
274
+ const effectiveSystem = await (0, core_1.getAgentPrompt)('screen-grouping', undefined, tenantId) ?? SYSTEM;
275
+ const result = await (0, llm_js_1.askTracked)(effectiveSystem, user, llmCfg, 4096, undefined, { temperature: 0, seed });
276
+ const text = result.text.trim();
277
+ const jsonStart = text.indexOf('{');
278
+ const jsonEnd = text.lastIndexOf('}');
279
+ if (jsonStart === -1 || jsonEnd === -1) {
280
+ console.error('[grouping-agent] LLM did not return valid JSON');
281
+ return;
282
+ }
283
+ const parsed = JSON.parse(text.slice(jsonStart, jsonEnd + 1));
284
+ if (!Array.isArray(parsed.groups))
285
+ return;
286
+ // Normalize group names: strip leading special chars (@, #, etc.) that the LLM
287
+ // sometimes adds, and drop "Other" catch-all groups by merging into the first group.
288
+ const normalizeGroupName = (name) => name.replace(/^[@#!]+/, '').trim();
289
+ const OTHER_NAMES = new Set(['other', 'miscellaneous', 'misc', 'general', 'uncategorized', 'unknown']);
290
+ // Merge duplicates: same normalized slug → combine screenIds into first occurrence
291
+ const seenSlugs = new Map();
292
+ const deduped = [];
293
+ for (const g of parsed.groups) {
294
+ g.name = normalizeGroupName(g.name);
295
+ g.slug = g.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
296
+ if (OTHER_NAMES.has(g.slug)) {
297
+ // Re-assign "Other" screens to the first non-Other group (or last deduped group)
298
+ const target = deduped[0];
299
+ if (target) {
300
+ target.screenIds = [...(target.screenIds ?? []), ...(g.screenIds ?? [])];
301
+ }
302
+ continue;
303
+ }
304
+ const existing = seenSlugs.get(g.slug);
305
+ if (existing) {
306
+ existing.screenIds = [...(existing.screenIds ?? []), ...(g.screenIds ?? [])];
307
+ }
308
+ else {
309
+ seenSlugs.set(g.slug, g);
310
+ deduped.push(g);
311
+ }
312
+ }
313
+ if (deduped.length === 0)
314
+ return;
315
+ // Wipe old groups only right before saving new ones (atomic as possible)
316
+ await app_brain_1.AppBrain.clearScreenGroups(tenantId, projectId);
317
+ for (let i = 0; i < deduped.length; i++) {
318
+ const g = deduped[i];
319
+ const color = GROUP_COLORS[i % GROUP_COLORS.length];
320
+ const group = await app_brain_1.AppBrain.saveScreenGroup(tenantId, projectId, {
321
+ name: g.name,
322
+ slug: g.slug || g.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
323
+ icon: g.icon || '◈',
324
+ color,
325
+ description: g.description,
326
+ order: i,
327
+ crawlJobId: _jobId,
328
+ });
329
+ if (Array.isArray(g.screenIds) && g.screenIds.length > 0) {
330
+ await app_brain_1.AppBrain.assignScreensToGroup(tenantId, g.screenIds, group.id);
331
+ }
332
+ }
333
+ console.log(`[grouping-agent] ${force ? 'force-regrouped' : 'created'} ${parsed.groups.length} groups for project ${projectId}`);
334
+ }
335
+ catch (err) {
336
+ console.error('[grouping-agent] grouping failed:', err);
337
+ }
338
+ }