@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
@@ -38,15 +38,38 @@ function renderStep(step) {
38
38
  return ` // TODO: ${step.description}`;
39
39
  }
40
40
  }
41
+ /**
42
+ * Render a recipe-specific step override for Cypress.
43
+ * Returns null when no override applies — falls through to renderStep.
44
+ */
45
+ function renderRecipeStep(step, scenario) {
46
+ const tags = scenario.tags ?? [];
47
+ // a11y recipe: title assertion — cy.title() instead of cy.get('title')
48
+ if (tags.includes('recipe-a11y') && step.action === 'assert-text' && step.target === 'title') {
49
+ return ` cy.title().should('not.be.empty');`;
50
+ }
51
+ // a11y recipe: nav count using proper Cypress assertion
52
+ if (tags.includes('recipe-a11y') && step.action === 'assert-count') {
53
+ const t = step.target != null ? JSON.stringify(step.target) : null;
54
+ if (t) {
55
+ return ` cy.get(${t}).its('length').should('be.gte', ${parseInt(step.value ?? '1', 10)});`;
56
+ }
57
+ }
58
+ return null;
59
+ }
41
60
  export class CypressE2EAdapter {
42
61
  adapterType = 'cypress-e2e';
43
62
  render(scenario) {
44
63
  const slug = slugify(scenario.title);
45
64
  const filename = `${slug}.cy.ts`;
46
- const stepLines = scenario.steps.map(renderStep).join('\n');
65
+ const stepLines = scenario.steps
66
+ .map((step) => renderRecipeStep(step, scenario) ?? renderStep(step))
67
+ .join('\n');
68
+ const recipeTag = (scenario.tags ?? []).find((t) => t.startsWith('recipe-'));
69
+ const recipeComment = recipeTag ? `\n// recipe: ${recipeTag.replace('recipe-', '')}` : '';
47
70
  const code = [
48
71
  `// ${scenario.description}`,
49
- `// qulib-generated — scenario: ${scenario.id}`,
72
+ `// qulib-generated — scenario: ${scenario.id}${recipeComment}`,
50
73
  ``,
51
74
  `describe(${JSON.stringify(scenario.title)}, () => {`,
52
75
  ` it(${JSON.stringify(scenario.description)}, () => {`,
@@ -1 +1 @@
1
- {"version":3,"file":"playwright-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/playwright-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,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,WAAW,gBAAgB;IAEpC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAIhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}
1
+ {"version":3,"file":"playwright-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/playwright-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAY,MAAM,mCAAmC,CAAC;AAqElG,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,WAAW,gBAAgB;IAEpC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAmChD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}
@@ -1,9 +1,101 @@
1
+ function slugify(title) {
2
+ return title
3
+ .toLowerCase()
4
+ .replace(/[^a-z0-9]+/g, '-')
5
+ .replace(/^-|-$/g, '');
6
+ }
7
+ function renderStep(step) {
8
+ const t = step.target != null ? JSON.stringify(step.target) : null;
9
+ const v = step.value != null ? JSON.stringify(step.value) : null;
10
+ switch (step.action) {
11
+ case 'navigate':
12
+ return ` await page.goto(${JSON.stringify(step.target ?? step.value ?? '/')});`;
13
+ case 'click':
14
+ return t ? ` await page.locator(${t}).click();` : ` // click: ${step.description}`;
15
+ case 'type':
16
+ return t && v ? ` await page.locator(${t}).fill(${v});` : ` // type: ${step.description}`;
17
+ case 'assert-visible':
18
+ return t
19
+ ? ` await expect(page.locator(${t})).toBeVisible();`
20
+ : ` await expect(page.locator('body')).toBeVisible();`;
21
+ case 'assert-hidden':
22
+ return t
23
+ ? ` await expect(page.locator(${t})).toBeHidden();`
24
+ : ` // assert-hidden: ${step.description}`;
25
+ case 'assert-text':
26
+ if (t && v)
27
+ return ` await expect(page.locator(${t})).toContainText(${v});`;
28
+ if (t)
29
+ return ` await expect(page.locator(${t})).not.toBeEmpty();`;
30
+ return ` // assert-text: ${step.description}`;
31
+ case 'assert-disabled':
32
+ return t
33
+ ? ` await expect(page.locator(${t})).toBeDisabled();`
34
+ : ` // assert-disabled: ${step.description}`;
35
+ case 'assert-count':
36
+ return t
37
+ ? ` expect(await page.locator(${t}).count()).toBeGreaterThanOrEqual(${parseInt(step.value ?? '1', 10)});`
38
+ : ` // assert-count: ${step.description}`;
39
+ case 'wait':
40
+ return ` await page.waitForTimeout(${parseInt(step.value ?? '1000', 10)});`;
41
+ case 'api-call':
42
+ return ` expect((await page.request.get(${JSON.stringify(step.target ?? step.value ?? '/')})).status()).toBe(200);`;
43
+ default:
44
+ return ` // TODO: ${step.description}`;
45
+ }
46
+ }
47
+ /**
48
+ * Render a recipe-specific step override for Playwright.
49
+ * Returns null when no override applies — falls through to renderStep.
50
+ */
51
+ function renderRecipeStep(step, scenario) {
52
+ const tags = scenario.tags ?? [];
53
+ // a11y recipe: title assertion — page.title() instead of page.locator('title')
54
+ if (tags.includes('recipe-a11y') && step.action === 'assert-text' && step.target === 'title') {
55
+ return ` expect(await page.title()).not.toBe('');`;
56
+ }
57
+ // a11y recipe: nav count using Playwright count()
58
+ if (tags.includes('recipe-a11y') && step.action === 'assert-count') {
59
+ const t = step.target != null ? JSON.stringify(step.target) : null;
60
+ if (t) {
61
+ return ` expect(await page.locator(${t}).count()).toBeGreaterThanOrEqual(${parseInt(step.value ?? '1', 10)});`;
62
+ }
63
+ }
64
+ return null;
65
+ }
1
66
  export class PlaywrightAdapter {
2
67
  adapterType = 'playwright';
3
68
  render(scenario) {
4
- throw new Error('Not implemented');
69
+ const slug = slugify(scenario.title);
70
+ const filename = `${slug}.spec.ts`;
71
+ const stepLines = scenario.steps
72
+ .map((step) => renderRecipeStep(step, scenario) ?? renderStep(step))
73
+ .join('\n');
74
+ const recipeTag = (scenario.tags ?? []).find((t) => t.startsWith('recipe-'));
75
+ const recipeComment = recipeTag ? `\n// recipe: ${recipeTag.replace('recipe-', '')}` : '';
76
+ const code = [
77
+ `// ${scenario.description}`,
78
+ `// qulib-generated — scenario: ${scenario.id}${recipeComment}`,
79
+ ``,
80
+ `import { test, expect } from '@playwright/test';`,
81
+ ``,
82
+ `test.describe(${JSON.stringify(scenario.title)}, () => {`,
83
+ ` test(${JSON.stringify(scenario.description)}, async ({ page }) => {`,
84
+ stepLines || ` // no steps — add assertions for: ${scenario.targetPath}`,
85
+ ` });`,
86
+ `});`,
87
+ ``,
88
+ ].join('\n');
89
+ return {
90
+ scenarioId: scenario.id,
91
+ adapter: 'playwright',
92
+ filename,
93
+ code,
94
+ source: 'template',
95
+ outputPath: `tests/${filename}`,
96
+ };
5
97
  }
6
98
  renderAll(scenarios) {
7
- throw new Error('Not implemented');
99
+ return scenarios.map((s) => this.render(s));
8
100
  }
9
101
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * PR-metadata evidence adapter (P4 — evidence collectors).
3
+ *
4
+ * Maps a pull-request review/checks payload (as returned by `gh pr view --json
5
+ * reviewDecision,statusCheckRollup,mergeable,number,url,additions,deletions`)
6
+ * into an `EvidenceItem` for `computeReleaseConfidence`, using the
7
+ * `deploy-metadata` source kind — the closest P3-reserved kind for
8
+ * PR-level ship-readiness.
9
+ *
10
+ * Design:
11
+ * - Pure function. The caller fetches the `gh` JSON; this adapter scores it.
12
+ * - Applicability:
13
+ * `applicable` — a PR exists and review/check state is readable
14
+ * `not_applicable` — no PR for this ref (direct push, script, etc.)
15
+ * `unknown` — PR exists but checks are still pending / state ambiguous
16
+ * - Score formula (0..100):
17
+ * Base 60 points: PR is open and merge-ready (no conflicts)
18
+ * +20: reviewDecision === 'APPROVED'
19
+ * +20: all status checks pass (statusCheckRollup every entry state === 'SUCCESS')
20
+ * Deductions:
21
+ * -10: any failing status check (per failing check, capped at 20)
22
+ * -15: reviewDecision === 'CHANGES_REQUESTED'
23
+ * - The adapter NEVER fabricates a PR number or URL; if absent from the payload
24
+ * the evidence strings omit them (WAVE-GUARDRAILS: no fabricated data).
25
+ */
26
+ import type { EvidenceItem } from '../schemas/confidence.schema.js';
27
+ /**
28
+ * Status-check entry shape from `gh pr view --json statusCheckRollup`.
29
+ * Only the fields we actually use — extra fields are harmlessly ignored.
30
+ */
31
+ export interface StatusCheck {
32
+ /** e.g. "SUCCESS", "FAILURE", "PENDING", "SKIPPED", "NEUTRAL" */
33
+ state: string;
34
+ /** CI job/check name — optional, used for the evidence string */
35
+ name?: string;
36
+ /** Direct URL to the check — never fabricated; omit rather than invent */
37
+ targetUrl?: string;
38
+ }
39
+ /** GitHub review decision values as returned by the `gh` CLI. */
40
+ export type ReviewDecision = 'APPROVED' | 'CHANGES_REQUESTED' | 'REVIEW_REQUIRED' | null | undefined;
41
+ /** Mergeable state as returned by `gh pr view --json mergeable`. */
42
+ export type MergeableState = 'MERGEABLE' | 'CONFLICTING' | 'UNKNOWN' | null | undefined;
43
+ /**
44
+ * Raw PR payload the caller provides. All fields are optional — the adapter
45
+ * degrades gracefully when partial data is available.
46
+ */
47
+ export interface PrMetadataInput {
48
+ /** PR number. Never fabricated — omit if absent. */
49
+ number?: number;
50
+ /** PR URL. Never fabricated — omit if absent. */
51
+ url?: string;
52
+ /** Review decision from the `gh` response. null/undefined = no review yet. */
53
+ reviewDecision?: ReviewDecision;
54
+ /** Array of status check entries (the `statusCheckRollup` field). */
55
+ statusCheckRollup?: StatusCheck[];
56
+ /**
57
+ * Whether the PR is currently mergeable (no conflicts).
58
+ * null/UNKNOWN counts as unknown-state.
59
+ */
60
+ mergeable?: MergeableState;
61
+ /** ISO-8601 when this payload was collected. Defaults to now. */
62
+ collectedAt?: string;
63
+ /**
64
+ * Set to true when there is explicitly NO PR for this ref (e.g. direct push).
65
+ * Produces a `not_applicable` contribution so the aggregator abstains honestly.
66
+ */
67
+ noPr?: boolean;
68
+ }
69
+ /**
70
+ * Produce a `deploy-metadata` EvidenceItem from a PR metadata payload.
71
+ * Returns `not_applicable` when `noPr` is true, `unknown` when checks are
72
+ * pending or state is ambiguous, and `applicable` with a real score otherwise.
73
+ */
74
+ export declare function prMetadataToEvidence(input: PrMetadataInput, collectedAt?: string): EvidenceItem;
75
+ //# sourceMappingURL=pr-metadata-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pr-metadata-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/pr-metadata-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAsB,MAAM,iCAAiC,CAAC;AAIxF;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,iEAAiE;AACjE,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,mBAAmB,GAAG,iBAAiB,GAAG,IAAI,GAAG,SAAS,CAAC;AAErG,oEAAoE;AACpE,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,aAAa,GAAG,SAAS,GAAG,IAAI,GAAG,SAAS,CAAC;AAExF;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,WAAW,EAAE,CAAC;IAClC;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,YAAY,CA+H/F"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * PR-metadata evidence adapter (P4 — evidence collectors).
3
+ *
4
+ * Maps a pull-request review/checks payload (as returned by `gh pr view --json
5
+ * reviewDecision,statusCheckRollup,mergeable,number,url,additions,deletions`)
6
+ * into an `EvidenceItem` for `computeReleaseConfidence`, using the
7
+ * `deploy-metadata` source kind — the closest P3-reserved kind for
8
+ * PR-level ship-readiness.
9
+ *
10
+ * Design:
11
+ * - Pure function. The caller fetches the `gh` JSON; this adapter scores it.
12
+ * - Applicability:
13
+ * `applicable` — a PR exists and review/check state is readable
14
+ * `not_applicable` — no PR for this ref (direct push, script, etc.)
15
+ * `unknown` — PR exists but checks are still pending / state ambiguous
16
+ * - Score formula (0..100):
17
+ * Base 60 points: PR is open and merge-ready (no conflicts)
18
+ * +20: reviewDecision === 'APPROVED'
19
+ * +20: all status checks pass (statusCheckRollup every entry state === 'SUCCESS')
20
+ * Deductions:
21
+ * -10: any failing status check (per failing check, capped at 20)
22
+ * -15: reviewDecision === 'CHANGES_REQUESTED'
23
+ * - The adapter NEVER fabricates a PR number or URL; if absent from the payload
24
+ * the evidence strings omit them (WAVE-GUARDRAILS: no fabricated data).
25
+ */
26
+ const PR_META_WEIGHT = 0.07; // conservative; real weight rebalanced by the aggregator
27
+ /**
28
+ * Produce a `deploy-metadata` EvidenceItem from a PR metadata payload.
29
+ * Returns `not_applicable` when `noPr` is true, `unknown` when checks are
30
+ * pending or state is ambiguous, and `applicable` with a real score otherwise.
31
+ */
32
+ export function prMetadataToEvidence(input, collectedAt) {
33
+ const now = collectedAt ?? input.collectedAt ?? new Date().toISOString();
34
+ const source = 'deploy-metadata';
35
+ const weight = PR_META_WEIGHT;
36
+ // Explicit no-PR case.
37
+ if (input.noPr) {
38
+ return {
39
+ source,
40
+ score: 0,
41
+ weight,
42
+ applicability: 'not_applicable',
43
+ blocking: false,
44
+ evidence: ['No pull request exists for this ref (direct push or pre-PR state).'],
45
+ recommendations: [],
46
+ reason: 'No PR for this ref — PR review and check signal not applicable.',
47
+ collectedAt: now,
48
+ collector: { tool: 'qulib.pr-metadata-adapter' },
49
+ };
50
+ }
51
+ const prLabel = input.number != null ? `PR #${input.number}` : 'PR';
52
+ const prRef = input.url ?? null;
53
+ // Checks-pending / UNKNOWN mergeable → unknown applicability (cannot score honestly).
54
+ const allChecks = input.statusCheckRollup ?? [];
55
+ const pending = allChecks.filter((c) => c.state === 'PENDING');
56
+ if ((pending.length > 0 && allChecks.length > 0 && pending.length === allChecks.length) ||
57
+ input.mergeable === 'UNKNOWN') {
58
+ const evidence = [
59
+ `${prLabel}: status checks still pending (${pending.length}/${allChecks.length}).`,
60
+ ...(prRef ? [`${prRef}`] : []),
61
+ ];
62
+ return {
63
+ source,
64
+ score: 0,
65
+ weight,
66
+ applicability: 'unknown',
67
+ blocking: false,
68
+ evidence,
69
+ recommendations: ['Wait for all status checks to complete before shipping.'],
70
+ reason: `${prLabel} checks are still pending — cannot score PR readiness honestly.`,
71
+ collectedAt: now,
72
+ collector: { tool: 'qulib.pr-metadata-adapter', inputRef: prRef ?? undefined },
73
+ };
74
+ }
75
+ // Score computation.
76
+ let score = 60; // base: a PR exists and is evaluable
77
+ // Mergeability.
78
+ if (input.mergeable === 'CONFLICTING') {
79
+ score -= 30; // merge conflicts are a hard deduction
80
+ }
81
+ // Review decision.
82
+ const rd = input.reviewDecision;
83
+ if (rd === 'APPROVED') {
84
+ score += 20;
85
+ }
86
+ else if (rd === 'CHANGES_REQUESTED') {
87
+ score -= 15;
88
+ }
89
+ // REVIEW_REQUIRED or null/undefined: no bonus, no deduction (reviewer not yet assigned)
90
+ // Status checks.
91
+ const passing = allChecks.filter((c) => c.state === 'SUCCESS' || c.state === 'NEUTRAL' || c.state === 'SKIPPED');
92
+ const failing = allChecks.filter((c) => c.state === 'FAILURE' || c.state === 'ERROR');
93
+ if (allChecks.length > 0 && failing.length === 0) {
94
+ score += 20; // all checks green
95
+ }
96
+ // Deduct per failing check (capped at −20).
97
+ score -= Math.min(20, failing.length * 10);
98
+ score = Math.max(0, Math.min(100, score));
99
+ // Build evidence strings — never fabricated.
100
+ const evidence = [];
101
+ const reviewStr = rd === 'APPROVED'
102
+ ? 'APPROVED'
103
+ : rd === 'CHANGES_REQUESTED'
104
+ ? 'CHANGES_REQUESTED'
105
+ : rd === 'REVIEW_REQUIRED'
106
+ ? 'REVIEW_REQUIRED'
107
+ : 'no review yet';
108
+ const mergeStr = input.mergeable === 'MERGEABLE'
109
+ ? 'mergeable'
110
+ : input.mergeable === 'CONFLICTING'
111
+ ? 'CONFLICTING (merge conflicts)'
112
+ : 'merge state unknown';
113
+ evidence.push(`${prLabel}: review=${reviewStr}, ${mergeStr}.`);
114
+ if (allChecks.length > 0) {
115
+ evidence.push(`Status checks: ${passing.length} passed, ${failing.length} failed, ${pending.length} pending of ${allChecks.length} total.`);
116
+ for (const f of failing.slice(0, 3)) {
117
+ evidence.push(` Check FAILED: ${f.name ?? 'unnamed'}${f.targetUrl ? ` (${f.targetUrl})` : ''}`);
118
+ }
119
+ }
120
+ else {
121
+ evidence.push('No status checks configured for this PR.');
122
+ }
123
+ if (prRef)
124
+ evidence.push(prRef);
125
+ // Recommendations.
126
+ const recommendations = [];
127
+ if (rd === 'CHANGES_REQUESTED')
128
+ recommendations.push('Address review comments before shipping.');
129
+ if (rd === 'REVIEW_REQUIRED' || rd == null)
130
+ recommendations.push('Request and obtain PR approval before shipping.');
131
+ if (failing.length > 0)
132
+ recommendations.push(`Fix ${failing.length} failing status check(s) before merging.`);
133
+ if (input.mergeable === 'CONFLICTING')
134
+ recommendations.push('Resolve merge conflicts before shipping.');
135
+ return {
136
+ source,
137
+ score,
138
+ weight,
139
+ applicability: 'applicable',
140
+ blocking: false,
141
+ evidence,
142
+ recommendations,
143
+ collectedAt: now,
144
+ collector: { tool: 'qulib.pr-metadata-adapter', inputRef: prRef ?? undefined },
145
+ };
146
+ }
@@ -0,0 +1,55 @@
1
+ import type { GeneratedTest } from '../schemas/gap-analysis.schema.js';
2
+ /**
3
+ * Per-spec validation outcome from the scaffold dry-run.
4
+ *
5
+ * `valid: false` means the TypeScript compiler reported one or more *syntactic*
6
+ * errors when transpiling the generated spec as a standalone module — i.e. the
7
+ * generator emitted code that will not parse, so it would fail the moment a
8
+ * developer ran the test suite. `errors` carries the flattened compiler
9
+ * messages so the failure is actionable, not just a boolean.
10
+ */
11
+ export interface SpecValidation {
12
+ scenarioId: string;
13
+ filename: string;
14
+ outputPath: string;
15
+ valid: boolean;
16
+ errors: string[];
17
+ }
18
+ /** Aggregate result of validating every generated spec in a scaffold run. */
19
+ export interface SpecValidationReport {
20
+ ok: boolean;
21
+ total: number;
22
+ invalidCount: number;
23
+ results: SpecValidation[];
24
+ }
25
+ /**
26
+ * Dry-run a single generated spec through the TypeScript compiler.
27
+ *
28
+ * `transpileModule` performs single-file syntax transformation only — no type
29
+ * checking, no module resolution — so it is the right tool to answer the one
30
+ * question the scaffold must not get wrong: *does the string we are about to
31
+ * write to disk actually parse as TypeScript?* A clean run (zero error-category
32
+ * diagnostics AND non-empty output) means the spec is syntactically valid; any
33
+ * error diagnostic means the generator produced broken code.
34
+ *
35
+ * We deliberately do NOT resolve `@playwright/test` / Cypress globals here:
36
+ * those are type-level concerns that require the consumer's node_modules. The
37
+ * gap this closes is *parse/compile-shape*, which is generator-local and cheap
38
+ * to check at scaffold time.
39
+ */
40
+ export declare function validateSpecCode(code: string): {
41
+ valid: boolean;
42
+ errors: string[];
43
+ };
44
+ /** Validate one GeneratedTest, returning a structured per-spec outcome. */
45
+ export declare function validateGeneratedTest(test: GeneratedTest): SpecValidation;
46
+ /**
47
+ * Validate every generated spec in a scaffold run.
48
+ *
49
+ * `ok` is true only when *all* specs parse. This is the gate the CLI consults
50
+ * to decide its exit code: a scaffold that writes a spec which cannot parse is a
51
+ * silent correctness failure, and the whole point of the dry-run is to catch it
52
+ * before the developer does.
53
+ */
54
+ export declare function validateGeneratedTests(tests: GeneratedTest[]): SpecValidationReport;
55
+ //# sourceMappingURL=validate-specs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-specs.d.ts","sourceRoot":"","sources":["../../src/adapters/validate-specs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAEvE;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,6EAA6E;AAC7E,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA2BnF;AAED,2EAA2E;AAC3E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,aAAa,GAAG,cAAc,CASzE;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,oBAAoB,CASnF"}
@@ -0,0 +1,67 @@
1
+ import ts from 'typescript';
2
+ /**
3
+ * Dry-run a single generated spec through the TypeScript compiler.
4
+ *
5
+ * `transpileModule` performs single-file syntax transformation only — no type
6
+ * checking, no module resolution — so it is the right tool to answer the one
7
+ * question the scaffold must not get wrong: *does the string we are about to
8
+ * write to disk actually parse as TypeScript?* A clean run (zero error-category
9
+ * diagnostics AND non-empty output) means the spec is syntactically valid; any
10
+ * error diagnostic means the generator produced broken code.
11
+ *
12
+ * We deliberately do NOT resolve `@playwright/test` / Cypress globals here:
13
+ * those are type-level concerns that require the consumer's node_modules. The
14
+ * gap this closes is *parse/compile-shape*, which is generator-local and cheap
15
+ * to check at scaffold time.
16
+ */
17
+ export function validateSpecCode(code) {
18
+ const result = ts.transpileModule(code, {
19
+ compilerOptions: {
20
+ module: ts.ModuleKind.ESNext,
21
+ target: ts.ScriptTarget.ES2020,
22
+ // isolatedModules keeps this a pure syntax pass and surfaces
23
+ // single-file-illegal constructs, matching how a bundler would see it.
24
+ isolatedModules: true,
25
+ },
26
+ reportDiagnostics: true,
27
+ });
28
+ const errorDiagnostics = (result.diagnostics ?? []).filter((d) => d.category === ts.DiagnosticCategory.Error);
29
+ const errors = errorDiagnostics.map((d) => ts.flattenDiagnosticMessageText(d.messageText, '\n'));
30
+ // A clean transpile yields output text. Empty output with no diagnostics would
31
+ // be suspicious, so treat it as invalid too rather than silently passing.
32
+ const producedOutput = result.outputText.trim().length > 0;
33
+ const valid = errors.length === 0 && producedOutput;
34
+ if (!valid && errors.length === 0) {
35
+ errors.push('transpile produced no output for a non-empty spec');
36
+ }
37
+ return { valid, errors };
38
+ }
39
+ /** Validate one GeneratedTest, returning a structured per-spec outcome. */
40
+ export function validateGeneratedTest(test) {
41
+ const { valid, errors } = validateSpecCode(test.code);
42
+ return {
43
+ scenarioId: test.scenarioId,
44
+ filename: test.filename,
45
+ outputPath: test.outputPath,
46
+ valid,
47
+ errors,
48
+ };
49
+ }
50
+ /**
51
+ * Validate every generated spec in a scaffold run.
52
+ *
53
+ * `ok` is true only when *all* specs parse. This is the gate the CLI consults
54
+ * to decide its exit code: a scaffold that writes a spec which cannot parse is a
55
+ * silent correctness failure, and the whole point of the dry-run is to catch it
56
+ * before the developer does.
57
+ */
58
+ export function validateGeneratedTests(tests) {
59
+ const results = tests.map(validateGeneratedTest);
60
+ const invalidCount = results.filter((r) => !r.valid).length;
61
+ return {
62
+ ok: invalidCount === 0,
63
+ total: results.length,
64
+ invalidCount,
65
+ results,
66
+ };
67
+ }
@@ -0,0 +1,54 @@
1
+ import type { GapAnalysis } from '../schemas/gap-analysis.schema.js';
2
+ import { type BaselineSnapshot, type BaselineDelta } from './baseline.schema.js';
3
+ /**
4
+ * Produce a filesystem-safe slug from a URL.
5
+ * e.g. "https://my-app.vercel.app/admin" → "my-app_vercel_app__admin"
6
+ */
7
+ export declare function slugifyUrl(url: string): string;
8
+ /**
9
+ * Return the default root for baseline storage: `<cwd>/.qulib-baselines`.
10
+ * Callers may supply an explicit `baseDir` to override.
11
+ */
12
+ export declare function defaultBaselineRoot(): string;
13
+ /**
14
+ * Save a baseline snapshot derived from the given `GapAnalysis` result.
15
+ *
16
+ * @returns The saved snapshot.
17
+ */
18
+ export declare function saveBaseline(analysis: GapAnalysis, url: string, options?: {
19
+ baseDir?: string;
20
+ label?: string;
21
+ }): Promise<BaselineSnapshot>;
22
+ /**
23
+ * Load a specific baseline snapshot by its `id`.
24
+ *
25
+ * @throws If the file does not exist or fails schema validation.
26
+ */
27
+ export declare function loadBaseline(id: string, options?: {
28
+ baseDir?: string;
29
+ }): Promise<BaselineSnapshot>;
30
+ /**
31
+ * List all saved baselines for the given URL, sorted newest-first.
32
+ *
33
+ * Returns an empty array if no baselines exist yet.
34
+ */
35
+ export declare function listBaselines(url: string, options?: {
36
+ baseDir?: string;
37
+ }): Promise<BaselineSnapshot[]>;
38
+ /**
39
+ * Delete a specific baseline snapshot by its `id`.
40
+ *
41
+ * @throws If the file does not exist.
42
+ */
43
+ export declare function deleteBaseline(id: string, options?: {
44
+ baseDir?: string;
45
+ }): Promise<void>;
46
+ /**
47
+ * Compare two baseline snapshots and return a structured delta report.
48
+ *
49
+ * - `newGaps`: problems present in `current` but not in `prior`.
50
+ * - `resolvedGaps`: problems present in `prior` but no longer in `current`.
51
+ * - `severityChanges`: same problem (matching key) with a different severity.
52
+ */
53
+ export declare function compareBaselines(prior: BaselineSnapshot, current: BaselineSnapshot): BaselineDelta;
54
+ //# sourceMappingURL=baseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../../src/baseline/baseline.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAO,MAAM,mCAAmC,CAAC;AAC1E,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAEnB,MAAM,sBAAsB,CAAC;AAY9B;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAeD;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,QAAQ,EAAE,WAAW,EACrB,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GACjD,OAAO,CAAC,gBAAgB,CAAC,CA0B3B;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuB5G;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACjC,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAoC7B;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAclG;AAcD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,gBAAgB,GAAG,aAAa,CAiFlG"}