@qulib/core 0.3.1 → 0.4.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 (70) hide show
  1. package/dist/analyze.d.ts +2 -0
  2. package/dist/analyze.d.ts.map +1 -1
  3. package/dist/analyze.js +29 -1
  4. package/dist/harness/decision-logger.d.ts +1 -0
  5. package/dist/harness/decision-logger.d.ts.map +1 -1
  6. package/dist/harness/decision-logger.js +15 -22
  7. package/dist/harness/run-options.d.ts +3 -0
  8. package/dist/harness/run-options.d.ts.map +1 -1
  9. package/dist/harness/state-manager.d.ts +3 -0
  10. package/dist/harness/state-manager.d.ts.map +1 -1
  11. package/dist/harness/state-manager.js +15 -18
  12. package/dist/index.d.ts +9 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +5 -0
  15. package/dist/llm/cost-intelligence.d.ts +13 -0
  16. package/dist/llm/cost-intelligence.d.ts.map +1 -1
  17. package/dist/llm/cost-intelligence.js +13 -0
  18. package/dist/llm/provider-registry.d.ts +9 -0
  19. package/dist/llm/provider-registry.d.ts.map +1 -0
  20. package/dist/llm/provider-registry.js +15 -0
  21. package/dist/llm/provider.d.ts +9 -11
  22. package/dist/llm/provider.d.ts.map +1 -1
  23. package/dist/llm/provider.interface.d.ts +16 -0
  24. package/dist/llm/provider.interface.d.ts.map +1 -0
  25. package/dist/llm/provider.interface.js +1 -0
  26. package/dist/llm/provider.js +8 -51
  27. package/dist/llm/providers/anthropic.d.ts +16 -0
  28. package/dist/llm/providers/anthropic.d.ts.map +1 -0
  29. package/dist/llm/providers/anthropic.js +104 -0
  30. package/dist/phases/act.d.ts.map +1 -1
  31. package/dist/phases/act.js +20 -6
  32. package/dist/phases/observe.d.ts.map +1 -1
  33. package/dist/phases/observe.js +20 -2
  34. package/dist/phases/think-finalize.d.ts.map +1 -1
  35. package/dist/phases/think-finalize.js +12 -3
  36. package/dist/phases/think.d.ts.map +1 -1
  37. package/dist/phases/think.js +14 -2
  38. package/dist/schemas/automation-maturity.schema.d.ts +78 -0
  39. package/dist/schemas/automation-maturity.schema.d.ts.map +1 -0
  40. package/dist/schemas/automation-maturity.schema.js +24 -0
  41. package/dist/schemas/config.schema.d.ts +37 -0
  42. package/dist/schemas/config.schema.d.ts.map +1 -1
  43. package/dist/schemas/config.schema.js +11 -0
  44. package/dist/schemas/gap-analysis.schema.d.ts +6 -6
  45. package/dist/schemas/index.d.ts +2 -1
  46. package/dist/schemas/index.d.ts.map +1 -1
  47. package/dist/schemas/index.js +2 -1
  48. package/dist/schemas/public-surface.schema.d.ts +4 -4
  49. package/dist/schemas/repo-analysis.schema.d.ts +134 -0
  50. package/dist/schemas/repo-analysis.schema.d.ts.map +1 -1
  51. package/dist/schemas/repo-analysis.schema.js +29 -0
  52. package/dist/telemetry/emit.d.ts +3 -0
  53. package/dist/telemetry/emit.d.ts.map +1 -0
  54. package/dist/telemetry/emit.js +11 -0
  55. package/dist/telemetry/telemetry.interface.d.ts +13 -0
  56. package/dist/telemetry/telemetry.interface.d.ts.map +1 -0
  57. package/dist/telemetry/telemetry.interface.js +3 -0
  58. package/dist/tools/automation-maturity.d.ts +4 -0
  59. package/dist/tools/automation-maturity.d.ts.map +1 -0
  60. package/dist/tools/automation-maturity.js +163 -0
  61. package/dist/tools/framework-detector.d.ts +15 -0
  62. package/dist/tools/framework-detector.d.ts.map +1 -0
  63. package/dist/tools/framework-detector.js +153 -0
  64. package/dist/tools/gap-engine.d.ts +1 -1
  65. package/dist/tools/gap-engine.d.ts.map +1 -1
  66. package/dist/tools/gap-engine.js +13 -3
  67. package/dist/tools/repo-scanner.d.ts +16 -0
  68. package/dist/tools/repo-scanner.d.ts.map +1 -1
  69. package/dist/tools/repo-scanner.js +31 -2
  70. package/package.json +1 -1
@@ -1,4 +1,25 @@
1
1
  import { z } from 'zod';
