@qulib/core 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +30 -5
  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/confidence-run.d.ts +16 -0
  26. package/dist/cli/confidence-run.d.ts.map +1 -0
  27. package/dist/cli/confidence-run.js +158 -0
  28. package/dist/cli/index.d.ts +11 -1
  29. package/dist/cli/index.d.ts.map +1 -1
  30. package/dist/cli/index.js +80 -4
  31. package/dist/cli/scaffold-run.d.ts +86 -0
  32. package/dist/cli/scaffold-run.d.ts.map +1 -0
  33. package/dist/cli/scaffold-run.js +232 -0
  34. package/dist/cli/score-automation-run.d.ts +25 -0
  35. package/dist/cli/score-automation-run.d.ts.map +1 -0
  36. package/dist/cli/score-automation-run.js +123 -0
  37. package/dist/examples/notquality-dogfood/fixture.d.ts +166 -0
  38. package/dist/examples/notquality-dogfood/fixture.d.ts.map +1 -0
  39. package/dist/examples/notquality-dogfood/fixture.js +174 -0
  40. package/dist/examples/notquality-dogfood/run.d.ts +34 -0
  41. package/dist/examples/notquality-dogfood/run.d.ts.map +1 -0
  42. package/dist/examples/notquality-dogfood/run.js +139 -0
  43. package/dist/index.d.ts +14 -1
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +11 -0
  46. package/dist/recipes/a11y.d.ts +36 -0
  47. package/dist/recipes/a11y.d.ts.map +1 -0
  48. package/dist/recipes/a11y.js +118 -0
  49. package/dist/recipes/auth.d.ts +38 -0
  50. package/dist/recipes/auth.d.ts.map +1 -0
  51. package/dist/recipes/auth.js +156 -0
  52. package/dist/recipes/index.d.ts +26 -0
  53. package/dist/recipes/index.d.ts.map +1 -0
  54. package/dist/recipes/index.js +41 -0
  55. package/dist/recipes/nav.d.ts +34 -0
  56. package/dist/recipes/nav.d.ts.map +1 -0
  57. package/dist/recipes/nav.js +128 -0
  58. package/dist/recipes/seed.d.ts +34 -0
  59. package/dist/recipes/seed.d.ts.map +1 -0
  60. package/dist/recipes/seed.js +87 -0
  61. package/dist/scaffold-tests.d.ts +21 -0
  62. package/dist/scaffold-tests.d.ts.map +1 -1
  63. package/dist/scaffold-tests.js +12 -2
  64. package/dist/schemas/confidence.schema.d.ts +526 -0
  65. package/dist/schemas/confidence.schema.d.ts.map +1 -0
  66. package/dist/schemas/confidence.schema.js +161 -0
  67. package/dist/schemas/index.d.ts +3 -0
  68. package/dist/schemas/index.d.ts.map +1 -1
  69. package/dist/schemas/index.js +3 -0
  70. package/dist/schemas/recipe.schema.d.ts +66 -0
  71. package/dist/schemas/recipe.schema.d.ts.map +1 -0
  72. package/dist/schemas/recipe.schema.js +45 -0
  73. package/dist/schemas/views.schema.d.ts +234 -0
  74. package/dist/schemas/views.schema.d.ts.map +1 -0
  75. package/dist/schemas/views.schema.js +82 -0
  76. package/dist/tools/scoring/confidence-from-qulib.d.ts +34 -0
  77. package/dist/tools/scoring/confidence-from-qulib.d.ts.map +1 -0
  78. package/dist/tools/scoring/confidence-from-qulib.js +206 -0
  79. package/dist/tools/scoring/confidence-views.d.ts +40 -0
  80. package/dist/tools/scoring/confidence-views.d.ts.map +1 -0
  81. package/dist/tools/scoring/confidence-views.js +163 -0
  82. package/dist/tools/scoring/confidence.d.ts +32 -0
  83. package/dist/tools/scoring/confidence.d.ts.map +1 -0
  84. package/dist/tools/scoring/confidence.js +180 -0
  85. package/dist/tools/scoring/levels.d.ts +15 -0
  86. package/dist/tools/scoring/levels.d.ts.map +1 -0
  87. package/dist/tools/scoring/levels.js +21 -0
  88. package/package.json +13 -7
