@qulib/core 0.1.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 (99) hide show
  1. package/README.md +146 -0
  2. package/bin/qulib.js +17 -0
  3. package/dist/adapters/adapter-factory.d.ts +5 -0
  4. package/dist/adapters/adapter-factory.d.ts.map +1 -0
  5. package/dist/adapters/adapter-factory.js +21 -0
  6. package/dist/adapters/adapter.interface.d.ts +7 -0
  7. package/dist/adapters/adapter.interface.d.ts.map +1 -0
  8. package/dist/adapters/adapter.interface.js +1 -0
  9. package/dist/adapters/api-adapter.d.ts +8 -0
  10. package/dist/adapters/api-adapter.d.ts.map +1 -0
  11. package/dist/adapters/api-adapter.js +9 -0
  12. package/dist/adapters/cypress-component-adapter.d.ts +8 -0
  13. package/dist/adapters/cypress-component-adapter.d.ts.map +1 -0
  14. package/dist/adapters/cypress-component-adapter.js +9 -0
  15. package/dist/adapters/cypress-e2e-adapter.d.ts +8 -0
  16. package/dist/adapters/cypress-e2e-adapter.d.ts.map +1 -0
  17. package/dist/adapters/cypress-e2e-adapter.js +9 -0
  18. package/dist/adapters/playwright-adapter.d.ts +8 -0
  19. package/dist/adapters/playwright-adapter.d.ts.map +1 -0
  20. package/dist/adapters/playwright-adapter.js +9 -0
  21. package/dist/analyze.d.ts +20 -0
  22. package/dist/analyze.d.ts.map +1 -0
  23. package/dist/analyze.js +21 -0
  24. package/dist/cli/index.d.ts +3 -0
  25. package/dist/cli/index.d.ts.map +1 -0
  26. package/dist/cli/index.js +102 -0
  27. package/dist/harness/decision-logger.d.ts +7 -0
  28. package/dist/harness/decision-logger.d.ts.map +1 -0
  29. package/dist/harness/decision-logger.js +68 -0
  30. package/dist/harness/run-options.d.ts +6 -0
  31. package/dist/harness/run-options.d.ts.map +1 -0
  32. package/dist/harness/run-options.js +1 -0
  33. package/dist/harness/state-manager.d.ts +6 -0
  34. package/dist/harness/state-manager.d.ts.map +1 -0
  35. package/dist/harness/state-manager.js +64 -0
  36. package/dist/index.d.ts +4 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +1 -0
  39. package/dist/llm/context-builder.d.ts +3 -0
  40. package/dist/llm/context-builder.d.ts.map +1 -0
  41. package/dist/llm/context-builder.js +33 -0
  42. package/dist/llm/provider.d.ts +4 -0
  43. package/dist/llm/provider.d.ts.map +1 -0
  44. package/dist/llm/provider.js +56 -0
  45. package/dist/phases/act.d.ts +5 -0
  46. package/dist/phases/act.d.ts.map +1 -0
  47. package/dist/phases/act.js +41 -0
  48. package/dist/phases/observe.d.ts +10 -0
  49. package/dist/phases/observe.d.ts.map +1 -0
  50. package/dist/phases/observe.js +48 -0
  51. package/dist/phases/think.d.ts +6 -0
  52. package/dist/phases/think.d.ts.map +1 -0
  53. package/dist/phases/think.js +85 -0
  54. package/dist/reporters/json-reporter.d.ts +3 -0
  55. package/dist/reporters/json-reporter.d.ts.map +1 -0
  56. package/dist/reporters/json-reporter.js +8 -0
  57. package/dist/reporters/markdown-reporter.d.ts +3 -0
  58. package/dist/reporters/markdown-reporter.d.ts.map +1 -0
  59. package/dist/reporters/markdown-reporter.js +42 -0
  60. package/dist/schemas/config.schema.d.ts +327 -0
  61. package/dist/schemas/config.schema.d.ts.map +1 -0
  62. package/dist/schemas/config.schema.js +39 -0
  63. package/dist/schemas/decision-log.schema.d.ts +22 -0
  64. package/dist/schemas/decision-log.schema.d.ts.map +1 -0
  65. package/dist/schemas/decision-log.schema.js +8 -0
  66. package/dist/schemas/gap-analysis.schema.d.ts +363 -0
  67. package/dist/schemas/gap-analysis.schema.d.ts.map +1 -0
  68. package/dist/schemas/gap-analysis.schema.js +60 -0
  69. package/dist/schemas/index.d.ts +6 -0
  70. package/dist/schemas/index.d.ts.map +1 -0
  71. package/dist/schemas/index.js +5 -0
  72. package/dist/schemas/repo-analysis.schema.d.ts +165 -0
  73. package/dist/schemas/repo-analysis.schema.d.ts.map +1 -0
  74. package/dist/schemas/repo-analysis.schema.js +29 -0
  75. package/dist/schemas/route-inventory.schema.d.ts +241 -0
  76. package/dist/schemas/route-inventory.schema.d.ts.map +1 -0
  77. package/dist/schemas/route-inventory.schema.js +30 -0
  78. package/dist/tools/auth.d.ts +4 -0
  79. package/dist/tools/auth.d.ts.map +1 -0
  80. package/dist/tools/auth.js +35 -0
  81. package/dist/tools/cypress-explorer.d.ts +7 -0
  82. package/dist/tools/cypress-explorer.d.ts.map +1 -0
  83. package/dist/tools/cypress-explorer.js +5 -0
  84. package/dist/tools/explorer-factory.d.ts +4 -0
  85. package/dist/tools/explorer-factory.d.ts.map +1 -0
  86. package/dist/tools/explorer-factory.js +12 -0
  87. package/dist/tools/explorer.interface.d.ts +6 -0
  88. package/dist/tools/explorer.interface.d.ts.map +1 -0
  89. package/dist/tools/explorer.interface.js +1 -0
  90. package/dist/tools/gap-engine.d.ts +6 -0
  91. package/dist/tools/gap-engine.d.ts.map +1 -0
  92. package/dist/tools/gap-engine.js +101 -0
  93. package/dist/tools/playwright-explorer.d.ts +7 -0
  94. package/dist/tools/playwright-explorer.d.ts.map +1 -0
  95. package/dist/tools/playwright-explorer.js +150 -0
  96. package/dist/tools/repo-scanner.d.ts +3 -0
  97. package/dist/tools/repo-scanner.d.ts.map +1 -0
  98. package/dist/tools/repo-scanner.js +147 -0
  99. package/package.json +54 -0
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # @qulib/core
2
+
3
+ **@qulib/core** is the TypeScript-first Qulib package for analyzing deployed web apps (and optionally a local repo) and surfacing honest quality gaps.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @qulib/core
9
+ ```
10
+
11
+ ## CLI (from npm)
12
+
13
+ ```bash
14
+ npx @qulib/core analyze --url https://example.com
15
+ ```
16
+
17
+ Use `npx playwright install chromium` the first time you scan (Playwright is a dependency).
18
+
19
+ ## Programmatic API
20
+
21
+ ```ts
22
+ import { analyzeApp, type HarnessConfig } from '@qulib/core';
23
+
24
+ const config: HarnessConfig = {
25
+ maxPagesToScan: 20,
26
+ maxDepth: 3,
27
+ minPagesForConfidence: 3,
28
+ timeoutMs: 30000,
29
+ retryCount: 2,
30
+ llmTokenBudget: 4000,
31
+ testGenerationLimit: 10,
32
+ readOnlyMode: true,
33
+ requireHumanReview: true,
34
+ failOnConsoleError: false,
35
+ explorer: 'playwright',
36
+ defaultAdapter: 'playwright',
37
+ adapters: ['playwright', 'cypress-e2e'],
38
+ };
39
+
40
+ const result = await analyzeApp({
41
+ url: 'https://example.com',
42
+ config,
43
+ writeArtifacts: false,
44
+ });
45
+
46
+ console.log(result.releaseConfidence, result.gapAnalysis);
47
+ ```
48
+
49
+ ## Repository
50
+
51
+ Source and issues: **[github.com/TapeshN/qulib](https://github.com/TapeshN/qulib)**.
52
+
53
+ ## Monorepo context
54
+
55
+ 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).
56
+
57
+ ## Current capabilities
58
+
59
+ - CLI `analyze` flow: `observe` → `think` → `act`.
60
+ - Playwright explorer: route discovery, **axe-core** (WCAG 2.0 A/AA), sampled internal link HEAD checks.
61
+ - Optional **authenticated** crawling via `auth` in config (`form-login` or Playwright `storage-state`).
62
+ - Repo scanner: routes, tests, Cypress structure.
63
+ - Gap engine: deterministic gaps, **release confidence** with a low-page coverage floor, coverage warnings.
64
+ - Reports: `output/report.json` and `output/report.md` when not using **`--ephemeral`**.
65
+ - State under `.scan-state/` unless **`--ephemeral`** (no disk writes; full JSON on stdout).
66
+ - **`npm run clean`** removes generated `output/` and `.scan-state/` and restores `.gitkeep` placeholders.
67
+
68
+ ## Tech stack
69
+
70
+ TypeScript (strict, NodeNext), Commander, Zod, Playwright, @axe-core/playwright, fast-glob; optional Anthropic API for scenario generation.
71
+
72
+ ## Layout
73
+
74
+ ```text
75
+ src/
76
+ adapters/ # test rendering adapters
77
+ analyze.ts # programmatic API (also used by @qulib/mcp)
78
+ cli/ # CLI entry
79
+ harness/ # state + decision logging
80
+ llm/ # LLM contracts
81
+ phases/ # observe / think / act
82
+ reporters/ # JSON + Markdown reports
83
+ schemas/ # Zod schemas
84
+ tools/ # explorers, auth, gap engine, repo scanner
85
+ ```
86
+
87
+ Repo rules: see [`CLAUDE.md`](../../CLAUDE.md).
88
+
89
+ ## Configuration
90
+
91
+ Default file: **`qulib.config.ts`** in this package directory (or pass **`--config <path>`** relative to the process working directory).
92
+
93
+ 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.
94
+
95
+ Use the same **origin** for `--url` as the app uses after login so same-origin links are discovered during the crawl.
96
+
97
+ ## Scripts (from `packages/core`)
98
+
99
+ - `npm run dev` — CLI via `tsx` (append subcommands, e.g. `npm run dev -- clean`)
100
+ - `npm run analyze -- --url <url> [--repo <path>] [--config <file>] [--ephemeral]`
101
+ - `npm run clean` — reset `output/` and `.scan-state/` here
102
+ - `npm run build` — compile to `dist/`
103
+
104
+ From the **repository root**:
105
+
106
+ - `npm run analyze -w @qulib/core -- --url <url> …`
107
+ - `npm run clean` — runs core clean via workspace
108
+
109
+ Binary name after publish: **`qulib`** (see `package.json` `bin`).
110
+
111
+ ## Usage examples
112
+
113
+ ```bash
114
+ cd packages/core
115
+
116
+ # app only
117
+ npm run analyze -- --url http://localhost:3000
118
+
119
+ # app + repo
120
+ npm run analyze -- --url http://localhost:3000 --repo ../your-app
121
+
122
+ # local auth config (keep out of git)
123
+ npm run analyze -- --config ../../qulib.test-auth.config.ts --url https://example.com
124
+
125
+ # ephemeral: JSON on stdout, logs on stderr
126
+ npm run analyze -- --url https://example.com --ephemeral > report.bundle.json
127
+
128
+ npm run clean
129
+ ```
130
+
131
+ ## Playwright browsers
132
+
133
+ ```bash
134
+ npx playwright install chromium
135
+ ```
136
+
137
+ ## Output and state (cwd = `packages/core` when you `cd` here)
138
+
139
+ **Ephemeral:** stdout prints one JSON object: `gapAnalysis`, `discoveredRoutes`, `repoInventory`, `decisionLog`.
140
+
141
+ **Persistent:**
142
+
143
+ - `.scan-state/discovered-routes.json`, `gap-analysis.json`, `decision-log.json`, and `repo-inventory.json` when `--repo` is set
144
+ - `output/report.json`, `output/report.md`
145
+
146
+ For more options (`repoPath`, loading config from disk), see `src/analyze.ts` in the repository.
package/bin/qulib.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, resolve } from 'node:path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const cliPath = resolve(__dirname, '../src/cli/index.ts');
9
+
10
+ const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
11
+ const child = spawn(npxCmd, ['tsx', cliPath, ...process.argv.slice(2)], {
12
+ stdio: 'inherit',
13
+ });
14
+
15
+ child.on('exit', (code) => {
16
+ process.exit(code ?? 1);
17
+ });
@@ -0,0 +1,5 @@
1
+ import type { AdapterType } from '../schemas/config.schema.js';
2
+ import type { TestAdapter } from './adapter.interface.js';
3
+ export declare function createAdapter(type: AdapterType): TestAdapter;
4
+ export declare function createAdapters(types: AdapterType[]): TestAdapter[];
5
+ //# sourceMappingURL=adapter-factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter-factory.d.ts","sourceRoot":"","sources":["../../src/adapters/adapter-factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAM1D,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAa5D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,WAAW,EAAE,CAElE"}
@@ -0,0 +1,21 @@
1
+ import { PlaywrightAdapter } from './playwright-adapter.js';
2
+ import { CypressE2EAdapter } from './cypress-e2e-adapter.js';
3
+ import { CypressComponentAdapter } from './cypress-component-adapter.js';
4
+ import { ApiAdapter } from './api-adapter.js';
5
+ export function createAdapter(type) {
6
+ switch (type) {
7
+ case 'playwright':
8
+ return new PlaywrightAdapter();
9
+ case 'cypress-e2e':
10
+ return new CypressE2EAdapter();
11
+ case 'cypress-component':
12
+ return new CypressComponentAdapter();
13
+ case 'api':
14
+ return new ApiAdapter();
15
+ default:
16
+ throw new Error(`Unknown adapter type: ${type}`);
17
+ }
18
+ }
19
+ export function createAdapters(types) {
20
+ return types.map(createAdapter);
21
+ }
@@ -0,0 +1,7 @@
1
+ import type { NeutralScenario, GeneratedTest } from '../schemas/gap-analysis.schema.js';
2
+ export interface TestAdapter {
3
+ readonly adapterType: string;
4
+ render(scenario: NeutralScenario): GeneratedTest;
5
+ renderAll(scenarios: NeutralScenario[]): GeneratedTest[];
6
+ }
7
+ //# sourceMappingURL=adapter.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.interface.d.ts","sourceRoot":"","sources":["../../src/adapters/adapter.interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAExF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa,CAAC;IACjD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE,CAAC;CAC1D"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { TestAdapter } from './adapter.interface.js';
2
+ import type { NeutralScenario, GeneratedTest } from '../schemas/gap-analysis.schema.js';
3
+ export declare class ApiAdapter implements TestAdapter {
4
+ readonly adapterType = "api";
5
+ render(scenario: NeutralScenario): GeneratedTest;
6
+ renderAll(scenarios: NeutralScenario[]): GeneratedTest[];
7
+ }
8
+ //# sourceMappingURL=api-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/api-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAExF,qBAAa,UAAW,YAAW,WAAW;IAC5C,QAAQ,CAAC,WAAW,SAAS;IAE7B,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAIhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}
@@ -0,0 +1,9 @@
1
+ export class ApiAdapter {
2
+ adapterType = 'api';
3
+ render(scenario) {
4
+ throw new Error('Not implemented');
5
+ }
6
+ renderAll(scenarios) {
7
+ throw new Error('Not implemented');
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ import type { TestAdapter } from './adapter.interface.js';
2
+ import type { NeutralScenario, GeneratedTest } from '../schemas/gap-analysis.schema.js';
3
+ export declare class CypressComponentAdapter implements TestAdapter {
4
+ readonly adapterType = "cypress-component";
5
+ render(scenario: NeutralScenario): GeneratedTest;
6
+ renderAll(scenarios: NeutralScenario[]): GeneratedTest[];
7
+ }
8
+ //# sourceMappingURL=cypress-component-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cypress-component-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/cypress-component-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAExF,qBAAa,uBAAwB,YAAW,WAAW;IACzD,QAAQ,CAAC,WAAW,uBAAuB;IAE3C,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAIhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}
@@ -0,0 +1,9 @@
1
+ export class CypressComponentAdapter {
2
+ adapterType = 'cypress-component';
3
+ render(scenario) {
4
+ throw new Error('Not implemented');
5
+ }
6
+ renderAll(scenarios) {
7
+ throw new Error('Not implemented');
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ import type { TestAdapter } from './adapter.interface.js';
2
+ import type { NeutralScenario, GeneratedTest } from '../schemas/gap-analysis.schema.js';
3
+ export declare class CypressE2EAdapter implements TestAdapter {
4
+ readonly adapterType = "cypress-e2e";
5
+ render(scenario: NeutralScenario): GeneratedTest;
6
+ renderAll(scenarios: NeutralScenario[]): GeneratedTest[];
7
+ }
8
+ //# sourceMappingURL=cypress-e2e-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cypress-e2e-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/cypress-e2e-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAExF,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,WAAW,iBAAiB;IAErC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAIhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}
@@ -0,0 +1,9 @@
1
+ export class CypressE2EAdapter {
2
+ adapterType = 'cypress-e2e';
3
+ render(scenario) {
4
+ throw new Error('Not implemented');
5
+ }
6
+ renderAll(scenarios) {
7
+ throw new Error('Not implemented');
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ import type { TestAdapter } from './adapter.interface.js';
2
+ import type { NeutralScenario, GeneratedTest } from '../schemas/gap-analysis.schema.js';
3
+ export declare class PlaywrightAdapter implements TestAdapter {
4
+ readonly adapterType = "playwright";
5
+ render(scenario: NeutralScenario): GeneratedTest;
6
+ renderAll(scenarios: NeutralScenario[]): GeneratedTest[];
7
+ }
8
+ //# sourceMappingURL=playwright-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/playwright-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAExF,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,WAAW,gBAAgB;IAEpC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAIhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}
@@ -0,0 +1,9 @@
1
+ export class PlaywrightAdapter {
2
+ adapterType = 'playwright';
3
+ render(scenario) {
4
+ throw new Error('Not implemented');
5
+ }
6
+ renderAll(scenarios) {
7
+ throw new Error('Not implemented');
8
+ }
9
+ }
@@ -0,0 +1,20 @@
1
+ import type { HarnessConfig } 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';
4
+ import type { RepoAnalysis } from './schemas/repo-analysis.schema.js';
5
+ import type { DecisionLogEntry } from './schemas/decision-log.schema.js';
6
+ export interface AnalyzeOptions {
7
+ url: string;
8
+ repoPath?: string;
9
+ config: HarnessConfig;
10
+ writeArtifacts?: boolean;
11
+ }
12
+ export interface AnalyzeResult {
13
+ releaseConfidence: number;
14
+ gapAnalysis: GapAnalysis;
15
+ routeInventory: RouteInventory;
16
+ repoInventory: RepoAnalysis | null;
17
+ decisionLog: DecisionLogEntry[];
18
+ }
19
+ export declare function analyzeApp(options: AnalyzeOptions): Promise<AnalyzeResult>;
20
+ //# sourceMappingURL=analyze.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AACpE,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;AAKzE,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;CAC1B;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;CACjC;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAmBhF"}
@@ -0,0 +1,21 @@
1
+ import { observe } from './phases/observe.js';
2
+ import { think } from './phases/think.js';
3
+ import { act } from './phases/act.js';
4
+ export async function analyzeApp(options) {
5
+ const writeArtifacts = options.writeArtifacts ?? false;
6
+ const decisionLog = [];
7
+ const artifacts = {
8
+ writeArtifacts,
9
+ decisionMemory: decisionLog,
10
+ };
11
+ const observed = await observe(options.url, options.repoPath, options.config, artifacts);
12
+ const analysis = await think(observed, options.config, artifacts);
13
+ await act(analysis, options.config, artifacts);
14
+ return {
15
+ releaseConfidence: analysis.releaseConfidence,
16
+ gapAnalysis: analysis,
17
+ routeInventory: observed.routes,
18
+ repoInventory: observed.repo,
19
+ decisionLog,
20
+ };
21
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { z } from 'zod';
6
+ import { HarnessConfigSchema } from '../schemas/config.schema.js';
7
+ import { analyzeApp } from '../analyze.js';
8
+ const program = new Command();
9
+ const AnalyzeUrlSchema = z.string().url();
10
+ async function loadConfigFile(relativePath) {
11
+ const configPath = resolve(process.cwd(), relativePath);
12
+ const configModule = await import(pathToFileURL(configPath).href);
13
+ return HarnessConfigSchema.parse(configModule.default);
14
+ }
15
+ function redactConfigForLog(config) {
16
+ const base = { ...config };
17
+ if (config.auth?.type === 'form-login') {
18
+ base.auth = {
19
+ ...config.auth,
20
+ credentials: {
21
+ username: config.auth.credentials.username,
22
+ password: '***',
23
+ },
24
+ };
25
+ }
26
+ return base;
27
+ }
28
+ async function runAnalyze(options) {
29
+ const validatedUrl = AnalyzeUrlSchema.parse(options.url);
30
+ const mode = options.repo ? 'url-repo' : 'url-only';
31
+ const config = await loadConfigFile(options.configFile ?? 'qulib.config.ts');
32
+ const ephemeral = options.ephemeral ?? false;
33
+ const writeArtifacts = !ephemeral;
34
+ if (ephemeral) {
35
+ console.error('[qulib] Ephemeral mode: no disk writes; full result JSON on stdout');
36
+ }
37
+ else {
38
+ console.log('[qulib] Detected mode:', mode);
39
+ console.log('[qulib] Active config:', redactConfigForLog(config));
40
+ }
41
+ const result = await analyzeApp({
42
+ url: validatedUrl,
43
+ repoPath: options.repo,
44
+ config,
45
+ writeArtifacts,
46
+ });
47
+ if (ephemeral) {
48
+ console.log(JSON.stringify({
49
+ gapAnalysis: result.gapAnalysis,
50
+ discoveredRoutes: result.routeInventory,
51
+ repoInventory: result.repoInventory,
52
+ decisionLog: result.decisionLog,
53
+ }, null, 2));
54
+ }
55
+ }
56
+ program
57
+ .name('qulib')
58
+ .description('Qulib — QA harness')
59
+ .version('0.1.0');
60
+ program
61
+ .command('clean')
62
+ .description('Remove all generated reports and scan state')
63
+ .action(async () => {
64
+ const fs = await import('node:fs/promises');
65
+ const targets = ['output', '.scan-state'];
66
+ for (const target of targets) {
67
+ try {
68
+ await fs.rm(target, { recursive: true, force: true });
69
+ console.log(`[qulib] removed ${target}/`);
70
+ }
71
+ catch (err) {
72
+ console.error(`[qulib] failed to remove ${target}/: ${String(err)}`);
73
+ }
74
+ }
75
+ await fs.mkdir('output', { recursive: true });
76
+ await fs.writeFile('output/.gitkeep', 'This folder is generated by `qulib analyze`. It contains report.json, report.md,\nand a `generated/` subfolder with test scaffolds. Run `qulib clean` to reset.\n', 'utf8');
77
+ await fs.mkdir('.scan-state', { recursive: true });
78
+ await fs.writeFile('.scan-state/.gitkeep', '', 'utf8');
79
+ console.log('[qulib] clean complete');
80
+ });
81
+ program
82
+ .command('analyze')
83
+ .description('Analyze an app for quality gaps')
84
+ .requiredOption('--url <url>', 'Base URL of the app to analyze')
85
+ .option('--repo <path>', 'Path to the app repo')
86
+ .option('--prd <path>', 'Path to a PRD markdown file')
87
+ .option('--config <file>', 'Path to config file (relative to cwd)', 'qulib.config.ts')
88
+ .option('--adapter <type>', 'Override default test adapter (playwright, cypress-e2e, cypress-component, api)', 'playwright')
89
+ .option('--ephemeral', 'Do not write to disk — return full report as JSON on stdout (use for MCP/CI)', false)
90
+ .action(async (options) => {
91
+ await runAnalyze({
92
+ url: options.url,
93
+ repo: options.repo,
94
+ configFile: options.config,
95
+ ephemeral: options.ephemeral,
96
+ });
97
+ });
98
+ program.parseAsync().catch((error) => {
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ console.error('[qulib] Analyze failed:', message);
101
+ process.exit(1);
102
+ });
@@ -0,0 +1,7 @@
1
+ import type { DecisionLogEntry } from '../schemas/decision-log.schema.js';
2
+ export type LogDecisionOptions = {
3
+ persist?: boolean;
4
+ memory?: DecisionLogEntry[];
5
+ };
6
+ export declare function logDecision(entry: DecisionLogEntry, options?: LogDecisionOptions): Promise<void>;
7
+ //# sourceMappingURL=decision-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decision-logger.d.ts","sourceRoot":"","sources":["../../src/harness/decision-logger.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAK1E,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC7B,CAAC;AAEF,wBAAsB,WAAW,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwDtG"}
@@ -0,0 +1,68 @@
1
+ import { readFile, writeFile, mkdir } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { DecisionLogEntrySchema } from '../schemas/decision-log.schema.js';
5
+ const LOG_FILE = join(process.cwd(), '.scan-state', 'decision-log.json');
6
+ const STATE_DIR = join(process.cwd(), '.scan-state');
7
+ export async function logDecision(entry, options) {
8
+ const persist = options?.persist !== false;
9
+ const memory = options?.memory;
10
+ try {
11
+ const validation = DecisionLogEntrySchema.safeParse(entry);
12
+ if (!validation.success) {
13
+ console.warn(`Failed to log decision entry to ${LOG_FILE}: validation issues ${JSON.stringify(validation.error.issues)}`);
14
+ return;
15
+ }
16
+ if (memory) {
17
+ memory.push(validation.data);
18
+ }
19
+ if (!persist) {
20
+ return;
21
+ }
22
+ await mkdir(STATE_DIR, { recursive: true });
23
+ let log = [];
24
+ if (existsSync(LOG_FILE)) {
25
+ try {
26
+ const raw = await readFile(LOG_FILE, 'utf8');
27
+ const parsed = JSON.parse(raw);
28
+ if (!Array.isArray(parsed)) {
29
+ console.warn(`Failed to log decision entry to ${LOG_FILE}: file is not a JSON array; resetting log`);
30
+ }
31
+ else {
32
+ const validatedEntries = [];
33
+ for (const item of parsed) {
34
+ const itemValidation = DecisionLogEntrySchema.safeParse(item);
35
+ if (itemValidation.success) {
36
+ validatedEntries.push(itemValidation.data);
37
+ }
38
+ else {
39
+ console.warn(`Invalid existing entry skipped in ${LOG_FILE}: ${JSON.stringify(itemValidation.error.issues)}`);
40
+ }
41
+ }
42
+ log = validatedEntries;
43
+ }
44
+ }
45
+ catch (error) {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ console.warn(`Failed to read existing decision log at ${LOG_FILE}: ${message}`);
48
+ }
49
+ }
50
+ log.push(validation.data);
51
+ await writeFile(LOG_FILE, JSON.stringify(log, null, 2), 'utf8');
52
+ }
53
+ catch (error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ console.warn(`Failed to log decision entry to ${LOG_FILE}: ${message}`);
56
+ }
57
+ }
58
+ /*
59
+ Manual smoke test:
60
+
61
+ await logDecision({
62
+ timestamp: new Date().toISOString(),
63
+ phase: 'observe',
64
+ decision: 'Started crawl',
65
+ reason: 'Initial discovery pass',
66
+ metadata: { url: 'https://example.com' },
67
+ });
68
+ */
@@ -0,0 +1,6 @@
1
+ import type { DecisionLogEntry } from '../schemas/decision-log.schema.js';
2
+ export type RunArtifactsOptions = {
3
+ writeArtifacts: boolean;
4
+ decisionMemory?: DecisionLogEntry[];
5
+ };
6
+ //# sourceMappingURL=run-options.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { ZodSchema } from 'zod';
2
+ export declare class StateManager {
3
+ readState<T>(filename: string, schema: ZodSchema<T>): Promise<T>;
4
+ writeState<T>(filename: string, data: T, schema: ZodSchema<T>): Promise<void>;
5
+ }
6
+ //# sourceMappingURL=state-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-manager.d.ts","sourceRoot":"","sources":["../../src/harness/state-manager.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,KAAK,CAAC;AAIrC,qBAAa,YAAY;IACjB,SAAS,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IA8BhE,UAAU,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAiBpF"}
@@ -0,0 +1,64 @@
1
+ import { readFile, writeFile, mkdir } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { ZodError } from 'zod';
5
+ const STATE_DIR = join(process.cwd(), '.scan-state');
6
+ export class StateManager {
7
+ async readState(filename, schema) {
8
+ await mkdir(STATE_DIR, { recursive: true });
9
+ const filepath = join(STATE_DIR, filename);
10
+ if (!existsSync(filepath)) {
11
+ throw new Error(`State file missing: ${filename} (${filepath})`);
12
+ }
13
+ let parsedJson;
14
+ try {
15
+ const raw = await readFile(filepath, 'utf8');
16
+ parsedJson = JSON.parse(raw);
17
+ }
18
+ catch (error) {
19
+ if (error instanceof SyntaxError) {
20
+ throw new Error(`Invalid JSON in state file ${filename}: ${error.message}`);
21
+ }
22
+ const message = error instanceof Error ? error.message : String(error);
23
+ throw new Error(`Failed to read state file ${filename}: ${message}`);
24
+ }
25
+ try {
26
+ return schema.parse(parsedJson);
27
+ }
28
+ catch (error) {
29
+ if (error instanceof ZodError) {
30
+ throw new Error(`Schema validation failed for state file ${filename}: ${JSON.stringify(error.issues)}`);
31
+ }
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ throw new Error(`Failed to validate state file ${filename}: ${message}`);
34
+ }
35
+ }
36
+ async writeState(filename, data, schema) {
37
+ const filepath = join(STATE_DIR, filename);
38
+ let validatedData;
39
+ try {
40
+ validatedData = schema.parse(data);
41
+ }
42
+ catch (error) {
43
+ if (error instanceof ZodError) {
44
+ throw new Error(`Schema validation failed for state file ${filename}: ${JSON.stringify(error.issues)}`);
45
+ }
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ throw new Error(`Failed to validate state data for ${filename}: ${message}`);
48
+ }
49
+ await mkdir(STATE_DIR, { recursive: true });
50
+ await writeFile(filepath, JSON.stringify(validatedData, null, 2), 'utf8');
51
+ }
52
+ }
53
+ /*
54
+ Manual smoke test:
55
+
56
+ import { z } from 'zod';
57
+
58
+ const manager = new StateManager();
59
+ const DemoSchema = z.object({ count: z.number().int() });
60
+
61
+ await manager.writeState('demo.json', { count: 1 }, DemoSchema);
62
+ const loaded = await manager.readState('demo.json', DemoSchema);
63
+ console.log(loaded.count); // 1
64
+ */
@@ -0,0 +1,4 @@
1
+ export { analyzeApp } from './analyze.js';
2
+ export type { AnalyzeOptions, AnalyzeResult } from './analyze.js';
3
+ export type { HarnessConfig, AuthConfig, RouteInventory, GapAnalysis, RepoAnalysis, } from './schemas/index.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,YAAY,EACV,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,GACb,MAAM,oBAAoB,CAAC"}