2
+ export declare const DetectedFrameworkPrimarySchema: z.ZodEnum<["nextjs-app-router", "nextjs-pages-router", "express", "remix", "nuxt", "sveltekit", "astro", "vite", "unknown"]>;
3
+ export declare const FrameworkDetectionConfidenceSchema: z.ZodEnum<["high", "medium", "low"]>;
4
+ export declare const TestFrameworkDetectedSchema: z.ZodEnum<["playwright", "cypress-e2e", "cypress-component", "jest", "vitest", "other"]>;
5
+ export declare const FrameworkDetectionSchema: z.ZodObject<{
6
+ primary: z.ZodEnum<["nextjs-app-router", "nextjs-pages-router", "express", "remix", "nuxt", "sveltekit", "astro", "vite", "unknown"]>;
7
+ confidence: z.ZodEnum<["high", "medium", "low"]>;
8
+ evidence: z.ZodArray<z.ZodString, "many">;
9
+ testFrameworks: z.ZodArray<z.ZodEnum<["playwright", "cypress-e2e", "cypress-component", "jest", "vitest", "other"]>, "many">;
10
+ }, "strip", z.ZodTypeAny, {
11
+ confidence: "high" | "medium" | "low";
12
+ evidence: string[];
13
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
14
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
15
+ }, {
16
+ confidence: "high" | "medium" | "low";
17
+ evidence: string[];
18
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
19
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
20
+ }>;
21
+ export type DetectedFrameworkPrimary = z.infer<typeof DetectedFrameworkPrimarySchema>;
22
+ export type FrameworkDetectionResult = z.infer<typeof FrameworkDetectionSchema>;
2
23
  export declare const RepoRouteSchema: z.ZodObject<{
3
24
  path: z.ZodString;
4
25
  file: z.ZodString;
@@ -111,6 +132,77 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
111
132
  fixturesFolder?: string | undefined;
112
133
  supportFolder?: string | undefined;
113
134
  }>;
135
+ framework: z.ZodOptional<z.ZodObject<{
136
+ primary: z.ZodEnum<["nextjs-app-router", "nextjs-pages-router", "express", "remix", "nuxt", "sveltekit", "astro", "vite", "unknown"]>;
137
+ confidence: z.ZodEnum<["high", "medium", "low"]>;
138
+ evidence: z.ZodArray<z.ZodString, "many">;
139
+ testFrameworks: z.ZodArray<z.ZodEnum<["playwright", "cypress-e2e", "cypress-component", "jest", "vitest", "other"]>, "many">;
140
+ }, "strip", z.ZodTypeAny, {
141
+ confidence: "high" | "medium" | "low";
142
+ evidence: string[];
143
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
144
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
145
+ }, {
146
+ confidence: "high" | "medium" | "low";
147
+ evidence: string[];
148
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
149
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
150
+ }>>;
151
+ automationMaturity: z.ZodOptional<z.ZodObject<{
152
+ computedAt: z.ZodString;
153
+ repoPath: z.ZodString;
154
+ overallScore: z.ZodNumber;
155
+ level: z.ZodNumber;
156
+ label: z.ZodString;
157
+ dimensions: z.ZodArray<z.ZodObject<{
158
+ dimension: z.ZodEnum<["test-coverage-breadth", "framework-adoption", "test-id-hygiene", "ci-integration", "auth-test-coverage", "component-test-ratio"]>;
159
+ score: z.ZodNumber;
160
+ weight: z.ZodNumber;
161
+ evidence: z.ZodArray<z.ZodString, "many">;
162
+ recommendations: z.ZodArray<z.ZodString, "many">;
163
+ }, "strip", z.ZodTypeAny, {
164
+ recommendations: string[];
165
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
166
+ score: number;
167
+ weight: number;
168
+ evidence: string[];
169
+ }, {
170
+ recommendations: string[];
171
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
172
+ score: number;
173
+ weight: number;
174
+ evidence: string[];
175
+ }>, "many">;
176
+ topRecommendations: z.ZodArray<z.ZodString, "many">;
177
+ }, "strip", z.ZodTypeAny, {
178
+ label: string;
179
+ level: number;
180
+ computedAt: string;
181
+ repoPath: string;
182
+ overallScore: number;
183
+ dimensions: {
184
+ recommendations: string[];
185
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
186
+ score: number;
187
+ weight: number;
188
+ evidence: string[];
189
+ }[];
190
+ topRecommendations: string[];
191
+ }, {
192
+ label: string;
193
+ level: number;
194
+ computedAt: string;
195
+ repoPath: string;
196
+ overallScore: number;
197
+ dimensions: {
198
+ recommendations: string[];
199
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
200
+ score: number;
201
+ weight: number;
202
+ evidence: string[];
203
+ }[];
204
+ topRecommendations: string[];
205
+ }>>;
114
206
  }, "strip", z.ZodTypeAny, {
115
207
  scannedAt: string;
116
208
  routes: {
@@ -135,6 +227,27 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
135
227
  fixturesFolder?: string | undefined;
136
228
  supportFolder?: string | undefined;
137
229
  };
230
+ framework?: {
231
+ confidence: "high" | "medium" | "low";
232
+ evidence: string[];
233
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
234
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
235
+ } | undefined;
236
+ automationMaturity?: {
237
+ label: string;
238
+ level: number;
239
+ computedAt: string;
240
+ repoPath: string;
241
+ overallScore: number;
242
+ dimensions: {
243
+ recommendations: string[];
244
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
245
+ score: number;
246
+ weight: number;
247
+ evidence: string[];
248
+ }[];
249
+ topRecommendations: string[];
250
+ } | undefined;
138
251
  }, {
139
252
  scannedAt: string;
140
253
  routes: {
@@ -159,6 +272,27 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
159
272
  fixturesFolder?: string | undefined;
160
273
  supportFolder?: string | undefined;
161
274
  };
275
+ framework?: {
276
+ confidence: "high" | "medium" | "low";
277
+ evidence: string[];
278
+ primary: "unknown" | "nextjs-app-router" | "nextjs-pages-router" | "express" | "remix" | "nuxt" | "sveltekit" | "astro" | "vite";
279
+ testFrameworks: ("playwright" | "cypress-e2e" | "cypress-component" | "jest" | "vitest" | "other")[];
280
+ } | undefined;
281
+ automationMaturity?: {
282
+ label: string;
283
+ level: number;
284
+ computedAt: string;
285
+ repoPath: string;
286
+ overallScore: number;
287
+ dimensions: {
288
+ recommendations: string[];
289
+ dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
290
+ score: number;
291
+ weight: number;
292
+ evidence: string[];
293
+ }[];
294
+ topRecommendations: string[];
295
+ } | undefined;
162
296
  }>;
