@qulib/core 0.7.0 → 0.9.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 (116) hide show
  1. package/README.md +30 -5
  2. package/bin/qulib.js +2 -3
  3. package/dist/__tests__/fixtures/api-fixture-repo/app/api/orders/route.d.ts +7 -0
  4. package/dist/__tests__/fixtures/api-fixture-repo/app/api/orders/route.d.ts.map +1 -0
  5. package/dist/__tests__/fixtures/api-fixture-repo/app/api/orders/route.js +7 -0
  6. package/dist/__tests__/fixtures/api-fixture-repo/app/api/users/route.d.ts +10 -0
  7. package/dist/__tests__/fixtures/api-fixture-repo/app/api/users/route.d.ts.map +1 -0
  8. package/dist/__tests__/fixtures/api-fixture-repo/app/api/users/route.js +9 -0
  9. package/dist/__tests__/fixtures/api-fixture-repo/pages/api/health.d.ts +9 -0
  10. package/dist/__tests__/fixtures/api-fixture-repo/pages/api/health.d.ts.map +1 -0
  11. package/dist/__tests__/fixtures/api-fixture-repo/pages/api/health.js +10 -0
  12. package/dist/__tests__/playwright-available.d.ts +32 -0
  13. package/dist/__tests__/playwright-available.d.ts.map +1 -0
  14. package/dist/__tests__/playwright-available.js +35 -0
  15. package/dist/adapters/api-adapter.d.ts +26 -0
  16. package/dist/adapters/api-adapter.d.ts.map +1 -1
  17. package/dist/adapters/api-adapter.js +156 -2
  18. package/dist/adapters/ci-results-adapter.d.ts +67 -0
  19. package/dist/adapters/ci-results-adapter.d.ts.map +1 -0
  20. package/dist/adapters/ci-results-adapter.js +143 -0
  21. package/dist/adapters/cypress-e2e-adapter.d.ts.map +1 -1
  22. package/dist/adapters/cypress-e2e-adapter.js +25 -2
  23. package/dist/adapters/playwright-adapter.d.ts.map +1 -1
  24. package/dist/adapters/playwright-adapter.js +94 -2
  25. package/dist/adapters/pr-metadata-adapter.d.ts +75 -0
  26. package/dist/adapters/pr-metadata-adapter.d.ts.map +1 -0
  27. package/dist/adapters/pr-metadata-adapter.js +146 -0
  28. package/dist/adapters/validate-specs.d.ts +55 -0
  29. package/dist/adapters/validate-specs.d.ts.map +1 -0
  30. package/dist/adapters/validate-specs.js +67 -0
  31. package/dist/baseline/baseline.d.ts +54 -0
  32. package/dist/baseline/baseline.d.ts.map +1 -0
  33. package/dist/baseline/baseline.js +252 -0
  34. package/dist/baseline/baseline.schema.d.ts +233 -0
  35. package/dist/baseline/baseline.schema.d.ts.map +1 -0
  36. package/dist/baseline/baseline.schema.js +59 -0
  37. package/dist/cli/confidence-run.d.ts +16 -0
  38. package/dist/cli/confidence-run.d.ts.map +1 -0
  39. package/dist/cli/confidence-run.js +158 -0
  40. package/dist/cli/index.d.ts +11 -1
  41. package/dist/cli/index.d.ts.map +1 -1
  42. package/dist/cli/index.js +80 -4
  43. package/dist/cli/scaffold-run.d.ts +86 -0
  44. package/dist/cli/scaffold-run.d.ts.map +1 -0
  45. package/dist/cli/scaffold-run.js +232 -0
  46. package/dist/cli/score-automation-run.d.ts +25 -0
  47. package/dist/cli/score-automation-run.d.ts.map +1 -0
  48. package/dist/cli/score-automation-run.js +123 -0
  49. package/dist/examples/notquality-dogfood/fixture.d.ts +166 -0
  50. package/dist/examples/notquality-dogfood/fixture.d.ts.map +1 -0
  51. package/dist/examples/notquality-dogfood/fixture.js +174 -0
  52. package/dist/examples/notquality-dogfood/run.d.ts +34 -0
  53. package/dist/examples/notquality-dogfood/run.d.ts.map +1 -0
  54. package/dist/examples/notquality-dogfood/run.js +139 -0
  55. package/dist/index.d.ts +18 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +13 -0
  58. package/dist/recipes/a11y.d.ts +36 -0
  59. package/dist/recipes/a11y.d.ts.map +1 -0
  60. package/dist/recipes/a11y.js +118 -0
  61. package/dist/recipes/auth.d.ts +38 -0
  62. package/dist/recipes/auth.d.ts.map +1 -0
  63. package/dist/recipes/auth.js +156 -0
  64. package/dist/recipes/index.d.ts +26 -0
  65. package/dist/recipes/index.d.ts.map +1 -0
  66. package/dist/recipes/index.js +41 -0
  67. package/dist/recipes/nav.d.ts +34 -0
  68. package/dist/recipes/nav.d.ts.map +1 -0
  69. package/dist/recipes/nav.js +128 -0
  70. package/dist/recipes/seed.d.ts +34 -0
  71. package/dist/recipes/seed.d.ts.map +1 -0
  72. package/dist/recipes/seed.js +87 -0
  73. package/dist/scaffold-tests.d.ts +21 -0
  74. package/dist/scaffold-tests.d.ts.map +1 -1
  75. package/dist/scaffold-tests.js +12 -2
  76. package/dist/schemas/automation-maturity.schema.d.ts +8 -8
  77. package/dist/schemas/automation-maturity.schema.d.ts.map +1 -1
  78. package/dist/schemas/automation-maturity.schema.js +1 -0
  79. package/dist/schemas/confidence.schema.d.ts +526 -0
  80. package/dist/schemas/confidence.schema.d.ts.map +1 -0
  81. package/dist/schemas/confidence.schema.js +161 -0
  82. package/dist/schemas/gap-analysis.schema.d.ts +8 -8
  83. package/dist/schemas/gap-analysis.schema.js +1 -1
  84. package/dist/schemas/index.d.ts +3 -0
  85. package/dist/schemas/index.d.ts.map +1 -1
  86. package/dist/schemas/index.js +3 -0
  87. package/dist/schemas/public-surface.schema.d.ts +5 -5
  88. package/dist/schemas/recipe.schema.d.ts +66 -0
  89. package/dist/schemas/recipe.schema.d.ts.map +1 -0
  90. package/dist/schemas/recipe.schema.js +45 -0
  91. package/dist/schemas/repo-analysis.schema.d.ts +7 -7
  92. package/dist/schemas/views.schema.d.ts +234 -0
  93. package/dist/schemas/views.schema.d.ts.map +1 -0
  94. package/dist/schemas/views.schema.js +82 -0
  95. package/dist/tools/repo/api-surface.d.ts +59 -0
  96. package/dist/tools/repo/api-surface.d.ts.map +1 -0
  97. package/dist/tools/repo/api-surface.js +414 -0
  98. package/dist/tools/scoring/api-coverage.d.ts +74 -0
  99. package/dist/tools/scoring/api-coverage.d.ts.map +1 -0
  100. package/dist/tools/scoring/api-coverage.js +158 -0
  101. package/dist/tools/scoring/automation-maturity.d.ts +11 -1
  102. package/dist/tools/scoring/automation-maturity.d.ts.map +1 -1
  103. package/dist/tools/scoring/automation-maturity.js +43 -9
  104. package/dist/tools/scoring/confidence-from-qulib.d.ts +34 -0
  105. package/dist/tools/scoring/confidence-from-qulib.d.ts.map +1 -0
  106. package/dist/tools/scoring/confidence-from-qulib.js +206 -0
  107. package/dist/tools/scoring/confidence-views.d.ts +40 -0
  108. package/dist/tools/scoring/confidence-views.d.ts.map +1 -0
  109. package/dist/tools/scoring/confidence-views.js +163 -0
  110. package/dist/tools/scoring/confidence.d.ts +32 -0
  111. package/dist/tools/scoring/confidence.d.ts.map +1 -0
  112. package/dist/tools/scoring/confidence.js +180 -0
  113. package/dist/tools/scoring/levels.d.ts +15 -0
  114. package/dist/tools/scoring/levels.d.ts.map +1 -0
  115. package/dist/tools/scoring/levels.js +21 -0
  116. package/package.json +15 -7