@@ -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,16 @@
1
+ import type { Command } from 'commander';
2
+ import type { ReleaseConfidence } from '../schemas/confidence.schema.js';
3
+ export interface ConfidenceOptions {
4
+ url?: string;
5
+ repo?: string;
6
+ json?: boolean;
7
+ }
8
+ /** Render the human-friendly report for a ReleaseConfidence result. */
9
+ export declare function formatConfidenceReport(rc: ReleaseConfidence, subjectRef: string): string;
10
+ /**
11
+ * Core of the command, factored out of the action handler so node:test can drive it
12
+ * without spawning a subprocess.
13
+ */
14
+ export declare function runConfidence(options: ConfidenceOptions, out?: (line: string) => void): Promise<ReleaseConfidence>;
15
+ export declare function registerConfidenceCommand(program: Command): void;
16
+ //# sourceMappingURL=confidence-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confidence-run.d.ts","sourceRoot":"","sources":["../../src/cli/confidence-run.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQzC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAGzE,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAiBD,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAuCxF;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,iBAAiB,EAC1B,GAAG,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAkC,GACxD,OAAO,CAAC,iBAAiB,CAAC,CAmE5B;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmBhE"}
@@ -0,0 +1,158 @@
1
+ /**
2
+ * `qulib confidence` — compute a fused Release Confidence verdict from qulib's own collectors.
3
+ *
4
+ * P3 — qulib Confidence Layer v1.
5
+ *
6
+ * Composes analyze_app (when --url is given) + computeAutomationMaturity + computeApiCoverage
7
+ * (when --repo is given) through buildConfidenceInputFromQulib → computeReleaseConfidence.
8
+ *
9
+ * The --json flag emits the full ReleaseConfidence object as JSON to stdout (for CI gates and
10
+ * orchestrators). Without --json, a human-readable report is printed.
11
+ *
12
+ * Mirrors the idiom established by score-automation-run.ts: one file owns the command end-to-end
13
+ * and is registered from cli/index.ts via registerConfidenceCommand(program).
14
+ */
15
+ import { resolve } from 'node:path';
16
+ import { existsSync, statSync } from 'node:fs';
17
+ import { analyzeApp } from '../analyze.js';
18
+ import { scanRepo } from '../tools/repo/scan.js';
19
+ import { computeAutomationMaturity } from '../tools/scoring/automation-maturity.js';
20
+ import { discoverApiSurfaceWithRepo } from '../tools/repo/api-surface.js';
21
+ import { computeApiCoverage } from '../tools/scoring/api-coverage.js';
22
+ import { buildConfidenceInputFromQulib } from '../tools/scoring/confidence-from-qulib.js';
23
+ import { computeReleaseConfidence } from '../tools/scoring/confidence.js';
24
+ /**
25
+ * Resolve and validate an optional --repo path. Returns null if none was provided.
26
+ */
27
+ function maybeResolveRepoPath(repoOption, cwd = process.cwd()) {
28
+ if (!repoOption || !repoOption.trim())
29
+ return null;
30
+ const abs = resolve(cwd, repoOption.trim());
31
+ if (!existsSync(abs)) {
32
+ throw new Error(`--repo path does not exist: ${abs}`);
33
+ }
34
+ if (!statSync(abs).isDirectory()) {
35
+ throw new Error(`--repo path is not a directory: ${abs}`);
36
+ }
37
+ return abs;
38
+ }
39
+ /** Render the human-friendly report for a ReleaseConfidence result. */
40
+ export function formatConfidenceReport(rc, subjectRef) {
41
+ const lines = [];
42
+ const scoreStr = rc.confidenceScore !== null ? `${rc.confidenceScore}/100` : 'null (nothing evaluable)';
43
+ lines.push(`[qulib] Release confidence for ${subjectRef}`);
44
+ lines.push(` verdict: ${rc.verdict} — ${rc.label} (score ${scoreStr}, level ${rc.level}/5)`);
45
+ if (rc.blockers.length > 0) {
46
+ lines.push(' blockers:');
47
+ for (const b of rc.blockers)
48
+ lines.push(` • ${b}`);
49
+ }
50
+ lines.push(' contributions:');
51
+ for (const c of rc.contributions) {
52
+ const scoreLabel = c.score !== null ? `${c.score}/100` : 'n/a';
53
+ const ewLabel = c.effectiveWeight > 0
54
+ ? `ew=${(c.effectiveWeight * 100).toFixed(1)}%`
55
+ : 'excluded';
56
+ const blockingTag = c.blocking ? ' [BLOCKER]' : '';
57
+ lines.push(` - ${c.source} [${c.applicability}]${blockingTag}: ${scoreLabel} ${ewLabel}`);
58
+ }
59
+ if (rc.topRisks.length > 0) {
60
+ lines.push(' top risks:');
61
+ for (const r of rc.topRisks)
62
+ lines.push(` • ${r}`);
63
+ }
64
+ if (rc.recommendedNextChecks.length > 0) {
65
+ lines.push(' recommended next checks:');
66
+ for (const r of rc.recommendedNextChecks)
67
+ lines.push(` • ${r}`);
68
+ }
69
+ if (rc.honestyNotes.length > 0) {
70
+ lines.push(' honesty notes:');
71
+ for (const n of rc.honestyNotes)
72
+ lines.push(` • ${n}`);
73
+ }
74
+ return lines.join('\n');
75
+ }
76
+ /**
77
+ * Core of the command, factored out of the action handler so node:test can drive it
78
+ * without spawning a subprocess.
79
+ */
80
+ export async function runConfidence(options, out = (line) => console.log(line)) {
81
+ if (!options.url && !options.repo) {
82
+ throw new Error('qulib confidence requires at least one of --url or --repo.');
83
+ }
84
+ const repoPath = maybeResolveRepoPath(options.repo);
85
+ const subjectRef = options.url ?? repoPath ?? 'unknown';
86
+ const subjectKind = options.url && repoPath ? 'release' : options.url ? 'app' : 'repo';
87
+ const subject = { kind: subjectKind, ref: subjectRef, tenantId: 'default' };
88
+ let analyzeResult;
89
+ let maturityResult;
90
+ let apiCoverageResult;
91
+ if (options.url) {
92
+ if (!options.json) {
93
+ out(`[qulib] Analyzing ${options.url} (analyze_app)…`);
94
+ }
95
+ const harnessConfig = {
96
+ maxPagesToScan: 10,
97
+ maxDepth: 3,
98
+ minPagesForConfidence: 3,
99
+ timeoutMs: 30000,
100
+ retryCount: 0,
101
+ llmTokenBudget: 4096,
102
+ testGenerationLimit: 5,
103
+ enableLlmScenarios: false,
104
+ readOnlyMode: true,
105
+ requireHumanReview: false,
106
+ failOnConsoleError: false,
107
+ explorer: 'playwright',
108
+ defaultAdapter: 'playwright',
109
+ adapters: ['playwright'],
110
+ };
111
+ analyzeResult = await analyzeApp({
112
+ url: options.url,
113
+ writeArtifacts: false,
114
+ config: harnessConfig,
115
+ });
116
+ }
117
+ if (repoPath) {
118
+ if (!options.json) {
119
+ out(`[qulib] Scoring automation maturity + API coverage for ${repoPath}…`);
120
+ }
121
+ const repo = await scanRepo(repoPath);
122
+ maturityResult = computeAutomationMaturity(repo);
123
+ const apiSurface = await discoverApiSurfaceWithRepo(repoPath, repo, { enableTier3: false });
124
+ apiCoverageResult = computeApiCoverage(repo, apiSurface);
125
+ }
126
+ const confidenceInput = buildConfidenceInputFromQulib({
127
+ analyze: analyzeResult,
128
+ maturity: maturityResult,
129
+ apiCoverage: apiCoverageResult,
130
+ subject,
131
+ });
132
+ const rc = computeReleaseConfidence(confidenceInput);
133
+ if (options.json) {
134
+ out(JSON.stringify(rc, null, 2));
135
+ }
136
+ else {
137
+ out(formatConfidenceReport(rc, subjectRef));
138
+ }
139
+ return rc;
140
+ }
141
+ export function registerConfidenceCommand(program) {
142
+ program
143
+ .command('confidence')
144
+ .description('Compute a fused Release Confidence verdict from qulib evidence collectors. ' +
145
+ 'Pass --url to include live-app quality + a11y + coverage evidence. ' +
146
+ 'Pass --repo to include test-automation maturity + API coverage. ' +
147
+ 'Both may be combined.')
148
+ .option('--url <url>', 'URL of the deployed app to analyze')
149
+ .option('--repo <path>', 'Path to the local repository to score')
150
+ .option('--json', 'Emit the full ReleaseConfidence object as JSON to stdout', false)
151
+ .action(async (options) => {
152
+ await runConfidence({
153
+ url: options.url,
154
+ repo: options.repo,
155
+ json: Boolean(options.json),
156
+ });
157
+ });
158
+ }
@@ -1,3 +1,13 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import { type HarnessConfig } from '../schemas/config.schema.js';
3
+ /**
4
+ * Built-in fallback used when no --config is passed AND the default qulib.config.ts
5
+ * is absent from the working directory. Mirrors the values in packages/core/qulib.config.ts
6
+ * so `npx @qulib/core analyze --url ... --ephemeral` works in any directory.
7
+ *
8
+ * An EXPLICIT --config pointing at a missing file is always a hard error.
9
+ * This constant lives in the CLI layer — schemas remain additive-only and required
10
+ * fields are NOT converted to defaulted ones.
11
+ */
12
+ export declare const DEFAULT_HARNESS_CONFIG: HarnessConfig;
3
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AASA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEtF;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,EAAE,aAepC,CAAC"}
package/dist/cli/index.js CHANGED
@@ -7,12 +7,40 @@ import { z } from 'zod';
7
7
  const requirePkg = createRequire(import.meta.url);
