@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.
Files changed (101) hide show
  1. package/README.md +38 -13
  2. package/bin/qulib.js +2 -3
  3. package/dist/__tests__/playwright-available.d.ts +32 -0
  4. package/dist/__tests__/playwright-available.d.ts.map +1 -0
  5. package/dist/__tests__/playwright-available.js +35 -0
  6. package/dist/adapters/ci-results-adapter.d.ts +67 -0
  7. package/dist/adapters/ci-results-adapter.d.ts.map +1 -0
  8. package/dist/adapters/ci-results-adapter.js +143 -0
  9. package/dist/adapters/cypress-e2e-adapter.d.ts.map +1 -1
  10. package/dist/adapters/cypress-e2e-adapter.js +25 -2
  11. package/dist/adapters/playwright-adapter.d.ts.map +1 -1
  12. package/dist/adapters/playwright-adapter.js +25 -2
  13. package/dist/adapters/pr-metadata-adapter.d.ts +75 -0
  14. package/dist/adapters/pr-metadata-adapter.d.ts.map +1 -0
  15. package/dist/adapters/pr-metadata-adapter.js +146 -0
  16. package/dist/adapters/validate-specs.d.ts +55 -0
  17. package/dist/adapters/validate-specs.d.ts.map +1 -0
  18. package/dist/adapters/validate-specs.js +67 -0
  19. package/dist/baseline/baseline.d.ts +54 -0
  20. package/dist/baseline/baseline.d.ts.map +1 -0
  21. package/dist/baseline/baseline.js +252 -0
  22. package/dist/baseline/baseline.schema.d.ts +233 -0
  23. package/dist/baseline/baseline.schema.d.ts.map +1 -0
  24. package/dist/baseline/baseline.schema.js +59 -0
  25. package/dist/cli/analyze-diff-run.d.ts +77 -0
  26. package/dist/cli/analyze-diff-run.d.ts.map +1 -0
  27. package/dist/cli/analyze-diff-run.js +266 -0
  28. package/dist/cli/baseline-run.d.ts +55 -0
  29. package/dist/cli/baseline-run.d.ts.map +1 -0
  30. package/dist/cli/baseline-run.js +259 -0
  31. package/dist/cli/confidence-run.d.ts +16 -0
  32. package/dist/cli/confidence-run.d.ts.map +1 -0
  33. package/dist/cli/confidence-run.js +162 -0
  34. package/dist/cli/index.d.ts +11 -1
  35. package/dist/cli/index.d.ts.map +1 -1
  36. package/dist/cli/index.js +84 -4
  37. package/dist/cli/scaffold-run.d.ts +86 -0
  38. package/dist/cli/scaffold-run.d.ts.map +1 -0
  39. package/dist/cli/scaffold-run.js +232 -0
  40. package/dist/cli/score-automation-run.d.ts +25 -0
  41. package/dist/cli/score-automation-run.d.ts.map +1 -0
  42. package/dist/cli/score-automation-run.js +127 -0
  43. package/dist/examples/notquality-dogfood/fixture.d.ts +166 -0
  44. package/dist/examples/notquality-dogfood/fixture.d.ts.map +1 -0
  45. package/dist/examples/notquality-dogfood/fixture.js +174 -0
  46. package/dist/examples/notquality-dogfood/run.d.ts +34 -0
  47. package/dist/examples/notquality-dogfood/run.d.ts.map +1 -0
  48. package/dist/examples/notquality-dogfood/run.js +139 -0
  49. package/dist/index.d.ts +18 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +15 -0
  52. package/dist/recipes/a11y.d.ts +36 -0
  53. package/dist/recipes/a11y.d.ts.map +1 -0
  54. package/dist/recipes/a11y.js +118 -0
  55. package/dist/recipes/auth.d.ts +38 -0
  56. package/dist/recipes/auth.d.ts.map +1 -0
  57. package/dist/recipes/auth.js +156 -0
  58. package/dist/recipes/index.d.ts +26 -0
  59. package/dist/recipes/index.d.ts.map +1 -0
  60. package/dist/recipes/index.js +41 -0
  61. package/dist/recipes/nav.d.ts +34 -0
  62. package/dist/recipes/nav.d.ts.map +1 -0
  63. package/dist/recipes/nav.js +128 -0
  64. package/dist/recipes/seed.d.ts +34 -0
  65. package/dist/recipes/seed.d.ts.map +1 -0
  66. package/dist/recipes/seed.js +87 -0
  67. package/dist/reporters/heatmap.d.ts +55 -0
  68. package/dist/reporters/heatmap.d.ts.map +1 -0
  69. package/dist/reporters/heatmap.js +146 -0
  70. package/dist/reporters/markdown-reporter.d.ts.map +1 -1
  71. package/dist/reporters/markdown-reporter.js +4 -1
  72. package/dist/scaffold-tests.d.ts +21 -0
  73. package/dist/scaffold-tests.d.ts.map +1 -1
  74. package/dist/scaffold-tests.js +12 -2
  75. package/dist/schemas/confidence.schema.d.ts +526 -0
  76. package/dist/schemas/confidence.schema.d.ts.map +1 -0
  77. package/dist/schemas/confidence.schema.js +161 -0
  78. package/dist/schemas/config.schema.d.ts.map +1 -1
  79. package/dist/schemas/config.schema.js +6 -1
  80. package/dist/schemas/index.d.ts +3 -0
  81. package/dist/schemas/index.d.ts.map +1 -1
  82. package/dist/schemas/index.js +3 -0
  83. package/dist/schemas/recipe.schema.d.ts +66 -0
  84. package/dist/schemas/recipe.schema.d.ts.map +1 -0
  85. package/dist/schemas/recipe.schema.js +45 -0
  86. package/dist/schemas/views.schema.d.ts +234 -0
  87. package/dist/schemas/views.schema.d.ts.map +1 -0
  88. package/dist/schemas/views.schema.js +82 -0
  89. package/dist/tools/scoring/confidence-from-qulib.d.ts +34 -0
  90. package/dist/tools/scoring/confidence-from-qulib.d.ts.map +1 -0
  91. package/dist/tools/scoring/confidence-from-qulib.js +206 -0
  92. package/dist/tools/scoring/confidence-views.d.ts +40 -0
  93. package/dist/tools/scoring/confidence-views.d.ts.map +1 -0
  94. package/dist/tools/scoring/confidence-views.js +163 -0
  95. package/dist/tools/scoring/confidence.d.ts +32 -0
  96. package/dist/tools/scoring/confidence.d.ts.map +1 -0
  97. package/dist/tools/scoring/confidence.js +180 -0
  98. package/dist/tools/scoring/levels.d.ts +15 -0
  99. package/dist/tools/scoring/levels.d.ts.map +1 -0
  100. package/dist/tools/scoring/levels.js +21 -0
  101. 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
+ }