@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.
Files changed (48) hide show
  1. package/README.md +11 -11
  2. package/dist/baseline/baseline.schema.d.ts +26 -26
  3. package/dist/baseline/baseline.schema.d.ts.map +1 -1
  4. package/dist/baseline/baseline.schema.js +1 -0
  5. package/dist/cli/analyze-diff-run.d.ts +77 -0
  6. package/dist/cli/analyze-diff-run.d.ts.map +1 -0
  7. package/dist/cli/analyze-diff-run.js +266 -0
  8. package/dist/cli/baseline-run.d.ts +55 -0
  9. package/dist/cli/baseline-run.d.ts.map +1 -0
  10. package/dist/cli/baseline-run.js +259 -0
  11. package/dist/cli/confidence-run.d.ts.map +1 -1
  12. package/dist/cli/confidence-run.js +10 -6
  13. package/dist/cli/index.js +4 -0
  14. package/dist/cli/score-automation-run.d.ts.map +1 -1
  15. package/dist/cli/score-automation-run.js +5 -1
  16. package/dist/index.d.ts +5 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +5 -0
  19. package/dist/phases/think.d.ts.map +1 -1
  20. package/dist/phases/think.js +4 -1
  21. package/dist/reporters/heatmap.d.ts +55 -0
  22. package/dist/reporters/heatmap.d.ts.map +1 -0
  23. package/dist/reporters/heatmap.js +148 -0
  24. package/dist/reporters/markdown-reporter.d.ts.map +1 -1
  25. package/dist/reporters/markdown-reporter.js +4 -1
  26. package/dist/schemas/confidence.schema.d.ts +2 -2
  27. package/dist/schemas/config.schema.d.ts.map +1 -1
  28. package/dist/schemas/config.schema.js +6 -1
  29. package/dist/schemas/gap-analysis.schema.d.ts +8 -8
  30. package/dist/schemas/gap-analysis.schema.js +1 -1
  31. package/dist/schemas/golden-manifest.schema.d.ts +137 -0
  32. package/dist/schemas/golden-manifest.schema.d.ts.map +1 -0
  33. package/dist/schemas/golden-manifest.schema.js +25 -0
  34. package/dist/schemas/index.d.ts +1 -0
  35. package/dist/schemas/index.d.ts.map +1 -1
  36. package/dist/schemas/index.js +1 -0
  37. package/dist/schemas/public-surface.schema.d.ts +15 -5
  38. package/dist/schemas/public-surface.schema.d.ts.map +1 -1
  39. package/dist/schemas/route-inventory.schema.d.ts +20 -0
  40. package/dist/schemas/route-inventory.schema.d.ts.map +1 -1
  41. package/dist/schemas/route-inventory.schema.js +4 -0
  42. package/dist/schemas/views.schema.d.ts +1 -1
  43. package/dist/tools/scoring/confidence.d.ts.map +1 -1
  44. package/dist/tools/scoring/confidence.js +140 -14
  45. package/dist/tools/scoring/prompt-leakage.d.ts +29 -0
  46. package/dist/tools/scoring/prompt-leakage.d.ts.map +1 -0
  47. package/dist/tools/scoring/prompt-leakage.js +256 -0
  48. 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
- From the local fixture baseline used in v0.5.0 PR 1/2:
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
- "... 4 total gap items ..."
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
- | `analyze_app` | Live-app QA scan: release confidence + gaps + a11y | `url`, optional `auth`, optional LLM knobs |
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/Playwright scaffold from a live URL | `url`, optional `framework`, `maxPagesToScan`, `recipes` |
374
- | `explore_auth` | Deeper auth-path discovery on unfamiliar apps | `url`, optional `timeoutMs` |
375
- | `detect_auth` | Fast single-pass auth pattern guess | `url`, optional `timeoutMs` |
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;;;;;;;;;;;;;;;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"}
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"}
@@ -15,6 +15,7 @@ export const BaselineGapSchema = z.object({
15
15
  'auth-surface',
16
16
  'coverage',
17
17
  'untested-api-endpoint',
18
+ 'prompt-leakage',
18
19
  ]),
19
20
  reason: z.string(),
20
21
  });
@@ -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
+ }