package/README.md CHANGED
@@ -140,16 +140,38 @@ After a normal **`analyze`**, `output/report.json` includes **`gapAnalysis.costI
140
140
  Re-print that block from disk:
141
141
 
142
142
  ```bash
143
- npx tsx src/cli/index.ts cost doctor
144
- # or: npx tsx src/cli/index.ts cost doctor --report output/report.json
143
+ qulib cost doctor
144
+ # or: qulib cost doctor --report output/report.json
145
145
  ```
146
146
 
147
147
  ## CLI (from npm)
148
148
 
149
+ **Release confidence — the flagship command:**
150
+
151
+ ```bash
152
+ npx @qulib/core confidence --url https://example.com
153
+ ```
154
+
155
+ Returns ship / caution / hold / block with a 0–100 score, top risks, and recommended next checks. Add `--repo` to also score test-automation maturity and API coverage.
156
+
157
+ **Analyze (full gap report):**
158
+
149
159
  ```bash
150
160
  npx @qulib/core analyze --url https://example.com
151
161
  ```
152
162
 
163
+ **Scaffold a test suite:**
164
+
165
+ ```bash
166
+ npx @qulib/core scaffold --url https://example.com --framework cypress-e2e
167
+ ```
168
+
169
+ **Score automation maturity (repo-only, no URL needed):**
170
+
171
+ ```bash
172
+ npx @qulib/core score-automation --repo /path/to/repo
173
+ ```
174
+
153
175
  Use `npx playwright install chromium` the first time you scan (Playwright is a dependency).
154
176
 
155
177
  ## Programmatic API
@@ -344,10 +366,13 @@ Use these as conservative reference numbers:
344
366
 
345
367
  | Tool | When to use | Key input |
346
368
  |---|---|---|
347
- | `analyze_app` | Main QA scan for release confidence + gaps | `url`, optional `auth`, optional LLM knobs |
348
- | `detect_auth` | Fast single-pass auth pattern guess | `url`, optional `timeoutMs` |
369
+ | **`qulib_score_confidence`** | **Flagship.** Fused verdict (ship/caution/hold/block) from all collectors | `url` and/or `repoPath`, optional `includeViews.replay` |
370
+ | `analyze_app` | Live-app QA scan: release confidence + gaps + a11y | `url`, optional `auth`, optional LLM knobs |
371
+ | `qulib_score_automation` | Score local repo test-automation maturity | absolute `repoPath`, optional `includeFullDimensions` |
372
+ | `qulib_score_api` | Discover API endpoints and score their test coverage | absolute `repoPath`, optional `enableTier3`, `includeEndpointDetail` |
373
+ | `qulib_scaffold_tests` | Generate Cypress/Playwright scaffold from a live URL | `url`, optional `framework`, `maxPagesToScan`, `recipes` |
349
374
  | `explore_auth` | Deeper auth-path discovery on unfamiliar apps | `url`, optional `timeoutMs` |
350
- | `qulib_score_automation` | Score local repo automation maturity | absolute `repoPath`, optional `includeFullDimensions` |
375
+ | `detect_auth` | Fast single-pass auth pattern guess | `url`, optional `timeoutMs` |
351
376
 
352
377
  ## Output directories
353
378
 
package/bin/qulib.js CHANGED
@@ -5,10 +5,9 @@ import { dirname, resolve } from 'node:path';
5
5
 
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = dirname(__filename);
8
- const cliPath = resolve(__dirname, '../src/cli/index.ts');
8
+ const cliPath = resolve(__dirname, '../dist/cli/index.js');
9
9
 
10
- const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
11
- const child = spawn(npxCmd, ['tsx', cliPath, ...process.argv.slice(2)], {
10
+ const child = spawn(process.execPath, [cliPath, ...process.argv.slice(2)], {
12
11
  stdio: 'inherit',
13
12
  });
14
13
 
@@ -0,0 +1,7 @@
1
+ export declare function DELETE(request: {
2
+ url: string;
3
+ }): Promise<{
4
+ deleted: boolean;
5
+ id: string | null;
6
+ }>;
7
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/__tests__/fixtures/api-fixture-repo/app/api/orders/route.ts"],"names":[],"mappings":"AAGA,wBAAsB,MAAM,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE;;;GAIpD"}
@@ -0,0 +1,7 @@
1
+ // Fixture: Next.js App Router API route for orders (DELETE only — high severity untested)
2
+ // This file is a static analysis fixture only — not compiled.
3
+ export async function DELETE(request) {
4
+ const url = new URL(request.url);
5
+ const id = url.searchParams.get('id');
6
+ return { deleted: true, id };
7
+ }
@@ -0,0 +1,10 @@
1
+ export declare function GET(): Promise<{
2
+ users: never[];
3
+ }>;
4
+ export declare function POST(request: {
5
+ json: () => Promise<unknown>;
6
+ }): Promise<{
7
+ created: boolean;
8
+ user: unknown;
9
+ }>;
10
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/__tests__/fixtures/api-fixture-repo/app/api/users/route.ts"],"names":[],"mappings":"AAGA,wBAAsB,GAAG;;GAExB;AAED,wBAAsB,IAAI,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE;;;GAGnE"}
@@ -0,0 +1,9 @@
1
+ // Fixture: Next.js App Router API route for users
2
+ // This file is a static analysis fixture only — not compiled.
3
+ export async function GET() {
4
+ return { users: [] };
5
+ }
6
+ export async function POST(request) {
7
+ const body = await request.json();
8
+ return { created: true, user: body };
9
+ }
@@ -0,0 +1,9 @@
1
+ export default function handler(req: {
2
+ method?: string;
3
+ }, res: {
4
+ status: (code: number) => {
5
+ json: (data: unknown) => void;
6
+ end: () => void;
7
+ };
8
+ }): void;
9
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../../../../../src/__tests__/fixtures/api-fixture-repo/pages/api/health.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,OAAO,UAAU,OAAO,CAC7B,GAAG,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EACxB,GAAG,EAAE;IAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;QAAC,GAAG,EAAE,MAAM,IAAI,CAAA;KAAE,CAAA;CAAE,QAOtF"}
@@ -0,0 +1,10 @@
1
+ // Fixture: Next.js Pages API route
2
+ // This file is a static analysis fixture only — not compiled.
3
+ export default function handler(req, res) {
4
+ if (req.method === 'GET') {
5
+ res.status(200).json({ status: 'ok' });
6
+ }
7
+ else {
8
+ res.status(405).end();
9
+ }
10
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Lightweight helper used by test files that require a Playwright Chromium
3
+ * installation. Checks for the browser at the filesystem level — no side
4
+ * effects, no subprocess launch.
5
+ *
6
+ * Two ways to skip:
7
+ * 1. Set PLAYWRIGHT_SKIP=1 in the environment — explicit opt-out, useful for
8
+ * fresh-clone CI jobs that intentionally skip browser tests.
9
+ * 2. The Playwright Chromium executable is simply absent from the expected
10
+ * install path (e.g. the developer never ran `npx playwright install`).
11
+ *
12
+ * Usage in test files:
13
+ *
14
+ * import { chromiumAvailable, CHROMIUM_SKIP_REASON } from './playwright-available.js';
15
+ *
16
+ * test('my browser test', { skip: !chromiumAvailable, skipMessage: CHROMIUM_SKIP_REASON }, () => { ... });
17
+ *
18
+ * Or for a subtree of tests (t.before guard):
19
+ *
20
+ * if (!chromiumAvailable) { t.skip(); return; }
21
+ */
22
+ /**
23
+ * True when a Playwright Chromium browser binary is present on disk AND the
24
+ * caller has not set PLAYWRIGHT_SKIP=1.
25
+ */
26
+ export declare const chromiumAvailable: boolean;
27
+ /**
28
+ * Human-readable reason string passed to `t.skip()` / `skipMessage` so the
29
+ * skip is self-documenting in test output.
30
+ */
31
+ export declare const CHROMIUM_SKIP_REASON: string;
32
+ //# sourceMappingURL=playwright-available.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright-available.d.ts","sourceRoot":"","sources":["../../src/__tests__/playwright-available.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,OAC0C,CAAC;AAE3E;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAEqE,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Lightweight helper used by test files that require a Playwright Chromium
3
+ * installation. Checks for the browser at the filesystem level — no side
4
+ * effects, no subprocess launch.
5
+ *
6
+ * Two ways to skip:
7
+ * 1. Set PLAYWRIGHT_SKIP=1 in the environment — explicit opt-out, useful for
8
+ * fresh-clone CI jobs that intentionally skip browser tests.
9
+ * 2. The Playwright Chromium executable is simply absent from the expected
10
+ * install path (e.g. the developer never ran `npx playwright install`).
11
+ *
12
+ * Usage in test files:
13
+ *
14
+ * import { chromiumAvailable, CHROMIUM_SKIP_REASON } from './playwright-available.js';
15
+ *
16
+ * test('my browser test', { skip: !chromiumAvailable, skipMessage: CHROMIUM_SKIP_REASON }, () => { ... });
17
+ *
18
+ * Or for a subtree of tests (t.before guard):
19
+ *
20
+ * if (!chromiumAvailable) { t.skip(); return; }
21
+ */
22
+ import { chromium } from '@playwright/test';
23
+ import { existsSync } from 'node:fs';
24
+ /**
25
+ * True when a Playwright Chromium browser binary is present on disk AND the
26
+ * caller has not set PLAYWRIGHT_SKIP=1.
27
+ */
28
+ export const chromiumAvailable = !process.env['PLAYWRIGHT_SKIP'] && existsSync(chromium.executablePath());
29
+ /**
30
+ * Human-readable reason string passed to `t.skip()` / `skipMessage` so the
31
+ * skip is self-documenting in test output.
32
+ */
33
+ export const CHROMIUM_SKIP_REASON = process.env['PLAYWRIGHT_SKIP']
34
+ ? 'PLAYWRIGHT_SKIP=1 is set — browser tests opted out. Unset PLAYWRIGHT_SKIP to enable.'
35
+ : 'Playwright Chromium is not installed. Run `npx playwright install chromium` to enable these tests.';
@@ -1,8 +1,34 @@
1
1
  import type { TestAdapter } from './adapter.interface.js';
2
2
  import type { NeutralScenario, GeneratedTest } from '../schemas/gap-analysis.schema.js';
3
+ import type { ApiSurface } from '../tools/repo/api-surface.js';
4
+ /**
5
+ * TestAdapter implementation for API testing via supertest.
6
+ *
7
+ * `render` / `renderAll`: convert gap-analysis NeutralScenarios that carry
8
+ * `api-call` steps into supertest specs. Used by the standard adapter pipeline.
9
+ *
10
+ * `scaffoldApiTests`: separate entry point for the repo-first API toolshed flow.
11
+ * Accepts discovered endpoints (ApiSurface) and generates a ready-to-run
12
+ * supertest test file — NOT URL-based.
13
+ */
3
14
  export declare class ApiAdapter implements TestAdapter {
4
15
  readonly adapterType = "api";
5
16
  render(scenario: NeutralScenario): GeneratedTest;
6
17
  renderAll(scenarios: NeutralScenario[]): GeneratedTest[];
18
+ /**
19
+ * Generate a supertest-based test file from discovered API endpoints.
20
+ * This is the repo-first entry point — does NOT require a running URL.
21
+ *
22
+ * Endpoints are grouped into a single test file. Each endpoint gets one
23
+ * `it` block that:
24
+ * - Makes the correct HTTP method call
25
+ * - Asserts status < 500 (smoke-level assertion, safely runnable against a live app)
26
+ * - POST/PUT/PATCH endpoints include a TODO for request body
27
+ *
28
+ * The file is NOT associated with a NeutralScenario; it uses a fixed scenarioId.
29
+ */
30
+ scaffoldApiTests(apiSurface: ApiSurface, options?: {
31
+ appImportPath?: string;
32
+ }): GeneratedTest;
7
33
  }
8
34
  //# sourceMappingURL=api-adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/api-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAExF,qBAAa,UAAW,YAAW,WAAW;IAC5C,QAAQ,CAAC,WAAW,SAAS;IAE7B,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAIhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}
1
+ {"version":3,"file":"api-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/api-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,KAAK,EAAsB,UAAU,EAAE,MAAM,8BAA8B,CAAC;AASnF;;;;;;;;;GASG;AACH,qBAAa,UAAW,YAAW,WAAW;IAC5C,QAAQ,CAAC,WAAW,SAAS;IAE7B,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAuDhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;IAIxD;;;;;;;;;;;OAWG;IACH,gBAAgB,CACd,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAO,GACvC,aAAa;CAoDjB"}
@@ -1,9 +1,163 @@
1
+ function slugify(title) {
2
+ return title
3
+ .toLowerCase()
4
+ .replace(/[^a-z0-9]+/g, '-')
5
+ .replace(/^-|-$/g, '');
6
+ }
7
+ /**
8
+ * TestAdapter implementation for API testing via supertest.
9
+ *
10
+ * `render` / `renderAll`: convert gap-analysis NeutralScenarios that carry
11
+ * `api-call` steps into supertest specs. Used by the standard adapter pipeline.
12
+ *
13
+ * `scaffoldApiTests`: separate entry point for the repo-first API toolshed flow.
14
+ * Accepts discovered endpoints (ApiSurface) and generates a ready-to-run
15
+ * supertest test file — NOT URL-based.
16
+ */
1
17
  export class ApiAdapter {
2
18
  adapterType = 'api';
3
19
  render(scenario) {
4
- throw new Error('Not implemented');
20
+ const slug = slugify(scenario.title);
21
+ const filename = `${slug}.api.test.ts`;
22
+ const stepLines = scenario.steps
23
+ .map((step) => {
24
+ if (step.action === 'api-call') {
25
+ const path = step.target ?? step.value ?? '/';
26
+ return [
27
+ ` // ${step.description}`,
28
+ ` const res = await request(app).get(${JSON.stringify(path)});`,
29
+ ` expect(res.status).toBe(200);`,
30
+ ].join('\n');
31
+ }
32
+ if (step.action === 'navigate') {
33
+ const path = step.target ?? step.value ?? '/';
34
+ return [
35
+ ` // ${step.description}`,
36
+ ` const res = await request(app).get(${JSON.stringify(path)});`,
37
+ ` expect(res.status).toBeLessThan(500);`,
38
+ ].join('\n');
39
+ }
40
+ return ` // TODO (${step.action}): ${step.description}`;
41
+ })
42
+ .join('\n');
43
+ const code = [
44
+ `// ${scenario.description}`,
45
+ `// qulib-generated — scenario: ${scenario.id}`,
46
+ ``,
47
+ `import request from 'supertest';`,
48
+ `import { describe, it, expect } from 'vitest';`,
49
+ ``,
50
+ `// TODO: import or create your Express/Fastify/Hono app here`,
51
+ `// import { app } from '../src/app.js';`,
52
+ `declare const app: unknown;`,
53
+ ``,
54
+ `describe(${JSON.stringify(scenario.title)}, () => {`,
55
+ ` it(${JSON.stringify(scenario.description)}, async () => {`,
56
+ stepLines || ` // no api-call steps — add assertions for: ${scenario.targetPath}`,
57
+ ` });`,
58
+ `});`,
59
+ ``,
60
+ ].join('\n');
61
+ return {
62
+ scenarioId: scenario.id,
63
+ adapter: 'api',
64
+ filename,
65
+ code,
66
+ source: 'template',
67
+ outputPath: `tests/api/${filename}`,
68
+ };
5
69
  }
