@qulib/core 0.8.2 → 0.10.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.
- package/README.md +38 -13
- package/bin/qulib.js +2 -3
- package/dist/__tests__/playwright-available.d.ts +32 -0
- package/dist/__tests__/playwright-available.d.ts.map +1 -0
- package/dist/__tests__/playwright-available.js +35 -0
- package/dist/adapters/ci-results-adapter.d.ts +67 -0
- package/dist/adapters/ci-results-adapter.d.ts.map +1 -0
- package/dist/adapters/ci-results-adapter.js +143 -0
- package/dist/adapters/cypress-e2e-adapter.d.ts.map +1 -1
- package/dist/adapters/cypress-e2e-adapter.js +25 -2
- package/dist/adapters/playwright-adapter.d.ts.map +1 -1
- package/dist/adapters/playwright-adapter.js +25 -2
- package/dist/adapters/pr-metadata-adapter.d.ts +75 -0
- package/dist/adapters/pr-metadata-adapter.d.ts.map +1 -0
- package/dist/adapters/pr-metadata-adapter.js +146 -0
- package/dist/adapters/validate-specs.d.ts +55 -0
- package/dist/adapters/validate-specs.d.ts.map +1 -0
- package/dist/adapters/validate-specs.js +67 -0
- package/dist/baseline/baseline.d.ts +54 -0
- package/dist/baseline/baseline.d.ts.map +1 -0
- package/dist/baseline/baseline.js +252 -0
- package/dist/baseline/baseline.schema.d.ts +233 -0
- package/dist/baseline/baseline.schema.d.ts.map +1 -0
- package/dist/baseline/baseline.schema.js +59 -0
- package/dist/cli/analyze-diff-run.d.ts +77 -0
- package/dist/cli/analyze-diff-run.d.ts.map +1 -0
- package/dist/cli/analyze-diff-run.js +266 -0
- package/dist/cli/baseline-run.d.ts +55 -0
- package/dist/cli/baseline-run.d.ts.map +1 -0
- package/dist/cli/baseline-run.js +259 -0
- package/dist/cli/confidence-run.d.ts +16 -0
- package/dist/cli/confidence-run.d.ts.map +1 -0
- package/dist/cli/confidence-run.js +162 -0
- package/dist/cli/index.d.ts +11 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +84 -4
- package/dist/cli/scaffold-run.d.ts +86 -0
- package/dist/cli/scaffold-run.d.ts.map +1 -0
- package/dist/cli/scaffold-run.js +232 -0
- package/dist/cli/score-automation-run.d.ts +25 -0
- package/dist/cli/score-automation-run.d.ts.map +1 -0
- package/dist/cli/score-automation-run.js +127 -0
- package/dist/examples/notquality-dogfood/fixture.d.ts +166 -0
- package/dist/examples/notquality-dogfood/fixture.d.ts.map +1 -0
- package/dist/examples/notquality-dogfood/fixture.js +174 -0
- package/dist/examples/notquality-dogfood/run.d.ts +34 -0
- package/dist/examples/notquality-dogfood/run.d.ts.map +1 -0
- package/dist/examples/notquality-dogfood/run.js +139 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/recipes/a11y.d.ts +36 -0
- package/dist/recipes/a11y.d.ts.map +1 -0
- package/dist/recipes/a11y.js +118 -0
- package/dist/recipes/auth.d.ts +38 -0
- package/dist/recipes/auth.d.ts.map +1 -0
- package/dist/recipes/auth.js +156 -0
- package/dist/recipes/index.d.ts +26 -0
- package/dist/recipes/index.d.ts.map +1 -0
- package/dist/recipes/index.js +41 -0
- package/dist/recipes/nav.d.ts +34 -0
- package/dist/recipes/nav.d.ts.map +1 -0
- package/dist/recipes/nav.js +128 -0
- package/dist/recipes/seed.d.ts +34 -0
- package/dist/recipes/seed.d.ts.map +1 -0
- package/dist/recipes/seed.js +87 -0
- package/dist/reporters/heatmap.d.ts +55 -0
- package/dist/reporters/heatmap.d.ts.map +1 -0
- package/dist/reporters/heatmap.js +146 -0
- package/dist/reporters/markdown-reporter.d.ts.map +1 -1
- package/dist/reporters/markdown-reporter.js +4 -1
- package/dist/scaffold-tests.d.ts +21 -0
- package/dist/scaffold-tests.d.ts.map +1 -1
- package/dist/scaffold-tests.js +12 -2
- package/dist/schemas/confidence.schema.d.ts +526 -0
- package/dist/schemas/confidence.schema.d.ts.map +1 -0
- package/dist/schemas/confidence.schema.js +161 -0
- package/dist/schemas/config.schema.d.ts.map +1 -1
- package/dist/schemas/config.schema.js +6 -1
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +3 -0
- package/dist/schemas/recipe.schema.d.ts +66 -0
- package/dist/schemas/recipe.schema.d.ts.map +1 -0
- package/dist/schemas/recipe.schema.js +45 -0
- package/dist/schemas/views.schema.d.ts +234 -0
- package/dist/schemas/views.schema.d.ts.map +1 -0
- package/dist/schemas/views.schema.js +82 -0
- package/dist/tools/scoring/confidence-from-qulib.d.ts +34 -0
- package/dist/tools/scoring/confidence-from-qulib.d.ts.map +1 -0
- package/dist/tools/scoring/confidence-from-qulib.js +206 -0
- package/dist/tools/scoring/confidence-views.d.ts +40 -0
- package/dist/tools/scoring/confidence-views.d.ts.map +1 -0
- package/dist/tools/scoring/confidence-views.js +163 -0
- package/dist/tools/scoring/confidence.d.ts +32 -0
- package/dist/tools/scoring/confidence.d.ts.map +1 -0
- package/dist/tools/scoring/confidence.js +180 -0
- package/dist/tools/scoring/levels.d.ts +15 -0
- package/dist/tools/scoring/levels.d.ts.map +1 -0
- package/dist/tools/scoring/levels.js +21 -0
- package/package.json +18 -8
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* A snapshot of a single gap found during a scan, stored in a baseline.
|
|
4
|
+
* Intentionally lighter than the full GapSchema: only the fields needed to
|
|
5
|
+
* detect meaningful drift between scans are captured.
|
|
6
|
+
*/
|
|
7
|
+
export declare const BaselineGapSchema: z.ZodObject<{
|
|
8
|
+
path: z.ZodString;
|
|
9
|
+
severity: z.ZodEnum<["critical", "high", "medium", "low"]>;
|
|
10
|
+
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint"]>;
|
|
11
|
+
reason: z.ZodString;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
path: string;
|
|
14
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
15
|
+
reason: string;
|
|
16
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
17
|
+
}, {
|
|
18
|
+
path: string;
|
|
19
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
20
|
+
reason: string;
|
|
21
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
22
|
+
}>;
|
|
23
|
+
export type BaselineGap = z.infer<typeof BaselineGapSchema>;
|
|
24
|
+
/**
|
|
25
|
+
* A persisted baseline snapshot for a given URL, saved by `qulib baseline save`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const BaselineSnapshotSchema: z.ZodObject<{
|
|
28
|
+
/** Monotonic slug used as the on-disk filename stem: <url-slug>__<timestamp> */
|
|
29
|
+
id: z.ZodString;
|
|
30
|
+
url: z.ZodString;
|
|
31
|
+
savedAt: z.ZodString;
|
|
32
|
+
releaseConfidence: z.ZodNumber;
|
|
33
|
+
gapCount: z.ZodNumber;
|
|
34
|
+
gaps: z.ZodArray<z.ZodObject<{
|
|
35
|
+
path: z.ZodString;
|
|
36
|
+
severity: z.ZodEnum<["critical", "high", "medium", "low"]>;
|
|
37
|
+
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint"]>;
|
|
38
|
+
reason: z.ZodString;
|
|
39
|
+
}, "strip", z.ZodTypeAny, {
|
|
40
|
+
path: string;
|
|
41
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
42
|
+
reason: string;
|
|
43
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
44
|
+
}, {
|
|
45
|
+
path: string;
|
|
46
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
47
|
+
reason: string;
|
|
48
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
49
|
+
}>, "many">;
|
|
50
|
+
label: z.ZodOptional<z.ZodString>;
|
|
51
|
+
}, "strip", z.ZodTypeAny, {
|
|
52
|
+
id: string;
|
|
53
|
+
url: string;
|
|
54
|
+
releaseConfidence: number;
|
|
55
|
+
gaps: {
|
|
56
|
+
path: string;
|
|
57
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
58
|
+
reason: string;
|
|
59
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
60
|
+
}[];
|
|
61
|
+
gapCount: number;
|
|
62
|
+
savedAt: string;
|
|
63
|
+
label?: string | undefined;
|
|
64
|
+
}, {
|
|
65
|
+
id: string;
|
|
66
|
+
url: string;
|
|
67
|
+
releaseConfidence: number;
|
|
68
|
+
gaps: {
|
|
69
|
+
path: string;
|
|
70
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
71
|
+
reason: string;
|
|
72
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
73
|
+
}[];
|
|
74
|
+
gapCount: number;
|
|
75
|
+
savedAt: string;
|
|
76
|
+
label?: string | undefined;
|
|
77
|
+
}>;
|
|
78
|
+
export type BaselineSnapshot = z.infer<typeof BaselineSnapshotSchema>;
|
|
79
|
+
/**
|
|
80
|
+
* A single change in gap status between two baselines.
|
|
81
|
+
*/
|
|
82
|
+
export declare const BaselineDeltaItemSchema: z.ZodObject<{
|
|
83
|
+
path: z.ZodString;
|
|
84
|
+
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint"]>;
|
|
85
|
+
severity: z.ZodEnum<["critical", "high", "medium", "low"]>;
|
|
86
|
+
reason: z.ZodString;
|
|
87
|
+
status: z.ZodEnum<["new", "resolved", "severity-increased", "severity-decreased"]>;
|
|
88
|
+
}, "strip", z.ZodTypeAny, {
|
|
89
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
90
|
+
path: string;
|
|
91
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
92
|
+
reason: string;
|
|
93
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
94
|
+
}, {
|
|
95
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
96
|
+
path: string;
|
|
97
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
98
|
+
reason: string;
|
|
99
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
100
|
+
}>;
|
|
101
|
+
export type BaselineDeltaItem = z.infer<typeof BaselineDeltaItemSchema>;
|
|
102
|
+
/**
|
|
103
|
+
* The result of comparing two snapshots.
|
|
104
|
+
*/
|
|
105
|
+
export declare const BaselineDeltaSchema: z.ZodObject<{
|
|
106
|
+
fromId: z.ZodString;
|
|
107
|
+
toId: z.ZodString;
|
|
108
|
+
fromSavedAt: z.ZodString;
|
|
109
|
+
toSavedAt: z.ZodString;
|
|
110
|
+
fromReleaseConfidence: z.ZodNumber;
|
|
111
|
+
toReleaseConfidence: z.ZodNumber;
|
|
112
|
+
confidenceDelta: z.ZodNumber;
|
|
113
|
+
newGaps: z.ZodArray<z.ZodObject<{
|
|
114
|
+
path: z.ZodString;
|
|
115
|
+
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint"]>;
|
|
116
|
+
severity: z.ZodEnum<["critical", "high", "medium", "low"]>;
|
|
117
|
+
reason: z.ZodString;
|
|
118
|
+
status: z.ZodEnum<["new", "resolved", "severity-increased", "severity-decreased"]>;
|
|
119
|
+
}, "strip", z.ZodTypeAny, {
|
|
120
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
121
|
+
path: string;
|
|
122
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
123
|
+
reason: string;
|
|
124
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
125
|
+
}, {
|
|
126
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
127
|
+
path: string;
|
|
128
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
129
|
+
reason: string;
|
|
130
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
131
|
+
}>, "many">;
|
|
132
|
+
resolvedGaps: z.ZodArray<z.ZodObject<{
|
|
133
|
+
path: z.ZodString;
|
|
134
|
+
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint"]>;
|
|
135
|
+
severity: z.ZodEnum<["critical", "high", "medium", "low"]>;
|
|
136
|
+
reason: z.ZodString;
|
|
137
|
+
status: z.ZodEnum<["new", "resolved", "severity-increased", "severity-decreased"]>;
|
|
138
|
+
}, "strip", z.ZodTypeAny, {
|
|
139
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
140
|
+
path: string;
|
|
141
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
142
|
+
reason: string;
|
|
143
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
144
|
+
}, {
|
|
145
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
146
|
+
path: string;
|
|
147
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
148
|
+
reason: string;
|
|
149
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
150
|
+
}>, "many">;
|
|
151
|
+
severityChanges: z.ZodArray<z.ZodObject<{
|
|
152
|
+
path: z.ZodString;
|
|
153
|
+
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint"]>;
|
|
154
|
+
severity: z.ZodEnum<["critical", "high", "medium", "low"]>;
|
|
155
|
+
reason: z.ZodString;
|
|
156
|
+
status: z.ZodEnum<["new", "resolved", "severity-increased", "severity-decreased"]>;
|
|
157
|
+
}, "strip", z.ZodTypeAny, {
|
|
158
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
159
|
+
path: string;
|
|
160
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
161
|
+
reason: string;
|
|
162
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
163
|
+
}, {
|
|
164
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
165
|
+
path: string;
|
|
166
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
167
|
+
reason: string;
|
|
168
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
169
|
+
}>, "many">;
|
|
170
|
+
summary: z.ZodString;
|
|
171
|
+
}, "strip", z.ZodTypeAny, {
|
|
172
|
+
summary: string;
|
|
173
|
+
fromId: string;
|
|
174
|
+
toId: string;
|
|
175
|
+
fromSavedAt: string;
|
|
176
|
+
toSavedAt: string;
|
|
177
|
+
fromReleaseConfidence: number;
|
|
178
|
+
toReleaseConfidence: number;
|
|
179
|
+
confidenceDelta: number;
|
|
180
|
+
newGaps: {
|
|
181
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
182
|
+
path: string;
|
|
183
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
184
|
+
reason: string;
|
|
185
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
186
|
+
}[];
|
|
187
|
+
resolvedGaps: {
|
|
188
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
189
|
+
path: string;
|
|
190
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
191
|
+
reason: string;
|
|
192
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
193
|
+
}[];
|
|
194
|
+
severityChanges: {
|
|
195
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
196
|
+
path: string;
|
|
197
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
198
|
+
reason: string;
|
|
199
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
200
|
+
}[];
|
|
201
|
+
}, {
|
|
202
|
+
summary: string;
|
|
203
|
+
fromId: string;
|
|
204
|
+
toId: string;
|
|
205
|
+
fromSavedAt: string;
|
|
206
|
+
toSavedAt: string;
|
|
207
|
+
fromReleaseConfidence: number;
|
|
208
|
+
toReleaseConfidence: number;
|
|
209
|
+
confidenceDelta: number;
|
|
210
|
+
newGaps: {
|
|
211
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
212
|
+
path: string;
|
|
213
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
214
|
+
reason: string;
|
|
215
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
216
|
+
}[];
|
|
217
|
+
resolvedGaps: {
|
|
218
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
219
|
+
path: string;
|
|
220
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
221
|
+
reason: string;
|
|
222
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
223
|
+
}[];
|
|
224
|
+
severityChanges: {
|
|
225
|
+
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
226
|
+
path: string;
|
|
227
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
228
|
+
reason: string;
|
|
229
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
230
|
+
}[];
|
|
231
|
+
}>;
|
|
232
|
+
export type BaselineDelta = z.infer<typeof BaselineDeltaSchema>;
|
|
233
|
+
//# sourceMappingURL=baseline.schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.schema.d.ts","sourceRoot":"","sources":["../../src/baseline/baseline.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;EAa5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,sBAAsB;IACjC,gFAAgF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQhF,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;GAEG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;EAMlC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAY9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* A snapshot of a single gap found during a scan, stored in a baseline.
|
|
4
|
+
* Intentionally lighter than the full GapSchema: only the fields needed to
|
|
5
|
+
* detect meaningful drift between scans are captured.
|
|
6
|
+
*/
|
|
7
|
+
export const BaselineGapSchema = z.object({
|
|
8
|
+
path: z.string(),
|
|
9
|
+
severity: z.enum(['critical', 'high', 'medium', 'low']),
|
|
10
|
+
category: z.enum([
|
|
11
|
+
'untested-route',
|
|
12
|
+
'a11y',
|
|
13
|
+
'console-error',
|
|
14
|
+
'broken-link',
|
|
15
|
+
'auth-surface',
|
|
16
|
+
'coverage',
|
|
17
|
+
'untested-api-endpoint',
|
|
18
|
+
]),
|
|
19
|
+
reason: z.string(),
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* A persisted baseline snapshot for a given URL, saved by `qulib baseline save`.
|
|
23
|
+
*/
|
|
24
|
+
export const BaselineSnapshotSchema = z.object({
|
|
25
|
+
/** Monotonic slug used as the on-disk filename stem: <url-slug>__<timestamp> */
|
|
26
|
+
id: z.string(),
|
|
27
|
+
url: z.string(),
|
|
28
|
+
savedAt: z.string().datetime(),
|
|
29
|
+
releaseConfidence: z.number().min(0).max(100),
|
|
30
|
+
gapCount: z.number().int().min(0),
|
|
31
|
+
gaps: z.array(BaselineGapSchema),
|
|
32
|
+
label: z.string().optional(),
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* A single change in gap status between two baselines.
|
|
36
|
+
*/
|
|
37
|
+
export const BaselineDeltaItemSchema = z.object({
|
|
38
|
+
path: z.string(),
|
|
39
|
+
category: BaselineGapSchema.shape.category,
|
|
40
|
+
severity: BaselineGapSchema.shape.severity,
|
|
41
|
+
reason: z.string(),
|
|
42
|
+
status: z.enum(['new', 'resolved', 'severity-increased', 'severity-decreased']),
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* The result of comparing two snapshots.
|
|
46
|
+
*/
|
|
47
|
+
export const BaselineDeltaSchema = z.object({
|
|
48
|
+
fromId: z.string(),
|
|
49
|
+
toId: z.string(),
|
|
50
|
+
fromSavedAt: z.string().datetime(),
|
|
51
|
+
toSavedAt: z.string().datetime(),
|
|
52
|
+
fromReleaseConfidence: z.number().min(0).max(100),
|
|
53
|
+
toReleaseConfidence: z.number().min(0).max(100),
|
|
54
|
+
confidenceDelta: z.number(),
|
|
55
|
+
newGaps: z.array(BaselineDeltaItemSchema),
|
|
56
|
+
resolvedGaps: z.array(BaselineDeltaItemSchema),
|
|
57
|
+
severityChanges: z.array(BaselineDeltaItemSchema),
|
|
58
|
+
summary: z.string(),
|
|
59
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { GapAnalysis } from '../schemas/gap-analysis.schema.js';
|
|
3
|
+
import type { BaselineDeltaItem } from '../baseline/baseline.schema.js';
|
|
4
|
+
/**
|
|
5
|
+
* The structured result of diffing two analyze_app outputs.
|
|
6
|
+
* Wraps `BaselineDelta` with source provenance (file labels, timestamps).
|
|
7
|
+
*/
|
|
8
|
+
export interface AnalyzeDiffResult {
|
|
9
|
+
/** Human label for the "before" report (path by default). */
|
|
10
|
+
fromLabel: string;
|
|
11
|
+
/** Human label for the "after" report (path by default). */
|
|
12
|
+
toLabel: string;
|
|
13
|
+
/** ISO timestamp from the "before" report's analyzedAt field. */
|
|
14
|
+
fromAnalyzedAt: string;
|
|
15
|
+
/** ISO timestamp from the "after" report's analyzedAt field. */
|
|
16
|
+
toAnalyzedAt: string;
|
|
17
|
+
/** Release confidence from the "before" report (0–100, or null). */
|
|
18
|
+
fromReleaseConfidence: number | null;
|
|
19
|
+
/** Release confidence from the "after" report (0–100, or null). */
|
|
20
|
+
toReleaseConfidence: number | null;
|
|
21
|
+
/** Numeric delta: toReleaseConfidence − fromReleaseConfidence. Null if either is null. */
|
|
22
|
+
confidenceDelta: number | null;
|
|
23
|
+
/** Direction of the confidence delta. */
|
|
24
|
+
direction: 'improved' | 'regressed' | 'unchanged' | 'unknown';
|
|
25
|
+
/** Findings present in "to" that were absent in "from" (new regressions). */
|
|
26
|
+
added: BaselineDeltaItem[];
|
|
27
|
+
/** Findings present in "from" that are absent in "to" (resolved issues). */
|
|
28
|
+
removed: BaselineDeltaItem[];
|
|
29
|
+
/** Same finding (path + category) with a changed severity between the two reports. */
|
|
30
|
+
changed: BaselineDeltaItem[];
|
|
31
|
+
/** One-line human summary. */
|
|
32
|
+
summary: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Pure function: diff two GapAnalysis objects.
|
|
36
|
+
*
|
|
37
|
+
* Does NOT read files, make network requests, or touch disk. Both inputs must
|
|
38
|
+
* already be validated GapAnalysis objects.
|
|
39
|
+
*
|
|
40
|
+
* @param from The "before" (baseline) analysis.
|
|
41
|
+
* @param to The "after" (current) analysis.
|
|
42
|
+
* @param opts Optional labels for provenance metadata.
|
|
43
|
+
*/
|
|
44
|
+
export declare function analyzeRunDiff(from: GapAnalysis, to: GapAnalysis, opts?: {
|
|
45
|
+
fromLabel?: string;
|
|
46
|
+
toLabel?: string;
|
|
47
|
+
}): AnalyzeDiffResult;
|
|
48
|
+
/**
|
|
49
|
+
* Read and validate a GapAnalysis from a report.json file path.
|
|
50
|
+
* Fails loudly on a missing/malformed/foreign file rather than diffing garbage.
|
|
51
|
+
*/
|
|
52
|
+
export declare function loadGapAnalysisFile(filePath: string, cwd?: string): Promise<GapAnalysis>;
|
|
53
|
+
export interface AnalyzeDiffOptions {
|
|
54
|
+
from: string;
|
|
55
|
+
to: string;
|
|
56
|
+
labelFrom?: string;
|
|
57
|
+
labelTo?: string;
|
|
58
|
+
json?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Core of `analyze diff`, factored out for direct testing.
|
|
62
|
+
* Loads both files, validates them, diffs them, and emits the result.
|
|
63
|
+
*/
|
|
64
|
+
export declare function runAnalyzeDiff(options: AnalyzeDiffOptions, out?: (line: string) => void): Promise<AnalyzeDiffResult>;
|
|
65
|
+
/**
|
|
66
|
+
* Render an AnalyzeDiffResult as a human-readable Markdown report.
|
|
67
|
+
*
|
|
68
|
+
* The report is structured for readability in CI logs, GitHub PR comments, and
|
|
69
|
+
* terminal output. It covers:
|
|
70
|
+
* - Header with report labels and timestamps
|
|
71
|
+
* - Confidence score delta with direction indicator
|
|
72
|
+
* - Added / Removed / Changed findings as tables
|
|
73
|
+
* - One-line summary
|
|
74
|
+
*/
|
|
75
|
+
export declare function formatAnalyzeDiffMarkdown(result: AnalyzeDiffResult): string;
|
|
76
|
+
export declare function registerAnalyzeDiffCommand(program: Command): void;
|
|
77
|
+
//# sourceMappingURL=analyze-diff-run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze-diff-run.d.ts","sourceRoot":"","sources":["../../src/cli/analyze-diff-run.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAGrE,OAAO,KAAK,EAAiB,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAOvF;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAC;IACvB,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,mEAAmE;IACnE,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,0FAA0F;IAC1F,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,yCAAyC;IACzC,SAAS,EAAE,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,SAAS,CAAC;IAC9D,6EAA6E;IAC7E,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,4EAA4E;IAC5E,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,sFAAsF;IACtF,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;CACjB;AAqCD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,WAAW,EACjB,EAAE,EAAE,WAAW,EACf,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GAClD,iBAAiB,CA6CnB;AAMD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAsB,GAC1B,OAAO,CAAC,WAAW,CAAC,CAsBtB;AAMD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,EAC3B,GAAG,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAkC,GACxD,OAAO,CAAC,iBAAiB,CAAC,CAgB5B;AA8BD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAgD3E;AAMD,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAiCjE"}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `qulib analyze diff` — structured diff between two analyze_app outputs.
|
|
3
|
+
*
|
|
4
|
+
* Produces a structured report (JSON + Markdown) that compares two GapAnalysis
|
|
5
|
+
* objects (the serialized output of `qulib analyze`): added findings, removed
|
|
6
|
+
* findings, severity changes, and a confidence score delta.
|
|
7
|
+
*
|
|
8
|
+
* The diff is a PURE function of two GapAnalysis objects — no disk state, no
|
|
9
|
+
* network, no LLM. Callers supply paths to report.json files; this module reads
|
|
10
|
+
* them, validates them, and produces the diff.
|
|
11
|
+
*
|
|
12
|
+
* Subcommand:
|
|
13
|
+
* qulib analyze diff --from <path> --to <path>
|
|
14
|
+
*
|
|
15
|
+
* Flags:
|
|
16
|
+
* --from <path> Path to the baseline report.json (the "before" state).
|
|
17
|
+
* --to <path> Path to the current report.json (the "after" state).
|
|
18
|
+
* --json Emit the AnalyzeDiffResult as JSON to stdout (default: Markdown).
|
|
19
|
+
* --label-from Optional human label for the baseline report.
|
|
20
|
+
* --label-to Optional human label for the current report.
|
|
21
|
+
*
|
|
22
|
+
* Design rationale:
|
|
23
|
+
* - Reuses the existing `BaselineDelta` shape and `compareBaselines` logic by
|
|
24
|
+
* converting GapAnalysis objects to transient BaselineSnapshot objects.
|
|
25
|
+
* No second format is introduced; the schema is the same.
|
|
26
|
+
* - The result type `AnalyzeDiffResult` wraps `BaselineDelta` with the source
|
|
27
|
+
* report metadata (analyzedAt, path labels) for full provenance.
|
|
28
|
+
* - `analyzeRunDiff` is factored out as a pure function so it is testable and
|
|
29
|
+
* importable without the CLI layer (follows the baseline-run.ts convention).
|
|
30
|
+
*
|
|
31
|
+
* Registered from cli/index.ts via `registerAnalyzeDiffCommand(program)` so this
|
|
32
|
+
* command never edits index.ts beyond a single additive registration line.
|
|
33
|
+
*/
|
|
34
|
+
import { readFile } from 'node:fs/promises';
|
|
35
|
+
import { resolve } from 'node:path';
|
|
36
|
+
import { GapAnalysisSchema } from '../schemas/gap-analysis.schema.js';
|
|
37
|
+
import { compareBaselines } from '../baseline/baseline.js';
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Pure diff function
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
/**
|
|
42
|
+
* Stable key used to match gaps between two reports.
|
|
43
|
+
* Same key as compareBaselines: path + category identifies the same problem.
|
|
44
|
+
*/
|
|
45
|
+
function gapKey(path, category) {
|
|
46
|
+
return `${path}|||${category}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Convert a GapAnalysis to a minimal BaselineSnapshot shape for reuse with
|
|
50
|
+
* compareBaselines. The `id` and `savedAt` fields are synthetic — we use
|
|
51
|
+
* `analyzedAt` for temporal ordering. `url` is left as an empty string since
|
|
52
|
+
* this path does not require URL-keyed baseline storage.
|
|
53
|
+
*/
|
|
54
|
+
function toTransientSnapshot(analysis, id) {
|
|
55
|
+
const confidence = analysis.releaseConfidence ?? 0;
|
|
56
|
+
return {
|
|
57
|
+
id,
|
|
58
|
+
url: '',
|
|
59
|
+
savedAt: analysis.analyzedAt,
|
|
60
|
+
releaseConfidence: confidence,
|
|
61
|
+
gapCount: analysis.gaps.length,
|
|
62
|
+
gaps: analysis.gaps.map((g) => ({
|
|
63
|
+
path: g.path,
|
|
64
|
+
severity: g.severity,
|
|
65
|
+
category: g.category,
|
|
66
|
+
reason: g.reason,
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Pure function: diff two GapAnalysis objects.
|
|
72
|
+
*
|
|
73
|
+
* Does NOT read files, make network requests, or touch disk. Both inputs must
|
|
74
|
+
* already be validated GapAnalysis objects.
|
|
75
|
+
*
|
|
76
|
+
* @param from The "before" (baseline) analysis.
|
|
77
|
+
* @param to The "after" (current) analysis.
|
|
78
|
+
* @param opts Optional labels for provenance metadata.
|
|
79
|
+
*/
|
|
80
|
+
export function analyzeRunDiff(from, to, opts = {}) {
|
|
81
|
+
const fromLabel = opts.fromLabel ?? 'from';
|
|
82
|
+
const toLabel = opts.toLabel ?? 'to';
|
|
83
|
+
const priorSnap = toTransientSnapshot(from, 'from');
|
|
84
|
+
const currentSnap = toTransientSnapshot(to, 'to');
|
|
85
|
+
const delta = compareBaselines(priorSnap, currentSnap);
|
|
86
|
+
const fromConf = from.releaseConfidence;
|
|
87
|
+
const toConf = to.releaseConfidence;
|
|
88
|
+
const confidenceDelta = fromConf !== null && toConf !== null ? toConf - fromConf : null;
|
|
89
|
+
let direction = 'unknown';
|
|
90
|
+
if (confidenceDelta !== null) {
|
|
91
|
+
direction = confidenceDelta > 0 ? 'improved' : confidenceDelta < 0 ? 'regressed' : 'unchanged';
|
|
92
|
+
}
|
|
93
|
+
// Build a richer summary that covers the null-confidence case.
|
|
94
|
+
const confLine = fromConf !== null && toConf !== null
|
|
95
|
+
? `Confidence ${direction} (${fromConf} → ${toConf})`
|
|
96
|
+
: 'Confidence unavailable in one or both reports';
|
|
97
|
+
const summaryParts = [
|
|
98
|
+
confLine,
|
|
99
|
+
delta.newGaps.length > 0 ? `${delta.newGaps.length} added finding(s)` : '',
|
|
100
|
+
delta.resolvedGaps.length > 0 ? `${delta.resolvedGaps.length} removed finding(s)` : '',
|
|
101
|
+
delta.severityChanges.length > 0 ? `${delta.severityChanges.length} severity change(s)` : '',
|
|
102
|
+
].filter(Boolean);
|
|
103
|
+
return {
|
|
104
|
+
fromLabel,
|
|
105
|
+
toLabel,
|
|
106
|
+
fromAnalyzedAt: from.analyzedAt,
|
|
107
|
+
toAnalyzedAt: to.analyzedAt,
|
|
108
|
+
fromReleaseConfidence: fromConf,
|
|
109
|
+
toReleaseConfidence: toConf,
|
|
110
|
+
confidenceDelta,
|
|
111
|
+
direction,
|
|
112
|
+
added: delta.newGaps,
|
|
113
|
+
removed: delta.resolvedGaps,
|
|
114
|
+
changed: delta.severityChanges,
|
|
115
|
+
summary: summaryParts.join(', '),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// File loader
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
/**
|
|
122
|
+
* Read and validate a GapAnalysis from a report.json file path.
|
|
123
|
+
* Fails loudly on a missing/malformed/foreign file rather than diffing garbage.
|
|
124
|
+
*/
|
|
125
|
+
export async function loadGapAnalysisFile(filePath, cwd = process.cwd()) {
|
|
126
|
+
const abs = resolve(cwd, filePath);
|
|
127
|
+
let raw;
|
|
128
|
+
try {
|
|
129
|
+
raw = await readFile(abs, 'utf8');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw new Error(`analyze diff: could not read file: ${abs}`);
|
|
133
|
+
}
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(raw);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw new Error(`analyze diff: file is not valid JSON: ${abs}`);
|
|
140
|
+
}
|
|
141
|
+
const result = GapAnalysisSchema.safeParse(parsed);
|
|
142
|
+
if (!result.success) {
|
|
143
|
+
throw new Error(`analyze diff: file is not a valid qulib report.json (GapAnalysis): ${abs}\n` +
|
|
144
|
+
result.error.issues.map((i) => ` • ${i.path.join('.')}: ${i.message}`).join('\n'));
|
|
145
|
+
}
|
|
146
|
+
return result.data;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Core of `analyze diff`, factored out for direct testing.
|
|
150
|
+
* Loads both files, validates them, diffs them, and emits the result.
|
|
151
|
+
*/
|
|
152
|
+
export async function runAnalyzeDiff(options, out = (line) => console.log(line)) {
|
|
153
|
+
const fromAnalysis = await loadGapAnalysisFile(options.from);
|
|
154
|
+
const toAnalysis = await loadGapAnalysisFile(options.to);
|
|
155
|
+
const result = analyzeRunDiff(fromAnalysis, toAnalysis, {
|
|
156
|
+
fromLabel: options.labelFrom ?? options.from,
|
|
157
|
+
toLabel: options.labelTo ?? options.to,
|
|
158
|
+
});
|
|
159
|
+
if (options.json) {
|
|
160
|
+
out(JSON.stringify(result, null, 2));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
out(formatAnalyzeDiffMarkdown(result));
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Markdown renderer
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
const SEVERITY_EMOJI = {
|
|
171
|
+
critical: '🔴',
|
|
172
|
+
high: '🟠',
|
|
173
|
+
medium: '🟡',
|
|
174
|
+
low: '🔵',
|
|
175
|
+
};
|
|
176
|
+
function severityTag(severity) {
|
|
177
|
+
return `${SEVERITY_EMOJI[severity] ?? ''} **${severity}**`.trim();
|
|
178
|
+
}
|
|
179
|
+
function renderDeltaTable(items) {
|
|
180
|
+
if (items.length === 0)
|
|
181
|
+
return '_none_';
|
|
182
|
+
const rows = items.map((i) => `| ${i.path} | ${i.category} | ${severityTag(i.severity)} | ${i.reason} |`);
|
|
183
|
+
return [
|
|
184
|
+
'| Path | Category | Severity | Reason |',
|
|
185
|
+
'|------|----------|----------|--------|',
|
|
186
|
+
...rows,
|
|
187
|
+
].join('\n');
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Render an AnalyzeDiffResult as a human-readable Markdown report.
|
|
191
|
+
*
|
|
192
|
+
* The report is structured for readability in CI logs, GitHub PR comments, and
|
|
193
|
+
* terminal output. It covers:
|
|
194
|
+
* - Header with report labels and timestamps
|
|
195
|
+
* - Confidence score delta with direction indicator
|
|
196
|
+
* - Added / Removed / Changed findings as tables
|
|
197
|
+
* - One-line summary
|
|
198
|
+
*/
|
|
199
|
+
export function formatAnalyzeDiffMarkdown(result) {
|
|
200
|
+
const lines = [];
|
|
201
|
+
lines.push('## qulib analyze diff');
|
|
202
|
+
lines.push('');
|
|
203
|
+
lines.push(`| | Report |`);
|
|
204
|
+
lines.push(`|---|---|`);
|
|
205
|
+
lines.push(`| **From** | ${result.fromLabel} (${result.fromAnalyzedAt}) |`);
|
|
206
|
+
lines.push(`| **To** | ${result.toLabel} (${result.toAnalyzedAt}) |`);
|
|
207
|
+
lines.push('');
|
|
208
|
+
// Confidence delta
|
|
209
|
+
lines.push('### Release Confidence');
|
|
210
|
+
if (result.fromReleaseConfidence !== null && result.toReleaseConfidence !== null) {
|
|
211
|
+
const delta = result.confidenceDelta;
|
|
212
|
+
const arrow = delta > 0 ? '↑' : delta < 0 ? '↓' : '→';
|
|
213
|
+
const sign = delta > 0 ? '+' : '';
|
|
214
|
+
lines.push(`${result.fromReleaseConfidence}/100 ${arrow} ${result.toReleaseConfidence}/100 ` +
|
|
215
|
+
`(${sign}${delta}) — **${result.direction}**`);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
lines.push('_Confidence unavailable in one or both reports._');
|
|
219
|
+
}
|
|
220
|
+
lines.push('');
|
|
221
|
+
// Added findings
|
|
222
|
+
lines.push(`### Added Findings (${result.added.length})`);
|
|
223
|
+
lines.push('');
|
|
224
|
+
lines.push(renderDeltaTable(result.added));
|
|
225
|
+
lines.push('');
|
|
226
|
+
// Removed findings
|
|
227
|
+
lines.push(`### Removed Findings (${result.removed.length})`);
|
|
228
|
+
lines.push('');
|
|
229
|
+
lines.push(renderDeltaTable(result.removed));
|
|
230
|
+
lines.push('');
|
|
231
|
+
// Changed severity
|
|
232
|
+
lines.push(`### Severity Changes (${result.changed.length})`);
|
|
233
|
+
lines.push('');
|
|
234
|
+
lines.push(renderDeltaTable(result.changed));
|
|
235
|
+
lines.push('');
|
|
236
|
+
lines.push(`---`);
|
|
237
|
+
lines.push(`_${result.summary}_`);
|
|
238
|
+
return lines.join('\n');
|
|
239
|
+
}
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// CLI registration
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
export function registerAnalyzeDiffCommand(program) {
|
|
244
|
+
// Nest `diff` under the existing (or new) `analyze` group.
|
|
245
|
+
// Commander allows a subcommand under a top-level command; the analyze command
|
|
246
|
+
// already exists in index.ts as a `program.command('analyze')` — we add a peer
|
|
247
|
+
// group `analyze-diff` to avoid colliding with the top-level `analyze` action.
|
|
248
|
+
// The user-facing name is `qulib analyze-diff` to keep wiring simple.
|
|
249
|
+
program
|
|
250
|
+
.command('analyze-diff')
|
|
251
|
+
.description('Diff two analyze_app report.json outputs — surface added / removed / changed findings and confidence delta')
|
|
252
|
+
.requiredOption('--from <path>', 'Path to the baseline report.json ("before")')
|
|
253
|
+
.requiredOption('--to <path>', 'Path to the current report.json ("after")')
|
|
254
|
+
.option('--label-from <label>', 'Human label for the baseline report (default: the file path)')
|
|
255
|
+
.option('--label-to <label>', 'Human label for the current report (default: the file path)')
|
|
256
|
+
.option('--json', 'Emit the AnalyzeDiffResult as JSON to stdout (default: Markdown)', false)
|
|
257
|
+
.action(async (options) => {
|
|
258
|
+
await runAnalyzeDiff({
|
|
259
|
+
from: options.from,
|
|
260
|
+
to: options.to,
|
|
261
|
+
labelFrom: options.labelFrom,
|
|
262
|
+
labelTo: options.labelTo,
|
|
263
|
+
json: Boolean(options.json),
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
}
|