@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 +11 -11
- package/dist/cli/analyze-diff-run.d.ts +77 -0
- package/dist/cli/analyze-diff-run.d.ts.map +1 -0
- package/dist/cli/analyze-diff-run.js +266 -0
- package/dist/cli/baseline-run.d.ts +55 -0
- package/dist/cli/baseline-run.d.ts.map +1 -0
- package/dist/cli/baseline-run.js +259 -0
- package/dist/cli/confidence-run.d.ts.map +1 -1
- package/dist/cli/confidence-run.js +5 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/score-automation-run.d.ts.map +1 -1
- package/dist/cli/score-automation-run.js +5 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/reporters/heatmap.d.ts +55 -0
- package/dist/reporters/heatmap.d.ts.map +1 -0
- package/dist/reporters/heatmap.js +146 -0
- package/dist/reporters/markdown-reporter.d.ts.map +1 -1
- package/dist/reporters/markdown-reporter.js +4 -1
- package/dist/schemas/config.schema.d.ts.map +1 -1
- package/dist/schemas/config.schema.js +6 -1
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -345,34 +345,34 @@ qulib analyze --url https://yourapp.com --auth-storage-state ./qulib-storage-sta
|
|
|
345
345
|
|
|
346
346
|
## Sample report (fixture baseline)
|
|
347
347
|
|
|
348
|
-
|
|
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
|
-
"...
|
|
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
|
-
| `
|
|
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
|
|
374
|
-
| `
|
|
375
|
-
| `
|
|
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,
|
|
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,
|
|
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
|
-
.
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,UAAU,EACV,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,cAAc,EACd,gBAAgB,GACjB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,GACd,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,SAAS,EACT,cAAc,EACd,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,4BAA4B,EAC5B,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAC1G,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AAC7F,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AAC7G,OAAO,EAAE,yBAAyB,EAAE,MAAM,wCAAwC,CAAC;AACnF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAC9F,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAClI,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACvF,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjF,YAAY,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,YAAY,EACV,aAAa,EACb,cAAc,EACd,kBAAkB,GACnB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC9E,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,YAAY,EACV,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,aAAa,EACb,kBAAkB,EAClB,2BAA2B,EAC3B,wBAAwB,EACxB,wBAAwB,EACxB,QAAQ,EACR,YAAY,GACb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAEzE,OAAO,EAAE,cAAc,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAE3G,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACtH,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACrG,YAAY,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAEnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,YAAY,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AACtH,OAAO,EAAE,6BAA6B,EAAE,MAAM,0CAA0C,CAAC;AACzF,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAC7G,YAAY,EACV,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,SAAS,EACT,aAAa,EACb,UAAU,EACV,WAAW,EACX,UAAU,GACX,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,uBAAuB,EACvB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,oBAAoB,CAAC"}
|
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;
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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
|
|
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.
|
|
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.
|
|
78
|
+
"tsx": "^4.22.4"
|
|
75
79
|
}
|
|
76
80
|
}
|