@democratize-quality/qualitylens-core 0.2.5 → 0.2.7

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.
@@ -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);
@@ -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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAwBO,IAAM,iBAAN,MAAqB;AAAA,EAC1B,YACU,SACA,cACA,SACA,aACA,QACR;AALQ;AACA;AACA;AACA;AACA;AAAA,EACP;AAAA,EAEH,MAAM,MAA+B;AAEnC,UAAM,gBAAgB,MAAM,QAAQ,IAAI,KAAK,QAAQ,IAAI,OAAK,EAAE,QAAQ,CAAC,CAAC;AAC1E,UAAM,WAAwB,cAAc,KAAK;AAGjD,UAAM,SAAS,MAAM,KAAK,aAAa,QAAQ;AAG/C,UAAM,WAAW,KAAK,QAAQ,MAAM,QAAQ,QAAQ;AAGpD,UAAM,eAAe,oBAAI,IAAe;AACxC,eAAW,SAAS,SAAS,OAAO,GAAG;AACrC,iBAAW,KAAK,MAAO,cAAa,IAAI,CAAC;AAAA,IAC3C;AAGA,UAAM,QAAQ,KAAK,OAAO,aAAa,CAAC;AACxC,UAAM,UAAU,oBAAI,IAAsB;AAC1C,eAAW,QAAQ,MAAO,SAAQ,IAAI,KAAK,MAAM,IAAI;AAErD,UAAM,YAAwB,CAAC;AAC/B,UAAM,oBAAgC,CAAC;AACvC,UAAM,eAAe,oBAAI,IAAe;AAExC,eAAW,QAAQ,UAAU;AAC3B,UAAI,aAAa,IAAI,IAAI,EAAG;AAC5B,YAAM,OAAO,QAAQ,IAAI,KAAK,KAAK;AACnC,UAAI,CAAC,KAAM;AAEX,UAAI,KAAK,WAAW,cAAc,KAAK,OAAO;AAE5C,cAAM,MAAM,KAAK,SAAS,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,KAAK,KAAK;AAChE,cAAM,WAAW,SAAS,IAAI,GAAG,KAAK,CAAC;AACvC,iBAAS,IAAI,KAAK,CAAC,GAAG,UAAU,IAAI,CAAC;AACrC,qBAAa,IAAI,IAAI;AACrB,qBAAa,IAAI,IAAI;AAAA,MACvB,WAAW,KAAK,WAAW,OAAO;AAChC,cAAM,WAAW,UAAU;AAAA,UAAK,OAC9B,EAAE,UAAU,KAAK,SAAS,EAAE,WAAW,KAAK;AAAA,QAC9C;AACA,YAAI,UAAU;AACZ,mBAAS,eAAe,KAAK,IAAI;AAAA,QACnC,OAAO;AACL,oBAAU,KAAK;AAAA,YACb,OAAO,KAAK,SAAS,KAAK;AAAA,YAC1B,QAAQ,KAAK;AAAA,YACb,QAAQ,KAAK;AAAA,YACb,gBAAgB,CAAC,IAAI;AAAA,UACvB,CAAC;AAAA,QACH;AACA,qBAAa,IAAI,IAAI;AAAA,MACvB,WAAW,KAAK,WAAW,kBAAkB,KAAK,WAAW,UAAU;AACrE,0BAAkB,KAAK,IAAI;AAC3B,qBAAa,IAAI,IAAI;AAAA,MACvB;AAAA,IACF;AAGA,UAAM,iBAAkC,OAAO,IAAI,WAAS;AAC1D,YAAM,MAAM,MAAM,SAAS,GAAG,MAAM,MAAM,IAAI,MAAM,IAAI,KAAK,MAAM;AACnE,YAAM,UAAU,SAAS,IAAI,GAAG,KAAK,CAAC;AACtC,YAAM,iBAAiB,QAAQ,OAAO,OAAK,EAAE,WAAW,YAAY;AACpE,YAAM,cAAc,QAAQ,OAAO,OAAK,EAAE,WAAW,SAAS,EAAE,WAAW,MAAM;AAEjF,YAAM,SAAS,KAAK,aAAa,eAAe,QAAQ,YAAY,MAAM;AAC1E,YAAM,eAAe,KAAK,WAAW,CAAC,GAAG,gBAAgB,GAAG,WAAW,CAAC;AACxE,YAAM,YAAY,KAAK,gBAAgB,cAAc,YAAY,MAAM;AAEvE,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,CAAC;AAGD,UAAM,EAAE,OAAO,cAAc,IAAI,KAAK,YAAY,OAAO,cAAc;AAGvE,UAAM,QAAQ,eAAe;AAC7B,UAAM,YAAY,eAAe,OAAO,OAAK,EAAE,WAAW,eAAe,EAAE,WAAW,MAAM,EAAE;AAC9F,UAAM,aAAa,eAAe,OAAO,OAAK,EAAE,WAAW,QAAQ,EAAE;AACrE,UAAM,OAAO,eAAe,OAAO,OAAK,EAAE,WAAW,MAAM,EAAE;AAI7D,UAAM,QAAQ,CAAC,WAA+B;AAC5C,UAAI,UAAU,EAAG,QAAO,OAAO,IAAI,MAAM,CAAC;AAC1C,YAAM,QAAQ,OAAO,IAAI,OAAM,IAAI,QAAS,GAAG;AAC/C,YAAM,UAAU,MAAM,IAAI,OAAK,KAAK,MAAM,CAAC,CAAC;AAC5C,YAAM,YAAY,MAAM,QAAQ,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC;AACzD,YAAM,QAAQ,MAAM,IAAI,CAAC,GAAG,OAAO,EAAE,GAAG,MAAM,IAAI,KAAK,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI;AAClG,eAAS,IAAI,GAAG,IAAI,WAAW,IAAK,SAAQ,MAAM,CAAC,EAAE,CAAC;AACtD,aAAO;AAAA,IACT;AACA,UAAM,CAAC,mBAAmB,oBAAoB,UAAU,IAAI,MAAM,CAAC,WAAW,YAAY,IAAI,CAAC;AAC/F,UAAM,gBAAgB,oBAAoB;AAE1C,WAAO;AAAA,MACL,aAAa,oBAAI,KAAK;AAAA,MACtB,aAAa,KAAK,OAAO;AAAA,MACzB,SAAS;AAAA,QACP,aAAa;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,MACA,gBAAgB,SAAS,OAAO,OAAK,CAAC,aAAa,IAAI,CAAC,CAAC;AAAA,MACzD;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,aAAa,WAAmB,aAAqC;AAC3E,QAAI,YAAY,KAAK,cAAc,EAAG,QAAO;AAC7C,QAAI,YAAY,EAAG,QAAO;AAC1B,QAAI,cAAc,EAAG,QAAO;AAC5B,WAAO;AAAA,EACT;AAAA,EAEQ,gBAAgB,cAAgC,aAAgC;AAEtF,QAAI,gBAAgB,EAAG,QAAO;AAC9B,QAAI,CAAC,aAAc,QAAO;AAE1B,UAAM,aAAa,KAAK,IAAI,IAAI,aAAa,QAAQ,MAAM,MAAO,KAAK,KAAK;AAC5E,WAAO,YAAY,KAAK,OAAO,qBAAqB,UAAU;AAAA,EAChE;AAAA,EAEQ,WAAW,OAAsC;AACvD,UAAM,QAAQ,MAAM,IAAI,OAAK,EAAE,OAAO,EAAE,OAAO,CAAC,MAAiB,aAAa,IAAI;AAClF,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,WAAO,IAAI,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,OAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;AAAA,EAC1D;AACF;","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":[]}
@@ -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);
@@ -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":";AAwBO,IAAM,iBAAN,MAAqB;AAAA,EAC1B,YACU,SACA,cACA,SACA,aACA,QACR;AALQ;AACA;AACA;AACA;AACA;AAAA,EACP;AAAA,EAEH,MAAM,MAA+B;AAEnC,UAAM,gBAAgB,MAAM,QAAQ,IAAI,KAAK,QAAQ,IAAI,OAAK,EAAE,QAAQ,CAAC,CAAC;AAC1E,UAAM,WAAwB,cAAc,KAAK;AAGjD,UAAM,SAAS,MAAM,KAAK,aAAa,QAAQ;AAG/C,UAAM,WAAW,KAAK,QAAQ,MAAM,QAAQ,QAAQ;AAGpD,UAAM,eAAe,oBAAI,IAAe;AACxC,eAAW,SAAS,SAAS,OAAO,GAAG;AACrC,iBAAW,KAAK,MAAO,cAAa,IAAI,CAAC;AAAA,IAC3C;AAGA,UAAM,QAAQ,KAAK,OAAO,aAAa,CAAC;AACxC,UAAM,UAAU,oBAAI,IAAsB;AAC1C,eAAW,QAAQ,MAAO,SAAQ,IAAI,KAAK,MAAM,IAAI;AAErD,UAAM,YAAwB,CAAC;AAC/B,UAAM,oBAAgC,CAAC;AACvC,UAAM,eAAe,oBAAI,IAAe;AAExC,eAAW,QAAQ,UAAU;AAC3B,UAAI,aAAa,IAAI,IAAI,EAAG;AAC5B,YAAM,OAAO,QAAQ,IAAI,KAAK,KAAK;AACnC,UAAI,CAAC,KAAM;AAEX,UAAI,KAAK,WAAW,cAAc,KAAK,OAAO;AAE5C,cAAM,MAAM,KAAK,SAAS,GAAG,KAAK,MAAM,IAAI,KAAK,KAAK,KAAK,KAAK;AAChE,cAAM,WAAW,SAAS,IAAI,GAAG,KAAK,CAAC;AACvC,iBAAS,IAAI,KAAK,CAAC,GAAG,UAAU,IAAI,CAAC;AACrC,qBAAa,IAAI,IAAI;AACrB,qBAAa,IAAI,IAAI;AAAA,MACvB,WAAW,KAAK,WAAW,OAAO;AAChC,cAAM,WAAW,UAAU;AAAA,UAAK,OAC9B,EAAE,UAAU,KAAK,SAAS,EAAE,WAAW,KAAK;AAAA,QAC9C;AACA,YAAI,UAAU;AACZ,mBAAS,eAAe,KAAK,IAAI;AAAA,QACnC,OAAO;AACL,oBAAU,KAAK;AAAA,YACb,OAAO,KAAK,SAAS,KAAK;AAAA,YAC1B,QAAQ,KAAK;AAAA,YACb,QAAQ,KAAK;AAAA,YACb,gBAAgB,CAAC,IAAI;AAAA,UACvB,CAAC;AAAA,QACH;AACA,qBAAa,IAAI,IAAI;AAAA,MACvB,WAAW,KAAK,WAAW,kBAAkB,KAAK,WAAW,UAAU;AACrE,0BAAkB,KAAK,IAAI;AAC3B,qBAAa,IAAI,IAAI;AAAA,MACvB;AAAA,IACF;AAGA,UAAM,iBAAkC,OAAO,IAAI,WAAS;AAC1D,YAAM,MAAM,MAAM,SAAS,GAAG,MAAM,MAAM,IAAI,MAAM,IAAI,KAAK,MAAM;AAEnE,YAAM,UAAU,MAAM,SAClB,CAAC,GAAI,SAAS,IAAI,GAAG,KAAK,CAAC,GAAI,GAAI,SAAS,IAAI,MAAM,IAAI,KAAK,CAAC,CAAE,IACjE,SAAS,IAAI,GAAG,KAAK,CAAC;AAC3B,YAAM,iBAAiB,QAAQ,OAAO,OAAK,EAAE,WAAW,YAAY;AACpE,YAAM,cAAc,QAAQ,OAAO,OAAK,EAAE,WAAW,SAAS,EAAE,WAAW,UAAU,EAAE,WAAW,KAAK;AAEvG,YAAM,SAAS,KAAK,aAAa,eAAe,QAAQ,YAAY,MAAM;AAC1E,YAAM,eAAe,KAAK,WAAW,CAAC,GAAG,gBAAgB,GAAG,WAAW,CAAC;AACxE,YAAM,YAAY,KAAK,gBAAgB,cAAc,YAAY,MAAM;AAEvE,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,CAAC;AAGD,UAAM,EAAE,OAAO,cAAc,IAAI,KAAK,YAAY,OAAO,cAAc;AAGvE,UAAM,QAAQ,eAAe;AAC7B,UAAM,YAAY,eAAe,OAAO,OAAK,EAAE,WAAW,eAAe,EAAE,WAAW,MAAM,EAAE;AAC9F,UAAM,aAAa,eAAe,OAAO,OAAK,EAAE,WAAW,QAAQ,EAAE;AACrE,UAAM,OAAO,eAAe,OAAO,OAAK,EAAE,WAAW,MAAM,EAAE;AAI7D,UAAM,QAAQ,CAAC,WAA+B;AAC5C,UAAI,UAAU,EAAG,QAAO,OAAO,IAAI,MAAM,CAAC;AAC1C,YAAM,QAAQ,OAAO,IAAI,OAAM,IAAI,QAAS,GAAG;AAC/C,YAAM,UAAU,MAAM,IAAI,OAAK,KAAK,MAAM,CAAC,CAAC;AAC5C,YAAM,YAAY,MAAM,QAAQ,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC;AACzD,YAAM,QAAQ,MAAM,IAAI,CAAC,GAAG,OAAO,EAAE,GAAG,MAAM,IAAI,KAAK,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI;AAClG,eAAS,IAAI,GAAG,IAAI,WAAW,IAAK,SAAQ,MAAM,CAAC,EAAE,CAAC;AACtD,aAAO;AAAA,IACT;AACA,UAAM,CAAC,mBAAmB,oBAAoB,UAAU,IAAI,MAAM,CAAC,WAAW,YAAY,IAAI,CAAC;AAC/F,UAAM,gBAAgB,oBAAoB;AAE1C,WAAO;AAAA,MACL,aAAa,oBAAI,KAAK;AAAA,MACtB,aAAa,KAAK,OAAO;AAAA,MACzB,SAAS;AAAA,QACP,aAAa;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,MACA,gBAAgB,SAAS,OAAO,OAAK,CAAC,aAAa,IAAI,CAAC,CAAC;AAAA,MACzD;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,aAAa,WAAmB,aAAqC;AAC3E,QAAI,YAAY,KAAK,cAAc,EAAG,QAAO;AAC7C,QAAI,YAAY,EAAG,QAAO;AAC1B,QAAI,cAAc,EAAG,QAAO;AAC5B,WAAO;AAAA,EACT;AAAA,EAEQ,gBAAgB,cAAgC,aAAgC;AAEtF,QAAI,gBAAgB,EAAG,QAAO;AAC9B,QAAI,CAAC,aAAc,QAAO;AAE1B,UAAM,aAAa,KAAK,IAAI,IAAI,aAAa,QAAQ,MAAM,MAAO,KAAK,KAAK;AAC5E,WAAO,YAAY,KAAK,OAAO,qBAAqB,UAAU;AAAA,EAChE;AAAA,EAEQ,WAAW,OAAsC;AACvD,UAAM,QAAQ,MAAM,IAAI,OAAK,EAAE,OAAO,EAAE,OAAO,CAAC,MAAiB,aAAa,IAAI;AAClF,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,WAAO,IAAI,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,OAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;AAAA,EAC1D;AACF;","names":[]}
@@ -6,9 +6,10 @@
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
+ type ActionHintStatus = 'covered' | 'gap' | 'acknowledged';
12
13
  interface TestHint {
13
14
  test: string;
14
15
  status: TestHintStatus;
@@ -18,6 +19,16 @@ interface TestHint {
18
19
  confidence?: 'high' | 'medium' | 'low';
19
20
  resolvedBy?: string;
20
21
  }
22
+ /** An AI or human resolution for an action coverage gap (Pro) */
23
+ interface ActionHint {
24
+ route: string;
25
+ action: string;
26
+ status: ActionHintStatus;
27
+ coveredByTest?: string;
28
+ priority?: 'p1' | 'p2' | 'p3';
29
+ reason?: string;
30
+ resolvedBy?: string;
31
+ }
21
32
  interface AppRoute {
22
33
  path: string;
23
34
  method?: string;
@@ -29,6 +40,7 @@ interface TestEntry {
29
40
  filePath?: string;
30
41
  lastRun?: Date;
31
42
  tags?: string[];
43
+ steps?: string;
32
44
  }
33
45
  interface RouteGap {
34
46
  route: string;
@@ -70,6 +82,52 @@ interface CoverageReport {
70
82
  unmatchedTests: TestEntry[];
71
83
  routeGaps: RouteGap[];
72
84
  acknowledgedTests: TestHint[];
85
+ actionCoverage?: RouteActionCoverage[];
86
+ actionSummary?: {
87
+ totalActions: number;
88
+ coveredActions: number;
89
+ coveragePercent: number;
90
+ };
91
+ }
92
+ /** A user-facing action discovered in the application component tree (Pro) */
93
+ interface AppAction {
94
+ label: string;
95
+ type: 'handler' | 'button' | 'form' | 'link';
96
+ sourceFile: string;
97
+ handlerName?: string;
98
+ category?: 'primary' | 'error-recovery';
99
+ }
100
+ /** An action extracted from a test — Playwright body or CSV step text (Pro) */
101
+ interface TestAction {
102
+ label: string;
103
+ routePath: string;
104
+ type: 'goto' | 'click' | 'fill' | 'submit';
105
+ testTitle?: string;
106
+ filePath?: string;
107
+ }
108
+ /** Per-route action coverage — app actions vs what tests exercise (Pro) */
109
+ interface RouteActionCoverage {
110
+ routePath: string;
111
+ discoveredActions: AppAction[];
112
+ coveredActions: string[];
113
+ uncoveredActions: AppAction[];
114
+ errorScenarioActions?: AppAction[];
115
+ note?: string;
116
+ aiResolvedActions?: Array<{
117
+ label: string;
118
+ reason?: string;
119
+ coveredByTest?: string;
120
+ }>;
121
+ prioritisedGaps?: {
122
+ p1: AppAction[];
123
+ p2: AppAction[];
124
+ p3: AppAction[];
125
+ unclassified: AppAction[];
126
+ };
127
+ gapHints?: Record<string, {
128
+ reason?: string;
129
+ resolvedBy?: string;
130
+ }>;
73
131
  }
74
132
  interface TestGapConfig {
75
133
  projectName: string;
@@ -93,6 +151,12 @@ interface TestGapConfig {
93
151
  planId: number;
94
152
  patEnvVar: string;
95
153
  };
154
+ csv?: Array<{
155
+ path: string;
156
+ titleField: string;
157
+ dateField?: string;
158
+ stepsField?: string;
159
+ }>;
96
160
  manualCoverage?: {
97
161
  route: string;
98
162
  lastTested: string;
@@ -104,6 +168,7 @@ interface TestGapConfig {
104
168
  minCoverage: number;
105
169
  }[];
106
170
  testHints?: TestHint[];
171
+ actionHints?: ActionHint[];
107
172
  }
108
173
 
109
- export type { AppRoute, AreaCoverage, CoverageReport, CoverageStatus, RouteCoverage, RouteGap, Staleness, TestEntry, TestGapConfig, TestHint, TestHintStatus, TestSource };
174
+ export type { ActionHint, ActionHintStatus, AppAction, AppRoute, AreaCoverage, CoverageReport, CoverageStatus, RouteActionCoverage, RouteCoverage, RouteGap, Staleness, TestAction, TestEntry, TestGapConfig, TestHint, TestHintStatus, TestSource };
@@ -6,9 +6,10 @@
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
+ type ActionHintStatus = 'covered' | 'gap' | 'acknowledged';
12
13
  interface TestHint {
13
14
  test: string;
14
15
  status: TestHintStatus;
@@ -18,6 +19,16 @@ interface TestHint {
18
19
  confidence?: 'high' | 'medium' | 'low';
19
20
  resolvedBy?: string;
20
21
  }
22
+ /** An AI or human resolution for an action coverage gap (Pro) */
23
+ interface ActionHint {
24
+ route: string;
25
+ action: string;
26
+ status: ActionHintStatus;
27
+ coveredByTest?: string;
28
+ priority?: 'p1' | 'p2' | 'p3';
29
+ reason?: string;
30
+ resolvedBy?: string;
31
+ }
21
32
  interface AppRoute {
22
33
  path: string;
23
34
  method?: string;
@@ -29,6 +40,7 @@ interface TestEntry {
29
40
  filePath?: string;
30
41
  lastRun?: Date;
31
42
  tags?: string[];
43
+ steps?: string;
32
44
  }
33
45
  interface RouteGap {
34
46
  route: string;
@@ -70,6 +82,52 @@ interface CoverageReport {
70
82
  unmatchedTests: TestEntry[];
71
83
  routeGaps: RouteGap[];
72
84
  acknowledgedTests: TestHint[];
85
+ actionCoverage?: RouteActionCoverage[];
86
+ actionSummary?: {
87
+ totalActions: number;
88
+ coveredActions: number;
89
+ coveragePercent: number;
90
+ };
91
+ }
92
+ /** A user-facing action discovered in the application component tree (Pro) */
93
+ interface AppAction {
94
+ label: string;
95
+ type: 'handler' | 'button' | 'form' | 'link';
96
+ sourceFile: string;
97
+ handlerName?: string;
98
+ category?: 'primary' | 'error-recovery';
99
+ }
100
+ /** An action extracted from a test — Playwright body or CSV step text (Pro) */
101
+ interface TestAction {
102
+ label: string;
103
+ routePath: string;
104
+ type: 'goto' | 'click' | 'fill' | 'submit';
105
+ testTitle?: string;
106
+ filePath?: string;
107
+ }
108
+ /** Per-route action coverage — app actions vs what tests exercise (Pro) */
109
+ interface RouteActionCoverage {
110
+ routePath: string;
111
+ discoveredActions: AppAction[];
112
+ coveredActions: string[];
113
+ uncoveredActions: AppAction[];
114
+ errorScenarioActions?: AppAction[];
115
+ note?: string;
116
+ aiResolvedActions?: Array<{
117
+ label: string;
118
+ reason?: string;
119
+ coveredByTest?: string;
120
+ }>;
121
+ prioritisedGaps?: {
122
+ p1: AppAction[];
123
+ p2: AppAction[];
124
+ p3: AppAction[];
125
+ unclassified: AppAction[];
126
+ };
127
+ gapHints?: Record<string, {
128
+ reason?: string;
129
+ resolvedBy?: string;
130
+ }>;
73
131
  }
74
132
  interface TestGapConfig {
75
133
  projectName: string;
@@ -93,6 +151,12 @@ interface TestGapConfig {
93
151
  planId: number;
94
152
  patEnvVar: string;
95
153
  };
154
+ csv?: Array<{
155
+ path: string;
156
+ titleField: string;
157
+ dateField?: string;
158
+ stepsField?: string;
159
+ }>;
96
160
  manualCoverage?: {
97
161
  route: string;
98
162
  lastTested: string;
@@ -104,6 +168,7 @@ interface TestGapConfig {
104
168
  minCoverage: number;
105
169
  }[];
106
170
  testHints?: TestHint[];
171
+ actionHints?: ActionHint[];
107
172
  }
108
173
 
109
- export type { AppRoute, AreaCoverage, CoverageReport, CoverageStatus, RouteCoverage, RouteGap, Staleness, TestEntry, TestGapConfig, TestHint, TestHintStatus, TestSource };
174
+ export type { ActionHint, ActionHintStatus, AppAction, AppRoute, AreaCoverage, CoverageReport, CoverageStatus, RouteActionCoverage, RouteCoverage, RouteGap, Staleness, TestAction, TestEntry, TestGapConfig, TestHint, TestHintStatus, TestSource };
@@ -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'\nexport type ActionHintStatus = 'covered' | 'gap' | 'acknowledged'\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\n/** An AI or human resolution for an action coverage gap (Pro) */\nexport interface ActionHint {\n route: string // route path e.g. /account\n action: string // action label e.g. \"Delete Account\"\n status: ActionHintStatus // covered | gap | acknowledged\n coveredByTest?: string // which test covers it (for status: covered)\n priority?: 'p1' | 'p2' | 'p3' // gap priority — p1 = must fix, p3 = nice to have\n reason?: string // explanation\n resolvedBy?: string // 'ai' | 'human'\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 steps?: string // raw step text from CSV stepsField — used by Pro action extractor\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 actionCoverage?: RouteActionCoverage[] // Pro: per-route action gap analysis\n actionSummary?: { // Pro: aggregate action coverage metric\n totalActions: number\n coveredActions: number\n coveragePercent: number\n }\n}\n\n/** A user-facing action discovered in the application component tree (Pro) */\nexport interface AppAction {\n label: string // \"Delete Account\", \"Place Order\", \"Apply Promo Code\"\n type: 'handler' | 'button' | 'form' | 'link'\n sourceFile: string // component file where it was found\n handlerName?: string // e.g. handleDeleteAccount (if type === 'handler')\n category?: 'primary' | 'error-recovery' // error-recovery = only shown in error state (Retry, Try again, etc.)\n}\n\n/** An action extracted from a test — Playwright body or CSV step text (Pro) */\nexport interface TestAction {\n label: string // extracted text: \"Delete Account\", \"Place Order\"\n routePath: string // route this action targets (from page.goto context or step URL)\n type: 'goto' | 'click' | 'fill' | 'submit'\n testTitle?: string // which test this came from\n filePath?: string // source file\n}\n\n/** Per-route action coverage — app actions vs what tests exercise (Pro) */\nexport interface RouteActionCoverage {\n routePath: string\n discoveredActions: AppAction[] // primary actions found in component tree\n coveredActions: string[] // labels matched by at least one test action\n uncoveredActions: AppAction[] // gap — exists in app, no test exercises it\n errorScenarioActions?: AppAction[] // error-recovery actions (Retry, Try again, etc.) — separate consideration\n note?: string // e.g. \"delegates to <CartView /> — partial scan\"\n // AI-enhanced fields — populated when actionHints are present\n aiResolvedActions?: Array<{ // actions confirmed covered by AI after reviewing test code\n label: string\n reason?: string\n coveredByTest?: string\n }>\n prioritisedGaps?: { // remaining gaps grouped by priority from actionHints\n p1: AppAction[]\n p2: AppAction[]\n p3: AppAction[]\n unclassified: AppAction[]\n }\n gapHints?: Record<string, { reason?: string; resolvedBy?: string }> // keyed by action label — AI-generated context for each gap\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?: Array<{\n path: string // path to a specific CSV file (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 stepsField?: string // optional: column with free-form test step text (Pro action extraction)\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 actionHints?: ActionHint[]\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@democratize-quality/qualitylens-core",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Core library for qualitylens — engine, types, sources, matchers, reporters",
5
5
  "author": "Raj Uppadhyay",
6
6
  "license": "AGPL-3.0-only",