@cementic/cementic-test 0.2.4 → 0.2.6

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.
@@ -0,0 +1,46 @@
1
+ # Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as contributors and maintainers pledge to make participation in the CementicTest community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for this project include:
12
+
13
+ - demonstrating empathy and kindness toward other people
14
+ - being respectful of differing opinions, viewpoints, and experiences
15
+ - giving and gracefully accepting constructive feedback
16
+ - taking responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ - focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ - the use of sexualized language or imagery, and sexual attention or advances of any kind
22
+ - trolling, insulting or derogatory comments, and personal or political attacks
23
+ - public or private harassment
24
+ - publishing others' private information, such as a physical or email address, without their explicit permission
25
+ - other conduct which could reasonably be considered inappropriate in a professional setting
26
+
27
+ ## Enforcement Responsibilities
28
+
29
+ Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
30
+
31
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
32
+
33
+ ## Scope
34
+
35
+ This Code of Conduct applies within all project spaces and also applies when an individual is officially representing the project in public spaces. Examples of representing the project include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
36
+
37
+ ## Enforcement
38
+
39
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainers through the repository hosting platform or the project contact channels. All complaints will be reviewed and investigated promptly and fairly.
40
+
41
+ All project maintainers are obligated to respect the privacy and security of the reporter of any incident.
42
+
43
+ ## Attribution
44
+
45
+ This Code of Conduct is adapted from the Contributor Covenant, version 2.1:
46
+ https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
@@ -0,0 +1,61 @@
1
+ # Contributing
2
+
3
+ Thanks for contributing to CementicTest CLI.
4
+
5
+ ## What to contribute
6
+
7
+ Useful contributions include:
8
+
9
+ - bug fixes
10
+ - test coverage improvements
11
+ - CLI workflow improvements
12
+ - generator accuracy improvements
13
+ - documentation updates
14
+ - template updates for scaffolded projects
15
+
16
+ If your change affects generated output, command behavior, or project scaffolding, include tests or a clear explanation of why tests are not practical.
17
+
18
+ ## Local setup
19
+
20
+ ```bash
21
+ npm install
22
+ npm run build
23
+ npm test
24
+ ```
25
+
26
+ ## Development workflow
27
+
28
+ 1. Create a branch for your change.
29
+ 2. Make the smallest coherent change that solves the problem.
30
+ 3. Run `npm test`.
31
+ 4. If your change affects generation, also verify a manual `tc -> normalize -> gen -> test` flow.
32
+ 5. Update docs when user-facing behavior changes.
33
+
34
+ ## Manual smoke flow
35
+
36
+ ```bash
37
+ mkdir demo
38
+ cd demo
39
+
40
+ ct new sample --lang ts --no-browsers
41
+ cd sample
42
+
43
+ ct tc url https://mini-bank.testamplify.com/login --ai --feature "Login" --count 1
44
+ ct normalize ./cases --and-gen --lang ts
45
+ ct test
46
+ ```
47
+
48
+ ## Pull requests
49
+
50
+ Please make sure your pull request includes:
51
+
52
+ - a short summary of the behavior change
53
+ - any relevant tests
54
+ - documentation updates when needed
55
+ - notes about manual verification if generation behavior changed
56
+
57
+ ## Issues and discussion
58
+
59
+ - Bugs and feature requests: use the GitHub issue tracker
60
+ - Community and product updates: https://t.me/+Wbx7oK7ivqgxZGJh
61
+ - Website and upcoming web app: https://cementic.testamplify.io/
package/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  CementicTest CLI (`ct`) turns plain-English test cases into runnable Playwright tests with a Page Object Model-oriented workflow.
4
4
 
5
+ ## Project links
6
+
7
+ - Website: https://cementic.testamplify.io/
8
+ - Community: https://t.me/+Wbx7oK7ivqgxZGJh
9
+ - Web app: browser-based AI testing workflow coming soon
10
+
5
11
  Current pipeline:
6
12
 
7
13
  ```text
@@ -10,11 +16,17 @@ ct tc -> ct normalize -> ct gen -> ct test
10
16
 
11
17
  It can scaffold a project, write or AI-generate case files, normalize them into JSON, generate Playwright specs plus page objects, and run the resulting suite.
12
18
 
19
+ ## Project status
20
+
21
+ This repository currently ships the CLI workflow.
22
+
23
+ The browser-based web app is in progress and early product updates will be shared on the website and in the community channel.
24
+
13
25
  ## What this version does
14
26
 
15
27
  - Scaffolds new Playwright projects in JavaScript or TypeScript
16
28
  - Generates Markdown test cases manually or with AI
17
- - Supports URL-aware case generation with live page scraping
29
+ - Supports URL-aware case generation with live page capture
18
30
  - Normalizes case files into structured JSON
19
31
  - Generates Playwright specs and POM classes from normalized cases
20
32
  - Avoids overwriting existing generated page-object files
@@ -124,13 +136,17 @@ Generates cases with awareness of a live page.
124
136
  ```bash
125
137
  ct tc url https://mini-bank.testamplify.com/login --feature "Login" --count 2
126
138
  ct tc url https://mini-bank.testamplify.com/login --ai --feature "Login" --count 2
139
+ ct tc url https://mini-bank.testamplify.com/login --ai --headed --feature "Login"
140
+ ct tc url https://mini-bank.testamplify.com/login --ai --capture-only --feature "Login"
127
141
  ```
128
142
 
129
143
  Current behavior:
130
144
 
131
- - scrapes the page for headings, buttons, links, inputs, and metadata
132
- - passes that context to the AI flow when `--ai` is enabled
133
- - writes `<!-- ct:url ... -->` metadata into generated Markdown so normalization and generation preserve the target URL
145
+ - captures a live page with Playwright and extracts headings, buttons, links, inputs, status regions, and selector candidates
146
+ - sends that captured element map to the capture-aware AI flow when `--ai` is enabled
147
+ - saves the full capture artifact to `.cementic/capture/capture-*.json`
148
+ - saves a runnable preview spec to `tests/preview/spec-preview-*.spec.cjs` when AI scenarios are generated
149
+ - writes `<!-- ct:url ... -->` metadata plus selector and `playwright` hints into generated Markdown so normalization and generation preserve the capture output
134
150
 
135
151
  ### 4. `ct normalize <path>`
136
152
 
@@ -263,34 +279,42 @@ Current workflow behavior:
263
279
 
264
280
  ## AI provider support
265
281
 
266
- Current AI flow lives in `src/core/llm.ts`.
282
+ Current AI flows live in `src/core/llm.ts` and `src/core/analyse.ts`.
267
283
 
268
284
  Provider behavior in this version:
269
285
 
270
- - Anthropic is first-class and preferred when `ANTHROPIC_API_KEY` is set
271
- - OpenAI-compatible mode is used when Anthropic is not configured
286
+ - `ct tc --ai` uses the standard Markdown case writer in `src/core/llm.ts`
287
+ - `ct tc url --ai` uses the capture-aware analysis flow in `src/core/analyse.ts`
272
288
  - manual template generation is used if AI generation fails
273
289
 
274
290
  Supported environment variables:
275
291
 
276
292
  | Variable | Purpose |
277
293
  | --- | --- |
294
+ | `DEEPSEEK_API_KEY` | DeepSeek key for capture-aware analysis |
278
295
  | `ANTHROPIC_API_KEY` | Primary Anthropic key |
279
296
  | `CT_ANTHROPIC_API_KEY` | Alternate Anthropic key |
297
+ | `GEMINI_API_KEY` | Gemini key for capture-aware analysis |
280
298
  | `OPENAI_API_KEY` | OpenAI key |
299
+ | `QWEN_API_KEY` | Qwen key for capture-aware analysis |
300
+ | `KIMI_API_KEY` | Kimi / Moonshot key for capture-aware analysis |
281
301
  | `CT_LLM_API_KEY` | Generic OpenAI-compatible API key |
282
- | `CT_LLM_PROVIDER` | Optional provider override (`anthropic` or `openai`) |
302
+ | `CT_LLM_PROVIDER` | Optional provider override (`deepseek`, `anthropic`, `gemini`, `qwen`, `kimi`, or `openai`) |
283
303
  | `CT_LLM_MODEL` | Model override |
284
304
  | `CT_LLM_BASE_URL` | OpenAI-compatible base URL override |
285
305
 
286
306
  Current defaults:
287
307
 
288
- - Anthropic model default: `claude-sonnet-4-5`
289
- - OpenAI-compatible model default: `gpt-4o-mini`
308
+ - DeepSeek default: `deepseek-chat`
309
+ - Anthropic default: `claude-sonnet-4-5`
310
+ - Gemini default: `gemini-2.5-flash`
311
+ - Qwen default: `qwen-plus`
312
+ - Kimi default: `moonshot-v1-8k`
313
+ - OpenAI-compatible default: `gpt-4o-mini`
290
314
 
291
- ## URL scraping
315
+ ## URL capture
292
316
 
293
- Current scraping behavior lives in `src/core/scrape.ts`.
317
+ Current capture behavior lives in `src/core/capture.ts`.
294
318
 
295
319
  Primary path:
296
320
 
@@ -301,11 +325,11 @@ Primary path:
301
325
  - buttons
302
326
  - links
303
327
  - inputs with label, placeholder, name, type, and `data-testid`
304
- - landmark roles
305
-
306
- Fallback path:
307
-
308
- - falls back to `fetch` + regex parsing when Playwright is unavailable
328
+ - status and alert regions
329
+ - selector confidence and alternative selectors
330
+ - builds a structured `ElementMap`
331
+ - passes that capture into `src/core/analyse.ts` for evidence-backed scenario generation
332
+ - formats markdown, JSON, and preview-spec outputs through `src/core/report.ts`
309
333
 
310
334
  ## Generated project structure
311
335
 
@@ -354,8 +378,9 @@ This version is intentionally heuristic and file-oriented.
354
378
  Important current characteristics:
355
379
 
356
380
  - generated specs are POM-oriented, not raw recorded scripts
357
- - generator rules cover common cases well but are not a replacement for live self-healing
358
- - Playwright itself is used for scraping and test execution
381
+ - capture-generated selector and assertion hints are preserved through `normalize` and `gen`
382
+ - generator rules still cover non-capture cases heuristically, but capture-backed cases now keep exact selectors and assertions
383
+ - Playwright itself is used for capture and test execution
359
384
  - Playwright CLI and Playwright agents are not yet first-class runtime backends in this version
360
385
 
361
386
  ## Developer setup
@@ -382,6 +407,7 @@ What `npm test` currently verifies:
382
407
 
383
408
  - build succeeds
384
409
  - `ct tc url` writes URL metadata
410
+ - capture-generated selector and `playwright` hints survive `normalize` and `gen`
385
411
  - `ct normalize` and `ct gen` preserve the recent generator fixes
386
412
  - the JavaScript scaffold contains the expected page objects and sample specs
387
413
 
@@ -403,7 +429,7 @@ cd demo
403
429
  ct new sample --lang ts --no-browsers
404
430
  cd sample
405
431
 
406
- ct tc url https://mini-bank.testamplify.com/login --feature "Login" --count 1
432
+ ct tc url https://mini-bank.testamplify.com/login --ai --feature "Login" --count 1
407
433
  ct normalize ./cases --and-gen --lang ts
408
434
  ct test
409
435
  ```
