@qulib/core 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +45 -3
  2. package/dist/analyze.d.ts +16 -4
  3. package/dist/analyze.d.ts.map +1 -1
  4. package/dist/analyze.js +98 -38
  5. package/dist/cli/cost-doctor.d.ts +2 -0
  6. package/dist/cli/cost-doctor.d.ts.map +1 -0
  7. package/dist/cli/cost-doctor.js +72 -0
  8. package/dist/cli/index.js +61 -0
  9. package/dist/harness/progress-log.d.ts +7 -0
  10. package/dist/harness/progress-log.d.ts.map +1 -0
  11. package/dist/harness/progress-log.js +1 -0
  12. package/dist/harness/run-options.d.ts +2 -0
  13. package/dist/harness/run-options.d.ts.map +1 -1
  14. package/dist/index.d.ts +6 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +3 -0
  17. package/dist/llm/content-hash.d.ts +2 -0
  18. package/dist/llm/content-hash.d.ts.map +1 -0
  19. package/dist/llm/content-hash.js +4 -0
  20. package/dist/llm/context-builder.js +1 -1
  21. package/dist/llm/cost-intelligence.d.ts +29 -0
  22. package/dist/llm/cost-intelligence.d.ts.map +1 -0
  23. package/dist/llm/cost-intelligence.js +153 -0
  24. package/dist/llm/provider.d.ts +11 -1
  25. package/dist/llm/provider.d.ts.map +1 -1
  26. package/dist/llm/provider.js +43 -4
  27. package/dist/phases/act.d.ts.map +1 -1
  28. package/dist/phases/act.js +4 -1
  29. package/dist/phases/observe.js +1 -1
  30. package/dist/phases/think-finalize.d.ts +6 -0
  31. package/dist/phases/think-finalize.d.ts.map +1 -0
  32. package/dist/phases/think-finalize.js +164 -0
  33. package/dist/phases/think.d.ts +2 -0
  34. package/dist/phases/think.d.ts.map +1 -1
  35. package/dist/phases/think.js +16 -65
  36. package/dist/reporters/markdown-reporter.d.ts.map +1 -1
  37. package/dist/reporters/markdown-reporter.js +23 -3
  38. package/dist/schemas/config.schema.d.ts +364 -0
  39. package/dist/schemas/config.schema.d.ts.map +1 -1
  40. package/dist/schemas/config.schema.js +55 -1
  41. package/dist/schemas/cost-intelligence.schema.d.ts +229 -0
  42. package/dist/schemas/cost-intelligence.schema.d.ts.map +1 -0
  43. package/dist/schemas/cost-intelligence.schema.js +41 -0
  44. package/dist/schemas/decision-log.schema.d.ts +2 -2
  45. package/dist/schemas/gap-analysis.schema.d.ts +288 -49
  46. package/dist/schemas/gap-analysis.schema.d.ts.map +1 -1
  47. package/dist/schemas/gap-analysis.schema.js +7 -3
  48. package/dist/schemas/index.d.ts +3 -1
  49. package/dist/schemas/index.d.ts.map +1 -1
  50. package/dist/schemas/index.js +3 -1
  51. package/dist/schemas/public-surface.schema.d.ts +268 -0
  52. package/dist/schemas/public-surface.schema.d.ts.map +1 -0
  53. package/dist/schemas/public-surface.schema.js +15 -0
  54. package/dist/schemas/repo-analysis.schema.d.ts +6 -6
  55. package/dist/tools/auth-block-gap.d.ts +3 -0
  56. package/dist/tools/auth-block-gap.d.ts.map +1 -0
  57. package/dist/tools/auth-block-gap.js +19 -0
  58. package/dist/tools/auth-detector.d.ts +2 -1
  59. package/dist/tools/auth-detector.d.ts.map +1 -1
  60. package/dist/tools/auth-detector.js +28 -3
  61. package/dist/tools/auth-explorer.d.ts +4 -0
  62. package/dist/tools/auth-explorer.d.ts.map +1 -0
  63. package/dist/tools/auth-explorer.js +346 -0
  64. package/dist/tools/auth-surface-analyzer.d.ts +4 -0
  65. package/dist/tools/auth-surface-analyzer.d.ts.map +1 -0
  66. package/dist/tools/auth-surface-analyzer.js +154 -0
  67. package/dist/tools/cypress-explorer.d.ts +2 -1
  68. package/dist/tools/cypress-explorer.d.ts.map +1 -1
  69. package/dist/tools/cypress-explorer.js +1 -1
  70. package/dist/tools/explorer.interface.d.ts +2 -1
  71. package/dist/tools/explorer.interface.d.ts.map +1 -1
  72. package/dist/tools/gap-engine.d.ts +3 -1
  73. package/dist/tools/gap-engine.d.ts.map +1 -1
  74. package/dist/tools/gap-engine.js +39 -12
  75. package/dist/tools/oauth-providers.d.ts +7 -0
  76. package/dist/tools/oauth-providers.d.ts.map +1 -0
  77. package/dist/tools/oauth-providers.js +21 -0
  78. package/dist/tools/playwright-explorer.d.ts +2 -1
  79. package/dist/tools/playwright-explorer.d.ts.map +1 -1
  80. package/dist/tools/playwright-explorer.js +21 -3
  81. package/dist/tools/public-surface.d.ts +5 -0
  82. package/dist/tools/public-surface.d.ts.map +1 -0
  83. package/dist/tools/public-surface.js +13 -0
  84. package/dist/tools/user-providers.d.ts +15 -0
  85. package/dist/tools/user-providers.d.ts.map +1 -0
  86. package/dist/tools/user-providers.js +62 -0
  87. package/package.json +6 -2
