@qulib/core 0.9.0 → 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.
package/README.md CHANGED
@@ -345,34 +345,34 @@ qulib analyze --url https://yourapp.com --auth-storage-state ./qulib-storage-sta
345
345
 
346
346
  ## Sample report (fixture baseline)
347
347
 
348
- From the local fixture baseline used in v0.5.0 PR 1/2:
348
+ The fixture tests in `packages/core/src/__tests__/analyze.fixtures.test.ts` assert structural shape — that `releaseConfidence` is a number, `gaps` is an array, and coverage scores are non-negative. Exact scores vary with each scoring version; re-run the fixture suite for current reference values.
349
+
350
+ A minimal structural snapshot looks like:
349
351
 
350
352
  ```json
351
353
  {
352
354
  "status": "complete",
353
355
  "releaseConfidence": 68,
354
356
  "gaps": [
355
- "... 4 total gap items ..."
357
+ "... gap items ..."
356
358
  ]
357
359
  }
358
360
  ```
359
361
 
360
- Use these as conservative reference numbers:
361
- - public fixture (`/`): `releaseConfidence: 68/100`, `gaps: 4`
362
- - auth-wall fixture (`/auth`): `releaseConfidence: 24/100`, `gaps: 2`
363
- - broken fixture (`/broken`): `releaseConfidence: 0/100`, `gaps: 6`
364
-
365
362
  ## MCP tools quick map
366
363
 
367
364
  | Tool | When to use | Key input |
368
365
  |---|---|---|
369
366
  | **`qulib_score_confidence`** | **Flagship.** Fused verdict (ship/caution/hold/block) from all collectors | `url` and/or `repoPath`, optional `includeViews.replay` |
370
- | `analyze_app` | Live-app QA scan: release confidence + gaps + a11y | `url`, optional `auth`, optional LLM knobs |
367
+ | `qulib_analyze_app` | Live-app QA scan: release confidence + gaps + a11y | `url`, optional `auth`, optional LLM knobs |
371
368
  | `qulib_score_automation` | Score local repo test-automation maturity | absolute `repoPath`, optional `includeFullDimensions` |
372
369
  | `qulib_score_api` | Discover API endpoints and score their test coverage | absolute `repoPath`, optional `enableTier3`, `includeEndpointDetail` |
373
- | `qulib_scaffold_tests` | Generate Cypress/Playwright scaffold from a live URL | `url`, optional `framework`, `maxPagesToScan`, `recipes` |
374
- | `explore_auth` | Deeper auth-path discovery on unfamiliar apps | `url`, optional `timeoutMs` |
375
- | `detect_auth` | Fast single-pass auth pattern guess | `url`, optional `timeoutMs` |
370
+ | `qulib_scaffold_tests` | Generate Cypress scaffold from a live URL (`cypress-e2e` only; playwright not yet implemented) | `url`, optional `framework`, `maxPagesToScan`, `recipes` |
371
+ | `qulib_explore_auth` | Deeper auth-path discovery on unfamiliar apps | `url`, optional `timeoutMs` |
372
+ | `qulib_detect_auth` | Fast single-pass auth pattern guess | `url`, optional `timeoutMs` |
373
+ | `analyze_app` | Legacy alias for `qulib_analyze_app` — kept for backwards compatibility | same as `qulib_analyze_app` |
374
+ | `explore_auth` | Legacy alias for `qulib_explore_auth` — kept for backwards compatibility | same as `qulib_explore_auth` |
375
+ | `detect_auth` | Legacy alias for `qulib_detect_auth` — kept for backwards compatibility | same as `qulib_detect_auth` |
376
376
 
377
377
  ## Output directories
378
378
 
@@ -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
+ }
@@ -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
+ }
@@ -1 +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"}
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"}
@@ -139,12 +139,16 @@ export async function runConfidence(options, out = (line) => console.log(line))
139
139
  return rc;
140
140
  }
141
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.
142
144
  program
143
145
  .command('confidence')
146
+ .alias('release-confidence')
144
147
  .description('Compute a fused Release Confidence verdict from qulib evidence collectors. ' +
145
148
  'Pass --url to include live-app quality + a11y + coverage evidence. ' +
146
149
  'Pass --repo to include test-automation maturity + API coverage. ' +
147
- 'Both may be combined.')
150
+ 'Both may be combined. ' +
151
+ '(Alias: release-confidence)')
148
152
  .option('--url <url>', 'URL of the deployed app to analyze')
