@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,431 @@
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/core/detector.ts
31
+ var detector_exports = {};
32
+ __export(detector_exports, {
33
+ detect: () => detect,
34
+ suggestAreas: () => suggestAreas
35
+ });
36
+ module.exports = __toCommonJS(detector_exports);
37
+ var fs3 = __toESM(require("fs"));
38
+ var path3 = __toESM(require("path"));
39
+
40
+ // src/utils/logger.ts
41
+ var fs = __toESM(require("fs"));
42
+ var path = __toESM(require("path"));
43
+ var Logger = class {
44
+ logPath = null;
45
+ fd = null;
46
+ /**
47
+ * Open (or create) qualitylens.log in the given directory.
48
+ * Appends to an existing log so multiple runs accumulate history.
49
+ * Call once at the start of each command.
50
+ */
51
+ configure(outputDir) {
52
+ try {
53
+ fs.mkdirSync(outputDir, { recursive: true });
54
+ this.logPath = path.join(outputDir, "qualitylens.log");
55
+ this.fd = fs.openSync(this.logPath, "a");
56
+ this.write("INFO", `--- qualitylens session started (pid ${process.pid}) ---`);
57
+ } catch {
58
+ this.logPath = null;
59
+ this.fd = null;
60
+ }
61
+ }
62
+ /** Path to the current log file, or null if not configured. */
63
+ get filePath() {
64
+ return this.logPath;
65
+ }
66
+ info(msg, detail) {
67
+ this.write("INFO", msg, detail);
68
+ }
69
+ warn(msg, detail) {
70
+ this.write("WARN", msg, detail);
71
+ }
72
+ /** Logs full detail (stack trace, raw error) to file only. */
73
+ error(msg, detail) {
74
+ this.write("ERROR", msg, detail);
75
+ }
76
+ debug(msg, detail) {
77
+ this.write("DEBUG", msg, detail);
78
+ }
79
+ close() {
80
+ if (this.fd !== null) {
81
+ try {
82
+ fs.closeSync(this.fd);
83
+ } catch {
84
+ }
85
+ this.fd = null;
86
+ }
87
+ }
88
+ write(level, msg, detail) {
89
+ if (this.fd === null) return;
90
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
91
+ let line = `[${ts}] ${level.padEnd(5)} ${msg}`;
92
+ if (detail !== void 0) {
93
+ if (detail instanceof Error) {
94
+ line += `
95
+ ${detail.message}`;
96
+ if (detail.stack) {
97
+ line += "\n" + detail.stack.split("\n").map((l) => " " + l).join("\n");
98
+ }
99
+ } else if (typeof detail === "string" && detail.trim()) {
100
+ line += "\n" + detail.split("\n").map((l) => " " + l).join("\n");
101
+ } else if (typeof detail === "object") {
102
+ try {
103
+ line += "\n " + JSON.stringify(detail, null, 2).replace(/\n/g, "\n ");
104
+ } catch {
105
+ }
106
+ }
107
+ }
108
+ try {
109
+ fs.writeSync(this.fd, line + "\n");
110
+ } catch {
111
+ }
112
+ }
113
+ };
114
+ var logger = new Logger();
115
+
116
+ // src/sources/routes.source.ts
117
+ var fs2 = __toESM(require("fs"));
118
+ var path2 = __toESM(require("path"));
119
+ var import_fast_glob = __toESM(require("fast-glob"));
120
+ var RoutesSource = class {
121
+ constructor(config) {
122
+ this.config = config;
123
+ }
124
+ async collect() {
125
+ const type = this.config.type;
126
+ logger.info(`[routes] collecting routes`, { type, path: this.config.path });
127
+ try {
128
+ switch (type) {
129
+ case "nextjs":
130
+ return await this.collectNextJs();
131
+ case "openapi":
132
+ return await this.collectOpenApi();
133
+ case "express":
134
+ return await this.collectExpress();
135
+ case "manual":
136
+ return await this.collectManual();
137
+ default:
138
+ throw new Error(
139
+ `Unknown routes.type "${this.config.type}" in qualitylens.yaml.
140
+ Valid values: nextjs, openapi, express, manual`
141
+ );
142
+ }
143
+ } catch (err) {
144
+ logger.error(`[routes] collection failed (type=${type}, path=${this.config.path})`, err);
145
+ throw err;
146
+ }
147
+ }
148
+ /**
149
+ * Next.js pages/ or app/ directory scanner.
150
+ *
151
+ * AI INSTRUCTIONS:
152
+ * 1. Glob all .tsx, .ts, .jsx, .js files under config.path
153
+ * 2. Convert file path to route:
154
+ * pages/checkout/payment.tsx → /checkout/payment
155
+ * pages/index.tsx → /
156
+ * pages/auth/[id].tsx → /auth/:id
157
+ * app/checkout/page.tsx → /checkout (App Router pattern)
158
+ * 3. Skip: _app, _document, _error, api/ routes, layout.tsx, loading.tsx
159
+ * 4. Return AppRoute[]
160
+ */
161
+ async collectNextJs() {
162
+ const rootPath = path2.resolve(this.config.path);
163
+ if (!fs2.existsSync(rootPath)) {
164
+ throw new Error(
165
+ `Next.js routes directory not found: ${rootPath}
166
+ Check routes.path in qualitylens.yaml \u2014 it should point to your pages/ or app/ directory.`
167
+ );
168
+ }
169
+ const appRouterPages = await (0, import_fast_glob.default)("**/page.{tsx,ts,jsx,js}", {
170
+ cwd: rootPath,
171
+ absolute: false,
172
+ ignore: ["**/node_modules/**"]
173
+ });
174
+ if (appRouterPages.length > 0) {
175
+ return appRouterPages.map((filePath) => {
176
+ const dir = filePath.replace(/\/page\.[jt]sx?$/, "").replace(/^page\.[jt]sx?$/, "");
177
+ const segments = dir.split("/").filter((seg) => !(seg.startsWith("(") && seg.endsWith(")")));
178
+ const routeSegments = segments.map(
179
+ (seg) => seg.startsWith("[") && seg.endsWith("]") ? `:${seg.slice(1, -1)}` : seg
180
+ );
181
+ const normalised = ("/" + routeSegments.join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/";
182
+ return { path: normalised };
183
+ });
184
+ }
185
+ const pageFiles = await (0, import_fast_glob.default)("**/*.{tsx,ts,jsx,js}", {
186
+ cwd: rootPath,
187
+ absolute: false,
188
+ ignore: ["**/node_modules/**", "**/_app.*", "**/_document.*", "**/_error.*", "**/api/**"]
189
+ });
190
+ return pageFiles.map((filePath) => {
191
+ const withoutExt = filePath.replace(/\.[jt]sx?$/, "");
192
+ const withoutIndex = withoutExt.replace(/(^|\/)index$/, "");
193
+ const segments = withoutIndex.split("/").map((seg) => seg.startsWith("[") && seg.endsWith("]") ? `:${seg.slice(1, -1)}` : seg);
194
+ return { path: ("/" + segments.join("/")).replace(/\/+/g, "/").replace(/\/$/, "") || "/" };
195
+ });
196
+ }
197
+ /**
198
+ * OpenAPI / Swagger spec parser.
199
+ *
200
+ * AI INSTRUCTIONS:
201
+ * 1. Read the JSON or YAML file at config.path
202
+ * 2. Extract spec.paths object — keys are route paths
203
+ * 3. For each path, extract HTTP methods (get, post, put, delete, patch)
204
+ * 4. Return one AppRoute per path+method combination
205
+ * 5. Support both JSON (.json) and YAML (.yaml, .yml) specs
206
+ */
207
+ async collectOpenApi() {
208
+ const absPath = path2.resolve(this.config.path);
209
+ if (!fs2.existsSync(absPath)) {
210
+ throw new Error(
211
+ `OpenAPI spec not found: ${absPath}
212
+ If your spec is generated at build time, check the routes.generate command in qualitylens.yaml.
213
+ Run the generate command manually first to confirm it produces the file.`
214
+ );
215
+ }
216
+ let content;
217
+ try {
218
+ content = fs2.readFileSync(absPath, "utf-8");
219
+ } catch (err) {
220
+ throw new Error(`Could not read OpenAPI spec at ${absPath}: ${err.message}`);
221
+ }
222
+ const ext = path2.extname(this.config.path).toLowerCase();
223
+ let spec;
224
+ try {
225
+ spec = ext === ".json" ? JSON.parse(content) : (await import("js-yaml")).load(content);
226
+ } catch (err) {
227
+ throw new Error(
228
+ `Failed to parse OpenAPI spec at ${absPath} as ${ext === ".json" ? "JSON" : "YAML"}.
229
+ Parse error: ${err.message}
230
+ Check the file is valid by running: node -e "JSON.parse(require('fs').readFileSync('${absPath}','utf-8'))"`
231
+ );
232
+ }
233
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
234
+ const routes = [];
235
+ for (const [routePath, pathItem] of Object.entries(spec.paths ?? {})) {
236
+ const normalisedPath = routePath.replace(/\{(\w+)\}/g, ":$1");
237
+ for (const method of HTTP_METHODS) {
238
+ if (pathItem[method]) {
239
+ routes.push({ path: normalisedPath, method: method.toUpperCase() });
240
+ }
241
+ }
242
+ }
243
+ return routes;
244
+ }
245
+ /**
246
+ * Express routes manifest (JSON file).
247
+ * Format: [{ path: '/checkout', method: 'GET' }, ...]
248
+ * Users generate this with express-list-routes or similar.
249
+ */
250
+ async collectExpress() {
251
+ return this.readJsonRouteManifest("express");
252
+ }
253
+ /**
254
+ * Manual route list in qualitylens.yaml.
255
+ * config.path points to a JSON file with [{ path: '/route' }]
256
+ */
257
+ async collectManual() {
258
+ return this.readJsonRouteManifest("manual");
259
+ }
260
+ /** Shared reader for express and manual JSON route manifests. */
261
+ readJsonRouteManifest(type) {
262
+ const absPath = path2.resolve(this.config.path);
263
+ if (!fs2.existsSync(absPath)) {
264
+ throw new Error(
265
+ `Routes manifest not found: ${absPath}
266
+ routes.type is "${type}" \u2014 qualitylens expects a JSON file at routes.path.
267
+ Format: [{ "path": "/api/users", "method": "GET" }, ...]`
268
+ );
269
+ }
270
+ let content;
271
+ try {
272
+ content = fs2.readFileSync(absPath, "utf-8");
273
+ } catch (err) {
274
+ throw new Error(`Could not read routes manifest at ${absPath}: ${err.message}`);
275
+ }
276
+ try {
277
+ return JSON.parse(content);
278
+ } catch (err) {
279
+ throw new Error(
280
+ `Failed to parse routes manifest at ${absPath} as JSON.
281
+ Parse error: ${err.message}
282
+ Expected format: [{ "path": "/api/users", "method": "GET" }, ...]`
283
+ );
284
+ }
285
+ }
286
+ };
287
+
288
+ // src/core/detector.ts
289
+ var KNOWN_AREA_NAMES = {
290
+ auth: "Authentication",
291
+ login: "Authentication",
292
+ register: "Authentication",
293
+ logout: "Authentication",
294
+ signup: "Authentication",
295
+ checkout: "Checkout",
296
+ cart: "Shopping & Cart",
297
+ basket: "Shopping & Cart",
298
+ orders: "Orders",
299
+ order: "Orders",
300
+ account: "Account",
301
+ profile: "Account",
302
+ admin: "Admin",
303
+ settings: "Settings",
304
+ products: "Products",
305
+ product: "Products",
306
+ users: "Users",
307
+ user: "Users",
308
+ health: "Health",
309
+ ai: "AI",
310
+ billing: "Billing",
311
+ dashboard: "Dashboard"
312
+ };
313
+ async function detect(targetDir) {
314
+ const projectName = path3.basename(targetDir);
315
+ const pkgPath = path3.join(targetDir, "package.json");
316
+ const pkg = fs3.existsSync(pkgPath) ? JSON.parse(fs3.readFileSync(pkgPath, "utf-8")) : {};
317
+ const allDeps = {
318
+ ...pkg.dependencies,
319
+ ...pkg.devDependencies
320
+ };
321
+ const hasNext = "next" in allDeps;
322
+ const hasExpress = "express" in allDeps;
323
+ const hasSwaggerJsdoc = "swagger-jsdoc" in allDeps;
324
+ const hasPlaywright = "@playwright/test" in allDeps;
325
+ const TEST_DIR_CANDIDATES = ["e2e-tests", "e2e", "tests", "__tests__", "cypress/e2e", "test"];
326
+ let testsPath = null;
327
+ for (const candidate of TEST_DIR_CANDIDATES) {
328
+ if (fs3.existsSync(path3.join(targetDir, candidate))) {
329
+ testsPath = `./${candidate}`;
330
+ break;
331
+ }
332
+ }
333
+ let framework = "unknown";
334
+ let routeType = "manual";
335
+ let routesPath = null;
336
+ let generate = null;
337
+ if (hasNext) {
338
+ framework = "nextjs";
339
+ routeType = "nextjs";
340
+ for (const candidate of ["src/app", "app", "src/pages", "pages"]) {
341
+ if (fs3.existsSync(path3.join(targetDir, candidate))) {
342
+ routesPath = `./${candidate}`;
343
+ break;
344
+ }
345
+ }
346
+ } else if (hasExpress && hasSwaggerJsdoc) {
347
+ framework = "express-openapi";
348
+ routeType = "openapi";
349
+ routesPath = "./openapi.json";
350
+ generate = "npx ts-node src/generate-openapi.ts";
351
+ } else if (hasExpress) {
352
+ framework = "express";
353
+ routeType = "express";
354
+ }
355
+ let discoveredRoutes = [];
356
+ if (routesPath) {
357
+ const absoluteRoutesPath = path3.join(targetDir, routesPath.replace("./", ""));
358
+ const shouldDiscover = routeType === "nextjs" || routeType === "openapi" && fs3.existsSync(absoluteRoutesPath);
359
+ if (shouldDiscover) {
360
+ try {
361
+ const source = new RoutesSource({
362
+ type: routeType === "openapi" ? "openapi" : "nextjs",
363
+ path: absoluteRoutesPath
364
+ });
365
+ discoveredRoutes = await source.collect();
366
+ } catch {
367
+ }
368
+ }
369
+ }
370
+ const basePath = detectBasePath(discoveredRoutes);
371
+ const suggestedAreas = suggestAreas(discoveredRoutes, basePath);
372
+ return {
373
+ projectName,
374
+ framework,
375
+ routeType,
376
+ routesPath,
377
+ testsPath,
378
+ generate,
379
+ basePath,
380
+ hasSwaggerJsdoc,
381
+ hasPlaywright,
382
+ discoveredRoutes,
383
+ suggestedAreas
384
+ };
385
+ }
386
+ function detectBasePath(routes) {
387
+ if (routes.length === 0) return null;
388
+ const versionedPrefix = routes[0].path.match(/^(\/api\/v\d+)\//)?.[1];
389
+ if (versionedPrefix) {
390
+ const count = routes.filter((r) => r.path.startsWith(versionedPrefix + "/")).length;
391
+ if (count > routes.length * 0.5) return versionedPrefix;
392
+ }
393
+ const apiCount = routes.filter((r) => r.path.startsWith("/api/")).length;
394
+ if (apiCount > routes.length * 0.5) return "/api";
395
+ return null;
396
+ }
397
+ function suggestAreas(routes, basePath) {
398
+ if (routes.length === 0) return [];
399
+ const nameGroups = /* @__PURE__ */ new Map();
400
+ for (const route of routes) {
401
+ const hasPrefix = basePath ? route.path.startsWith(basePath + "/") : false;
402
+ const normalised = hasPrefix ? route.path.slice(basePath.length) : route.path;
403
+ const segments = normalised.split("/").filter((s) => s.length > 0 && !s.startsWith(":"));
404
+ const firstSegment = segments[0] ?? "";
405
+ if (!firstSegment) continue;
406
+ const areaName = toAreaName(firstSegment);
407
+ const pattern = hasPrefix ? `${basePath}/${firstSegment}` : `/${firstSegment}`;
408
+ if (!nameGroups.has(areaName)) {
409
+ nameGroups.set(areaName, { routes: [], patterns: /* @__PURE__ */ new Set() });
410
+ }
411
+ const group = nameGroups.get(areaName);
412
+ group.routes.push(route);
413
+ group.patterns.add(pattern);
414
+ }
415
+ return Array.from(nameGroups.entries()).map(([name, { routes: routes2, patterns }]) => ({
416
+ suggestedName: name,
417
+ routes: routes2,
418
+ patterns: Array.from(patterns)
419
+ }));
420
+ }
421
+ function toAreaName(segment) {
422
+ const lower = segment.toLowerCase();
423
+ if (KNOWN_AREA_NAMES[lower]) return KNOWN_AREA_NAMES[lower];
424
+ return segment.charAt(0).toUpperCase() + segment.slice(1);
425
+ }
426
+ // Annotate the CommonJS export names for ESM import in node:
427
+ 0 && (module.exports = {
428
+ detect,
429
+ suggestAreas
430
+ });
431
+ //# sourceMappingURL=detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/detector.ts","../../src/utils/logger.ts","../../src/sources/routes.source.ts"],"sourcesContent":["/**\n * src/core/detector.ts\n *\n * Auto-detection logic for `qualitylens init`.\n * Inspects a target directory and returns everything it can infer:\n * framework, route type, routes path, discovered routes, and suggested areas.\n *\n * Nothing here writes files or prompts the user — it only observes.\n */\n\nimport * as fs from 'fs'\nimport * as path from 'path'\nimport { RoutesSource } from '../sources/routes.source'\nimport { AppRoute } from './types'\n\nexport type Framework = 'nextjs' | 'express-openapi' | 'express' | 'unknown'\nexport type RouteType = 'nextjs' | 'openapi' | 'express' | 'manual'\n\nexport interface SuggestedArea {\n suggestedName: string\n routes: AppRoute[]\n patterns: string[]\n}\n\nexport interface DetectionResult {\n projectName: string\n framework: Framework\n routeType: RouteType\n routesPath: string | null // null = could not detect\n testsPath: string | null // null = use config dir default\n generate: string | null // suggested generate command for openapi\n basePath: string | null // detected route prefix to strip when grouping areas (e.g. /api, /api/v1)\n hasSwaggerJsdoc: boolean\n hasPlaywright: boolean\n discoveredRoutes: AppRoute[]\n suggestedAreas: SuggestedArea[]\n}\n\n// Known path segment → human-readable area name.\n// Segments not listed here get title-cased automatically (e.g. 'billing' → 'Billing').\nconst KNOWN_AREA_NAMES: Record<string, string> = {\n auth: 'Authentication',\n login: 'Authentication',\n register: 'Authentication',\n logout: 'Authentication',\n signup: 'Authentication',\n checkout: 'Checkout',\n cart: 'Shopping & Cart',\n basket: 'Shopping & Cart',\n orders: 'Orders',\n order: 'Orders',\n account: 'Account',\n profile: 'Account',\n admin: 'Admin',\n settings: 'Settings',\n products: 'Products',\n product: 'Products',\n users: 'Users',\n user: 'Users',\n health: 'Health',\n ai: 'AI',\n billing: 'Billing',\n dashboard: 'Dashboard',\n}\n\nexport async function detect(targetDir: string): Promise<DetectionResult> {\n const projectName = path.basename(targetDir)\n\n // ── Read package.json ────────────────────────────────────────────────────\n const pkgPath = path.join(targetDir, 'package.json')\n const pkg = fs.existsSync(pkgPath)\n ? JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))\n : {}\n\n const allDeps: Record<string, string> = {\n ...pkg.dependencies,\n ...pkg.devDependencies,\n }\n\n const hasNext = 'next' in allDeps\n const hasExpress = 'express' in allDeps\n const hasSwaggerJsdoc = 'swagger-jsdoc' in allDeps\n const hasPlaywright = '@playwright/test' in allDeps\n\n // ── Detect test directory ────────────────────────────────────────────────\n // Check common test directory names in order of specificity.\n // null means \"use the config file directory\" (the default behaviour).\n const TEST_DIR_CANDIDATES = ['e2e-tests', 'e2e', 'tests', '__tests__', 'cypress/e2e', 'test']\n let testsPath: string | null = null\n for (const candidate of TEST_DIR_CANDIDATES) {\n if (fs.existsSync(path.join(targetDir, candidate))) {\n testsPath = `./${candidate}`\n break\n }\n }\n\n // ── Determine framework and route type ──────────────────────────────────\n let framework: Framework = 'unknown'\n let routeType: RouteType = 'manual'\n let routesPath: string | null = null\n let generate: string | null = null\n\n if (hasNext) {\n framework = 'nextjs'\n routeType = 'nextjs'\n\n // Check for App Router first, then Pages Router, in likely locations\n for (const candidate of ['src/app', 'app', 'src/pages', 'pages']) {\n if (fs.existsSync(path.join(targetDir, candidate))) {\n routesPath = `./${candidate}`\n break\n }\n }\n } else if (hasExpress && hasSwaggerJsdoc) {\n framework = 'express-openapi'\n routeType = 'openapi'\n routesPath = './openapi.json'\n // Suggest generate command — user must create the script, but we show them what to put\n generate = 'npx ts-node src/generate-openapi.ts'\n } else if (hasExpress) {\n framework = 'express'\n routeType = 'express'\n }\n\n // ── Discover routes ──────────────────────────────────────────────────────\n // Next.js: always discoverable from the file system.\n // OpenAPI: discoverable only if the spec file already exists on disk.\n // Express/manual: need a generate step — skip discovery.\n let discoveredRoutes: AppRoute[] = []\n\n if (routesPath) {\n const absoluteRoutesPath = path.join(targetDir, routesPath.replace('./', ''))\n const shouldDiscover =\n routeType === 'nextjs' ||\n (routeType === 'openapi' && fs.existsSync(absoluteRoutesPath))\n\n if (shouldDiscover) {\n try {\n const source = new RoutesSource({\n type: routeType === 'openapi' ? 'openapi' : 'nextjs',\n path: absoluteRoutesPath,\n })\n discoveredRoutes = await source.collect()\n } catch {\n // Not a blocker — init still works, areas will be empty\n }\n }\n }\n\n // ── Detect basePath ──────────────────────────────────────────────────────\n const basePath = detectBasePath(discoveredRoutes)\n\n // ── Suggest areas from discovered routes ────────────────────────────────\n const suggestedAreas = suggestAreas(discoveredRoutes, basePath)\n\n return {\n projectName,\n framework,\n routeType,\n routesPath,\n testsPath,\n generate,\n basePath,\n hasSwaggerJsdoc,\n hasPlaywright,\n discoveredRoutes,\n suggestedAreas,\n }\n}\n\n/**\n * Infers the common base path prefix from discovered routes.\n * Checks for /api/vN (versioned) first, then plain /api.\n * Returns null when fewer than half the routes share the prefix — avoids false guesses.\n */\nfunction detectBasePath(routes: AppRoute[]): string | null {\n if (routes.length === 0) return null\n\n // Check for versioned prefix /api/vN\n const versionedPrefix = routes[0].path.match(/^(\\/api\\/v\\d+)\\//)?.[1]\n if (versionedPrefix) {\n const count = routes.filter(r => r.path.startsWith(versionedPrefix + '/')).length\n if (count > routes.length * 0.5) return versionedPrefix\n }\n\n // Check for plain /api prefix\n const apiCount = routes.filter(r => r.path.startsWith('/api/')).length\n if (apiCount > routes.length * 0.5) return '/api'\n\n return null\n}\n\n/**\n * Groups routes by their first meaningful path segment and maps each group\n * to a human-readable area name.\n * basePath (e.g. /api, /api/v1) is stripped per-route before grouping so routes\n * are organised by service name rather than the shared prefix.\n * Exported so the init command can re-run it after the user confirms/changes basePath.\n */\nexport function suggestAreas(routes: AppRoute[], basePath: string | null): SuggestedArea[] {\n if (routes.length === 0) return []\n\n // Group routes by suggested area name (multiple segments can map to the same name)\n const nameGroups = new Map<string, { routes: AppRoute[]; patterns: Set<string> }>()\n\n for (const route of routes) {\n // Strip basePath per-route when it matches — handles mixed specs where only\n // some routes carry the prefix (e.g. /health alongside /api/products)\n const hasPrefix = basePath ? route.path.startsWith(basePath + '/') : false\n const normalised = hasPrefix ? route.path.slice(basePath!.length) : route.path\n const segments = normalised.split('/').filter(s => s.length > 0 && !s.startsWith(':'))\n const firstSegment = segments[0] ?? ''\n\n // Root route (/) goes into an 'uncategorised' bucket — not forced into an area\n if (!firstSegment) continue\n\n const areaName = toAreaName(firstSegment)\n const pattern = hasPrefix ? `${basePath}/${firstSegment}` : `/${firstSegment}`\n\n if (!nameGroups.has(areaName)) {\n nameGroups.set(areaName, { routes: [], patterns: new Set() })\n }\n const group = nameGroups.get(areaName)!\n group.routes.push(route)\n group.patterns.add(pattern)\n }\n\n return Array.from(nameGroups.entries()).map(([name, { routes, patterns }]) => ({\n suggestedName: name,\n routes,\n patterns: Array.from(patterns),\n }))\n}\n\n/** Converts a raw path segment to a human-readable area name. */\nfunction toAreaName(segment: string): string {\n const lower = segment.toLowerCase()\n if (KNOWN_AREA_NAMES[lower]) return KNOWN_AREA_NAMES[lower]\n // Title-case unknown segments: 'invoices' → 'Invoices'\n return segment.charAt(0).toUpperCase() + segment.slice(1)\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/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","fs","path","glob","routes"]}