8
8
  const pkg = requirePkg('../../package.json');
9
9
  import { HarnessConfigSchema } from '../schemas/config.schema.js';
10
+ /**
11
+ * Built-in fallback used when no --config is passed AND the default qulib.config.ts
12
+ * is absent from the working directory. Mirrors the values in packages/core/qulib.config.ts
13
+ * so `npx @qulib/core analyze --url ... --ephemeral` works in any directory.
14
+ *
15
+ * An EXPLICIT --config pointing at a missing file is always a hard error.
16
+ * This constant lives in the CLI layer — schemas remain additive-only and required
17
+ * fields are NOT converted to defaulted ones.
18
+ */
19
+ export const DEFAULT_HARNESS_CONFIG = {
20
+ maxPagesToScan: 20,
21
+ maxDepth: 3,
22
+ minPagesForConfidence: 3,
23
+ timeoutMs: 30000,
24
+ retryCount: 2,
25
+ llmTokenBudget: 4000,
26
+ testGenerationLimit: 10,
27
+ enableLlmScenarios: true,
28
+ readOnlyMode: true,
29
+ requireHumanReview: true,
30
+ failOnConsoleError: false,
31
+ explorer: 'playwright',
32
+ defaultAdapter: 'playwright',
33
+ adapters: ['playwright'],
34
+ };
10
35
  import { analyzeApp } from '../analyze.js';