163
297
  export type RepoAnalysis = z.infer<typeof RepoAnalysisSchema>;
164
298
  export type CypressStructure = z.infer<typeof CypressStructureSchema>;
@@ -1 +1 @@
1
- {"version":3,"file":"repo-analysis.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/repo-analysis.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,eAAe;;;;;;;;;;;;EAI1B,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;EAIzB,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;EASjC,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAO7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
1
+ {"version":3,"file":"repo-analysis.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/repo-analysis.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,8BAA8B,8HAUzC,CAAC;AAEH,eAAO,MAAM,kCAAkC,sCAAoC,CAAC;AAEpF,eAAO,MAAM,2BAA2B,0FAOtC,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;EAKnC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AACtF,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF,eAAO,MAAM,eAAe;;;;;;;;;;;;EAI1B,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;EAIzB,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;EASjC,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAS7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
@@ -1,4 +1,31 @@
1
1
  import { z } from 'zod';
2
+ import { AutomationMaturitySchema } from './automation-maturity.schema.js';
3
+ export const DetectedFrameworkPrimarySchema = z.enum([
4
+ 'nextjs-app-router',
5
+ 'nextjs-pages-router',
6
+ 'express',
7
+ 'remix',
8
+ 'nuxt',
9
+ 'sveltekit',
10
+ 'astro',
11
+ 'vite',
12
+ 'unknown',
13
+ ]);
14
+ export const FrameworkDetectionConfidenceSchema = z.enum(['high', 'medium', 'low']);
15
+ export const TestFrameworkDetectedSchema = z.enum([
16
+ 'playwright',
17
+ 'cypress-e2e',
18
+ 'cypress-component',
19
+ 'jest',
20
+ 'vitest',
21
+ 'other',
22
+ ]);
23
+ export const FrameworkDetectionSchema = z.object({
24
+ primary: DetectedFrameworkPrimarySchema,
25
+ confidence: FrameworkDetectionConfidenceSchema,
26
+ evidence: z.array(z.string()),
27
+ testFrameworks: z.array(TestFrameworkDetectedSchema),
28
+ });
2
29
  export const RepoRouteSchema = z.object({
3
30
  path: z.string(),
4
31
  file: z.string(),
@@ -26,4 +53,6 @@ export const RepoAnalysisSchema = z.object({
26
53
  testFiles: z.array(TestFileSchema),
27
54
  missingTestIds: z.array(z.string()),
28
55
  cypressStructure: CypressStructureSchema,
56
+ framework: FrameworkDetectionSchema.optional(),
57
+ automationMaturity: AutomationMaturitySchema.optional(),
29
58
  });
@@ -0,0 +1,3 @@
1
+ import type { TelemetryEvent, TelemetryEventKind, TelemetrySink } from './telemetry.interface.js';
2
+ export declare function emitTelemetry(sink: TelemetrySink | undefined, kind: TelemetryEventKind, sessionId: string, metadata: TelemetryEvent['metadata'], durationMs?: number): void;
3
+ //# sourceMappingURL=emit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../src/telemetry/emit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAElG,wBAAgB,aAAa,CAC3B,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,IAAI,EAAE,kBAAkB,EACxB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,EACpC,UAAU,CAAC,EAAE,MAAM,GAClB,IAAI,CASN"}
@@ -0,0 +1,11 @@
1
+ export function emitTelemetry(sink, kind, sessionId, metadata, durationMs) {
2
+ if (!sink)
3
+ return;
4
+ sink.emit({
5
+ kind,
6
+ timestamp: new Date().toISOString(),
7
+ sessionId,
8
+ metadata,
9
+ ...(durationMs !== undefined && { durationMs }),
10
+ });
11
+ }
@@ -0,0 +1,13 @@
1
+ export type TelemetryEventKind = 'scan.started' | 'scan.completed' | 'scan.blocked' | 'phase.observe.started' | 'phase.observe.completed' | 'phase.think.started' | 'phase.think.completed' | 'phase.act.started' | 'phase.act.completed' | 'llm.call.started' | 'llm.call.completed' | 'llm.call.failed' | 'gap.detected' | 'auth.detected' | 'repo.scanned';
2
+ export interface TelemetryEvent {
3
+ kind: TelemetryEventKind;
4
+ timestamp: string;
5
+ sessionId: string;
6
+ durationMs?: number;
7
+ metadata: Record<string, string | number | boolean | null>;
8
+ }
9
+ export interface TelemetrySink {
10
+ emit(event: TelemetryEvent): void;
11
+ }
12
+ export declare const NoopTelemetrySink: TelemetrySink;
13
+ //# sourceMappingURL=telemetry.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.interface.d.ts","sourceRoot":"","sources":["../../src/telemetry/telemetry.interface.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAC1B,cAAc,GACd,gBAAgB,GAChB,cAAc,GACd,uBAAuB,GACvB,yBAAyB,GACzB,qBAAqB,GACrB,uBAAuB,GACvB,mBAAmB,GACnB,qBAAqB,GACrB,kBAAkB,GAClB,oBAAoB,GACpB,iBAAiB,GACjB,cAAc,GACd,eAAe,GACf,cAAc,CAAC;AAEnB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;CACnC;AAED,eAAO,MAAM,iBAAiB,EAAE,aAE/B,CAAC"}
@@ -0,0 +1,3 @@
1
+ export const NoopTelemetrySink = {
2
+ emit: () => { },
3
+ };
@@ -0,0 +1,4 @@
1
+ import type { RepoAnalysis } from '../schemas/repo-analysis.schema.js';
2
+ import type { AutomationMaturity } from '../schemas/automation-maturity.schema.js';
3
+ export declare function computeAutomationMaturity(repo: RepoAnalysis): AutomationMaturity;
4
+ //# sourceMappingURL=automation-maturity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"automation-maturity.d.ts","sourceRoot":"","sources":["../../src/tools/automation-maturity.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAA+B,MAAM,0CAA0C,CAAC;AAiDhH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,kBAAkB,CA6HhF"}
@@ -0,0 +1,163 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { AutomationMaturitySchema } from '../schemas/automation-maturity.schema.js';
4
+ /**
5
+ * Dimension weights (sum = 1). Breadth + harness adoption dominate: shipping risk is mostly
6
+ * untested routes and missing Playwright/Cypress-level coverage.
7
+ */
8
+ const W_TEST_BREADTH = 0.28;
9
+ const W_FRAMEWORK = 0.22;
10
+ const W_TEST_ID = 0.18;
11
+ const W_CI = 0.14;
12
+ const W_AUTH_TESTS = 0.1;
13
+ const W_COMPONENT_RATIO = 0.08;
14
+ function hasCiAtRoot(repoPath) {
15
+ const ev = [];
16
+ const gh = join(repoPath, '.github', 'workflows');
17
+ if (existsSync(gh) && statSync(gh).isDirectory()) {
18
+ try {
19
+ const files = readdirSync(gh).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
20
+ if (files.length > 0) {
21
+ ev.push(`.github/workflows (${files.length} workflow file(s))`);
22
+ return { ok: true, evidence: ev };
23
+ }
24
+ }
25
+ catch {
26
+ /* ignore */
27
+ }
28
+ }
29
+ if (existsSync(join(repoPath, '.circleci'))) {
30
+ ev.push('.circleci/ present');
31
+ return { ok: true, evidence: ev };
32
+ }
33
+ for (const f of ['.gitlab-ci.yml', 'Jenkinsfile']) {
34
+ if (existsSync(join(repoPath, f))) {
35
+ ev.push(`${f} present`);
36
+ return { ok: true, evidence: ev };
37
+ }
38
+ }
39
+ return { ok: false, evidence: ['No GitHub Actions, CircleCI, GitLab CI, or Jenkinsfile detected at repo root'] };
40
+ }
41
+ function scoreLevel(overall) {
42
+ if (overall < 20)
43
+ return { level: 1, label: 'L1 — nascent automation' };
44
+ if (overall < 40)
45
+ return { level: 2, label: 'L2 — emerging coverage' };
46
+ if (overall < 60)
47
+ return { level: 3, label: 'L3 — building maturity' };
48
+ if (overall < 80)
49
+ return { level: 4, label: 'L4 — strong automation' };
50
+ return { level: 5, label: 'L5 — advanced QA automation' };
51
+ }
52
+ export function computeAutomationMaturity(repo) {
53
+ const routePaths = [...new Set(repo.routes.map((r) => r.path))];
54
+ let coveredRoutes = 0;
55
+ for (const p of routePaths) {
56
+ const covered = repo.testFiles.some((tf) => tf.coveredPaths.some((c) => p === c || (c !== '/' && p.startsWith(c))));
57
+ if (covered)
58
+ coveredRoutes++;
59
+ }
60
+ const breadthScore = routePaths.length === 0 ? 100 : Math.round((100 * coveredRoutes) / routePaths.length);
61
+ const breadthDim = {
62
+ dimension: 'test-coverage-breadth',
63
+ score: breadthScore,
64
+ weight: W_TEST_BREADTH,
65
+ evidence: routePaths.length === 0
66
+ ? ['No static routes inferred from repo layout']
67
+ : [
68
+ `${coveredRoutes}/${routePaths.length} inferred routes appear in at least one test coveredPaths`,
69
+ ],
70
+ recommendations: breadthScore >= 80
71
+ ? []
72
+ : ['Add route-level smoke tests that assert critical paths referenced in production URLs.'],
73
+ };
74
+ const types = new Set(repo.testFiles.map((t) => t.type));
75
+ let frameworkScore = 0;
76
+ const fwEvidence = [`Test runners seen: ${[...types].join(', ') || 'none'}`];
77
+ if (types.has('playwright') || types.has('cypress-e2e') || types.has('cypress-component')) {
78
+ frameworkScore = 100;
79
+ fwEvidence.push('Playwright or Cypress present — good browser harness signal.');
80
+ }
81
+ else if (types.has('jest') || types.has('vitest')) {
82
+ frameworkScore = 55;
83
+ fwEvidence.push('Jest/Vitest only — add Playwright or Cypress for deployment-facing checks.');
84
+ }
85
+ else if (repo.testFiles.length > 0) {
86
+ frameworkScore = 30;
87
+ fwEvidence.push('Tests exist but no recognized browser harness in scanned files.');
88
+ }
89
+ else {
90
+ frameworkScore = 0;
91
+ fwEvidence.push('No test files matched qulib scan globs.');
92
+ }
93
+ const frameworkDim = {
94
+ dimension: 'framework-adoption',
95
+ score: frameworkScore,
96
+ weight: W_FRAMEWORK,
97
+ evidence: fwEvidence,
98
+ recommendations: frameworkScore >= 80 ? [] : ['Standardize on Playwright or Cypress for E2E against deployed URLs.'],
99
+ };
100
+ const hygienePenalty = Math.min(100, repo.missingTestIds.length * 6);
101
+ const hygieneScore = Math.max(0, 100 - hygienePenalty);
102
+ const hygieneDim = {
103
+ dimension: 'test-id-hygiene',
104
+ score: hygieneScore,
105
+ weight: W_TEST_ID,
106
+ evidence: [
107
+ `${repo.missingTestIds.length} TSX file(s) with interactive markup but no data-testid (heuristic scan).`,
108
+ ],
109
+ recommendations: hygieneScore >= 85 ? [] : ['Add stable data-testid (or role-based selectors) on interactive components used in tests.'],
110
+ };
111
+ const ci = hasCiAtRoot(repo.repoPath);
112
+ const ciDim = {
113
+ dimension: 'ci-integration',
114
+ score: ci.ok ? 100 : 0,
115
+ weight: W_CI,
116
+ evidence: ci.evidence,
117
+ recommendations: ci.ok ? [] : ['Add a CI workflow that runs unit/E2E tests on every PR.'],
118
+ };
119
+ const authRe = /\/(login|auth|signin)(\/|$)/i;
120
+ const authCovered = repo.testFiles.some((tf) => tf.coveredPaths.some((c) => authRe.test(c)));
121
+ const authScore = authCovered ? 90 : 25;
122
+ const authDim = {
123
+ dimension: 'auth-test-coverage',
124
+ score: authScore,
125
+ weight: W_AUTH_TESTS,
126
+ evidence: authCovered
127
+ ? ['At least one test references /login, /auth, or /signin in coveredPaths.']
128
+ : ['No obvious auth-route coverage in extracted test path strings.'],
129
+ recommendations: authCovered ? [] : ['Add focused tests for sign-in and post-auth landing behavior.'],
130
+ };
131
+ const cypressE2e = repo.testFiles.filter((t) => t.type === 'cypress-e2e').length;
132
+ const cypressComp = repo.testFiles.filter((t) => t.type === 'cypress-component').length;
133
+ const cypressTotal = cypressE2e + cypressComp;
134
+ const compRatioScore = cypressTotal === 0 ? 50 : Math.round((100 * cypressComp) / cypressTotal);
135
+ const compDim = {
136
+ dimension: 'component-test-ratio',
137
+ score: compRatioScore,
138
+ weight: W_COMPONENT_RATIO,
139
+ evidence: [
140
+ `Cypress e2e files (matched): ${cypressE2e}, component: ${cypressComp}.`,
141
+ ],
142
+ recommendations: cypressComp === 0 || cypressTotal === 0
143
+ ? []
144
+ : ['Balance component vs E2E Cypress tests so critical flows stay fast in CI.'],
145
+ };
146
+ const dimensions = [breadthDim, frameworkDim, hygieneDim, ciDim, authDim, compDim];
147
+ const overallScore = Math.round(dimensions.reduce((s, d) => s + d.score * d.weight, 0));
148
+ const { level, label } = scoreLevel(overallScore);
149
+ const topRecommendations = [...dimensions]
150
+ .sort((a, b) => a.score - b.score)
151
+ .flatMap((d) => d.recommendations)
152
+ .filter(Boolean)
153
+ .slice(0, 8);
154
+ return AutomationMaturitySchema.parse({
155
+ computedAt: new Date().toISOString(),
156
+ repoPath: repo.repoPath,
157
+ overallScore,
158
+ level,
159
+ label,
160
+ dimensions,
161
+ topRecommendations,
162
+ });
163
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @module framework-detector
3
+ * @packageBoundary @qulib/core (candidate: @qulib/analyzer)
4
+ *
5
+ * Framework detection runs during the observe phase as part of repo scanning.
6
+ * It is a pure static analysis operation with no browser or LLM dependency.
7
+ * Move this to @qulib/analyzer when that package is created.
8
+ *
9
+ * // TODO(@qulib/analyzer): When @qulib/analyzer is extracted, this module should move there.
10
+ * // It is currently embedded in @qulib/core because repo scanning is part of the observe phase.
11
+ * // The package boundary decision: core = runtime QA analysis, analyzer = static repo intelligence.
12
+ */
13
+ import { type FrameworkDetectionResult } from '../schemas/repo-analysis.schema.js';
14
+ export declare function detectFramework(repoPath: string): Promise<FrameworkDetectionResult>;
15
+ //# sourceMappingURL=framework-detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"framework-detector.d.ts","sourceRoot":"","sources":["../../src/tools/framework-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAA4B,KAAK,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAuB7G,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC,CA4GzF"}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @module framework-detector
3
+ * @packageBoundary @qulib/core (candidate: @qulib/analyzer)
4
+ *
5
+ * Framework detection runs during the observe phase as part of repo scanning.
6
+ * It is a pure static analysis operation with no browser or LLM dependency.
7
+ * Move this to @qulib/analyzer when that package is created.
8
+ *
9
+ * // TODO(@qulib/analyzer): When @qulib/analyzer is extracted, this module should move there.
10
+ * // It is currently embedded in @qulib/core because repo scanning is part of the observe phase.
11
+ * // The package boundary decision: core = runtime QA analysis, analyzer = static repo intelligence.
12
+ */
13
+ import { access, readFile } from 'node:fs/promises';
14
+ import { constants } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { FrameworkDetectionSchema } from '../schemas/repo-analysis.schema.js';
17
+ async function fileExists(repoPath, rel) {
18
+ try {
19
+ await access(join(repoPath, rel), constants.F_OK);
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ function depNames(pkg) {
27
+ return new Set([
28
+ ...Object.keys(pkg.dependencies ?? {}),
29
+ ...Object.keys(pkg.devDependencies ?? {}),
30
+ ]);
31
+ }
32
+ export async function detectFramework(repoPath) {
33
+ const evidence = [];
34
+ const testFrameworks = new Set();
35
+ let pkg = {};
36
+ try {
37
+ const raw = await readFile(join(repoPath, 'package.json'), 'utf8');
38
+ pkg = JSON.parse(raw);
39
+ evidence.push('read package.json');
40
+ }
41
+ catch {
42
+ evidence.push('package.json missing or unreadable');
43
+ }
44
+ const deps = depNames(pkg);
45
+ const has = (n) => deps.has(n);
46
+ if (has('@playwright/test') || has('playwright')) {
47
+ testFrameworks.add('playwright');
48
+ evidence.push('dependency: @playwright/test or playwright');
49
+ }
50
+ if (has('cypress')) {
51
+ testFrameworks.add('cypress-e2e');
52
+ evidence.push('dependency: cypress');
53
+ }
54
+ if (has('jest')) {
55
+ testFrameworks.add('jest');
56
+ evidence.push('dependency: jest');
57
+ }
58
+ if (has('vitest')) {
59
+ testFrameworks.add('vitest');
60
+ evidence.push('dependency: vitest');
61
+ }
62
+ if (testFrameworks.size === 0) {
63
+ testFrameworks.add('other');
64
+ }
65
+ const nextCfg = (await fileExists(repoPath, 'next.config.js')) ||
66
+ (await fileExists(repoPath, 'next.config.mjs')) ||
67
+ (await fileExists(repoPath, 'next.config.ts'));
68
+ const nuxtCfg = await fileExists(repoPath, 'nuxt.config.ts');
69
+ const svelteCfg = await fileExists(repoPath, 'svelte.config.js');
70
+ const astroCfg = await fileExists(repoPath, 'astro.config.mjs');
71
+ const remixCfg = await fileExists(repoPath, 'remix.config.js');
72
+ const viteCfg = await fileExists(repoPath, 'vite.config.ts');
73
+ if (nextCfg)
74
+ evidence.push('found next.config.*');
75
+ if (nuxtCfg)
76
+ evidence.push('found nuxt.config.ts');
77
+ if (svelteCfg)
78
+ evidence.push('found svelte.config.js');
79
+ if (astroCfg)
80
+ evidence.push('found astro.config.mjs');
81
+ if (remixCfg)
82
+ evidence.push('found remix.config.js');
83
+ if (viteCfg)
84
+ evidence.push('found vite.config.ts');
85
+ const hasAppDir = await fileExists(repoPath, 'app');
86
+ const hasPagesDir = await fileExists(repoPath, 'pages');
87
+ if (has('next') && hasAppDir)
88
+ evidence.push('Next.js app/ directory present');
89
+ if (has('next') && hasPagesDir)
90
+ evidence.push('Next.js pages/ directory present');
91
+ if (has('@remix-run/react') || has('@remix-run/node'))
92
+ evidence.push('Remix packages in package.json');
93
+ if (has('nuxt') || has('nuxt3'))
94
+ evidence.push('Nuxt in package.json');
95
+ if (has('@sveltejs/kit'))
96
+ evidence.push('@sveltejs/kit in package.json');
97
+ if (has('astro'))
98
+ evidence.push('astro in package.json');
99
+ if (has('vite') && !has('next'))
100
+ evidence.push('vite in package.json (non-Next)');
101
+ let primary = 'unknown';
102
+ let confidence = 'low';
103
+ if (has('next')) {
104
+ if (hasAppDir && (await fileExists(repoPath, join('app', 'layout.tsx')))) {
105
+ primary = 'nextjs-app-router';
106
+ confidence = nextCfg || hasAppDir ? 'high' : 'medium';
107
+ }
108
+ else if (hasPagesDir) {
109
+ primary = 'nextjs-pages-router';
110
+ confidence = nextCfg || hasPagesDir ? 'high' : 'medium';
111
+ }
112
+ else {
113
+ primary = 'nextjs-app-router';
114
+ confidence = 'medium';
115
+ evidence.push('next detected without clear app/ vs pages/ layout');
116
+ }
117
+ }
118
+ else if (has('@remix-run/react') || remixCfg) {
119
+ primary = 'remix';
120
+ confidence = remixCfg ? 'high' : 'medium';
121
+ }
122
+ else if (has('nuxt') || nuxtCfg) {
123
+ primary = 'nuxt';
124
+ confidence = nuxtCfg ? 'high' : 'medium';
125
+ }
126
+ else if (has('@sveltejs/kit') || svelteCfg) {
127
+ primary = 'sveltekit';
128
+ confidence = svelteCfg ? 'high' : 'medium';
129
+ }
130
+ else if (has('astro') || astroCfg) {
131
+ primary = 'astro';
132
+ confidence = astroCfg ? 'high' : 'medium';
133
+ }
134
+ else if (viteCfg && !has('next')) {
135
+ primary = 'vite';
136
+ confidence = 'medium';
137
+ }
138
+ else if (has('express')) {
139
+ primary = 'express';
140
+ confidence = 'medium';
141
+ evidence.push('express listed in dependencies');
142
+ }
143
+ else {
144
+ /* keep unknown */
145
+ }
146
+ const raw = {
147
+ primary,
148
+ confidence,
149
+ evidence,
150
+ testFrameworks: [...testFrameworks],
151
+ };
152
+ return FrameworkDetectionSchema.parse(raw);
153
+ }
@@ -2,7 +2,7 @@ import { type GapAnalysis, type Gap } from '../schemas/gap-analysis.schema.js';
2
2
  import type { RouteInventory } from '../schemas/route-inventory.schema.js';
3
3
  import type { RepoAnalysis } from '../schemas/repo-analysis.schema.js';
4
4
  import type { HarnessConfig } from '../schemas/config.schema.js';
5
- export declare function computeQualityScoreFromGaps(gaps: Gap[]): number;
5
+ export declare function computeQualityScoreFromGaps(gaps: Gap[], scoringWeights?: HarnessConfig['scoringWeights']): number;
6
6
  export declare function computeCoverageScore(routes: RouteInventory): number | null;
7
7
  export declare function analyzeGaps(routes: RouteInventory, repo: RepoAnalysis | null, mode: 'url-only' | 'url-repo', config: HarnessConfig): Omit<GapAnalysis, 'scenarios' | 'generatedTests'>;
8
8
  //# sourceMappingURL=gap-engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gap-engine.d.ts","sourceRoot":"","sources":["../../src/tools/gap-engine.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,WAAW,EAAE,KAAK,GAAG,EAAE,MAAM,mCAAmC,CAAC;AAC1F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CAY/D;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAa1E;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,GAAG,IAAI,EACzB,IAAI,EAAE,UAAU,GAAG,UAAU,EAC7B,MAAM,EAAE,aAAa,GACpB,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,gBAAgB,CAAC,CAqGnD"}
1
+ {"version":3,"file":"gap-engine.d.ts","sourceRoot":"","sources":["../../src/tools/gap-engine.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,KAAK,WAAW,EAAE,KAAK,GAAG,EAAE,MAAM,mCAAmC,CAAC;AAC1F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAQjE,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,GAAG,EAAE,EACX,cAAc,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,GAC/C,MAAM,CAkBR;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAa1E;AAED,wBAAgB,WAAW,CACzB,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,YAAY,GAAG,IAAI,EACzB,IAAI,EAAE,UAAU,GAAG,UAAU,EAC7B,MAAM,EAAE,aAAa,GACpB,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,gBAAgB,CAAC,CAqGnD"}
@@ -1,6 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { GapSchema } from '../schemas/gap-analysis.schema.js';
3
- export function computeQualityScoreFromGaps(gaps) {
3
+ // TODO: Add category-specific weight overrides (e.g., auth-surface gaps should cost more than untested-route gaps by default).
4
+ // Requires a 2D weight matrix: { [severity]: { [category]: number } }.
5
+ // Deferred until there is empirical data from real scan runs to calibrate.
6
+ const DEFAULT_SCORING_WEIGHTS = { critical: 25, high: 20, medium: 8, low: 3 };
7
+ export function computeQualityScoreFromGaps(gaps, scoringWeights) {
4
8
  let critical = 0;
5
9
  let high = 0;
6
10
  let medium = 0;
@@ -15,7 +19,13 @@ export function computeQualityScoreFromGaps(gaps) {
15
19
  else
16
20
  low++;
17
21
  }
18
- return Math.max(0, 100 - critical * 25 - high * 20 - medium * 8 - low * 3);
22
+ const w = {
23
+ critical: scoringWeights?.critical ?? DEFAULT_SCORING_WEIGHTS.critical,
24
+ high: scoringWeights?.high ?? DEFAULT_SCORING_WEIGHTS.high,
25
+ medium: scoringWeights?.medium ?? DEFAULT_SCORING_WEIGHTS.medium,
26
+ low: scoringWeights?.low ?? DEFAULT_SCORING_WEIGHTS.low,
27
+ };
28
+ return Math.max(0, 100 - critical * w.critical - high * w.high - medium * w.medium - low * w.low);
19
29
  }
20
30
  export function computeCoverageScore(routes) {
21
31
  const scanned = routes.routes.length;
@@ -104,7 +114,7 @@ export function analyzeGaps(routes, repo, mode, config) {
104
114
  });
105
115
  }
106
116
  }
107
- const releaseConfidence = computeQualityScoreFromGaps(gaps);
117
+ const releaseConfidence = computeQualityScoreFromGaps(gaps, config.scoringWeights);
108
118
  const pagesScanned = routes.routes.length;
109
119
  let coverageWarning;
110
120
  if (routes.budgetExceeded) {