@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,55 @@
1
+ import type { Command } from 'commander';
2
+ import type { BaselineSnapshot, BaselineDelta } from '../baseline/baseline.schema.js';
3
+ import { type GapAnalysis } from '../schemas/gap-analysis.schema.js';
4
+ /**
5
+ * Read and validate a GapAnalysis from a report.json written by `qulib analyze`.
6
+ * report.json is exactly a serialized GapAnalysis (see reporters/json-reporter.ts),
7
+ * so we parse it through GapAnalysisSchema — fail loudly on a malformed/foreign file
8
+ * rather than baselining garbage.
9
+ */
10
+ export declare function loadGapAnalysisFromReport(reportPath: string, cwd?: string): Promise<GapAnalysis>;
11
+ export interface BaselineSaveOptions {
12
+ url: string;
13
+ fromReport?: string;
14
+ label?: string;
15
+ dir?: string;
16
+ json?: boolean;
17
+ }
18
+ /** Render a saved snapshot as a short human line. */
19
+ export declare function formatSavedSnapshot(snap: BaselineSnapshot): string;
20
+ /**
21
+ * Core of `baseline save`, factored out so node:test can drive it without a process.
22
+ * Either --from-report (deterministic, reads a real on-disk report.json) or a live
23
+ * crawl of --url. --from-report is preferred for CI/agents that already ran analyze.
24
+ */
25
+ export declare function runBaselineSave(options: BaselineSaveOptions, out?: (line: string) => void): Promise<BaselineSnapshot>;
26
+ export interface BaselineListOptions {
27
+ url: string;
28
+ dir?: string;
29
+ json?: boolean;
30
+ }
31
+ /** Render a list of snapshots as a human table. */
32
+ export declare function formatBaselineList(url: string, snaps: BaselineSnapshot[]): string;
33
+ /** Core of `baseline list`, factored out for direct testing. */
34
+ export declare function runBaselineList(options: BaselineListOptions, out?: (line: string) => void): Promise<BaselineSnapshot[]>;
35
+ export interface BaselineCompareOptions {
36
+ /** Compare two explicit baseline ids. Takes precedence over --url. */
37
+ from?: string;
38
+ to?: string;
39
+ /** Or: compare the two most-recent baselines for this URL. */
40
+ url?: string;
41
+ dir?: string;
42
+ json?: boolean;
43
+ }
44
+ /** Render a delta as a human report. */
45
+ export declare function formatBaselineDelta(delta: BaselineDelta): string;
46
+ /**
47
+ * Core of `baseline compare`, factored out for direct testing.
48
+ * Resolution order:
49
+ * 1. --from and --to explicit ids → load both, compare.
50
+ * 2. --url → take the two most-recent baselines (prior = older, current = newest).
51
+ * Fails clearly when fewer than two baselines are available — never invents a delta.
52
+ */
53
+ export declare function runBaselineCompare(options: BaselineCompareOptions, out?: (line: string) => void): Promise<BaselineDelta>;
54
+ export declare function registerBaselineCommand(program: Command): void;
55
+ //# sourceMappingURL=baseline-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline-run.d.ts","sourceRoot":"","sources":["../../src/cli/baseline-run.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AACtF,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,mCAAmC,CAAC;AA0BxF;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,MAAsB,GAC1B,OAAO,CAAC,WAAW,CAAC,CAsBtB;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,qDAAqD;AACrD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAQlE;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,EAC5B,GAAG,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAkC,GACxD,OAAO,CAAC,gBAAgB,CAAC,CA6B3B;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,mDAAmD;AACnD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAUjF;AAED,gEAAgE;AAChE,wBAAsB,eAAe,CACnC,OAAO,EAAE,mBAAmB,EAC5B,GAAG,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAkC,GACxD,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAW7B;AAED,MAAM,WAAW,sBAAsB;IACrC,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,wCAAwC;AACxC,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAiBhE;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,sBAAsB,EAC/B,GAAG,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAkC,GACxD,OAAO,CAAC,aAAa,CAAC,CAmCxB;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwD9D"}
@@ -0,0 +1,259 @@
1
+ /**
2
+ * `qulib baseline` — capture and compare quality-gap baselines over time.
3
+ *
4
+ * Surfaces the previously dead-ended baseline module (../baseline/baseline.js) as a
5
+ * first-class CLI. Until now save/load/list/compare existed only as a programmatic API
6
+ * with no consumer; this exposes them so an agent or CI can snapshot a release's gaps
7
+ * and detect drift (new / resolved / severity-changed gaps + confidence delta) between
8
+ * runs.
9
+ *
10
+ * Subcommands:
11
+ * baseline save — snapshot a GapAnalysis for a URL. Source the analysis either by
12
+ * crawling live (--url alone) or, deterministically, from an
13
+ * existing report.json written by `qulib analyze` (--from-report).
14
+ * baseline list — list saved baselines for a URL, newest-first.
15
+ * baseline compare — diff two baselines (explicit ids, or the two most-recent for a URL).
16
+ *
17
+ * Storage honesty (root design principle — no false confidence):
18
+ * Baselines persist under <cwd>/.qulib-baselines/<url-slug>/ unless --dir overrides.
19
+ * `compare` with fewer than two baselines fails with a clear, actionable message
20
+ * rather than fabricating a delta against nothing.
21
+ *
22
+ * This file owns the `baseline` command group end-to-end and is registered from
23
+ * cli/index.ts via `registerBaselineCommand(program)`, so this command's build never
24
+ * edits the body of index.ts beyond a single additive registration line (mirrors the
25
+ * score-automation / scaffold / confidence convention).
26
+ */
27
+ import { resolve } from 'node:path';
28
+ import { readFile } from 'node:fs/promises';
29
+ import { z } from 'zod';
30
+ import { saveBaseline, listBaselines, loadBaseline, compareBaselines, } from '../baseline/baseline.js';
31
+ import { GapAnalysisSchema } from '../schemas/gap-analysis.schema.js';
32
+ import { analyzeApp } from '../analyze.js';
33
+ const UrlSchema = z.string().url();
34
+ /** Harness config used when `save --url` crawls live (no repo, read-only, no LLM). */
35
+ function liveCrawlConfig() {
36
+ return {
37
+ maxPagesToScan: 10,
38
+ maxDepth: 3,
39
+ minPagesForConfidence: 3,
40
+ timeoutMs: 30000,
41
+ retryCount: 0,
42
+ llmTokenBudget: 4096,
43
+ testGenerationLimit: 5,
44
+ enableLlmScenarios: false,
45
+ readOnlyMode: true,
46
+ requireHumanReview: false,
47
+ failOnConsoleError: false,
48
+ explorer: 'playwright',
49
+ defaultAdapter: 'playwright',
50
+ adapters: ['playwright'],
51
+ };
52
+ }
53
+ /**
54
+ * Read and validate a GapAnalysis from a report.json written by `qulib analyze`.
55
+ * report.json is exactly a serialized GapAnalysis (see reporters/json-reporter.ts),
56
+ * so we parse it through GapAnalysisSchema — fail loudly on a malformed/foreign file
57
+ * rather than baselining garbage.
58
+ */
59
+ export async function loadGapAnalysisFromReport(reportPath, cwd = process.cwd()) {
60
+ const abs = resolve(cwd, reportPath);
61
+ let raw;
62
+ try {
63
+ raw = await readFile(abs, 'utf8');
64
+ }
65
+ catch {
66
+ throw new Error(`--from-report path could not be read: ${abs}`);
67
+ }
68
+ let parsed;
69
+ try {
70
+ parsed = JSON.parse(raw);
71
+ }
72
+ catch {
73
+ throw new Error(`--from-report file is not valid JSON: ${abs}`);
74
+ }
75
+ const result = GapAnalysisSchema.safeParse(parsed);
76
+ if (!result.success) {
77
+ throw new Error(`--from-report file is not a valid qulib report.json (GapAnalysis): ${abs}\n` +
78
+ result.error.issues.map((i) => ` • ${i.path.join('.')}: ${i.message}`).join('\n'));
79
+ }
80
+ return result.data;
81
+ }
82
+ /** Render a saved snapshot as a short human line. */
83
+ export function formatSavedSnapshot(snap) {
84
+ const labelTag = snap.label ? ` "${snap.label}"` : '';
85
+ return (`[qulib] Saved baseline ${snap.id}${labelTag}\n` +
86
+ ` url: ${snap.url}\n` +
87
+ ` releaseConfidence: ${snap.releaseConfidence}/100\n` +
88
+ ` gaps: ${snap.gapCount}`);
89
+ }
90
+ /**
91
+ * Core of `baseline save`, factored out so node:test can drive it without a process.
92
+ * Either --from-report (deterministic, reads a real on-disk report.json) or a live
93
+ * crawl of --url. --from-report is preferred for CI/agents that already ran analyze.
94
+ */
95
+ export async function runBaselineSave(options, out = (line) => console.log(line)) {
96
+ const url = UrlSchema.parse(options.url);
97
+ let analysis;
98
+ if (options.fromReport && options.fromReport.trim()) {
99
+ analysis = await loadGapAnalysisFromReport(options.fromReport.trim());
100
+ }
101
+ else {
102
+ if (!options.json) {
103
+ out(`[qulib] Crawling ${url} to capture a fresh baseline (analyze_app)…`);
104
+ }
105
+ const result = await analyzeApp({
106
+ url,
107
+ writeArtifacts: false,
108
+ config: liveCrawlConfig(),
109
+ });
110
+ analysis = result.gapAnalysis;
111
+ }
112
+ const snapshot = await saveBaseline(analysis, url, {
113
+ ...(options.dir ? { baseDir: resolve(process.cwd(), options.dir) } : {}),
114
+ ...(options.label !== undefined ? { label: options.label } : {}),
115
+ });
116
+ if (options.json) {
117
+ out(JSON.stringify(snapshot, null, 2));
118
+ }
119
+ else {
120
+ out(formatSavedSnapshot(snapshot));
121
+ }
122
+ return snapshot;
123
+ }
124
+ /** Render a list of snapshots as a human table. */
125
+ export function formatBaselineList(url, snaps) {
126
+ if (snaps.length === 0) {
127
+ return `[qulib] No baselines saved for ${url} yet. Run: qulib baseline save --url ${url} --from-report output/report.json`;
128
+ }
129
+ const lines = [`[qulib] ${snaps.length} baseline(s) for ${url} (newest first):`];
130
+ for (const s of snaps) {
131
+ const labelTag = s.label ? ` "${s.label}"` : '';
132
+ lines.push(` ${s.id} — confidence ${s.releaseConfidence}/100, ${s.gapCount} gap(s)${labelTag}`);
133
+ }
134
+ return lines.join('\n');
135
+ }
136
+ /** Core of `baseline list`, factored out for direct testing. */
137
+ export async function runBaselineList(options, out = (line) => console.log(line)) {
138
+ const url = UrlSchema.parse(options.url);
139
+ const snaps = await listBaselines(url, {
140
+ ...(options.dir ? { baseDir: resolve(process.cwd(), options.dir) } : {}),
141
+ });
142
+ if (options.json) {
143
+ out(JSON.stringify(snaps, null, 2));
144
+ }
145
+ else {
146
+ out(formatBaselineList(url, snaps));
147
+ }
148
+ return snaps;
149
+ }
150
+ /** Render a delta as a human report. */
151
+ export function formatBaselineDelta(delta) {
152
+ const lines = [];
153
+ lines.push(`[qulib] Baseline comparison`);
154
+ lines.push(` from: ${delta.fromId} (${delta.fromReleaseConfidence}/100)`);
155
+ lines.push(` to: ${delta.toId} (${delta.toReleaseConfidence}/100)`);
156
+ lines.push(` ${delta.summary}`);
157
+ const section = (title, items) => {
158
+ if (items.length === 0)
159
+ return;
160
+ lines.push(` ${title}:`);
161
+ for (const g of items) {
162
+ lines.push(` • [${g.severity}] ${g.path} (${g.category}) — ${g.reason}`);
163
+ }
164
+ };
165
+ section('new gaps', delta.newGaps);
166
+ section('resolved gaps', delta.resolvedGaps);
167
+ section('severity changes', delta.severityChanges);
168
+ return lines.join('\n');
169
+ }
170
+ /**
171
+ * Core of `baseline compare`, factored out for direct testing.
172
+ * Resolution order:
173
+ * 1. --from and --to explicit ids → load both, compare.
174
+ * 2. --url → take the two most-recent baselines (prior = older, current = newest).
175
+ * Fails clearly when fewer than two baselines are available — never invents a delta.
176
+ */
177
+ export async function runBaselineCompare(options, out = (line) => console.log(line)) {
178
+ const baseDirOpt = options.dir ? { baseDir: resolve(process.cwd(), options.dir) } : {};
179
+ let prior;
180
+ let current;
181
+ if (options.from || options.to) {
182
+ if (!options.from || !options.to) {
183
+ throw new Error('baseline compare with explicit ids requires BOTH --from and --to.');
184
+ }
185
+ prior = await loadBaseline(options.from, baseDirOpt);
186
+ current = await loadBaseline(options.to, baseDirOpt);
187
+ }
188
+ else if (options.url) {
189
+ const url = UrlSchema.parse(options.url);
190
+ const snaps = await listBaselines(url, baseDirOpt);
191
+ if (snaps.length < 2) {
192
+ throw new Error(`baseline compare needs at least two saved baselines for ${url}; found ${snaps.length}. ` +
193
+ `Save another with: qulib baseline save --url ${url} --from-report output/report.json`);
194
+ }
195
+ // listBaselines is newest-first: snaps[0] is current, snaps[1] is the prior.
196
+ current = snaps[0];
197
+ prior = snaps[1];
198
+ }
199
+ else {
200
+ throw new Error('baseline compare requires either --from + --to, or --url.');
201
+ }
202
+ const delta = compareBaselines(prior, current);
203
+ if (options.json) {
204
+ out(JSON.stringify(delta, null, 2));
205
+ }
206
+ else {
207
+ out(formatBaselineDelta(delta));
208
+ }
209
+ return delta;
210
+ }
211
+ export function registerBaselineCommand(program) {
212
+ const baseline = program
213
+ .command('baseline')
214
+ .description('Capture and compare quality-gap baselines over time. Snapshot a release\'s gaps, ' +
215
+ 'then diff later runs to surface new / resolved / severity-changed gaps and confidence drift.');
216
+ baseline
217
+ .command('save')
218
+ .description('Save a baseline snapshot for a URL (from a report.json or a fresh live crawl)')
219
+ .requiredOption('--url <url>', 'URL the baseline is keyed to')
220
+ .option('--from-report <path>', 'Path to a report.json from `qulib analyze` (deterministic, no network). If omitted, the URL is crawled live.')
221
+ .option('--label <label>', 'Optional human label for this snapshot, e.g. before-refactor')
222
+ .option('--dir <path>', 'Baseline storage root (default: <cwd>/.qulib-baselines)')
223
+ .option('--json', 'Emit the saved BaselineSnapshot as JSON to stdout', false)
224
+ .action(async (options) => {
225
+ await runBaselineSave({
226
+ url: options.url,
227
+ fromReport: options.fromReport,
228
+ label: options.label,
229
+ dir: options.dir,
230
+ json: Boolean(options.json),
231
+ });
232
+ });
233
+ baseline
234
+ .command('list')
235
+ .description('List saved baselines for a URL, newest first')
236
+ .requiredOption('--url <url>', 'URL whose baselines to list')
237
+ .option('--dir <path>', 'Baseline storage root (default: <cwd>/.qulib-baselines)')
238
+ .option('--json', 'Emit the BaselineSnapshot[] as JSON to stdout', false)
239
+ .action(async (options) => {
240
+ await runBaselineList({ url: options.url, dir: options.dir, json: Boolean(options.json) });
241
+ });
242
+ baseline
243
+ .command('compare')
244
+ .description('Compare two baselines — pass --from and --to ids, or --url for the two most-recent')
245
+ .option('--from <id>', 'Prior baseline id')
246
+ .option('--to <id>', 'Current baseline id')
247
+ .option('--url <url>', 'Compare the two most-recent baselines for this URL')
248
+ .option('--dir <path>', 'Baseline storage root (default: <cwd>/.qulib-baselines)')
249
+ .option('--json', 'Emit the full BaselineDelta as JSON to stdout', false)
250
+ .action(async (options) => {
251
+ await runBaselineCompare({
252
+ from: options.from,
253
+ to: options.to,
254
+ url: options.url,
255
+ dir: options.dir,
256
+ json: Boolean(options.json),
257
+ });
258
+ });
259
+ }
@@ -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,CAuBhE"}
@@ -0,0 +1,162 @@
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
+ // Canonical flagship command. Alias: release-confidence (for integrations that prefer the
143
+ // full concept name over the short form). Both names are kept through 1.0.
144
+ program
145
+ .command('confidence')
146
+ .alias('release-confidence')
147
+ .description('Compute a fused Release Confidence verdict from qulib evidence collectors. ' +
148
+ 'Pass --url to include live-app quality + a11y + coverage evidence. ' +
149
+ 'Pass --repo to include test-automation maturity + API coverage. ' +
150
+ 'Both may be combined. ' +
151
+ '(Alias: release-confidence)')
152
+ .option('--url <url>', 'URL of the deployed app to analyze')
153
+ .option('--repo <path>', 'Path to the local repository to score')
154
+ .option('--json', 'Emit the full ReleaseConfidence object as JSON to stdout', false)
155
+ .action(async (options) => {
156
+ await runConfidence({
157
+ url: options.url,
158
+ repo: options.repo,
159
+ json: Boolean(options.json),
160
+ });
161
+ });
162
+ }
@@ -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,42 @@ 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';
44
+ import { registerBaselineCommand } from './baseline-run.js';
45
+ import { registerAnalyzeDiffCommand } from './analyze-diff-run.js';
16
46
  const program = new Command();