6
70
  renderAll(scenarios) {
7
- throw new Error('Not implemented');
71
+ return scenarios.map((s) => this.render(s));
72
+ }
73
+ /**
74
+ * Generate a supertest-based test file from discovered API endpoints.
75
+ * This is the repo-first entry point — does NOT require a running URL.
76
+ *
77
+ * Endpoints are grouped into a single test file. Each endpoint gets one
78
+ * `it` block that:
79
+ * - Makes the correct HTTP method call
80
+ * - Asserts status < 500 (smoke-level assertion, safely runnable against a live app)
81
+ * - POST/PUT/PATCH endpoints include a TODO for request body
82
+ *
83
+ * The file is NOT associated with a NeutralScenario; it uses a fixed scenarioId.
84
+ */
85
+ scaffoldApiTests(apiSurface, options = {}) {
86
+ const appImport = options.appImportPath ?? '../src/app.js';
87
+ const endpoints = apiSurface.endpoints;
88
+ if (endpoints.length === 0) {
89
+ const code = [
90
+ `// qulib-generated API scaffold — no endpoints discovered`,
91
+ `// qulib-generated — repo: ${apiSurface.repoPath}`,
92
+ ``,
93
+ `// No API endpoints were discovered in this repository.`,
94
+ `// If your app has REST endpoints, ensure they are declared in a supported`,
95
+ `// framework (Next.js route.ts, Express, Fastify, NestJS) or an OpenAPI spec.`,
96
+ ``,
97
+ ].join('\n');
98
+ return {
99
+ scenarioId: 'qulib-api-scaffold',
100
+ adapter: 'api',
101
+ filename: 'api-scaffold.test.ts',
102
+ code,
103
+ source: 'template',
104
+ outputPath: 'tests/api/api-scaffold.test.ts',
105
+ };
106
+ }
107
+ const itBlocks = endpoints.map((ep) => renderEndpointTest(ep)).join('\n\n');
108
+ const code = [
109
+ `// qulib-generated API scaffold — ${endpoints.length} endpoint(s) discovered`,
110
+ `// qulib-generated — repo: ${apiSurface.repoPath}`,
111
+ `// Discovery tier breakdown: ${describeDiscoveryTiers(endpoints)}`,
112
+ ``,
113
+ `import request from 'supertest';`,
114
+ `import { describe, it, expect, beforeAll, afterAll } from 'vitest';`,
115
+ ``,
116
+ `// TODO: replace with your actual app export`,
117
+ `import { app } from ${JSON.stringify(appImport)};`,
118
+ ``,
119
+ `describe('API surface smoke tests (qulib-generated)', () => {`,
120
+ itBlocks,
121
+ `});`,
122
+ ``,
123
+ ].join('\n');
124
+ return {
125
+ scenarioId: 'qulib-api-scaffold',
126
+ adapter: 'api',
127
+ filename: 'api-scaffold.test.ts',
128
+ code,
129
+ source: 'template',
130
+ outputPath: 'tests/api/api-scaffold.test.ts',
131
+ };
8
132
  }