149
153
  .option('--repo <path>', 'Path to the local repository to score')
150
154
  .option('--json', 'Emit the full ReleaseConfidence object as JSON to stdout', false)
package/dist/cli/index.js CHANGED
@@ -41,6 +41,8 @@ import { runAutomatedAuthLogin } from './auth-login-run.js';
41
41
  import { registerScaffoldCommand } from './scaffold-run.js';
42
42
  import { registerScoreAutomationCommand } from './score-automation-run.js';
43
43
  import { registerConfidenceCommand } from './confidence-run.js';
44
+ import { registerBaselineCommand } from './baseline-run.js';
45
+ import { registerAnalyzeDiffCommand } from './analyze-diff-run.js';
44
46
  const program = new Command();
45
47
  const AnalyzeUrlSchema = z.string().url();
46
48
  const FormLoginCliSchema = z.object({
@@ -207,6 +209,8 @@ program
207
209
  registerScaffoldCommand(program);
208
210
  registerScoreAutomationCommand(program);
209
211
  registerConfidenceCommand(program);
212
+ registerBaselineCommand(program);
213
+ registerAnalyzeDiffCommand(program);
210
214
  program
211
215
  .command('clean')
212
216
  .description('Remove all generated reports and scan state')
@@ -1 +1 @@
1
- {"version":3,"file":"score-automation-run.d.ts","sourceRoot":"","sources":["../../src/cli/score-automation-run.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EACV,kBAAkB,EAEnB,MAAM,0CAA0C,CAAC;AAIlD,MAAM,WAAW,sBAAsB;IACrC,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CAYnG;AA+BD,gGAAgG;AAChG,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAiBtE;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,sBAAsB,EAC/B,GAAG,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAkC,GACxD,OAAO,CAAC,kBAAkB,CAAC,CAW7B;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAWrE"}
1
+ {"version":3,"file":"score-automation-run.d.ts","sourceRoot":"","sources":["../../src/cli/score-automation-run.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EACV,kBAAkB,EAEnB,MAAM,0CAA0C,CAAC;AAIlD,MAAM,WAAW,sBAAsB;IACrC,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CAYnG;AA+BD,gGAAgG;AAChG,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAiBtE;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,sBAAsB,EAC/B,GAAG,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAkC,GACxD,OAAO,CAAC,kBAAkB,CAAC,CAW7B;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAerE"}
@@ -112,9 +112,13 @@ export async function runScoreAutomation(options, out = (line) => console.log(li
112
112
  return maturity;
113
113
  }
114
114
  export function registerScoreAutomationCommand(program) {
115
+ // Canonical name kept for backwards compatibility. Alias: automation-score
116
+ // (confidence-family naming — shorter and consistent with the qulib_ MCP convention).
115
117
  program
116
118
  .command('score-automation')
117
- .description("Score a local repo's test-automation maturity (overall + per-dimension, with honest applicability)")
119
+ .alias('automation-score')
120
+ .description("Score a local repo's test-automation maturity (overall + per-dimension, with honest applicability). " +
121
+ '(Alias: automation-score)')
118
122
  .requiredOption('--repo <path>', 'Path to the local repository to score')
119
123
  .option('--json', 'Emit the full AutomationMaturity object as JSON to stdout', false)
120
124
  .action(async (options) => {
package/dist/index.d.ts CHANGED
@@ -29,6 +29,10 @@ export type { CallLlmConfigSlice } from './llm/provider.js';
29
29
  export type { HarnessConfig, AuthConfig, RouteInventory, GapAnalysis, RepoAnalysis, DetectedAuth, AuthExploration, AuthPath, AuthPathRequirements, CostIntelligence, LlmUsageRecord, RepeatedAiPattern, DeterministicMaturity, PublicSurface, AutomationMaturity, AutomationMaturityDimension, FrameworkDetectionResult, DetectedFrameworkPrimary, RecipeId, RecipeConfig, } from './schemas/index.js';
30
30
  export { RecipeIdSchema } from './schemas/index.js';
31
31
  export { computeReleaseConfidence } from './tools/scoring/confidence.js';
32
+ export { analyzeRunDiff, formatAnalyzeDiffMarkdown, loadGapAnalysisFile } from './cli/analyze-diff-run.js';
33
+ export { buildPageHeatmap, renderHeatmapSection, HEATMAP_DIMENSIONS, DIMENSION_LABELS } from './reporters/heatmap.js';
34
+ export type { PageHeatmap, HeatmapRow, HeatmapCell, HeatmapDimension } from './reporters/heatmap.js';
35
+ export type { AnalyzeDiffResult } from './cli/analyze-diff-run.js';
32
36
  export { ciResultsToEvidence } from './adapters/ci-results-adapter.js';
33
37
  export type { CiRunInput } from './adapters/ci-results-adapter.js';
34
38
  export { prMetadataToEvidence } from './adapters/pr-metadata-adapter.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -18,6 +18,10 @@ export { redactUrlForTelemetry } from './telemetry/emit.js';
18
18
  export { RecipeIdSchema } from './schemas/index.js';
19
19
  // P3 — Confidence Layer exports
20
20
  export { computeReleaseConfidence } from './tools/scoring/confidence.js';
21
+ // analyze-diff — structured diff between two analyze_app outputs
22
+ export { analyzeRunDiff, formatAnalyzeDiffMarkdown, loadGapAnalysisFile } from './cli/analyze-diff-run.js';
23
+ // per-page coverage heatmap
24
+ export { buildPageHeatmap, renderHeatmapSection, HEATMAP_DIMENSIONS, DIMENSION_LABELS } from './reporters/heatmap.js';
21
25
  // P4 — Evidence adapters (CI results + PR metadata)
22
26
  export { ciResultsToEvidence } from './adapters/ci-results-adapter.js';
23
27
  export { prMetadataToEvidence } from './adapters/pr-metadata-adapter.js';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Per-page coverage heatmap for Markdown reports.
3
+ *
4
+ * buildPageHeatmap() is a pure function — no I/O, no side-effects.
5
+ * It derives a rows × dimensions matrix from GapAnalysis.gaps and sorts
6
+ * rows worst-first (most critical coverage problems bubble to the top).
7
+ *
8
+ * Dimensions map to the GapSchema.category enum values so the heatmap
9
+ * always stays in sync with what the scanner can produce.
10
+ */
11
+ import type { Gap } from '../schemas/gap-analysis.schema.js';
12
+ /** The ordered set of gap categories that appear as heatmap columns. */
13
+ export declare const HEATMAP_DIMENSIONS: readonly ["untested-route", "a11y", "console-error", "broken-link", "coverage", "untested-api-endpoint", "auth-surface"];
14
+ export type HeatmapDimension = (typeof HEATMAP_DIMENSIONS)[number];
15
+ /** Display labels for each dimension column header. */
16
+ export declare const DIMENSION_LABELS: Record<HeatmapDimension, string>;
17
+ /** One cell in the heatmap: the worst severity for that page × dimension. */
18
+ export type HeatmapCell = {
19
+ /** Worst Gap severity found, or null when no gap exists. */
20
+ worstSeverity: Gap['severity'] | null;
21
+ /** Glyph to render in Markdown. */
22
+ glyph: string;
23
+ /** Count of gaps contributing to this cell. */
24
+ count: number;
25
+ };
26
+ /** One row in the heatmap: a page path and its per-dimension cells. */
27
+ export type HeatmapRow = {
28
+ path: string;
29
+ /** Map from dimension to cell. Guaranteed to contain every HEATMAP_DIMENSION key. */
30
+ cells: Record<HeatmapDimension, HeatmapCell>;
31
+ /** Sum of severity scores across all cells — used for worst-first sort. */
32
+ worstScore: number;
33
+ };
34
+ /** The full heatmap structure returned by buildPageHeatmap(). */
35
+ export type PageHeatmap = {
36
+ /** Rows sorted worst-first (highest total severity score first). */
37
+ rows: HeatmapRow[];
38
+ /** Ordered dimension labels for use as column headers. */
39
+ dimensions: typeof HEATMAP_DIMENSIONS;
40
+ /** Total number of gaps that fed into the heatmap. */
41
+ totalGaps: number;
42
+ };
43
+ /**
44
+ * Build a per-page coverage heatmap from a flat list of gaps.
45
+ *
46
+ * @param gaps The gaps array from GapAnalysis.
47
+ * @returns A PageHeatmap with rows sorted worst-first.
48
+ */
49
+ export declare function buildPageHeatmap(gaps: Gap[]): PageHeatmap;
50
+ /**
51
+ * Render a PageHeatmap as a Markdown section string.
52
+ * Returns an empty string when there are no rows (nothing scanned).
53
+ */
54
+ export declare function renderHeatmapSection(heatmap: PageHeatmap): string;
55
+ //# sourceMappingURL=heatmap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heatmap.d.ts","sourceRoot":"","sources":["../../src/reporters/heatmap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,mCAAmC,CAAC;AAM7D,wEAAwE;AACxE,eAAO,MAAM,kBAAkB,0HAQoB,CAAC;AAEpD,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEnE,uDAAuD;AACvD,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAQ7D,CAAC;AAuBF,6EAA6E;AAC7E,MAAM,MAAM,WAAW,GAAG;IACxB,4DAA4D;IAC5D,aAAa,EAAE,GAAG,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;IACtC,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,uEAAuE;AACvE,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;IAC7C,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,iEAAiE;AACjE,MAAM,MAAM,WAAW,GAAG;IACxB,oEAAoE;IACpE,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,0DAA0D;IAC1D,UAAU,EAAE,OAAO,kBAAkB,CAAC;IACtC,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAMF;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,WAAW,CAuDzD;AAMD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAmCjE"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Per-page coverage heatmap for Markdown reports.
3
+ *
4
+ * buildPageHeatmap() is a pure function — no I/O, no side-effects.
5
+ * It derives a rows × dimensions matrix from GapAnalysis.gaps and sorts
6
+ * rows worst-first (most critical coverage problems bubble to the top).
7
+ *
8
+ * Dimensions map to the GapSchema.category enum values so the heatmap
9
+ * always stays in sync with what the scanner can produce.
10
+ */
11
+ // ---------------------------------------------------------------------------
12
+ // Types
13
+ // ---------------------------------------------------------------------------
14
+ /** The ordered set of gap categories that appear as heatmap columns. */
15
+ export const HEATMAP_DIMENSIONS = [
16
+ 'untested-route',
17
+ 'a11y',
18
+ 'console-error',
19
+ 'broken-link',
20
+ 'coverage',
21
+ 'untested-api-endpoint',
22
+ 'auth-surface',
23
+ ];
24
+ /** Display labels for each dimension column header. */
25
+ export const DIMENSION_LABELS = {
26
+ 'untested-route': 'Untested',
27
+ 'a11y': 'A11y',
28
+ 'console-error': 'Console',
29
+ 'broken-link': 'Links',
30
+ 'coverage': 'Coverage',
31
+ 'untested-api-endpoint': 'API',
32
+ 'auth-surface': 'Auth',
33
+ };
34
+ /** Severity order — higher index = worse. */
35
+ const SEVERITY_ORDER = {
36
+ low: 1,
37
+ medium: 2,
38
+ high: 3,
39
+ critical: 4,
40
+ };
41
+ /** Intensity glyph scale, indexed by SEVERITY_ORDER value (1..4). */
42
+ const SEVERITY_GLYPHS = {
43
+ 0: '·', // no gap on this page for this dimension
44
+ 1: '🟡', // low
45
+ 2: '🟠', // medium
46
+ 3: '🔴', // high
47
+ 4: '🚨', // critical
48
+ };
49
+ // ---------------------------------------------------------------------------
50
+ // Pure builder
51
+ // ---------------------------------------------------------------------------
52
+ /**
53
+ * Build a per-page coverage heatmap from a flat list of gaps.
54
+ *
55
+ * @param gaps The gaps array from GapAnalysis.
56
+ * @returns A PageHeatmap with rows sorted worst-first.
57
+ */
58
+ export function buildPageHeatmap(gaps) {
59
+ const pageMap = new Map();
60
+ for (const gap of gaps) {
61
+ // Only include dimensions the heatmap tracks; skip unknowns gracefully.
62
+ const dim = gap.category;
63
+ if (!HEATMAP_DIMENSIONS.includes(dim))
64
+ continue;
65
+ if (!pageMap.has(gap.path)) {
66
+ pageMap.set(gap.path, new Map());
67
+ }
68
+ const dimMap = pageMap.get(gap.path);
69
+ if (!dimMap.has(dim)) {
70
+ dimMap.set(dim, []);
71
+ }
72
+ dimMap.get(dim).push(gap);
73
+ }
74
+ const rows = [];
75
+ for (const [path, dimMap] of pageMap) {
76
+ const cells = {};
77
+ let worstScore = 0;
78
+ for (const dim of HEATMAP_DIMENSIONS) {
79
+ const dimGaps = dimMap.get(dim) ?? [];
80
+ if (dimGaps.length === 0) {
81
+ cells[dim] = { worstSeverity: null, glyph: SEVERITY_GLYPHS[0], count: 0 };
82
+ }
83
+ else {
84
+ let worst = 'low';
85
+ for (const g of dimGaps) {
86
+ if (SEVERITY_ORDER[g.severity] > SEVERITY_ORDER[worst]) {
87
+ worst = g.severity;
88
+ }
89
+ }
90
+ const score = SEVERITY_ORDER[worst];
91
+ worstScore += score;
92
+ cells[dim] = { worstSeverity: worst, glyph: SEVERITY_GLYPHS[score], count: dimGaps.length };
93
+ }
94
+ }
95
+ rows.push({ path, cells, worstScore });
96
+ }
97
+ // Sort worst-first (highest worstScore first), then alphabetically by path for stability.
98
+ rows.sort((a, b) => {
99
+ if (b.worstScore !== a.worstScore)
100
+ return b.worstScore - a.worstScore;
101
+ return a.path.localeCompare(b.path);
102
+ });
103
+ return {
104
+ rows,
105
+ dimensions: HEATMAP_DIMENSIONS,
106
+ totalGaps: gaps.length,
107
+ };
108
+ }
109
+ // ---------------------------------------------------------------------------
110
+ // Markdown renderer
111
+ // ---------------------------------------------------------------------------
112
+ /**
113
+ * Render a PageHeatmap as a Markdown section string.
114
+ * Returns an empty string when there are no rows (nothing scanned).
115
+ */
116
+ export function renderHeatmapSection(heatmap) {
117
+ if (heatmap.rows.length === 0) {
118
+ return '';
119
+ }
120
+ const dimLabels = heatmap.dimensions.map((d) => DIMENSION_LABELS[d]);
121
+ // Build table header
122
+ const header = `| Page | ${dimLabels.join(' | ')} |`;
123
+ const separator = `|------|${heatmap.dimensions.map(() => ':---:').join('|')}|`;
124
+ const tableRows = heatmap.rows
125
+ .map((row) => {
126
+ const cells = heatmap.dimensions.map((d) => row.cells[d].glyph).join(' | ');
127
+ return `| \`${row.path}\` | ${cells} |`;
128
+ })
129
+ .join('\n');
130
+ const legend = [
131
+ '**Legend:**',
132
+ `🚨 critical`,
133
+ `🔴 high`,
134
+ `🟠 medium`,
135
+ `🟡 low`,
136
+ `· none`,
137
+ ].join(' · ');
138
+ return `## Per-page coverage heatmap
139
+
140
+ ${header}
141
+ ${separator}
142
+ ${tableRows}
143
+
144
+ ${legend}
145
+ `;
146
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-reporter.d.ts","sourceRoot":"","sources":["../../src/reporters/markdown-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAErE,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkEjG"}
1
+ {"version":3,"file":"markdown-reporter.d.ts","sourceRoot":"","sources":["../../src/reporters/markdown-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAGrE,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqEjG"}
@@ -1,5 +1,6 @@
1
1
  import { writeFile, mkdir } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ import { buildPageHeatmap, renderHeatmapSection } from './heatmap.js';
3
4
  export async function writeMarkdownReport(analysis, outputDir) {
4
5
  await mkdir(outputDir, { recursive: true });
5
6
  const rc = analysis.releaseConfidence;
@@ -16,6 +17,8 @@ export async function writeMarkdownReport(analysis, outputDir) {
16
17
  const scenarioBlocks = analysis.scenarios
17
18
  .map((s) => `### ${s.title}\n${s.description}\n\nSteps:\n${s.steps.map((step) => `- ${step.description}`).join('\n')}\n\nRecommended adapters: ${s.recommendations.map((r) => r.adapter).join(', ')}`)
18
19
  .join('\n\n---\n\n');
20
+ const heatmap = buildPageHeatmap(analysis.gaps);
21
+ const heatmapSection = renderHeatmapSection(heatmap);
19
22
  const ci = analysis.costIntelligence;
20
23
  const costSection = ci
21
24
  ? `## Cost Intelligence
@@ -41,7 +44,7 @@ export async function writeMarkdownReport(analysis, outputDir) {
41
44
  - Scan budget exhausted (unfinished queue): ${analysis.coverageBudgetExceeded ? 'yes' : 'no'}
42
45
  ${analysis.coverageWarning ? `- Warning: **${analysis.coverageWarning}**` : '- Warning: none'}
43
46
 
44
- ${costSection}
47
+ ${heatmapSection}${costSection}
45
48
  ## Coverage gaps (${analysis.gaps.length})
46
49
 
47
50
  | Path | Category | Severity | Reason |
@@ -1 +1 @@
1
- {"version":3,"file":"config.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/config.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;AACpD,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,aAAa,GAAG,mBAAmB,GAAG,KAAK,GAAG,eAAe,CAAC;AAEvG,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBvB,CAAC;AAEH,QAAA,MAAM,sBAAsB;;;;;;;;;EAG1B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAA8E,CAAC;AAE5G,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACtE,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC5E,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0C9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAE9E;AAED,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAcrC,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASzB,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoB7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAehC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAC9E,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
1
+ {"version":3,"file":"config.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/config.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,SAAS,CAAC;AACpD,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,aAAa,GAAG,mBAAmB,GAAG,KAAK,GAAG,eAAe,CAAC;AAEvG,QAAA,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBvB,CAAC;AAEH,QAAA,MAAM,sBAAsB;;;;;;;;;EAG1B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAA8E,CAAC;AAE5G,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AACtE,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC5E,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiD9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,wBAAgB,gCAAgC,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAE9E;AAED,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAcrC,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASzB,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoB7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAehC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAC9E,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AACtD,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
@@ -46,7 +46,12 @@ export const HarnessConfigSchema = z.object({
46
46
  readOnlyMode: z.boolean(),
47
47
  requireHumanReview: z.boolean(),
48
48
  failOnConsoleError: z.boolean(),
49
- explorer: z.enum(['playwright', 'cypress']).default('playwright'),
49
+ explorer: z
50
+ .enum(['playwright', 'cypress'])
51
+ .default('playwright')
52
+ .describe("Browser explorer to use. 'playwright' is the production explorer. " +
53
+ "'cypress' is reserved for future Cypress-driven exploration and is not yet implemented — " +
54
+ 'it will throw at runtime. Always use playwright in production.'),
50
55
  defaultAdapter: z.enum(['playwright', 'cypress-e2e', 'cypress-component', 'api', 'accessibility']).default('playwright'),
51
56
  adapters: z.array(z.enum(['playwright', 'cypress-e2e', 'cypress-component', 'api', 'accessibility'])).default(['playwright']),
52
57
  llmProvider: z.enum(['anthropic']).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qulib/core",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Qulib — release confidence for deployed web apps. Fuses live-app quality, automation maturity, and API coverage into a single ship/caution/hold/block verdict.",
5
5
  "license": "MIT",
6
6
  "author": "Tapesh Nagarwal",
@@ -23,7 +23,11 @@
23
23
  "accessibility",
24
24
  "playwright",
25
25
  "mcp",
26
- "ai"
26
+ "ai",
27
+ "ci-gate",
28
+ "test-confidence",
29
+ "web-quality",
30
+ "wcag"
27
31
  ],
28
32
  "publishConfig": {
29
33
  "access": "public"
@@ -52,7 +56,7 @@
52
56
  "build": "tsc",
53
57
  "prepack": "npm run build",
54
58
  "prepublishOnly": "npm run build",
55
- "test": "node --import tsx/esm --test src/llm/__tests__/cost-intelligence.test.ts src/llm/__tests__/context-builder.test.ts src/tools/scoring/__tests__/gaps.test.ts src/tools/auth/__tests__/gaps.test.ts src/tools/auth/__tests__/detect.test.ts src/tools/scoring/__tests__/automation-maturity.test.ts src/tools/scoring/__tests__/api-coverage.test.ts src/tools/scoring/__tests__/automation-maturity-with-api.test.ts src/harness/__tests__/state-manager.test.ts src/telemetry/__tests__/redact-url.test.ts src/cli/__tests__/auth-login.test.ts src/cli/__tests__/cli-version.test.ts src/cli/__tests__/bin-shim.test.ts src/cli/__tests__/score-automation.test.ts src/cli/__tests__/scaffold.test.ts src/__tests__/agent-summary.test.ts src/__tests__/cli-agent-summary.test.ts src/__tests__/analyze.storage-state-invalid.test.ts src/__tests__/analyze.fixtures.test.ts src/adapters/__tests__/playwright-adapter.test.ts src/adapters/__tests__/api-adapter.test.ts src/adapters/__tests__/ci-results-adapter.test.ts src/adapters/__tests__/pr-metadata-adapter.test.ts src/adapters/__tests__/validate-specs.test.ts src/tools/repo/__tests__/api-surface.test.ts src/baseline/__tests__/baseline.test.ts evals/runner/__tests__/runner.test.ts evals/judge/__tests__/judge.test.ts src/tools/scoring/__tests__/confidence.test.ts src/tools/scoring/__tests__/confidence-from-qulib.test.ts src/tools/scoring/__tests__/confidence-views.test.ts src/cli/__tests__/confidence.test.ts src/__tests__/notquality-dogfood.test.ts src/cli/__tests__/default-config-fallback.test.ts",
59
+ "test": "node --import tsx/esm --test src/llm/__tests__/cost-intelligence.test.ts src/llm/__tests__/context-builder.test.ts src/tools/scoring/__tests__/gaps.test.ts src/tools/auth/__tests__/gaps.test.ts src/tools/auth/__tests__/detect.test.ts src/tools/scoring/__tests__/automation-maturity.test.ts src/tools/scoring/__tests__/api-coverage.test.ts src/tools/scoring/__tests__/automation-maturity-with-api.test.ts src/harness/__tests__/state-manager.test.ts src/telemetry/__tests__/redact-url.test.ts src/cli/__tests__/auth-login.test.ts src/cli/__tests__/cli-version.test.ts src/cli/__tests__/bin-shim.test.ts src/cli/__tests__/score-automation.test.ts src/cli/__tests__/scaffold.test.ts src/__tests__/agent-summary.test.ts src/__tests__/cli-agent-summary.test.ts src/__tests__/analyze.storage-state-invalid.test.ts src/__tests__/analyze.fixtures.test.ts src/adapters/__tests__/playwright-adapter.test.ts src/adapters/__tests__/api-adapter.test.ts src/adapters/__tests__/ci-results-adapter.test.ts src/adapters/__tests__/pr-metadata-adapter.test.ts src/adapters/__tests__/validate-specs.test.ts src/tools/repo/__tests__/api-surface.test.ts src/baseline/__tests__/baseline.test.ts evals/runner/__tests__/runner.test.ts evals/judge/__tests__/judge.test.ts src/tools/scoring/__tests__/confidence.test.ts src/tools/scoring/__tests__/confidence-from-qulib.test.ts src/tools/scoring/__tests__/confidence-views.test.ts src/cli/__tests__/confidence.test.ts src/__tests__/notquality-dogfood.test.ts src/cli/__tests__/default-config-fallback.test.ts src/cli/__tests__/baseline.test.ts src/cli/__tests__/naming-aliases.test.ts src/cli/__tests__/analyze-diff.test.ts src/reporters/__tests__/heatmap.test.ts",
56
60
  "test:integration": "node --import tsx/esm --test src/__tests__/analyze.integration.test.ts",
57
61
  "eval": "node --import tsx/esm evals/runner/index.ts",
58
62
  "eval:judge": "node --import tsx/esm evals/judge/eval-judge.ts",
@@ -71,6 +75,6 @@
71
75
  "devDependencies": {
72
76
  "@types/js-yaml": "^4.0.9",
73
77
  "@types/node": "^20.0.0",
74
- "tsx": "^4.11.0"
78
+ "tsx": "^4.22.4"
75
79
  }
76
80
  }