@yasserkhanorg/impact-gate 2.0.0 → 2.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.
- package/README.md +1 -1
- package/dist/cli/commands/install_skill.d.ts +2 -0
- package/dist/cli/commands/install_skill.d.ts.map +1 -0
- package/dist/cli/commands/install_skill.js +60 -0
- package/dist/cli/parse_args.js +1 -1
- package/dist/cli/types.d.ts +1 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/usage.d.ts.map +1 -1
- package/dist/cli/usage.js +1 -0
- package/dist/cli.js +7 -1
- package/dist/esm/cli/commands/install_skill.js +57 -0
- package/dist/esm/cli/parse_args.js +1 -1
- package/dist/esm/cli/usage.js +1 -0
- package/dist/esm/cli.js +7 -1
- package/dist/esm/qa-agent/cli.js +26 -0
- package/dist/esm/qa-agent/finding_taxonomy.js +102 -0
- package/dist/esm/qa-agent/health_score.js +99 -0
- package/dist/esm/qa-agent/orchestrator.js +67 -9
- package/dist/esm/qa-agent/phase2/agent_loop.js +13 -1
- package/dist/esm/qa-agent/phase2/tools.js +10 -4
- package/dist/esm/qa-agent/phase25/fix_loop.js +238 -0
- package/dist/esm/qa-agent/phase25/fix_tools.js +262 -0
- package/dist/esm/qa-agent/phase25/wtf_heuristic.js +60 -0
- package/dist/esm/qa-agent/phase3/reporter.js +100 -30
- package/dist/esm/qa-agent/phase3/verdict.js +21 -3
- package/dist/esm/qa-agent/regression/baseline.js +89 -0
- package/dist/qa-agent/cli.js +26 -0
- package/dist/qa-agent/finding_taxonomy.d.ts +23 -0
- package/dist/qa-agent/finding_taxonomy.d.ts.map +1 -0
- package/dist/qa-agent/finding_taxonomy.js +108 -0
- package/dist/qa-agent/health_score.d.ts +19 -0
- package/dist/qa-agent/health_score.d.ts.map +1 -0
- package/dist/qa-agent/health_score.js +104 -0
- package/dist/qa-agent/orchestrator.d.ts.map +1 -1
- package/dist/qa-agent/orchestrator.js +67 -9
- package/dist/qa-agent/phase2/agent_loop.d.ts.map +1 -1
- package/dist/qa-agent/phase2/agent_loop.js +13 -1
- package/dist/qa-agent/phase2/tools.d.ts.map +1 -1
- package/dist/qa-agent/phase2/tools.js +10 -4
- package/dist/qa-agent/phase25/fix_loop.d.ts +4 -0
- package/dist/qa-agent/phase25/fix_loop.d.ts.map +1 -0
- package/dist/qa-agent/phase25/fix_loop.js +244 -0
- package/dist/qa-agent/phase25/fix_tools.d.ts +18 -0
- package/dist/qa-agent/phase25/fix_tools.d.ts.map +1 -0
- package/dist/qa-agent/phase25/fix_tools.js +266 -0
- package/dist/qa-agent/phase25/wtf_heuristic.d.ts +27 -0
- package/dist/qa-agent/phase25/wtf_heuristic.d.ts.map +1 -0
- package/dist/qa-agent/phase25/wtf_heuristic.js +64 -0
- package/dist/qa-agent/phase3/reporter.d.ts +2 -2
- package/dist/qa-agent/phase3/reporter.d.ts.map +1 -1
- package/dist/qa-agent/phase3/reporter.js +100 -30
- package/dist/qa-agent/phase3/verdict.d.ts +2 -2
- package/dist/qa-agent/phase3/verdict.d.ts.map +1 -1
- package/dist/qa-agent/phase3/verdict.js +21 -3
- package/dist/qa-agent/regression/baseline.d.ts +14 -0
- package/dist/qa-agent/regression/baseline.d.ts.map +1 -0
- package/dist/qa-agent/regression/baseline.js +94 -0
- package/dist/qa-agent/types.d.ts +65 -2
- package/dist/qa-agent/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/skills/qa/SKILL.md +138 -0
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ The fastest way to evaluate the package is the deterministic CI path. These comm
|
|
|
63
63
|
Install the package:
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
npm install @yasserkhanorg/impact-gate
|
|
66
|
+
npm install -D @yasserkhanorg/impact-gate
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
Requires Node.js >= 20. Ships both CommonJS and ESM builds.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install_skill.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/install_skill.ts"],"names":[],"mappings":"AAsBA,wBAAgB,sBAAsB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CA8C/D"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
3
|
+
// See LICENSE.txt for license information.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.runInstallSkillCommand = runInstallSkillCommand;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
// skills/ lives at the package root, two levels up from dist/cli/commands/
|
|
9
|
+
const SKILLS_SOURCE = (0, path_1.join)(__dirname, '..', '..', '..', 'skills');
|
|
10
|
+
function getSkillsDir() {
|
|
11
|
+
if ((0, fs_1.existsSync)(SKILLS_SOURCE)) {
|
|
12
|
+
return SKILLS_SOURCE;
|
|
13
|
+
}
|
|
14
|
+
throw new Error('Could not find skills/ directory in the impact-gate package. Reinstall with: npm install @yasserkhanorg/impact-gate');
|
|
15
|
+
}
|
|
16
|
+
function listAvailableSkills(skillsDir) {
|
|
17
|
+
return (0, fs_1.readdirSync)(skillsDir, { withFileTypes: true })
|
|
18
|
+
.filter((d) => d.isDirectory() && (0, fs_1.existsSync)((0, path_1.join)(skillsDir, d.name, 'SKILL.md')))
|
|
19
|
+
.map((d) => d.name);
|
|
20
|
+
}
|
|
21
|
+
function runInstallSkillCommand(skillName) {
|
|
22
|
+
const targetDir = process.cwd();
|
|
23
|
+
const claudeSkillsDir = (0, path_1.join)(targetDir, '.claude', 'skills');
|
|
24
|
+
const skillsDir = getSkillsDir();
|
|
25
|
+
const available = listAvailableSkills(skillsDir);
|
|
26
|
+
if (!skillName) {
|
|
27
|
+
// List available skills
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log(' Available skills:');
|
|
30
|
+
console.log('');
|
|
31
|
+
for (const name of available) {
|
|
32
|
+
const installed = (0, fs_1.existsSync)((0, path_1.join)(claudeSkillsDir, name, 'SKILL.md'));
|
|
33
|
+
const status = installed ? ' (installed)' : '';
|
|
34
|
+
console.log(` /${name}${status}`);
|
|
35
|
+
}
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log(' Usage: impact-gate install-skill <name>');
|
|
38
|
+
console.log(' Example: impact-gate install-skill qa');
|
|
39
|
+
console.log('');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!available.includes(skillName)) {
|
|
43
|
+
console.error(`Unknown skill: "${skillName}". Available: ${available.join(', ')}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const source = (0, path_1.join)(skillsDir, skillName);
|
|
47
|
+
const dest = (0, path_1.join)(claudeSkillsDir, skillName);
|
|
48
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(dest, 'SKILL.md'))) {
|
|
49
|
+
console.log(` /${skillName} is already installed at .claude/skills/${skillName}/`);
|
|
50
|
+
console.log(' To reinstall, remove the directory first and re-run.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
(0, fs_1.mkdirSync)(dest, { recursive: true });
|
|
54
|
+
(0, fs_1.cpSync)(source, dest, { recursive: true });
|
|
55
|
+
console.log('');
|
|
56
|
+
console.log(` Installed /${skillName} → .claude/skills/${skillName}/`);
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log(' You can now use /' + skillName + ' in Claude Code.');
|
|
59
|
+
console.log('');
|
|
60
|
+
}
|
package/dist/cli/parse_args.js
CHANGED
|
@@ -168,7 +168,7 @@ const COMMANDS = new Set([
|
|
|
168
168
|
'finalize-generated-tests', 'feedback',
|
|
169
169
|
'traceability-capture', 'traceability-ingest',
|
|
170
170
|
'analyze', 'llm-health', 'train', 'crew', 'cost-report', 'gate',
|
|
171
|
-
'bootstrap',
|
|
171
|
+
'bootstrap', 'install-skill',
|
|
172
172
|
]);
|
|
173
173
|
// ---------------------------------------------------------------------------
|
|
174
174
|
// Parser
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AnalysisProfile, FrameworkType } from '../agent/config.js';
|
|
2
|
-
export type Command = 'init' | 'impact' | 'plan' | 'heal' | 'suggest' | 'generate' | 'finalize-generated-tests' | 'feedback' | 'traceability-capture' | 'traceability-ingest' | 'analyze' | 'llm-health' | 'train' | 'crew' | 'cost-report' | 'gate' | 'bootstrap';
|
|
2
|
+
export type Command = 'init' | 'impact' | 'plan' | 'heal' | 'suggest' | 'generate' | 'finalize-generated-tests' | 'feedback' | 'traceability-capture' | 'traceability-ingest' | 'analyze' | 'llm-health' | 'train' | 'crew' | 'cost-report' | 'gate' | 'bootstrap' | 'install-skill';
|
|
3
3
|
export interface ParsedArgs {
|
|
4
4
|
command?: Command;
|
|
5
5
|
configPath?: string;
|
package/dist/cli/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cli/types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,eAAe,EAAE,aAAa,EAAC,MAAM,oBAAoB,CAAC;AAEvE,MAAM,MAAM,OAAO,GACf,MAAM,GACJ,QAAQ,GACR,MAAM,GACN,MAAM,GACN,SAAS,GACT,UAAU,GACV,0BAA0B,GAC1B,UAAU,GACV,sBAAsB,GACtB,qBAAqB,GACrB,SAAS,GACT,YAAY,GACZ,OAAO,GACP,MAAM,GACN,aAAa,GACb,MAAM,GACN,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cli/types.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/usage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/cli/usage.ts"],"names":[],"mappings":"AAGA,wBAAgB,UAAU,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/cli/usage.ts"],"names":[],"mappings":"AAGA,wBAAgB,UAAU,IAAI,IAAI,CA0GjC"}
|
package/dist/cli/usage.js
CHANGED
|
@@ -24,6 +24,7 @@ function printUsage() {
|
|
|
24
24
|
' init [--yes]',
|
|
25
25
|
' train --path <project-root> [--no-enrich] [--validate] [--since <ref>] [--pr <num>]',
|
|
26
26
|
' bootstrap --path <app-root> [options]',
|
|
27
|
+
' install-skill [name] Install a Claude Code skill (e.g. /qa)',
|
|
27
28
|
' feedback --path <app-root> --feedback-input <json>',
|
|
28
29
|
' traceability-capture --path <app-root> --traceability-report <json>',
|
|
29
30
|
' traceability-ingest --path <app-root> --traceability-input <json>',
|
package/dist/cli.js
CHANGED
|
@@ -22,9 +22,10 @@ const crew_js_1 = require("./cli/commands/crew.js");
|
|
|
22
22
|
const cost_report_js_1 = require("./cli/commands/cost_report.js");
|
|
23
23
|
const gate_js_1 = require("./cli/commands/gate.js");
|
|
24
24
|
const bootstrap_js_1 = require("./cli/commands/bootstrap.js");
|
|
25
|
+
const install_skill_js_1 = require("./cli/commands/install_skill.js");
|
|
25
26
|
const errors_js_1 = require("./cli/errors.js");
|
|
26
27
|
// Commands that skip default resolution (they handle their own setup)
|
|
27
|
-
const SKIP_DEFAULTS_COMMANDS = new Set(['init', 'llm-health', 'cost-report', 'bootstrap']);
|
|
28
|
+
const SKIP_DEFAULTS_COMMANDS = new Set(['init', 'llm-health', 'cost-report', 'bootstrap', 'install-skill']);
|
|
28
29
|
// Commands that need path/testsRoot/framework/since
|
|
29
30
|
const NEEDS_DEFAULTS_COMMANDS = new Set([
|
|
30
31
|
'impact', 'plan', 'suggest', 'crew', 'generate', 'heal', 'analyze', 'train',
|
|
@@ -51,6 +52,11 @@ async function main() {
|
|
|
51
52
|
await (0, init_js_1.runInitCommand)(hasYes);
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
55
|
+
if (args.command === 'install-skill') {
|
|
56
|
+
const skillName = process.argv[3]; // impact-gate install-skill <name>
|
|
57
|
+
(0, install_skill_js_1.runInstallSkillCommand)(skillName);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
54
60
|
if (args.command === 'bootstrap') {
|
|
55
61
|
await (0, bootstrap_js_1.runBootstrapCommand)(args);
|
|
56
62
|
return;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { existsSync, mkdirSync, cpSync, readdirSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
// skills/ lives at the package root, two levels up from dist/cli/commands/
|
|
6
|
+
const SKILLS_SOURCE = join(__dirname, '..', '..', '..', 'skills');
|
|
7
|
+
function getSkillsDir() {
|
|
8
|
+
if (existsSync(SKILLS_SOURCE)) {
|
|
9
|
+
return SKILLS_SOURCE;
|
|
10
|
+
}
|
|
11
|
+
throw new Error('Could not find skills/ directory in the impact-gate package. Reinstall with: npm install @yasserkhanorg/impact-gate');
|
|
12
|
+
}
|
|
13
|
+
function listAvailableSkills(skillsDir) {
|
|
14
|
+
return readdirSync(skillsDir, { withFileTypes: true })
|
|
15
|
+
.filter((d) => d.isDirectory() && existsSync(join(skillsDir, d.name, 'SKILL.md')))
|
|
16
|
+
.map((d) => d.name);
|
|
17
|
+
}
|
|
18
|
+
export function runInstallSkillCommand(skillName) {
|
|
19
|
+
const targetDir = process.cwd();
|
|
20
|
+
const claudeSkillsDir = join(targetDir, '.claude', 'skills');
|
|
21
|
+
const skillsDir = getSkillsDir();
|
|
22
|
+
const available = listAvailableSkills(skillsDir);
|
|
23
|
+
if (!skillName) {
|
|
24
|
+
// List available skills
|
|
25
|
+
console.log('');
|
|
26
|
+
console.log(' Available skills:');
|
|
27
|
+
console.log('');
|
|
28
|
+
for (const name of available) {
|
|
29
|
+
const installed = existsSync(join(claudeSkillsDir, name, 'SKILL.md'));
|
|
30
|
+
const status = installed ? ' (installed)' : '';
|
|
31
|
+
console.log(` /${name}${status}`);
|
|
32
|
+
}
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(' Usage: impact-gate install-skill <name>');
|
|
35
|
+
console.log(' Example: impact-gate install-skill qa');
|
|
36
|
+
console.log('');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!available.includes(skillName)) {
|
|
40
|
+
console.error(`Unknown skill: "${skillName}". Available: ${available.join(', ')}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const source = join(skillsDir, skillName);
|
|
44
|
+
const dest = join(claudeSkillsDir, skillName);
|
|
45
|
+
if (existsSync(join(dest, 'SKILL.md'))) {
|
|
46
|
+
console.log(` /${skillName} is already installed at .claude/skills/${skillName}/`);
|
|
47
|
+
console.log(' To reinstall, remove the directory first and re-run.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
mkdirSync(dest, { recursive: true });
|
|
51
|
+
cpSync(source, dest, { recursive: true });
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log(` Installed /${skillName} → .claude/skills/${skillName}/`);
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log(' You can now use /' + skillName + ' in Claude Code.');
|
|
56
|
+
console.log('');
|
|
57
|
+
}
|
|
@@ -162,7 +162,7 @@ const COMMANDS = new Set([
|
|
|
162
162
|
'finalize-generated-tests', 'feedback',
|
|
163
163
|
'traceability-capture', 'traceability-ingest',
|
|
164
164
|
'analyze', 'llm-health', 'train', 'crew', 'cost-report', 'gate',
|
|
165
|
-
'bootstrap',
|
|
165
|
+
'bootstrap', 'install-skill',
|
|
166
166
|
]);
|
|
167
167
|
// ---------------------------------------------------------------------------
|
|
168
168
|
// Parser
|
package/dist/esm/cli/usage.js
CHANGED
|
@@ -21,6 +21,7 @@ export function printUsage() {
|
|
|
21
21
|
' init [--yes]',
|
|
22
22
|
' train --path <project-root> [--no-enrich] [--validate] [--since <ref>] [--pr <num>]',
|
|
23
23
|
' bootstrap --path <app-root> [options]',
|
|
24
|
+
' install-skill [name] Install a Claude Code skill (e.g. /qa)',
|
|
24
25
|
' feedback --path <app-root> --feedback-input <json>',
|
|
25
26
|
' traceability-capture --path <app-root> --traceability-report <json>',
|
|
26
27
|
' traceability-ingest --path <app-root> --traceability-input <json>',
|
package/dist/esm/cli.js
CHANGED
|
@@ -20,9 +20,10 @@ import { runCrewCommand } from './cli/commands/crew.js';
|
|
|
20
20
|
import { runCostReportCommand } from './cli/commands/cost_report.js';
|
|
21
21
|
import { runGateCommand } from './cli/commands/gate.js';
|
|
22
22
|
import { runBootstrapCommand } from './cli/commands/bootstrap.js';
|
|
23
|
+
import { runInstallSkillCommand } from './cli/commands/install_skill.js';
|
|
23
24
|
import { classifyError, EXIT_CODES } from './cli/errors.js';
|
|
24
25
|
// Commands that skip default resolution (they handle their own setup)
|
|
25
|
-
const SKIP_DEFAULTS_COMMANDS = new Set(['init', 'llm-health', 'cost-report', 'bootstrap']);
|
|
26
|
+
const SKIP_DEFAULTS_COMMANDS = new Set(['init', 'llm-health', 'cost-report', 'bootstrap', 'install-skill']);
|
|
26
27
|
// Commands that need path/testsRoot/framework/since
|
|
27
28
|
const NEEDS_DEFAULTS_COMMANDS = new Set([
|
|
28
29
|
'impact', 'plan', 'suggest', 'crew', 'generate', 'heal', 'analyze', 'train',
|
|
@@ -49,6 +50,11 @@ async function main() {
|
|
|
49
50
|
await runInitCommand(hasYes);
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
53
|
+
if (args.command === 'install-skill') {
|
|
54
|
+
const skillName = process.argv[3]; // impact-gate install-skill <name>
|
|
55
|
+
runInstallSkillCommand(skillName);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
52
58
|
if (args.command === 'bootstrap') {
|
|
53
59
|
await runBootstrapCommand(args);
|
|
54
60
|
return;
|
package/dist/esm/qa-agent/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ const MODES = new Set(['pr', 'hunt', 'fix', 'release']);
|
|
|
7
7
|
const KNOWN_FLAGS = new Set([
|
|
8
8
|
'--base-url', '--since', '--phase', '--time', '--budget',
|
|
9
9
|
'--headed', '--tests-root', '--project', '--output', '--help', '-h',
|
|
10
|
+
'--fix-tier', '--no-fix', '--regression',
|
|
10
11
|
]);
|
|
11
12
|
function printUsage() {
|
|
12
13
|
console.log(`
|
|
@@ -28,6 +29,9 @@ Options:
|
|
|
28
29
|
--tests-root <path> Path to tests directory
|
|
29
30
|
--project <name> Playwright project name
|
|
30
31
|
--output <dir> Output directory (default: .e2e-ai-agents)
|
|
32
|
+
--fix-tier <tier> Fix tier: quick (critical+high), standard (+medium), exhaustive (+low) (default: standard)
|
|
33
|
+
--no-fix Skip the fix loop (Phase 2.5)
|
|
34
|
+
--regression Compare against previous baseline
|
|
31
35
|
--help Show this help
|
|
32
36
|
|
|
33
37
|
Examples:
|
|
@@ -59,6 +63,9 @@ function parseCliArgs(argv) {
|
|
|
59
63
|
let testsRoot;
|
|
60
64
|
let project;
|
|
61
65
|
let outputDir;
|
|
66
|
+
let fixTier = 'standard';
|
|
67
|
+
let fixEnabled = true;
|
|
68
|
+
let regression = false;
|
|
62
69
|
// For hunt mode, the second positional arg is the target
|
|
63
70
|
let startFlags = 1;
|
|
64
71
|
if (mode === 'hunt' && argv[1] && !argv[1].startsWith('--')) {
|
|
@@ -122,6 +129,22 @@ function parseCliArgs(argv) {
|
|
|
122
129
|
outputDir = next;
|
|
123
130
|
i++;
|
|
124
131
|
break;
|
|
132
|
+
case '--fix-tier': {
|
|
133
|
+
const validTiers = new Set(['quick', 'standard', 'exhaustive']);
|
|
134
|
+
if (!validTiers.has(next)) {
|
|
135
|
+
console.error(`Error: --fix-tier must be quick, standard, or exhaustive (got "${next}")`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
fixTier = next;
|
|
139
|
+
i++;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case '--no-fix':
|
|
143
|
+
fixEnabled = false;
|
|
144
|
+
break;
|
|
145
|
+
case '--regression':
|
|
146
|
+
regression = true;
|
|
147
|
+
break;
|
|
125
148
|
default:
|
|
126
149
|
if (arg.startsWith('--') && !KNOWN_FLAGS.has(arg)) {
|
|
127
150
|
console.error(`Warning: unknown flag "${arg}" (ignored)`);
|
|
@@ -178,6 +201,9 @@ function parseCliArgs(argv) {
|
|
|
178
201
|
testsRoot,
|
|
179
202
|
project,
|
|
180
203
|
outputDir,
|
|
204
|
+
fixTier,
|
|
205
|
+
fixEnabled,
|
|
206
|
+
regression,
|
|
181
207
|
};
|
|
182
208
|
}
|
|
183
209
|
async function main() {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Legacy type → canonical category mapping
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
const LEGACY_TO_CATEGORY = {
|
|
7
|
+
'bug': 'functional',
|
|
8
|
+
'gap': 'functional',
|
|
9
|
+
'visual-regression': 'visual',
|
|
10
|
+
'ux-issue': 'ux',
|
|
11
|
+
};
|
|
12
|
+
const CANONICAL_CATEGORIES = new Set([
|
|
13
|
+
'visual', 'functional', 'ux', 'content', 'performance', 'console', 'accessibility',
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* Normalize any FindingType (legacy or canonical) to a canonical FindingCategory.
|
|
17
|
+
* Returns 'functional' for unrecognized types.
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeFindingType(raw) {
|
|
20
|
+
if (CANONICAL_CATEGORIES.has(raw)) {
|
|
21
|
+
return raw;
|
|
22
|
+
}
|
|
23
|
+
return LEGACY_TO_CATEGORY[raw] ?? 'functional';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Map a FindingType to its HealthScoreCategory.
|
|
27
|
+
* Same logic as normalizeFindingType — the category names match.
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeFindingCategory(type) {
|
|
30
|
+
if (type === 'verified-ok') {
|
|
31
|
+
return 'functional';
|
|
32
|
+
}
|
|
33
|
+
return normalizeFindingType(type);
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Severity definitions
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
export const SEVERITY_DEFINITIONS = {
|
|
39
|
+
critical: 'Blocks a core workflow, causes data loss, or crashes the app. Examples: form submit error page, checkout broken, data deleted without confirmation.',
|
|
40
|
+
high: 'Major feature broken or unusable, no workaround. Examples: search returns wrong results, file upload silently fails, auth redirect loop.',
|
|
41
|
+
medium: 'Feature works but with noticeable problems, workaround exists. Examples: slow page load (>5s), missing form validation, layout broken on mobile only.',
|
|
42
|
+
low: 'Minor cosmetic or polish issue. Examples: typo in footer, 1px alignment, hover state inconsistent.',
|
|
43
|
+
info: 'Observation or suggestion, not a defect. Examples: missing alt text noted, potential optimization.',
|
|
44
|
+
};
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Category definitions
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
export const CATEGORY_DEFINITIONS = {
|
|
49
|
+
visual: {
|
|
50
|
+
label: 'Visual/UI',
|
|
51
|
+
description: 'Layout breaks, broken images, z-index issues, font/color inconsistencies, animation glitches, alignment issues, dark mode problems.',
|
|
52
|
+
examples: ['Overlapping elements', 'Clipped text', 'Horizontal scrollbar', 'Incorrect z-index'],
|
|
53
|
+
},
|
|
54
|
+
functional: {
|
|
55
|
+
label: 'Functional',
|
|
56
|
+
description: 'Broken links, dead buttons, form validation failures, incorrect redirects, state not persisting, race conditions.',
|
|
57
|
+
examples: ['404 links', 'Click does nothing', 'Form bypasses validation', 'Data lost on refresh'],
|
|
58
|
+
},
|
|
59
|
+
ux: {
|
|
60
|
+
label: 'UX',
|
|
61
|
+
description: 'Confusing navigation, missing loading indicators, slow interactions, unclear error messages, no destructive-action confirmation.',
|
|
62
|
+
examples: ['No breadcrumbs', 'No loading spinner', '>500ms with no feedback', '"Something went wrong" with no detail'],
|
|
63
|
+
},
|
|
64
|
+
content: {
|
|
65
|
+
label: 'Content',
|
|
66
|
+
description: 'Typos, grammar errors, outdated text, placeholder/lorem ipsum left in, truncated text, wrong labels.',
|
|
67
|
+
examples: ['Typo in heading', 'Lorem ipsum visible', 'Button label says "Submit" instead of "Save"'],
|
|
68
|
+
},
|
|
69
|
+
performance: {
|
|
70
|
+
label: 'Performance',
|
|
71
|
+
description: 'Slow page loads (>3s), janky scrolling, layout shifts, excessive network requests, large unoptimized images.',
|
|
72
|
+
examples: ['Page takes 5s to load', 'Content jumping after load', '>50 network requests'],
|
|
73
|
+
},
|
|
74
|
+
console: {
|
|
75
|
+
label: 'Console/Errors',
|
|
76
|
+
description: 'JavaScript exceptions, failed network requests (4xx/5xx), deprecation warnings, CORS errors, mixed content.',
|
|
77
|
+
examples: ['Uncaught TypeError', '500 on API call', 'CORS blocked', 'Mixed content warning'],
|
|
78
|
+
},
|
|
79
|
+
accessibility: {
|
|
80
|
+
label: 'Accessibility',
|
|
81
|
+
description: 'Missing alt text, unlabeled inputs, broken keyboard navigation, focus traps, missing ARIA attributes, insufficient contrast.',
|
|
82
|
+
examples: ['Image without alt', 'Can\'t tab to button', 'Modal focus trap', 'Low contrast text'],
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Fix eligibility
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
const TIER_SEVERITIES = {
|
|
89
|
+
quick: new Set(['critical', 'high']),
|
|
90
|
+
standard: new Set(['critical', 'high', 'medium']),
|
|
91
|
+
exhaustive: new Set(['critical', 'high', 'medium', 'low']),
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Determine whether a finding should be fixed based on the selected tier.
|
|
95
|
+
* 'info' severity and 'verified-ok' type are never fixable.
|
|
96
|
+
*/
|
|
97
|
+
export function isFixable(finding, tier) {
|
|
98
|
+
if (finding.type === 'verified-ok' || finding.severity === 'info') {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return TIER_SEVERITIES[tier].has(finding.severity);
|
|
102
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { normalizeFindingCategory } from './finding_taxonomy.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Category weights (must sum to 1.0)
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
const CATEGORY_WEIGHTS = {
|
|
8
|
+
console: 0.15,
|
|
9
|
+
links: 0.10,
|
|
10
|
+
visual: 0.10,
|
|
11
|
+
functional: 0.20,
|
|
12
|
+
ux: 0.15,
|
|
13
|
+
performance: 0.10,
|
|
14
|
+
content: 0.05,
|
|
15
|
+
accessibility: 0.15,
|
|
16
|
+
};
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Severity deductions
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const SEVERITY_DEDUCTIONS = {
|
|
21
|
+
critical: 25,
|
|
22
|
+
high: 15,
|
|
23
|
+
medium: 8,
|
|
24
|
+
low: 3,
|
|
25
|
+
info: 0,
|
|
26
|
+
};
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Public API
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Map a finding to its health-score category.
|
|
32
|
+
* Handles both canonical categories and legacy finding types.
|
|
33
|
+
*/
|
|
34
|
+
export function mapFindingToCategory(finding) {
|
|
35
|
+
return normalizeFindingCategory(finding.type);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Compute a weighted health score (0-100) from a set of findings.
|
|
39
|
+
*
|
|
40
|
+
* Each of 8 categories starts at 100. Per finding, deduct based on severity
|
|
41
|
+
* (critical -25, high -15, medium -8, low -3). Clamp each category at 0.
|
|
42
|
+
* Overall = sum(category_score * weight).
|
|
43
|
+
*/
|
|
44
|
+
export function computeHealthScore(findings) {
|
|
45
|
+
// Initialize per-category state
|
|
46
|
+
const categoryState = {
|
|
47
|
+
console: { score: 100, findings: [] },
|
|
48
|
+
links: { score: 100, findings: [] },
|
|
49
|
+
visual: { score: 100, findings: [] },
|
|
50
|
+
functional: { score: 100, findings: [] },
|
|
51
|
+
ux: { score: 100, findings: [] },
|
|
52
|
+
performance: { score: 100, findings: [] },
|
|
53
|
+
content: { score: 100, findings: [] },
|
|
54
|
+
accessibility: { score: 100, findings: [] },
|
|
55
|
+
};
|
|
56
|
+
// Apply deductions
|
|
57
|
+
for (const finding of findings) {
|
|
58
|
+
if (finding.type === 'verified-ok') {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const category = mapFindingToCategory(finding);
|
|
62
|
+
const deduction = SEVERITY_DEDUCTIONS[finding.severity] ?? 0;
|
|
63
|
+
categoryState[category].score = Math.max(0, categoryState[category].score - deduction);
|
|
64
|
+
categoryState[category].findings.push(finding.id);
|
|
65
|
+
}
|
|
66
|
+
// Build category scores
|
|
67
|
+
const categories = Object.keys(CATEGORY_WEIGHTS).map((cat) => ({
|
|
68
|
+
category: cat,
|
|
69
|
+
score: categoryState[cat].score,
|
|
70
|
+
weight: CATEGORY_WEIGHTS[cat],
|
|
71
|
+
findings: categoryState[cat].findings,
|
|
72
|
+
}));
|
|
73
|
+
// Compute weighted overall
|
|
74
|
+
const overall = Math.round(categories.reduce((sum, c) => sum + c.score * c.weight, 0));
|
|
75
|
+
return {
|
|
76
|
+
overall,
|
|
77
|
+
categories,
|
|
78
|
+
computedAt: new Date().toISOString(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Render a health score as a markdown table.
|
|
83
|
+
*/
|
|
84
|
+
export function formatHealthScoreMarkdown(score) {
|
|
85
|
+
const lines = [
|
|
86
|
+
`## Health Score: ${score.overall}/100`,
|
|
87
|
+
'',
|
|
88
|
+
'| Category | Score | Weight | Issues |',
|
|
89
|
+
'|----------|-------|--------|--------|',
|
|
90
|
+
];
|
|
91
|
+
for (const cat of score.categories) {
|
|
92
|
+
const pct = `${Math.round(cat.weight * 100)}%`;
|
|
93
|
+
lines.push(`| ${capitalize(cat.category)} | ${cat.score} | ${pct} | ${cat.findings.length} |`);
|
|
94
|
+
}
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
function capitalize(s) {
|
|
98
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
99
|
+
}
|
|
@@ -5,10 +5,14 @@ import { mkdirSync } from 'fs';
|
|
|
5
5
|
import { logger } from '../logger.js';
|
|
6
6
|
import { runPhase1 } from './phase1/runner.js';
|
|
7
7
|
import { runAgentLoop } from './phase2/agent_loop.js';
|
|
8
|
+
import { AgentBrowser } from './phase2/agent_browser.js';
|
|
8
9
|
import { computeVerdict } from './phase3/verdict.js';
|
|
9
10
|
import { generateReport } from './phase3/reporter.js';
|
|
10
11
|
import { generateSpecsForFindings } from './phase3/spec_generator.js';
|
|
11
12
|
import { submitFeedback } from './phase3/feedback.js';
|
|
13
|
+
import { computeHealthScore } from './health_score.js';
|
|
14
|
+
import { runFixLoop } from './phase25/fix_loop.js';
|
|
15
|
+
import { saveBaseline, loadBaseline, compareBaselines } from './regression/baseline.js';
|
|
12
16
|
function emptyPhase2Result() {
|
|
13
17
|
return { findings: [], flowsExplored: [], actionsCount: 0, tokensUsed: 0, costUSD: 0, durationMs: 0 };
|
|
14
18
|
}
|
|
@@ -76,15 +80,64 @@ export async function runQAAgent(inputConfig) {
|
|
|
76
80
|
return earlyReturn(config, phase1, phase2);
|
|
77
81
|
}
|
|
78
82
|
// -----------------------------------------------------------------------
|
|
83
|
+
// Phase 2.5: Fix Loop (optional)
|
|
84
|
+
// -----------------------------------------------------------------------
|
|
85
|
+
let phase25;
|
|
86
|
+
let healthScore = phase2.healthScore || computeHealthScore(phase2.findings);
|
|
87
|
+
if (config.fixEnabled !== false && phase2.findings.length > 0) {
|
|
88
|
+
logger.info('=== Phase 2.5: Fix Loop ===');
|
|
89
|
+
// Create a browser instance for the fix loop to verify fixes
|
|
90
|
+
const fixBrowser = new AgentBrowser({ session: config.headed ? 'qa-fix-headed' : undefined });
|
|
91
|
+
try {
|
|
92
|
+
const projectRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf-8' }).trim();
|
|
93
|
+
phase25 = await runFixLoop(config, phase2.findings, fixBrowser, projectRoot);
|
|
94
|
+
healthScore = phase25.healthScoreAfter;
|
|
95
|
+
logger.info('Phase 2.5 complete', {
|
|
96
|
+
attempted: phase25.fixesAttempted,
|
|
97
|
+
verified: phase25.fixesVerified,
|
|
98
|
+
reverted: phase25.fixesReverted,
|
|
99
|
+
scoreDelta: `${phase25.healthScoreBefore.overall} → ${phase25.healthScoreAfter.overall}`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
logger.warn('Phase 2.5 failed, continuing without fixes', { error: String(err) });
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
if (!config.headed) {
|
|
107
|
+
fixBrowser.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// -----------------------------------------------------------------------
|
|
112
|
+
// Regression comparison (optional)
|
|
113
|
+
// -----------------------------------------------------------------------
|
|
114
|
+
let regressionComparison;
|
|
115
|
+
if (config.regression) {
|
|
116
|
+
const baseline = loadBaseline(outputDir);
|
|
117
|
+
if (baseline) {
|
|
118
|
+
regressionComparison = compareBaselines(healthScore, phase2.findings, baseline);
|
|
119
|
+
logger.info('Regression comparison', {
|
|
120
|
+
scoreDelta: regressionComparison.scoreDelta,
|
|
121
|
+
fixedIssues: regressionComparison.fixedIssues.length,
|
|
122
|
+
newIssues: regressionComparison.newIssues.length,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
logger.info('No baseline found — saving current run as baseline');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Always save baseline for future comparisons
|
|
130
|
+
saveBaseline(outputDir, healthScore, phase2.findings, config.baseUrl);
|
|
131
|
+
// -----------------------------------------------------------------------
|
|
79
132
|
// Phase 3: Report + Spec Generation + Verdict
|
|
80
133
|
// -----------------------------------------------------------------------
|
|
81
134
|
logger.info('=== Phase 3: Report & Verdict ===');
|
|
82
135
|
// Generate specs for discovered bugs
|
|
83
136
|
const generatedSpecs = generateSpecsForFindings(phase2.findings, config);
|
|
84
|
-
// Compute verdict
|
|
85
|
-
const verdict = computeVerdict(phase1, phase2);
|
|
137
|
+
// Compute verdict (now with health score)
|
|
138
|
+
const verdict = computeVerdict(phase1, phase2, healthScore, phase25);
|
|
86
139
|
// Generate report
|
|
87
|
-
const phase3 = generateReport(config, phase1, phase2, verdict, generatedSpecs);
|
|
140
|
+
const phase3 = generateReport(config, phase1, phase2, verdict, generatedSpecs, phase25, healthScore, regressionComparison);
|
|
88
141
|
// Submit feedback
|
|
89
142
|
try {
|
|
90
143
|
submitFeedback(config);
|
|
@@ -94,27 +147,32 @@ export async function runQAAgent(inputConfig) {
|
|
|
94
147
|
}
|
|
95
148
|
logger.info(`=== QA Agent Complete: ${verdict.decision.toUpperCase()} ===`);
|
|
96
149
|
logger.info(verdict.reason);
|
|
97
|
-
return buildQAReport(config, phase1, phase2, phase3, verdict);
|
|
150
|
+
return buildQAReport(config, phase1, phase2, phase3, verdict, phase25, healthScore, regressionComparison);
|
|
98
151
|
}
|
|
99
152
|
function earlyReturn(config, phase1, phase2) {
|
|
100
153
|
const p2 = phase2 || emptyPhase2Result();
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
|
|
154
|
+
const healthScore = computeHealthScore(p2.findings);
|
|
155
|
+
const verdict = computeVerdict(phase1, p2, healthScore);
|
|
156
|
+
const phase3 = generateReport(config, phase1, p2, verdict, [], undefined, healthScore);
|
|
157
|
+
return buildQAReport(config, phase1, p2, phase3, verdict, undefined, healthScore);
|
|
104
158
|
}
|
|
105
|
-
function buildQAReport(config, phase1, phase2, phase3, verdict) {
|
|
159
|
+
function buildQAReport(config, phase1, phase2, phase3, verdict, phase25, healthScore, regressionComparison) {
|
|
106
160
|
return {
|
|
107
|
-
schemaVersion: '1.
|
|
161
|
+
schemaVersion: '1.1.0',
|
|
108
162
|
generatedAt: new Date().toISOString(),
|
|
109
163
|
mode: config.mode,
|
|
110
164
|
config: {
|
|
111
165
|
baseUrl: config.baseUrl,
|
|
112
166
|
timeLimitMinutes: config.timeLimitMinutes,
|
|
113
167
|
budgetUSD: config.budgetUSD,
|
|
168
|
+
fixTier: config.fixTier,
|
|
114
169
|
},
|
|
115
170
|
phase1,
|
|
116
171
|
phase2,
|
|
172
|
+
phase25,
|
|
117
173
|
phase3,
|
|
118
174
|
verdict,
|
|
175
|
+
healthScore,
|
|
176
|
+
regressionComparison,
|
|
119
177
|
};
|
|
120
178
|
}
|