# @qulib/core **@qulib/core** is the TypeScript-first Qulib package for analyzing deployed web apps (and optionally a local repo) and surfacing honest quality gaps. ## Install ```bash npm install @qulib/core ``` ## One-time browser setup Qulib uses Playwright. Install Chromium once on the machine that runs scans: ```bash npx playwright install chromium ``` If browsers are missing, commands fail with a short message pointing you here. ## Scanning authenticated apps Qulib supports three auth modes: anonymous (default), form-login, and storage-state. ### Form login If your app uses a simple username/password form: ```bash qulib analyze --url https://app.example.com \ --auth-form-login \ --login-url https://app.example.com/login \ --username you@example.com \ --password "..." \ --username-selector "input[name=email]" \ --password-selector "input[name=password]" \ --submit-selector "button[type=submit]" ``` ### OAuth, magic link, SSO, or anything else These can't be automated. Qulib has a helper for this: ```bash qulib auth init --base-url https://app.example.com ``` This opens a real browser. Log in normally (OAuth, magic link, password manager, whatever). Press ENTER in the terminal when you reach a logged-in page. Qulib saves your session to `qulib-storage-state.json`. ### Automated form login (`auth login`) When **`detect-auth`** shows **`authOptions`** with **`type: "form-login"`** and **`requirements.method: "credentials"`** (including click-to-reveal paths such as NQ Login), you can save a storage state **without** manual clicking: ```bash qulib auth login --base-url https://notquality.com \ --auth-path nq-login \ --credentials-file ~/.qulib/nq-creds.json \ --out ~/.qulib/nq-state.json ``` The JSON file must map **field `name`** values from `authOptions` to secrets, e.g. `{"username":"…","password":"…","hidden.datasource":"…"}`. Prefer **`--credentials-file`** over **`--credentials`** so values are not stored in shell history. Then analyze with the saved session: ```bash qulib analyze --url https://notquality.com \ --auth-storage-state ~/.qulib/nq-state.json ``` Use **`--auth-path `** when multiple **`form-login`** paths appear in **`authOptions`**. Use **`--success-url-contains `** for stricter success detection; otherwise Qulib infers success from URL changes or the password field disappearing (and warns if it cannot confirm). Then scan with it: ```bash qulib analyze --url https://app.example.com --auth-storage-state ./qulib-storage-state.json ``` The storage state is just a JSON file of cookies and localStorage — keep it private, treat it like a credential. #### Storage state is validated before crawl Qulib now validates the provided storage state before doing any work. If the file is missing, unreadable, empty, on the wrong origin, or carries a session that is already expired, Qulib stops with an honest `blocked` result (no fake `releaseConfidence`) and a structured gap explaining how to recover. The validator reports one of these stable reason codes: | Reason code | Meaning | | ------------------------- | ----------------------------------------------------------------------- | | `missing-file` | Path passed to `--auth-storage-state` does not exist. | | `unreadable-file` | File exists but the process can't read it (permissions). | | `invalid-json` | File is present and readable but not valid JSON. | | `no-auth-cookies` | File parses, but has zero cookies and zero localStorage entries. | | `wrong-origin` | Session redirects to a different origin (host/port/scheme mismatch). | | `expired-or-unauthorized` | Loaded session shows the login form again, or the app returns 401/403. | | `unknown` | Validation could not be completed for an unexpected reason. | Origin matching is strict — `https://app.example` and `https://www.app.example` are different origins, as are `http://localhost:3000` and `http://localhost:4000`. Re-run `qulib auth login` against the same origin you plan to `analyze`. Relatedly, `qulib auth login` will now refuse to save a storage state if the browser ends the flow on a different origin than `--base-url` (a federated/SSO redirect that never returned to the app). This prevents Qulib from quietly persisting an IdP-domain session that would later produce false-confidence scans. ### Multi-path auth exploration (`explore-auth`) For unfamiliar apps (especially enterprise SSO with several buttons), run **`qulib explore-auth --url `** before `analyze`. The JSON lists every detected path (built-in OAuth names like Google/Clever, **heuristic** unknown buttons such as tenant-specific SSO labels, password forms, and magic-link copy) plus **`suggestedAgentBehavior`** for the agent. Unknown SSO buttons include **`unrecognizedButtons`** with a hint. Teach this machine to recognize a label next time: ```bash qulib auth providers add --id nq-login --label "NQ Login" --pattern "nq login" qulib auth providers list qulib auth providers remove --id nq-login ``` Patterns live in **`~/.qulib/providers.json`** (per user, not in the repo). Built-in public platforms stay in qulib’s curated list; tenant-specific names are never shipped as built-ins. ### Auth detection To check what auth pattern a site uses before configuring anything: ```bash qulib detect-auth --url https://app.example.com ``` Or via MCP: > "Use qulib's detect_auth tool on https://app.example.com — what's the recommended auth setup?" ## Release confidence The score (0–100) is derived from **deterministic gaps** (untested routes vs repo, console errors, broken links, axe violations). High-severity items subtract more than low-severity ones. If **`coveragePagesScanned` is below `minPagesForConfidence`**, the score is **capped at 40** and `coverageWarning` is set to **`low-coverage`** so a shallow crawl cannot masquerade as high confidence. When **`mode` is `auth-required`**, the scan never reached real app pages behind login: **release confidence is 0**, gaps are empty, and Cost Intelligence reflects the blocked state (L0 maturity). ## LLM scenario budget (naming) - **`llmTokenBudget`** (legacy name, still required in config files): **max output tokens for a single** scenario-generation LLM completion. It maps to the provider’s **per-request completion cap**, not a multi-call or “whole run” token budget. - **`llmMaxOutputTokensPerCall`** (optional): when set, **overrides** `llmTokenBudget` for the same purpose—clearer naming. - **`enableLlmScenarios`**: when **`false`**, Qulib never calls an LLM for scenarios (templates only). ## Cost Intelligence and `qulib cost doctor` After a normal **`analyze`**, `output/report.json` includes **`gapAnalysis.costIntelligence`**: usage records (**`actual`** vs **`estimated`** vs **`none`**), per-completion ceiling, budget warnings, repeated prompt fingerprints (when the same hash appears twice in one run), deterministic maturity (L0–L3 with an explicit ceiling for L4/L5), and conversion recommendations. Re-print that block from disk: ```bash qulib cost doctor # or: qulib cost doctor --report output/report.json ``` ## CLI (from npm) **Release confidence — the flagship command:** ```bash npx @qulib/core confidence --url https://example.com ``` Returns ship / caution / hold / block with a 0–100 score, top risks, and recommended next checks. Add `--repo` to also score test-automation maturity and API coverage. **Analyze (full gap report):** ```bash npx @qulib/core analyze --url https://example.com ``` **Scaffold a test suite:** ```bash npx @qulib/core scaffold --url https://example.com --framework cypress-e2e ``` **Score automation maturity (repo-only, no URL needed):** ```bash npx @qulib/core score-automation --repo /path/to/repo ``` Use `npx playwright install chromium` the first time you scan (Playwright is a dependency). ## Programmatic API ```ts import { analyzeApp, type HarnessConfig } from '@qulib/core'; const config: HarnessConfig = { maxPagesToScan: 20, maxDepth: 3, minPagesForConfidence: 3, timeoutMs: 30000, retryCount: 2, llmTokenBudget: 4000, llmMaxOutputTokensPerCall: undefined, enableLlmScenarios: true, testGenerationLimit: 10, readOnlyMode: true, requireHumanReview: true, failOnConsoleError: false, explorer: 'playwright', defaultAdapter: 'playwright', adapters: ['playwright', 'cypress-e2e'], }; const result = await analyzeApp({ url: 'https://example.com', config, writeArtifacts: false, }); console.log(result.releaseConfidence, result.gapAnalysis.costIntelligence); ``` ## Repository Source and issues: **[github.com/TapeshN/qulib](https://github.com/TapeshN/qulib)**. ## Monorepo context This package is part of **[Qulib](https://github.com/TapeshN/qulib)** ([repo README](../../README.md)). Install dependencies from the repository root: `npm install`. Build all packages: `npm run build` (from root). ## Current capabilities - CLI `analyze` flow: `observe` → `think` → `act`. - Playwright explorer: route discovery, **axe-core** (WCAG 2.0 A/AA), sampled internal link HEAD checks. - Optional **authenticated** crawling via `auth` in config (`form-login` or Playwright `storage-state`). - Repo scanner: routes, tests, Cypress structure. - Gap engine: deterministic gaps, **release confidence** with a low-page coverage floor, coverage warnings. - Reports: `output/report.json` and `output/report.md` when not using **`--ephemeral`** (both include **Cost Intelligence** when present on `gapAnalysis`). - State under `.scan-state/` unless **`--ephemeral`** (no disk writes; full JSON on stdout). - **`npm run clean`** removes generated `output/` and `.scan-state/` and restores `.gitkeep` placeholders. ## Tech stack TypeScript (strict, NodeNext), Commander, Zod, Playwright, @axe-core/playwright, fast-glob; optional Anthropic API for scenario generation. ## Layout ```text src/ adapters/ # test rendering adapters analyze.ts # programmatic API (also used by @qulib/mcp) cli/ # CLI entry harness/ # state + decision logging llm/ # LLM contracts phases/ # observe / think / act reporters/ # JSON + Markdown reports schemas/ # Zod schemas telemetry/ # event sink + URL redaction tools/ auth/ # detection, exploration, validation, providers, gap builders explorers/ # browser launch, Playwright/Cypress crawlers, factory repo/ # repo scanner, framework detection scoring/ # gap engine, automation maturity, public surface __tests__/ # integration and wiring tests live in __tests__/ in each folder ``` A contributor map of which folder to touch for each kind of change lives at [`docs/source-map.md`](../../docs/source-map.md). Repo rules: see [`CLAUDE.md`](../../CLAUDE.md). ## Configuration Default file: **`qulib.config.ts`** in this package directory (or pass **`--config `** relative to the process working directory). Optional `auth` for authenticated scanning — see commented example in `qulib.config.ts`. For local credentials, use a separate file (e.g. `qulib.test-auth.config.ts`, gitignored at the repo root) and point `--config` at it. Use the same **hostname** for `--url` as your app’s canonical host when you can. The crawler treats **`www` and apex** (e.g. `example.com` and `www.example.com`) as the same site for internal link discovery, so hydration and redirects still queue in-site URLs. ## Scripts (from `packages/core`) - `npm run dev` — CLI via `tsx` (append subcommands, e.g. `npm run dev -- clean`) - `npm run analyze -- --url [--repo ] [--config ] [--ephemeral]` - `npm run clean` — reset `output/` and `.scan-state/` here - `npm run test` — unit tests (cost intelligence + hashing) - `npm run smoke` — ephemeral analyze of `https://example.com` (uses this package’s `qulib.config.ts`) - `npm run cost-doctor` — print Cost Intelligence from `output/report.json` (run a non-ephemeral `analyze` first) - `npm run build` — compile to `dist/` From the **repository root**: - `npm run analyze -w @qulib/core -- --url …` - `npm run clean` — runs core clean via workspace Binary name after publish: **`qulib`** (see `package.json` `bin`). ## Usage examples ```bash cd packages/core # app only npm run analyze -- --url http://localhost:3000 # app + repo npm run analyze -- --url http://localhost:3000 --repo ../your-app # local auth config (keep out of git) npm run analyze -- --config ../../qulib.test-auth.config.ts --url https://example.com # ephemeral: JSON on stdout, logs on stderr npm run analyze -- --url https://example.com --ephemeral > report.bundle.json npm run clean ``` ## Minimum config Smallest legal `qulib.config.ts`: ```ts import type { HarnessConfig } from './src/schemas/config.schema.js'; const config: HarnessConfig = { maxPagesToScan: 20, maxDepth: 3, timeoutMs: 30000, }; export default config; ``` All other fields inherit from schema defaults or CLI/runtime defaults. ## Scan walkthroughs (copy-paste) ### 1) Public scan ```bash npx @qulib/core analyze --url https://yourapp.com ``` ### 2) Auth-blocked scan (honest blocked mode) ```bash npx @qulib/core analyze --url https://yourapp.com/auth ``` 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. ### 3) Authenticated scan with storage state ```bash # Capture once (manual OAuth/SSO-safe flow) qulib auth init --base-url https://yourapp.com # Reuse saved session qulib analyze --url https://yourapp.com --auth-storage-state ./qulib-storage-state.json ``` ## Sample report (fixture baseline) 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. A minimal structural snapshot looks like: ```json { "status": "complete", "releaseConfidence": 68, "gaps": [ "... gap items ..." ] } ``` ## MCP tools quick map | Tool | When to use | Key input | |---|---|---| | **`qulib_score_confidence`** | **Flagship.** Fused verdict (ship/caution/hold/block) from all collectors | `url` and/or `repoPath`, optional `includeViews.replay` | | `qulib_analyze_app` | Live-app QA scan: release confidence + gaps + a11y | `url`, optional `auth`, optional LLM knobs | | `qulib_score_automation` | Score local repo test-automation maturity | absolute `repoPath`, optional `includeFullDimensions` | | `qulib_score_api` | Discover API endpoints and score their test coverage | absolute `repoPath`, optional `enableTier3`, `includeEndpointDetail` | | `qulib_scaffold_tests` | Generate Cypress scaffold from a live URL (`cypress-e2e` only; playwright not yet implemented) | `url`, optional `framework`, `maxPagesToScan`, `recipes` | | **`qulib_score_bug_report`** | LLM-as-judge of a learner bug report vs planted-bug target | `report` (title, description, steps, severity), `target` (description, type, severity, expectedBehavior) | | **`qulib_score_decisions`** | Pivotal-decision evaluation at agent forks | absolute `forksPath` (JSONL), optional `enableLlmJudge` | | `qulib_explore_auth` | Deeper auth-path discovery on unfamiliar apps | `url`, optional `timeoutMs` | | `qulib_detect_auth` | Fast single-pass auth pattern guess | `url`, optional `timeoutMs` | | `analyze_app` | Legacy alias for `qulib_analyze_app` — kept for backwards compatibility | same as `qulib_analyze_app` | | `explore_auth` | Legacy alias for `qulib_explore_auth` — kept for backwards compatibility | same as `qulib_explore_auth` | | `detect_auth` | Legacy alias for `qulib_detect_auth` — kept for backwards compatibility | same as `qulib_detect_auth` | ## Output directories Qulib writes runtime artifacts to: - `.scan-state/` — intermediate state (discovered routes, gap analysis snapshots, decision log) - `output/` — final `report.json` and `report.md` Both are gitignored and safe to delete; Qulib recreates them on the next non-ephemeral run. ## ANTHROPIC_API_KEY (LLM scenarios) For MCP-hosted usage, set `ANTHROPIC_API_KEY` in your host's `env` block: ```json { "mcpServers": { "qulib": { "command": "npx", "args": ["@qulib/mcp"], "env": { "ANTHROPIC_API_KEY": "sk-ant-..." } } } } ``` Without this key, Qulib still runs deterministic checks (crawl, a11y, links, console, scoring) and falls back to template scenarios instead of LLM-generated ones. ## Playwright browsers ```bash npx playwright install chromium ``` ## Output and state (cwd = `packages/core` when you `cd` here) **Ephemeral:** stdout prints one JSON object: `gapAnalysis` (including **`costIntelligence`** when populated), `discoveredRoutes`, `repoInventory`, `decisionLog`. **Persistent:** - `.scan-state/discovered-routes.json`, `gap-analysis.json`, `decision-log.json`, and `repo-inventory.json` when `--repo` is set - `output/report.json`, `output/report.md` For more options (`repoPath`, loading config from disk), see `src/analyze.ts` in the repository.