@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,288 @@
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/routes.source.ts
31
+ var routes_source_exports = {};
32
+ __export(routes_source_exports, {
33
+ RoutesSource: () => RoutesSource
34
+ });
35
+ module.exports = __toCommonJS(routes_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/routes.source.ts
114
+ var fs2 = __toESM(require("fs"));
115
+ var path2 = __toESM(require("path"));
116
+ var import_fast_glob = __toESM(require("fast-glob"));
117
+ var RoutesSource = class {
118
+ constructor(config) {
119
+ this.config = config;
120
+ }
121
+ async collect() {
122
+ const type = this.config.type;
123
+ logger.info(`[routes] collecting routes`, { type, path: this.config.path });
124
+ try {
125
+ switch (type) {
126
+ case "nextjs":
127
+ return await this.collectNextJs();
128
+ case "openapi":
129
+ return await this.collectOpenApi();
130
+ case "express":
131
+ return await this.collectExpress();
132
+ case "manual":
133
+ return await this.collectManual();
134
+ default:
135
+ throw new Error(
136
+ `Unknown routes.type "${this.config.type}" in qualitylens.yaml.
137
+ Valid values: nextjs, openapi, express, manual`
138
+ );
139
+ }
140
+ } catch (err) {
141
+ logger.error(`[routes] collection failed (type=${type}, path=${this.config.path})`, err);
142
+ throw err;
143
+ }
144
+ }
145
+ /**
146
+ * Next.js pages/ or app/ directory scanner.
147
+ *
148
+ * AI INSTRUCTIONS:
149
+ * 1. Glob all .tsx, .ts, .jsx, .js files under config.path
150
+ * 2. Convert file path to route:
151
+ * pages/checkout/payment.tsx → /checkout/payment
152
+ * pages/index.tsx → /
153
+ * pages/auth/[id].tsx → /auth/:id
154
+ * app/checkout/page.tsx → /checkout (App Router pattern)
155
+ * 3. Skip: _app, _document, _error, api/ routes, layout.tsx, loading.tsx
156
+ * 4. Return AppRoute[]
157
+ */
158
+ async collectNextJs() {
159
+ const rootPath = path2.resolve(this.config.path);
160
+ if (!fs2.existsSync(rootPath)) {
161
+ throw new Error(
162
+ `Next.js routes directory not found: ${rootPath}
163
+ Check routes.path in qualitylens.yaml \u2014 it should point to your pages/ or app/ directory.`
164
+ );
165
+ }
166
+ const appRouterPages = await (0, import_fast_glob.default)("**/page.{tsx,ts,jsx,js}", {
167
+ cwd: rootPath,
168
+ absolute: false,
169
+ ignore: ["**/node_modules/**"]
170
+ });
171
+ if (appRouterPages.length > 0) {
172
+ return appRouterPages.map((filePath) => {
173
+ const dir = filePath.replace(/\/page\.[jt]sx?$/, "").replace(/^page\.[jt]sx?$/, "");
174
+ const segments = dir.split("/").filter((seg) => !(seg.startsWith("(") && seg.endsWith(")")));
175
+ const routeSegments = segments.map(
176
+ (seg) => seg.startsWith("[") && seg.endsWith("]") ? `:${seg.slice(1, -1)}` : seg
177
+ );
178
+ const normalised = ("/" + routeSegments.join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
179
+ return { path: normalised };
180
+ });
181
+ }
182
+ const pageFiles = await (0, import_fast_glob.default)("**/*.{tsx,ts,jsx,js}", {
183
+ cwd: rootPath,
184
+ absolute: false,
185
+ ignore: ["**/node_modules/**", "**/_app.*", "**/_document.*", "**/_error.*", "**/api/**"]
186
+ });
187
+ return pageFiles.map((filePath) => {
188
+ const withoutExt = filePath.replace(/\.[jt]sx?$/, "");
189
+ const withoutIndex = withoutExt.replace(/(^|\/)index$/, "");
190
+ const segments = withoutIndex.split("/").map((seg) => seg.startsWith("[") && seg.endsWith("]") ? `:${seg.slice(1, -1)}` : seg);
191
+ return { path: ("/" + segments.join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/" };
192
+ });
193
+ }
194
+ /**
195
+ * OpenAPI / Swagger spec parser.
196
+ *
197
+ * AI INSTRUCTIONS:
198
+ * 1. Read the JSON or YAML file at config.path
199
+ * 2. Extract spec.paths object — keys are route paths
200
+ * 3. For each path, extract HTTP methods (get, post, put, delete, patch)
201
+ * 4. Return one AppRoute per path+method combination
202
+ * 5. Support both JSON (.json) and YAML (.yaml, .yml) specs
203
+ */
204
+ async collectOpenApi() {
205
+ const absPath = path2.resolve(this.config.path);
206
+ if (!fs2.existsSync(absPath)) {
207
+ throw new Error(
208
+ `OpenAPI spec not found: ${absPath}
209
+ If your spec is generated at build time, check the routes.generate command in qualitylens.yaml.
210
+ Run the generate command manually first to confirm it produces the file.`
211
+ );
212
+ }
213
+ let content;
214
+ try {
215
+ content = fs2.readFileSync(absPath, "utf-8");
216
+ } catch (err) {
217
+ throw new Error(`Could not read OpenAPI spec at ${absPath}: ${err.message}`);
218
+ }
219
+ const ext = path2.extname(this.config.path).toLowerCase();
220
+ let spec;
221
+ try {
222
+ spec = ext === ".json" ? JSON.parse(content) : (await import("js-yaml")).load(content);
223
+ } catch (err) {
224
+ throw new Error(
225
+ `Failed to parse OpenAPI spec at ${absPath} as ${ext === ".json" ? "JSON" : "YAML"}.
226
+ Parse error: ${err.message}
227
+ Check the file is valid by running: node -e "JSON.parse(require('fs').readFileSync('${absPath}','utf-8'))"`
228
+ );
229
+ }
230
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
231
+ const routes = [];
232
+ for (const [routePath, pathItem] of Object.entries(spec.paths ?? {})) {
233
+ const normalisedPath = routePath.replace(/\{(\w+)\}/g, ":$1");
234
+ for (const method of HTTP_METHODS) {
235
+ if (pathItem[method]) {
236
+ routes.push({ path: normalisedPath, method: method.toUpperCase() });
237
+ }
238
+ }
239
+ }
240
+ return routes;
241
+ }
242
+ /**
243
+ * Express routes manifest (JSON file).
244
+ * Format: [{ path: '/checkout', method: 'GET' }, ...]
245
+ * Users generate this with express-list-routes or similar.
246
+ */
247
+ async collectExpress() {
248
+ return this.readJsonRouteManifest("express");
249
+ }
250
+ /**
251
+ * Manual route list in qualitylens.yaml.
252
+ * config.path points to a JSON file with [{ path: '/route' }]
253
+ */
254
+ async collectManual() {
255
+ return this.readJsonRouteManifest("manual");
256
+ }
257
+ /** Shared reader for express and manual JSON route manifests. */
258
+ readJsonRouteManifest(type) {
259
+ const absPath = path2.resolve(this.config.path);
260
+ if (!fs2.existsSync(absPath)) {
261
+ throw new Error(
262
+ `Routes manifest not found: ${absPath}
263
+ routes.type is "${type}" \u2014 qualitylens expects a JSON file at routes.path.
264
+ Format: [{ "path": "/api/users", "method": "GET" }, ...]`
265
+ );
266
+ }
267
+ let content;
268
+ try {
269
+ content = fs2.readFileSync(absPath, "utf-8");
270
+ } catch (err) {
271
+ throw new Error(`Could not read routes manifest at ${absPath}: ${err.message}`);
272
+ }
273
+ try {
274
+ return JSON.parse(content);
275
+ } catch (err) {
276
+ throw new Error(
277
+ `Failed to parse routes manifest at ${absPath} as JSON.
278
+ Parse error: ${err.message}
279
+ Expected format: [{ "path": "/api/users", "method": "GET" }, ...]`
280
+ );
281
+ }
282
+ }
283
+ };
284
+ // Annotate the CommonJS export names for ESM import in node:
285
+ 0 && (module.exports = {
286
+ RoutesSource
287
+ });
288
+ //# sourceMappingURL=routes.source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/sources/routes.source.ts","../../src/utils/logger.ts"],"sourcesContent":["/**\n * src/sources/routes.source.ts\n * \n * Discovers application routes from the project structure.\n * Supports: Next.js pages/, Next.js app/, OpenAPI spec, Express manifest.\n * \n * AI INSTRUCTIONS:\n * Implement each strategy as a private method.\n * collect() dispatches to the right strategy based on config.type.\n */\n\nimport { AppRoute, TestGapConfig } from '../core/types'\nimport { logger } from '../utils/logger'\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport glob from 'fast-glob'\n\nexport class RoutesSource {\n constructor(private config: TestGapConfig['routes']) {}\n\n async collect(): Promise<AppRoute[]> {\n const type = this.config.type\n logger.info(`[routes] collecting routes`, { type, path: this.config.path })\n\n try {\n switch (type) {\n case 'nextjs': return await this.collectNextJs()\n case 'openapi': return await this.collectOpenApi()\n case 'express': return await this.collectExpress()\n case 'manual': return await this.collectManual()\n default:\n throw new Error(\n `Unknown routes.type \"${(this.config as any).type}\" in qualitylens.yaml.\\n` +\n ` Valid values: nextjs, openapi, express, manual`\n )\n }\n } catch (err) {\n logger.error(`[routes] collection failed (type=${type}, path=${this.config.path})`, err)\n throw err\n }\n }\n\n /**\n * Next.js pages/ or app/ directory scanner.\n * \n * AI INSTRUCTIONS:\n * 1. Glob all .tsx, .ts, .jsx, .js files under config.path\n * 2. Convert file path to route:\n * pages/checkout/payment.tsx → /checkout/payment\n * pages/index.tsx → /\n * pages/auth/[id].tsx → /auth/:id\n * app/checkout/page.tsx → /checkout (App Router pattern)\n * 3. Skip: _app, _document, _error, api/ routes, layout.tsx, loading.tsx\n * 4. Return AppRoute[]\n */\n private async collectNextJs(): Promise<AppRoute[]> {\n const rootPath = path.resolve(this.config.path)\n\n if (!fs.existsSync(rootPath)) {\n throw new Error(\n `Next.js routes directory not found: ${rootPath}\\n` +\n ` Check routes.path in qualitylens.yaml — it should point to your pages/ or app/ directory.`\n )\n }\n\n // Detect App Router vs Pages Router by looking for files named \"page.tsx/ts\".\n // App Router → files are named page.tsx inside route folders: app/cart/page.tsx\n // Pages Router → files ARE the routes: pages/cart.tsx\n const appRouterPages = await glob('**/page.{tsx,ts,jsx,js}', {\n cwd: rootPath,\n absolute: false,\n ignore: ['**/node_modules/**'],\n })\n\n if (appRouterPages.length > 0) {\n // ── App Router ──────────────────────────────────────────────────────────\n return appRouterPages.map(filePath => {\n // fast-glob always uses forward slashes — safe to split on '/' on all OSes.\n // filePath examples:\n // page.tsx → /\n // cart/page.tsx → /cart\n // orders/[id]/page.tsx → /orders/:id\n // (auth)/login/page.tsx → /login (route groups in parens are stripped)\n\n // Remove the trailing \"/page.tsx\" (or page.ts etc.)\n const dir = filePath\n .replace(/\\/page\\.[jt]sx?$/, '')\n .replace(/^page\\.[jt]sx?$/, '')\n\n // Strip route groups: (auth) → removed entirely\n const segments = dir\n .split('/')\n .filter(seg => !(seg.startsWith('(') && seg.endsWith(')')))\n\n // Convert [param] dynamic segments to :param\n const routeSegments = segments.map(seg =>\n seg.startsWith('[') && seg.endsWith(']') ? `:${seg.slice(1, -1)}` : seg\n )\n\n const normalised = ('/' + routeSegments.join('/')).replace(/\\/+/g, '/').replace(/\\/$/, '') || '/'\n return { path: normalised }\n })\n }\n\n // ── Pages Router ──────────────────────────────────────────────────────────\n const pageFiles = await glob('**/*.{tsx,ts,jsx,js}', {\n cwd: rootPath,\n absolute: false,\n ignore: ['**/node_modules/**', '**/_app.*', '**/_document.*', '**/_error.*', '**/api/**'],\n })\n\n return pageFiles.map(filePath => {\n // pages/checkout/payment.tsx → /checkout/payment\n // pages/index.tsx → /\n const withoutExt = filePath.replace(/\\.[jt]sx?$/, '')\n const withoutIndex = withoutExt.replace(/(^|\\/)index$/, '')\n const segments = withoutIndex\n .split('/')\n .map(seg => seg.startsWith('[') && seg.endsWith(']') ? `:${seg.slice(1, -1)}` : seg)\n\n return { path: ('/' + segments.join('/')).replace(/\\/+/g, '/').replace(/\\/$/, '') || '/' }\n })\n }\n\n /**\n * OpenAPI / Swagger spec parser.\n * \n * AI INSTRUCTIONS:\n * 1. Read the JSON or YAML file at config.path\n * 2. Extract spec.paths object — keys are route paths\n * 3. For each path, extract HTTP methods (get, post, put, delete, patch)\n * 4. Return one AppRoute per path+method combination\n * 5. Support both JSON (.json) and YAML (.yaml, .yml) specs\n */\n private async collectOpenApi(): Promise<AppRoute[]> {\n const absPath = path.resolve(this.config.path)\n\n if (!fs.existsSync(absPath)) {\n throw new Error(\n `OpenAPI spec not found: ${absPath}\\n` +\n ` If your spec is generated at build time, check the routes.generate command in qualitylens.yaml.\\n` +\n ` Run the generate command manually first to confirm it produces the file.`\n )\n }\n\n let content: string\n try {\n content = fs.readFileSync(absPath, 'utf-8')\n } catch (err) {\n throw new Error(`Could not read OpenAPI spec at ${absPath}: ${(err as Error).message}`)\n }\n\n // Support both JSON and YAML OpenAPI specs — detect by file extension\n const ext = path.extname(this.config.path).toLowerCase()\n let spec: any\n try {\n spec = (ext === '.json')\n ? JSON.parse(content)\n : (await import('js-yaml')).load(content)\n } catch (err) {\n throw new Error(\n `Failed to parse OpenAPI spec at ${absPath} as ${ext === '.json' ? 'JSON' : 'YAML'}.\\n` +\n ` Parse error: ${(err as Error).message}\\n` +\n ` Check the file is valid by running: node -e \"JSON.parse(require('fs').readFileSync('${absPath}','utf-8'))\"`\n )\n }\n\n // The OpenAPI `paths` object is a map of route → { get: {}, post: {}, ... }\n // e.g. { '/api/products': { get: {...}, post: {...} }, '/api/products/{id}': { get: {...} } }\n const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']\n const routes: AppRoute[] = []\n\n for (const [routePath, pathItem] of Object.entries(spec.paths ?? {})) {\n // Convert OpenAPI {param} style to :param style (consistent with the rest of qualitylens)\n const normalisedPath = routePath.replace(/\\{(\\w+)\\}/g, ':$1')\n\n for (const method of HTTP_METHODS) {\n if ((pathItem as any)[method]) {\n routes.push({ path: normalisedPath, method: method.toUpperCase() })\n }\n }\n }\n\n return routes\n }\n\n /**\n * Express routes manifest (JSON file).\n * Format: [{ path: '/checkout', method: 'GET' }, ...]\n * Users generate this with express-list-routes or similar.\n */\n private async collectExpress(): Promise<AppRoute[]> {\n return this.readJsonRouteManifest('express')\n }\n\n /**\n * Manual route list in qualitylens.yaml.\n * config.path points to a JSON file with [{ path: '/route' }]\n */\n private async collectManual(): Promise<AppRoute[]> {\n return this.readJsonRouteManifest('manual')\n }\n\n /** Shared reader for express and manual JSON route manifests. */\n private readJsonRouteManifest(type: string): AppRoute[] {\n const absPath = path.resolve(this.config.path)\n\n if (!fs.existsSync(absPath)) {\n throw new Error(\n `Routes manifest not found: ${absPath}\\n` +\n ` routes.type is \"${type}\" — qualitylens expects a JSON file at routes.path.\\n` +\n ` Format: [{ \"path\": \"/api/users\", \"method\": \"GET\" }, ...]`\n )\n }\n\n let content: string\n try {\n content = fs.readFileSync(absPath, 'utf-8')\n } catch (err) {\n throw new Error(`Could not read routes manifest at ${absPath}: ${(err as Error).message}`)\n }\n\n try {\n return JSON.parse(content) as AppRoute[]\n } catch (err) {\n throw new Error(\n `Failed to parse routes manifest at ${absPath} as JSON.\\n` +\n ` Parse error: ${(err as Error).message}\\n` +\n ` Expected format: [{ \"path\": \"/api/users\", \"method\": \"GET\" }, ...]`\n )\n }\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACcA,SAAoB;AACpB,WAAsB;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;;;ADpFjC,IAAAA,MAAoB;AACpB,IAAAC,QAAsB;AACtB,uBAAiB;AAEV,IAAM,eAAN,MAAmB;AAAA,EACxB,YAAoB,QAAiC;AAAjC;AAAA,EAAkC;AAAA,EAEtD,MAAM,UAA+B;AACnC,UAAM,OAAO,KAAK,OAAO;AACzB,WAAO,KAAK,8BAA8B,EAAE,MAAM,MAAM,KAAK,OAAO,KAAK,CAAC;AAE1E,QAAI;AACF,cAAQ,MAAM;AAAA,QACZ,KAAK;AAAY,iBAAO,MAAM,KAAK,cAAc;AAAA,QACjD,KAAK;AAAY,iBAAO,MAAM,KAAK,eAAe;AAAA,QAClD,KAAK;AAAY,iBAAO,MAAM,KAAK,eAAe;AAAA,QAClD,KAAK;AAAY,iBAAO,MAAM,KAAK,cAAc;AAAA,QACjD;AACE,gBAAM,IAAI;AAAA,YACR,wBAAyB,KAAK,OAAe,IAAI;AAAA;AAAA,UAEnD;AAAA,MACJ;AAAA,IACF,SAAS,KAAK;AACZ,aAAO,MAAM,oCAAoC,IAAI,UAAU,KAAK,OAAO,IAAI,KAAK,GAAG;AACvF,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAc,gBAAqC;AACjD,UAAM,WAAgB,cAAQ,KAAK,OAAO,IAAI;AAE9C,QAAI,CAAI,eAAW,QAAQ,GAAG;AAC5B,YAAM,IAAI;AAAA,QACR,uCAAuC,QAAQ;AAAA;AAAA,MAEjD;AAAA,IACF;AAKA,UAAM,iBAAiB,UAAM,iBAAAC,SAAK,2BAA2B;AAAA,MAC3D,KAAK;AAAA,MACL,UAAU;AAAA,MACV,QAAQ,CAAC,oBAAoB;AAAA,IAC/B,CAAC;AAED,QAAI,eAAe,SAAS,GAAG;AAE7B,aAAO,eAAe,IAAI,cAAY;AASpC,cAAM,MAAM,SACT,QAAQ,oBAAoB,EAAE,EAC9B,QAAQ,mBAAmB,EAAE;AAGhC,cAAM,WAAW,IACd,MAAM,GAAG,EACT,OAAO,SAAO,EAAE,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,GAAG,EAAE;AAG5D,cAAM,gBAAgB,SAAS;AAAA,UAAI,SACjC,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,GAAG,IAAI,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,KAAK;AAAA,QACtE;AAEA,cAAM,cAAc,MAAM,cAAc,KAAK,GAAG,GAAG,QAAQ,QAAQ,GAAG,EAAE,QAAQ,OAAO,EAAE,KAAK;AAC9F,eAAO,EAAE,MAAM,WAAW;AAAA,MAC5B,CAAC;AAAA,IACH;AAGA,UAAM,YAAY,UAAM,iBAAAA,SAAK,wBAAwB;AAAA,MACnD,KAAK;AAAA,MACL,UAAU;AAAA,MACV,QAAQ,CAAC,sBAAsB,aAAa,kBAAkB,eAAe,WAAW;AAAA,IAC1F,CAAC;AAED,WAAO,UAAU,IAAI,cAAY;AAG/B,YAAM,aAAa,SAAS,QAAQ,cAAc,EAAE;AACpD,YAAM,eAAe,WAAW,QAAQ,gBAAgB,EAAE;AAC1D,YAAM,WAAW,aACd,MAAM,GAAG,EACT,IAAI,SAAO,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,GAAG,IAAI,IAAI,IAAI,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG;AAErF,aAAO,EAAE,OAAO,MAAM,SAAS,KAAK,GAAG,GAAG,QAAQ,QAAQ,GAAG,EAAE,QAAQ,OAAO,EAAE,KAAK,IAAI;AAAA,IAC3F,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAc,iBAAsC;AAClD,UAAM,UAAe,cAAQ,KAAK,OAAO,IAAI;AAE7C,QAAI,CAAI,eAAW,OAAO,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR,2BAA2B,OAAO;AAAA;AAAA;AAAA,MAGpC;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,gBAAa,iBAAa,SAAS,OAAO;AAAA,IAC5C,SAAS,KAAK;AACZ,YAAM,IAAI,MAAM,kCAAkC,OAAO,KAAM,IAAc,OAAO,EAAE;AAAA,IACxF;AAGA,UAAM,MAAW,cAAQ,KAAK,OAAO,IAAI,EAAE,YAAY;AACvD,QAAI;AACJ,QAAI;AACF,aAAQ,QAAQ,UACZ,KAAK,MAAM,OAAO,KACjB,MAAM,OAAO,SAAS,GAAG,KAAK,OAAO;AAAA,IAC5C,SAAS,KAAK;AACZ,YAAM,IAAI;AAAA,QACR,mCAAmC,OAAO,OAAO,QAAQ,UAAU,SAAS,MAAM;AAAA,iBAC/D,IAAc,OAAO;AAAA,wFACiD,OAAO;AAAA,MAClG;AAAA,IACF;AAIA,UAAM,eAAe,CAAC,OAAO,QAAQ,OAAO,SAAS,UAAU,QAAQ,SAAS;AAChF,UAAM,SAAqB,CAAC;AAE5B,eAAW,CAAC,WAAW,QAAQ,KAAK,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,GAAG;AAEpE,YAAM,iBAAiB,UAAU,QAAQ,cAAc,KAAK;AAE5D,iBAAW,UAAU,cAAc;AACjC,YAAK,SAAiB,MAAM,GAAG;AAC7B,iBAAO,KAAK,EAAE,MAAM,gBAAgB,QAAQ,OAAO,YAAY,EAAE,CAAC;AAAA,QACpE;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,iBAAsC;AAClD,WAAO,KAAK,sBAAsB,SAAS;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,gBAAqC;AACjD,WAAO,KAAK,sBAAsB,QAAQ;AAAA,EAC5C;AAAA;AAAA,EAGQ,sBAAsB,MAA0B;AACtD,UAAM,UAAe,cAAQ,KAAK,OAAO,IAAI;AAE7C,QAAI,CAAI,eAAW,OAAO,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR,8BAA8B,OAAO;AAAA,oBAChB,IAAI;AAAA;AAAA,MAE3B;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,gBAAa,iBAAa,SAAS,OAAO;AAAA,IAC5C,SAAS,KAAK;AACZ,YAAM,IAAI,MAAM,qCAAqC,OAAO,KAAM,IAAc,OAAO,EAAE;AAAA,IAC3F;AAEA,QAAI;AACF,aAAO,KAAK,MAAM,OAAO;AAAA,IAC3B,SAAS,KAAK;AACZ,YAAM,IAAI;AAAA,QACR,sCAAsC,OAAO;AAAA,iBAC1B,IAAc,OAAO;AAAA;AAAA,MAE1C;AAAA,IACF;AAAA,EACF;AACF;","names":["fs","path","glob"]}
@@ -0,0 +1,251 @@
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/routes.source.ts
78
+ import * as fs2 from "fs";
79
+ import * as path2 from "path";
80
+ import glob from "fast-glob";
81
+ var RoutesSource = class {
82
+ constructor(config) {
83
+ this.config = config;
84
+ }
85
+ async collect() {
86
+ const type = this.config.type;
87
+ logger.info(`[routes] collecting routes`, { type, path: this.config.path });
88
+ try {
89
+ switch (type) {
90
+ case "nextjs":
91
+ return await this.collectNextJs();
92
+ case "openapi":
93
+ return await this.collectOpenApi();
94
+ case "express":
95
+ return await this.collectExpress();
96
+ case "manual":
97
+ return await this.collectManual();
98
+ default:
99
+ throw new Error(
100
+ `Unknown routes.type "${this.config.type}" in qualitylens.yaml.
101
+ Valid values: nextjs, openapi, express, manual`
102
+ );
103
+ }
104
+ } catch (err) {
105
+ logger.error(`[routes] collection failed (type=${type}, path=${this.config.path})`, err);
106
+ throw err;
107
+ }
108
+ }
109
+ /**
110
+ * Next.js pages/ or app/ directory scanner.
111
+ *
112
+ * AI INSTRUCTIONS:
113
+ * 1. Glob all .tsx, .ts, .jsx, .js files under config.path
114
+ * 2. Convert file path to route:
115
+ * pages/checkout/payment.tsx → /checkout/payment
116
+ * pages/index.tsx → /
117
+ * pages/auth/[id].tsx → /auth/:id
118
+ * app/checkout/page.tsx → /checkout (App Router pattern)
119
+ * 3. Skip: _app, _document, _error, api/ routes, layout.tsx, loading.tsx
120
+ * 4. Return AppRoute[]
121
+ */
122
+ async collectNextJs() {
123
+ const rootPath = path2.resolve(this.config.path);
124
+ if (!fs2.existsSync(rootPath)) {
125
+ throw new Error(
126
+ `Next.js routes directory not found: ${rootPath}
127
+ Check routes.path in qualitylens.yaml \u2014 it should point to your pages/ or app/ directory.`
128
+ );
129
+ }
130
+ const appRouterPages = await glob("**/page.{tsx,ts,jsx,js}", {
131
+ cwd: rootPath,
132
+ absolute: false,
133
+ ignore: ["**/node_modules/**"]
134
+ });
135
+ if (appRouterPages.length > 0) {
136
+ return appRouterPages.map((filePath) => {
137
+ const dir = filePath.replace(/\/page\.[jt]sx?$/, "").replace(/^page\.[jt]sx?$/, "");
138
+ const segments = dir.split("/").filter((seg) => !(seg.startsWith("(") && seg.endsWith(")")));
139
+ const routeSegments = segments.map(
140
+ (seg) => seg.startsWith("[") && seg.endsWith("]") ? `:${seg.slice(1, -1)}` : seg
141
+ );
142
+ const normalised = ("/" + routeSegments.join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
143
+ return { path: normalised };
144
+ });
145
+ }
146
+ const pageFiles = await glob("**/*.{tsx,ts,jsx,js}", {
147
+ cwd: rootPath,
148
+ absolute: false,
149
+ ignore: ["**/node_modules/**", "**/_app.*", "**/_document.*", "**/_error.*", "**/api/**"]
150
+ });
151
+ return pageFiles.map((filePath) => {
152
+ const withoutExt = filePath.replace(/\.[jt]sx?$/, "");
153
+ const withoutIndex = withoutExt.replace(/(^|\/)index$/, "");
154
+ const segments = withoutIndex.split("/").map((seg) => seg.startsWith("[") && seg.endsWith("]") ? `:${seg.slice(1, -1)}` : seg);
155
+ return { path: ("/" + segments.join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/" };
156
+ });
157
+ }
158
+ /**
159
+ * OpenAPI / Swagger spec parser.
160
+ *
161
+ * AI INSTRUCTIONS:
162
+ * 1. Read the JSON or YAML file at config.path
163
+ * 2. Extract spec.paths object — keys are route paths
164
+ * 3. For each path, extract HTTP methods (get, post, put, delete, patch)
165
+ * 4. Return one AppRoute per path+method combination
166
+ * 5. Support both JSON (.json) and YAML (.yaml, .yml) specs
167
+ */
168
+ async collectOpenApi() {
169
+ const absPath = path2.resolve(this.config.path);
170
+ if (!fs2.existsSync(absPath)) {
171
+ throw new Error(
172
+ `OpenAPI spec not found: ${absPath}
173
+ If your spec is generated at build time, check the routes.generate command in qualitylens.yaml.
174
+ Run the generate command manually first to confirm it produces the file.`
175
+ );
176
+ }
177
+ let content;
178
+ try {
179
+ content = fs2.readFileSync(absPath, "utf-8");
180
+ } catch (err) {
181
+ throw new Error(`Could not read OpenAPI spec at ${absPath}: ${err.message}`);
182
+ }
183
+ const ext = path2.extname(this.config.path).toLowerCase();
184
+ let spec;
185
+ try {
186
+ spec = ext === ".json" ? JSON.parse(content) : (await import("js-yaml")).load(content);
187
+ } catch (err) {
188
+ throw new Error(
189
+ `Failed to parse OpenAPI spec at ${absPath} as ${ext === ".json" ? "JSON" : "YAML"}.
190
+ Parse error: ${err.message}
191
+ Check the file is valid by running: node -e "JSON.parse(require('fs').readFileSync('${absPath}','utf-8'))"`
192
+ );
193
+ }
194
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
195
+ const routes = [];
196
+ for (const [routePath, pathItem] of Object.entries(spec.paths ?? {})) {
197
+ const normalisedPath = routePath.replace(/\{(\w+)\}/g, ":$1");
198
+ for (const method of HTTP_METHODS) {
199
+ if (pathItem[method]) {
200
+ routes.push({ path: normalisedPath, method: method.toUpperCase() });
201
+ }
202
+ }
203
+ }
204
+ return routes;
205
+ }
206
+ /**
207
+ * Express routes manifest (JSON file).
208
+ * Format: [{ path: '/checkout', method: 'GET' }, ...]
209
+ * Users generate this with express-list-routes or similar.
210
+ */
211
+ async collectExpress() {
212
+ return this.readJsonRouteManifest("express");
213
+ }
214
+ /**
215
+ * Manual route list in qualitylens.yaml.
216
+ * config.path points to a JSON file with [{ path: '/route' }]
217
+ */
218
+ async collectManual() {
219
+ return this.readJsonRouteManifest("manual");
220
+ }
221
+ /** Shared reader for express and manual JSON route manifests. */
222
+ readJsonRouteManifest(type) {
223
+ const absPath = path2.resolve(this.config.path);
224
+ if (!fs2.existsSync(absPath)) {
225
+ throw new Error(
226
+ `Routes manifest not found: ${absPath}
227
+ routes.type is "${type}" \u2014 qualitylens expects a JSON file at routes.path.
228
+ Format: [{ "path": "/api/users", "method": "GET" }, ...]`
229
+ );
230
+ }
231
+ let content;
232
+ try {
233
+ content = fs2.readFileSync(absPath, "utf-8");
234
+ } catch (err) {
235
+ throw new Error(`Could not read routes manifest at ${absPath}: ${err.message}`);
236
+ }
237
+ try {
238
+ return JSON.parse(content);
239
+ } catch (err) {
240
+ throw new Error(
241
+ `Failed to parse routes manifest at ${absPath} as JSON.
242
+ Parse error: ${err.message}
243
+ Expected format: [{ "path": "/api/users", "method": "GET" }, ...]`
244
+ );
245
+ }
246
+ }
247
+ };
248
+ export {
249
+ RoutesSource
250
+ };
251
+ //# sourceMappingURL=routes.source.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/logger.ts","../../src/sources/routes.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/routes.source.ts\n * \n * Discovers application routes from the project structure.\n * Supports: Next.js pages/, Next.js app/, OpenAPI spec, Express manifest.\n * \n * AI INSTRUCTIONS:\n * Implement each strategy as a private method.\n * collect() dispatches to the right strategy based on config.type.\n */\n\nimport { AppRoute, TestGapConfig } from '../core/types'\nimport { logger } from '../utils/logger'\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport glob from 'fast-glob'\n\nexport class RoutesSource {\n constructor(private config: TestGapConfig['routes']) {}\n\n async collect(): Promise<AppRoute[]> {\n const type = this.config.type\n logger.info(`[routes] collecting routes`, { type, path: this.config.path })\n\n try {\n switch (type) {\n case 'nextjs': return await this.collectNextJs()\n case 'openapi': return await this.collectOpenApi()\n case 'express': return await this.collectExpress()\n case 'manual': return await this.collectManual()\n default:\n throw new Error(\n `Unknown routes.type \"${(this.config as any).type}\" in qualitylens.yaml.\\n` +\n ` Valid values: nextjs, openapi, express, manual`\n )\n }\n } catch (err) {\n logger.error(`[routes] collection failed (type=${type}, path=${this.config.path})`, err)\n throw err\n }\n }\n\n /**\n * Next.js pages/ or app/ directory scanner.\n * \n * AI INSTRUCTIONS:\n * 1. Glob all .tsx, .ts, .jsx, .js files under config.path\n * 2. Convert file path to route:\n * pages/checkout/payment.tsx → /checkout/payment\n * pages/index.tsx → /\n * pages/auth/[id].tsx → /auth/:id\n * app/checkout/page.tsx → /checkout (App Router pattern)\n * 3. Skip: _app, _document, _error, api/ routes, layout.tsx, loading.tsx\n * 4. Return AppRoute[]\n */\n private async collectNextJs(): Promise<AppRoute[]> {\n const rootPath = path.resolve(this.config.path)\n\n if (!fs.existsSync(rootPath)) {\n throw new Error(\n `Next.js routes directory not found: ${rootPath}\\n` +\n ` Check routes.path in qualitylens.yaml — it should point to your pages/ or app/ directory.`\n )\n }\n\n // Detect App Router vs Pages Router by looking for files named \"page.tsx/ts\".\n // App Router → files are named page.tsx inside route folders: app/cart/page.tsx\n // Pages Router → files ARE the routes: pages/cart.tsx\n const appRouterPages = await glob('**/page.{tsx,ts,jsx,js}', {\n cwd: rootPath,\n absolute: false,\n ignore: ['**/node_modules/**'],\n })\n\n if (appRouterPages.length > 0) {\n // ── App Router ──────────────────────────────────────────────────────────\n return appRouterPages.map(filePath => {\n // fast-glob always uses forward slashes — safe to split on '/' on all OSes.\n // filePath examples:\n // page.tsx → /\n // cart/page.tsx → /cart\n // orders/[id]/page.tsx → /orders/:id\n // (auth)/login/page.tsx → /login (route groups in parens are stripped)\n\n // Remove the trailing \"/page.tsx\" (or page.ts etc.)\n const dir = filePath\n .replace(/\\/page\\.[jt]sx?$/, '')\n .replace(/^page\\.[jt]sx?$/, '')\n\n // Strip route groups: (auth) → removed entirely\n const segments = dir\n .split('/')\n .filter(seg => !(seg.startsWith('(') && seg.endsWith(')')))\n\n // Convert [param] dynamic segments to :param\n const routeSegments = segments.map(seg =>\n seg.startsWith('[') && seg.endsWith(']') ? `:${seg.slice(1, -1)}` : seg\n )\n\n const normalised = ('/' + routeSegments.join('/')).replace(/\\/+/g, '/').replace(/\\/$/, '') || '/'\n return { path: normalised }\n })\n }\n\n // ── Pages Router ──────────────────────────────────────────────────────────\n const pageFiles = await glob('**/*.{tsx,ts,jsx,js}', {\n cwd: rootPath,\n absolute: false,\n ignore: ['**/node_modules/**', '**/_app.*', '**/_document.*', '**/_error.*', '**/api/**'],\n })\n\n return pageFiles.map(filePath => {\n // pages/checkout/payment.tsx → /checkout/payment\n // pages/index.tsx → /\n const withoutExt = filePath.replace(/\\.[jt]sx?$/, '')\n const withoutIndex = withoutExt.replace(/(^|\\/)index$/, '')\n const segments = withoutIndex\n .split('/')\n .map(seg => seg.startsWith('[') && seg.endsWith(']') ? `:${seg.slice(1, -1)}` : seg)\n\n return { path: ('/' + segments.join('/')).replace(/\\/+/g, '/').replace(/\\/$/, '') || '/' }\n })\n }\n\n /**\n * OpenAPI / Swagger spec parser.\n * \n * AI INSTRUCTIONS:\n * 1. Read the JSON or YAML file at config.path\n * 2. Extract spec.paths object — keys are route paths\n * 3. For each path, extract HTTP methods (get, post, put, delete, patch)\n * 4. Return one AppRoute per path+method combination\n * 5. Support both JSON (.json) and YAML (.yaml, .yml) specs\n */\n private async collectOpenApi(): Promise<AppRoute[]> {\n const absPath = path.resolve(this.config.path)\n\n if (!fs.existsSync(absPath)) {\n throw new Error(\n `OpenAPI spec not found: ${absPath}\\n` +\n ` If your spec is generated at build time, check the routes.generate command in qualitylens.yaml.\\n` +\n ` Run the generate command manually first to confirm it produces the file.`\n )\n }\n\n let content: string\n try {\n content = fs.readFileSync(absPath, 'utf-8')\n } catch (err) {\n throw new Error(`Could not read OpenAPI spec at ${absPath}: ${(err as Error).message}`)\n }\n\n // Support both JSON and YAML OpenAPI specs — detect by file extension\n const ext = path.extname(this.config.path).toLowerCase()\n let spec: any\n try {\n spec = (ext === '.json')\n ? JSON.parse(content)\n : (await import('js-yaml')).load(content)\n } catch (err) {\n throw new Error(\n `Failed to parse OpenAPI spec at ${absPath} as ${ext === '.json' ? 'JSON' : 'YAML'}.\\n` +\n ` Parse error: ${(err as Error).message}\\n` +\n ` Check the file is valid by running: node -e \"JSON.parse(require('fs').readFileSync('${absPath}','utf-8'))\"`\n )\n }\n\n // The OpenAPI `paths` object is a map of route → { get: {}, post: {}, ... }\n // e.g. { '/api/products': { get: {...}, post: {...} }, '/api/products/{id}': { get: {...} } }\n const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']\n const routes: AppRoute[] = []\n\n for (const [routePath, pathItem] of Object.entries(spec.paths ?? {})) {\n // Convert OpenAPI {param} style to :param style (consistent with the rest of qualitylens)\n const normalisedPath = routePath.replace(/\\{(\\w+)\\}/g, ':$1')\n\n for (const method of HTTP_METHODS) {\n if ((pathItem as any)[method]) {\n routes.push({ path: normalisedPath, method: method.toUpperCase() })\n }\n }\n }\n\n return routes\n }\n\n /**\n * Express routes manifest (JSON file).\n * Format: [{ path: '/checkout', method: 'GET' }, ...]\n * Users generate this with express-list-routes or similar.\n */\n private async collectExpress(): Promise<AppRoute[]> {\n return this.readJsonRouteManifest('express')\n }\n\n /**\n * Manual route list in qualitylens.yaml.\n * config.path points to a JSON file with [{ path: '/route' }]\n */\n private async collectManual(): Promise<AppRoute[]> {\n return this.readJsonRouteManifest('manual')\n }\n\n /** Shared reader for express and manual JSON route manifests. */\n private readJsonRouteManifest(type: string): AppRoute[] {\n const absPath = path.resolve(this.config.path)\n\n if (!fs.existsSync(absPath)) {\n throw new Error(\n `Routes manifest not found: ${absPath}\\n` +\n ` routes.type is \"${type}\" — qualitylens expects a JSON file at routes.path.\\n` +\n ` Format: [{ \"path\": \"/api/users\", \"method\": \"GET\" }, ...]`\n )\n }\n\n let content: string\n try {\n content = fs.readFileSync(absPath, 'utf-8')\n } catch (err) {\n throw new Error(`Could not read routes manifest at ${absPath}: ${(err as Error).message}`)\n }\n\n try {\n return JSON.parse(content) as AppRoute[]\n } catch (err) {\n throw new Error(\n `Failed to parse routes manifest at ${absPath} as JSON.\\n` +\n ` Parse error: ${(err as Error).message}\\n` +\n ` Expected format: [{ \"path\": \"/api/users\", \"method\": \"GET\" }, ...]`\n )\n }\n }\n}\n"],"mappings":"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","names":["fs","path"]}
@@ -0,0 +1,16 @@
1
+ import { BaseSource } from './base.source.mjs';
2
+ import { TestEntry } from '../core/types.mjs';
3
+
4
+ /**
5
+ * src/sources/yaml.source.ts
6
+ * Reads manual coverage entries from qualitylens.yaml
7
+ */
8
+
9
+ declare class YamlSource extends BaseSource {
10
+ private configPath;
11
+ readonly name = "yaml";
12
+ constructor(configPath: string);
13
+ collect(): Promise<TestEntry[]>;
14
+ }
15
+
16
+ export { YamlSource };
@@ -0,0 +1,16 @@
1
+ import { BaseSource } from './base.source.js';
2
+ import { TestEntry } from '../core/types.js';
3
+
4
+ /**
5
+ * src/sources/yaml.source.ts
6
+ * Reads manual coverage entries from qualitylens.yaml
7
+ */
8
+
9
+ declare class YamlSource extends BaseSource {
10
+ private configPath;
11
+ readonly name = "yaml";
12
+ constructor(configPath: string);
13
+ collect(): Promise<TestEntry[]>;
14
+ }
15
+
16
+ export { YamlSource };