@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
@@ -1,16 +1,29 @@
1
1
  import { existsSync, readdirSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { AutomationMaturitySchema } from '../../schemas/automation-maturity.schema.js';
4
+ import { REBALANCED_WEIGHTS } from './api-coverage.js';
4
5
  /**
5
- * Dimension weights (sum = 1). Breadth + harness adoption dominate: shipping risk is mostly
6
- * untested routes and missing Playwright/Cypress-level coverage.
6
+ * Dimension weights (sum = 1).
7
+ *
8
+ * When apiSurface is NOT provided (backward-compat path), the original 6 dimensions
9
+ * continue to use the original weights (sum = 1.0). When apiSurface IS provided, the
10
+ * rebalanced weights from api-coverage.ts are used and the 7th dimension is added.
11
+ *
12
+ * Original weights (no API surface):
13
+ * test-coverage-breadth 0.28, framework-adoption 0.22, test-id-hygiene 0.18,
14
+ * ci-integration 0.14, auth-test-coverage 0.10, component-test-ratio 0.08
15
+ *
16
+ * Rebalanced weights (with API surface, sum still = 1.0 across all 7):
17
+ * test-coverage-breadth 0.24, framework-adoption 0.19, test-id-hygiene 0.15,
18
+ * ci-integration 0.12, auth-test-coverage 0.09, component-test-ratio 0.06,
19
+ * api-test-coverage 0.15
7
20
  */
8
- const W_TEST_BREADTH = 0.28;
9
- const W_FRAMEWORK = 0.22;
10
- const W_TEST_ID = 0.18;
11
- const W_CI = 0.14;
12
- const W_AUTH_TESTS = 0.1;
13
- const W_COMPONENT_RATIO = 0.08;
21
+ const W_TEST_BREADTH_ORIGINAL = 0.28;
22
+ const W_FRAMEWORK_ORIGINAL = 0.22;
23
+ const W_TEST_ID_ORIGINAL = 0.18;
24
+ const W_CI_ORIGINAL = 0.14;
25
+ const W_AUTH_TESTS_ORIGINAL = 0.1;
26
+ const W_COMPONENT_RATIO_ORIGINAL = 0.08;
14
27
  function hasCiAtRoot(repoPath) {
15
28
  const ev = [];
16
29
  const gh = join(repoPath, '.github', 'workflows');
@@ -49,7 +62,24 @@ function scoreLevel(overall) {
49
62
  return { level: 4, label: 'L4 — strong automation' };
50
63
  return { level: 5, label: 'L5 — advanced QA automation' };
51
64
  }
52
- export function computeAutomationMaturity(repo) {
65
+ /**
66
+ * Compute automation maturity for a repo.
67
+ *
68
+ * @param repo - The scanned repo analysis.
69
+ * @param apiCoverageResult - Optional pre-computed API coverage result. When absent the
70
+ * 6 original dimensions are scored with the original weights (backward-compatible).
71
+ * When provided, a 7th dimension (`api-test-coverage`) is added and weights are
72
+ * rebalanced so the total still sums to 1.0 across applicable dimensions.
73
+ */
74
+ export function computeAutomationMaturity(repo, apiCoverageResult) {
75
+ // Choose weights based on whether we have an API surface result
76
+ const hasApiDim = apiCoverageResult !== undefined;
77
+ const W_TEST_BREADTH = hasApiDim ? REBALANCED_WEIGHTS.TEST_BREADTH : W_TEST_BREADTH_ORIGINAL;
78
+ const W_FRAMEWORK = hasApiDim ? REBALANCED_WEIGHTS.FRAMEWORK : W_FRAMEWORK_ORIGINAL;
79
+ const W_TEST_ID = hasApiDim ? REBALANCED_WEIGHTS.TEST_ID : W_TEST_ID_ORIGINAL;
80
+ const W_CI = hasApiDim ? REBALANCED_WEIGHTS.CI : W_CI_ORIGINAL;
81
+ const W_AUTH_TESTS = hasApiDim ? REBALANCED_WEIGHTS.AUTH_TESTS : W_AUTH_TESTS_ORIGINAL;
82
+ const W_COMPONENT_RATIO = hasApiDim ? REBALANCED_WEIGHTS.COMPONENT_RATIO : W_COMPONENT_RATIO_ORIGINAL;
53
83
  const routePaths = [...new Set(repo.routes.map((r) => r.path))];
54
84
  let coveredRoutes = 0;
55
85
  for (const p of routePaths) {
@@ -204,6 +234,10 @@ export function computeAutomationMaturity(repo) {
204
234
  ...(compGuidance && { guidance: compGuidance }),
205
235
  };
206
236
  const dimensions = [breadthDim, frameworkDim, hygieneDim, ciDim, authDim, compDim];
237
+ // Append api-test-coverage dimension when an API surface result was provided
238
+ if (apiCoverageResult) {
239
+ dimensions.push(apiCoverageResult.dimension);
240
+ }
207
241
  // Overall score normalizes over applicable dimensions only.
208
242
  // overallScore = round( Σ score_i * weight_i / Σ weight_i ) for i ∈ applicable.
209
243
  // If no dimension is applicable (degenerate repo), overall = 0 and level = L1.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * qulib-native adapter — maps qulib collector outputs to EvidenceItem[].
3
+ *
4
+ * P3 — qulib Confidence Layer v1.
5
+ *
6
+ * This is the THIN WIRING layer, not the pure scorer. It translates:
7
+ * AnalyzeResult → live-app-quality + accessibility + crawl-coverage EvidenceItems
8
+ * AutomationMaturity → test-automation EvidenceItem
9
+ * ApiCoverageResult → api-coverage EvidenceItem
10
+ *
11
+ * Honesty rules (mirrors agent-summary.ts and the spec §2.5):
12
+ * - auth-required scan → applicability='unknown' (never silent pass)
13
+ * - blocked scan → blocking=true (hard blocker)
14
+ * - low-coverage → crawl-coverage applicability='unknown'
15
+ * - 0-endpoint API → api-coverage carries its own not_applicable (passed through verbatim)
16
+ *
17
+ * Pure function: no I/O.
18
+ */
19
+ import type { AnalyzeResult } from '../../analyze.js';
20
+ import type { AutomationMaturity } from '../../schemas/automation-maturity.schema.js';
21
+ import type { ApiCoverageResult } from './api-coverage.js';
22
+ import type { ConfidenceInput, ConfidenceSubject } from '../../schemas/confidence.schema.js';
23
+ /**
24
+ * Build a ConfidenceInput from qulib's own collector outputs.
25
+ * Pass whichever collectors you have; omitted collectors produce no evidence item.
26
+ */
27
+ export declare function buildConfidenceInputFromQulib(args: {
28
+ analyze?: AnalyzeResult;
29
+ maturity?: AutomationMaturity;
30
+ apiCoverage?: ApiCoverageResult;
31
+ subject: ConfidenceSubject;
32
+ policy?: ConfidenceInput['policy'];
33
+ }): ConfidenceInput;
34
+ //# sourceMappingURL=confidence-from-qulib.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confidence-from-qulib.d.ts","sourceRoot":"","sources":["../../../src/tools/scoring/confidence-from-qulib.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6CAA6C,CAAC;AACtF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EAEV,eAAe,EACf,iBAAiB,EAClB,MAAM,oCAAoC,CAAC;AAS5C;;;GAGG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE;IAClD,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC;CACpC,GAAG,eAAe,CAgMlB"}
@@ -0,0 +1,206 @@
1
+ /**
2
+ * qulib-native adapter — maps qulib collector outputs to EvidenceItem[].
3
+ *
4
+ * P3 — qulib Confidence Layer v1.
5
+ *
6
+ * This is the THIN WIRING layer, not the pure scorer. It translates:
7
+ * AnalyzeResult → live-app-quality + accessibility + crawl-coverage EvidenceItems
8
+ * AutomationMaturity → test-automation EvidenceItem
9
+ * ApiCoverageResult → api-coverage EvidenceItem
10
+ *
11
+ * Honesty rules (mirrors agent-summary.ts and the spec §2.5):
12
+ * - auth-required scan → applicability='unknown' (never silent pass)
13
+ * - blocked scan → blocking=true (hard blocker)
14
+ * - low-coverage → crawl-coverage applicability='unknown'
15
+ * - 0-endpoint API → api-coverage carries its own not_applicable (passed through verbatim)
16
+ *
17
+ * Pure function: no I/O.
18
+ */
19
+ // Default weights for the qulib-native sources (match confidence.ts DEFAULT_WEIGHTS).
20
+ const W_LIVE_APP = 0.30;
21
+ const W_TEST_AUTOMATION = 0.22;
22
+ const W_API_COVERAGE = 0.15;
23
+ const W_ACCESSIBILITY = 0.13;
24
+ const W_CRAWL_COVERAGE = 0.10;
25
+ /**
26
+ * Build a ConfidenceInput from qulib's own collector outputs.
27
+ * Pass whichever collectors you have; omitted collectors produce no evidence item.
28
+ */
29
+ export function buildConfidenceInputFromQulib(args) {
30
+ const items = [];
31
+ const now = new Date().toISOString();
32
+ // ------------------------------------------------------------------
33
+ // AnalyzeResult → live-app-quality + accessibility + crawl-coverage
34
+ // ------------------------------------------------------------------
35
+ if (args.analyze) {
36
+ const r = args.analyze;
37
+ const g = r.gapAnalysis;
38
+ // Determine if auth-required (honest: never silently pass).
39
+ const authRequired = g.mode === 'auth-required' || g.coverageWarning === 'auth-required';
40
+ const isBlocked = r.status === 'blocked';
41
+ // --- live-app-quality ---
42
+ const appRecs = [];
43
+ if (authRequired) {
44
+ appRecs.push('Provide auth credentials (form login or storage state) and re-run to evaluate the protected surface.');
45
+ }
46
+ const criticalGaps = r.gaps.filter((gap) => gap.severity === 'critical');
47
+ const highGaps = r.gaps.filter((gap) => gap.severity === 'high');
48
+ if (criticalGaps.length > 0) {
49
+ appRecs.push(`Fix ${criticalGaps.length} critical gap(s) before shipping.`);
50
+ }
51
+ else if (highGaps.length > 0) {
52
+ appRecs.push(`Address ${highGaps.length} high-severity gap(s).`);
53
+ }
54
+ const appEvidence = [];
55
+ if (isBlocked) {
56
+ appEvidence.push('Scan was blocked before producing a meaningful evaluation.');
57
+ }
58
+ else if (authRequired) {
59
+ appEvidence.push('Auth wall prevented scanning the protected surface.');
60
+ }
61
+ else {
62
+ appEvidence.push(`releaseConfidence=${r.releaseConfidence ?? 'null'}, status=${r.status}, gaps=${r.gaps.length}`);
63
+ if (criticalGaps.length > 0) {
64
+ appEvidence.push(`Critical gaps: ${criticalGaps.map((g2) => g2.path).join(', ')}`);
65
+ }
66
+ }
67
+ const liveAppItem = {
68
+ source: 'live-app-quality',
69
+ score: isBlocked ? null : (authRequired ? null : (r.releaseConfidence ?? null)),
70
+ weight: W_LIVE_APP,
71
+ applicability: authRequired ? 'unknown' : 'applicable',
72
+ blocking: isBlocked || criticalGaps.length > 0,
73
+ evidence: appEvidence,
74
+ recommendations: appRecs,
75
+ reason: authRequired
76
+ ? 'Auth wall prevented scanning — confidence score would be dishonest without the protected surface.'
77
+ : isBlocked
78
+ ? 'Scan was blocked; no evaluable surface.'
79
+ : undefined,
80
+ collectedAt: g.analyzedAt,
81
+ collector: {
82
+ tool: 'analyze_app',
83
+ inputRef: undefined,
84
+ },
85
+ };
86
+ items.push(liveAppItem);
87
+ // --- accessibility ---
88
+ const a11yGaps = r.gaps.filter((gap) => gap.category === 'a11y');
89
+ const a11yPenalty = a11yGaps.reduce((acc, gap) => {
90
+ const penalties = { critical: 30, high: 20, medium: 10, low: 5 };
91
+ return acc + (penalties[gap.severity] ?? 5);
92
+ }, 0);
93
+ const a11yScore = !isBlocked && !authRequired
94
+ ? Math.max(0, 100 - a11yPenalty)
95
+ : null;
96
+ const a11yItem = {
97
+ source: 'accessibility',
98
+ score: a11yScore,
99
+ weight: W_ACCESSIBILITY,
100
+ applicability: authRequired ? 'unknown' : 'applicable',
101
+ blocking: false,
102
+ evidence: isBlocked || authRequired
103
+ ? ['Accessibility could not be evaluated (scan blocked or auth-required).']
104
+ : a11yGaps.length === 0
105
+ ? ['No a11y gaps detected.']
106
+ : [`${a11yGaps.length} a11y gap(s) — penalty ${a11yPenalty} pts.`],
107
+ recommendations: a11yGaps.length > 0
108
+ ? ['Fix a11y violations flagged by the qulib scan (see gaps[].category=\'a11y\').']
109
+ : [],
110
+ reason: authRequired
111
+ ? 'Auth wall prevented a11y evaluation.'
112
+ : isBlocked
113
+ ? 'Scan blocked; no a11y signal.'
114
+ : undefined,
115
+ collectedAt: g.analyzedAt,
116
+ collector: {
117
+ tool: 'analyze_app',
118
+ inputRef: undefined,
119
+ },
120
+ };
121
+ items.push(a11yItem);
122
+ // --- crawl-coverage ---
123
+ const lowCoverage = g.coverageWarning === 'low-coverage';
124
+ const crawlScore = !isBlocked && !authRequired
125
+ ? (r.coverageScore ?? null)
126
+ : null;
127
+ const crawlItem = {
128
+ source: 'crawl-coverage',
129
+ score: crawlScore,
130
+ weight: W_CRAWL_COVERAGE,
131
+ applicability: authRequired || lowCoverage ? 'unknown' : 'applicable',
132
+ blocking: false,
133
+ evidence: [
134
+ `coverageScore=${r.coverageScore ?? 'null'}, pagesScanned=${g.coveragePagesScanned}`,
135
+ ...(g.coverageWarning ? [`coverageWarning: ${g.coverageWarning}`] : []),
136
+ ],
137
+ recommendations: lowCoverage
138
+ ? ['Increase crawl budget or supply deeper entry URLs to raise coverage above the floor.']
139
+ : [],
140
+ reason: authRequired
141
+ ? 'Auth-required scan; coverage limited to pre-auth pages.'
142
+ : lowCoverage
143
+ ? 'Coverage was below the confidence floor; treating as unknown signal.'
144
+ : undefined,
145
+ collectedAt: g.analyzedAt,
146
+ collector: {
147
+ tool: 'analyze_app',
148
+ inputRef: undefined,
149
+ },
150
+ };
151
+ items.push(crawlItem);
152
+ }
153
+ // ------------------------------------------------------------------
154
+ // AutomationMaturity → test-automation
155
+ // ------------------------------------------------------------------
156
+ if (args.maturity) {
157
+ const m = args.maturity;
158
+ const maturityItem = {
159
+ source: 'test-automation',
160
+ score: m.overallScore,
161
+ weight: W_TEST_AUTOMATION,
162
+ applicability: 'applicable',
163
+ blocking: false,
164
+ evidence: [`Automation maturity: ${m.label} (score ${m.overallScore})`],
165
+ recommendations: m.topRecommendations.slice(0, 3),
166
+ collectedAt: m.computedAt,
167
+ collector: {
168
+ tool: 'qulib_score_automation',
169
+ inputRef: m.repoPath,
170
+ },
171
+ };
172
+ items.push(maturityItem);
173
+ }
174
+ // ------------------------------------------------------------------
175
+ // ApiCoverageResult → api-coverage
176
+ // ------------------------------------------------------------------
177
+ if (args.apiCoverage) {
178
+ const d = args.apiCoverage.dimension;
179
+ const apiApplicability = d.applicability === 'not_applicable'
180
+ ? 'not_applicable'
181
+ : d.applicability === 'unknown'
182
+ ? 'unknown'
183
+ : 'applicable';
184
+ const apiItem = {
185
+ source: 'api-coverage',
186
+ score: d.score,
187
+ weight: W_API_COVERAGE,
188
+ applicability: apiApplicability,
189
+ blocking: false,
190
+ evidence: d.evidence,
191
+ recommendations: d.recommendations,
192
+ reason: d.reason,
193
+ collectedAt: new Date().toISOString(),
194
+ collector: {
195
+ tool: 'qulib_score_api',
196
+ inputRef: undefined,
197
+ },
198
+ };
199
+ items.push(apiItem);
200
+ }
201
+ return {
202
+ subject: args.subject,
203
+ evidence: items,
204
+ policy: args.policy,
205
+ };
206
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Pure view projections for the qulib Confidence Layer (Views 2–5).
3
+ *
4
+ * P3 — qulib Confidence Layer v1.
5
+ *
6
+ * All functions are pure (no I/O). Persistence sinks (file/db) are deferred to P4.
7
+ * View 1 (Release Confidence) IS the ReleaseConfidence object from the scorer.
8
+ *
9
+ * View 2 — diffConfidence: build a DeliveryTrafficPoint from two consecutive verdicts.
10
+ * View 3 — deriveInbox: extract human-judgment items from a verdict.
11
+ * View 4 — buildReplay: construct the provenance trace from input + result.
12
+ * View 5 — toAuditEntry: serialize a verdict to a tamper-evident audit record.
13
+ */
14
+ import type { ReleaseConfidence, ConfidenceInput } from '../../schemas/confidence.schema.js';
15
+ import type { DeliveryTrafficPoint, InboxItem, ReplayTrace, AuditEntry } from '../../schemas/views.schema.js';
16
+ /**
17
+ * Build a DeliveryTrafficPoint from the current verdict and an optional prior verdict.
18
+ * deltaFromPrev is null when there is no prior point.
19
+ */
20
+ export declare function diffConfidence(current: ReleaseConfidence, prior: ReleaseConfidence | null): DeliveryTrafficPoint;
21
+ /**
22
+ * Derive human-judgment inbox items from a verdict.
23
+ * Raises items for:
24
+ * - every blocking evidence item
25
+ * - every 'unknown' contribution on a requiredSource (when policy provides them)
26
+ * - 'block' verdict with a null score (nothing evaluable)
27
+ */
28
+ export declare function deriveInbox(rc: ReleaseConfidence, input: ConfidenceInput): InboxItem[];
29
+ /**
30
+ * Build the provenance trace from the scorer input + result.
31
+ * Steps are ordered by their appearance in the input evidence array,
32
+ * with all provenance fields carried from EvidenceItem.collector.
33
+ */
34
+ export declare function buildReplay(input: ConfidenceInput, rc: ReleaseConfidence): ReplayTrace;
35
+ /**
36
+ * Serialize a verdict to a tamper-evident audit record.
37
+ * recordHash is SHA-256 over the canonical record — changes when any field changes.
38
+ */
39
+ export declare function toAuditEntry(rc: ReleaseConfidence, evidenceSourceCount: number): AuditEntry;
40
+ //# sourceMappingURL=confidence-views.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confidence-views.d.ts","sourceRoot":"","sources":["../../../src/tools/scoring/confidence-views.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAC7F,OAAO,KAAK,EACV,oBAAoB,EACpB,SAAS,EACT,WAAW,EACX,UAAU,EACX,MAAM,+BAA+B,CAAC;AAYvC;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,iBAAiB,GAAG,IAAI,GAC9B,oBAAoB,CAgBtB;AAMD;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,EAAE,EAAE,iBAAiB,EACrB,KAAK,EAAE,eAAe,GACrB,SAAS,EAAE,CAwDb;AAMD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,EAAE,iBAAiB,GAAG,WAAW,CAsBtF;AA0BD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,GAAG,UAAU,CAe3F"}
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Pure view projections for the qulib Confidence Layer (Views 2–5).
3
+ *
4
+ * P3 — qulib Confidence Layer v1.
5
+ *
6
+ * All functions are pure (no I/O). Persistence sinks (file/db) are deferred to P4.
7
+ * View 1 (Release Confidence) IS the ReleaseConfidence object from the scorer.
8
+ *
9
+ * View 2 — diffConfidence: build a DeliveryTrafficPoint from two consecutive verdicts.
10
+ * View 3 — deriveInbox: extract human-judgment items from a verdict.
11
+ * View 4 — buildReplay: construct the provenance trace from input + result.
12
+ * View 5 — toAuditEntry: serialize a verdict to a tamper-evident audit record.
13
+ */
14
+ import { createHash } from 'node:crypto';
15
+ import { randomUUID } from 'node:crypto';
16
+ import { DeliveryTrafficPointSchema, InboxItemSchema, ReplayTraceSchema, AuditEntrySchema, } from '../../schemas/views.schema.js';
17
+ // ---------------------------------------------------------------------------
18
+ // View 2 — Delivery Traffic
19
+ // ---------------------------------------------------------------------------
20
+ /**
21
+ * Build a DeliveryTrafficPoint from the current verdict and an optional prior verdict.
22
+ * deltaFromPrev is null when there is no prior point.
23
+ */
24
+ export function diffConfidence(current, prior) {
25
+ const delta = prior !== null &&
26
+ current.confidenceScore !== null &&
27
+ prior.confidenceScore !== null
28
+ ? current.confidenceScore - prior.confidenceScore
29
+ : null;
30
+ return DeliveryTrafficPointSchema.parse({
31
+ subjectRef: current.subject.ref,
32
+ tenantId: current.subject.tenantId,
33
+ computedAt: current.computedAt,
34
+ confidenceScore: current.confidenceScore,
35
+ verdict: current.verdict,
36
+ deltaFromPrev: delta,
37
+ });
38
+ }
39
+ // ---------------------------------------------------------------------------
40
+ // View 3 — Inbox
41
+ // ---------------------------------------------------------------------------
42
+ /**
43
+ * Derive human-judgment inbox items from a verdict.
44
+ * Raises items for:
45
+ * - every blocking evidence item
46
+ * - every 'unknown' contribution on a requiredSource (when policy provides them)
47
+ * - 'block' verdict with a null score (nothing evaluable)
48
+ */
49
+ export function deriveInbox(rc, input) {
50
+ const items = [];
51
+ const now = rc.computedAt;
52
+ const requiredSources = input.policy?.requiredSources ?? [];
53
+ for (const evidence of input.evidence) {
54
+ if (evidence.blocking) {
55
+ items.push(InboxItemSchema.parse({
56
+ id: randomUUID(),
57
+ subjectRef: rc.subject.ref,
58
+ tenantId: rc.subject.tenantId,
59
+ kind: 'blocker',
60
+ source: evidence.source,
61
+ summary: evidence.reason
62
+ ? `${evidence.source}: ${evidence.reason}`
63
+ : `${evidence.source} is a hard blocker.`,
64
+ raisedAt: now,
65
+ }));
66
+ }
67
+ else if ((evidence.applicability ?? 'applicable') === 'unknown' &&
68
+ requiredSources.includes(evidence.source)) {
69
+ items.push(InboxItemSchema.parse({
70
+ id: randomUUID(),
71
+ subjectRef: rc.subject.ref,
72
+ tenantId: rc.subject.tenantId,
73
+ kind: 'unknown-signal',
74
+ source: evidence.source,
75
+ summary: evidence.reason
76
+ ? `${evidence.source}: ${evidence.reason}`
77
+ : `${evidence.source} could not produce a reliable score and is a required source.`,
78
+ raisedAt: now,
79
+ }));
80
+ }
81
+ }
82
+ // Raise an inbox item if verdict=block with null score (nothing evaluable).
83
+ if (rc.verdict === 'block' && rc.confidenceScore === null && input.evidence.every((e) => !e.blocking)) {
84
+ items.push(InboxItemSchema.parse({
85
+ id: randomUUID(),
86
+ subjectRef: rc.subject.ref,
87
+ tenantId: rc.subject.tenantId,
88
+ kind: 'approval-needed',
89
+ source: 'human-approval',
90
+ summary: 'No applicable evidence produced a score — manual review required before shipping.',
91
+ raisedAt: now,
92
+ }));
93
+ }
94
+ return items;
95
+ }
96
+ // ---------------------------------------------------------------------------
97
+ // View 4 — Replay
98
+ // ---------------------------------------------------------------------------
99
+ /**
100
+ * Build the provenance trace from the scorer input + result.
101
+ * Steps are ordered by their appearance in the input evidence array,
102
+ * with all provenance fields carried from EvidenceItem.collector.
103
+ */
104
+ export function buildReplay(input, rc) {
105
+ const steps = input.evidence.map((item, idx) => {
106
+ const contribution = rc.contributions[idx];
107
+ return {
108
+ source: item.source,
109
+ tool: item.collector.tool,
110
+ inputRef: item.collector.inputRef,
111
+ score: item.score,
112
+ weight: contribution?.weight ?? item.weight,
113
+ effectiveWeight: contribution?.effectiveWeight ?? 0,
114
+ durationMs: item.collector.durationMs,
115
+ cost: item.collector.cost,
116
+ };
117
+ });
118
+ return ReplayTraceSchema.parse({
119
+ subjectRef: rc.subject.ref,
120
+ computedAt: rc.computedAt,
121
+ steps,
122
+ formula: rc.scoreFormula,
123
+ finalVerdict: rc.verdict,
124
+ });
125
+ }
126
+ // ---------------------------------------------------------------------------
127
+ // View 5 — Audit Trail
128
+ // ---------------------------------------------------------------------------
129
+ /**
130
+ * Canonical audit record shape for hashing.
131
+ * Fields are sorted so the hash is deterministic regardless of insertion order.
132
+ */
133
+ function canonicalRecord(rc, evidenceSourceCount) {
134
+ return JSON.stringify({
135
+ blockers: [...rc.blockers].sort(),
136
+ computedAt: rc.computedAt,
137
+ confidenceScore: rc.confidenceScore,
138
+ evidenceSourceCount,
139
+ schemaVersion: 1,
140
+ subjectRef: rc.subject.ref,
141
+ tenantId: rc.subject.tenantId,
142
+ verdict: rc.verdict,
143
+ });
144
+ }
145
+ /**
146
+ * Serialize a verdict to a tamper-evident audit record.
147
+ * recordHash is SHA-256 over the canonical record — changes when any field changes.
148
+ */
149
+ export function toAuditEntry(rc, evidenceSourceCount) {
150
+ const canonical = canonicalRecord(rc, evidenceSourceCount);
151
+ const recordHash = createHash('sha256').update(canonical).digest('hex');
152
+ return AuditEntrySchema.parse({
153
+ tenantId: rc.subject.tenantId,
154
+ subjectRef: rc.subject.ref,
155
+ computedAt: rc.computedAt,
156
+ confidenceScore: rc.confidenceScore,
157
+ verdict: rc.verdict,
158
+ evidenceSourceCount,
159
+ blockers: rc.blockers,
160
+ schemaVersion: 1,
161
+ recordHash,
162
+ });
163
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Release Confidence Aggregator — pure scorer.
3
+ *
4
+ * P3 — qulib Confidence Layer v1.
5
+ *
6
+ * Pure function: no I/O, no side effects. All I/O (CLI, MCP) lives in the wiring layer.
7
+ * Algorithm mirrors computeAutomationMaturity's denominator-renormalization math, generalized
8
+ * to operate over a heterogeneous evidence bundle.
9
+ *
10
+ * Score formula:
11
+ * confidenceScore = round( Σ score_i * weight_i / Σ weight_i )
12
+ * where i ∈ { applicable items with score !== null }
13
+ *
14
+ * Excluded from denominator: not_applicable | unknown | score === null items.
15
+ * Each excluded item is reported in contributions + narrated in honestyNotes.
16
+ *
17
+ * Verdict ladder (mirrors agent-summary.ts deriveGate, lifted to fused score):
18
+ * any blocking item → block
19
+ * confidenceScore === null → block (nothing evaluable; honesty floor)
20
+ * confidenceScore < failThreshold → hold
21
+ * unknown on a requiredSource OR
22
+ * confidenceScore < passThreshold → caution
23
+ * else → ship
24
+ */
25
+ import type { ConfidenceInput, ReleaseConfidence } from '../../schemas/confidence.schema.js';
26
+ /**
27
+ * Compute the fused Release Confidence result from an evidence bundle.
28
+ *
29
+ * Pure function — deterministic over the same input.
30
+ */
31
+ export declare function computeReleaseConfidence(input: ConfidenceInput): ReleaseConfidence;
32
+ //# sourceMappingURL=confidence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confidence.d.ts","sourceRoot":"","sources":["../../../src/tools/scoring/confidence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EACV,eAAe,EAGf,iBAAiB,EAElB,MAAM,oCAAoC,CAAC;AAiE5C;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,eAAe,GAAG,iBAAiB,CA8HlF"}