@dujaunpaul/qass 0.1.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 (103) hide show
  1. package/LICENSE +40 -0
  2. package/README.md +146 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +117 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/core/config.d.ts +4 -0
  8. package/dist/core/config.d.ts.map +1 -0
  9. package/dist/core/config.js +128 -0
  10. package/dist/core/config.js.map +1 -0
  11. package/dist/core/diff-analyzer.d.ts +3 -0
  12. package/dist/core/diff-analyzer.d.ts.map +1 -0
  13. package/dist/core/diff-analyzer.js +194 -0
  14. package/dist/core/diff-analyzer.js.map +1 -0
  15. package/dist/core/discover.d.ts +3 -0
  16. package/dist/core/discover.d.ts.map +1 -0
  17. package/dist/core/discover.js +51 -0
  18. package/dist/core/discover.js.map +1 -0
  19. package/dist/core/license.d.ts +13 -0
  20. package/dist/core/license.d.ts.map +1 -0
  21. package/dist/core/license.js +132 -0
  22. package/dist/core/license.js.map +1 -0
  23. package/dist/core/report.d.ts +4 -0
  24. package/dist/core/report.d.ts.map +1 -0
  25. package/dist/core/report.js +95 -0
  26. package/dist/core/report.js.map +1 -0
  27. package/dist/core/runner.d.ts +3 -0
  28. package/dist/core/runner.d.ts.map +1 -0
  29. package/dist/core/runner.js +136 -0
  30. package/dist/core/runner.js.map +1 -0
  31. package/dist/core/test-planner.d.ts +3 -0
  32. package/dist/core/test-planner.d.ts.map +1 -0
  33. package/dist/core/test-planner.js +107 -0
  34. package/dist/core/test-planner.js.map +1 -0
  35. package/dist/integrations/cursor-rule.d.ts +2 -0
  36. package/dist/integrations/cursor-rule.d.ts.map +1 -0
  37. package/dist/integrations/cursor-rule.js +46 -0
  38. package/dist/integrations/cursor-rule.js.map +1 -0
  39. package/dist/integrations/mcp-server.d.ts +67 -0
  40. package/dist/integrations/mcp-server.d.ts.map +1 -0
  41. package/dist/integrations/mcp-server.js +61 -0
  42. package/dist/integrations/mcp-server.js.map +1 -0
  43. package/dist/runners/api/api-runner.d.ts +3 -0
  44. package/dist/runners/api/api-runner.d.ts.map +1 -0
  45. package/dist/runners/api/api-runner.js +258 -0
  46. package/dist/runners/api/api-runner.js.map +1 -0
  47. package/dist/runners/api/endpoint-discovery.d.ts +3 -0
  48. package/dist/runners/api/endpoint-discovery.d.ts.map +1 -0
  49. package/dist/runners/api/endpoint-discovery.js +106 -0
  50. package/dist/runners/api/endpoint-discovery.js.map +1 -0
  51. package/dist/runners/e2e/playwright-runner.d.ts +3 -0
  52. package/dist/runners/e2e/playwright-runner.d.ts.map +1 -0
  53. package/dist/runners/e2e/playwright-runner.js +309 -0
  54. package/dist/runners/e2e/playwright-runner.js.map +1 -0
  55. package/dist/runners/security/dynamic-checker.d.ts +3 -0
  56. package/dist/runners/security/dynamic-checker.d.ts.map +1 -0
  57. package/dist/runners/security/dynamic-checker.js +136 -0
  58. package/dist/runners/security/dynamic-checker.js.map +1 -0
  59. package/dist/runners/security/rules/auth-middleware.d.ts +13 -0
  60. package/dist/runners/security/rules/auth-middleware.d.ts.map +1 -0
  61. package/dist/runners/security/rules/auth-middleware.js +94 -0
  62. package/dist/runners/security/rules/auth-middleware.js.map +1 -0
  63. package/dist/runners/security/rules/config-audit.d.ts +14 -0
  64. package/dist/runners/security/rules/config-audit.d.ts.map +1 -0
  65. package/dist/runners/security/rules/config-audit.js +91 -0
  66. package/dist/runners/security/rules/config-audit.js.map +1 -0
  67. package/dist/runners/security/rules/dep-audit.d.ts +7 -0
  68. package/dist/runners/security/rules/dep-audit.d.ts.map +1 -0
  69. package/dist/runners/security/rules/dep-audit.js +82 -0
  70. package/dist/runners/security/rules/dep-audit.js.map +1 -0
  71. package/dist/runners/security/rules/input-sanitization.d.ts +12 -0
  72. package/dist/runners/security/rules/input-sanitization.d.ts.map +1 -0
  73. package/dist/runners/security/rules/input-sanitization.js +64 -0
  74. package/dist/runners/security/rules/input-sanitization.js.map +1 -0
  75. package/dist/runners/security/rules/rate-limit-audit.d.ts +11 -0
  76. package/dist/runners/security/rules/rate-limit-audit.d.ts.map +1 -0
  77. package/dist/runners/security/rules/rate-limit-audit.js +51 -0
  78. package/dist/runners/security/rules/rate-limit-audit.js.map +1 -0
  79. package/dist/runners/security/rules/secrets-scan.d.ts +4 -0
  80. package/dist/runners/security/rules/secrets-scan.d.ts.map +1 -0
  81. package/dist/runners/security/rules/secrets-scan.js +129 -0
  82. package/dist/runners/security/rules/secrets-scan.js.map +1 -0
  83. package/dist/runners/security/rules/xss-vectors.d.ts +13 -0
  84. package/dist/runners/security/rules/xss-vectors.d.ts.map +1 -0
  85. package/dist/runners/security/rules/xss-vectors.js +76 -0
  86. package/dist/runners/security/rules/xss-vectors.js.map +1 -0
  87. package/dist/runners/security/static-analyzer.d.ts +7 -0
  88. package/dist/runners/security/static-analyzer.d.ts.map +1 -0
  89. package/dist/runners/security/static-analyzer.js +87 -0
  90. package/dist/runners/security/static-analyzer.js.map +1 -0
  91. package/dist/runners/unit/unit-runner.d.ts +3 -0
  92. package/dist/runners/unit/unit-runner.d.ts.map +1 -0
  93. package/dist/runners/unit/unit-runner.js +157 -0
  94. package/dist/runners/unit/unit-runner.js.map +1 -0
  95. package/dist/types.d.ts +153 -0
  96. package/dist/types.d.ts.map +1 -0
  97. package/dist/types.js +2 -0
  98. package/dist/types.js.map +1 -0
  99. package/dist/util/glob.d.ts +2 -0
  100. package/dist/util/glob.d.ts.map +1 -0
  101. package/dist/util/glob.js +32 -0
  102. package/dist/util/glob.js.map +1 -0
  103. package/package.json +68 -0