package/README.md CHANGED
@@ -55,6 +55,20 @@ qulib analyze --url https://app.example.com --auth-storage-state ./qulib-storage
55
55
 
56
56
  The storage state is just a JSON file of cookies and localStorage — keep it private, treat it like a credential.
57
57
 
58
+ ### Multi-path auth exploration (`explore-auth`)
59
+
60
+ For unfamiliar apps (especially enterprise SSO with several buttons), run **`qulib explore-auth --url <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.
61
+
62
+ Unknown SSO buttons include **`unrecognizedButtons`** with a hint. Teach this machine to recognize a label next time:
63
+
64
+ ```bash
65
+ qulib auth providers add --id scholastic-sync --label "Scholastic Sync" --pattern "scholastic sync"
66
+ qulib auth providers list
67
+ qulib auth providers remove --id scholastic-sync
68
+ ```
69
+
70
+ 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.
71
+
58
72
  ### Auth detection
59
73
 
60
74
  To check what auth pattern a site uses before configuring anything:
@@ -67,6 +81,29 @@ Or via MCP:
67
81
 
68
82
  > "Use qulib's detect_auth tool on https://app.example.com — what's the recommended auth setup?"
69
83
 
84
+ ## Release confidence
85
+
86
+ 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.
87
+
88
+ 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).
89
+
90
+ ## LLM scenario budget (naming)
91
+
92
+ - **`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.
93
+ - **`llmMaxOutputTokensPerCall`** (optional): when set, **overrides** `llmTokenBudget` for the same purpose—clearer naming.
94
+ - **`enableLlmScenarios`**: when **`false`**, Qulib never calls an LLM for scenarios (templates only).
95
+
96
+ ## Cost Intelligence and `qulib cost doctor`
97
+
98
+ 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.
99
+
100
+ Re-print that block from disk:
101
+
102
+ ```bash
103
+ npx tsx src/cli/index.ts cost doctor
104
+ # or: npx tsx src/cli/index.ts cost doctor --report output/report.json
105
+ ```
106
+
70
107
  ## CLI (from npm)
71
108
 
72
109
  ```bash
@@ -87,6 +124,8 @@ const config: HarnessConfig = {
87
124
  timeoutMs: 30000,
88
125
  retryCount: 2,
89
126
  llmTokenBudget: 4000,
127
+ llmMaxOutputTokensPerCall: undefined,
128
+ enableLlmScenarios: true,
90
129
  testGenerationLimit: 10,
91
130
  readOnlyMode: true,
92
131
  requireHumanReview: true,
@@ -102,7 +141,7 @@ const result = await analyzeApp({
102
141
  writeArtifacts: false,
103
142
  });
104
143
 
105
- console.log(result.releaseConfidence, result.gapAnalysis);
144
+ console.log(result.releaseConfidence, result.gapAnalysis.costIntelligence);
106
145
  ```
107
146
 
108
147
  ## Repository
