@democratize-quality/qualitylens-core 0.2.5 → 0.2.6
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.
- package/dist/core/config.d.mts +11 -0
- package/dist/core/config.d.ts +11 -0
- package/dist/core/config.js +15 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/config.mjs +15 -2
- package/dist/core/config.mjs.map +1 -1
- package/dist/core/engine.js +2 -2
- package/dist/core/engine.js.map +1 -1
- package/dist/core/engine.mjs +2 -2
- package/dist/core/engine.mjs.map +1 -1
- package/dist/core/types.d.mts +6 -1
- package/dist/core/types.d.ts +6 -1
- package/dist/core/types.js.map +1 -1
- package/package.json +1 -1
package/dist/core/engine.js
CHANGED
|
@@ -78,9 +78,9 @@ var CoverageEngine = class {
|
|
|
78
78
|
}
|
|
79
79
|
const routeCoverages = routes.map((route) => {
|
|
80
80
|
const key = route.method ? `${route.method} ${route.path}` : route.path;
|
|
81
|
-
const matched = matchMap.get(key) ?? [];
|
|
81
|
+
const matched = route.method ? [...matchMap.get(key) ?? [], ...matchMap.get(route.path) ?? []] : matchMap.get(key) ?? [];
|
|
82
82
|
const automatedTests = matched.filter((t) => t.source === "playwright");
|
|
83
|
-
const manualTests = matched.filter((t) => t.source === "ado" || t.source === "yaml");
|
|
83
|
+
const manualTests = matched.filter((t) => t.source === "ado" || t.source === "yaml" || t.source === "csv");
|
|
84
84
|
const status = this.deriveStatus(automatedTests.length, manualTests.length);
|
|
85
85
|
const lastTestedAt = this.latestDate([...automatedTests, ...manualTests]);
|
|
86
86
|
const staleness = this.deriveStaleness(lastTestedAt, manualTests.length);
|
package/dist/core/engine.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/engine.ts"],"sourcesContent":["/**\n * src/core/engine.ts\n * \n * The main orchestrator. Zero I/O — all dependencies injected.\n * This design makes the engine fully unit-testable with mock sources.\n * \n * AI INSTRUCTIONS: implement run() using the step-by-step comments.\n */\n\nimport {\n CoverageReport,\n CoverageStatus,\n RouteGap,\n RouteCoverage,\n Staleness,\n TestEntry,\n TestGapConfig,\n TestHint,\n} from './types'\nimport { BaseSource } from '../sources/base.source'\nimport { RoutesSource } from '../sources/routes.source'\nimport { FuzzyMatcher } from '../matchers/fuzzy.matcher'\nimport { AreaMatcher } from '../matchers/area.matcher'\n\nexport class CoverageEngine {\n constructor(\n private sources: BaseSource[],\n private routesSource: RoutesSource,\n private matcher: FuzzyMatcher,\n private areaMatcher: AreaMatcher,\n private config: TestGapConfig\n ) {}\n\n async run(): Promise<CoverageReport> {\n // Step 1: Collect from all sources in parallel\n const sourceResults = await Promise.all(this.sources.map(s => s.collect()))\n const allTests: TestEntry[] = sourceResults.flat()\n\n // Step 2: Discover routes\n const routes = await this.routesSource.collect()\n\n // Step 3: Match tests to routes\n const matchMap = this.matcher.match(routes, allTests)\n\n // Collect all matched tests so we can find orphans after\n const matchedTests = new Set<TestEntry>()\n for (const tests of matchMap.values()) {\n for (const t of tests) matchedTests.add(t)\n }\n\n // Step 3b: Apply testHints to fuzzy-unmatched tests\n const hints = this.config.testHints ?? []\n const hintMap = new Map<string, TestHint>()\n for (const hint of hints) hintMap.set(hint.test, hint)\n\n const routeGaps: RouteGap[] = []\n const acknowledgedTests: TestHint[] = []\n const hintResolved = new Set<TestEntry>()\n\n for (const test of allTests) {\n if (matchedTests.has(test)) continue // already matched by fuzzy\n const hint = hintMap.get(test.title)\n if (!hint) continue\n\n if (hint.status === 'resolved' && hint.route) {\n // Inject into matchMap so RouteCoverage picks it up\n const key = hint.method ? `${hint.method} ${hint.route}` : hint.route\n const existing = matchMap.get(key) ?? []\n matchMap.set(key, [...existing, test])\n matchedTests.add(test)\n hintResolved.add(test)\n } else if (hint.status === 'gap') {\n const existing = routeGaps.find(g =>\n g.route === hint.route && g.method === hint.method\n )\n if (existing) {\n existing.coveredByTests.push(test)\n } else {\n routeGaps.push({\n route: hint.route ?? test.title,\n method: hint.method,\n reason: hint.reason,\n coveredByTests: [test],\n })\n }\n matchedTests.add(test)\n } else if (hint.status === 'acknowledged' || hint.status === 'ignore') {\n acknowledgedTests.push(hint)\n matchedTests.add(test)\n }\n }\n\n // Step 4: Build RouteCoverage for each route\n const routeCoverages: RouteCoverage[] = routes.map(route => {\n const key = route.method ? `${route.method} ${route.path}` : route.path\n const matched = matchMap.get(key) ?? []\n const automatedTests = matched.filter(t => t.source === 'playwright')\n const manualTests = matched.filter(t => t.source === 'ado' || t.source === 'yaml')\n\n const status = this.deriveStatus(automatedTests.length, manualTests.length)\n const lastTestedAt = this.latestDate([...automatedTests, ...manualTests])\n const staleness = this.deriveStaleness(lastTestedAt, manualTests.length)\n\n return {\n route,\n status,\n automatedTests,\n manualTests,\n staleness,\n lastTestedAt,\n }\n })\n\n // Step 5: Assign to functional areas\n const { areas, uncategorised } = this.areaMatcher.assign(routeCoverages)\n\n // Step 6: Build summary\n const total = routeCoverages.length\n const automated = routeCoverages.filter(r => r.status === 'automated' || r.status === 'both').length\n const manualOnly = routeCoverages.filter(r => r.status === 'manual').length\n const none = routeCoverages.filter(r => r.status === 'none').length\n\n // LRM: apply rounding only to the three mutually exclusive groups so they\n // always sum to 100. totalCoverage is derived, never independently rounded.\n const toLRM = (counts: number[]): number[] => {\n if (total === 0) return counts.map(() => 0)\n const exact = counts.map(n => (n / total) * 100)\n const floored = exact.map(n => Math.floor(n))\n const remainder = 100 - floored.reduce((a, b) => a + b, 0)\n const order = exact.map((n, i) => ({ i, frac: n - Math.floor(n) })).sort((a, b) => b.frac - a.frac)\n for (let k = 0; k < remainder; k++) floored[order[k].i]++\n return floored\n }\n const [automatedCoverage, manualOnlyCoverage, noCoverage] = toLRM([automated, manualOnly, none])\n const totalCoverage = automatedCoverage + manualOnlyCoverage\n\n return {\n generatedAt: new Date(),\n projectName: this.config.projectName,\n summary: {\n totalRoutes: total,\n totalCoverage,\n automatedCoverage,\n manualOnlyCoverage,\n noCoverage,\n },\n areas,\n uncategorised,\n unmatchedTests: allTests.filter(t => !matchedTests.has(t)),\n routeGaps,\n acknowledgedTests,\n }\n }\n\n private deriveStatus(autoCount: number, manualCount: number): CoverageStatus {\n if (autoCount > 0 && manualCount > 0) return 'both'\n if (autoCount > 0) return 'automated'\n if (manualCount > 0) return 'manual'\n return 'none'\n }\n\n private deriveStaleness(lastTestedAt: Date | undefined, manualCount: number): Staleness {\n // Only flag staleness for manual tests — automated tests run on every push\n if (manualCount === 0) return 'unknown'\n if (!lastTestedAt) return 'unknown'\n\n const daysSince = (Date.now() - lastTestedAt.getTime()) / (1000 * 60 * 60 * 24)\n return daysSince > this.config.staleThresholdDays ? 'stale' : 'fresh'\n }\n\n private latestDate(tests: TestEntry[]): Date | undefined {\n const dates = tests.map(t => t.lastRun).filter((d): d is Date => d instanceof Date)\n if (dates.length === 0) return undefined\n return new Date(Math.max(...dates.map(d => d.getTime())))\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/engine.ts"],"sourcesContent":["/**\n * src/core/engine.ts\n * \n * The main orchestrator. Zero I/O — all dependencies injected.\n * This design makes the engine fully unit-testable with mock sources.\n * \n * AI INSTRUCTIONS: implement run() using the step-by-step comments.\n */\n\nimport {\n CoverageReport,\n CoverageStatus,\n RouteGap,\n RouteCoverage,\n Staleness,\n TestEntry,\n TestGapConfig,\n TestHint,\n} from './types'\nimport { BaseSource } from '../sources/base.source'\nimport { RoutesSource } from '../sources/routes.source'\nimport { FuzzyMatcher } from '../matchers/fuzzy.matcher'\nimport { AreaMatcher } from '../matchers/area.matcher'\n\nexport class CoverageEngine {\n constructor(\n private sources: BaseSource[],\n private routesSource: RoutesSource,\n private matcher: FuzzyMatcher,\n private areaMatcher: AreaMatcher,\n private config: TestGapConfig\n ) {}\n\n async run(): Promise<CoverageReport> {\n // Step 1: Collect from all sources in parallel\n const sourceResults = await Promise.all(this.sources.map(s => s.collect()))\n const allTests: TestEntry[] = sourceResults.flat()\n\n // Step 2: Discover routes\n const routes = await this.routesSource.collect()\n\n // Step 3: Match tests to routes\n const matchMap = this.matcher.match(routes, allTests)\n\n // Collect all matched tests so we can find orphans after\n const matchedTests = new Set<TestEntry>()\n for (const tests of matchMap.values()) {\n for (const t of tests) matchedTests.add(t)\n }\n\n // Step 3b: Apply testHints to fuzzy-unmatched tests\n const hints = this.config.testHints ?? []\n const hintMap = new Map<string, TestHint>()\n for (const hint of hints) hintMap.set(hint.test, hint)\n\n const routeGaps: RouteGap[] = []\n const acknowledgedTests: TestHint[] = []\n const hintResolved = new Set<TestEntry>()\n\n for (const test of allTests) {\n if (matchedTests.has(test)) continue // already matched by fuzzy\n const hint = hintMap.get(test.title)\n if (!hint) continue\n\n if (hint.status === 'resolved' && hint.route) {\n // Inject into matchMap so RouteCoverage picks it up\n const key = hint.method ? `${hint.method} ${hint.route}` : hint.route\n const existing = matchMap.get(key) ?? []\n matchMap.set(key, [...existing, test])\n matchedTests.add(test)\n hintResolved.add(test)\n } else if (hint.status === 'gap') {\n const existing = routeGaps.find(g =>\n g.route === hint.route && g.method === hint.method\n )\n if (existing) {\n existing.coveredByTests.push(test)\n } else {\n routeGaps.push({\n route: hint.route ?? test.title,\n method: hint.method,\n reason: hint.reason,\n coveredByTests: [test],\n })\n }\n matchedTests.add(test)\n } else if (hint.status === 'acknowledged' || hint.status === 'ignore') {\n acknowledgedTests.push(hint)\n matchedTests.add(test)\n }\n }\n\n // Step 4: Build RouteCoverage for each route\n const routeCoverages: RouteCoverage[] = routes.map(route => {\n const key = route.method ? `${route.method} ${route.path}` : route.path\n // Also pick up hints resolved without a method (stored under just the path)\n const matched = route.method\n ? [...(matchMap.get(key) ?? []), ...(matchMap.get(route.path) ?? [])]\n : (matchMap.get(key) ?? [])\n const automatedTests = matched.filter(t => t.source === 'playwright')\n const manualTests = matched.filter(t => t.source === 'ado' || t.source === 'yaml' || t.source === 'csv')\n\n const status = this.deriveStatus(automatedTests.length, manualTests.length)\n const lastTestedAt = this.latestDate([...automatedTests, ...manualTests])\n const staleness = this.deriveStaleness(lastTestedAt, manualTests.length)\n\n return {\n route,\n status,\n automatedTests,\n manualTests,\n staleness,\n lastTestedAt,\n }\n })\n\n // Step 5: Assign to functional areas\n const { areas, uncategorised } = this.areaMatcher.assign(routeCoverages)\n\n // Step 6: Build summary\n const total = routeCoverages.length\n const automated = routeCoverages.filter(r => r.status === 'automated' || r.status === 'both').length\n const manualOnly = routeCoverages.filter(r => r.status === 'manual').length\n const none = routeCoverages.filter(r => r.status === 'none').length\n\n // LRM: apply rounding only to the three mutually exclusive groups so they\n // always sum to 100. totalCoverage is derived, never independently rounded.\n const toLRM = (counts: number[]): number[] => {\n if (total === 0) return counts.map(() => 0)\n const exact = counts.map(n => (n / total) * 100)\n const floored = exact.map(n => Math.floor(n))\n const remainder = 100 - floored.reduce((a, b) => a + b, 0)\n const order = exact.map((n, i) => ({ i, frac: n - Math.floor(n) })).sort((a, b) => b.frac - a.frac)\n for (let k = 0; k < remainder; k++) floored[order[k].i]++\n return floored\n }\n const [automatedCoverage, manualOnlyCoverage, noCoverage] = toLRM([automated, manualOnly, none])\n const totalCoverage = automatedCoverage + manualOnlyCoverage\n\n return {\n generatedAt: new Date(),\n projectName: this.config.projectName,\n summary: {\n totalRoutes: total,\n totalCoverage,\n automatedCoverage,\n manualOnlyCoverage,\n noCoverage,\n },\n areas,\n uncategorised,\n unmatchedTests: allTests.filter(t => !matchedTests.has(t)),\n routeGaps,\n acknowledgedTests,\n }\n }\n\n private deriveStatus(autoCount: number, manualCount: number): CoverageStatus {\n if (autoCount > 0 && manualCount > 0) return 'both'\n if (autoCount > 0) return 'automated'\n if (manualCount > 0) return 'manual'\n return 'none'\n }\n\n private deriveStaleness(lastTestedAt: Date | undefined, manualCount: number): Staleness {\n // Only flag staleness for manual tests — automated tests run on every push\n if (manualCount === 0) return 'unknown'\n if (!lastTestedAt) return 'unknown'\n\n const daysSince = (Date.now() - lastTestedAt.getTime()) / (1000 * 60 * 60 * 24)\n return daysSince > this.config.staleThresholdDays ? 'stale' : 'fresh'\n }\n\n private latestDate(tests: TestEntry[]): Date | undefined {\n const dates = tests.map(t => t.lastRun).filter((d): d is Date => d instanceof Date)\n if (dates.length === 0) return undefined\n return new Date(Math.max(...dates.map(d => d.getTime())))\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/core/engine.mjs
CHANGED
|
@@ -54,9 +54,9 @@ var CoverageEngine = class {
|
|
|
54
54
|
}
|
|
55
55
|
const routeCoverages = routes.map((route) => {
|
|
56
56
|
const key = route.method ? `${route.method} ${route.path}` : route.path;
|
|
57
|
-
const matched = matchMap.get(key) ?? [];
|
|
57
|
+
const matched = route.method ? [...matchMap.get(key) ?? [], ...matchMap.get(route.path) ?? []] : matchMap.get(key) ?? [];
|
|
58
58
|
const automatedTests = matched.filter((t) => t.source === "playwright");
|
|
59
|
-
const manualTests = matched.filter((t) => t.source === "ado" || t.source === "yaml");
|
|
59
|
+
const manualTests = matched.filter((t) => t.source === "ado" || t.source === "yaml" || t.source === "csv");
|
|
60
60
|
const status = this.deriveStatus(automatedTests.length, manualTests.length);
|
|
61
61
|
const lastTestedAt = this.latestDate([...automatedTests, ...manualTests]);
|
|
62
62
|
const staleness = this.deriveStaleness(lastTestedAt, manualTests.length);
|
package/dist/core/engine.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/engine.ts"],"sourcesContent":["/**\n * src/core/engine.ts\n * \n * The main orchestrator. Zero I/O — all dependencies injected.\n * This design makes the engine fully unit-testable with mock sources.\n * \n * AI INSTRUCTIONS: implement run() using the step-by-step comments.\n */\n\nimport {\n CoverageReport,\n CoverageStatus,\n RouteGap,\n RouteCoverage,\n Staleness,\n TestEntry,\n TestGapConfig,\n TestHint,\n} from './types'\nimport { BaseSource } from '../sources/base.source'\nimport { RoutesSource } from '../sources/routes.source'\nimport { FuzzyMatcher } from '../matchers/fuzzy.matcher'\nimport { AreaMatcher } from '../matchers/area.matcher'\n\nexport class CoverageEngine {\n constructor(\n private sources: BaseSource[],\n private routesSource: RoutesSource,\n private matcher: FuzzyMatcher,\n private areaMatcher: AreaMatcher,\n private config: TestGapConfig\n ) {}\n\n async run(): Promise<CoverageReport> {\n // Step 1: Collect from all sources in parallel\n const sourceResults = await Promise.all(this.sources.map(s => s.collect()))\n const allTests: TestEntry[] = sourceResults.flat()\n\n // Step 2: Discover routes\n const routes = await this.routesSource.collect()\n\n // Step 3: Match tests to routes\n const matchMap = this.matcher.match(routes, allTests)\n\n // Collect all matched tests so we can find orphans after\n const matchedTests = new Set<TestEntry>()\n for (const tests of matchMap.values()) {\n for (const t of tests) matchedTests.add(t)\n }\n\n // Step 3b: Apply testHints to fuzzy-unmatched tests\n const hints = this.config.testHints ?? []\n const hintMap = new Map<string, TestHint>()\n for (const hint of hints) hintMap.set(hint.test, hint)\n\n const routeGaps: RouteGap[] = []\n const acknowledgedTests: TestHint[] = []\n const hintResolved = new Set<TestEntry>()\n\n for (const test of allTests) {\n if (matchedTests.has(test)) continue // already matched by fuzzy\n const hint = hintMap.get(test.title)\n if (!hint) continue\n\n if (hint.status === 'resolved' && hint.route) {\n // Inject into matchMap so RouteCoverage picks it up\n const key = hint.method ? `${hint.method} ${hint.route}` : hint.route\n const existing = matchMap.get(key) ?? []\n matchMap.set(key, [...existing, test])\n matchedTests.add(test)\n hintResolved.add(test)\n } else if (hint.status === 'gap') {\n const existing = routeGaps.find(g =>\n g.route === hint.route && g.method === hint.method\n )\n if (existing) {\n existing.coveredByTests.push(test)\n } else {\n routeGaps.push({\n route: hint.route ?? test.title,\n method: hint.method,\n reason: hint.reason,\n coveredByTests: [test],\n })\n }\n matchedTests.add(test)\n } else if (hint.status === 'acknowledged' || hint.status === 'ignore') {\n acknowledgedTests.push(hint)\n matchedTests.add(test)\n }\n }\n\n // Step 4: Build RouteCoverage for each route\n const routeCoverages: RouteCoverage[] = routes.map(route => {\n const key = route.method ? `${route.method} ${route.path}` : route.path\n const matched = matchMap.get(key) ?? []\n const automatedTests = matched.filter(t => t.source === 'playwright')\n const manualTests = matched.filter(t => t.source === 'ado' || t.source === 'yaml')\n\n const status = this.deriveStatus(automatedTests.length, manualTests.length)\n const lastTestedAt = this.latestDate([...automatedTests, ...manualTests])\n const staleness = this.deriveStaleness(lastTestedAt, manualTests.length)\n\n return {\n route,\n status,\n automatedTests,\n manualTests,\n staleness,\n lastTestedAt,\n }\n })\n\n // Step 5: Assign to functional areas\n const { areas, uncategorised } = this.areaMatcher.assign(routeCoverages)\n\n // Step 6: Build summary\n const total = routeCoverages.length\n const automated = routeCoverages.filter(r => r.status === 'automated' || r.status === 'both').length\n const manualOnly = routeCoverages.filter(r => r.status === 'manual').length\n const none = routeCoverages.filter(r => r.status === 'none').length\n\n // LRM: apply rounding only to the three mutually exclusive groups so they\n // always sum to 100. totalCoverage is derived, never independently rounded.\n const toLRM = (counts: number[]): number[] => {\n if (total === 0) return counts.map(() => 0)\n const exact = counts.map(n => (n / total) * 100)\n const floored = exact.map(n => Math.floor(n))\n const remainder = 100 - floored.reduce((a, b) => a + b, 0)\n const order = exact.map((n, i) => ({ i, frac: n - Math.floor(n) })).sort((a, b) => b.frac - a.frac)\n for (let k = 0; k < remainder; k++) floored[order[k].i]++\n return floored\n }\n const [automatedCoverage, manualOnlyCoverage, noCoverage] = toLRM([automated, manualOnly, none])\n const totalCoverage = automatedCoverage + manualOnlyCoverage\n\n return {\n generatedAt: new Date(),\n projectName: this.config.projectName,\n summary: {\n totalRoutes: total,\n totalCoverage,\n automatedCoverage,\n manualOnlyCoverage,\n noCoverage,\n },\n areas,\n uncategorised,\n unmatchedTests: allTests.filter(t => !matchedTests.has(t)),\n routeGaps,\n acknowledgedTests,\n }\n }\n\n private deriveStatus(autoCount: number, manualCount: number): CoverageStatus {\n if (autoCount > 0 && manualCount > 0) return 'both'\n if (autoCount > 0) return 'automated'\n if (manualCount > 0) return 'manual'\n return 'none'\n }\n\n private deriveStaleness(lastTestedAt: Date | undefined, manualCount: number): Staleness {\n // Only flag staleness for manual tests — automated tests run on every push\n if (manualCount === 0) return 'unknown'\n if (!lastTestedAt) return 'unknown'\n\n const daysSince = (Date.now() - lastTestedAt.getTime()) / (1000 * 60 * 60 * 24)\n return daysSince > this.config.staleThresholdDays ? 'stale' : 'fresh'\n }\n\n private latestDate(tests: TestEntry[]): Date | undefined {\n const dates = tests.map(t => t.lastRun).filter((d): d is Date => d instanceof Date)\n if (dates.length === 0) return undefined\n return new Date(Math.max(...dates.map(d => d.getTime())))\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/engine.ts"],"sourcesContent":["/**\n * src/core/engine.ts\n * \n * The main orchestrator. Zero I/O — all dependencies injected.\n * This design makes the engine fully unit-testable with mock sources.\n * \n * AI INSTRUCTIONS: implement run() using the step-by-step comments.\n */\n\nimport {\n CoverageReport,\n CoverageStatus,\n RouteGap,\n RouteCoverage,\n Staleness,\n TestEntry,\n TestGapConfig,\n TestHint,\n} from './types'\nimport { BaseSource } from '../sources/base.source'\nimport { RoutesSource } from '../sources/routes.source'\nimport { FuzzyMatcher } from '../matchers/fuzzy.matcher'\nimport { AreaMatcher } from '../matchers/area.matcher'\n\nexport class CoverageEngine {\n constructor(\n private sources: BaseSource[],\n private routesSource: RoutesSource,\n private matcher: FuzzyMatcher,\n private areaMatcher: AreaMatcher,\n private config: TestGapConfig\n ) {}\n\n async run(): Promise<CoverageReport> {\n // Step 1: Collect from all sources in parallel\n const sourceResults = await Promise.all(this.sources.map(s => s.collect()))\n const allTests: TestEntry[] = sourceResults.flat()\n\n // Step 2: Discover routes\n const routes = await this.routesSource.collect()\n\n // Step 3: Match tests to routes\n const matchMap = this.matcher.match(routes, allTests)\n\n // Collect all matched tests so we can find orphans after\n const matchedTests = new Set<TestEntry>()\n for (const tests of matchMap.values()) {\n for (const t of tests) matchedTests.add(t)\n }\n\n // Step 3b: Apply testHints to fuzzy-unmatched tests\n const hints = this.config.testHints ?? []\n const hintMap = new Map<string, TestHint>()\n for (const hint of hints) hintMap.set(hint.test, hint)\n\n const routeGaps: RouteGap[] = []\n const acknowledgedTests: TestHint[] = []\n const hintResolved = new Set<TestEntry>()\n\n for (const test of allTests) {\n if (matchedTests.has(test)) continue // already matched by fuzzy\n const hint = hintMap.get(test.title)\n if (!hint) continue\n\n if (hint.status === 'resolved' && hint.route) {\n // Inject into matchMap so RouteCoverage picks it up\n const key = hint.method ? `${hint.method} ${hint.route}` : hint.route\n const existing = matchMap.get(key) ?? []\n matchMap.set(key, [...existing, test])\n matchedTests.add(test)\n hintResolved.add(test)\n } else if (hint.status === 'gap') {\n const existing = routeGaps.find(g =>\n g.route === hint.route && g.method === hint.method\n )\n if (existing) {\n existing.coveredByTests.push(test)\n } else {\n routeGaps.push({\n route: hint.route ?? test.title,\n method: hint.method,\n reason: hint.reason,\n coveredByTests: [test],\n })\n }\n matchedTests.add(test)\n } else if (hint.status === 'acknowledged' || hint.status === 'ignore') {\n acknowledgedTests.push(hint)\n matchedTests.add(test)\n }\n }\n\n // Step 4: Build RouteCoverage for each route\n const routeCoverages: RouteCoverage[] = routes.map(route => {\n const key = route.method ? `${route.method} ${route.path}` : route.path\n // Also pick up hints resolved without a method (stored under just the path)\n const matched = route.method\n ? [...(matchMap.get(key) ?? []), ...(matchMap.get(route.path) ?? [])]\n : (matchMap.get(key) ?? [])\n const automatedTests = matched.filter(t => t.source === 'playwright')\n const manualTests = matched.filter(t => t.source === 'ado' || t.source === 'yaml' || t.source === 'csv')\n\n const status = this.deriveStatus(automatedTests.length, manualTests.length)\n const lastTestedAt = this.latestDate([...automatedTests, ...manualTests])\n const staleness = this.deriveStaleness(lastTestedAt, manualTests.length)\n\n return {\n route,\n status,\n automatedTests,\n manualTests,\n staleness,\n lastTestedAt,\n }\n })\n\n // Step 5: Assign to functional areas\n const { areas, uncategorised } = this.areaMatcher.assign(routeCoverages)\n\n // Step 6: Build summary\n const total = routeCoverages.length\n const automated = routeCoverages.filter(r => r.status === 'automated' || r.status === 'both').length\n const manualOnly = routeCoverages.filter(r => r.status === 'manual').length\n const none = routeCoverages.filter(r => r.status === 'none').length\n\n // LRM: apply rounding only to the three mutually exclusive groups so they\n // always sum to 100. totalCoverage is derived, never independently rounded.\n const toLRM = (counts: number[]): number[] => {\n if (total === 0) return counts.map(() => 0)\n const exact = counts.map(n => (n / total) * 100)\n const floored = exact.map(n => Math.floor(n))\n const remainder = 100 - floored.reduce((a, b) => a + b, 0)\n const order = exact.map((n, i) => ({ i, frac: n - Math.floor(n) })).sort((a, b) => b.frac - a.frac)\n for (let k = 0; k < remainder; k++) floored[order[k].i]++\n return floored\n }\n const [automatedCoverage, manualOnlyCoverage, noCoverage] = toLRM([automated, manualOnly, none])\n const totalCoverage = automatedCoverage + manualOnlyCoverage\n\n return {\n generatedAt: new Date(),\n projectName: this.config.projectName,\n summary: {\n totalRoutes: total,\n totalCoverage,\n automatedCoverage,\n manualOnlyCoverage,\n noCoverage,\n },\n areas,\n uncategorised,\n unmatchedTests: allTests.filter(t => !matchedTests.has(t)),\n routeGaps,\n acknowledgedTests,\n }\n }\n\n private deriveStatus(autoCount: number, manualCount: number): CoverageStatus {\n if (autoCount > 0 && manualCount > 0) return 'both'\n if (autoCount > 0) return 'automated'\n if (manualCount > 0) return 'manual'\n return 'none'\n }\n\n private deriveStaleness(lastTestedAt: Date | undefined, manualCount: number): Staleness {\n // Only flag staleness for manual tests — automated tests run on every push\n if (manualCount === 0) return 'unknown'\n if (!lastTestedAt) return 'unknown'\n\n const daysSince = (Date.now() - lastTestedAt.getTime()) / (1000 * 60 * 60 * 24)\n return daysSince > this.config.staleThresholdDays ? 'stale' : 'fresh'\n }\n\n private latestDate(tests: TestEntry[]): Date | undefined {\n const dates = tests.map(t => t.lastRun).filter((d): d is Date => d instanceof Date)\n if (dates.length === 0) return undefined\n return new Date(Math.max(...dates.map(d => d.getTime())))\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/core/types.d.mts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Do not scatter interface definitions — keep this as the single source of truth.
|
|
7
7
|
*/
|
|
8
8
|
type CoverageStatus = 'automated' | 'manual' | 'both' | 'none';
|
|
9
|
-
type TestSource = 'playwright' | 'ado' | 'yaml';
|
|
9
|
+
type TestSource = 'playwright' | 'ado' | 'yaml' | 'csv';
|
|
10
10
|
type Staleness = 'fresh' | 'stale' | 'unknown';
|
|
11
11
|
type TestHintStatus = 'resolved' | 'gap' | 'acknowledged' | 'ignore';
|
|
12
12
|
interface TestHint {
|
|
@@ -93,6 +93,11 @@ interface TestGapConfig {
|
|
|
93
93
|
planId: number;
|
|
94
94
|
patEnvVar: string;
|
|
95
95
|
};
|
|
96
|
+
csv?: {
|
|
97
|
+
path: string;
|
|
98
|
+
titleField: string;
|
|
99
|
+
dateField?: string;
|
|
100
|
+
};
|
|
96
101
|
manualCoverage?: {
|
|
97
102
|
route: string;
|
|
98
103
|
lastTested: string;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Do not scatter interface definitions — keep this as the single source of truth.
|
|
7
7
|
*/
|
|
8
8
|
type CoverageStatus = 'automated' | 'manual' | 'both' | 'none';
|
|
9
|
-
type TestSource = 'playwright' | 'ado' | 'yaml';
|
|
9
|
+
type TestSource = 'playwright' | 'ado' | 'yaml' | 'csv';
|
|
10
10
|
type Staleness = 'fresh' | 'stale' | 'unknown';
|
|
11
11
|
type TestHintStatus = 'resolved' | 'gap' | 'acknowledged' | 'ignore';
|
|
12
12
|
interface TestHint {
|
|
@@ -93,6 +93,11 @@ interface TestGapConfig {
|
|
|
93
93
|
planId: number;
|
|
94
94
|
patEnvVar: string;
|
|
95
95
|
};
|
|
96
|
+
csv?: {
|
|
97
|
+
path: string;
|
|
98
|
+
titleField: string;
|
|
99
|
+
dateField?: string;
|
|
100
|
+
};
|
|
96
101
|
manualCoverage?: {
|
|
97
102
|
route: string;
|
|
98
103
|
lastTested: string;
|
package/dist/core/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/types.ts"],"sourcesContent":["/**\n * src/core/types.ts\n * \n * ALL shared interfaces live here.\n * Every other file in this project imports types from this file.\n * Do not scatter interface definitions — keep this as the single source of truth.\n */\n\nexport type CoverageStatus = 'automated' | 'manual' | 'both' | 'none'\nexport type TestSource = 'playwright' | 'ado' | 'yaml'\nexport type Staleness = 'fresh' | 'stale' | 'unknown'\nexport type TestHintStatus = 'resolved' | 'gap' | 'acknowledged' | 'ignore'\n\nexport interface TestHint {\n test: string // exact test title to match\n status: TestHintStatus // resolved | gap | acknowledged | ignore\n route?: string // route path — for resolved + gap\n method?: string // HTTP method — for gap (e.g. DELETE, PATCH)\n reason?: string\n confidence?: 'high' | 'medium' | 'low'\n resolvedBy?: string // 'agent' | 'human' | 'ai'\n}\n\nexport interface AppRoute {\n path: string // e.g. /checkout/payment\n method?: string // GET | POST | etc — from OpenAPI\n area?: string // populated by AreaMatcher\n}\n\nexport interface TestEntry {\n title: string\n source: TestSource\n filePath?: string // playwright: which spec file\n lastRun?: Date // manual: from yaml or ADO\n tags?: string[]\n}\n\nexport interface RouteGap {\n route: string\n method?: string\n reason?: string\n coveredByTests: TestEntry[]\n}\n\nexport interface RouteCoverage {\n route: AppRoute\n status: CoverageStatus\n automatedTests: TestEntry[]\n manualTests: TestEntry[]\n staleness: Staleness\n lastTestedAt?: Date\n notes?: string\n}\n\nexport interface AreaCoverage {\n name: string\n routes: RouteCoverage[]\n totalRoutes: number\n coveredRoutes: number\n automatedCount: number\n manualOnlyCount: number\n noCoverageCount: number\n coveragePercent: number // 0-100, rounded integer\n}\n\nexport interface CoverageReport {\n generatedAt: Date\n projectName: string\n summary: {\n totalRoutes: number\n totalCoverage: number\n automatedCoverage: number\n manualOnlyCoverage: number\n noCoverage: number\n }\n areas: AreaCoverage[]\n uncategorised: RouteCoverage[]\n unmatchedTests: TestEntry[] // still unresolved — no hint applied\n routeGaps: RouteGap[] // tests targeting routes not in schema (gap hints)\n acknowledgedTests: TestHint[] // acknowledged or ignored hints\n}\n\nexport interface TestGapConfig {\n projectName: string\n staleThresholdDays: number\n routes: {\n type: 'nextjs' | 'express' | 'openapi' | 'manual'\n path: string\n generate?: string // shell command to run before route discovery (e.g. to produce openapi.json)\n basePath?: string // prefix stripped when grouping routes into areas (e.g. /api, /api/v1)\n }\n tests?: {\n path: string // directory to scan for *.spec.ts / *.test.ts files\n // relative to qualitylens.yaml location\n // default: same directory as qualitylens.yaml\n }\n areas: {\n name: string\n patterns: string[]\n }[]\n ado?: {\n orgUrl: string\n project: string\n planId: number\n patEnvVar: string // env var name — never hardcode the PAT value\n }\n manualCoverage?: {\n route: string\n lastTested: string // ISO date string\n tester?: string\n notes?: string\n }[]\n thresholds?: {\n area?: string // area name or '*' for global\n minCoverage: number // 0-100\n }[]\n testHints?: TestHint[]\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/types.ts"],"sourcesContent":["/**\n * src/core/types.ts\n * \n * ALL shared interfaces live here.\n * Every other file in this project imports types from this file.\n * Do not scatter interface definitions — keep this as the single source of truth.\n */\n\nexport type CoverageStatus = 'automated' | 'manual' | 'both' | 'none'\nexport type TestSource = 'playwright' | 'ado' | 'yaml' | 'csv'\nexport type Staleness = 'fresh' | 'stale' | 'unknown'\nexport type TestHintStatus = 'resolved' | 'gap' | 'acknowledged' | 'ignore'\n\nexport interface TestHint {\n test: string // exact test title to match\n status: TestHintStatus // resolved | gap | acknowledged | ignore\n route?: string // route path — for resolved + gap\n method?: string // HTTP method — for gap (e.g. DELETE, PATCH)\n reason?: string\n confidence?: 'high' | 'medium' | 'low'\n resolvedBy?: string // 'agent' | 'human' | 'ai'\n}\n\nexport interface AppRoute {\n path: string // e.g. /checkout/payment\n method?: string // GET | POST | etc — from OpenAPI\n area?: string // populated by AreaMatcher\n}\n\nexport interface TestEntry {\n title: string\n source: TestSource\n filePath?: string // playwright: which spec file\n lastRun?: Date // manual: from yaml or ADO\n tags?: string[]\n}\n\nexport interface RouteGap {\n route: string\n method?: string\n reason?: string\n coveredByTests: TestEntry[]\n}\n\nexport interface RouteCoverage {\n route: AppRoute\n status: CoverageStatus\n automatedTests: TestEntry[]\n manualTests: TestEntry[]\n staleness: Staleness\n lastTestedAt?: Date\n notes?: string\n}\n\nexport interface AreaCoverage {\n name: string\n routes: RouteCoverage[]\n totalRoutes: number\n coveredRoutes: number\n automatedCount: number\n manualOnlyCount: number\n noCoverageCount: number\n coveragePercent: number // 0-100, rounded integer\n}\n\nexport interface CoverageReport {\n generatedAt: Date\n projectName: string\n summary: {\n totalRoutes: number\n totalCoverage: number\n automatedCoverage: number\n manualOnlyCoverage: number\n noCoverage: number\n }\n areas: AreaCoverage[]\n uncategorised: RouteCoverage[]\n unmatchedTests: TestEntry[] // still unresolved — no hint applied\n routeGaps: RouteGap[] // tests targeting routes not in schema (gap hints)\n acknowledgedTests: TestHint[] // acknowledged or ignored hints\n}\n\nexport interface TestGapConfig {\n projectName: string\n staleThresholdDays: number\n routes: {\n type: 'nextjs' | 'express' | 'openapi' | 'manual'\n path: string\n generate?: string // shell command to run before route discovery (e.g. to produce openapi.json)\n basePath?: string // prefix stripped when grouping routes into areas (e.g. /api, /api/v1)\n }\n tests?: {\n path: string // directory to scan for *.spec.ts / *.test.ts files\n // relative to qualitylens.yaml location\n // default: same directory as qualitylens.yaml\n }\n areas: {\n name: string\n patterns: string[]\n }[]\n ado?: {\n orgUrl: string\n project: string\n planId: number\n patEnvVar: string // env var name — never hardcode the PAT value\n }\n csv?: {\n path: string // folder containing *.csv files (relative to qualitylens.yaml)\n titleField: string // column name that holds the test title\n dateField?: string // optional: column with last-run date (ISO 8601)\n }\n manualCoverage?: {\n route: string\n lastTested: string // ISO date string\n tester?: string\n notes?: string\n }[]\n thresholds?: {\n area?: string // area name or '*' for global\n minCoverage: number // 0-100\n }[]\n testHints?: TestHint[]\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
package/package.json
CHANGED