package/LICENSE ADDED
@@ -0,0 +1,40 @@
1
+ QASS — Proprietary Software License
2
+
3
+ Copyright (c) 2026 Dujaun Paul. All rights reserved.
4
+
5
+ This software and its source code are the proprietary property of Dujaun Paul
6
+ ("Licensor"). By installing, downloading, or using this software ("QASS"), you
7
+ agree to the following terms:
8
+
9
+ 1. GRANT OF USE
10
+ You are granted a non-exclusive, non-transferable, revocable license to use
11
+ QASS in accordance with the plan you have purchased (Free, Pro, or Team).
12
+
13
+ 2. RESTRICTIONS
14
+ You may NOT:
15
+ - Copy, modify, merge, or create derivative works of this software
16
+ - Distribute, sublicense, sell, or make this software available to third
17
+ parties
18
+ - Reverse engineer, decompile, or disassemble this software
19
+ - Remove or alter any proprietary notices or labels
20
+
21
+ 3. FREE TIER
22
+ The Free tier grants usage of the core security scanner and basic smoke
23
+ testing features at no cost, subject to these license terms. Free tier
24
+ usage does not grant any rights to the source code.
25
+
26
+ 4. PAID TIERS
27
+ Pro and Team features require a valid license key. Usage without a valid
28
+ license key constitutes a violation of this agreement.
29
+
30
+ 5. NO WARRANTY
31
+ THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32
+ IMPLIED. THE LICENSOR SHALL NOT BE LIABLE FOR ANY CLAIMS, DAMAGES, OR OTHER
33
+ LIABILITY ARISING FROM THE USE OF THIS SOFTWARE.
34
+
35
+ 6. TERMINATION
36
+ This license is effective until terminated. It terminates automatically if
37
+ you fail to comply with any term. Upon termination, you must destroy all
38
+ copies of this software in your possession.
39
+
40
+ For licensing inquiries: dujaun@qass.dev
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # QASS
2
+
3
+ **QA + Security Scanner for vibe-coded apps.**
4
+
5
+ Your AI writes code. QASS catches the security holes, broken flows, and silent failures it left behind — before your users do. Works with Cursor, Windsurf, Copilot, and any AI editor.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g qass
11
+ ```
12
+
13
+ Or run without installing:
14
+
15
+ ```bash
16
+ npx qass scan --project .
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```bash
22
+ # Initialize config in your project
23
+ qass init --project .
24
+
25
+ # Run a full security scan
26
+ qass scan --project . --full
27
+
28
+ # Run tests based on your latest git changes
29
+ qass test --project . --diff HEAD
30
+ ```
31
+
32
+ ## What It Catches
33
+
34
+ ### Free
35
+
36
+ - **7 static security rules** — missing auth middleware, SQL/NoSQL injection, hardcoded secrets, XSS vectors, CORS misconfiguration, rate limiting gaps, dependency CVEs
37
+ - **Basic smoke crawl** — page load verification, console error detection
38
+ - **Endpoint discovery** — auto-detects Express routes
39
+ - **Git diff analysis** — only scans what changed
40
+ - **AI-readable reports** — structured for your AI editor to read and fix
41
+
42
+ ### Pro
43
+
44
+ - **Full smoke crawl** — clicks every button, fills every form, catches silent failures
45
+ - **Visual regression** — pixel-diff screenshots against baselines
46
+ - **Flow testing** — multi-step user journeys defined in YAML
47
+ - **API testing** — auth, plan gating, response validation with Supabase support
48
+ - **Dynamic security probing** — tests live endpoints for error disclosure, missing headers
49
+
50
+ ## How It Works With AI Editors
51
+
52
+ QASS generates a rule file that tells your AI editor to run tests after every code change:
53
+
54
+ ```bash
55
+ # Generate a Cursor Rule
56
+ qass cursor-rule --project .
57
+
58
+ # Creates .cursor/rules/qass.mdc
59
+ ```
60
+
61
+ The rule instructs your AI to:
62
+
63
+ 1. Run `qass test` after making changes
64
+ 2. Read the report at `.qass/results/latest.md`
65
+ 3. Fix every finding (each has exact file, line, and fix instructions)
66
+ 4. Re-run until clean
67
+ 5. Only then tell you it's done
68
+
69
+ This works with any AI editor that can run terminal commands — Cursor, Windsurf, Copilot, Bolt, Lovable.
70
+
71
+ ## Configuration
72
+
73
+ QASS uses a `.qass/config.yaml` file in your project root:
74
+
75
+ ```yaml
76
+ project:
77
+ name: my-app
78
+
79
+ services:
80
+ api:
81
+ framework: express
82
+ entry: src/server.ts
83
+ port: 3001
84
+ frontend:
85
+ framework: nextjs
86
+ port: 3000
87
+
88
+ security:
89
+ static_rules:
90
+ - auth-middleware
91
+ - input-sanitization
92
+ - secrets-scan
93
+ - xss-vectors
94
+ - config-audit
95
+ - rate-limit-audit
96
+ - dep-audit
97
+ severity_threshold: LOW
98
+
99
+ paths:
100
+ api_routes: "src/**/*.routes.ts"
101
+ middleware: "src/middleware/**"
102
+ frontend_pages: "app/**/page.tsx"
103
+ components: "components/**/*.tsx"
104
+ ```
105
+
106
+ Run `qass init` to generate a default config.
107
+
108
+ ## CLI Commands
109
+
110
+ | Command | Description |
111
+ |---------|-------------|
112
+ | `qass init` | Initialize `.qass/config.yaml` in your project |
113
+ | `qass scan` | Run security scan only |
114
+ | `qass test` | Run full test suite (security + API + E2E + unit) |
115
+ | `qass discover` | List discovered endpoints, pages, and accounts |
116
+ | `qass cursor-rule` | Generate AI editor rule file |
117
+ | `qass activate <key>` | Activate a Pro/Team license |
118
+ | `qass status` | Show current license and plan info |
119
+
120
+ ## Reports
121
+
122
+ QASS generates reports in two formats:
123
+
124
+ - **`.qass/results/latest.json`** — machine-readable, for programmatic use
125
+ - **`.qass/results/latest.md`** — human/AI-readable, with fix instructions
126
+
127
+ Each finding includes:
128
+
129
+ ```markdown
130
+ #### MEDIUM: input-sanitization — routes/contacts.ts:6
131
+ **Issue**: Unsanitized user input passed to .filter()
132
+ **Fix**: Use a sanitization function: const q = sanitize(req.query.q);
133
+ ```
134
+
135
+ ## Requirements
136
+
137
+ - Node.js >= 20.11.0
138
+ - Git (for diff analysis)
139
+ - Playwright (optional, for E2E testing): `npm i -D playwright`
140
+ - Vitest (optional, for unit test generation): `npm i -D vitest`
141
+
142
+ ## License
143
+
144
+ Proprietary. See [LICENSE](./LICENSE) for details.
145
+
146
+ Free tier available. Pro and Team require a license key — see [qass.dev](https://qass.dev) for pricing.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import chalk from "chalk";
4
+ import { loadConfig, initConfig } from "./core/config.js";
5
+ const program = new Command();
6
+ program
7
+ .name("qass")
8
+ .description("QA + Security Scanner for vibe-coded apps. Ship fast. Ship safe.")
9
+ .version("0.1.0");
10
+ program
11
+ .command("init")
12
+ .description("Initialize QASS config in a project")
13
+ .option("-p, --project <path>", "Project root path", ".")
14
+ .action(async (opts) => {
15
+ try {
16
+ const configPath = await initConfig(opts.project);
17
+ console.log(chalk.green("✓") + ` Config created at ${configPath}`);
18
+ console.log(chalk.dim(" Edit .qass/config.yaml to configure your project."));
19
+ }
20
+ catch (e) {
21
+ console.error(chalk.red("✗"), e instanceof Error ? e.message : "Failed to initialize");
22
+ process.exit(1);
23
+ }
24
+ });
25
+ program
26
+ .command("test")
27
+ .description("Run tests based on git diff")
28
+ .option("-p, --project <path>", "Project root path", ".")
29
+ .option("-d, --diff <ref>", "Git diff reference", "HEAD")
30
+ .option("-t, --type <type>", "Test type: api, e2e, unit, security, smoke, all", "all")
31
+ .action(async (opts) => {
32
+ try {
33
+ const config = await loadConfig(opts.project);
34
+ console.log(chalk.blue("QASS") +
35
+ chalk.dim(` testing ${config.project.name}...`));
36
+ const { runTests } = await import("./core/runner.js");
37
+ await runTests(config, opts.project, opts.diff, opts.type);
38
+ }
39
+ catch (e) {
40
+ console.error(chalk.red("✗"), e instanceof Error ? e.message : "Test run failed");
41
+ process.exit(1);
42
+ }
43
+ });
44
+ program
45
+ .command("scan")
46
+ .description("Run security scan only")
47
+ .option("-p, --project <path>", "Project root path", ".")
48
+ .option("-d, --diff <ref>", "Git diff reference", "HEAD")
49
+ .option("--full", "Scan all files, not just changed ones", false)
50
+ .action(async (opts) => {
51
+ try {
52
+ const config = await loadConfig(opts.project);
53
+ console.log(chalk.blue("QASS") +
54
+ chalk.dim(` scanning ${config.project.name}...`));
55
+ const { runTests } = await import("./core/runner.js");
56
+ await runTests(config, opts.project, opts.diff, "security", opts.full);
57
+ }
58
+ catch (e) {
59
+ console.error(chalk.red("✗"), e instanceof Error ? e.message : "Scan failed");
60
+ process.exit(1);
61
+ }
62
+ });
63
+ program
64
+ .command("discover")
65
+ .description("List discovered endpoints and pages")
66
+ .option("-p, --project <path>", "Project root path", ".")
67
+ .action(async (opts) => {
68
+ try {
69
+ const config = await loadConfig(opts.project);
70
+ const { discover } = await import("./core/discover.js");
71
+ await discover(config, opts.project);
72
+ }
73
+ catch (e) {
74
+ console.error(chalk.red("✗"), e instanceof Error ? e.message : "Discovery failed");
75
+ process.exit(1);
76
+ }
77
+ });
78
+ program
79
+ .command("cursor-rule")
80
+ .description("Generate Cursor Rule for this project")
81
+ .option("-p, --project <path>", "Project root path", ".")
82
+ .action(async (opts) => {
83
+ try {
84
+ const { generateCursorRule } = await import("./integrations/cursor-rule.js");
85
+ const rulePath = await generateCursorRule(opts.project);
86
+ console.log(chalk.green("✓") + ` Cursor rule created at ${rulePath}`);
87
+ }
88
+ catch (e) {
89
+ console.error(chalk.red("✗"), e instanceof Error ? e.message : "Failed to generate rule");
90
+ process.exit(1);
91
+ }
92
+ });
93
+ program
94
+ .command("activate <key>")
95
+ .description("Activate a Pro or Team license key")
96
+ .action(async (key) => {
97
+ const { activateLicense } = await import("./core/license.js");
98
+ const ok = await activateLicense(key);
99
+ if (!ok)
100
+ process.exit(1);
101
+ });
102
+ program
103
+ .command("deactivate")
104
+ .description("Remove license from this machine")
105
+ .action(async () => {
106
+ const { deactivateLicense } = await import("./core/license.js");
107
+ await deactivateLicense();
108
+ });
109
+ program
110
+ .command("status")
111
+ .description("Show current license and plan info")
112
+ .action(async () => {
113
+ const { showStatus } = await import("./core/license.js");
114
+ await showStatus();
115
+ });
116
+ program.parse();
117
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { QassConfig } from "../types.js";
2
+ export declare function loadConfig(projectPath: string): Promise<QassConfig>;
3
+ export declare function initConfig(projectPath: string): Promise<string>;
4
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAK9C,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAiBzE;AAiBD,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAmCrE"}
@@ -0,0 +1,128 @@
1
+ import { readFile, access, copyFile, mkdir } from "node:fs/promises";
2
+ import { resolve, join } from "node:path";
3
+ import { parse as parseYaml } from "yaml";
4
+ const CONFIG_DIR = ".qass";
5
+ const CONFIG_FILE = "config.yaml";
6
+ export async function loadConfig(projectPath) {
7
+ const configPath = resolve(projectPath, CONFIG_DIR, CONFIG_FILE);
8
+ try {
9
+ await access(configPath);
10
+ }
11
+ catch {
12
+ throw new Error(`No QASS config found at ${configPath}\nRun 'qass init --project ${projectPath}' to create one.`);
13
+ }
14
+ const raw = await readFile(configPath, "utf-8");
15
+ const envResolved = resolveEnvVars(raw);
16
+ const config = parseYaml(envResolved);
17
+ validateConfig(config);
18
+ return config;
19
+ }
20
+ function resolveEnvVars(content) {
21
+ return content.replace(/\$\{(\w+)\}/g, (_, name) => {
22
+ return process.env[name] ?? "";
23
+ });
24
+ }
25
+ function validateConfig(config) {
26
+ if (!config.project?.name) {
27
+ throw new Error("Config missing required field: project.name");
28
+ }
29
+ if (!config.project?.root) {
30
+ throw new Error("Config missing required field: project.root");
31
+ }
32
+ }
33
+ export async function initConfig(projectPath) {
34
+ const targetDir = resolve(projectPath, CONFIG_DIR);
35
+ const targetFile = join(targetDir, CONFIG_FILE);
36
+ try {
37
+ await access(targetFile);
38
+ throw new Error(`Config already exists at ${targetFile}`);
39
+ }
40
+ catch (e) {
41
+ if (e instanceof Error && e.message.startsWith("Config already exists")) {
42
+ throw e;
43
+ }
44
+ }
45
+ await mkdir(targetDir, { recursive: true });
46
+ const templatePath = resolve(import.meta.dirname, "..", "templates", "config.example.yaml");
47
+ try {
48
+ await access(templatePath);
49
+ await copyFile(templatePath, targetFile);
50
+ }
51
+ catch {
52
+ const defaultConfig = generateDefaultConfig(projectPath);
53
+ const { writeFile } = await import("node:fs/promises");
54
+ await writeFile(targetFile, defaultConfig, "utf-8");
55
+ }
56
+ await mkdir(join(targetDir, "results"), { recursive: true });
57
+ await mkdir(join(targetDir, "baselines"), { recursive: true });
58
+ return targetFile;
59
+ }
60
+ function generateDefaultConfig(projectPath) {
61
+ const name = projectPath.split(/[\\/]/).pop() ?? "my-project";
62
+ return `project:
63
+ name: ${name}
64
+ root: .
65
+
66
+ # services:
67
+ # api:
68
+ # start: "npm run dev"
69
+ # port: 3000
70
+ # health: "http://localhost:3000/health"
71
+ # frontend:
72
+ # start: "npm run dev"
73
+ # port: 3001
74
+ # url: "http://localhost:3001"
75
+
76
+ # auth:
77
+ # provider: supabase
78
+ # supabase_url: \${NEXT_PUBLIC_SUPABASE_URL}
79
+ # supabase_anon_key: \${NEXT_PUBLIC_SUPABASE_ANON_KEY}
80
+
81
+ # test_accounts:
82
+ # - email: test@example.com
83
+ # password: TestPass123!
84
+ # role: default
85
+
86
+ paths:
87
+ api_routes: "src/**/*.routes.ts"
88
+ frontend_pages: "app/**/page.tsx"
89
+ components: "components/**/*.tsx"
90
+
91
+ # route_mounting:
92
+ # example.routes.ts: /api/v1/example
93
+
94
+ # feature_matrix:
95
+ # feature_name: [role1, role2]
96
+
97
+ security:
98
+ enabled: true
99
+ severity_threshold: LOW
100
+ static_rules:
101
+ - auth-middleware
102
+ - input-sanitization
103
+ - secrets-scan
104
+ - xss-vectors
105
+ - config-audit
106
+ - rate-limit-audit
107
+ - dep-audit
108
+ dynamic_checks: true
109
+
110
+ e2e:
111
+ viewports:
112
+ - { width: 1280, height: 720, name: desktop }
113
+ - { width: 375, height: 812, name: mobile }
114
+ smoke_crawl: true
115
+ visual_regression: true
116
+ visual_threshold: 0.01
117
+ stuck_timeout: 10000
118
+ slow_response: 3000
119
+
120
+ # flows:
121
+ # example_flow:
122
+ # as: default
123
+ # steps:
124
+ # - goto: /
125
+ # - assert_visible: "h1"
126
+ `;
127
+ }
128
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { QassConfig, DiffAnalysis } from "../types.js";
2
+ export declare function analyzeDiff(projectPath: string, diffRef: string, config: QassConfig): Promise<DiffAnalysis>;
3
+ //# sourceMappingURL=diff-analyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-analyzer.d.ts","sourceRoot":"","sources":["../../src/core/diff-analyzer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EAGb,MAAM,aAAa,CAAC;AAsCrB,wBAAsB,WAAW,CAC/B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,CAAC,CAevB"}
@@ -0,0 +1,194 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { minimatch } from "minimatch";
4
+ const exec = promisify(execFile);
5
+ const FEATURE_KEYWORDS = {
6
+ logo_upload: ["logo", "logoUpload", "LogoUpload", "logo_upload"],
7
+ custom_branding: [
8
+ "branding",
9
+ "customBranding",
10
+ "brand_color",
11
+ "brandColor",
12
+ "display_name",
13
+ ],
14
+ route_optimization: [
15
+ "routeOptimiz",
16
+ "optimize-route",
17
+ "optimizeRoute",
18
+ "route_optimization",
19
+ ],
20
+ fleet_management: [
21
+ "fleet",
22
+ "Fleet",
23
+ "requireFleetOwner",
24
+ "fleetOperator",
25
+ "fleet_operators",
26
+ ],
27
+ analytics: ["analytics", "Analytics", "analyticsRoutes"],
28
+ api_access: ["apiAccess", "api_access"],
29
+ yaady_bot: ["yaady", "Yaady", "whatsapp", "WhatsApp", "yaadyBot"],
30
+ auth: ["auth", "signIn", "signUp", "requireCourier", "requireAdmin"],
31
+ plan_gating: [
32
+ "planGate",
33
+ "requireActiveCourierSubscription",
34
+ "SUBSCRIPTION_REQUIRED",
35
+ "PLAN_UPGRADE_REQUIRED",
36
+ ],
37
+ };
38
+ export async function analyzeDiff(projectPath, diffRef, config) {
39
+ const changedFiles = await getChangedFiles(projectPath, diffRef);
40
+ const categorized = categorizeFiles(changedFiles, config);
41
+ const affectedFeatures = detectAffectedFeatures(categorized, config);
42
+ const affectedRoles = detectAffectedRoles(affectedFeatures, config);
43
+ const changeCategories = [
44
+ ...new Set(categorized.map((f) => f.category)),
45
+ ];
46
+ return {
47
+ changedFiles: categorized,
48
+ affectedFeatures,
49
+ affectedRoles,
50
+ changeCategories,
51
+ };
52
+ }
53
+ async function getChangedFiles(projectPath, diffRef) {
54
+ const files = [];
55
+ try {
56
+ const { stdout: nameStatus } = await exec("git", ["diff", "--name-status", diffRef], { cwd: projectPath });
57
+ for (const line of nameStatus.trim().split("\n")) {
58
+ if (!line)
59
+ continue;
60
+ const [status, ...pathParts] = line.split("\t");
61
+ const filePath = pathParts.join("\t");
62
+ if (!filePath)
63
+ continue;
64
+ files.push({
65
+ path: filePath,
66
+ status: status.startsWith("R") ? "R" : status,
67
+ });
68
+ }
69
+ }
70
+ catch {
71
+ try {
72
+ const { stdout: nameStatus } = await exec("git", ["diff", "--name-status", "--staged"], { cwd: projectPath });
73
+ for (const line of nameStatus.trim().split("\n")) {
74
+ if (!line)
75
+ continue;
76
+ const [status, ...pathParts] = line.split("\t");
77
+ const filePath = pathParts.join("\t");
78
+ if (!filePath)
79
+ continue;
80
+ files.push({
81
+ path: filePath,
82
+ status: status.startsWith("R") ? "R" : status,
83
+ });
84
+ }
85
+ }
86
+ catch {
87
+ return [];
88
+ }
89
+ }
90
+ for (const file of files) {
91
+ if (file.status === "D")
92
+ continue;
93
+ try {
94
+ const { stdout: diff } = await exec("git", ["diff", diffRef, "--", file.path], { cwd: projectPath });
95
+ file.diff = diff;
96
+ }
97
+ catch {
98
+ // no diff available
99
+ }
100
+ }
101
+ return files;
102
+ }
103
+ function categorizeFiles(files, config) {
104
+ const paths = config.paths ?? {};
105
+ return files.map((file) => {
106
+ let category = "other";
107
+ for (const [key, pattern] of Object.entries(paths)) {
108
+ if (minimatch(file.path, pattern)) {
109
+ category = mapKeyToCategory(key);
110
+ break;
111
+ }
112
+ }
113
+ if (category === "other") {
114
+ category = inferCategory(file.path);
115
+ }
116
+ const statusMap = {
117
+ A: "added",
118
+ M: "modified",
119
+ D: "deleted",
120
+ R: "renamed",
121
+ };
122
+ return {
123
+ path: file.path,
124
+ status: statusMap[file.status] ?? "modified",
125
+ category,
126
+ diff: file.diff,
127
+ };
128
+ });
129
+ }
130
+ function mapKeyToCategory(key) {
131
+ const mapping = {
132
+ api_routes: "api_route",
133
+ api_app: "config",
134
+ frontend_pages: "frontend_page",
135
+ components: "component",
136
+ shared_lib: "shared_lib",
137
+ middleware: "middleware",
138
+ };
139
+ return mapping[key] ?? "other";
140
+ }
141
+ function inferCategory(filePath) {
142
+ if (/\.routes\.(ts|js)$/.test(filePath))
143
+ return "api_route";
144
+ if (/middleware/i.test(filePath))
145
+ return "middleware";
146
+ if (/page\.(tsx|jsx)$/.test(filePath))
147
+ return "frontend_page";
148
+ if (/components?\//i.test(filePath))
149
+ return "component";
150
+ if (/migrations?\//i.test(filePath))
151
+ return "migration";
152
+ if (/package\.json$|tsconfig|\.env|config\.(ts|js|yaml|yml)$/.test(filePath))
153
+ return "config";
154
+ if (/shared|packages\//i.test(filePath))
155
+ return "shared_lib";
156
+ return "other";
157
+ }
158
+ function detectAffectedFeatures(files, _config) {
159
+ const features = new Set();
160
+ const allDiffs = files.map((f) => f.diff ?? "").join("\n");
161
+ for (const [feature, keywords] of Object.entries(FEATURE_KEYWORDS)) {
162
+ if (keywords.some((kw) => allDiffs.includes(kw))) {
163
+ features.add(feature);
164
+ }
165
+ }
166
+ for (const file of files) {
167
+ if (file.path.includes("fleet"))
168
+ features.add("fleet_management");
169
+ if (file.path.includes("analytics"))
170
+ features.add("analytics");
171
+ if (file.path.includes("yaady"))
172
+ features.add("yaady_bot");
173
+ if (file.path.includes("auth"))
174
+ features.add("auth");
175
+ if (file.path.includes("subscription"))
176
+ features.add("plan_gating");
177
+ }
178
+ return [...features];
179
+ }
180
+ function detectAffectedRoles(features, config) {
181
+ const roles = new Set();
182
+ const matrix = config.feature_matrix ?? {};
183
+ for (const feature of features) {
184
+ const featureRoles = matrix[feature];
185
+ if (featureRoles) {
186
+ featureRoles.forEach((r) => roles.add(r));
187
+ }
188
+ }
189
+ if (roles.size === 0 && config.test_accounts) {
190
+ config.test_accounts.forEach((a) => roles.add(a.role));
191
+ }
192
+ return [...roles];
193
+ }
194
+ //# sourceMappingURL=diff-analyzer.js.map