@democratize-quality/qualitylens-core 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 (85) hide show
  1. package/dist/core/config.d.mts +26 -0
  2. package/dist/core/config.d.ts +26 -0
  3. package/dist/core/config.js +139 -0
  4. package/dist/core/config.js.map +1 -0
  5. package/dist/core/config.mjs +104 -0
  6. package/dist/core/config.mjs.map +1 -0
  7. package/dist/core/detector.d.mts +43 -0
  8. package/dist/core/detector.d.ts +43 -0
  9. package/dist/core/detector.js +431 -0
  10. package/dist/core/detector.js.map +1 -0
  11. package/dist/core/detector.mjs +395 -0
  12. package/dist/core/detector.mjs.map +1 -0
  13. package/dist/core/engine.d.mts +29 -0
  14. package/dist/core/engine.d.ts +29 -0
  15. package/dist/core/engine.js +151 -0
  16. package/dist/core/engine.js.map +1 -0
  17. package/dist/core/engine.mjs +126 -0
  18. package/dist/core/engine.mjs.map +1 -0
  19. package/dist/core/types.d.mts +109 -0
  20. package/dist/core/types.d.ts +109 -0
  21. package/dist/core/types.js +19 -0
  22. package/dist/core/types.js.map +1 -0
  23. package/dist/core/types.mjs +1 -0
  24. package/dist/core/types.mjs.map +1 -0
  25. package/dist/matchers/area.matcher.d.mts +38 -0
  26. package/dist/matchers/area.matcher.d.ts +38 -0
  27. package/dist/matchers/area.matcher.js +102 -0
  28. package/dist/matchers/area.matcher.js.map +1 -0
  29. package/dist/matchers/area.matcher.mjs +77 -0
  30. package/dist/matchers/area.matcher.mjs.map +1 -0
  31. package/dist/matchers/fuzzy.matcher.d.mts +83 -0
  32. package/dist/matchers/fuzzy.matcher.d.ts +83 -0
  33. package/dist/matchers/fuzzy.matcher.js +161 -0
  34. package/dist/matchers/fuzzy.matcher.js.map +1 -0
  35. package/dist/matchers/fuzzy.matcher.mjs +136 -0
  36. package/dist/matchers/fuzzy.matcher.mjs.map +1 -0
  37. package/dist/reporters/base.reporter.d.mts +19 -0
  38. package/dist/reporters/base.reporter.d.ts +19 -0
  39. package/dist/reporters/base.reporter.js +32 -0
  40. package/dist/reporters/base.reporter.js.map +1 -0
  41. package/dist/reporters/base.reporter.mjs +7 -0
  42. package/dist/reporters/base.reporter.mjs.map +1 -0
  43. package/dist/reporters/console.reporter.d.mts +16 -0
  44. package/dist/reporters/console.reporter.d.ts +16 -0
  45. package/dist/reporters/console.reporter.js +130 -0
  46. package/dist/reporters/console.reporter.js.map +1 -0
  47. package/dist/reporters/console.reporter.mjs +95 -0
  48. package/dist/reporters/console.reporter.mjs.map +1 -0
  49. package/dist/sources/base.source.d.mts +22 -0
  50. package/dist/sources/base.source.d.ts +22 -0
  51. package/dist/sources/base.source.js +130 -0
  52. package/dist/sources/base.source.js.map +1 -0
  53. package/dist/sources/base.source.mjs +93 -0
  54. package/dist/sources/base.source.mjs.map +1 -0
  55. package/dist/sources/playwright.source.d.mts +34 -0
  56. package/dist/sources/playwright.source.d.ts +34 -0
  57. package/dist/sources/playwright.source.js +209 -0
  58. package/dist/sources/playwright.source.js.map +1 -0
  59. package/dist/sources/playwright.source.mjs +172 -0
  60. package/dist/sources/playwright.source.mjs.map +1 -0
  61. package/dist/sources/routes.source.d.mts +58 -0
  62. package/dist/sources/routes.source.d.ts +58 -0
  63. package/dist/sources/routes.source.js +288 -0
  64. package/dist/sources/routes.source.js.map +1 -0
  65. package/dist/sources/routes.source.mjs +251 -0
  66. package/dist/sources/routes.source.mjs.map +1 -0
  67. package/dist/sources/yaml.source.d.mts +16 -0
  68. package/dist/sources/yaml.source.d.ts +16 -0
  69. package/dist/sources/yaml.source.js +160 -0
  70. package/dist/sources/yaml.source.js.map +1 -0
  71. package/dist/sources/yaml.source.mjs +123 -0
  72. package/dist/sources/yaml.source.mjs.map +1 -0
  73. package/dist/utils/http.d.mts +7 -0
  74. package/dist/utils/http.d.ts +7 -0
  75. package/dist/utils/http.js +37 -0
  76. package/dist/utils/http.js.map +1 -0
  77. package/dist/utils/http.mjs +12 -0
  78. package/dist/utils/http.mjs.map +1 -0
  79. package/dist/utils/logger.d.mts +35 -0
  80. package/dist/utils/logger.d.ts +35 -0
  81. package/dist/utils/logger.js +114 -0
  82. package/dist/utils/logger.js.map +1 -0
  83. package/dist/utils/logger.mjs +79 -0
  84. package/dist/utils/logger.mjs.map +1 -0
  85. package/package.json +115 -0