9
133
  }
134
+ // ---------------------------------------------------------------------------
135
+ // Internal helpers
136
+ // ---------------------------------------------------------------------------
137
+ function renderEndpointTest(ep) {
138
+ const method = ep.method === 'unknown' ? 'GET' : ep.method;
139
+ const methodLower = method.toLowerCase();
140
+ const hasBody = method === 'POST' || method === 'PUT' || method === 'PATCH';
141
+ const sourceLine = ` // Source: ${ep.sourceFile} (${ep.sourceTier}, confidence: ${ep.confidence})`;
142
+ const itTitle = `${method} ${ep.path}`;
143
+ const requestLine = hasBody
144
+ ? ` const res = await request(app).${methodLower}(${JSON.stringify(ep.path)});\n // TODO: add request body — e.g. .send({ ... })`
145
+ : ` const res = await request(app).${methodLower}(${JSON.stringify(ep.path)});`;
146
+ const summaryLine = ep.summary ? ` // ${ep.summary}\n` : '';
147
+ return [
148
+ summaryLine + sourceLine,
149
+ ` it(${JSON.stringify(itTitle)}, async () => {`,
150
+ requestLine,
151
+ ` expect(res.status).toBeLessThan(500);`,
152
+ ` });`,
153
+ ].join('\n');
154
+ }
155
+ function describeDiscoveryTiers(endpoints) {
156
+ const counts = { openapi: 0, framework: 0, heuristic: 0 };
157
+ for (const ep of endpoints)
158
+ counts[ep.sourceTier]++;
159
+ return Object.entries(counts)
160
+ .filter(([, v]) => v > 0)
161
+ .map(([k, v]) => `${v} ${k}`)
162
+ .join(', ');
163
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * CI-results evidence adapter (P4 — evidence collectors).
3
+ *
4
+ * Maps a CI run summary (test pass/fail counts, build status, optional flakiness
5
+ * data) into an `EvidenceItem` for `computeReleaseConfidence`, using the
6
+ * `ci-results` source kind reserved in confidence.schema.ts.
7
+ *
8
+ * Design:
9
+ * - Pure function — no I/O, no side effects. The caller owns fetching the CI data
10
+ * (e.g. from the `gh` API or a CI provider webhook); this adapter scores + maps.
11
+ * - Applicability:
12
+ * `applicable` — a real run with ≥1 test executed
13
+ * `not_applicable` — no CI run on record for this ref (e.g. no CI configured)
14
+ * `unknown` — run exists but results are incomplete/in-progress
15
+ * - Score formula: pass-rate * build-weight * freshness-factor
16
+ * pass-rate = passed / (passed + failed + errored) [0..1]
17
+ * build-weight = 0.85 if build succeeded, 0.0 if build failed (a build failure
18
+ * overrides the test pass-rate — tests that never ran are not "passing")
19
+ * freshness = 1.0 when ageSeconds < FRESH_THRESHOLD, decaying linearly to
20
+ * MIN_FRESHNESS over STALE_THRESHOLD. Beyond STALE_THRESHOLD the
21
+ * applicability is coerced to `unknown` (matches §2.6 rule 3 of the spec).
22
+ * - A build failure OR 0 tests passing marks evidence ['critical'] and forces a
23
+ * blocking recommendation (the aggregator uses recommendations for narrative).
24
+ * - Multi-tenant: tenantId flows through unchanged (caller sets it on the EvidenceItem
25
+ * via the `collector.tool` string; the full tenant stamp is the subject, not the item).
26
+ */
27
+ import type { EvidenceItem } from '../schemas/confidence.schema.js';
28
+ /**
29
+ * Raw CI run data the caller provides (from `gh run view --json`, provider API, etc.).
30
+ * Only the counts matter for scoring; everything else is for evidence strings + provenance.
31
+ */
32
+ export interface CiRunInput {
33
+ /** ISO-8601 timestamp when the run completed. */
34
+ completedAt: string;
35
+ /** Whether the CI build step itself succeeded (compilation, lint, etc. — before tests). */
36
+ buildPassed: boolean;
37
+ /** Number of test cases that passed. */
38
+ testsPassed: number;
39
+ /** Number of test cases that failed (hard). */
40
+ testsFailed: number;
41
+ /** Number of test cases that errored (infra/setup failure, not assertion failure). */
42
+ testsErrored: number;
43
+ /**
44
+ * Optional: number of tests that were flaky (passed on retry). Presence lowers score
45
+ * slightly but doesn't fail the run — flaky tests are a warn, not a block.
46
+ */
47
+ testsFlaky?: number;
48
+ /**
49
+ * Optional: CI provider URL for the run (e.g. `https://github.com/…/actions/runs/…`).
50
+ * Never fabricated — omit rather than invent.
51
+ */
52
+ runUrl?: string;
53
+ /** Optional: CI workflow/pipeline name for the evidence string. */
54
+ workflowName?: string;
55
+ /**
56
+ * Optional: collector freshness budget override (seconds). Defaults to 24 h for stale.
57
+ */
58
+ staleAfterSeconds?: number;
59
+ }
60
+ /**
61
+ * Produce a `ci-results` EvidenceItem from a raw CI run summary.
62
+ *
63
+ * Deterministic and pure. Returns `not_applicable` when the run is absent, `unknown`
64
+ * when stale or incomplete, and `applicable` with a real score otherwise.
65
+ */
66
+ export declare function ciResultsToEvidence(run: CiRunInput, collectedAt?: string): EvidenceItem;
67
+ //# sourceMappingURL=ci-results-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci-results-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/ci-results-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAsB,MAAM,iCAAiC,CAAC;AASxF;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,iDAAiD;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,2FAA2F;IAC3F,WAAW,EAAE,OAAO,CAAC;IACrB,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,sFAAsF;IACtF,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,YAAY,CA+GvF"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * CI-results evidence adapter (P4 — evidence collectors).
3
+ *
4
+ * Maps a CI run summary (test pass/fail counts, build status, optional flakiness
5
+ * data) into an `EvidenceItem` for `computeReleaseConfidence`, using the
6
+ * `ci-results` source kind reserved in confidence.schema.ts.
7
+ *
8
+ * Design:
9
+ * - Pure function — no I/O, no side effects. The caller owns fetching the CI data
10
+ * (e.g. from the `gh` API or a CI provider webhook); this adapter scores + maps.
11
+ * - Applicability:
12
+ * `applicable` — a real run with ≥1 test executed
13
+ * `not_applicable` — no CI run on record for this ref (e.g. no CI configured)
14
+ * `unknown` — run exists but results are incomplete/in-progress
15
+ * - Score formula: pass-rate * build-weight * freshness-factor
16
+ * pass-rate = passed / (passed + failed + errored) [0..1]
17
+ * build-weight = 0.85 if build succeeded, 0.0 if build failed (a build failure
18
+ * overrides the test pass-rate — tests that never ran are not "passing")
19
+ * freshness = 1.0 when ageSeconds < FRESH_THRESHOLD, decaying linearly to
20
+ * MIN_FRESHNESS over STALE_THRESHOLD. Beyond STALE_THRESHOLD the
21
+ * applicability is coerced to `unknown` (matches §2.6 rule 3 of the spec).
22
+ * - A build failure OR 0 tests passing marks evidence ['critical'] and forces a
23
+ * blocking recommendation (the aggregator uses recommendations for narrative).
24
+ * - Multi-tenant: tenantId flows through unchanged (caller sets it on the EvidenceItem
25
+ * via the `collector.tool` string; the full tenant stamp is the subject, not the item).
26
+ */
27
+ // Freshness constants (seconds). Configurable at call-site via CiRunInput.
28
+ const DEFAULT_FRESH_THRESHOLD_S = 60 * 60 * 4; // 4 h
29
+ const DEFAULT_STALE_THRESHOLD_S = 60 * 60 * 24; // 24 h
30
+ const MIN_FRESHNESS = 0.5; // floor before we coerce to unknown
31
+ const CI_WEIGHT = 0.10; // matches DEFAULT_WEIGHTS['ci-results'] in confidence.ts
32
+ /**
33
+ * Produce a `ci-results` EvidenceItem from a raw CI run summary.
34
+ *
35
+ * Deterministic and pure. Returns `not_applicable` when the run is absent, `unknown`
36
+ * when stale or incomplete, and `applicable` with a real score otherwise.
37
+ */
38
+ export function ciResultsToEvidence(run, collectedAt) {
39
+ const now = collectedAt ?? new Date().toISOString();
40
+ const source = 'ci-results';
41
+ const weight = CI_WEIGHT;
42
+ // Freshness computation.
43
+ const ageMs = Date.parse(now) - Date.parse(run.completedAt);
44
+ const ageS = Math.max(0, ageMs / 1000);
45
+ const staleThreshold = run.staleAfterSeconds ?? DEFAULT_STALE_THRESHOLD_S;
46
+ if (ageS > staleThreshold) {
47
+ return {
48
+ source,
49
+ score: 0,
50
+ weight,
51
+ applicability: 'unknown',
52
+ blocking: false,
53
+ evidence: [
54
+ `CI run completed at ${run.completedAt} — stale (${Math.round(ageS / 3600)}h > ${staleThreshold / 3600}h threshold).`,
55
+ ],
56
+ recommendations: ['Re-run CI against the current commit before shipping.'],
57
+ reason: `CI run is stale (${Math.round(ageS / 3600)}h old, threshold ${staleThreshold / 3600}h).`,
58
+ collectedAt: now,
59
+ collector: { tool: 'qulib.ci-results-adapter' },
60
+ };
61
+ }
62
+ const total = run.testsPassed + run.testsFailed + run.testsErrored;
63
+ // Build failure: tests may not even have run — score 0, blocking recommendation.
64
+ if (!run.buildPassed) {
65
+ const evidence = [`Build FAILED (${run.workflowName ?? 'CI workflow'}).`];
66
+ if (total > 0)
67
+ evidence.push(`${run.testsPassed}/${total} tests passed before build failure.`);
68
+ if (run.runUrl)
69
+ evidence.push(`Run: ${run.runUrl}`);
70
+ return {
71
+ source,
72
+ score: 0,
73
+ weight,
74
+ applicability: 'applicable',
75
+ blocking: false, // the aggregator decides blocking; we report honestly
76
+ evidence,
77
+ recommendations: ['Fix the build failure before shipping.'],
78
+ collectedAt: now,
79
+ collector: { tool: 'qulib.ci-results-adapter', inputRef: run.runUrl },
80
+ };
81
+ }
82
+ // No tests ran at all — cannot score honestly.
83
+ if (total === 0) {
84
+ return {
85
+ source,
86
+ score: 0,
87
+ weight,
88
+ applicability: 'unknown',
89
+ blocking: false,
90
+ evidence: [
91
+ `Build passed (${run.workflowName ?? 'CI workflow'}) but 0 tests executed.`,
92
+ ...(run.runUrl ? [`Run: ${run.runUrl}`] : []),
93
+ ],
94
+ recommendations: ['Add a test suite to CI for a meaningful confidence signal.'],
95
+ reason: 'Build passed but zero tests were executed — no test signal.',
96
+ collectedAt: now,
97
+ collector: { tool: 'qulib.ci-results-adapter', inputRef: run.runUrl },
98
+ };
99
+ }
100
+ // Normal case: compute pass-rate.
101
+ const passRate = run.testsPassed / total; // 0..1
102
+ const freshnessRatio = ageS <= DEFAULT_FRESH_THRESHOLD_S
103
+ ? 1.0
104
+ : MIN_FRESHNESS +
105
+ (1 - MIN_FRESHNESS) *
106
+ (1 - (ageS - DEFAULT_FRESH_THRESHOLD_S) / (staleThreshold - DEFAULT_FRESH_THRESHOLD_S));
107
+ const rawScore = passRate * freshnessRatio;
108
+ const score = Math.round(Math.max(0, Math.min(100, rawScore * 100)));
109
+ const evidence = [];
110
+ evidence.push(`${run.testsPassed}/${total} tests passed` +
111
+ (run.testsFailed > 0 ? ` (${run.testsFailed} failed)` : '') +
112
+ (run.testsErrored > 0 ? ` (${run.testsErrored} errored)` : '') +
113
+ ` — ${Math.round(passRate * 100)}% pass-rate.`);
114
+ if (run.workflowName)
115
+ evidence.push(`Workflow: ${run.workflowName}.`);
116
+ if (run.testsFlaky && run.testsFlaky > 0) {
117
+ evidence.push(`${run.testsFlaky} test(s) flaky (passed on retry).`);
118
+ }
119
+ if (run.runUrl)
120
+ evidence.push(`Run: ${run.runUrl}`);
121
+ if (freshnessRatio < 1.0) {
122
+ evidence.push(`Freshness factor ${freshnessRatio.toFixed(2)} applied (run age ${Math.round(ageS / 3600)}h).`);
123
+ }
124
+ const recommendations = [];
125
+ if (run.testsFailed > 0)
126
+ recommendations.push(`Fix ${run.testsFailed} failing test(s) before shipping.`);
127
+ if (run.testsErrored > 0)
128
+ recommendations.push(`Investigate ${run.testsErrored} errored test(s) (infra/setup).`);
129
+ if (run.testsFlaky && run.testsFlaky > 0) {
130
+ recommendations.push(`Stabilize ${run.testsFlaky} flaky test(s) to improve signal quality.`);
131
+ }
132
+ return {
133
+ source,
134
+ score,
135
+ weight,
136
+ applicability: 'applicable',
137
+ blocking: false,
138
+ evidence,
139
+ recommendations,
140
+ collectedAt: now,
141
+ collector: { tool: 'qulib.ci-results-adapter', inputRef: run.runUrl },
142
+ };
143
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"cypress-e2e-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/cypress-e2e-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAY,MAAM,mCAAmC,CAAC;AA2ClG,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,WAAW,iBAAiB;IAErC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IA4BhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}
1
+ {"version":3,"file":"cypress-e2e-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/cypress-e2e-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAY,MAAM,mCAAmC,CAAC;AA+DlG,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,WAAW,iBAAiB;IAErC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAiChD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}