@@ -120,7 +159,7 @@ This package is part of **[Qulib](https://github.com/TapeshN/qulib)** ([repo REA
120
159
  - Optional **authenticated** crawling via `auth` in config (`form-login` or Playwright `storage-state`).
121
160
  - Repo scanner: routes, tests, Cypress structure.
122
161
  - Gap engine: deterministic gaps, **release confidence** with a low-page coverage floor, coverage warnings.
123
- - Reports: `output/report.json` and `output/report.md` when not using **`--ephemeral`**.
162
+ - Reports: `output/report.json` and `output/report.md` when not using **`--ephemeral`** (both include **Cost Intelligence** when present on `gapAnalysis`).
124
163
  - State under `.scan-state/` unless **`--ephemeral`** (no disk writes; full JSON on stdout).
125
164
  - **`npm run clean`** removes generated `output/` and `.scan-state/` and restores `.gitkeep` placeholders.
126
165
 
@@ -158,6 +197,9 @@ Use the same **hostname** for `--url` as your app’s canonical host when you ca
158
197
  - `npm run dev` — CLI via `tsx` (append subcommands, e.g. `npm run dev -- clean`)
159
198
  - `npm run analyze -- --url <url> [--repo <path>] [--config <file>] [--ephemeral]`
160
199
  - `npm run clean` — reset `output/` and `.scan-state/` here
200
+ - `npm run test` — unit tests (cost intelligence + hashing)
201
+ - `npm run smoke` — ephemeral analyze of `https://example.com` (uses this package’s `qulib.config.ts`)
202
+ - `npm run cost-doctor` — print Cost Intelligence from `output/report.json` (run a non-ephemeral `analyze` first)
161
203
  - `npm run build` — compile to `dist/`
162
204
 
163
205
  From the **repository root**:
@@ -195,7 +237,7 @@ npx playwright install chromium
195
237
 
196
238
  ## Output and state (cwd = `packages/core` when you `cd` here)
197
239
 
198
- **Ephemeral:** stdout prints one JSON object: `gapAnalysis`, `discoveredRoutes`, `repoInventory`, `decisionLog`.
240
+ **Ephemeral:** stdout prints one JSON object: `gapAnalysis` (including **`costIntelligence`** when populated), `discoveredRoutes`, `repoInventory`, `decisionLog`.
199
241
 
200
242
  **Persistent:**
201
243
 
package/dist/analyze.d.ts CHANGED
@@ -1,22 +1,34 @@
1
- import type { HarnessConfig, DetectedAuth } from './schemas/config.schema.js';
2
- import { type GapAnalysis } from './schemas/gap-analysis.schema.js';
3
- import type { RouteInventory } from './schemas/route-inventory.schema.js';
1
+ import { type HarnessConfig, type DetectedAuth } from './schemas/config.schema.js';
2
+ import type { Gap, GapAnalysis } from './schemas/gap-analysis.schema.js';
3
+ import { type RouteInventory } from './schemas/route-inventory.schema.js';
4
4
  import type { RepoAnalysis } from './schemas/repo-analysis.schema.js';
5
5
  import type { DecisionLogEntry } from './schemas/decision-log.schema.js';
6
+ import { type PublicSurface } from './schemas/public-surface.schema.js';
7
+ import type { AnalyzeProgressSink } from './harness/progress-log.js';
8
+ export type AnalyzeStatus = 'complete' | 'blocked' | 'partial';
6
9
  export interface AnalyzeOptions {
7
10
  url: string;
8
11
  repoPath?: string;
9
12
  config: HarnessConfig;
10
13
  writeArtifacts?: boolean;
11
14
  skipAuthDetection?: boolean;
15
+ progressLog?: AnalyzeProgressSink;
12
16
  }
13
17
  export interface AnalyzeResult {
14
- releaseConfidence: number;
18
+ status: AnalyzeStatus;
19
+ coverageScore: number | null;
20
+ /** Quality of evaluated pages only; `null` when no evaluable surface produced a score (fully blocked). */
21
+ releaseConfidence: number | null;
22
+ /** Same entries as `gapAnalysis.gaps` for consumers that read a flat `gaps` field. */
23
+ gaps: Gap[];
15
24
  gapAnalysis: GapAnalysis;
25
+ /** Authenticated crawl scope only; empty routes when the scan stopped at an auth wall without credentials. */
16
26
  routeInventory: RouteInventory;
17
27
  repoInventory: RepoAnalysis | null;
18
28
  decisionLog: DecisionLogEntry[];
19
29
  detectedAuth?: DetectedAuth;
30
+ /** Crawled public routes and their gap-derived surface (complete scans); OAuth-wall scans use the pre-auth crawl only. */
31
+ publicSurface: PublicSurface | null;
20
32
  }
21
33
  export declare function analyzeApp(options: AnalyzeOptions): Promise<AnalyzeResult>;
22
34
  //# sourceMappingURL=analyze.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,kCAAkC,CAAC;AACvF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAMzE,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,WAAW,CAAC;IACzB,cAAc,EAAE,cAAc,CAAC;IAC/B,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;IACnC,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA0DhF"}
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,4BAA4B,CAAC;AACnF,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,qCAAqC,CAAC;AAChG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAU7F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/D,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,mBAAmB,CAAC;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,0GAA0G;IAC1G,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,sFAAsF;IACtF,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,WAAW,EAAE,WAAW,CAAC;IACzB,8GAA8G;IAC9G,cAAc,EAAE,cAAc,CAAC;IAC/B,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;IACnC,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,0HAA0H;IAC1H,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;CACrC;AAcD,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA8HhF"}
package/dist/analyze.js CHANGED
@@ -1,61 +1,121 @@
1
- import { GapAnalysisSchema } from './schemas/gap-analysis.schema.js';
1
+ import { RouteInventorySchema } from './schemas/route-inventory.schema.js';
2
+ import { PublicSurfaceSchema } from './schemas/public-surface.schema.js';
2
3
  import { observe } from './phases/observe.js';
3
4
  import { think } from './phases/think.js';
4
5
  import { act } from './phases/act.js';
5
6
  import { detectAuth } from './tools/auth-detector.js';
7
+ import { analyzeGaps, computeCoverageScore, computeQualityScoreFromGaps } from './tools/gap-engine.js';
8
+ import { analyzeAuthSurfaceGaps } from './tools/auth-surface-analyzer.js';
9
+ import { buildPublicSurface } from './tools/public-surface.js';
10
+ import { buildAuthBlockGap } from './tools/auth-block-gap.js';
11
+ import { finalizeGapAnalysisFromDraft } from './phases/think-finalize.js';
12
+ function logScanEnd(progress, result) {
13
+ const rc = result.releaseConfidence === null ? 'null' : String(result.releaseConfidence);
14
+ const cs = result.coverageScore === null ? 'null' : String(result.coverageScore);
15
+ progress?.info(`status=${result.status} | coverageScore=${cs} | releaseConfidence=${rc} | gaps=${result.gaps.length}`);
16
+ for (const g of result.gaps) {
17
+ progress?.debug(`gap id=${g.id} severity=${g.severity} category=${g.category}`);
18
+ }
19
+ if (process.env.QULIB_DEBUG === '1') {
20
+ progress?.debug(`gaps json=${JSON.stringify(result.gaps)}`);
21
+ }
22
+ }
6
23
  export async function analyzeApp(options) {
7
24
  const writeArtifacts = options.writeArtifacts ?? false;
8
25
  const decisionLog = [];
26
+ const progress = options.progressLog;
9
27
  const artifacts = {
10
28
  writeArtifacts,
11
29
  decisionMemory: decisionLog,
30
+ ...(progress !== undefined && { progressLog: progress }),
12
31
  };
32
+ progress?.info(`Starting scan → ${options.url} maxPagesToScan=${options.config.maxPagesToScan}`);
33
+ let detectedAuth;
34
+ let authWall = false;
13
35
  if (!options.config.auth && !options.skipAuthDetection) {
14
- const detection = await detectAuth(options.url, options.config.timeoutMs);
15
- if (detection.hasAuth) {
16
- const gapAnalysis = GapAnalysisSchema.parse({
17
- analyzedAt: new Date().toISOString(),
18
- mode: 'auth-required',
19
- releaseConfidence: 0,
20
- coveragePagesScanned: 0,
21
- coverageBudgetExceeded: false,
22
- coverageWarning: 'auth-required',
23
- gaps: [],
24
- scenarios: [],
25
- generatedTests: [],
26
- });
27
- return {
28
- releaseConfidence: 0,
29
- gapAnalysis,
30
- routeInventory: {
31
- scannedAt: new Date().toISOString(),
32
- baseUrl: options.url,
33
- routes: [],
34
- pagesSkipped: 0,
35
- budgetExceeded: false,
36
- },
37
- repoInventory: null,
38
- decisionLog: [
39
- {
40
- timestamp: new Date().toISOString(),
41
- phase: 'observe',
42
- decision: 'auth-required',
43
- reason: detection.recommendation,
44
- metadata: { detection },
45
- },
46
- ],
47
- detectedAuth: detection,
48
- };
49
- }
36
+ detectedAuth = await detectAuth(options.url, options.config.timeoutMs, progress);
37
+ authWall = Boolean(detectedAuth.hasAuth);
50
38
  }
51
39
  const observed = await observe(options.url, options.repoPath, options.config, artifacts);
40
+ if (authWall && !options.config.auth && detectedAuth) {
41
+ decisionLog.push({
42
+ timestamp: new Date().toISOString(),
43
+ phase: 'observe',
44
+ decision: 'auth-required',
45
+ reason: detectedAuth.recommendation,
46
+ metadata: { detection: detectedAuth },
47
+ });
48
+ const status = observed.routes.routes.length === 0 ? 'blocked' : 'partial';
49
+ if (status === 'blocked') {
50
+ progress?.warn('Scan blocked by auth wall');
51
+ }
52
+ else {
53
+ progress?.warn('Auth wall: continuing with public surface only (partial)');
54
+ }
55
+ const mode = observed.repo ? 'url-repo' : 'url-only';
56
+ const publicAnalysis = analyzeGaps(observed.routes, observed.repo, mode, options.config);
57
+ const publicSurface = PublicSurfaceSchema.parse(buildPublicSurface(observed.routes.routes, publicAnalysis.gaps));
58
+ progress?.info(`Public surface crawl: ${publicSurface.pages.length} page(s) reachable pre-auth`);
59
+ const authSurfaceGaps = await analyzeAuthSurfaceGaps(options.url, detectedAuth, options.config.timeoutMs);
60
+ const authBlockGap = buildAuthBlockGap(options.url);
61
+ const qualityInputGaps = [...publicAnalysis.gaps, ...authSurfaceGaps];
62
+ const qualityScore = computeQualityScoreFromGaps(qualityInputGaps);
63
+ const draftRelease = status === 'blocked' ? null : qualityScore;
64
+ const draft = {
65
+ analyzedAt: new Date().toISOString(),
66
+ mode: 'auth-required',
67
+ releaseConfidence: draftRelease,
68
+ coveragePagesScanned: 0,
69
+ coverageBudgetExceeded: false,
70
+ coverageWarning: 'auth-required',
71
+ gaps: [...authSurfaceGaps, authBlockGap],
72
+ };
73
+ const costContext = {
74
+ mode: publicAnalysis.mode,
75
+ coveragePagesScanned: observed.routes.routes.length,
76
+ releaseConfidence: qualityScore,
77
+ gaps: publicAnalysis.gaps,
78
+ };
79
+ const gapAnalysis = await finalizeGapAnalysisFromDraft(draft, options.config, artifacts, costContext);
80
+ const emptyAuthRoutes = RouteInventorySchema.parse({
81
+ scannedAt: new Date().toISOString(),
82
+ baseUrl: options.url,
83
+ routes: [],
84
+ pagesSkipped: 0,
85
+ budgetExceeded: false,
86
+ });
87
+ await act(gapAnalysis, options.config, artifacts);
88
+ const result = {
89
+ status,
90
+ coverageScore: computeCoverageScore(observed.routes),
91
+ releaseConfidence: draftRelease,
92
+ gaps: gapAnalysis.gaps,
93
+ gapAnalysis,
94
+ routeInventory: emptyAuthRoutes,
95
+ repoInventory: observed.repo,
96
+ decisionLog,
97
+ detectedAuth,
98
+ publicSurface,
99
+ };
100
+ logScanEnd(progress, result);
101
+ return result;
102
+ }
52
103
  const analysis = await think(observed, options.config, artifacts);
53
104
  await act(analysis, options.config, artifacts);
54
- return {
105
+ const publicSurface = PublicSurfaceSchema.parse(buildPublicSurface(observed.routes.routes, analysis.gaps));
106
+ progress?.info(`Public surface crawl: ${publicSurface.pages.length} page(s) reachable pre-auth`);
107
+ const result = {
108
+ status: 'complete',
109
+ coverageScore: computeCoverageScore(observed.routes),
55
110
  releaseConfidence: analysis.releaseConfidence,
111
+ gaps: analysis.gaps,
56
112
  gapAnalysis: analysis,
57
113
  routeInventory: observed.routes,
58
114
  repoInventory: observed.repo,
59
115
  decisionLog,
116
+ ...(detectedAuth !== undefined && { detectedAuth }),
117
+ publicSurface,
60
118
  };
119
+ logScanEnd(progress, result);
120
+ return result;
61
121
  }
@@ -0,0 +1,2 @@
1
+ export declare function runCostDoctor(reportPath: string): Promise<void>;
2
+ //# sourceMappingURL=cost-doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cost-doctor.d.ts","sourceRoot":"","sources":["../../src/cli/cost-doctor.ts"],"names":[],"mappings":"AAIA,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6ErE"}
@@ -0,0 +1,72 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { GapAnalysisSchema } from '../schemas/gap-analysis.schema.js';
4
+ export async function runCostDoctor(reportPath) {
5
+ const abs = resolve(process.cwd(), reportPath);
6
+ let raw;
7
+ try {
8
+ raw = await readFile(abs, 'utf8');
9
+ }
10
+ catch {
11
+ throw new Error(`Could not read ${abs}. Run \`qulib analyze --url <url>\` (without --ephemeral) from this directory first.`);
12
+ }
13
+ const parsed = GapAnalysisSchema.safeParse(JSON.parse(raw));
14
+ if (!parsed.success) {
15
+ throw new Error('File is not a valid gap analysis report (report.json schema mismatch).');
16
+ }
17
+ const ci = parsed.data.costIntelligence;
18
+ if (!ci) {
19
+ console.log('[qulib] No costIntelligence in this report (older scan). Re-run analyze with the current qulib version to populate Cost Intelligence.');
20
+ return;
21
+ }
22
+ console.log('# Qulib cost doctor\n');
23
+ console.log(`Report: ${abs}`);
24
+ console.log(`Analyzed at: ${parsed.data.analyzedAt}\n`);
25
+ console.log('## Token ceiling (per LLM completion)\n');
26
+ console.log(`- maxOutputTokensPerLlmCall: ${ci.maxOutputTokensPerLlmCall}`);
27
+ console.log(`- budgetRole: ${ci.budgetRole}\n`);
28
+ console.log('## Usage (this scan)\n');
29
+ console.log(`- Input / output tokens: ${ci.usageSummary.totalInputTokens} / ${ci.usageSummary.totalOutputTokens} (${ci.usageSummary.dataQuality})`);
30
+ if (ci.budgetWarnings.length) {
31
+ console.log('\n## Budget warnings\n');
32
+ for (const w of ci.budgetWarnings) {
33
+ console.log(`- ${w}`);
34
+ }
35
+ }
36
+ else {
37
+ console.log('\n## Budget warnings\n\n- (none)\n');
38
+ }
39
+ if (ci.repeatedOperations.length) {
40
+ console.log('\n## Repeated AI patterns\n');
41
+ for (const r of ci.repeatedOperations) {
42
+ console.log(`- ${r.promptHash} ×${r.count}`);
43
+ console.log(` ${r.recommendation}`);
44
+ }
45
+ }
46
+ else {
47
+ console.log('\n## Repeated AI patterns\n\n- (none in this run)\n');
48
+ }
49
+ console.log('\n## Deterministic maturity\n');
50
+ console.log(`- ${ci.deterministicMaturity.label}`);
51
+ console.log(`- ${ci.deterministicMaturity.rationale}`);
52
+ if (ci.deterministicMaturity.ceilingNote) {
53
+ console.log(`- _${ci.deterministicMaturity.ceilingNote}_`);
54
+ }
55
+ console.log('\n## Conversion recommendations\n');
56
+ for (const c of ci.conversionRecommendations) {
57
+ console.log(`- ${c}`);
58
+ }
59
+ const topGap = [...parsed.data.gaps].sort((a, b) => {
60
+ const o = { critical: 0, high: 1, medium: 2, low: 3 };
61
+ return o[a.severity] - o[b.severity];
62
+ })[0];
63
+ console.log('\n## Next best deterministic check\n');
64
+ if (topGap) {
65
+ console.log(`- Prioritize **${topGap.category}** on \`${topGap.path}\` (${topGap.severity}): ${topGap.reason}`);
66
+ }
67
+ else {
68
+ console.log('- No gaps in this report; extend crawl coverage or add auth before chasing new checks.');
69
+ }
70
+ console.log('\n---\n');
71
+ console.log('TODO: correlate multiple historical reports and CI adapters for cross-run “cost doctor” diffing (not implemented yet).');
72
+ }
package/dist/cli/index.js CHANGED
@@ -6,6 +6,7 @@ import { z } from 'zod';
6
6
  import { HarnessConfigSchema } from '../schemas/config.schema.js';
7
7
  import { analyzeApp } from '../analyze.js';
8
8
  import { detectAuth } from '../tools/auth-detector.js';
9
+ import { exploreAuth } from '../tools/auth-explorer.js';
9
10
  const program = new Command();
10
11
  const AnalyzeUrlSchema = z.string().url();
11
12
  const FormLoginCliSchema = z.object({
@@ -92,8 +93,13 @@ async function runAnalyze(options) {
92
93
  });
93
94
  if (ephemeral) {
94
95
  console.log(JSON.stringify({
96
+ status: result.status,
97
+ coverageScore: result.coverageScore,
98
+ releaseConfidence: result.releaseConfidence,
99
+ gaps: result.gaps,
95
100
  gapAnalysis: result.gapAnalysis,
96
101
  discoveredRoutes: result.routeInventory,
102
+ publicSurface: result.publicSurface,
97
103
  repoInventory: result.repoInventory,
98
104
  decisionLog: result.decisionLog,
99
105
  ...(result.detectedAuth !== undefined && { detectedAuth: result.detectedAuth }),
@@ -125,6 +131,15 @@ program
125
131
  await fs.writeFile('.scan-state/.gitkeep', '', 'utf8');
126
132
  console.log('[qulib] clean complete');
127
133
  });
134
+ const costCmd = program.command('cost').description('Cost intelligence helpers');
135
+ costCmd
136
+ .command('doctor')
137
+ .description('Print Cost Intelligence from output/report.json (run analyze without --ephemeral first)')
138
+ .option('--report <file>', 'Path to report.json relative to cwd', 'output/report.json')
139
+ .action(async (opts) => {
140
+ const { runCostDoctor } = await import('./cost-doctor.js');
141
+ await runCostDoctor(opts.report);
142
+ });
128
143
  program
129
144
  .command('analyze')
130
145
  .description('Analyze an app for quality gaps')
@@ -166,6 +181,15 @@ program
166
181
  submitSelector: options.submitSelector,
167
182
  });
168
183
  });
184
+ program
185
+ .command('explore-auth')
186
+ .description('Explore all sign-in paths (OAuth, forms, magic link) for agent-driven setup before analyze')
187
+ .requiredOption('--url <url>', 'URL of the app or login page')
188
+ .option('--timeout <ms>', 'Navigation timeout in ms', '20000')
189
+ .action(async (options) => {
190
+ const result = await exploreAuth(options.url, parseInt(options.timeout, 10));
191
+ console.log(JSON.stringify(result, null, 2));
192
+ });
169
193
  program
170
194
  .command('detect-auth')
171
195
  .description('Detect the authentication pattern used by a deployed web app')
@@ -176,6 +200,43 @@ program
176
200
  console.log(JSON.stringify(result, null, 2));
177
201
  });
