@qulib/core 0.4.3 → 0.5.1
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 +101 -0
- package/dist/__tests__/cli-smoke-fixture.d.ts +2 -0
- package/dist/__tests__/cli-smoke-fixture.d.ts.map +1 -0
- package/dist/__tests__/cli-smoke-fixture.js +58 -0
- package/dist/__tests__/fixture-server.d.ts +6 -0
- package/dist/__tests__/fixture-server.d.ts.map +1 -0
- package/dist/__tests__/fixture-server.js +141 -0
- package/dist/cli/auth-login-resolve.js +1 -1
- package/dist/cli/auth-login-run.d.ts.map +1 -1
- package/dist/cli/auth-login-run.js +3 -1
- package/dist/cli/index.js +3 -0
- package/dist/llm/providers/anthropic.js +1 -1
- package/dist/phases/think-finalize.d.ts.map +1 -1
- package/dist/phases/think-finalize.js +7 -1
- package/dist/schemas/automation-maturity.schema.d.ts +8 -0
- package/dist/schemas/automation-maturity.schema.d.ts.map +1 -1
- package/dist/schemas/automation-maturity.schema.js +1 -0
- package/dist/schemas/repo-analysis.schema.d.ts +7 -0
- package/dist/schemas/repo-analysis.schema.d.ts.map +1 -1
- package/dist/tools/auth/detect.d.ts.map +1 -1
- package/dist/tools/auth/detect.js +3 -2
- package/dist/tools/auth/explore.d.ts.map +1 -1
- package/dist/tools/auth/explore.js +52 -0
- package/dist/tools/scoring/automation-maturity.d.ts.map +1 -1
- package/dist/tools/scoring/automation-maturity.js +12 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -277,6 +277,107 @@ npm run analyze -- --url https://example.com --ephemeral > report.bundle.json
|
|
|
277
277
|
npm run clean
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
+
## Minimum config
|
|
281
|
+
|
|
282
|
+
Smallest legal `qulib.config.ts`:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import type { HarnessConfig } from './src/schemas/config.schema.js';
|
|
286
|
+
|
|
287
|
+
const config: HarnessConfig = {
|
|
288
|
+
maxPagesToScan: 20,
|
|
289
|
+
maxDepth: 3,
|
|
290
|
+
timeoutMs: 30000,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export default config;
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
All other fields inherit from schema defaults or CLI/runtime defaults.
|
|
297
|
+
|
|
298
|
+
## Scan walkthroughs (copy-paste)
|
|
299
|
+
|
|
300
|
+
### 1) Public scan
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
npx @qulib/core analyze --url https://yourapp.com
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### 2) Auth-blocked scan (honest blocked mode)
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
npx @qulib/core analyze --url https://yourapp.com/auth
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
When auth blocks access and no auth config is supplied, Qulib reports `status: "blocked"` (or `partial` if it could still crawl some public pages). This is intentional honesty, not a failure mode.
|
|
313
|
+
|
|
314
|
+
### 3) Authenticated scan with storage state
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
# Capture once (manual OAuth/SSO-safe flow)
|
|
318
|
+
qulib auth init --base-url https://yourapp.com
|
|
319
|
+
|
|
320
|
+
# Reuse saved session
|
|
321
|
+
qulib analyze --url https://yourapp.com --auth-storage-state ./qulib-storage-state.json
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Sample report (fixture baseline)
|
|
325
|
+
|
|
326
|
+
From the local fixture baseline used in v0.5.0 PR 1/2:
|
|
327
|
+
|
|
328
|
+
```json
|
|
329
|
+
{
|
|
330
|
+
"status": "complete",
|
|
331
|
+
"releaseConfidence": 68,
|
|
332
|
+
"gaps": [
|
|
333
|
+
"... 4 total gap items ..."
|
|
334
|
+
]
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Use these as conservative reference numbers:
|
|
339
|
+
- public fixture (`/`): `releaseConfidence: 68/100`, `gaps: 4`
|
|
340
|
+
- auth-wall fixture (`/auth`): `releaseConfidence: 24/100`, `gaps: 2`
|
|
341
|
+
- broken fixture (`/broken`): `releaseConfidence: 0/100`, `gaps: 6`
|
|
342
|
+
|
|
343
|
+
## MCP tools quick map
|
|
344
|
+
|
|
345
|
+
| Tool | When to use | Key input |
|
|
346
|
+
|---|---|---|
|
|
347
|
+
| `analyze_app` | Main QA scan for release confidence + gaps | `url`, optional `auth`, optional LLM knobs |
|
|
348
|
+
| `detect_auth` | Fast single-pass auth pattern guess | `url`, optional `timeoutMs` |
|
|
349
|
+
| `explore_auth` | Deeper auth-path discovery on unfamiliar apps | `url`, optional `timeoutMs` |
|
|
350
|
+
| `qulib_score_automation` | Score local repo automation maturity | absolute `repoPath`, optional `includeFullDimensions` |
|
|
351
|
+
|
|
352
|
+
## Output directories
|
|
353
|
+
|
|
354
|
+
Qulib writes runtime artifacts to:
|
|
355
|
+
|
|
356
|
+
- `.scan-state/` — intermediate state (discovered routes, gap analysis snapshots, decision log)
|
|
357
|
+
- `output/` — final `report.json` and `report.md`
|
|
358
|
+
|
|
359
|
+
Both are gitignored and safe to delete; Qulib recreates them on the next non-ephemeral run.
|
|
360
|
+
|
|
361
|
+
## ANTHROPIC_API_KEY (LLM scenarios)
|
|
362
|
+
|
|
363
|
+
For MCP-hosted usage, set `ANTHROPIC_API_KEY` in your host's `env` block:
|
|
364
|
+
|
|
365
|
+
```json
|
|
366
|
+
{
|
|
367
|
+
"mcpServers": {
|
|
368
|
+
"qulib": {
|
|
369
|
+
"command": "npx",
|
|
370
|
+
"args": ["@qulib/mcp"],
|
|
371
|
+
"env": {
|
|
372
|
+
"ANTHROPIC_API_KEY": "sk-ant-..."
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Without this key, Qulib still runs deterministic checks (crawl, a11y, links, console, scoring) and falls back to template scenarios instead of LLM-generated ones.
|
|
380
|
+
|
|
280
381
|
## Playwright browsers
|
|
281
382
|
|
|
282
383
|
```bash
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-smoke-fixture.d.ts","sourceRoot":"","sources":["../../src/__tests__/cli-smoke-fixture.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline CLI smoke: spawn `node bin/qulib.js analyze --url <fixture>` against
|
|
3
|
+
* the local fixture server and assert the CLI exited 0. Runnable script (not a
|
|
4
|
+
* node:test file) — invoked in CI by `node --import tsx/esm src/__tests__/cli-smoke-fixture.ts`.
|
|
5
|
+
*
|
|
6
|
+
* Removes the live `https://example.com` dependency from CI's smoke-test-cli job.
|
|
7
|
+
*
|
|
8
|
+
* The fixture server runs in this process; the CLI is spawned as a child. We use
|
|
9
|
+
* async `spawn` (not `spawnSync`) so the parent event loop stays free to serve
|
|
10
|
+
* the child's HTTP requests against the fixture.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { dirname, resolve } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { startFixtureServer } from './fixture-server.js';
|
|
16
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const cliPath = resolve(__dir, '../../bin/qulib.js');
|
|
18
|
+
function runCli(url) {
|
|
19
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
20
|
+
const child = spawn('node', [cliPath, 'analyze', '--url', url, '--ephemeral'], {
|
|
21
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
22
|
+
});
|
|
23
|
+
const stdoutChunks = [];
|
|
24
|
+
const stderrChunks = [];
|
|
25
|
+
child.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
|
|
26
|
+
child.stderr.on('data', (chunk) => stderrChunks.push(chunk));
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
child.kill('SIGKILL');
|
|
29
|
+
rejectPromise(new Error('CLI smoke timed out after 120s'));
|
|
30
|
+
}, 120_000);
|
|
31
|
+
child.on('error', (err) => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
rejectPromise(err);
|
|
34
|
+
});
|
|
35
|
+
child.on('close', (code) => {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
resolvePromise({
|
|
38
|
+
exitCode: code ?? -1,
|
|
39
|
+
stdout: Buffer.concat(stdoutChunks).toString('utf8'),
|
|
40
|
+
stderr: Buffer.concat(stderrChunks).toString('utf8'),
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function assertCliPassed(result) {
|
|
46
|
+
if (result.exitCode !== 0) {
|
|
47
|
+
throw new Error(`CLI exited with code ${result.exitCode}\n--- stdout ---\n${result.stdout}\n--- stderr ---\n${result.stderr}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const handle = await startFixtureServer();
|
|
51
|
+
try {
|
|
52
|
+
const result = await runCli(`${handle.baseUrl}/`);
|
|
53
|
+
assertCliPassed(result);
|
|
54
|
+
console.log('[cli-smoke] ✔ CLI exited 0 against fixture public surface');
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await handle.close();
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture-server.d.ts","sourceRoot":"","sources":["../../src/__tests__/fixture-server.ts"],"names":[],"mappings":"AAeA,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B;AAyGD,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,CAAC,CA0CvE"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Node.js fixture server for offline Qulib integration tests.
|
|
3
|
+
*
|
|
4
|
+
* Serves the static HTML files in `packages/core/fixtures/` over loopback so
|
|
5
|
+
* the test suite never depends on a live website. Used by
|
|
6
|
+
* `analyze.fixtures.test.ts` and any future offline integration coverage.
|
|
7
|
+
*
|
|
8
|
+
* Never imported by product code. Helpers are private to this module; only
|
|
9
|
+
* `startFixtureServer` and `FixtureServerHandle` are exported.
|
|
10
|
+
*/
|
|
11
|
+
import { createServer } from 'node:http';
|
|
12
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
13
|
+
import { dirname, join, resolve } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
const HTML_CONTENT_TYPE = 'text/html; charset=utf-8';
|
|
16
|
+
const TEXT_CONTENT_TYPE = 'text/plain; charset=utf-8';
|
|
17
|
+
function resolveFixturesDir() {
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
return resolve(here, '../../fixtures');
|
|
20
|
+
}
|
|
21
|
+
function routeToFile(pathname) {
|
|
22
|
+
if (pathname.includes('..'))
|
|
23
|
+
return null;
|
|
24
|
+
const fixturesDir = resolveFixturesDir();
|
|
25
|
+
if (pathname === '/' || pathname === '/index.html') {
|
|
26
|
+
return join(fixturesDir, 'public/index.html');
|
|
27
|
+
}
|
|
28
|
+
if (pathname === '/about') {
|
|
29
|
+
return join(fixturesDir, 'public/about.html');
|
|
30
|
+
}
|
|
31
|
+
if (pathname === '/features') {
|
|
32
|
+
return join(fixturesDir, 'public/features.html');
|
|
33
|
+
}
|
|
34
|
+
if (pathname === '/docs') {
|
|
35
|
+
return join(fixturesDir, 'public/index.html');
|
|
36
|
+
}
|
|
37
|
+
if (pathname === '/auth') {
|
|
38
|
+
return join(fixturesDir, 'auth-wall/index.html');
|
|
39
|
+
}
|
|
40
|
+
if (pathname === '/authenticated' || pathname.startsWith('/authenticated/')) {
|
|
41
|
+
return join(fixturesDir, 'authenticated/index.html');
|
|
42
|
+
}
|
|
43
|
+
if (pathname === '/broken') {
|
|
44
|
+
return join(fixturesDir, 'broken/index.html');
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
async function readFixture(filePath) {
|
|
49
|
+
return readFile(filePath);
|
|
50
|
+
}
|
|
51
|
+
function respond(res, status, body, contentType) {
|
|
52
|
+
const buf = typeof body === 'string' ? Buffer.from(body, 'utf8') : body;
|
|
53
|
+
res.writeHead(status, {
|
|
54
|
+
'Content-Type': contentType,
|
|
55
|
+
'Content-Length': buf.length,
|
|
56
|
+
'Cache-Control': 'no-store',
|
|
57
|
+
});
|
|
58
|
+
res.end(buf);
|
|
59
|
+
}
|
|
60
|
+
function respondNotFound(res) {
|
|
61
|
+
respond(res, 404, 'Not found', TEXT_CONTENT_TYPE);
|
|
62
|
+
}
|
|
63
|
+
function respondServerError(res, message) {
|
|
64
|
+
respond(res, 500, `Fixture server error: ${message}`, TEXT_CONTENT_TYPE);
|
|
65
|
+
}
|
|
66
|
+
function respondMethodNotAllowed(res) {
|
|
67
|
+
res.writeHead(405, {
|
|
68
|
+
Allow: 'GET',
|
|
69
|
+
'Content-Type': TEXT_CONTENT_TYPE,
|
|
70
|
+
'Cache-Control': 'no-store',
|
|
71
|
+
});
|
|
72
|
+
res.end('Method Not Allowed');
|
|
73
|
+
}
|
|
74
|
+
async function handleRequest(req, res) {
|
|
75
|
+
try {
|
|
76
|
+
if (req.method !== 'GET') {
|
|
77
|
+
respondMethodNotAllowed(res);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const rawUrl = req.url ?? '/';
|
|
81
|
+
let pathname;
|
|
82
|
+
try {
|
|
83
|
+
pathname = new URL(rawUrl, 'http://127.0.0.1').pathname;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
respondNotFound(res);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const filePath = routeToFile(pathname);
|
|
90
|
+
if (filePath === null) {
|
|
91
|
+
respondNotFound(res);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const body = await readFixture(filePath);
|
|
95
|
+
respond(res, 200, body, HTML_CONTENT_TYPE);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
+
respondServerError(res, message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export async function startFixtureServer() {
|
|
103
|
+
const fixturesDir = resolveFixturesDir();
|
|
104
|
+
try {
|
|
105
|
+
const s = await stat(fixturesDir);
|
|
106
|
+
if (!s.isDirectory()) {
|
|
107
|
+
throw new Error(`fixtures path is not a directory: ${fixturesDir}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
112
|
+
throw new Error(`Fixture directory not found at ${fixturesDir}: ${detail}`);
|
|
113
|
+
}
|
|
114
|
+
const server = createServer((req, res) => {
|
|
115
|
+
void handleRequest(req, res);
|
|
116
|
+
});
|
|
117
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
118
|
+
server.once('error', rejectPromise);
|
|
119
|
+
server.listen(0, '127.0.0.1', () => {
|
|
120
|
+
server.off('error', rejectPromise);
|
|
121
|
+
resolvePromise();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
const address = server.address();
|
|
125
|
+
if (address === null || typeof address === 'string') {
|
|
126
|
+
server.close();
|
|
127
|
+
throw new Error('Fixture server did not return a usable address after listen');
|
|
128
|
+
}
|
|
129
|
+
const baseUrl = `http://127.0.0.1:${address.port}`;
|
|
130
|
+
return {
|
|
131
|
+
baseUrl,
|
|
132
|
+
close: () => new Promise((resolvePromise, rejectPromise) => {
|
|
133
|
+
server.close((err) => {
|
|
134
|
+
if (err)
|
|
135
|
+
rejectPromise(err);
|
|
136
|
+
else
|
|
137
|
+
resolvePromise();
|
|
138
|
+
});
|
|
139
|
+
}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -29,7 +29,7 @@ export function parseCredentialsJsonString(json) {
|
|
|
29
29
|
return out;
|
|
30
30
|
}
|
|
31
31
|
export function resolveFormLoginPath(baseUrl, authOptions, authPathId) {
|
|
32
|
-
const formPaths = (authOptions ?? []).filter((o) => o.type === 'form-login' && o.requirements.method === 'credentials');
|
|
32
|
+
const formPaths = (authOptions ?? []).filter((o) => (o.type === 'form-login' || o.type === 'form-multi') && o.requirements.method === 'credentials');
|
|
33
33
|
if (formPaths.length === 0) {
|
|
34
34
|
throw new Error(`No automatable form-login path detected on ${baseUrl}. Use \`qulib auth init\` for manual login.`);
|
|
35
35
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-login-run.d.ts","sourceRoot":"","sources":["../../src/cli/auth-login-run.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAsB5D,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"auth-login-run.d.ts","sourceRoot":"","sources":["../../src/cli/auth-login-run.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AAsB5D,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAMhE;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,QAAQ,CAAC;IACf,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoIhB"}
|
|
@@ -16,7 +16,9 @@ function sleep(ms) {
|
|
|
16
16
|
return new Promise((r) => setTimeout(r, ms));
|
|
17
17
|
}
|
|
18
18
|
export function authPathNeedsClickReveal(path) {
|
|
19
|
-
return path.type === 'form-login'
|
|
19
|
+
return ((path.type === 'form-login' || path.type === 'form-multi') &&
|
|
20
|
+
(path.source === 'heuristic' || path.source === 'user-local') &&
|
|
21
|
+
!builtInOAuthIds.has(path.id));
|
|
20
22
|
}
|
|
21
23
|
export async function runAutomatedAuthLogin(params) {
|
|
22
24
|
const { chromium } = await import('@playwright/test');
|
package/dist/cli/index.js
CHANGED
|
@@ -98,6 +98,7 @@ async function runAnalyze(options) {
|
|
|
98
98
|
repoPath: options.repo,
|
|
99
99
|
config,
|
|
100
100
|
writeArtifacts,
|
|
101
|
+
skipAuthDetection: options.skipAuthDetection,
|
|
101
102
|
});
|
|
102
103
|
if (ephemeral) {
|
|
103
104
|
console.log(JSON.stringify({
|
|
@@ -157,6 +158,7 @@ program
|
|
|
157
158
|
.option('--config <file>', 'Path to config file (relative to cwd)', 'qulib.config.ts')
|
|
158
159
|
.option('--adapter <type>', 'Override default test adapter (playwright, cypress-e2e, cypress-component, api)', 'playwright')
|
|
159
160
|
.option('--ephemeral', 'Do not write to disk — return full report as JSON on stdout (use for MCP/CI)', false)
|
|
161
|
+
.option('--skip-auth-detection', 'Crawl the public surface even if auth is detected (useful for sites with sign-in CTAs on public pages)', false)
|
|
160
162
|
.option('--auth-storage-state <path>', 'Path to a storage state JSON file (use after `qulib auth init`)')
|
|
161
163
|
.option('--auth-form-login', 'Use form-login; requires --login-url, credentials, and selectors', false)
|
|
162
164
|
.option('--login-url <url>', 'Form login page URL (required with --auth-form-login)')
|
|
@@ -179,6 +181,7 @@ program
|
|
|
179
181
|
repo: options.repo,
|
|
180
182
|
configFile: options.config,
|
|
181
183
|
ephemeral: options.ephemeral,
|
|
184
|
+
skipAuthDetection: Boolean(options.skipAuthDetection),
|
|
182
185
|
authStorageState: options.authStorageState,
|
|
183
186
|
authFormLogin,
|
|
184
187
|
loginUrl,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"think-finalize.d.ts","sourceRoot":"","sources":["../../src/phases/think-finalize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACnG,OAAO,EAA4C,KAAK,WAAW,EAAwB,MAAM,mCAAmC,CAAC;AAQrI,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,gBAAgB,GAAG,kBAAkB,CAAC,CAAC;AAEtG,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,aAAa,EACrB,SAAS,GAAE,mBAA8C,EACzD,WAAW,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,sBAAsB,GAAG,mBAAmB,GAAG,MAAM,CAAC,GAC9F,OAAO,CAAC,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"think-finalize.d.ts","sourceRoot":"","sources":["../../src/phases/think-finalize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACnG,OAAO,EAA4C,KAAK,WAAW,EAAwB,MAAM,mCAAmC,CAAC;AAQrI,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,gBAAgB,GAAG,kBAAkB,CAAC,CAAC;AAEtG,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,aAAa,EACrB,SAAS,GAAE,mBAA8C,EACzD,WAAW,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,sBAAsB,GAAG,mBAAmB,GAAG,MAAM,CAAC,GAC9F,OAAO,CAAC,WAAW,CAAC,CAuLtB"}
|
|
@@ -87,7 +87,13 @@ export async function finalizeGapAnalysisFromDraft(draft, config, artifacts = {
|
|
|
87
87
|
: undefined,
|
|
88
88
|
});
|
|
89
89
|
try {
|
|
90
|
-
|
|
90
|
+
// Claude 4 models wrap JSON in markdown fences despite instructions.
|
|
91
|
+
// Strip ```json ... ``` or ``` ... ``` before parsing.
|
|
92
|
+
const rawText = llmResult.text.trim();
|
|
93
|
+
const stripped = rawText.replace(/^```(?:json)?\s*\n?/i, '').replace(/\n?```\s*$/i, '').trim();
|
|
94
|
+
// Also handle models that embed the array mid-prose — grab first [...] block
|
|
95
|
+
const jsonText = stripped.startsWith('[') ? stripped : (stripped.match(/\[[\s\S]*\]/)?.[0] ?? stripped);
|
|
96
|
+
const parsed = JSON.parse(jsonText);
|
|
91
97
|
const candidates = Array.isArray(parsed) ? parsed : [];
|
|
92
98
|
for (const item of candidates) {
|
|
93
99
|
const validated = NeutralScenarioSchema.safeParse(item);
|
|
@@ -27,6 +27,7 @@ export declare const AutomationMaturityDimensionSchema: z.ZodObject<{
|
|
|
27
27
|
recommendations: z.ZodArray<z.ZodString, "many">;
|
|
28
28
|
applicability: z.ZodOptional<z.ZodEnum<["applicable", "not_applicable", "unknown"]>>;
|
|
29
29
|
reason: z.ZodOptional<z.ZodString>;
|
|
30
|
+
guidance: z.ZodOptional<z.ZodString>;
|
|
30
31
|
}, "strip", z.ZodTypeAny, {
|
|
31
32
|
recommendations: string[];
|
|
32
33
|
dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
|
|
@@ -35,6 +36,7 @@ export declare const AutomationMaturityDimensionSchema: z.ZodObject<{
|
|
|
35
36
|
evidence: string[];
|
|
36
37
|
reason?: string | undefined;
|
|
37
38
|
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
39
|
+
guidance?: string | undefined;
|
|
38
40
|
}, {
|
|
39
41
|
recommendations: string[];
|
|
40
42
|
dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
|
|
@@ -43,6 +45,7 @@ export declare const AutomationMaturityDimensionSchema: z.ZodObject<{
|
|
|
43
45
|
evidence: string[];
|
|
44
46
|
reason?: string | undefined;
|
|
45
47
|
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
48
|
+
guidance?: string | undefined;
|
|
46
49
|
}>;
|
|
47
50
|
export declare const AutomationMaturitySchema: z.ZodObject<{
|
|
48
51
|
computedAt: z.ZodString;
|
|
@@ -58,6 +61,7 @@ export declare const AutomationMaturitySchema: z.ZodObject<{
|
|
|
58
61
|
recommendations: z.ZodArray<z.ZodString, "many">;
|
|
59
62
|
applicability: z.ZodOptional<z.ZodEnum<["applicable", "not_applicable", "unknown"]>>;
|
|
60
63
|
reason: z.ZodOptional<z.ZodString>;
|
|
64
|
+
guidance: z.ZodOptional<z.ZodString>;
|
|
61
65
|
}, "strip", z.ZodTypeAny, {
|
|
62
66
|
recommendations: string[];
|
|
63
67
|
dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
|
|
@@ -66,6 +70,7 @@ export declare const AutomationMaturitySchema: z.ZodObject<{
|
|
|
66
70
|
evidence: string[];
|
|
67
71
|
reason?: string | undefined;
|
|
68
72
|
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
73
|
+
guidance?: string | undefined;
|
|
69
74
|
}, {
|
|
70
75
|
recommendations: string[];
|
|
71
76
|
dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
|
|
@@ -74,6 +79,7 @@ export declare const AutomationMaturitySchema: z.ZodObject<{
|
|
|
74
79
|
evidence: string[];
|
|
75
80
|
reason?: string | undefined;
|
|
76
81
|
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
82
|
+
guidance?: string | undefined;
|
|
77
83
|
}>, "many">;
|
|
78
84
|
topRecommendations: z.ZodArray<z.ZodString, "many">;
|
|
79
85
|
scoreFormula: z.ZodOptional<z.ZodString>;
|
|
@@ -91,6 +97,7 @@ export declare const AutomationMaturitySchema: z.ZodObject<{
|
|
|
91
97
|
evidence: string[];
|
|
92
98
|
reason?: string | undefined;
|
|
93
99
|
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
100
|
+
guidance?: string | undefined;
|
|
94
101
|
}[];
|
|
95
102
|
topRecommendations: string[];
|
|
96
103
|
scoreFormula?: string | undefined;
|
|
@@ -108,6 +115,7 @@ export declare const AutomationMaturitySchema: z.ZodObject<{
|
|
|
108
115
|
evidence: string[];
|
|
109
116
|
reason?: string | undefined;
|
|
110
117
|
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
118
|
+
guidance?: string | undefined;
|
|
111
119
|
}[];
|
|
112
120
|
topRecommendations: string[];
|
|
113
121
|
scoreFormula?: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"automation-maturity.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/automation-maturity.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,qCAAqC,wDAIhD,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,iCAAiC
|
|
1
|
+
{"version":3,"file":"automation-maturity.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/automation-maturity.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,qCAAqC,wDAIhD,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgB5C,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASnC,CAAC;AAEH,MAAM,MAAM,+BAA+B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qCAAqC,CAAC,CAAC;AACpG,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAC5F,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
|
|
@@ -38,6 +38,7 @@ export const AutomationMaturityDimensionSchema = z.object({
|
|
|
38
38
|
recommendations: z.array(z.string()),
|
|
39
39
|
applicability: AutomationMaturityApplicabilitySchema.optional(),
|
|
40
40
|
reason: z.string().optional(),
|
|
41
|
+
guidance: z.string().optional(),
|
|
41
42
|
});
|
|
42
43
|
export const AutomationMaturitySchema = z.object({
|
|
43
44
|
computedAt: z.string().datetime(),
|
|
@@ -163,6 +163,7 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
163
163
|
recommendations: z.ZodArray<z.ZodString, "many">;
|
|
164
164
|
applicability: z.ZodOptional<z.ZodEnum<["applicable", "not_applicable", "unknown"]>>;
|
|
165
165
|
reason: z.ZodOptional<z.ZodString>;
|
|
166
|
+
guidance: z.ZodOptional<z.ZodString>;
|
|
166
167
|
}, "strip", z.ZodTypeAny, {
|
|
167
168
|
recommendations: string[];
|
|
168
169
|
dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
|
|
@@ -171,6 +172,7 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
171
172
|
evidence: string[];
|
|
172
173
|
reason?: string | undefined;
|
|
173
174
|
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
175
|
+
guidance?: string | undefined;
|
|
174
176
|
}, {
|
|
175
177
|
recommendations: string[];
|
|
176
178
|
dimension: "test-coverage-breadth" | "framework-adoption" | "test-id-hygiene" | "ci-integration" | "auth-test-coverage" | "component-test-ratio";
|
|
@@ -179,6 +181,7 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
179
181
|
evidence: string[];
|
|
180
182
|
reason?: string | undefined;
|
|
181
183
|
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
184
|
+
guidance?: string | undefined;
|
|
182
185
|
}>, "many">;
|
|
183
186
|
topRecommendations: z.ZodArray<z.ZodString, "many">;
|
|
184
187
|
scoreFormula: z.ZodOptional<z.ZodString>;
|
|
@@ -196,6 +199,7 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
196
199
|
evidence: string[];
|
|
197
200
|
reason?: string | undefined;
|
|
198
201
|
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
202
|
+
guidance?: string | undefined;
|
|
199
203
|
}[];
|
|
200
204
|
topRecommendations: string[];
|
|
201
205
|
scoreFormula?: string | undefined;
|
|
@@ -213,6 +217,7 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
213
217
|
evidence: string[];
|
|
214
218
|
reason?: string | undefined;
|
|
215
219
|
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
220
|
+
guidance?: string | undefined;
|
|
216
221
|
}[];
|
|
217
222
|
topRecommendations: string[];
|
|
218
223
|
scoreFormula?: string | undefined;
|
|
@@ -262,6 +267,7 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
262
267
|
evidence: string[];
|
|
263
268
|
reason?: string | undefined;
|
|
264
269
|
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
270
|
+
guidance?: string | undefined;
|
|
265
271
|
}[];
|
|
266
272
|
topRecommendations: string[];
|
|
267
273
|
scoreFormula?: string | undefined;
|
|
@@ -311,6 +317,7 @@ export declare const RepoAnalysisSchema: z.ZodObject<{
|
|
|
311
317
|
evidence: string[];
|
|
312
318
|
reason?: string | undefined;
|
|
313
319
|
applicability?: "unknown" | "applicable" | "not_applicable" | undefined;
|
|
320
|
+
guidance?: string | undefined;
|
|
314
321
|
}[];
|
|
315
322
|
topRecommendations: string[];
|
|
316
323
|
scoreFormula?: string | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repo-analysis.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/repo-analysis.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,8BAA8B,8HAUzC,CAAC;AAEH,eAAO,MAAM,kCAAkC,sCAAoC,CAAC;AAEpF,eAAO,MAAM,2BAA2B,0FAOtC,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;EAKnC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AACtF,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF,eAAO,MAAM,eAAe;;;;;;;;;;;;EAI1B,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;EAIzB,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;EASjC,CAAC;AAEH,eAAO,MAAM,kBAAkB
|
|
1
|
+
{"version":3,"file":"repo-analysis.schema.d.ts","sourceRoot":"","sources":["../../src/schemas/repo-analysis.schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,8BAA8B,8HAUzC,CAAC;AAEH,eAAO,MAAM,kCAAkC,sCAAoC,CAAC;AAEpF,eAAO,MAAM,2BAA2B,0FAOtC,CAAC;AAEH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;EAKnC,CAAC;AAEH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AACtF,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF,eAAO,MAAM,eAAe;;;;;;;;;;;;EAI1B,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;EAIzB,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;EASjC,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/detect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAY,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAIzE,MAAM,MAAM,yBAAyB,GACjC,cAAc,GACd,iBAAiB,GACjB,cAAc,GACd,cAAc,GACd,yBAAyB,GACzB,iBAAiB,GACjB,SAAS,CAAC;AAEd,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,yBAAyB,GAAG,IAAI,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/detect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAY,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAIzE,MAAM,MAAM,yBAAyB,GACjC,cAAc,GACd,iBAAiB,GACjB,cAAc,GACd,cAAc,GACd,yBAAyB,GACzB,iBAAiB,GACjB,SAAS,CAAC;AAEd,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,yBAAyB,GAAG,IAAI,CAAC;IAC7C,MAAM,EAAE,MAAM,CAAC;CAChB;AAwQD,wBAAgB,4BAA4B,CAAC,OAAO,EAAE;IACpD,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,OAAO,CAAC;CAC9B,GAAG,4BAA4B,CA6B/B;AAOD,wBAAsB,yBAAyB,CAC7C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,4BAA4B,GAAG,IAAI,CAAC,CAgD9C;AAED,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,EACf,SAAS,SAAQ,GAChB,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAelD;AAED,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,SAAS,SAAQ,GAChB,OAAO,CAAC,4BAA4B,CAAC,CAyDvC;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,YAAY,CAAC,CAqJvB"}
|
|
@@ -151,7 +151,7 @@ function authPathsFromOauthButtons(oauthButtons, loginUrl) {
|
|
|
151
151
|
}
|
|
152
152
|
async function probeClickToRevealForms(page, loginUrl, alreadyMatchedTexts, timeoutMs, progress) {
|
|
153
153
|
const out = [];
|
|
154
|
-
const buttons = page.locator('button');
|
|
154
|
+
const buttons = page.locator('button, [role="button"]');
|
|
155
155
|
const n = await buttons.count();
|
|
156
156
|
const seenLabels = new Set();
|
|
157
157
|
const SUBMIT_RE = /^(sign in|log in|submit|continue|next|cancel|close)$/i;
|
|
@@ -207,8 +207,9 @@ async function probeClickToRevealForms(page, loginUrl, alreadyMatchedTexts, time
|
|
|
207
207
|
await waitNetworkIdleBestEffort(page);
|
|
208
208
|
continue;
|
|
209
209
|
}
|
|
210
|
+
await waitNetworkIdleBestEffort(page);
|
|
210
211
|
try {
|
|
211
|
-
await page.locator('input[type="password"]:visible').first().waitFor({ state: 'visible', timeout:
|
|
212
|
+
await page.locator('input[type="password"]:visible').first().waitFor({ state: 'visible', timeout: 5000 });
|
|
212
213
|
}
|
|
213
214
|
catch {
|
|
214
215
|
await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"explore.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/explore.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"explore.d.ts","sourceRoot":"","sources":["../../../src/tools/auth/explore.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AA2PzE,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAiM1B"}
|
|
@@ -185,6 +185,43 @@ async function buildFormPaths(page) {
|
|
|
185
185
|
},
|
|
186
186
|
];
|
|
187
187
|
}
|
|
188
|
+
async function probeUserLocalProviderClick(page, providerLabel, loginUrl, timeoutMs) {
|
|
189
|
+
const originBefore = new URL(page.url()).origin;
|
|
190
|
+
let clicked = false;
|
|
191
|
+
try {
|
|
192
|
+
await page.getByRole('button', { name: providerLabel, exact: true }).first().click({ timeout: 3000 });
|
|
193
|
+
clicked = true;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
try {
|
|
197
|
+
const escaped = providerLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
198
|
+
await page
|
|
199
|
+
.locator('button, [role="button"]')
|
|
200
|
+
.filter({ hasText: new RegExp(`^\\s*${escaped}\\s*$`, 'i') })
|
|
201
|
+
.first()
|
|
202
|
+
.click({ timeout: 3000 });
|
|
203
|
+
clicked = true;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
/* skip */
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!clicked)
|
|
210
|
+
return [];
|
|
211
|
+
try {
|
|
212
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 5000 });
|
|
213
|
+
}
|
|
214
|
+
catch { /* best-effort */ }
|
|
215
|
+
await waitNetworkIdleBestEffort(page);
|
|
216
|
+
if (new URL(page.url()).origin !== originBefore) {
|
|
217
|
+
await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
const formPaths = await buildFormPaths(page);
|
|
221
|
+
await page.goto(loginUrl, { timeout: timeoutMs, waitUntil: 'domcontentloaded' });
|
|
222
|
+
await waitNetworkIdleBestEffort(page);
|
|
223
|
+
return formPaths;
|
|
224
|
+
}
|
|
188
225
|
export async function exploreAuth(url, timeoutMs = 20000, progress) {
|
|
189
226
|
const browser = await launchBrowser();
|
|
190
227
|
try {
|
|
@@ -246,6 +283,21 @@ export async function exploreAuth(url, timeoutMs = 20000, progress) {
|
|
|
246
283
|
continue;
|
|
247
284
|
}
|
|
248
285
|
consumed.add(id);
|
|
286
|
+
if (p.source === 'user-local') {
|
|
287
|
+
const probed = await probeUserLocalProviderClick(page, p.label, finalUrl, timeoutMs);
|
|
288
|
+
if (probed.length > 0) {
|
|
289
|
+
for (const fp of probed) {
|
|
290
|
+
authPaths.push({
|
|
291
|
+
...fp,
|
|
292
|
+
id: p.id,
|
|
293
|
+
label: p.label,
|
|
294
|
+
source: 'user-local',
|
|
295
|
+
});
|
|
296
|
+
progress?.info(`explore_auth path id=${p.id} type=${fp.type} automatable=${fp.automatable} (user-local probe)`);
|
|
297
|
+
}
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
249
301
|
authPaths.push({
|
|
250
302
|
id,
|
|
251
303
|
label: p.label,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"automation-maturity.d.ts","sourceRoot":"","sources":["../../../src/tools/scoring/automation-maturity.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EACV,kBAAkB,EAGnB,MAAM,6CAA6C,CAAC;AAiDrD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,kBAAkB,
|
|
1
|
+
{"version":3,"file":"automation-maturity.d.ts","sourceRoot":"","sources":["../../../src/tools/scoring/automation-maturity.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EACV,kBAAkB,EAGnB,MAAM,6CAA6C,CAAC;AAiDrD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,YAAY,GAAG,kBAAkB,CAmMhF"}
|
|
@@ -102,10 +102,13 @@ export function computeAutomationMaturity(repo) {
|
|
|
102
102
|
let hygieneScore = 0;
|
|
103
103
|
let hygieneApplicability = 'applicable';
|
|
104
104
|
let hygieneReason;
|
|
105
|
+
let hygieneGuidance;
|
|
105
106
|
const hygieneEvidence = [];
|
|
106
107
|
if (interactiveTsxScanned === 0) {
|
|
107
108
|
hygieneApplicability = 'unknown';
|
|
108
109
|
hygieneReason = 'No interactive TSX files scanned — cannot compute a missing-id ratio honestly.';
|
|
110
|
+
hygieneGuidance =
|
|
111
|
+
'Qulib could not collect enough signal to score this dimension. Run against a repo with more test files or a larger page scan to improve signal.';
|
|
109
112
|
hygieneEvidence.push(hygieneReason);
|
|
110
113
|
}
|
|
111
114
|
else {
|
|
@@ -123,6 +126,7 @@ export function computeAutomationMaturity(repo) {
|
|
|
123
126
|
: [],
|
|
124
127
|
applicability: hygieneApplicability,
|
|
125
128
|
...(hygieneReason && { reason: hygieneReason }),
|
|
129
|
+
...(hygieneGuidance && { guidance: hygieneGuidance }),
|
|
126
130
|
};
|
|
127
131
|
const ci = hasCiAtRoot(repo.repoPath);
|
|
128
132
|
const ciDim = {
|
|
@@ -141,10 +145,13 @@ export function computeAutomationMaturity(repo) {
|
|
|
141
145
|
let authScore = 0;
|
|
142
146
|
let authApplicability = 'applicable';
|
|
143
147
|
let authReason;
|
|
148
|
+
let authGuidance;
|
|
144
149
|
const authEvidence = [];
|
|
145
150
|
if (!repoHasAnyAuthSignal) {
|
|
146
151
|
authApplicability = 'not_applicable';
|
|
147
152
|
authReason = 'No auth routes, auth-named test files, or auth path coverage detected — repo appears auth-free.';
|
|
153
|
+
authGuidance =
|
|
154
|
+
'No auth signal detected in this app. If authentication exists, run qulib with a storage-state file to enable auth-test-coverage scoring.';
|
|
148
155
|
authEvidence.push(authReason);
|
|
149
156
|
}
|
|
150
157
|
else {
|
|
@@ -163,6 +170,7 @@ export function computeAutomationMaturity(repo) {
|
|
|
163
170
|
: [],
|
|
164
171
|
applicability: authApplicability,
|
|
165
172
|
...(authReason && { reason: authReason }),
|
|
173
|
+
...(authGuidance && { guidance: authGuidance }),
|
|
166
174
|
};
|
|
167
175
|
const cypressE2e = repo.testFiles.filter((t) => t.type === 'cypress-e2e').length;
|
|
168
176
|
const cypressComp = repo.testFiles.filter((t) => t.type === 'cypress-component').length;
|
|
@@ -170,10 +178,13 @@ export function computeAutomationMaturity(repo) {
|
|
|
170
178
|
let compRatioScore = 0;
|
|
171
179
|
let compApplicability = 'applicable';
|
|
172
180
|
let compReason;
|
|
181
|
+
let compGuidance;
|
|
173
182
|
const compEvidence = [];
|
|
174
183
|
if (cypressTotal === 0) {
|
|
175
184
|
compApplicability = 'not_applicable';
|
|
176
185
|
compReason = 'No Cypress (e2e or component) tests detected — component-test-ratio does not apply.';
|
|
186
|
+
compGuidance =
|
|
187
|
+
'No Cypress component test setup detected. Add cypress/component/ tests and a component config to enable this dimension.';
|
|
177
188
|
compEvidence.push(compReason);
|
|
178
189
|
}
|
|
179
190
|
else {
|
|
@@ -190,6 +201,7 @@ export function computeAutomationMaturity(repo) {
|
|
|
190
201
|
: [],
|
|
191
202
|
applicability: compApplicability,
|
|
192
203
|
...(compReason && { reason: compReason }),
|
|
204
|
+
...(compGuidance && { guidance: compGuidance }),
|
|
193
205
|
};
|
|
194
206
|
const dimensions = [breadthDim, frameworkDim, hygieneDim, ciDim, authDim, compDim];
|
|
195
207
|
// Overall score normalizes over applicable dimensions only.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qulib/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Qulib — analyze deployed web apps for honest quality gaps (CLI + programmatic API)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Tapesh Nagarwal",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"analyze": "tsx src/cli/index.ts analyze",
|
|
49
49
|
"clean": "tsx src/cli/index.ts clean",
|
|
50
50
|
"build": "tsc",
|
|
51
|
-
"test": "node --import tsx/esm --test src/llm/__tests__/cost-intelligence.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/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/__tests__/analyze.storage-state-invalid.test.ts",
|
|
51
|
+
"test": "node --import tsx/esm --test src/llm/__tests__/cost-intelligence.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/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/__tests__/analyze.storage-state-invalid.test.ts src/__tests__/analyze.fixtures.test.ts",
|
|
52
52
|
"test:integration": "node --import tsx/esm --test src/__tests__/analyze.integration.test.ts",
|
|
53
53
|
"smoke": "tsx src/cli/index.ts analyze --url https://example.com --ephemeral",
|
|
54
54
|
"cost-doctor": "tsx src/cli/index.ts cost doctor"
|