@@ -0,0 +1,34 @@
1
+ import { BaseSource } from './base.source.mjs';
2
+ import { TestEntry } from '../core/types.mjs';
3
+
4
+ /**
5
+ * src/sources/playwright.source.ts
6
+ *
7
+ * Reads Playwright test files and extracts test titles via AST parsing.
8
+ * Uses @typescript-eslint/parser — NOT regex — for reliability.
9
+ *
10
+ * AI INSTRUCTIONS (Claude Code / Copilot):
11
+ * Implement the collect() method using the skeleton and comments below.
12
+ * Do not change the class signature or constructor shape.
13
+ */
14
+
15
+ declare class PlaywrightSource extends BaseSource {
16
+ private rootDir;
17
+ readonly name = "playwright";
18
+ constructor(rootDir: string);
19
+ collect(): Promise<TestEntry[]>;
20
+ /**
21
+ * AI INSTRUCTIONS:
22
+ * Parse the source string using @typescript-eslint/parser.
23
+ * Walk the AST recursively (write a helper walkNode(node, visitor)).
24
+ * Find CallExpression nodes where:
25
+ * - callee.type === 'Identifier' && callee.name === 'test' or 'it'
26
+ * - OR callee.type === 'MemberExpression' && callee.property.name === 'only' or 'skip'
27
+ * Extract the first argument as a string literal title.
28
+ * Return one TestEntry per title found.
29
+ */
30
+ private extractTestTitles;
31
+ private getCalleeName;
32
+ }
33
+
34
+ export { PlaywrightSource };
@@ -0,0 +1,34 @@
1
+ import { BaseSource } from './base.source.js';
2
+ import { TestEntry } from '../core/types.js';
3
+
4
+ /**
5
+ * src/sources/playwright.source.ts
6
+ *
7
+ * Reads Playwright test files and extracts test titles via AST parsing.
8
+ * Uses @typescript-eslint/parser — NOT regex — for reliability.
9
+ *
10
+ * AI INSTRUCTIONS (Claude Code / Copilot):
11
+ * Implement the collect() method using the skeleton and comments below.
12
+ * Do not change the class signature or constructor shape.
13
+ */
14
+
15
+ declare class PlaywrightSource extends BaseSource {
16
+ private rootDir;
17
+ readonly name = "playwright";
18
+ constructor(rootDir: string);
19
+ collect(): Promise<TestEntry[]>;
20
+ /**
21
+ * AI INSTRUCTIONS:
22
+ * Parse the source string using @typescript-eslint/parser.
23
+ * Walk the AST recursively (write a helper walkNode(node, visitor)).
24
+ * Find CallExpression nodes where:
25
+ * - callee.type === 'Identifier' && callee.name === 'test' or 'it'
26
+ * - OR callee.type === 'MemberExpression' && callee.property.name === 'only' or 'skip'
27
+ * Extract the first argument as a string literal title.
28
+ * Return one TestEntry per title found.
29
+ */
30
+ private extractTestTitles;
31
+ private getCalleeName;
32
+ }
33
+
34
+ export { PlaywrightSource };
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/sources/playwright.source.ts
31
+ var playwright_source_exports = {};
32
+ __export(playwright_source_exports, {
33
+ PlaywrightSource: () => PlaywrightSource
34
+ });
35
+ module.exports = __toCommonJS(playwright_source_exports);
36
+
37
+ // src/utils/logger.ts
38
+ var fs = __toESM(require("fs"));
39
+ var path = __toESM(require("path"));
40
+ var Logger = class {
41
+ logPath = null;
42
+ fd = null;
43
+ /**
44
+ * Open (or create) qualitylens.log in the given directory.
45
+ * Appends to an existing log so multiple runs accumulate history.
46
+ * Call once at the start of each command.
47
+ */
48
+ configure(outputDir) {
49
+ try {
50
+ fs.mkdirSync(outputDir, { recursive: true });
51
+ this.logPath = path.join(outputDir, "qualitylens.log");
52
+ this.fd = fs.openSync(this.logPath, "a");
53
+ this.write("INFO", `--- qualitylens session started (pid ${process.pid}) ---`);
54
+ } catch {
55
+ this.logPath = null;
56
+ this.fd = null;
57
+ }
58
+ }
59
+ /** Path to the current log file, or null if not configured. */
60
+ get filePath() {
61
+ return this.logPath;
62
+ }
63
+ info(msg, detail) {
64
+ this.write("INFO", msg, detail);
65
+ }
66
+ warn(msg, detail) {
67
+ this.write("WARN", msg, detail);
68
+ }
69
+ /** Logs full detail (stack trace, raw error) to file only. */
70
+ error(msg, detail) {
71
+ this.write("ERROR", msg, detail);
72
+ }
73
+ debug(msg, detail) {
74
+ this.write("DEBUG", msg, detail);
75
+ }
76
+ close() {
77
+ if (this.fd !== null) {
78
+ try {
79
+ fs.closeSync(this.fd);
80
+ } catch {
81
+ }
82
+ this.fd = null;
83
+ }
84
+ }
85
+ write(level, msg, detail) {
86
+ if (this.fd === null) return;
87
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
88
+ let line = `[${ts}] ${level.padEnd(5)} ${msg}`;
89
+ if (detail !== void 0) {
90
+ if (detail instanceof Error) {
91
+ line += `
92
+ ${detail.message}`;
93
+ if (detail.stack) {
94
+ line += "\n" + detail.stack.split("\n").map((l) => " " + l).join("\n");
95
+ }
96
+ } else if (typeof detail === "string" && detail.trim()) {
97
+ line += "\n" + detail.split("\n").map((l) => " " + l).join("\n");
98
+ } else if (typeof detail === "object") {
99
+ try {
100
+ line += "\n " + JSON.stringify(detail, null, 2).replace(/\n/g, "\n ");
101
+ } catch {
102
+ }
103
+ }
104
+ }
105
+ try {
106
+ fs.writeSync(this.fd, line + "\n");
107
+ } catch {
108
+ }
109
+ }
110
+ };
111
+ var logger = new Logger();
112
+
113
+ // src/sources/base.source.ts
114
+ var BaseSource = class {
115
+ log(msg) {
116
+ process.stdout.write(` [${this.name}] ${msg}
117
+ `);
118
+ logger.info(`[${this.name}] ${msg}`);
119
+ }
120
+ warn(msg, detail) {
121
+ process.stderr.write(` [${this.name}] WARNING: ${msg}
122
+ `);
123
+ logger.warn(`[${this.name}] ${msg}`, detail);
124
+ }
125
+ };
126
+
127
+ // src/sources/playwright.source.ts
128
+ var path2 = __toESM(require("path"));
129
+ var fs2 = __toESM(require("fs"));
130
+ var import_fast_glob = __toESM(require("fast-glob"));
131
+ var import_parser = require("@typescript-eslint/parser");
132
+ var PlaywrightSource = class extends BaseSource {
133
+ constructor(rootDir) {
134
+ super();
135
+ this.rootDir = rootDir;
136
+ }
137
+ name = "playwright";
138
+ async collect() {
139
+ const entries = [];
140
+ const specFiles = await (0, import_fast_glob.default)(["**/*.spec.ts", "**/*.test.ts"], {
141
+ cwd: this.rootDir,
142
+ absolute: true,
143
+ ignore: ["**/node_modules/**", "**/dist/**"]
144
+ });
145
+ this.log(`found ${specFiles.length} spec files`);
146
+ for (const filePath of specFiles) {
147
+ try {
148
+ const source = fs2.readFileSync(filePath, "utf-8");
149
+ const titles = this.extractTestTitles(source, filePath);
150
+ entries.push(...titles);
151
+ } catch (err) {
152
+ this.warn(`skipping ${path2.basename(filePath)}: ${err.message}`);
153
+ }
154
+ }
155
+ this.log(`extracted ${entries.length} test titles`);
156
+ return entries;
157
+ }
158
+ /**
159
+ * AI INSTRUCTIONS:
160
+ * Parse the source string using @typescript-eslint/parser.
161
+ * Walk the AST recursively (write a helper walkNode(node, visitor)).
162
+ * Find CallExpression nodes where:
163
+ * - callee.type === 'Identifier' && callee.name === 'test' or 'it'
164
+ * - OR callee.type === 'MemberExpression' && callee.property.name === 'only' or 'skip'
165
+ * Extract the first argument as a string literal title.
166
+ * Return one TestEntry per title found.
167
+ */
168
+ extractTestTitles(source, filePath) {
169
+ const ast = (0, import_parser.parse)(source, { jsx: true, range: true });
170
+ const titles = [];
171
+ const walkNode = (node) => {
172
+ if (!node || typeof node !== "object") return;
173
+ if (node.type === "CallExpression") {
174
+ const calleeName = this.getCalleeName(node.callee);
175
+ if (["test", "it", "test.only", "it.only", "test.skip", "it.skip"].includes(calleeName)) {
176
+ const firstArg = node.arguments?.[0];
177
+ if (firstArg?.type === "Literal" && typeof firstArg.value === "string") {
178
+ titles.push(firstArg.value);
179
+ }
180
+ }
181
+ }
182
+ for (const key of Object.keys(node)) {
183
+ const child = node[key];
184
+ if (Array.isArray(child)) {
185
+ child.forEach(walkNode);
186
+ } else if (child && typeof child === "object" && child.type) {
187
+ walkNode(child);
188
+ }
189
+ }
190
+ };
191
+ walkNode(ast);
192
+ return titles.map((title) => ({ title, source: "playwright", filePath }));
193
+ }
194
+ // Extracts the name from a callee node.
195
+ // test(...) → callee is Identifier { name: 'test' } → 'test'
196
+ // test.only(...) → callee is MemberExpression { object: 'test', property: 'only' } → 'test.only'
197
+ getCalleeName(callee) {
198
+ if (callee?.type === "Identifier") return callee.name;
199
+ if (callee?.type === "MemberExpression") {
200
+ return `${callee.object?.name}.${callee.property?.name}`;
201
+ }
202
+ return "";
203
+ }
204
+ };
205
+ // Annotate the CommonJS export names for ESM import in node:
206
+ 0 && (module.exports = {
207
+ PlaywrightSource
208
+ });
209
+ //# sourceMappingURL=playwright.source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/sources/playwright.source.ts","../../src/utils/logger.ts","../../src/sources/base.source.ts"],"sourcesContent":["/**\n * src/sources/playwright.source.ts\n * \n * Reads Playwright test files and extracts test titles via AST parsing.\n * Uses @typescript-eslint/parser — NOT regex — for reliability.\n * \n * AI INSTRUCTIONS (Claude Code / Copilot):\n * Implement the collect() method using the skeleton and comments below.\n * Do not change the class signature or constructor shape.\n */\n\nimport { BaseSource } from './base.source'\nimport { TestEntry } from '../core/types'\nimport * as path from 'path'\nimport * as fs from 'fs'\nimport glob from 'fast-glob'\n\nimport { parse } from '@typescript-eslint/parser'\n\nexport class PlaywrightSource extends BaseSource {\n readonly name = 'playwright'\n\n constructor(private rootDir: string) {\n super()\n }\n\n async collect(): Promise<TestEntry[]> {\n const entries: TestEntry[] = []\n\n // Step 1: find all spec files\n const specFiles = await glob(['**/*.spec.ts', '**/*.test.ts'], {\n cwd: this.rootDir,\n absolute: true,\n ignore: ['**/node_modules/**', '**/dist/**'],\n })\n\n this.log(`found ${specFiles.length} spec files`)\n\n // Step 2: parse each file\n for (const filePath of specFiles) {\n try {\n const source = fs.readFileSync(filePath, 'utf-8')\n const titles = this.extractTestTitles(source, filePath)\n entries.push(...titles)\n } catch (err) {\n // gracefully skip unparseable files\n this.warn(`skipping ${path.basename(filePath)}: ${(err as Error).message}`)\n }\n }\n\n this.log(`extracted ${entries.length} test titles`)\n return entries\n }\n\n /**\n * AI INSTRUCTIONS:\n * Parse the source string using @typescript-eslint/parser.\n * Walk the AST recursively (write a helper walkNode(node, visitor)).\n * Find CallExpression nodes where:\n * - callee.type === 'Identifier' && callee.name === 'test' or 'it'\n * - OR callee.type === 'MemberExpression' && callee.property.name === 'only' or 'skip'\n * Extract the first argument as a string literal title.\n * Return one TestEntry per title found.\n */\n private extractTestTitles(source: string, filePath: string): TestEntry[] {\n // Parse the file into an AST (Abstract Syntax Tree).\n // jsx:true handles .tsx files; range:true attaches character positions (unused but harmless).\n const ast = parse(source, { jsx: true, range: true })\n const titles: string[] = []\n\n // Recursive walker — visits every node in the tree.\n // We pass a visitor function that runs on each node.\n const walkNode = (node: any) => {\n if (!node || typeof node !== 'object') return\n\n // We're looking for function calls: test('title', ...) or it('title', ...)\n if (node.type === 'CallExpression') {\n const calleeName = this.getCalleeName(node.callee)\n // Match bare calls (test, it) and scoped calls (test.only, test.skip)\n if (['test', 'it', 'test.only', 'it.only', 'test.skip', 'it.skip'].includes(calleeName)) {\n const firstArg = node.arguments?.[0]\n // Only grab plain string literals — skip template literals and variables\n if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {\n titles.push(firstArg.value)\n }\n }\n }\n\n // Walk all child properties of this node\n for (const key of Object.keys(node)) {\n const child = node[key]\n if (Array.isArray(child)) {\n child.forEach(walkNode)\n } else if (child && typeof child === 'object' && child.type) {\n walkNode(child)\n }\n }\n }\n\n walkNode(ast)\n return titles.map(title => ({ title, source: 'playwright' as const, filePath }))\n }\n\n // Extracts the name from a callee node.\n // test(...) → callee is Identifier { name: 'test' } → 'test'\n // test.only(...) → callee is MemberExpression { object: 'test', property: 'only' } → 'test.only'\n private getCalleeName(callee: any): string {\n if (callee?.type === 'Identifier') return callee.name\n if (callee?.type === 'MemberExpression') {\n return `${callee.object?.name}.${callee.property?.name}`\n }\n return ''\n }\n}\n","/**\n * src/utils/logger.ts\n *\n * Module-level singleton logger.\n * - Always writes to a log file (qualitylens.log) with timestamps and full detail.\n * - Console output is handled separately by each command — the logger only writes the file.\n *\n * Usage:\n * import { logger } from './utils/logger'\n * logger.configure('./reports') // call once at startup\n * logger.info('scan started', { config: opts.config })\n * logger.error('route discovery failed', err)\n */\n\nimport * as fs from 'fs'\nimport * as path from 'path'\n\ntype LogLevel = 'INFO' | 'WARN' | 'ERROR' | 'DEBUG'\n\nclass Logger {\n private logPath: string | null = null\n private fd: number | null = null\n\n /**\n * Open (or create) qualitylens.log in the given directory.\n * Appends to an existing log so multiple runs accumulate history.\n * Call once at the start of each command.\n */\n configure(outputDir: string): void {\n try {\n fs.mkdirSync(outputDir, { recursive: true })\n this.logPath = path.join(outputDir, 'qualitylens.log')\n this.fd = fs.openSync(this.logPath, 'a')\n this.write('INFO', `--- qualitylens session started (pid ${process.pid}) ---`)\n } catch {\n // If we can't open a log file, continue silently — logging is non-blocking\n this.logPath = null\n this.fd = null\n }\n }\n\n /** Path to the current log file, or null if not configured. */\n get filePath(): string | null {\n return this.logPath\n }\n\n info(msg: string, detail?: unknown): void {\n this.write('INFO', msg, detail)\n }\n\n warn(msg: string, detail?: unknown): void {\n this.write('WARN', msg, detail)\n }\n\n /** Logs full detail (stack trace, raw error) to file only. */\n error(msg: string, detail?: unknown): void {\n this.write('ERROR', msg, detail)\n }\n\n debug(msg: string, detail?: unknown): void {\n this.write('DEBUG', msg, detail)\n }\n\n close(): void {\n if (this.fd !== null) {\n try { fs.closeSync(this.fd) } catch { /* ignore */ }\n this.fd = null\n }\n }\n\n private write(level: LogLevel, msg: string, detail?: unknown): void {\n if (this.fd === null) return\n\n const ts = new Date().toISOString()\n let line = `[${ts}] ${level.padEnd(5)} ${msg}`\n\n if (detail !== undefined) {\n if (detail instanceof Error) {\n line += `\\n ${detail.message}`\n if (detail.stack) {\n line += '\\n' + detail.stack.split('\\n').map(l => ' ' + l).join('\\n')\n }\n } else if (typeof detail === 'string' && detail.trim()) {\n line += '\\n' + detail.split('\\n').map(l => ' ' + l).join('\\n')\n } else if (typeof detail === 'object') {\n try {\n line += '\\n ' + JSON.stringify(detail, null, 2).replace(/\\n/g, '\\n ')\n } catch { /* circular ref or similar — skip */ }\n }\n }\n\n try {\n fs.writeSync(this.fd, line + '\\n')\n } catch { /* disk full or fd closed — ignore */ }\n }\n}\n\nexport const logger = new Logger()\n","/**\n * src/sources/base.source.ts\n * \n * Abstract base class for all test sources.\n * To add a new source: extend this class, implement collect().\n * Sources are stateless — no caching, no shared state.\n */\n\nimport { TestEntry } from '../core/types'\nimport { logger } from '../utils/logger'\n\nexport abstract class BaseSource {\n abstract readonly name: string\n\n /**\n * Collect all test entries from this source.\n * Must never throw — return [] and log a warning on failure.\n */\n abstract collect(): Promise<TestEntry[]>\n\n protected log(msg: string): void {\n process.stdout.write(` [${this.name}] ${msg}\\n`)\n logger.info(`[${this.name}] ${msg}`)\n }\n\n protected warn(msg: string, detail?: unknown): void {\n process.stderr.write(` [${this.name}] WARNING: ${msg}\\n`)\n logger.warn(`[${this.name}] ${msg}`, detail)\n }\n}\n"],"mappings":"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","names":["path","fs","glob"]}
@@ -0,0 +1,172 @@
1
+ // src/utils/logger.ts
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ var Logger = class {
5
+ logPath = null;
6
+ fd = null;
7
+ /**
8
+ * Open (or create) qualitylens.log in the given directory.
9
+ * Appends to an existing log so multiple runs accumulate history.
10
+ * Call once at the start of each command.
11
+ */
12
+ configure(outputDir) {
13
+ try {
14
+ fs.mkdirSync(outputDir, { recursive: true });
15
+ this.logPath = path.join(outputDir, "qualitylens.log");
16
+ this.fd = fs.openSync(this.logPath, "a");
17
+ this.write("INFO", `--- qualitylens session started (pid ${process.pid}) ---`);
18
+ } catch {
19
+ this.logPath = null;
20
+ this.fd = null;
21
+ }
22
+ }
23
+ /** Path to the current log file, or null if not configured. */
24
+ get filePath() {
25
+ return this.logPath;
26
+ }
27
+ info(msg, detail) {
28
+ this.write("INFO", msg, detail);
29
+ }
30
+ warn(msg, detail) {
31
+ this.write("WARN", msg, detail);
32
+ }
33
+ /** Logs full detail (stack trace, raw error) to file only. */
34
+ error(msg, detail) {
35
+ this.write("ERROR", msg, detail);
36
+ }
37
+ debug(msg, detail) {
38
+ this.write("DEBUG", msg, detail);
39
+ }
40
+ close() {
41
+ if (this.fd !== null) {
42
+ try {
43
+ fs.closeSync(this.fd);
44
+ } catch {
45
+ }
46
+ this.fd = null;
47
+ }
48
+ }
49
+ write(level, msg, detail) {
50
+ if (this.fd === null) return;
51
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
52
+ let line = `[${ts}] ${level.padEnd(5)} ${msg}`;
53
+ if (detail !== void 0) {
54
+ if (detail instanceof Error) {
55
+ line += `
56
+ ${detail.message}`;
57
+ if (detail.stack) {
58
+ line += "\n" + detail.stack.split("\n").map((l) => " " + l).join("\n");
59
+ }
60
+ } else if (typeof detail === "string" && detail.trim()) {
61
+ line += "\n" + detail.split("\n").map((l) => " " + l).join("\n");
62
+ } else if (typeof detail === "object") {
63
+ try {
64
+ line += "\n " + JSON.stringify(detail, null, 2).replace(/\n/g, "\n ");
65
+ } catch {
66
+ }
67
+ }
68
+ }
69
+ try {
70
+ fs.writeSync(this.fd, line + "\n");
71
+ } catch {
72
+ }
73
+ }
74
+ };
75
+ var logger = new Logger();
76
+
77
+ // src/sources/base.source.ts
78
+ var BaseSource = class {
79
+ log(msg) {
80
+ process.stdout.write(` [${this.name}] ${msg}
81
+ `);
82
+ logger.info(`[${this.name}] ${msg}`);
83
+ }
84
+ warn(msg, detail) {
85
+ process.stderr.write(` [${this.name}] WARNING: ${msg}
86
+ `);
87
+ logger.warn(`[${this.name}] ${msg}`, detail);
88
+ }
89
+ };
90
+
91
+ // src/sources/playwright.source.ts
92
+ import * as path2 from "path";
93
+ import * as fs2 from "fs";
94
+ import glob from "fast-glob";
95
+ import { parse } from "@typescript-eslint/parser";
96
+ var PlaywrightSource = class extends BaseSource {
97
+ constructor(rootDir) {
98
+ super();
99
+ this.rootDir = rootDir;
100
+ }
101
+ name = "playwright";
102
+ async collect() {
103
+ const entries = [];
104
+ const specFiles = await glob(["**/*.spec.ts", "**/*.test.ts"], {
105
+ cwd: this.rootDir,
106
+ absolute: true,
107
+ ignore: ["**/node_modules/**", "**/dist/**"]
108
+ });
109
+ this.log(`found ${specFiles.length} spec files`);
110
+ for (const filePath of specFiles) {
111
+ try {
112
+ const source = fs2.readFileSync(filePath, "utf-8");
113
+ const titles = this.extractTestTitles(source, filePath);
114
+ entries.push(...titles);
115
+ } catch (err) {
116
+ this.warn(`skipping ${path2.basename(filePath)}: ${err.message}`);
117
+ }
118
+ }
119
+ this.log(`extracted ${entries.length} test titles`);
120
+ return entries;
121
+ }
122
+ /**
123
+ * AI INSTRUCTIONS:
124
+ * Parse the source string using @typescript-eslint/parser.
125
+ * Walk the AST recursively (write a helper walkNode(node, visitor)).
126
+ * Find CallExpression nodes where:
127
+ * - callee.type === 'Identifier' && callee.name === 'test' or 'it'
128
+ * - OR callee.type === 'MemberExpression' && callee.property.name === 'only' or 'skip'
129
+ * Extract the first argument as a string literal title.
130
+ * Return one TestEntry per title found.
131
+ */
132
+ extractTestTitles(source, filePath) {
133
+ const ast = parse(source, { jsx: true, range: true });
134
+ const titles = [];
135
+ const walkNode = (node) => {
136
+ if (!node || typeof node !== "object") return;
137
+ if (node.type === "CallExpression") {
138
+ const calleeName = this.getCalleeName(node.callee);
139
+ if (["test", "it", "test.only", "it.only", "test.skip", "it.skip"].includes(calleeName)) {
140
+ const firstArg = node.arguments?.[0];
141
+ if (firstArg?.type === "Literal" && typeof firstArg.value === "string") {
142
+ titles.push(firstArg.value);
143
+ }
144
+ }
145
+ }
146
+ for (const key of Object.keys(node)) {
147
+ const child = node[key];
148
+ if (Array.isArray(child)) {
149
+ child.forEach(walkNode);
150
+ } else if (child && typeof child === "object" && child.type) {
151
+ walkNode(child);
152
+ }
153
+ }
154
+ };
155
+ walkNode(ast);
156
+ return titles.map((title) => ({ title, source: "playwright", filePath }));
157
+ }
158
+ // Extracts the name from a callee node.
159
+ // test(...) → callee is Identifier { name: 'test' } → 'test'
160
+ // test.only(...) → callee is MemberExpression { object: 'test', property: 'only' } → 'test.only'
161
+ getCalleeName(callee) {
162
+ if (callee?.type === "Identifier") return callee.name;
163
+ if (callee?.type === "MemberExpression") {
164
+ return `${callee.object?.name}.${callee.property?.name}`;
165
+ }
166
+ return "";
167
+ }
168
+ };
169
+ export {
170
+ PlaywrightSource
171
+ };
172
+ //# sourceMappingURL=playwright.source.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/logger.ts","../../src/sources/base.source.ts","../../src/sources/playwright.source.ts"],"sourcesContent":["/**\n * src/utils/logger.ts\n *\n * Module-level singleton logger.\n * - Always writes to a log file (qualitylens.log) with timestamps and full detail.\n * - Console output is handled separately by each command — the logger only writes the file.\n *\n * Usage:\n * import { logger } from './utils/logger'\n * logger.configure('./reports') // call once at startup\n * logger.info('scan started', { config: opts.config })\n * logger.error('route discovery failed', err)\n */\n\nimport * as fs from 'fs'\nimport * as path from 'path'\n\ntype LogLevel = 'INFO' | 'WARN' | 'ERROR' | 'DEBUG'\n\nclass Logger {\n private logPath: string | null = null\n private fd: number | null = null\n\n /**\n * Open (or create) qualitylens.log in the given directory.\n * Appends to an existing log so multiple runs accumulate history.\n * Call once at the start of each command.\n */\n configure(outputDir: string): void {\n try {\n fs.mkdirSync(outputDir, { recursive: true })\n this.logPath = path.join(outputDir, 'qualitylens.log')\n this.fd = fs.openSync(this.logPath, 'a')\n this.write('INFO', `--- qualitylens session started (pid ${process.pid}) ---`)\n } catch {\n // If we can't open a log file, continue silently — logging is non-blocking\n this.logPath = null\n this.fd = null\n }\n }\n\n /** Path to the current log file, or null if not configured. */\n get filePath(): string | null {\n return this.logPath\n }\n\n info(msg: string, detail?: unknown): void {\n this.write('INFO', msg, detail)\n }\n\n warn(msg: string, detail?: unknown): void {\n this.write('WARN', msg, detail)\n }\n\n /** Logs full detail (stack trace, raw error) to file only. */\n error(msg: string, detail?: unknown): void {\n this.write('ERROR', msg, detail)\n }\n\n debug(msg: string, detail?: unknown): void {\n this.write('DEBUG', msg, detail)\n }\n\n close(): void {\n if (this.fd !== null) {\n try { fs.closeSync(this.fd) } catch { /* ignore */ }\n this.fd = null\n }\n }\n\n private write(level: LogLevel, msg: string, detail?: unknown): void {\n if (this.fd === null) return\n\n const ts = new Date().toISOString()\n let line = `[${ts}] ${level.padEnd(5)} ${msg}`\n\n if (detail !== undefined) {\n if (detail instanceof Error) {\n line += `\\n ${detail.message}`\n if (detail.stack) {\n line += '\\n' + detail.stack.split('\\n').map(l => ' ' + l).join('\\n')\n }\n } else if (typeof detail === 'string' && detail.trim()) {\n line += '\\n' + detail.split('\\n').map(l => ' ' + l).join('\\n')\n } else if (typeof detail === 'object') {\n try {\n line += '\\n ' + JSON.stringify(detail, null, 2).replace(/\\n/g, '\\n ')\n } catch { /* circular ref or similar — skip */ }\n }\n }\n\n try {\n fs.writeSync(this.fd, line + '\\n')\n } catch { /* disk full or fd closed — ignore */ }\n }\n}\n\nexport const logger = new Logger()\n","/**\n * src/sources/base.source.ts\n * \n * Abstract base class for all test sources.\n * To add a new source: extend this class, implement collect().\n * Sources are stateless — no caching, no shared state.\n */\n\nimport { TestEntry } from '../core/types'\nimport { logger } from '../utils/logger'\n\nexport abstract class BaseSource {\n abstract readonly name: string\n\n /**\n * Collect all test entries from this source.\n * Must never throw — return [] and log a warning on failure.\n */\n abstract collect(): Promise<TestEntry[]>\n\n protected log(msg: string): void {\n process.stdout.write(` [${this.name}] ${msg}\\n`)\n logger.info(`[${this.name}] ${msg}`)\n }\n\n protected warn(msg: string, detail?: unknown): void {\n process.stderr.write(` [${this.name}] WARNING: ${msg}\\n`)\n logger.warn(`[${this.name}] ${msg}`, detail)\n }\n}\n","/**\n * src/sources/playwright.source.ts\n * \n * Reads Playwright test files and extracts test titles via AST parsing.\n * Uses @typescript-eslint/parser — NOT regex — for reliability.\n * \n * AI INSTRUCTIONS (Claude Code / Copilot):\n * Implement the collect() method using the skeleton and comments below.\n * Do not change the class signature or constructor shape.\n */\n\nimport { BaseSource } from './base.source'\nimport { TestEntry } from '../core/types'\nimport * as path from 'path'\nimport * as fs from 'fs'\nimport glob from 'fast-glob'\n\nimport { parse } from '@typescript-eslint/parser'\n\nexport class PlaywrightSource extends BaseSource {\n readonly name = 'playwright'\n\n constructor(private rootDir: string) {\n super()\n }\n\n async collect(): Promise<TestEntry[]> {\n const entries: TestEntry[] = []\n\n // Step 1: find all spec files\n const specFiles = await glob(['**/*.spec.ts', '**/*.test.ts'], {\n cwd: this.rootDir,\n absolute: true,\n ignore: ['**/node_modules/**', '**/dist/**'],\n })\n\n this.log(`found ${specFiles.length} spec files`)\n\n // Step 2: parse each file\n for (const filePath of specFiles) {\n try {\n const source = fs.readFileSync(filePath, 'utf-8')\n const titles = this.extractTestTitles(source, filePath)\n entries.push(...titles)\n } catch (err) {\n // gracefully skip unparseable files\n this.warn(`skipping ${path.basename(filePath)}: ${(err as Error).message}`)\n }\n }\n\n this.log(`extracted ${entries.length} test titles`)\n return entries\n }\n\n /**\n * AI INSTRUCTIONS:\n * Parse the source string using @typescript-eslint/parser.\n * Walk the AST recursively (write a helper walkNode(node, visitor)).\n * Find CallExpression nodes where:\n * - callee.type === 'Identifier' && callee.name === 'test' or 'it'\n * - OR callee.type === 'MemberExpression' && callee.property.name === 'only' or 'skip'\n * Extract the first argument as a string literal title.\n * Return one TestEntry per title found.\n */\n private extractTestTitles(source: string, filePath: string): TestEntry[] {\n // Parse the file into an AST (Abstract Syntax Tree).\n // jsx:true handles .tsx files; range:true attaches character positions (unused but harmless).\n const ast = parse(source, { jsx: true, range: true })\n const titles: string[] = []\n\n // Recursive walker — visits every node in the tree.\n // We pass a visitor function that runs on each node.\n const walkNode = (node: any) => {\n if (!node || typeof node !== 'object') return\n\n // We're looking for function calls: test('title', ...) or it('title', ...)\n if (node.type === 'CallExpression') {\n const calleeName = this.getCalleeName(node.callee)\n // Match bare calls (test, it) and scoped calls (test.only, test.skip)\n if (['test', 'it', 'test.only', 'it.only', 'test.skip', 'it.skip'].includes(calleeName)) {\n const firstArg = node.arguments?.[0]\n // Only grab plain string literals — skip template literals and variables\n if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {\n titles.push(firstArg.value)\n }\n }\n }\n\n // Walk all child properties of this node\n for (const key of Object.keys(node)) {\n const child = node[key]\n if (Array.isArray(child)) {\n child.forEach(walkNode)\n } else if (child && typeof child === 'object' && child.type) {\n walkNode(child)\n }\n }\n }\n\n walkNode(ast)\n return titles.map(title => ({ title, source: 'playwright' as const, filePath }))\n }\n\n // Extracts the name from a callee node.\n // test(...) → callee is Identifier { name: 'test' } → 'test'\n // test.only(...) → callee is MemberExpression { object: 'test', property: 'only' } → 'test.only'\n private getCalleeName(callee: any): string {\n if (callee?.type === 'Identifier') return callee.name\n if (callee?.type === 'MemberExpression') {\n return `${callee.object?.name}.${callee.property?.name}`\n }\n return ''\n }\n}\n"],"mappings":";AAcA,YAAY,QAAQ;AACpB,YAAY,UAAU;AAItB,IAAM,SAAN,MAAa;AAAA,EACH,UAAyB;AAAA,EACzB,KAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO5B,UAAU,WAAyB;AACjC,QAAI;AACF,MAAG,aAAU,WAAW,EAAE,WAAW,KAAK,CAAC;AAC3C,WAAK,UAAe,UAAK,WAAW,iBAAiB;AACrD,WAAK,KAAQ,YAAS,KAAK,SAAS,GAAG;AACvC,WAAK,MAAM,QAAQ,wCAAwC,QAAQ,GAAG,OAAO;AAAA,IAC/E,QAAQ;AAEN,WAAK,UAAU;AACf,WAAK,KAAK;AAAA,IACZ;AAAA,EACF;AAAA;AAAA,EAGA,IAAI,WAA0B;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,KAAK,KAAa,QAAwB;AACxC,SAAK,MAAM,QAAQ,KAAK,MAAM;AAAA,EAChC;AAAA,EAEA,KAAK,KAAa,QAAwB;AACxC,SAAK,MAAM,QAAQ,KAAK,MAAM;AAAA,EAChC;AAAA;AAAA,EAGA,MAAM,KAAa,QAAwB;AACzC,SAAK,MAAM,SAAS,KAAK,MAAM;AAAA,EACjC;AAAA,EAEA,MAAM,KAAa,QAAwB;AACzC,SAAK,MAAM,SAAS,KAAK,MAAM;AAAA,EACjC;AAAA,EAEA,QAAc;AACZ,QAAI,KAAK,OAAO,MAAM;AACpB,UAAI;AAAE,QAAG,aAAU,KAAK,EAAE;AAAA,MAAE,QAAQ;AAAA,MAAe;AACnD,WAAK,KAAK;AAAA,IACZ;AAAA,EACF;AAAA,EAEQ,MAAM,OAAiB,KAAa,QAAwB;AAClE,QAAI,KAAK,OAAO,KAAM;AAEtB,UAAM,MAAK,oBAAI,KAAK,GAAE,YAAY;AAClC,QAAI,OAAO,IAAI,EAAE,KAAK,MAAM,OAAO,CAAC,CAAC,IAAI,GAAG;AAE5C,QAAI,WAAW,QAAW;AACxB,UAAI,kBAAkB,OAAO;AAC3B,gBAAQ;AAAA,IAAO,OAAO,OAAO;AAC7B,YAAI,OAAO,OAAO;AAChB,kBAAQ,OAAO,OAAO,MAAM,MAAM,IAAI,EAAE,IAAI,OAAK,OAAO,CAAC,EAAE,KAAK,IAAI;AAAA,QACtE;AAAA,MACF,WAAW,OAAO,WAAW,YAAY,OAAO,KAAK,GAAG;AACtD,gBAAQ,OAAO,OAAO,MAAM,IAAI,EAAE,IAAI,OAAK,OAAO,CAAC,EAAE,KAAK,IAAI;AAAA,MAChE,WAAW,OAAO,WAAW,UAAU;AACrC,YAAI;AACF,kBAAQ,SAAS,KAAK,UAAU,QAAQ,MAAM,CAAC,EAAE,QAAQ,OAAO,MAAM;AAAA,QACxE,QAAQ;AAAA,QAAuC;AAAA,MACjD;AAAA,IACF;AAEA,QAAI;AACF,MAAG,aAAU,KAAK,IAAI,OAAO,IAAI;AAAA,IACnC,QAAQ;AAAA,IAAwC;AAAA,EAClD;AACF;AAEO,IAAM,SAAS,IAAI,OAAO;;;ACtF1B,IAAe,aAAf,MAA0B;AAAA,EASrB,IAAI,KAAmB;AAC/B,YAAQ,OAAO,MAAM,MAAM,KAAK,IAAI,KAAK,GAAG;AAAA,CAAI;AAChD,WAAO,KAAK,IAAI,KAAK,IAAI,KAAK,GAAG,EAAE;AAAA,EACrC;AAAA,EAEU,KAAK,KAAa,QAAwB;AAClD,YAAQ,OAAO,MAAM,MAAM,KAAK,IAAI,cAAc,GAAG;AAAA,CAAI;AACzD,WAAO,KAAK,IAAI,KAAK,IAAI,KAAK,GAAG,IAAI,MAAM;AAAA,EAC7C;AACF;;;AChBA,YAAYA,WAAU;AACtB,YAAYC,SAAQ;AACpB,OAAO,UAAU;AAEjB,SAAS,aAAa;AAEf,IAAM,mBAAN,cAA+B,WAAW;AAAA,EAG/C,YAAoB,SAAiB;AACnC,UAAM;AADY;AAAA,EAEpB;AAAA,EAJS,OAAO;AAAA,EAMhB,MAAM,UAAgC;AACpC,UAAM,UAAuB,CAAC;AAG9B,UAAM,YAAY,MAAM,KAAK,CAAC,gBAAgB,cAAc,GAAG;AAAA,MAC7D,KAAK,KAAK;AAAA,MACV,UAAU;AAAA,MACV,QAAQ,CAAC,sBAAsB,YAAY;AAAA,IAC7C,CAAC;AAED,SAAK,IAAI,SAAS,UAAU,MAAM,aAAa;AAG/C,eAAW,YAAY,WAAW;AAChC,UAAI;AACF,cAAM,SAAY,iBAAa,UAAU,OAAO;AAChD,cAAM,SAAS,KAAK,kBAAkB,QAAQ,QAAQ;AACtD,gBAAQ,KAAK,GAAG,MAAM;AAAA,MACxB,SAAS,KAAK;AAEZ,aAAK,KAAK,YAAiB,eAAS,QAAQ,CAAC,KAAM,IAAc,OAAO,EAAE;AAAA,MAC5E;AAAA,IACF;AAEA,SAAK,IAAI,aAAa,QAAQ,MAAM,cAAc;AAClD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYQ,kBAAkB,QAAgB,UAA+B;AAGvE,UAAM,MAAM,MAAM,QAAQ,EAAE,KAAK,MAAM,OAAO,KAAK,CAAC;AACpD,UAAM,SAAmB,CAAC;AAI1B,UAAM,WAAW,CAAC,SAAc;AAC9B,UAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AAGvC,UAAI,KAAK,SAAS,kBAAkB;AAClC,cAAM,aAAa,KAAK,cAAc,KAAK,MAAM;AAEjD,YAAI,CAAC,QAAQ,MAAM,aAAa,WAAW,aAAa,SAAS,EAAE,SAAS,UAAU,GAAG;AACvF,gBAAM,WAAW,KAAK,YAAY,CAAC;AAEnC,cAAI,UAAU,SAAS,aAAa,OAAO,SAAS,UAAU,UAAU;AACtE,mBAAO,KAAK,SAAS,KAAK;AAAA,UAC5B;AAAA,QACF;AAAA,MACF;AAGA,iBAAW,OAAO,OAAO,KAAK,IAAI,GAAG;AACnC,cAAM,QAAQ,KAAK,GAAG;AACtB,YAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,gBAAM,QAAQ,QAAQ;AAAA,QACxB,WAAW,SAAS,OAAO,UAAU,YAAY,MAAM,MAAM;AAC3D,mBAAS,KAAK;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAEA,aAAS,GAAG;AACZ,WAAO,OAAO,IAAI,YAAU,EAAE,OAAO,QAAQ,cAAuB,SAAS,EAAE;AAAA,EACjF;AAAA;AAAA;AAAA;AAAA,EAKQ,cAAc,QAAqB;AACzC,QAAI,QAAQ,SAAS,aAAc,QAAO,OAAO;AACjD,QAAI,QAAQ,SAAS,oBAAoB;AACvC,aAAO,GAAG,OAAO,QAAQ,IAAI,IAAI,OAAO,UAAU,IAAI;AAAA,IACxD;AACA,WAAO;AAAA,EACT;AACF;","names":["path","fs"]}
@@ -0,0 +1,58 @@
1
+ import { TestGapConfig, AppRoute } from '../core/types.mjs';
2
+
3
+ /**
4
+ * src/sources/routes.source.ts
5
+ *
6
+ * Discovers application routes from the project structure.
7
+ * Supports: Next.js pages/, Next.js app/, OpenAPI spec, Express manifest.
8
+ *
9
+ * AI INSTRUCTIONS:
10
+ * Implement each strategy as a private method.
11
+ * collect() dispatches to the right strategy based on config.type.
12
+ */
13
+
14
+ declare class RoutesSource {
15
+ private config;
16
+ constructor(config: TestGapConfig['routes']);
17
+ collect(): Promise<AppRoute[]>;
18
+ /**
19
+ * Next.js pages/ or app/ directory scanner.
20
+ *
21
+ * AI INSTRUCTIONS:
22
+ * 1. Glob all .tsx, .ts, .jsx, .js files under config.path
23
+ * 2. Convert file path to route:
24
+ * pages/checkout/payment.tsx → /checkout/payment
25
+ * pages/index.tsx → /
26
+ * pages/auth/[id].tsx → /auth/:id
27
+ * app/checkout/page.tsx → /checkout (App Router pattern)
28
+ * 3. Skip: _app, _document, _error, api/ routes, layout.tsx, loading.tsx
29
+ * 4. Return AppRoute[]
30
+ */
31
+ private collectNextJs;
32
+ /**
33
+ * OpenAPI / Swagger spec parser.
34
+ *
35
+ * AI INSTRUCTIONS:
36
+ * 1. Read the JSON or YAML file at config.path
37
+ * 2. Extract spec.paths object — keys are route paths
38
+ * 3. For each path, extract HTTP methods (get, post, put, delete, patch)
39
+ * 4. Return one AppRoute per path+method combination
40
+ * 5. Support both JSON (.json) and YAML (.yaml, .yml) specs
41
+ */
42
+ private collectOpenApi;
43
+ /**
44
+ * Express routes manifest (JSON file).
45
+ * Format: [{ path: '/checkout', method: 'GET' }, ...]
46
+ * Users generate this with express-list-routes or similar.
47
+ */
48
+ private collectExpress;
49
+ /**
50
+ * Manual route list in qualitylens.yaml.
51
+ * config.path points to a JSON file with [{ path: '/route' }]
52
+ */
53
+ private collectManual;
54
+ /** Shared reader for express and manual JSON route manifests. */
55
+ private readJsonRouteManifest;
56
+ }
57
+
58
+ export { RoutesSource };
@@ -0,0 +1,58 @@
1
+ import { TestGapConfig, AppRoute } from '../core/types.js';
2
+
3
+ /**
4
+ * src/sources/routes.source.ts
5
+ *
6
+ * Discovers application routes from the project structure.
7
+ * Supports: Next.js pages/, Next.js app/, OpenAPI spec, Express manifest.
8
+ *
9
+ * AI INSTRUCTIONS:
10
+ * Implement each strategy as a private method.
11
+ * collect() dispatches to the right strategy based on config.type.
12
+ */
13
+
14
+ declare class RoutesSource {
15
+ private config;
16
+ constructor(config: TestGapConfig['routes']);
17
+ collect(): Promise<AppRoute[]>;
18
+ /**
19
+ * Next.js pages/ or app/ directory scanner.
20
+ *
21
+ * AI INSTRUCTIONS:
22
+ * 1. Glob all .tsx, .ts, .jsx, .js files under config.path
23
+ * 2. Convert file path to route:
24
+ * pages/checkout/payment.tsx → /checkout/payment
25
+ * pages/index.tsx → /
26
+ * pages/auth/[id].tsx → /auth/:id
27
+ * app/checkout/page.tsx → /checkout (App Router pattern)
28
+ * 3. Skip: _app, _document, _error, api/ routes, layout.tsx, loading.tsx
29
+ * 4. Return AppRoute[]
30
+ */
31
+ private collectNextJs;
32
+ /**
33
+ * OpenAPI / Swagger spec parser.
34
+ *
35
+ * AI INSTRUCTIONS:
36
+ * 1. Read the JSON or YAML file at config.path
37
+ * 2. Extract spec.paths object — keys are route paths
38
+ * 3. For each path, extract HTTP methods (get, post, put, delete, patch)
39
+ * 4. Return one AppRoute per path+method combination
40
+ * 5. Support both JSON (.json) and YAML (.yaml, .yml) specs
41
+ */
42
+ private collectOpenApi;
43
+ /**
44
+ * Express routes manifest (JSON file).
45
+ * Format: [{ path: '/checkout', method: 'GET' }, ...]
46
+ * Users generate this with express-list-routes or similar.
47
+ */
48
+ private collectExpress;
49
+ /**
50
+ * Manual route list in qualitylens.yaml.
51
+ * config.path points to a JSON file with [{ path: '/route' }]
52
+ */
53
+ private collectManual;
54
+ /** Shared reader for express and manual JSON route manifests. */
55
+ private readJsonRouteManifest;
56
+ }
57
+
58
+ export { RoutesSource };