@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/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { analyzeApp } from './analyze.js';
@@ -0,0 +1,3 @@
1
+ import type { Gap } from '../schemas/gap-analysis.schema.js';
2
+ export declare function buildGapPrompt(gaps: Gap[], limit: number): string;
3
+ //# sourceMappingURL=context-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-builder.d.ts","sourceRoot":"","sources":["../../src/llm/context-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,mCAAmC,CAAC;AAE7D,wBAAgB,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAkCjE"}
@@ -0,0 +1,33 @@
1
+ export function buildGapPrompt(gaps, limit) {
2
+ const topGaps = [...gaps]
3
+ .sort((a, b) => {
4
+ const order = { high: 0, medium: 1, low: 2 };
5
+ return order[a.severity] - order[b.severity];
6
+ })
7
+ .slice(0, limit);
8
+ const gapList = topGaps
9
+ .map((g, i) => `${i + 1}. [${g.severity}] ${g.category} at ${g.path}: ${g.reason}`)
10
+ .join('\n');
11
+ return `You are a QA engineer. Given these quality gaps found in a web application, generate test scenarios.
12
+
13
+ Gaps to address:
14
+ ${gapList}
15
+
16
+ Return ONLY a JSON array. No markdown. No explanation. No code fences.
17
+
18
+ Each item must match this exact shape:
19
+ {
20
+ "id": "string (unique)",
21
+ "title": "string",
22
+ "description": "string",
23
+ "targetPath": "string (the route path)",
24
+ "steps": [
25
+ { "action": "navigate|click|type|assert-visible|assert-hidden|assert-text|assert-disabled|assert-count|wait|api-call", "target": "string (optional)", "value": "string (optional)", "description": "string" }
26
+ ],
27
+ "tags": ["string"],
28
+ "recommendations": [
29
+ { "adapter": "playwright|cypress-e2e|cypress-component|api|accessibility", "reason": "string", "confidence": "high|medium|low" }
30
+ ],
31
+ "sourceGapIds": ["string"]
32
+ }`;
33
+ }
@@ -0,0 +1,4 @@
1
+ import { type Gap, type NeutralScenario } from '../schemas/gap-analysis.schema.js';
2
+ export declare function callLLM(prompt: string, tokenBudget: number): Promise<string>;
3
+ export declare function generateScenariosFromTemplate(gaps: Gap[]): NeutralScenario[];
4
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/llm/provider.ts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,GAAG,EAAE,KAAK,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAE1G,wBAAsB,OAAO,CAC3B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,CA0BjB;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,eAAe,EAAE,CA4B5E"}
@@ -0,0 +1,56 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { NeutralScenarioSchema } from '../schemas/gap-analysis.schema.js';
3
+ export async function callLLM(prompt, tokenBudget) {
4
+ const apiKey = process.env.ANTHROPIC_API_KEY;
5
+ if (!apiKey)
6
+ throw new Error('ANTHROPIC_API_KEY is not set');
7
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
8
+ method: 'POST',
9
+ headers: {
10
+ 'x-api-key': apiKey,
11
+ 'anthropic-version': '2023-06-01',
12
+ 'content-type': 'application/json',
13
+ },
14
+ body: JSON.stringify({
15
+ model: 'claude-sonnet-4-20250514',
16
+ max_tokens: tokenBudget,
17
+ messages: [{ role: 'user', content: prompt }],
18
+ }),
19
+ });
20
+ if (!response.ok) {
21
+ const text = await response.text();
22
+ throw new Error(`LLM call failed: ${response.status} ${text}`);
23
+ }
24
+ const data = await response.json();
25
+ const text = data.content.find((b) => b.type === 'text')?.text ?? '';
26
+ return text;
27
+ }
28
+ export function generateScenariosFromTemplate(gaps) {
29
+ return gaps.map((gap) => {
30
+ const steps = [];
31
+ steps.push({ action: 'navigate', target: gap.path, description: `Navigate to ${gap.path}` });
32
+ if (gap.category === 'untested-route') {
33
+ steps.push({ action: 'assert-visible', description: 'Assert page loaded successfully' });
34
+ }
35
+ else if (gap.category === 'console-error') {
36
+ steps.push({ action: 'assert-hidden', description: 'Assert no console errors are present' });
37
+ }
38
+ else if (gap.category === 'a11y') {
39
+ steps.push({ action: 'assert-visible', description: 'Run accessibility scan on page' });
40
+ }
41
+ else if (gap.category === 'broken-link') {
42
+ steps.push({ action: 'assert-visible', description: 'Assert all links resolve correctly' });
43
+ }
44
+ const adapter = gap.category === 'a11y' ? 'accessibility' : 'playwright';
45
+ return NeutralScenarioSchema.parse({
46
+ id: randomUUID(),
47
+ title: `[${gap.severity.toUpperCase()}] ${gap.category} — ${gap.path}`,
48
+ description: gap.reason,
49
+ targetPath: gap.path,
50
+ steps,
51
+ tags: [gap.category, gap.severity],
52
+ recommendations: [{ adapter, reason: 'Generated from template', confidence: 'low' }],
53
+ sourceGapIds: [gap.id],
54
+ });
55
+ });
56
+ }
@@ -0,0 +1,5 @@
1
+ import type { HarnessConfig } from '../schemas/config.schema.js';
2
+ import type { GapAnalysis } from '../schemas/gap-analysis.schema.js';
3
+ import type { RunArtifactsOptions } from '../harness/run-options.js';
4
+ export declare function act(analysis: GapAnalysis, config: HarnessConfig, artifacts?: RunArtifactsOptions): Promise<void>;
5
+ //# sourceMappingURL=act.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"act.d.ts","sourceRoot":"","sources":["../../src/phases/act.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAIrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,wBAAsB,GAAG,CACvB,QAAQ,EAAE,WAAW,EACrB,MAAM,EAAE,aAAa,EACrB,SAAS,GAAE,mBAA8C,GACxD,OAAO,CAAC,IAAI,CAAC,CA0Cf"}
@@ -0,0 +1,41 @@
1
+ import { join } from 'node:path';
2
+ import { writeJsonReport } from '../reporters/json-reporter.js';
3
+ import { writeMarkdownReport } from '../reporters/markdown-reporter.js';
4
+ import { logDecision } from '../harness/decision-logger.js';
5
+ export async function act(analysis, config, artifacts = { writeArtifacts: true }) {
6
+ const outputDir = join(process.cwd(), 'output');
7
+ const logOpts = { persist: artifacts.writeArtifacts, memory: artifacts.decisionMemory };
8
+ const log = artifacts.writeArtifacts ? console.log : console.error;
9
+ if (artifacts.writeArtifacts) {
10
+ await writeJsonReport(analysis, outputDir);
11
+ await writeMarkdownReport(analysis, outputDir);
12
+ }
13
+ await logDecision({
14
+ timestamp: new Date().toISOString(),
15
+ phase: 'act',
16
+ decision: 'reports-written',
17
+ reason: artifacts.writeArtifacts
18
+ ? `Wrote JSON and Markdown reports to ${outputDir}`
19
+ : 'Skipped writing reports (ephemeral run)',
20
+ metadata: {
21
+ gapCount: analysis.gaps.length,
22
+ scenarioCount: analysis.scenarios.length,
23
+ releaseConfidence: analysis.releaseConfidence,
24
+ requireHumanReview: config.requireHumanReview,
25
+ },
26
+ }, logOpts);
27
+ log('\n[qulib] Analysis complete');
28
+ log(` Gaps found: ${analysis.gaps.length}`);
29
+ log(` Scenarios generated: ${analysis.scenarios.length}`);
30
+ log(` Release confidence: ${analysis.releaseConfidence}/100`);
31
+ if (config.requireHumanReview) {
32
+ log('\n[qulib] Human review required before applying any generated output.');
33
+ if (artifacts.writeArtifacts) {
34
+ log(' Reports: output/report.json and output/report.md');
35
+ log(' Decisions: .scan-state/decision-log.json');
36
+ }
37
+ else {
38
+ log(' Ephemeral run: inspect JSON printed to stdout (no files written).');
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,10 @@
1
+ import type { HarnessConfig } from '../schemas/config.schema.js';
2
+ import { type RouteInventory } from '../schemas/route-inventory.schema.js';
3
+ import { type RepoAnalysis } from '../schemas/repo-analysis.schema.js';
4
+ import type { RunArtifactsOptions } from '../harness/run-options.js';
5
+ export interface ObserveResult {
6
+ routes: RouteInventory;
7
+ repo: RepoAnalysis | null;
8
+ }
9
+ export declare function observe(baseUrl: string, repoPath: string | undefined, config: HarnessConfig, artifacts?: RunArtifactsOptions): Promise<ObserveResult>;
10
+ //# sourceMappingURL=observe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observe.d.ts","sourceRoot":"","sources":["../../src/phases/observe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,sCAAsC,CAAC;AACjG,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAK3F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,cAAc,CAAC;IACvB,IAAI,EAAE,YAAY,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAsB,OAAO,CAC3B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,aAAa,EACrB,SAAS,GAAE,mBAA8C,GACxD,OAAO,CAAC,aAAa,CAAC,CAoDxB"}
@@ -0,0 +1,48 @@
1
+ import { RouteInventorySchema } from '../schemas/route-inventory.schema.js';
2
+ import { RepoAnalysisSchema } from '../schemas/repo-analysis.schema.js';
3
+ import { createExplorer } from '../tools/explorer-factory.js';
4
+ import { scanRepo } from '../tools/repo-scanner.js';
5
+ import { StateManager } from '../harness/state-manager.js';
6
+ import { logDecision } from '../harness/decision-logger.js';
7
+ export async function observe(baseUrl, repoPath, config, artifacts = { writeArtifacts: true }) {
8
+ const explorer = createExplorer(config.explorer);
9
+ const stateManager = new StateManager();
10
+ const logOpts = { persist: artifacts.writeArtifacts, memory: artifacts.decisionMemory };
11
+ const rawRoutes = await explorer.explore(baseUrl, config);
12
+ const routes = RouteInventorySchema.parse(rawRoutes);
13
+ if (artifacts.writeArtifacts) {
14
+ await stateManager.writeState('discovered-routes.json', routes, RouteInventorySchema);
15
+ }
16
+ await logDecision({
17
+ timestamp: new Date().toISOString(),
18
+ phase: 'observe',
19
+ decision: 'exploration-complete',
20
+ reason: `Discovered ${routes.routes.length} routes; budgetExceeded=${routes.budgetExceeded}`,
21
+ metadata: {
22
+ baseUrl,
23
+ scannedRoutes: routes.routes.length,
24
+ budgetExceeded: routes.budgetExceeded,
25
+ pagesSkipped: routes.pagesSkipped,
26
+ },
27
+ }, logOpts);
28
+ let repo = null;
29
+ if (repoPath) {
30
+ const rawRepo = await scanRepo(repoPath);
31
+ repo = RepoAnalysisSchema.parse(rawRepo);
32
+ if (artifacts.writeArtifacts) {
33
+ await stateManager.writeState('repo-inventory.json', repo, RepoAnalysisSchema);
34
+ }
35
+ await logDecision({
36
+ timestamp: new Date().toISOString(),
37
+ phase: 'observe',
38
+ decision: 'repo-scan-complete',
39
+ reason: `Scanned repo inventory: ${repo.routes.length} routes, ${repo.testFiles.length} test files`,
40
+ metadata: {
41
+ repoPath,
42
+ routeCount: repo.routes.length,
43
+ testFileCount: repo.testFiles.length,
44
+ },
45
+ }, logOpts);
46
+ }
47
+ return { routes, repo };
48
+ }
@@ -0,0 +1,6 @@
1
+ import type { HarnessConfig } from '../schemas/config.schema.js';
2
+ import { type GapAnalysis } from '../schemas/gap-analysis.schema.js';
3
+ import type { ObserveResult } from './observe.js';
4
+ import type { RunArtifactsOptions } from '../harness/run-options.js';
5
+ export declare function think(observed: ObserveResult, config: HarnessConfig, artifacts?: RunArtifactsOptions): Promise<GapAnalysis>;
6
+ //# sourceMappingURL=think.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"think.d.ts","sourceRoot":"","sources":["../../src/phases/think.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAA4C,KAAK,WAAW,EAAwB,MAAM,mCAAmC,CAAC;AACrI,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAMlD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,wBAAsB,KAAK,CACzB,QAAQ,EAAE,aAAa,EACvB,MAAM,EAAE,aAAa,EACrB,SAAS,GAAE,mBAA8C,GACxD,OAAO,CAAC,WAAW,CAAC,CA6FtB"}
@@ -0,0 +1,85 @@
1
+ import { GapAnalysisSchema, NeutralScenarioSchema } from '../schemas/gap-analysis.schema.js';
2
+ import { analyzeGaps } from '../tools/gap-engine.js';
3
+ import { StateManager } from '../harness/state-manager.js';
4
+ import { logDecision } from '../harness/decision-logger.js';
5
+ import { callLLM, generateScenariosFromTemplate } from '../llm/provider.js';
6
+ import { buildGapPrompt } from '../llm/context-builder.js';
7
+ export async function think(observed, config, artifacts = { writeArtifacts: true }) {
8
+ const mode = observed.repo ? 'url-repo' : 'url-only';
9
+ const stateManager = new StateManager();
10
+ const logOpts = { persist: artifacts.writeArtifacts, memory: artifacts.decisionMemory };
11
+ const partialAnalysis = GapAnalysisSchema.parse({
12
+ ...analyzeGaps(observed.routes, observed.repo, mode, config),
13
+ scenarios: [],
14
+ generatedTests: [],
15
+ });
16
+ if (artifacts.writeArtifacts) {
17
+ await stateManager.writeState('gap-analysis.json', partialAnalysis, GapAnalysisSchema);
18
+ }
19
+ await logDecision({
20
+ timestamp: new Date().toISOString(),
21
+ phase: 'think',
22
+ decision: 'gap-analysis-complete',
23
+ reason: `Computed ${partialAnalysis.gaps.length} gaps with release confidence ${partialAnalysis.releaseConfidence}`,
24
+ metadata: {
25
+ mode,
26
+ gapCount: partialAnalysis.gaps.length,
27
+ releaseConfidence: partialAnalysis.releaseConfidence,
28
+ },
29
+ }, logOpts);
30
+ let scenarioSource = 'template';
31
+ let generatedScenarios = [];
32
+ if (!process.env.ANTHROPIC_API_KEY) {
33
+ await logDecision({
34
+ timestamp: new Date().toISOString(),
35
+ phase: 'think',
36
+ decision: 'llm-scenarios-skipped',
37
+ reason: 'Skipped LLM scenario generation because ANTHROPIC_API_KEY is not set',
38
+ }, logOpts);
39
+ generatedScenarios = generateScenariosFromTemplate(partialAnalysis.gaps);
40
+ }
41
+ else {
42
+ try {
43
+ const prompt = buildGapPrompt(partialAnalysis.gaps, config.testGenerationLimit);
44
+ const response = await callLLM(prompt, config.llmTokenBudget);
45
+ const parsed = JSON.parse(response);
46
+ const candidates = Array.isArray(parsed) ? parsed : [];
47
+ for (const item of candidates) {
48
+ const validated = NeutralScenarioSchema.safeParse(item);
49
+ if (validated.success) {
50
+ generatedScenarios.push(validated.data);
51
+ }
52
+ }
53
+ if (generatedScenarios.length === 0) {
54
+ generatedScenarios = generateScenariosFromTemplate(partialAnalysis.gaps);
55
+ scenarioSource = 'template';
56
+ }
57
+ else {
58
+ scenarioSource = 'llm';
59
+ }
60
+ }
61
+ catch {
62
+ generatedScenarios = generateScenariosFromTemplate(partialAnalysis.gaps);
63
+ scenarioSource = 'template';
64
+ }
65
+ }
66
+ await logDecision({
67
+ timestamp: new Date().toISOString(),
68
+ phase: 'think',
69
+ decision: 'scenario-generation-complete',
70
+ reason: `Generated ${generatedScenarios.length} scenarios via ${scenarioSource}`,
71
+ metadata: {
72
+ source: scenarioSource,
73
+ scenarioCount: generatedScenarios.length,
74
+ },
75
+ }, logOpts);
76
+ const completeAnalysis = GapAnalysisSchema.parse({
77
+ ...partialAnalysis,
78
+ scenarios: generatedScenarios,
79
+ generatedTests: [],
80
+ });
81
+ if (artifacts.writeArtifacts) {
82
+ await stateManager.writeState('gap-analysis.json', completeAnalysis, GapAnalysisSchema);
83
+ }
84
+ return completeAnalysis;
85
+ }
@@ -0,0 +1,3 @@
1
+ import type { GapAnalysis } from '../schemas/gap-analysis.schema.js';
2
+ export declare function writeJsonReport(analysis: GapAnalysis, outputDir: string): Promise<void>;
3
+ //# sourceMappingURL=json-reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-reporter.d.ts","sourceRoot":"","sources":["../../src/reporters/json-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAErE,wBAAsB,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK7F"}
@@ -0,0 +1,8 @@
1
+ import { writeFile, mkdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ export async function writeJsonReport(analysis, outputDir) {
4
+ await mkdir(outputDir, { recursive: true });
5
+ const filePath = join(outputDir, 'report.json');
6
+ await writeFile(filePath, JSON.stringify(analysis, null, 2), 'utf-8');
7
+ console.log(`[qulib] JSON report written to ${filePath}`);
8
+ }
@@ -0,0 +1,3 @@
1
+ import type { GapAnalysis } from '../schemas/gap-analysis.schema.js';
2
+ export declare function writeMarkdownReport(analysis: GapAnalysis, outputDir: string): Promise<void>;
3
+ //# sourceMappingURL=markdown-reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-reporter.d.ts","sourceRoot":"","sources":["../../src/reporters/markdown-reporter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAErE,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6CjG"}
@@ -0,0 +1,42 @@
1
+ import { writeFile, mkdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ export async function writeMarkdownReport(analysis, outputDir) {
4
+ await mkdir(outputDir, { recursive: true });
5
+ const recommendation = analysis.releaseConfidence >= 80 ? 'READY' :
6
+ analysis.releaseConfidence >= 50 ? 'CONDITIONAL' : 'NOT READY';
7
+ const gapRows = analysis.gaps
8
+ .map((g) => `| ${g.path} | ${g.category} | ${g.severity} | ${g.reason} |`)
9
+ .join('\n');
10
+ const scenarioBlocks = analysis.scenarios
11
+ .map((s) => `### ${s.title}\n${s.description}\n\nSteps:\n${s.steps.map((step) => `- ${step.description}`).join('\n')}\n\nRecommended adapters: ${s.recommendations.map((r) => r.adapter).join(', ')}`)
12
+ .join('\n\n---\n\n');
13
+ const md = `# Qulib Quality Gap Report
14
+
15
+ **Generated:** ${analysis.analyzedAt}
16
+ **Mode:** ${analysis.mode}
17
+ **Release confidence:** ${analysis.releaseConfidence}/100 — ${recommendation}
18
+
19
+ ## Coverage
20
+
21
+ - Pages scanned: ${analysis.coveragePagesScanned}
22
+ - Scan budget exhausted (unfinished queue): ${analysis.coverageBudgetExceeded ? 'yes' : 'no'}
23
+ ${analysis.coverageWarning ? `- Warning: **${analysis.coverageWarning}**` : '- Warning: none'}
24
+
25
+ ## Coverage gaps (${analysis.gaps.length})
26
+
27
+ | Path | Category | Severity | Reason |
28
+ |------|----------|----------|--------|
29
+ ${gapRows}
30
+
31
+ ## Generated scenarios (${analysis.scenarios.length})
32
+
33
+ ${scenarioBlocks || '_No scenarios generated._'}
34
+
35
+ ## Decision log
36
+
37
+ See \`.scan-state/decision-log.json\` for the full audit trail.
38
+ `;
39
+ const filePath = join(outputDir, 'report.md');
40
+ await writeFile(filePath, md, 'utf-8');
41
+ console.log(`[qulib] Markdown report written to ${filePath}`);
42
+ }