178
202
  const authCmd = program.command('auth').description('Authentication helpers for scans');
203
+ const providersCmd = authCmd
204
+ .command('providers')
205
+ .description('User-local OAuth/SSO button patterns (~/.qulib/providers.json)');
206
+ providersCmd
207
+ .command('list')
208
+ .description('List user-local providers registered on this machine')
209
+ .action(async () => {
210
+ const { listUserProviders } = await import('../tools/user-providers.js');
211
+ const providers = listUserProviders();
212
+ console.log(JSON.stringify(providers, null, 2));
213
+ });
214
+ providersCmd
215
+ .command('add')
216
+ .description('Register a custom provider pattern (case-insensitive regex source)')
217
+ .requiredOption('--id <id>', 'Stable id (kebab-case), e.g. scholastic-sync')
218
+ .requiredOption('--label <label>', 'Human-readable label')
219
+ .requiredOption('--pattern <regex>', 'Regex source, e.g. scholastic sync')
220
+ .action(async (opts) => {
221
+ try {
222
+ new RegExp(opts.pattern, 'i');
223
+ }
224
+ catch {
225
+ throw new Error(`Invalid regex pattern: ${opts.pattern}`);
226
+ }
227
+ const { addUserProvider } = await import('../tools/user-providers.js');
228
+ addUserProvider({ id: opts.id, label: opts.label, pattern: opts.pattern });
229
+ console.log(`[qulib] Added provider "${opts.label}" (id: ${opts.id}) to ~/.qulib/providers.json`);
230
+ });
231
+ providersCmd
232
+ .command('remove')
233
+ .description('Remove a user-local provider by id')
234
+ .requiredOption('--id <id>', 'Provider id to remove')
235
+ .action(async (opts) => {
236
+ const { removeUserProvider } = await import('../tools/user-providers.js');
237
+ const removed = removeUserProvider(opts.id);
238
+ console.log(removed ? `[qulib] Removed "${opts.id}"` : `[qulib] No provider with id "${opts.id}" found`);
239
+ });
179
240
  authCmd