17
47
  const AnalyzeUrlSchema = z.string().url();
18
48
  const FormLoginCliSchema = z.object({
@@ -23,11 +53,50 @@ const FormLoginCliSchema = z.object({
23
53
  passwordSelector: z.string().min(1),
24
54
  submitSelector: z.string().min(1),
25
55
  });
26
- async function loadConfigFile(relativePath) {
56
+ /**
57
+ * Load a harness config from a file path.
58
+ *
59
+ * When `explicit` is false (the user did not pass --config) and the file does
60
+ * not exist, this returns DEFAULT_HARNESS_CONFIG silently rather than crashing.
61
+ * An explicit --config that points at a missing file is always a hard error.
62
+ */
63
+ async function loadConfigFile(relativePath, explicit) {
27
64
  const configPath = resolve(process.cwd(), relativePath);
28
- const configModule = await import(pathToFileURL(configPath).href);
65
+ // Implicit default + file absent → use built-in fallback, no crash.
66
+ if (!explicit) {
67
+ const { existsSync } = await import('node:fs');
68
+ if (!existsSync(configPath)) {
69
+ console.error('[qulib] No qulib.config.ts found in the working directory; using built-in default config. ' +
70
+ 'Pass --config <file> to supply your own.');
71
+ return DEFAULT_HARNESS_CONFIG;
72
+ }
73
+ }
74
+ const href = pathToFileURL(configPath).href;
75
+ let configModule;
76
+ try {
77
+ configModule = await import(href);
78
+ }
79
+ catch (err) {
80
+ // Plain node (the published CLI) cannot import a .ts config directly.
81
+ if (!configPath.endsWith('.ts') ||
82
+ err.code !== 'ERR_UNKNOWN_FILE_EXTENSION') {
83
+ throw err;
84
+ }
85
+ configModule = await importTsConfigViaTsx(configPath, href);
86
+ }
29
87
  return HarnessConfigSchema.parse(configModule.default);
30
88
  }
89
+ async function importTsConfigViaTsx(configPath, href) {
90
+ let tsImport;
91
+ try {
92
+ ({ tsImport } = await import('tsx/esm/api'));
93
+ }
94
+ catch {
95
+ throw new Error(`"${configPath}" is a TypeScript config, which this Node runtime cannot import directly. ` +
96
+ 'Install tsx (npm i -D tsx) or point --config at a .js or .mjs config file.');
97
+ }
98
+ return tsImport(href, import.meta.url);
99
+ }
31
100
  function redactConfigForLog(config) {
32
101
  const base = { ...config };
33
102
  if (config.auth?.type === 'form-login') {
@@ -86,7 +155,10 @@ async function runAnalyze(options) {
86
155
  }
87
156
  const validatedUrl = AnalyzeUrlSchema.parse(options.url);
88
157
  const mode = options.repo ? 'url-repo' : 'url-only';
89
- const baseConfig = await loadConfigFile(options.configFile ?? 'qulib.config.ts');
158
+ // configFile is undefined when --config was not explicitly passed (Commander
159
+ // no longer sets a default on the flag so we can distinguish the two cases).
160
+ const configFileExplicit = options.configFile !== undefined;
161
+ const baseConfig = await loadConfigFile(options.configFile ?? 'qulib.config.ts', configFileExplicit);
90
162
  const config = mergeAuthFromCli(baseConfig, options);
91
163
  const ephemeral = options.ephemeral ?? false;
92
164
  const agentSummary = options.agentSummary ?? false;
@@ -131,6 +203,14 @@ program
131
203
  .name('qulib')
132
204
  .description('Qulib — QA harness')
133
205
  .version(pkg.version);
206
+ // Q2 (CLIs + evals): each new CLI surface owns its own file and self-registers
207
+ // here, so the scaffold and score-automation commands never edit index.ts
208
+ // concurrently. Implemented in scaffold-run.ts / score-automation-run.ts.
209
+ registerScaffoldCommand(program);
210
+ registerScoreAutomationCommand(program);
211
+ registerConfidenceCommand(program);
212
+ registerBaselineCommand(program);
213
+ registerAnalyzeDiffCommand(program);
134
214
  program
135
215
  .command('clean')
136
216
  .description('Remove all generated reports and scan state')
@@ -167,7 +247,7 @@ program
167
247
  .requiredOption('--url <url>', 'Base URL of the app to analyze')
168
248
  .option('--repo <path>', 'Path to the app repo')
169
249
  .option('--prd <path>', 'Path to a PRD markdown file')
170
- .option('--config <file>', 'Path to config file (relative to cwd)', 'qulib.config.ts')
250
+ .option('--config <file>', 'Path to config file (relative to cwd; defaults to qulib.config.ts if present, otherwise built-in defaults are used)')
171
251
  .option('--adapter <type>', 'Override default test adapter (playwright, cypress-e2e, cypress-component, api)', 'playwright')
172
252
  .option('--ephemeral', 'Do not write to disk — return full report as JSON on stdout (use for MCP/CI)', false)
173
253
  .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)