@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 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli-smoke-fixture.d.ts.map
@@ -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,6 @@
1
+ export interface FixtureServerHandle {
2
+ baseUrl: string;
3
+ close: () => Promise<void>;
4
+ }
5
+ export declare function startFixtureServer(): Promise<FixtureServerHandle>;
6
+ //# sourceMappingURL=fixture-server.d.ts.map
@@ -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,CAEhE;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"}
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' && path.source === 'heuristic' && !builtInOAuthIds.has(path.id);
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,5 +1,5 @@
1
1
  import { emitTelemetry } from '../../telemetry/emit.js';
2
- const DEFAULT_MODEL = 'claude-sonnet-4-20250514';
2
+ const DEFAULT_MODEL = 'claude-haiku-4-5-20251001';
3
3
  function estimateTokensFromChars(chars) {
4
4
  return Math.max(0, Math.ceil(chars / 4));
5
5
  }
@@ -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,CAiLtB"}
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
- const parsed = JSON.parse(llmResult.text);
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;;;;;;;;;;;;;;;;;;;;;;;;EAe5C,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"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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
+ {"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;AAsQD,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"}
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: 2000 });
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;AAiNzE,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,SAAS,SAAQ,EACjB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAgL1B"}
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,CAuLhF"}
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.4.3",
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"