180
241
  .command('init')
181
242
  .description('Open a browser, let the user log in manually, save the storage state to a file for reuse')
@@ -0,0 +1,7 @@
1
+ export interface AnalyzeProgressSink {
2
+ info(message: string): void;
3
+ warn(message: string): void;
4
+ error(message: string): void;
5
+ debug(message: string): void;
6
+ }
7
+ //# sourceMappingURL=progress-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress-log.d.ts","sourceRoot":"","sources":["../../src/harness/progress-log.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B"}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,8 @@
1
1
  import type { DecisionLogEntry } from '../schemas/decision-log.schema.js';
2
+ import type { AnalyzeProgressSink } from './progress-log.js';
2
3
  export type RunArtifactsOptions = {
3
4
  writeArtifacts: boolean;
4
5
  decisionMemory?: DecisionLogEntry[];
6
+ progressLog?: AnalyzeProgressSink;
5
7
  };
6
8
  //# sourceMappingURL=run-options.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"run-options.d.ts","sourceRoot":"","sources":["../../src/harness/run-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAE1E,MAAM,MAAM,mBAAmB,GAAG;IAChC,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACrC,CAAC"}
1
+ {"version":3,"file":"run-options.d.ts","sourceRoot":"","sources":["../../src/harness/run-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAC1E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACpC,WAAW,CAAC,EAAE,mBAAmB,CAAC;CACnC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  export { analyzeApp } from './analyze.js';
2
2
  export { detectAuth } from './tools/auth-detector.js';
3
- export type { AnalyzeOptions, AnalyzeResult } from './analyze.js';
4
- export type { HarnessConfig, AuthConfig, RouteInventory, GapAnalysis, RepoAnalysis, DetectedAuth, } from './schemas/index.js';
3
+ export { exploreAuth } from './tools/auth-explorer.js';
4
+ export { addUserProvider, removeUserProvider, listUserProviders } from './tools/user-providers.js';
5
+ export { resolveMaxOutputTokensPerLlmCall } from './schemas/config.schema.js';
6
+ export type { AnalyzeOptions, AnalyzeResult, AnalyzeStatus } from './analyze.js';
7
+ export type { AnalyzeProgressSink } from './harness/progress-log.js';
8
+ export type { HarnessConfig, AuthConfig, RouteInventory, GapAnalysis, RepoAnalysis, DetectedAuth, AuthExploration, AuthPath, AuthPathRequirements, CostIntelligence, LlmUsageRecord, RepeatedAiPattern, DeterministicMaturity, PublicSurface, } from './schemas/index.js';
5
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,YAAY,EACV,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,YAAY,GACb,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnG,OAAO,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC;AAC9E,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjF,YAAY,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,YAAY,EACV,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,aAAa,GACd,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,5 @@
1
1
  export { analyzeApp } from './analyze.js';
2
2
  export { detectAuth } from './tools/auth-detector.js';
3
+ export { exploreAuth } from './tools/auth-explorer.js';
4
+ export { addUserProvider, removeUserProvider, listUserProviders } from './tools/user-providers.js';
5
+ export { resolveMaxOutputTokensPerLlmCall } from './schemas/config.schema.js';
@@ -0,0 +1,2 @@
1
+ export declare function hashForCostIntelligence(payload: string): string;
2
+ //# sourceMappingURL=content-hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-hash.d.ts","sourceRoot":"","sources":["../../src/llm/content-hash.ts"],"names":[],"mappings":"AAEA,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE/D"}
@@ -0,0 +1,4 @@
1
+ import { createHash } from 'node:crypto';
2
+ export function hashForCostIntelligence(payload) {
3
+ return createHash('sha256').update(payload, 'utf8').digest('hex').slice(0, 32);
4
+ }
@@ -1,7 +1,7 @@
1
1
  export function buildGapPrompt(gaps, limit) {
2
2
  const topGaps = [...gaps]
3
3
  .sort((a, b) => {
4
- const order = { high: 0, medium: 1, low: 2 };
4
+ const order = { critical: 0, high: 1, medium: 2, low: 3 };
5
5
  return order[a.severity] - order[b.severity];
6
6
  })
7
7
  .slice(0, limit);
@@ -0,0 +1,29 @@
1
+ import type { GapAnalysis } from '../schemas/gap-analysis.schema.js';
2
+ import type { CostIntelligence, DeterministicMaturity, LlmUsageRecord, RepeatedAiPattern } from '../schemas/cost-intelligence.schema.js';
3
+ export declare function summarizeUsageQuality(records: LlmUsageRecord[]): CostIntelligence['usageSummary'];
4
+ export declare function buildBudgetWarnings(records: LlmUsageRecord[], maxOutputTokensPerLlmCall: number): string[];
5
+ export declare function findRepeatedPromptPatterns(records: LlmUsageRecord[]): RepeatedAiPattern[];
6
+ export declare function buildConversionRecommendations(params: {
7
+ scenarioSource: 'llm' | 'template';
8
+ repeatedOperations: RepeatedAiPattern[];
9
+ budgetWarnings: string[];
10
+ gapCount: number;
11
+ }): string[];
12
+ export declare function computeDeterministicMaturity(params: {
13
+ mode: GapAnalysis['mode'];
14
+ coveragePagesScanned: number;
15
+ gapCount: number;
16
+ scenarioSource: 'llm' | 'template';
17
+ repeatedOperations: RepeatedAiPattern[];
18
+ releaseConfidence: number | null;
19
+ requireHumanReview: boolean;
20
+ }): DeterministicMaturity;
21
+ export declare function assembleCostIntelligence(params: {
22
+ maxOutputTokensPerLlmCall: number;
23
+ records: LlmUsageRecord[];
24
+ partial: Pick<GapAnalysis, 'mode' | 'coveragePagesScanned' | 'releaseConfidence' | 'gaps'>;
25
+ scenarioSource: 'llm' | 'template';
26
+ requireHumanReview: boolean;
27
+ }): CostIntelligence;
28
+ export declare function costIntelligenceForAuthBlocked(maxOutputTokensPerLlmCall: number): CostIntelligence;
29
+ //# sourceMappingURL=cost-intelligence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cost-intelligence.d.ts","sourceRoot":"","sources":["../../src/llm/cost-intelligence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,KAAK,EACV,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACd,iBAAiB,EAClB,MAAM,wCAAwC,CAAC;AAEhD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,gBAAgB,CAAC,cAAc,CAAC,CAYjG;AAED,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,cAAc,EAAE,EACzB,yBAAyB,EAAE,MAAM,GAChC,MAAM,EAAE,CAuBV;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,iBAAiB,EAAE,CAiBzF;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,cAAc,EAAE,KAAK,GAAG,UAAU,CAAC;IACnC,kBAAkB,EAAE,iBAAiB,EAAE,CAAC;IACxC,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,EAAE,CAuBX;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE;IACnD,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,KAAK,GAAG,UAAU,CAAC;IACnC,kBAAkB,EAAE,iBAAiB,EAAE,CAAC;IACxC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,kBAAkB,EAAE,OAAO,CAAC;CAC7B,GAAG,qBAAqB,CA+CxB;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,yBAAyB,EAAE,MAAM,CAAC;IAClC,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,OAAO,EAAE,IAAI,CACX,WAAW,EACX,MAAM,GAAG,sBAAsB,GAAG,mBAAmB,GAAG,MAAM,CAC/D,CAAC;IACF,cAAc,EAAE,KAAK,GAAG,UAAU,CAAC;IACnC,kBAAkB,EAAE,OAAO,CAAC;CAC7B,GAAG,gBAAgB,CA8BnB;AAED,wBAAgB,8BAA8B,CAAC,yBAAyB,EAAE,MAAM,GAAG,gBAAgB,CAoBlG"}