11
36
  import { toAgentSummary } from '../agent-summary.js';
12
37
  import { detectAuth } from '../tools/auth/detect.js';
13
38
  import { exploreAuth } from '../tools/auth/explore.js';
14
39
  import { assertExactlyOneCredentialSource, parseCredentialsJsonString, resolveAuthLoginConfig, } from './auth-login-resolve.js';
15
40
  import { runAutomatedAuthLogin } from './auth-login-run.js';
41
+ import { registerScaffoldCommand } from './scaffold-run.js';
42
+ import { registerScoreAutomationCommand } from './score-automation-run.js';
43
+ import { registerConfidenceCommand } from './confidence-run.js';
16
44
  const program = new Command();
17
45
  const AnalyzeUrlSchema = z.string().url();
18
46
  const FormLoginCliSchema = z.object({
@@ -23,11 +51,50 @@ const FormLoginCliSchema = z.object({
23
51
  passwordSelector: z.string().min(1),
24
52
  submitSelector: z.string().min(1),
25
53
  });
26
- async function loadConfigFile(relativePath) {
54
+ /**
55
+ * Load a harness config from a file path.
56
+ *
57
+ * When `explicit` is false (the user did not pass --config) and the file does
58
+ * not exist, this returns DEFAULT_HARNESS_CONFIG silently rather than crashing.
59
+ * An explicit --config that points at a missing file is always a hard error.
60
+ */
61
+ async function loadConfigFile(relativePath, explicit) {
27
62
  const configPath = resolve(process.cwd(), relativePath);
28
- const configModule = await import(pathToFileURL(configPath).href);
63
+ // Implicit default + file absent → use built-in fallback, no crash.
64
+ if (!explicit) {
65
+ const { existsSync } = await import('node:fs');
66
+ if (!existsSync(configPath)) {
67
+ console.error('[qulib] No qulib.config.ts found in the working directory; using built-in default config. ' +
68
+ 'Pass --config <file> to supply your own.');
69
+ return DEFAULT_HARNESS_CONFIG;
70
+ }
71
+ }
72
+ const href = pathToFileURL(configPath).href;
73
+ let configModule;
74
+ try {
75
+ configModule = await import(href);
76
+ }
77
+ catch (err) {
78
+ // Plain node (the published CLI) cannot import a .ts config directly.
79
+ if (!configPath.endsWith('.ts') ||
80
+ err.code !== 'ERR_UNKNOWN_FILE_EXTENSION') {
81
+ throw err;
82
+ }
83
+ configModule = await importTsConfigViaTsx(configPath, href);
84
+ }
29
85
  return HarnessConfigSchema.parse(configModule.default);
30
86
  }
87
+ async function importTsConfigViaTsx(configPath, href) {
88
+ let tsImport;
89
+ try {
90
+ ({ tsImport } = await import('tsx/esm/api'));
91
+ }
92
+ catch {
93
+ throw new Error(`"${configPath}" is a TypeScript config, which this Node runtime cannot import directly. ` +
94
+ 'Install tsx (npm i -D tsx) or point --config at a .js or .mjs config file.');
95
+ }
96
+ return tsImport(href, import.meta.url);
97
+ }
31
98
  function redactConfigForLog(config) {
32
99
  const base = { ...config };
33
100
  if (config.auth?.type === 'form-login') {
@@ -86,7 +153,10 @@ async function runAnalyze(options) {
86
153
  }
87
154
  const validatedUrl = AnalyzeUrlSchema.parse(options.url);
88
155
  const mode = options.repo ? 'url-repo' : 'url-only';
89
- const baseConfig = await loadConfigFile(options.configFile ?? 'qulib.config.ts');
156
+ // configFile is undefined when --config was not explicitly passed (Commander
157
+ // no longer sets a default on the flag so we can distinguish the two cases).
158
+ const configFileExplicit = options.configFile !== undefined;
159
+ const baseConfig = await loadConfigFile(options.configFile ?? 'qulib.config.ts', configFileExplicit);
90
160
  const config = mergeAuthFromCli(baseConfig, options);
91
161
  const ephemeral = options.ephemeral ?? false;
92
162
  const agentSummary = options.agentSummary ?? false;
@@ -131,6 +201,12 @@ program
131
201
  .name('qulib')
132
202
  .description('Qulib — QA harness')
133
203
  .version(pkg.version);
204
+ // Q2 (CLIs + evals): each new CLI surface owns its own file and self-registers
205
+ // here, so the scaffold and score-automation commands never edit index.ts
206
+ // concurrently. Implemented in scaffold-run.ts / score-automation-run.ts.
207
+ registerScaffoldCommand(program);
208
+ registerScoreAutomationCommand(program);
209
+ registerConfidenceCommand(program);
134
210
  program
135
211
  .command('clean')
136
212
  .description('Remove all generated reports and scan state')
@@ -167,7 +243,7 @@ program
167
243
  .requiredOption('--url <url>', 'Base URL of the app to analyze')
168
244
  .option('--repo <path>', 'Path to the app repo')
169
245
  .option('--prd <path>', 'Path to a PRD markdown file')
170
- .option('--config <file>', 'Path to config file (relative to cwd)', 'qulib.config.ts')
246
+ .option('--config <file>', 'Path to config file (relative to cwd; defaults to qulib.config.ts if present, otherwise built-in defaults are used)')
171
247
  .option('--adapter <type>', 'Override default test adapter (playwright, cypress-e2e, cypress-component, api)', 'playwright')
172
248
  .option('--ephemeral', 'Do not write to disk — return full report as JSON on stdout (use for MCP/CI)', false)
173
249
  .option('--agent-summary', 'Do not write to disk — return the compact agent-summary gate JSON on stdout (pass/warn/fail with honesty notes). Mutually exclusive with --ephemeral.', false)