@@ -418,13 +444,34 @@ src/commands/normalize.ts
418
444
  src/commands/gen.ts
419
445
  src/commands/test.ts
420
446
  src/commands/flow.ts
447
+ src/core/capture.ts
448
+ src/core/analyse.ts
421
449
  src/core/llm.ts
422
- src/core/scrape.ts
450
+ src/core/report.ts
423
451
  src/core/prefix.ts
424
452
  templates/student-framework/
425
453
  templates/student-framework-ts/
426
454
  ```
427
455
 
456
+ ## Changelog
457
+
458
+ ### v0.2.6
459
+
460
+ - added an install-time banner with links to the website and community
461
+ - surfaced project links and web app status near the top of the README
462
+ - added a standalone `CONTRIBUTING.md`
463
+ - added a `CODE_OF_CONDUCT.md`
464
+
465
+ ### v0.2.5
466
+
467
+ - replaced the legacy URL scraping path with full Playwright-based live page capture
468
+ - integrated the POC capture and AI analysis flow into `ct tc url --ai`
469
+ - added capture artifacts at `.cementic/capture/capture-*.json`
470
+ - added preview spec output at `tests/preview/spec-preview-*.spec.cjs`
471
+ - preserved capture-generated selector hints and exact `playwright` assertions through `normalize` and `gen`
472
+ - added `--headed` and `--capture-only` support for the URL capture flow
473
+ - expanded capture-aware AI provider support to DeepSeek, Anthropic, Gemini, Qwen, Kimi, and OpenAI-compatible endpoints
474
+
428
475
  ## Current limitations
429
476
 
430
477
  - generator assertions and step mapping are heuristic
@@ -434,7 +481,4 @@ templates/student-framework-ts/
434
481
 
435
482
  ## Contributing
436
483
 
437
- 1. Create a branch
438
- 2. Run `npm test`
439
- 3. Verify a manual `tc -> normalize -> gen -> test` flow if your change affects generation
440
- 4. Open a PR with the behavior change clearly described
484
+ Contribution guidelines live in [CONTRIBUTING.md](./CONTRIBUTING.md).
@@ -46,8 +46,14 @@ function visibleTextRegexFromPhrase(value) {
46
46
  if (tokens.length === 0) return ".+";
47
47
  return tokens.map((token) => escapeForRegex(token)).join("\\s+");
48
48
  }
49
- function stepToPlaywright(step, url) {
49
+ function ensureStatement(value) {
50
+ const trimmed = value.trim();
51
+ if (!trimmed) return trimmed;
52
+ return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
53
+ }
54
+ function stepToPlaywright(step, url, hint) {
50
55
  const s = step.trim();
56
+ const hintedSelector = hint?.selector ? `page.${hint.selector}` : void 0;
51
57
  if (/^(navigate|go to|open|visit|load)/i.test(s)) {
52
58
  const urlMatch = s.match(/https?:\/\/[^\s'"]+/) || s.match(/["']([^"']+)["']/);
53
59
  const dest = urlMatch?.[1] ?? urlMatch?.[0] ?? url ?? "/";
@@ -59,6 +65,7 @@ function stepToPlaywright(step, url) {
59
65
  if (/\b(type|enter|fill|input|write)\b/i.test(s)) {
60
66
  const values = s.match(/["']([^"']+)["']/g) ?? [];
61
67
  const value = values[values.length - 1]?.replace(/['"]/g, "") ?? "value";
68
+ if (hintedSelector) return `await ${hintedSelector}.fill('${escapeForSingleQuotedString(value)}');`;
62
69
  const inWithMatch = s.match(/\bin\s+([a-zA-Z][a-zA-Z\s]{1,25})\s+with\b/i);
63
70
  const intoMatch = s.match(/\bin(?:to)?\s+(?:the\s+)?([a-zA-Z][a-zA-Z\s]{1,25})(?:\s+field|\s+input|\s+box|\s+area)?/i);
64
71
  const fieldMatch = s.match(/([a-zA-Z][a-zA-Z\s]{1,25})\s+(?:field|input|box|area)/i);
@@ -66,16 +73,19 @@ function stepToPlaywright(step, url) {
66
73
  return `await page.getByLabel('${escapeForSingleQuotedString(field)}').fill('${escapeForSingleQuotedString(value)}');`;
67
74
  }
68
75
  if (/\bclick\b.*(button|btn|submit|sign in|log in|login|register|continue|next|save|confirm)/i.test(s)) {
76
+ if (hintedSelector) return `await ${hintedSelector}.click();`;
69
77
  const nameMatch = s.match(/["']([^"']+)["']/);
70
78
  const name = nameMatch?.[1] ?? s.replace(/click\s+(the\s+)?/i, "").trim();
71
79
  return `await page.getByRole('button', { name: '${escapeForSingleQuotedString(name)}' }).click();`;
72
80
  }
73
81
  if (/\bclick\b.*(link|anchor|nav)/i.test(s)) {
82
+ if (hintedSelector) return `await ${hintedSelector}.click();`;
74
83
  const nameMatch = s.match(/["']([^"']+)["']/);
75
84
  const name = nameMatch?.[1] ?? s.replace(/click\s+(the\s+)?/i, "").trim();
76
85
  return `await page.getByRole('link', { name: '${escapeForSingleQuotedString(name)}' }).click();`;
77
86
  }
78
87
  if (/^(click|press|tap)\b/i.test(s)) {
88
+ if (hintedSelector) return `await ${hintedSelector}.click();`;
79
89
  const nameMatch = s.match(/["']([^"']+)["']/);
80
90
  if (nameMatch) return `await page.getByText('${escapeForSingleQuotedString(nameMatch[1])}').click();`;
81
91
  const target = s.replace(/^(click|press|tap)\s+(on\s+)?/i, "").trim();
@@ -84,14 +94,17 @@ function stepToPlaywright(step, url) {
84
94
  if (/\b(select|choose|pick)\b/i.test(s)) {
85
95
  const valueMatch = s.match(/["']([^"']+)["']/);
86
96
  const value = valueMatch?.[1] ?? "option";
97
+ if (hintedSelector) return `await ${hintedSelector}.selectOption('${escapeForSingleQuotedString(value)}');`;
87
98
  return `await page.getByRole('combobox').selectOption('${escapeForSingleQuotedString(value)}');`;
88
99
  }
89
100
  if (/\b(uncheck|untick|disable)\b/i.test(s)) {
101
+ if (hintedSelector) return `await ${hintedSelector}.uncheck();`;
90
102
  const nameMatch = s.match(/["']([^"']+)["']/);
91
103
  const name = nameMatch?.[1] ?? s.replace(/uncheck|untick|disable/gi, "").trim();
92
104
  return `await page.getByLabel('${escapeForSingleQuotedString(name)}').uncheck();`;
93
105
  }
94
106
  if (/\b(check|tick|enable)\b/i.test(s)) {
107
+ if (hintedSelector) return `await ${hintedSelector}.check();`;
95
108
  const nameMatch = s.match(/["']([^"']+)["']/);
96
109
  const name = nameMatch?.[1] ?? s.replace(/check|tick|enable/gi, "").trim();
97
110
  return `await page.getByLabel('${escapeForSingleQuotedString(name)}').check();`;
@@ -105,6 +118,7 @@ function stepToPlaywright(step, url) {
105
118
  return `await page.reload();`;
106
119
  }
107
120
  if (/\bhover\b/i.test(s)) {
121
+ if (hintedSelector) return `await ${hintedSelector}.hover();`;
108
122
  const nameMatch = s.match(/["']([^"']+)["']/);
109
123
  const name = nameMatch?.[1] ?? s.replace(/hover\s+(over\s+)?/i, "").trim();
110
124
  return `await page.getByText('${escapeForSingleQuotedString(name)}').hover();`;
@@ -114,7 +128,8 @@ function stepToPlaywright(step, url) {
114
128
  }
115
129
  return `// TODO: map to Playwright action \u2192 "${s}"`;
