@qulib/core 0.9.0 → 0.10.1
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 +11 -11
- package/dist/baseline/baseline.schema.d.ts +26 -26
- package/dist/baseline/baseline.schema.d.ts.map +1 -1
- package/dist/baseline/baseline.schema.js +1 -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.map +1 -1
- package/dist/cli/confidence-run.js +10 -6
- package/dist/cli/index.js +4 -0
- package/dist/cli/score-automation-run.d.ts.map +1 -1
- package/dist/cli/score-automation-run.js +5 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/phases/think.d.ts.map +1 -1
- package/dist/phases/think.js +4 -1
- package/dist/reporters/heatmap.d.ts +55 -0
- package/dist/reporters/heatmap.d.ts.map +1 -0
- package/dist/reporters/heatmap.js +148 -0
- package/dist/reporters/markdown-reporter.d.ts.map +1 -1
- package/dist/reporters/markdown-reporter.js +4 -1
- package/dist/schemas/confidence.schema.d.ts +2 -2
- package/dist/schemas/config.schema.d.ts.map +1 -1
- package/dist/schemas/config.schema.js +6 -1
- package/dist/schemas/gap-analysis.schema.d.ts +8 -8
- package/dist/schemas/gap-analysis.schema.js +1 -1
- package/dist/schemas/golden-manifest.schema.d.ts +137 -0
- package/dist/schemas/golden-manifest.schema.d.ts.map +1 -0
- package/dist/schemas/golden-manifest.schema.js +25 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/public-surface.schema.d.ts +15 -5
- package/dist/schemas/public-surface.schema.d.ts.map +1 -1
- package/dist/schemas/route-inventory.schema.d.ts +20 -0
- package/dist/schemas/route-inventory.schema.d.ts.map +1 -1
- package/dist/schemas/route-inventory.schema.js +4 -0
- package/dist/schemas/views.schema.d.ts +1 -1
- package/dist/tools/scoring/confidence.d.ts.map +1 -1
- package/dist/tools/scoring/confidence.js +140 -14
- package/dist/tools/scoring/prompt-leakage.d.ts +29 -0
- package/dist/tools/scoring/prompt-leakage.d.ts.map +1 -0
- package/dist/tools/scoring/prompt-leakage.js +256 -0
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -345,34 +345,34 @@ qulib analyze --url https://yourapp.com --auth-storage-state ./qulib-storage-sta
|
|
|
345
345
|
|
|
346
346
|
## Sample report (fixture baseline)
|
|
347
347
|
|
|
348
|
-
|
|
348
|
+
The fixture tests in `packages/core/src/__tests__/analyze.fixtures.test.ts` assert structural shape — that `releaseConfidence` is a number, `gaps` is an array, and coverage scores are non-negative. Exact scores vary with each scoring version; re-run the fixture suite for current reference values.
|
|
349
|
+
|
|
350
|
+
A minimal structural snapshot looks like:
|
|
349
351
|
|
|
350
352
|
```json
|
|
351
353
|
{
|
|
352
354
|
"status": "complete",
|
|
353
355
|
"releaseConfidence": 68,
|
|
354
356
|
"gaps": [
|
|
355
|
-
"...
|
|
357
|
+
"... gap items ..."
|
|
356
358
|
]
|
|
357
359
|
}
|
|
358
360
|
```
|
|
359
361
|
|
|
360
|
-
Use these as conservative reference numbers:
|
|
361
|
-
- public fixture (`/`): `releaseConfidence: 68/100`, `gaps: 4`
|
|
362
|
-
- auth-wall fixture (`/auth`): `releaseConfidence: 24/100`, `gaps: 2`
|
|
363
|
-
- broken fixture (`/broken`): `releaseConfidence: 0/100`, `gaps: 6`
|
|
364
|
-
|
|
365
362
|
## MCP tools quick map
|
|
366
363
|
|
|
367
364
|
| Tool | When to use | Key input |
|
|
368
365
|
|---|---|---|
|
|
369
366
|
| **`qulib_score_confidence`** | **Flagship.** Fused verdict (ship/caution/hold/block) from all collectors | `url` and/or `repoPath`, optional `includeViews.replay` |
|
|
370
|
-
| `
|
|
367
|
+
| `qulib_analyze_app` | Live-app QA scan: release confidence + gaps + a11y | `url`, optional `auth`, optional LLM knobs |
|
|
371
368
|
| `qulib_score_automation` | Score local repo test-automation maturity | absolute `repoPath`, optional `includeFullDimensions` |
|
|
372
369
|
| `qulib_score_api` | Discover API endpoints and score their test coverage | absolute `repoPath`, optional `enableTier3`, `includeEndpointDetail` |
|
|
373
|
-
| `qulib_scaffold_tests` | Generate Cypress
|
|
374
|
-
| `
|
|
375
|
-
| `
|
|
370
|
+
| `qulib_scaffold_tests` | Generate Cypress scaffold from a live URL (`cypress-e2e` only; playwright not yet implemented) | `url`, optional `framework`, `maxPagesToScan`, `recipes` |
|
|
371
|
+
| `qulib_explore_auth` | Deeper auth-path discovery on unfamiliar apps | `url`, optional `timeoutMs` |
|
|
372
|
+
| `qulib_detect_auth` | Fast single-pass auth pattern guess | `url`, optional `timeoutMs` |
|
|
373
|
+
| `analyze_app` | Legacy alias for `qulib_analyze_app` — kept for backwards compatibility | same as `qulib_analyze_app` |
|
|
374
|
+
| `explore_auth` | Legacy alias for `qulib_explore_auth` — kept for backwards compatibility | same as `qulib_explore_auth` |
|
|
375
|
+
| `detect_auth` | Legacy alias for `qulib_detect_auth` — kept for backwards compatibility | same as `qulib_detect_auth` |
|
|
376
376
|
|
|
377
377
|
## Output directories
|
|
378
378
|
|
|
@@ -7,18 +7,18 @@ import { z } from 'zod';
|
|
|
7
7
|
export declare const BaselineGapSchema: z.ZodObject<{
|
|
8
8
|
path: z.ZodString;
|
|
9
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"]>;
|
|
10
|
+
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint", "prompt-leakage"]>;
|
|
11
11
|
reason: z.ZodString;
|
|
12
12
|
}, "strip", z.ZodTypeAny, {
|
|
13
13
|
path: string;
|
|
14
14
|
severity: "critical" | "high" | "medium" | "low";
|
|
15
15
|
reason: string;
|
|
16
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
16
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
17
17
|
}, {
|
|
18
18
|
path: string;
|
|
19
19
|
severity: "critical" | "high" | "medium" | "low";
|
|
20
20
|
reason: string;
|
|
21
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
21
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
22
22
|
}>;
|
|
23
23
|
export type BaselineGap = z.infer<typeof BaselineGapSchema>;
|
|
24
24
|
/**
|
|
@@ -34,18 +34,18 @@ export declare const BaselineSnapshotSchema: z.ZodObject<{
|
|
|
34
34
|
gaps: z.ZodArray<z.ZodObject<{
|
|
35
35
|
path: z.ZodString;
|
|
36
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"]>;
|
|
37
|
+
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint", "prompt-leakage"]>;
|
|
38
38
|
reason: z.ZodString;
|
|
39
39
|
}, "strip", z.ZodTypeAny, {
|
|
40
40
|
path: string;
|
|
41
41
|
severity: "critical" | "high" | "medium" | "low";
|
|
42
42
|
reason: string;
|
|
43
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
43
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
44
44
|
}, {
|
|
45
45
|
path: string;
|
|
46
46
|
severity: "critical" | "high" | "medium" | "low";
|
|
47
47
|
reason: string;
|
|
48
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
48
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
49
49
|
}>, "many">;
|
|
50
50
|
label: z.ZodOptional<z.ZodString>;
|
|
51
51
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -56,7 +56,7 @@ export declare const BaselineSnapshotSchema: z.ZodObject<{
|
|
|
56
56
|
path: string;
|
|
57
57
|
severity: "critical" | "high" | "medium" | "low";
|
|
58
58
|
reason: string;
|
|
59
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
59
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
60
60
|
}[];
|
|
61
61
|
gapCount: number;
|
|
62
62
|
savedAt: string;
|
|
@@ -69,7 +69,7 @@ export declare const BaselineSnapshotSchema: z.ZodObject<{
|
|
|
69
69
|
path: string;
|
|
70
70
|
severity: "critical" | "high" | "medium" | "low";
|
|
71
71
|
reason: string;
|
|
72
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
72
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
73
73
|
}[];
|
|
74
74
|
gapCount: number;
|
|
75
75
|
savedAt: string;
|
|
@@ -81,7 +81,7 @@ export type BaselineSnapshot = z.infer<typeof BaselineSnapshotSchema>;
|
|
|
81
81
|
*/
|
|
82
82
|
export declare const BaselineDeltaItemSchema: z.ZodObject<{
|
|
83
83
|
path: z.ZodString;
|
|
84
|
-
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint"]>;
|
|
84
|
+
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint", "prompt-leakage"]>;
|
|
85
85
|
severity: z.ZodEnum<["critical", "high", "medium", "low"]>;
|
|
86
86
|
reason: z.ZodString;
|
|
87
87
|
status: z.ZodEnum<["new", "resolved", "severity-increased", "severity-decreased"]>;
|
|
@@ -90,13 +90,13 @@ export declare const BaselineDeltaItemSchema: z.ZodObject<{
|
|
|
90
90
|
path: string;
|
|
91
91
|
severity: "critical" | "high" | "medium" | "low";
|
|
92
92
|
reason: string;
|
|
93
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
93
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
94
94
|
}, {
|
|
95
95
|
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
96
96
|
path: string;
|
|
97
97
|
severity: "critical" | "high" | "medium" | "low";
|
|
98
98
|
reason: string;
|
|
99
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
99
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
100
100
|
}>;
|
|
101
101
|
export type BaselineDeltaItem = z.infer<typeof BaselineDeltaItemSchema>;
|
|
102
102
|
/**
|
|
@@ -112,7 +112,7 @@ export declare const BaselineDeltaSchema: z.ZodObject<{
|
|
|
112
112
|
confidenceDelta: z.ZodNumber;
|
|
113
113
|
newGaps: z.ZodArray<z.ZodObject<{
|
|
114
114
|
path: z.ZodString;
|
|
115
|
-
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint"]>;
|
|
115
|
+
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint", "prompt-leakage"]>;
|
|
116
116
|
severity: z.ZodEnum<["critical", "high", "medium", "low"]>;
|
|
117
117
|
reason: z.ZodString;
|
|
118
118
|
status: z.ZodEnum<["new", "resolved", "severity-increased", "severity-decreased"]>;
|
|
@@ -121,17 +121,17 @@ export declare const BaselineDeltaSchema: z.ZodObject<{
|
|
|
121
121
|
path: string;
|
|
122
122
|
severity: "critical" | "high" | "medium" | "low";
|
|
123
123
|
reason: string;
|
|
124
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
124
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
125
125
|
}, {
|
|
126
126
|
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
127
127
|
path: string;
|
|
128
128
|
severity: "critical" | "high" | "medium" | "low";
|
|
129
129
|
reason: string;
|
|
130
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
130
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
131
131
|
}>, "many">;
|
|
132
132
|
resolvedGaps: z.ZodArray<z.ZodObject<{
|
|
133
133
|
path: z.ZodString;
|
|
134
|
-
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint"]>;
|
|
134
|
+
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint", "prompt-leakage"]>;
|
|
135
135
|
severity: z.ZodEnum<["critical", "high", "medium", "low"]>;
|
|
136
136
|
reason: z.ZodString;
|
|
137
137
|
status: z.ZodEnum<["new", "resolved", "severity-increased", "severity-decreased"]>;
|
|
@@ -140,17 +140,17 @@ export declare const BaselineDeltaSchema: z.ZodObject<{
|
|
|
140
140
|
path: string;
|
|
141
141
|
severity: "critical" | "high" | "medium" | "low";
|
|
142
142
|
reason: string;
|
|
143
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
143
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
144
144
|
}, {
|
|
145
145
|
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
146
146
|
path: string;
|
|
147
147
|
severity: "critical" | "high" | "medium" | "low";
|
|
148
148
|
reason: string;
|
|
149
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
149
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
150
150
|
}>, "many">;
|
|
151
151
|
severityChanges: z.ZodArray<z.ZodObject<{
|
|
152
152
|
path: z.ZodString;
|
|
153
|
-
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint"]>;
|
|
153
|
+
category: z.ZodEnum<["untested-route", "a11y", "console-error", "broken-link", "auth-surface", "coverage", "untested-api-endpoint", "prompt-leakage"]>;
|
|
154
154
|
severity: z.ZodEnum<["critical", "high", "medium", "low"]>;
|
|
155
155
|
reason: z.ZodString;
|
|
156
156
|
status: z.ZodEnum<["new", "resolved", "severity-increased", "severity-decreased"]>;
|
|
@@ -159,13 +159,13 @@ export declare const BaselineDeltaSchema: z.ZodObject<{
|
|
|
159
159
|
path: string;
|
|
160
160
|
severity: "critical" | "high" | "medium" | "low";
|
|
161
161
|
reason: string;
|
|
162
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
162
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
163
163
|
}, {
|
|
164
164
|
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
165
165
|
path: string;
|
|
166
166
|
severity: "critical" | "high" | "medium" | "low";
|
|
167
167
|
reason: string;
|
|
168
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
168
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
169
169
|
}>, "many">;
|
|
170
170
|
summary: z.ZodString;
|
|
171
171
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -182,21 +182,21 @@ export declare const BaselineDeltaSchema: z.ZodObject<{
|
|
|
182
182
|
path: string;
|
|
183
183
|
severity: "critical" | "high" | "medium" | "low";
|
|
184
184
|
reason: string;
|
|
185
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
185
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
186
186
|
}[];
|
|
187
187
|
resolvedGaps: {
|
|
188
188
|
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
189
189
|
path: string;
|
|
190
190
|
severity: "critical" | "high" | "medium" | "low";
|
|
191
191
|
reason: string;
|
|
192
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
192
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
193
193
|
}[];
|
|
194
194
|
severityChanges: {
|
|
195
195
|
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
196
196
|
path: string;
|
|
197
197
|
severity: "critical" | "high" | "medium" | "low";
|
|
198
198
|
reason: string;
|
|
199
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
199
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
200
200
|
}[];
|
|
201
201
|
}, {
|
|
202
202
|
summary: string;
|
|
@@ -212,21 +212,21 @@ export declare const BaselineDeltaSchema: z.ZodObject<{
|
|
|
212
212
|
path: string;
|
|
213
213
|
severity: "critical" | "high" | "medium" | "low";
|
|
214
214
|
reason: string;
|
|
215
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
215
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
216
216
|
}[];
|
|
217
217
|
resolvedGaps: {
|
|
218
218
|
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
219
219
|
path: string;
|
|
220
220
|
severity: "critical" | "high" | "medium" | "low";
|
|
221
221
|
reason: string;
|
|
222
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
222
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
223
223
|
}[];
|
|
224
224
|
severityChanges: {
|
|
225
225
|
status: "new" | "resolved" | "severity-increased" | "severity-decreased";
|
|
226
226
|
path: string;
|
|
227
227
|
severity: "critical" | "high" | "medium" | "low";
|
|
228
228
|
reason: string;
|
|
229
|
-
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint";
|
|
229
|
+
category: "untested-route" | "a11y" | "console-error" | "broken-link" | "auth-surface" | "coverage" | "untested-api-endpoint" | "prompt-leakage";
|
|
230
230
|
}[];
|
|
231
231
|
}>;
|
|
232
232
|
export type BaselineDelta = z.infer<typeof BaselineDeltaSchema>;
|
|
@@ -1 +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;;;;;;;;;;;;;;;
|
|
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;;;;;;;;;;;;;;;EAc5B,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,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
|
+
}
|