116
130
  }
117
- function expectedToAssertion(expected, norm) {
131
+ function expectedToAssertion(expected, norm, hint) {
132
+ if (hint?.playwright) return ensureStatement(hint.playwright);
118
133
  const s = expected.trim();
119
134
  const fieldName = fieldNameFromSentence(s);
120
135
  const currentUrlPattern = urlPatternFromAbsoluteUrl(norm?.url);
@@ -264,8 +279,8 @@ function buildSpecFile(norm, pomClassName, pomImportPath) {
264
279
  let importPath = pomImportPath.replace(/\\/g, "/");
265
280
  if (!importPath.startsWith(".")) importPath = `./${importPath}`;
266
281
  importPath = importPath.replace(/\.ts$/, "");
267
- const stepLines = steps.length ? steps.map((s) => ` ${stepToPlaywright(s, norm.url)}`).join("\n") : " // TODO: add steps";
268
- const assertionLines = expected.length ? expected.map((e) => ` ${expectedToAssertion(e, norm)}`).join("\n") : " // TODO: add assertions";
282
+ const stepLines = steps.length ? steps.map((s, index) => ` ${stepToPlaywright(s, norm.url, norm.step_hints?.[index])}`).join("\n") : " // TODO: add steps";
283
+ const assertionLines = expected.length ? expected.map((e, index) => ` ${expectedToAssertion(e, norm, norm.assertion_hints?.[index])}`).join("\n") : " // TODO: add assertions";
269
284
  const reviewNote = norm.needs_review ? `
270
285
  // \u26A0\uFE0F Flagged for review \u2014 steps or assertions may need manual refinement
271
286
  ` : "";
@@ -350,4 +365,4 @@ export {
350
365
  gen,
351
366
  genCmd
352
367
  };
353
- //# sourceMappingURL=chunk-3EE7LWWT.js.map
368
+ //# sourceMappingURL=chunk-5QRDTCSM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/gen.ts"],"sourcesContent":["import { Command } from 'commander';\nimport fg from 'fast-glob';\nimport { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';\nimport { join, basename, relative, resolve } from 'node:path';\n\ntype NormalizedCase = {\n id?: string;\n title: string;\n tags?: string[];\n steps?: string[];\n step_hints?: Array<{ selector?: string }>;\n expected?: string[];\n assertion_hints?: Array<{ playwright?: string }>;\n needs_review?: boolean;\n review_reasons?: string[];\n source?: string;\n url?: string;\n};\n\nfunction escapeForSingleQuotedString(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\nfunction escapeForRegex(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\\\/]/g, '\\\\$&');\n}\n\nfunction normalizeUrlForComparison(value?: string): string | undefined {\n if (!value) return undefined;\n try {\n return new URL(value).toString().replace(/\\/$/, '');\n } catch {\n return value.trim().replace(/\\/$/, '');\n }\n}\n\nfunction urlPatternFromAbsoluteUrl(value?: string): string | undefined {\n if (!value) return undefined;\n try {\n const parsed = new URL(value);\n const normalizedPath = `${parsed.pathname}${parsed.search}`.replace(/\\/$/, '') || '/';\n if (normalizedPath === '/') return '\\\\/';\n return normalizedPath.replace(/^\\/+/, '').split('/').map(escapeForRegex).join('\\\\/');\n } catch {\n return undefined;\n }\n}\n\nfunction pageNameToUrlPattern(value: string): string | undefined {\n const cleaned = value\n .replace(/\\b(the|a|an|user)\\b/gi, ' ')\n .replace(/\\b(page|screen)\\b/gi, ' ')\n .trim();\n const tokens = cleaned.split(/[\\s/-]+/).map(token => token.trim()).filter(Boolean);\n if (tokens.length === 0) return undefined;\n return tokens.map(token => escapeForRegex(token.toLowerCase())).join('[-_\\\\/]?');\n}\n\nfunction fieldNameFromSentence(value: string): string | undefined {\n const match =\n value.match(/\\bfor\\s+([a-zA-Z][a-zA-Z\\s-]{0,30}?)\\s+field\\b/i) ??\n value.match(/\\b([a-zA-Z][a-zA-Z\\s-]{0,30}?)\\s+field\\b/i) ??\n value.match(/\\b([a-zA-Z][a-zA-Z\\s-]{0,30}?)\\s+input\\b/i);\n return match?.[1]?.trim();\n}\n\nfunction visibleTextRegexFromPhrase(value: string): string {\n const cleaned = value\n .replace(/[\"']/g, '')\n .replace(/\\b(the|a|an|user|should|must|is|are|be|visible|shown|showing|displayed|present|appears?|rendered)\\b/gi, ' ')\n .replace(/\\b(message|text|content|heading|label)\\b/gi, ' ')\n .trim();\n const tokens = cleaned.split(/\\s+/).filter(Boolean);\n if (tokens.length === 0) return '.+';\n return tokens.map(token => escapeForRegex(token)).join('\\\\s+');\n}\n\n// ─── Step → Playwright action ─────────────────────────────────────────────────\n\nfunction ensureStatement(value: string): string {\n const trimmed = value.trim();\n if (!trimmed) return trimmed;\n return trimmed.endsWith(';') ? trimmed : `${trimmed};`;\n}\n\nfunction stepToPlaywright(step: string, url?: string, hint?: { selector?: string }): string {\n const s = step.trim();\n const hintedSelector = hint?.selector ? `page.${hint.selector}` : undefined;\n\n // Navigate\n if (/^(navigate|go to|open|visit|load)/i.test(s)) {\n const urlMatch = s.match(/https?:\\/\\/[^\\s'\"]+/) || s.match(/[\"']([^\"']+)[\"']/);\n const dest = urlMatch?.[1] ?? urlMatch?.[0] ?? url ?? '/';\n if (\n normalizeUrlForComparison(dest) &&\n normalizeUrlForComparison(dest) === normalizeUrlForComparison(url)\n ) {\n return `// navigation handled by pomPage.goto()`;\n }\n return `await page.goto('${escapeForSingleQuotedString(dest)}');`;\n }\n\n // Fill / type / enter into a field\n // Pattern: \"Fill in <field> with '<value>'\" or \"Enter '<value>' in <field>\"\n if (/\\b(type|enter|fill|input|write)\\b/i.test(s)) {\n const values = s.match(/[\"']([^\"']+)[\"']/g) ?? [];\n const value = values[values.length - 1]?.replace(/['\"]/g, '') ?? 'value';\n if (hintedSelector) return `await ${hintedSelector}.fill('${escapeForSingleQuotedString(value)}');`;\n\n // \"Fill in <field> with ...\" — capture the word(s) between \"in\" and \"with\"\n const inWithMatch = s.match(/\\bin\\s+([a-zA-Z][a-zA-Z\\s]{1,25})\\s+with\\b/i);\n // \"Enter/type <value> in/into <field>\"\n const intoMatch = s.match(/\\bin(?:to)?\\s+(?:the\\s+)?([a-zA-Z][a-zA-Z\\s]{1,25})(?:\\s+field|\\s+input|\\s+box|\\s+area)?/i);\n // \"... <field> field/input\"\n const fieldMatch = s.match(/([a-zA-Z][a-zA-Z\\s]{1,25})\\s+(?:field|input|box|area)/i);\n\n const field = (inWithMatch?.[1] ?? intoMatch?.[1] ?? fieldMatch?.[1] ?? 'field').trim();\n return `await page.getByLabel('${escapeForSingleQuotedString(field)}').fill('${escapeForSingleQuotedString(value)}');`;\n }\n\n // Click a button\n if (/\\bclick\\b.*(button|btn|submit|sign in|log in|login|register|continue|next|save|confirm)/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.click();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/click\\s+(the\\s+)?/i, '').trim();\n return `await page.getByRole('button', { name: '${escapeForSingleQuotedString(name)}' }).click();`;\n }\n\n // Click a link\n if (/\\bclick\\b.*(link|anchor|nav)/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.click();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/click\\s+(the\\s+)?/i, '').trim();\n return `await page.getByRole('link', { name: '${escapeForSingleQuotedString(name)}' }).click();`;\n }\n\n // Generic click\n if (/^(click|press|tap)\\b/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.click();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (nameMatch) return `await page.getByText('${escapeForSingleQuotedString(nameMatch[1])}').click();`;\n const target = s.replace(/^(click|press|tap)\\s+(on\\s+)?/i, '').trim();\n return `await page.getByText('${escapeForSingleQuotedString(target)}').click();`;\n }\n\n // Select dropdown\n if (/\\b(select|choose|pick)\\b/i.test(s)) {\n const valueMatch = s.match(/[\"']([^\"']+)[\"']/);\n const value = valueMatch?.[1] ?? 'option';\n if (hintedSelector) return `await ${hintedSelector}.selectOption('${escapeForSingleQuotedString(value)}');`;\n return `await page.getByRole('combobox').selectOption('${escapeForSingleQuotedString(value)}');`;\n }\n\n // Check / uncheck\n if (/\\b(uncheck|untick|disable)\\b/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.uncheck();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/uncheck|untick|disable/gi, '').trim();\n return `await page.getByLabel('${escapeForSingleQuotedString(name)}').uncheck();`;\n }\n if (/\\b(check|tick|enable)\\b/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.check();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/check|tick|enable/gi, '').trim();\n return `await page.getByLabel('${escapeForSingleQuotedString(name)}').check();`;\n }\n\n // Keyboard key\n if (/press.*(enter|tab|escape|esc|space|backspace)/i.test(s)) {\n const keyMatch = s.match(/enter|tab|escape|esc|space|backspace/i);\n const key = (keyMatch?.[0] ?? 'Enter');\n return `await page.keyboard.press('${key.charAt(0).toUpperCase() + key.slice(1).toLowerCase()}');`;\n }\n\n // Reload\n if (/\\b(reload|refresh)\\b/i.test(s)) {\n return `await page.reload();`;\n }\n\n // Hover\n if (/\\bhover\\b/i.test(s)) {\n if (hintedSelector) return `await ${hintedSelector}.hover();`;\n const nameMatch = s.match(/[\"']([^\"']+)[\"']/);\n const name = nameMatch?.[1] ?? s.replace(/hover\\s+(over\\s+)?/i, '').trim();\n return `await page.getByText('${escapeForSingleQuotedString(name)}').hover();`;\n }\n\n // Scroll\n if (/\\bscroll\\b/i.test(s)) {\n return `await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));`;\n }\n\n // Fallback — preserve the step as a comment so the file is still valid\n return `// TODO: map to Playwright action → \"${s}\"`;\n}\n\n// ─── Expected result → assertion ──────────────────────────────────────────────\n\nfunction expectedToAssertion(\n expected: string,\n norm?: NormalizedCase,\n hint?: { playwright?: string },\n): string {\n if (hint?.playwright) return ensureStatement(hint.playwright);\n const s = expected.trim();\n const fieldName = fieldNameFromSentence(s);\n const currentUrlPattern = urlPatternFromAbsoluteUrl(norm?.url);\n\n // Cleared / empty field\n if (/\\b(clear|cleared|empty|blank)\\b/i.test(s) && /\\b(field|input|value)\\b/i.test(s) && fieldName) {\n return `await expect(page.getByLabel('${escapeForSingleQuotedString(fieldName)}')).toHaveValue('');`;\n }\n\n // Stayed on the same page / did not submit\n if (/\\bform\\b.*\\b(?:does not submit|doesn't submit|not submit|not submitted)\\b/i.test(s)) {\n const fallbackPattern = currentUrlPattern || 'login';\n return `await expect(page).toHaveURL(/${fallbackPattern}/); // form did not navigate`;\n }\n if (/\\b(remains?|stays?|still)\\s+on\\b/i.test(s)) {\n const pageMatch = s.match(/\\b(?:remains?|stays?|still)\\s+on\\s+(?:the\\s+)?(.+?)(?:\\s+page|\\s+screen)?$/i);\n const pagePattern = pageMatch?.[1] ? pageNameToUrlPattern(pageMatch[1]) : undefined;\n return `await expect(page).toHaveURL(/${pagePattern || currentUrlPattern || 'login'}/);`;\n }\n\n // URL / redirect check\n if (/\\b(url|redirect(?:s|ed)?|navigate(?:s|d)?|route|path)\\b/i.test(s)) {\n const pathMatch =\n s.match(/[\"'](\\/[^\"']+)[\"']/) ??\n s.match(/to\\s+(\\/[\\w/-]+)/i) ??\n s.match(/https?:\\/\\/[^\\s'\"]+/i);\n const matchedPath = pathMatch?.[1] ?? pathMatch?.[0];\n if (matchedPath) {\n const fromUrl = matchedPath.startsWith('http') ? urlPatternFromAbsoluteUrl(matchedPath) : undefined;\n const directPath = matchedPath.startsWith('/') ? matchedPath.replace(/^\\/+/, '').split('/').map(escapeForRegex).join('\\\\/') : undefined;\n const finalPattern = fromUrl ?? directPath ?? currentUrlPattern;\n if (finalPattern !== undefined) return `await expect(page).toHaveURL(/${finalPattern}/);`;\n }\n return `await expect(page).toHaveURL(/dashboard|success|home/);`;\n }\n\n // Page title\n if (/\\bpage title\\b|\\btitle should\\b|\\bdocument title\\b/i.test(s)) {\n const titleMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (titleMatch) return `await expect(page).toHaveTitle('${escapeForSingleQuotedString(titleMatch[1])}');`;\n return `await expect(page).toHaveTitle(/.+/);`;\n }\n\n // Error / validation message\n if (/\\b(error|invalid|fail|incorrect|required|validation)\\b/i.test(s)) {\n const msgMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (msgMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(msgMatch[1])}')).toBeVisible();`;\n if (fieldName) return `await expect(page.getByText(/${escapeForRegex(fieldName)}/i)).toBeVisible();`;\n return `await expect(page.getByRole('alert')).toBeVisible();`;\n }\n\n // Success / confirmation\n if (/\\b(success|confirm|complete|thank|welcome|sent|saved)\\b/i.test(s)) {\n const msgMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (msgMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(msgMatch[1])}')).toBeVisible();`;\n if (/\\bwelcome\\b/i.test(s)) return `await expect(page.getByText(/welcome/i)).toBeVisible();`;\n return `await expect(page.getByRole('status')).toBeVisible();`;\n }\n\n // Not visible / hidden\n if (/\\b(not visible|hidden|disappear|removed|gone)\\b/i.test(s)) {\n const elementMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (elementMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(elementMatch[1])}')).not.toBeVisible();`;\n return `await expect(page.locator('.modal, [role=\"dialog\"]').first()).not.toBeVisible();`;\n }\n\n // Visible / present\n if (/\\b(visible|appear|display|show|render|present)\\b/i.test(s)) {\n const elementMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (elementMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(elementMatch[1])}')).toBeVisible();`;\n const subjectMatch =\n s.match(/^(?:the\\s+)?(.+?)\\s+(?:is|are|should|must|becomes?|appears?|renders?|shows?|displays?)\\b/i) ??\n s.match(/^(?:the\\s+)?(.+?)\\s+(?:visible|present)\\b/i);\n const subject = subjectMatch?.[1]?.trim();\n if (subject) {\n if (/\\bnavigation\\b.*\\bmenu\\b|\\bmenu\\b.*\\bnavigation\\b|\\bnavigation\\b/i.test(subject)) {\n return `await expect(page.getByRole('navigation')).toBeVisible();`;\n }\n return `await expect(page.getByText(/${visibleTextRegexFromPhrase(subject)}/i)).toBeVisible();`;\n }\n return `await expect(page.locator('[data-testid]').first()).toBeVisible();`;\n }\n\n // Count of items\n if (/\\b(count|number of|list of|\\d+\\s+item)\\b/i.test(s)) {\n const countMatch = s.match(/(\\d+)/);\n if (countMatch) return `await expect(page.locator('li, tr, [role=\"listitem\"]')).toHaveCount(${countMatch[1]});`;\n return `await expect(page.locator('li, tr').first()).toBeVisible();`;\n }\n\n // Enabled / disabled\n if (/\\b(enabled|clickable|active)\\b/i.test(s)) {\n return `await expect(page.getByRole('button').first()).toBeEnabled();`;\n }\n if (/\\b(disabled|inactive)\\b/i.test(s)) {\n return `await expect(page.getByRole('button').first()).toBeDisabled();`;\n }\n\n // Input value\n if (/\\b(clear|cleared|empty|blank)\\b/i.test(s) && /\\b(field|input|value)\\b/i.test(s)) {\n return `await expect(page.getByRole('textbox').first()).toHaveValue('');`;\n }\n if (/\\b(field|input|value|filled)\\b/i.test(s)) {\n const valMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (valMatch) return `await expect(page.getByRole('textbox').first()).toHaveValue('${escapeForSingleQuotedString(valMatch[1])}');`;\n return `await expect(page.getByRole('textbox').first()).not.toBeEmpty();`;\n }\n\n // Text / heading / content (broad fallback before final fallback)\n if (/\\b(text|content|label|message|heading)\\b/i.test(s)) {\n const textMatch = s.match(/[\"']([^\"']+)[\"']/);\n if (textMatch) return `await expect(page.getByText('${escapeForSingleQuotedString(textMatch[1])}')).toBeVisible();`;\n const words = s.replace(/\\b(the|should|must|contain|display|show|have|text|content)\\b/gi, '').trim();\n return `await expect(page.getByText(/${visibleTextRegexFromPhrase(words)}/i)).toBeVisible();`;\n }\n\n // Final fallback\n const cleaned = s.replace(/[\"']/g, '').trim();\n return `await expect(page.getByText(/${visibleTextRegexFromPhrase(cleaned)}/i)).toBeVisible(); // TODO: refine assertion`;\n}\n\n// ─── POM class name / filename ────────────────────────────────────────────────\n\nfunction derivePomClassName(norm: NormalizedCase): string {\n const raw = norm.id ?? norm.title ?? 'Landing';\n const withoutNum = raw.replace(/-\\d+$/, '').replace(/[-_\\s]+/g, ' ');\n const words = withoutNum\n .split(/\\s+/)\n .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase());\n return words.join('') + 'Page';\n}\n\n// ─── POM class source ─────────────────────────────────────────────────────────\n\nfunction buildPomClass(className: string, norm: NormalizedCase, lang: 'ts' | 'js'): string {\n const pageUrl = norm.url ?? '/';\n const isTs = lang === 'ts';\n\n return isTs\n ? `import { Page } from '@playwright/test';\n\nexport class ${className} {\n readonly page: Page;\n\n constructor(page: Page) {\n this.page = page;\n }\n\n async goto(): Promise<void> {\n await this.page.goto('${escapeForSingleQuotedString(pageUrl)}');\n }\n\n async waitForLoad(): Promise<void> {\n await this.page.waitForLoadState('domcontentloaded');\n }\n}\n`\n : `// @ts-check\nexport class ${className} {\n /** @param {import('@playwright/test').Page} page */\n constructor(page) {\n /** @type {import('@playwright/test').Page} */\n this.page = page;\n }\n\n async goto() {\n await this.page.goto('${escapeForSingleQuotedString(pageUrl)}');\n }\n\n async waitForLoad() {\n await this.page.waitForLoadState('domcontentloaded');\n }\n}\n`;\n}\n\n// ─── Spec file source ─────────────────────────────────────────────────────────\n\nfunction buildTestTitle(norm: NormalizedCase): string {\n const idPart = norm.id ?? '';\n\n // norm.title may already include the ID prefix (e.g. \"AUTH-001 — User can log in\")\n // Strip it to avoid \"AUTH-001 — AUTH-001 — User can log in\" in the test name.\n let cleanTitle = norm.title || 'Untitled';\n if (idPart && cleanTitle.startsWith(idPart)) {\n cleanTitle = cleanTitle.slice(idPart.length).replace(/^\\s*[—\\-–]+\\s*/, '').trim();\n }\n\n const tagSuffix = (norm.tags ?? []).map(t => `@${t}`).join(' ');\n return [idPart, cleanTitle, tagSuffix].filter(Boolean).join(' — ').trim();\n}\n\nfunction buildSpecFile(\n norm: NormalizedCase,\n pomClassName: string,\n pomImportPath: string,\n): string {\n const title = buildTestTitle(norm);\n const steps = norm.steps ?? [];\n const expected = norm.expected ?? [];\n\n let importPath = pomImportPath.replace(/\\\\/g, '/');\n if (!importPath.startsWith('.')) importPath = `./${importPath}`;\n // Strip .ts extension — TypeScript resolves without it\n importPath = importPath.replace(/\\.ts$/, '');\n\n const stepLines = steps.length\n ? steps.map((s, index) => ` ${stepToPlaywright(s, norm.url, norm.step_hints?.[index])}`).join('\\n')\n : ' // TODO: add steps';\n\n const assertionLines = expected.length\n ? expected.map((e, index) => ` ${expectedToAssertion(e, norm, norm.assertion_hints?.[index])}`).join('\\n')\n : ' // TODO: add assertions';\n\n const reviewNote = norm.needs_review\n ? `\\n // ⚠️ Flagged for review — steps or assertions may need manual refinement\\n`\n : '';\n\n return `import { test, expect } from '@playwright/test';\nimport { ${pomClassName} } from '${importPath}';\n\ntest('${title}', async ({ page }) => {${reviewNote}\n const pomPage = new ${pomClassName}(page);\n\n // ── Setup ─────────────────────────────────────────────────────────────────\n await pomPage.goto();\n await pomPage.waitForLoad();\n\n // ── Steps ─────────────────────────────────────────────────────────────────\n${stepLines}\n\n // ── Assertions ────────────────────────────────────────────────────────────\n${assertionLines}\n});\n`;\n}\n\n// ─── Main ─────────────────────────────────────────────────────────────────────\n\nexport async function gen(opts: { lang: string; out: string }) {\n if (opts.lang !== 'ts' && opts.lang !== 'js') {\n console.error('❌ --lang must be ts or js');\n process.exit(1);\n }\n const lang = opts.lang as 'ts' | 'js';\n const specExt = lang === 'js' ? 'spec.js' : 'spec.ts';\n const pomExt = lang === 'js' ? '.js' : '.ts';\n\n const normalized = await fg([\n '.cementic/normalized/*.json',\n '!.cementic/normalized/_index.json',\n ]);\n\n if (normalized.length === 0) {\n console.warn('⚠️ No normalized cases found in .cementic/normalized/');\n console.warn(' Run: ct normalize ./cases');\n process.exit(1);\n }\n\n const projectRoot = process.cwd();\n const testsOutDir = resolve(projectRoot, opts.out);\n const pagesOutDir = resolve(projectRoot, 'pages');\n\n mkdirSync(testsOutDir, { recursive: true });\n mkdirSync(pagesOutDir, { recursive: true });\n\n let specCount = 0;\n let pomCount = 0;\n\n for (const f of normalized) {\n const norm = JSON.parse(readFileSync(f, 'utf8')) as NormalizedCase;\n\n const pomClassName = derivePomClassName(norm);\n const pomFileName = pomClassName + pomExt;\n const pomFilePath = join(pagesOutDir, pomFileName);\n\n // Only write a POM file if one doesn't exist yet — never overwrite user edits\n if (!existsSync(pomFilePath)) {\n writeFileSync(pomFilePath, buildPomClass(pomClassName, norm, lang));\n pomCount++;\n }\n\n // Relative import from the test file location to the POM file\n const relToPages = relative(testsOutDir, pagesOutDir);\n const pomImportPath = join(relToPages, pomFileName);\n\n const stem = basename(f).replace(/\\.json$/, '').replace(/[^\\w-]+/g, '-');\n const specPath = join(testsOutDir, `${stem}.${specExt}`);\n writeFileSync(specPath, buildSpecFile(norm, pomClassName, pomImportPath));\n specCount++;\n }\n\n console.log(`✅ Generated ${specCount} spec file(s) → ${opts.out}/`);\n if (pomCount > 0) {\n console.log(`✅ Generated ${pomCount} POM class file(s) → pages/`);\n }\n if (pomCount === 0 && specCount > 0) {\n console.log(`ℹ️ POM classes already exist — skipped regeneration`);\n }\n}\n\nexport function genCmd() {\n const cmd = new Command('gen')\n .description('Generate Playwright POM spec + page-object files from normalized cases')\n .addHelpText('after', `\nExamples:\n $ ct gen --lang ts\n $ ct gen --lang js --out tests/e2e\n`)\n .option('--lang <lang>', 'Target language (ts|js)', 'ts')\n .option('--out <dir>', 'Output directory for spec files', 'tests/generated')\n .action(async opts => {\n await gen({ lang: opts.lang, out: opts.out });\n });\n return cmd;\n}\n"],"mappings":";;;AAAA,SAAS,eAAe;AACxB,OAAO,QAAQ;AACf,SAAS,cAAc,WAAW,eAAe,kBAAkB;AACnE,SAAS,MAAM,UAAU,UAAU,eAAe;AAgBlD,SAAS,4BAA4B,OAAuB;AAC1D,SAAO,MAAM,QAAQ,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAK;AACzD;AAEA,SAAS,eAAe,OAAuB;AAC7C,SAAO,MAAM,QAAQ,yBAAyB,MAAM;AACtD;AAEA,SAAS,0BAA0B,OAAoC;AACrE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,WAAO,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,OAAO,EAAE;AAAA,EACpD,QAAQ;AACN,WAAO,MAAM,KAAK,EAAE,QAAQ,OAAO,EAAE;AAAA,EACvC;AACF;AAEA,SAAS,0BAA0B,OAAoC;AACrE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,KAAK;AAC5B,UAAM,iBAAiB,GAAG,OAAO,QAAQ,GAAG,OAAO,MAAM,GAAG,QAAQ,OAAO,EAAE,KAAK;AAClF,QAAI,mBAAmB,IAAK,QAAO;AACnC,WAAO,eAAe,QAAQ,QAAQ,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,cAAc,EAAE,KAAK,KAAK;AAAA,EACrF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,qBAAqB,OAAmC;AAC/D,QAAM,UAAU,MACb,QAAQ,yBAAyB,GAAG,EACpC,QAAQ,uBAAuB,GAAG,EAClC,KAAK;AACR,QAAM,SAAS,QAAQ,MAAM,SAAS,EAAE,IAAI,WAAS,MAAM,KAAK,CAAC,EAAE,OAAO,OAAO;AACjF,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,SAAO,OAAO,IAAI,WAAS,eAAe,MAAM,YAAY,CAAC,CAAC,EAAE,KAAK,UAAU;AACjF;AAEA,SAAS,sBAAsB,OAAmC;AAChE,QAAM,QACJ,MAAM,MAAM,iDAAiD,KAC7D,MAAM,MAAM,2CAA2C,KACvD,MAAM,MAAM,2CAA2C;AACzD,SAAO,QAAQ,CAAC,GAAG,KAAK;AAC1B;AAEA,SAAS,2BAA2B,OAAuB;AACzD,QAAM,UAAU,MACb,QAAQ,SAAS,EAAE,EACnB,QAAQ,yGAAyG,GAAG,EACpH,QAAQ,8CAA8C,GAAG,EACzD,KAAK;AACR,QAAM,SAAS,QAAQ,MAAM,KAAK,EAAE,OAAO,OAAO;AAClD,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,SAAO,OAAO,IAAI,WAAS,eAAe,KAAK,CAAC,EAAE,KAAK,MAAM;AAC/D;AAIA,SAAS,gBAAgB,OAAuB;AAC9C,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,QAAQ,SAAS,GAAG,IAAI,UAAU,GAAG,OAAO;AACrD;AAEA,SAAS,iBAAiB,MAAc,KAAc,MAAsC;AAC1F,QAAM,IAAI,KAAK,KAAK;AACpB,QAAM,iBAAiB,MAAM,WAAW,QAAQ,KAAK,QAAQ,KAAK;AAGlE,MAAI,qCAAqC,KAAK,CAAC,GAAG;AAChD,UAAM,WAAW,EAAE,MAAM,qBAAqB,KAAK,EAAE,MAAM,kBAAkB;AAC7E,UAAM,OAAO,WAAW,CAAC,KAAK,WAAW,CAAC,KAAK,OAAO;AACtD,QACE,0BAA0B,IAAI,KAC9B,0BAA0B,IAAI,MAAM,0BAA0B,GAAG,GACjE;AACA,aAAO;AAAA,IACT;AACA,WAAO,oBAAoB,4BAA4B,IAAI,CAAC;AAAA,EAC9D;AAIA,MAAI,qCAAqC,KAAK,CAAC,GAAG;AAChD,UAAM,SAAS,EAAE,MAAM,mBAAmB,KAAK,CAAC;AAChD,UAAM,QAAQ,OAAO,OAAO,SAAS,CAAC,GAAG,QAAQ,SAAS,EAAE,KAAK;AACjE,QAAI,eAAgB,QAAO,SAAS,cAAc,UAAU,4BAA4B,KAAK,CAAC;AAG9F,UAAM,cAAc,EAAE,MAAM,6CAA6C;AAEzE,UAAM,YAAY,EAAE,MAAM,2FAA2F;AAErH,UAAM,aAAa,EAAE,MAAM,wDAAwD;AAEnF,UAAM,SAAS,cAAc,CAAC,KAAK,YAAY,CAAC,KAAK,aAAa,CAAC,KAAK,SAAS,KAAK;AACtF,WAAO,0BAA0B,4BAA4B,KAAK,CAAC,YAAY,4BAA4B,KAAK,CAAC;AAAA,EACnH;AAGA,MAAI,2FAA2F,KAAK,CAAC,GAAG;AACtG,QAAI,eAAgB,QAAO,SAAS,cAAc;AAClD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,UAAM,OAAO,YAAY,CAAC,KAAK,EAAE,QAAQ,sBAAsB,EAAE,EAAE,KAAK;AACxE,WAAO,2CAA2C,4BAA4B,IAAI,CAAC;AAAA,EACrF;AAGA,MAAI,gCAAgC,KAAK,CAAC,GAAG;AAC3C,QAAI,eAAgB,QAAO,SAAS,cAAc;AAClD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,UAAM,OAAO,YAAY,CAAC,KAAK,EAAE,QAAQ,sBAAsB,EAAE,EAAE,KAAK;AACxE,WAAO,yCAAyC,4BAA4B,IAAI,CAAC;AAAA,EACnF;AAGA,MAAI,wBAAwB,KAAK,CAAC,GAAG;AACnC,QAAI,eAAgB,QAAO,SAAS,cAAc;AAClD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,QAAI,UAAW,QAAO,yBAAyB,4BAA4B,UAAU,CAAC,CAAC,CAAC;AACxF,UAAM,SAAS,EAAE,QAAQ,kCAAkC,EAAE,EAAE,KAAK;AACpE,WAAO,yBAAyB,4BAA4B,MAAM,CAAC;AAAA,EACrE;AAGA,MAAI,4BAA4B,KAAK,CAAC,GAAG;AACvC,UAAM,aAAa,EAAE,MAAM,kBAAkB;AAC7C,UAAM,QAAQ,aAAa,CAAC,KAAK;AACjC,QAAI,eAAgB,QAAO,SAAS,cAAc,kBAAkB,4BAA4B,KAAK,CAAC;AACtG,WAAO,kDAAkD,4BAA4B,KAAK,CAAC;AAAA,EAC7F;AAGA,MAAI,gCAAgC,KAAK,CAAC,GAAG;AAC3C,QAAI,eAAgB,QAAO,SAAS,cAAc;AAClD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,UAAM,OAAO,YAAY,CAAC,KAAK,EAAE,QAAQ,4BAA4B,EAAE,EAAE,KAAK;AAC9E,WAAO,0BAA0B,4BAA4B,IAAI,CAAC;AAAA,EACpE;AACA,MAAI,2BAA2B,KAAK,CAAC,GAAG;AACtC,QAAI,eAAgB,QAAO,SAAS,cAAc;AAClD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,UAAM,OAAO,YAAY,CAAC,KAAK,EAAE,QAAQ,uBAAuB,EAAE,EAAE,KAAK;AACzE,WAAO,0BAA0B,4BAA4B,IAAI,CAAC;AAAA,EACpE;AAGA,MAAI,iDAAiD,KAAK,CAAC,GAAG;AAC5D,UAAM,WAAW,EAAE,MAAM,uCAAuC;AAChE,UAAM,MAAO,WAAW,CAAC,KAAK;AAC9B,WAAO,8BAA8B,IAAI,OAAO,CAAC,EAAE,YAAY,IAAI,IAAI,MAAM,CAAC,EAAE,YAAY,CAAC;AAAA,EAC/F;AAGA,MAAI,wBAAwB,KAAK,CAAC,GAAG;AACnC,WAAO;AAAA,EACT;AAGA,MAAI,aAAa,KAAK,CAAC,GAAG;AACxB,QAAI,eAAgB,QAAO,SAAS,cAAc;AAClD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,UAAM,OAAO,YAAY,CAAC,KAAK,EAAE,QAAQ,uBAAuB,EAAE,EAAE,KAAK;AACzE,WAAO,yBAAyB,4BAA4B,IAAI,CAAC;AAAA,EACnE;AAGA,MAAI,cAAc,KAAK,CAAC,GAAG;AACzB,WAAO;AAAA,EACT;AAGA,SAAO,6CAAwC,CAAC;AAClD;AAIA,SAAS,oBACP,UACA,MACA,MACQ;AACR,MAAI,MAAM,WAAY,QAAO,gBAAgB,KAAK,UAAU;AAC5D,QAAM,IAAI,SAAS,KAAK;AACxB,QAAM,YAAY,sBAAsB,CAAC;AACzC,QAAM,oBAAoB,0BAA0B,MAAM,GAAG;AAG7D,MAAI,mCAAmC,KAAK,CAAC,KAAK,2BAA2B,KAAK,CAAC,KAAK,WAAW;AACjG,WAAO,iCAAiC,4BAA4B,SAAS,CAAC;AAAA,EAChF;AAGA,MAAI,6EAA6E,KAAK,CAAC,GAAG;AACxF,UAAM,kBAAkB,qBAAqB;AAC7C,WAAO,iCAAiC,eAAe;AAAA,EACzD;AACA,MAAI,oCAAoC,KAAK,CAAC,GAAG;AAC/C,UAAM,YAAY,EAAE,MAAM,6EAA6E;AACvG,UAAM,cAAc,YAAY,CAAC,IAAI,qBAAqB,UAAU,CAAC,CAAC,IAAI;AAC1E,WAAO,iCAAiC,eAAe,qBAAqB,OAAO;AAAA,EACrF;AAGA,MAAI,2DAA2D,KAAK,CAAC,GAAG;AACtE,UAAM,YACJ,EAAE,MAAM,oBAAoB,KAC5B,EAAE,MAAM,mBAAmB,KAC3B,EAAE,MAAM,sBAAsB;AAChC,UAAM,cAAc,YAAY,CAAC,KAAK,YAAY,CAAC;AACnD,QAAI,aAAa;AACf,YAAM,UAAU,YAAY,WAAW,MAAM,IAAI,0BAA0B,WAAW,IAAI;AAC1F,YAAM,aAAa,YAAY,WAAW,GAAG,IAAI,YAAY,QAAQ,QAAQ,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,cAAc,EAAE,KAAK,KAAK,IAAI;AAC9H,YAAM,eAAe,WAAW,cAAc;AAC9C,UAAI,iBAAiB,OAAW,QAAO,iCAAiC,YAAY;AAAA,IACtF;AACA,WAAO;AAAA,EACT;AAGA,MAAI,sDAAsD,KAAK,CAAC,GAAG;AACjE,UAAM,aAAa,EAAE,MAAM,kBAAkB;AAC7C,QAAI,WAAY,QAAO,mCAAmC,4BAA4B,WAAW,CAAC,CAAC,CAAC;AACpG,WAAO;AAAA,EACT;AAGA,MAAI,0DAA0D,KAAK,CAAC,GAAG;AACrE,UAAM,WAAW,EAAE,MAAM,kBAAkB;AAC3C,QAAI,SAAU,QAAO,gCAAgC,4BAA4B,SAAS,CAAC,CAAC,CAAC;AAC7F,QAAI,UAAW,QAAO,gCAAgC,eAAe,SAAS,CAAC;AAC/E,WAAO;AAAA,EACT;AAGA,MAAI,2DAA2D,KAAK,CAAC,GAAG;AACtE,UAAM,WAAW,EAAE,MAAM,kBAAkB;AAC3C,QAAI,SAAU,QAAO,gCAAgC,4BAA4B,SAAS,CAAC,CAAC,CAAC;AAC7F,QAAI,eAAe,KAAK,CAAC,EAAG,QAAO;AACnC,WAAO;AAAA,EACT;AAGA,MAAI,mDAAmD,KAAK,CAAC,GAAG;AAC9D,UAAM,eAAe,EAAE,MAAM,kBAAkB;AAC/C,QAAI,aAAc,QAAO,gCAAgC,4BAA4B,aAAa,CAAC,CAAC,CAAC;AACrG,WAAO;AAAA,EACT;AAGA,MAAI,oDAAoD,KAAK,CAAC,GAAG;AAC/D,UAAM,eAAe,EAAE,MAAM,kBAAkB;AAC/C,QAAI,aAAc,QAAO,gCAAgC,4BAA4B,aAAa,CAAC,CAAC,CAAC;AACrG,UAAM,eACJ,EAAE,MAAM,2FAA2F,KACnG,EAAE,MAAM,4CAA4C;AACtD,UAAM,UAAU,eAAe,CAAC,GAAG,KAAK;AACxC,QAAI,SAAS;AACX,UAAI,oEAAoE,KAAK,OAAO,GAAG;AACrF,eAAO;AAAA,MACT;AACA,aAAO,gCAAgC,2BAA2B,OAAO,CAAC;AAAA,IAC5E;AACA,WAAO;AAAA,EACT;AAGA,MAAI,4CAA4C,KAAK,CAAC,GAAG;AACvD,UAAM,aAAa,EAAE,MAAM,OAAO;AAClC,QAAI,WAAY,QAAO,uEAAuE,WAAW,CAAC,CAAC;AAC3G,WAAO;AAAA,EACT;AAGA,MAAI,kCAAkC,KAAK,CAAC,GAAG;AAC7C,WAAO;AAAA,EACT;AACA,MAAI,2BAA2B,KAAK,CAAC,GAAG;AACtC,WAAO;AAAA,EACT;AAGA,MAAI,mCAAmC,KAAK,CAAC,KAAK,2BAA2B,KAAK,CAAC,GAAG;AACpF,WAAO;AAAA,EACT;AACA,MAAI,kCAAkC,KAAK,CAAC,GAAG;AAC7C,UAAM,WAAW,EAAE,MAAM,kBAAkB;AAC3C,QAAI,SAAU,QAAO,gEAAgE,4BAA4B,SAAS,CAAC,CAAC,CAAC;AAC7H,WAAO;AAAA,EACT;AAGA,MAAI,4CAA4C,KAAK,CAAC,GAAG;AACvD,UAAM,YAAY,EAAE,MAAM,kBAAkB;AAC5C,QAAI,UAAW,QAAO,gCAAgC,4BAA4B,UAAU,CAAC,CAAC,CAAC;AAC/F,UAAM,QAAQ,EAAE,QAAQ,kEAAkE,EAAE,EAAE,KAAK;AACnG,WAAO,gCAAgC,2BAA2B,KAAK,CAAC;AAAA,EAC1E;AAGA,QAAM,UAAU,EAAE,QAAQ,SAAS,EAAE,EAAE,KAAK;AAC5C,SAAO,gCAAgC,2BAA2B,OAAO,CAAC;AAC5E;AAIA,SAAS,mBAAmB,MAA8B;AACxD,QAAM,MAAM,KAAK,MAAM,KAAK,SAAS;AACrC,QAAM,aAAa,IAAI,QAAQ,SAAS,EAAE,EAAE,QAAQ,YAAY,GAAG;AACnE,QAAM,QAAQ,WACX,MAAM,KAAK,EACX,IAAI,OAAK,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC,EAAE,YAAY,CAAC;AAChE,SAAO,MAAM,KAAK,EAAE,IAAI;AAC1B;AAIA,SAAS,cAAc,WAAmB,MAAsB,MAA2B;AACzF,QAAM,UAAU,KAAK,OAAO;AAC5B,QAAM,OAAO,SAAS;AAEtB,SAAO,OACH;AAAA;AAAA,eAES,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAQI,4BAA4B,OAAO,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQ1D;AAAA,eACS,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAQI,4BAA4B,OAAO,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQhE;AAIA,SAAS,eAAe,MAA8B;AACpD,QAAM,SAAS,KAAK,MAAM;AAI1B,MAAI,aAAa,KAAK,SAAS;AAC/B,MAAI,UAAU,WAAW,WAAW,MAAM,GAAG;AAC3C,iBAAa,WAAW,MAAM,OAAO,MAAM,EAAE,QAAQ,kBAAkB,EAAE,EAAE,KAAK;AAAA,EAClF;AAEA,QAAM,aAAa,KAAK,QAAQ,CAAC,GAAG,IAAI,OAAK,IAAI,CAAC,EAAE,EAAE,KAAK,GAAG;AAC9D,SAAO,CAAC,QAAQ,YAAY,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,UAAK,EAAE,KAAK;AAC1E;AAEA,SAAS,cACP,MACA,cACA,eACQ;AACR,QAAM,QAAQ,eAAe,IAAI;AACjC,QAAM,QAAQ,KAAK,SAAS,CAAC;AAC7B,QAAM,WAAW,KAAK,YAAY,CAAC;AAEnC,MAAI,aAAa,cAAc,QAAQ,OAAO,GAAG;AACjD,MAAI,CAAC,WAAW,WAAW,GAAG,EAAG,cAAa,KAAK,UAAU;AAE7D,eAAa,WAAW,QAAQ,SAAS,EAAE;AAE3C,QAAM,YAAY,MAAM,SACpB,MAAM,IAAI,CAAC,GAAG,UAAU,KAAK,iBAAiB,GAAG,KAAK,KAAK,KAAK,aAAa,KAAK,CAAC,CAAC,EAAE,EAAE,KAAK,IAAI,IACjG;AAEJ,QAAM,iBAAiB,SAAS,SAC5B,SAAS,IAAI,CAAC,GAAG,UAAU,KAAK,oBAAoB,GAAG,MAAM,KAAK,kBAAkB,KAAK,CAAC,CAAC,EAAE,EAAE,KAAK,IAAI,IACxG;AAEJ,QAAM,aAAa,KAAK,eACpB;AAAA;AAAA,IACA;AAEJ,SAAO;AAAA,WACE,YAAY,YAAY,UAAU;AAAA;AAAA,QAErC,KAAK,2BAA2B,UAAU;AAAA,wBAC1B,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlC,SAAS;AAAA;AAAA;AAAA,EAGT,cAAc;AAAA;AAAA;AAGhB;AAIA,eAAsB,IAAI,MAAqC;AAC7D,MAAI,KAAK,SAAS,QAAQ,KAAK,SAAS,MAAM;AAC5C,YAAQ,MAAM,gCAA2B;AACzC,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,OAAO,KAAK;AAClB,QAAM,UAAU,SAAS,OAAO,YAAY;AAC5C,QAAM,SAAU,SAAS,OAAO,QAAQ;AAExC,QAAM,aAAa,MAAM,GAAG;AAAA,IAC1B;AAAA,IACA;AAAA,EACF,CAAC;AAED,MAAI,WAAW,WAAW,GAAG;AAC3B,YAAQ,KAAK,kEAAwD;AACrE,YAAQ,KAAK,+BAA+B;AAC5C,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,QAAM,cAAc,QAAQ,IAAI;AAChC,QAAM,cAAc,QAAQ,aAAa,KAAK,GAAG;AACjD,QAAM,cAAc,QAAQ,aAAa,OAAO;AAEhD,YAAU,aAAa,EAAE,WAAW,KAAK,CAAC;AAC1C,YAAU,aAAa,EAAE,WAAW,KAAK,CAAC;AAE1C,MAAI,YAAY;AAChB,MAAI,WAAY;AAEhB,aAAW,KAAK,YAAY;AAC1B,UAAM,OAAO,KAAK,MAAM,aAAa,GAAG,MAAM,CAAC;AAE/C,UAAM,eAAe,mBAAmB,IAAI;AAC5C,UAAM,cAAe,eAAe;AACpC,UAAM,cAAe,KAAK,aAAa,WAAW;AAGlD,QAAI,CAAC,WAAW,WAAW,GAAG;AAC5B,oBAAc,aAAa,cAAc,cAAc,MAAM,IAAI,CAAC;AAClE;AAAA,IACF;AAGA,UAAM,aAAe,SAAS,aAAa,WAAW;AACtD,UAAM,gBAAgB,KAAK,YAAY,WAAW;AAElD,UAAM,OAAW,SAAS,CAAC,EAAE,QAAQ,WAAW,EAAE,EAAE,QAAQ,YAAY,GAAG;AAC3E,UAAM,WAAW,KAAK,aAAa,GAAG,IAAI,IAAI,OAAO,EAAE;AACvD,kBAAc,UAAU,cAAc,MAAM,cAAc,aAAa,CAAC;AACxE;AAAA,EACF;AAEA,UAAQ,IAAI,oBAAe,SAAS,yBAAoB,KAAK,GAAG,GAAG;AACnE,MAAI,WAAW,GAAG;AAChB,YAAQ,IAAI,oBAAe,QAAQ,kCAA6B;AAAA,EAClE;AACA,MAAI,aAAa,KAAK,YAAY,GAAG;AACnC,YAAQ,IAAI,qEAAsD;AAAA,EACpE;AACF;AAEO,SAAS,SAAS;AACvB,QAAM,MAAM,IAAI,QAAQ,KAAK,EAC1B,YAAY,wEAAwE,EACpF,YAAY,SAAS;AAAA;AAAA;AAAA;AAAA,CAIzB,EACI,OAAO,iBAAiB,2BAA2B,IAAI,EACvD,OAAO,eAAgB,mCAAmC,iBAAiB,EAC3E,OAAO,OAAM,SAAQ;AACpB,UAAM,IAAI,EAAE,MAAM,KAAK,MAAM,KAAK,KAAK,IAAI,CAAC;AAAA,EAC9C,CAAC;AACH,SAAO;AACT;","names":[]}