playwright-intelligence-engine 2.0.0 → 2.0.2
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 +229 -229
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/core/version.d.ts +3 -3
- package/dist/index.js +81 -24
- package/package.json +76 -76
- package/src/authoring/author.ts +81 -81
- package/src/authoring/generation-contract.ts +283 -283
- package/src/authoring/hybrid-author.ts +115 -115
- package/src/authoring/index.ts +7 -7
- package/src/authoring/llm-author.ts +282 -282
- package/src/authoring/naming/name-plan.ts +28 -28
- package/src/authoring/naming/name-planner.ts +339 -339
- package/src/authoring/naming/naming-linter.ts +292 -292
- package/src/cli/index.ts +1238 -1172
- package/src/collectors/git-diff-collector.ts +190 -190
- package/src/core/version.ts +25 -25
- package/src/diagnostics/diagnostic-report.ts +204 -204
- package/src/diagnostics/failure-classifier.ts +701 -701
- package/src/evaluation/repository-runner.ts +197 -197
- package/src/gate/quality.ts +101 -101
- package/src/generator/architecture-generator.ts +582 -582
- package/src/generator/assertion-quality-gate.ts +88 -88
- package/src/generator/candidate-writer.ts +288 -288
- package/src/generator/test-generator.ts +52 -52
- package/src/index.ts +107 -107
- package/src/intelligence/evidence-bundle.ts +300 -300
- package/src/intelligence/generation-agent.ts +561 -561
- package/src/intelligence/generation-verifier.ts +1115 -1115
- package/src/intelligence/locator-disambiguation-engine.ts +331 -331
- package/src/intelligence/project-intelligence.ts +140 -140
- package/src/intelligence/repository-profile.ts +796 -796
- package/src/intelligence/trace-proposal-workflow.ts +318 -318
- package/src/intelligence/workflow-planner.ts +966 -966
- package/src/mcp/server.ts +970 -970
- package/src/perception/app-model.ts +32 -32
- package/src/perception/crawler.ts +263 -263
- package/src/perception/index.ts +3 -3
- package/src/repair/assertion-hasher.ts +386 -386
- package/src/repair/diff-allowlist.ts +612 -612
- package/src/repair/intent-validator.ts +761 -761
- package/src/repair/repair-planner.ts +467 -467
- package/src/repair/statement-patcher.ts +544 -544
- package/src/reproduce/interactive-workflow-runner.ts +828 -828
- package/src/sandbox/execution-runner.ts +408 -408
- package/src/selector/abstention-policy.ts +175 -175
- package/src/selector/candidate-extractor.ts +343 -343
- package/src/selector/repair-candidate-ranker.ts +92 -92
- package/src/selector/selector-scorer.ts +344 -344
|
@@ -1,197 +1,197 @@
|
|
|
1
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { RepositoryManifest } from './repository-inspector.js';
|
|
4
|
-
|
|
5
|
-
export interface RepositoryRunOptions {
|
|
6
|
-
script?: string | undefined;
|
|
7
|
-
timeoutMs?: number | undefined;
|
|
8
|
-
environment?: Record<string, string> | undefined;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface RepositoryRunResult {
|
|
12
|
-
repository: string;
|
|
13
|
-
command: string;
|
|
14
|
-
args: string[];
|
|
15
|
-
cwd: string;
|
|
16
|
-
status: 'PASS' | 'FAIL' | 'TIMEOUT' | 'BLOCKED';
|
|
17
|
-
exitCode: number | null;
|
|
18
|
-
signal?: string | undefined;
|
|
19
|
-
stdout: string;
|
|
20
|
-
stderr: string;
|
|
21
|
-
durationMs: number;
|
|
22
|
-
reason?: string | undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
26
|
-
const SAFE_COMMANDS = new Set(['npm', 'pnpm', 'yarn', 'npx']);
|
|
27
|
-
|
|
28
|
-
function executableFor(manager: RepositoryManifest['packageManager']): string {
|
|
29
|
-
if (manager === 'pnpm') return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
|
|
30
|
-
if (manager === 'yarn') return process.platform === 'win32' ? 'yarn.cmd' : 'yarn';
|
|
31
|
-
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function chooseScript(manifest: RepositoryManifest, requested?: string): string | undefined {
|
|
35
|
-
if (requested) return requested;
|
|
36
|
-
const candidates = ['test:e2e', 'test:smoke', 'test:sanity', 'test'];
|
|
37
|
-
return candidates.find((candidate) => manifest.scripts[candidate]);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export class RepositoryRunner {
|
|
41
|
-
public static async runBaseline(manifest: RepositoryManifest, options: RepositoryRunOptions = {}): Promise<RepositoryRunResult> {
|
|
42
|
-
if (manifest.status !== 'DISCOVERED') {
|
|
43
|
-
return {
|
|
44
|
-
repository: manifest.name,
|
|
45
|
-
command: '',
|
|
46
|
-
args: [],
|
|
47
|
-
cwd: manifest.root,
|
|
48
|
-
status: 'BLOCKED',
|
|
49
|
-
exitCode: null,
|
|
50
|
-
stdout: '',
|
|
51
|
-
stderr: '',
|
|
52
|
-
durationMs: 0,
|
|
53
|
-
reason: `Repository status is ${manifest.status}`,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const script = chooseScript(manifest, options.script);
|
|
58
|
-
const executable = executableFor(manifest.packageManager);
|
|
59
|
-
const args = script ? ['run', script] : ['exec', 'playwright', 'test'];
|
|
60
|
-
return this.runCommand(manifest, executable, args, options);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
public static runCommand(
|
|
64
|
-
manifest: RepositoryManifest,
|
|
65
|
-
executable: string,
|
|
66
|
-
args: string[],
|
|
67
|
-
options: RepositoryRunOptions = {},
|
|
68
|
-
): Promise<RepositoryRunResult> {
|
|
69
|
-
const normalizedExecutable = path.basename(executable).replace(/\.cmd$/i, '').toLowerCase();
|
|
70
|
-
if (!SAFE_COMMANDS.has(normalizedExecutable)) {
|
|
71
|
-
return Promise.resolve({
|
|
72
|
-
repository: manifest.name,
|
|
73
|
-
command: executable,
|
|
74
|
-
args,
|
|
75
|
-
cwd: manifest.root,
|
|
76
|
-
status: 'BLOCKED',
|
|
77
|
-
exitCode: null,
|
|
78
|
-
stdout: '',
|
|
79
|
-
stderr: '',
|
|
80
|
-
durationMs: 0,
|
|
81
|
-
reason: `Command '${executable}' is not in the repository evaluation allowlist`,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (args.some((argument) => /[&|<>]/.test(argument))) {
|
|
86
|
-
return Promise.resolve({
|
|
87
|
-
repository: manifest.name,
|
|
88
|
-
command: executable,
|
|
89
|
-
args,
|
|
90
|
-
cwd: manifest.root,
|
|
91
|
-
status: 'BLOCKED',
|
|
92
|
-
exitCode: null,
|
|
93
|
-
stdout: '',
|
|
94
|
-
stderr: '',
|
|
95
|
-
durationMs: 0,
|
|
96
|
-
reason: 'Command arguments contain shell metacharacters',
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const isWindows = process.platform === 'win32';
|
|
101
|
-
const command = isWindows ? (process.env.ComSpec ?? 'cmd.exe') : executable;
|
|
102
|
-
const spawnArgs = isWindows
|
|
103
|
-
? ['/d', '/s', '/c', [executable, ...args].map((argument) => argument.includes(' ') ? `"${argument.replace(/"/g, '\\"')}"` : argument).join(' ')]
|
|
104
|
-
: args;
|
|
105
|
-
|
|
106
|
-
return new Promise((resolve) => {
|
|
107
|
-
const started = Date.now();
|
|
108
|
-
const child = spawn(command, spawnArgs, {
|
|
109
|
-
cwd: manifest.root,
|
|
110
|
-
shell: false,
|
|
111
|
-
windowsHide: true,
|
|
112
|
-
env: { ...process.env, CI: 'true', ...options.environment },
|
|
113
|
-
});
|
|
114
|
-
let stdout = '';
|
|
115
|
-
let stderr = '';
|
|
116
|
-
let truncated = false;
|
|
117
|
-
let timedOut = false;
|
|
118
|
-
let settled = false;
|
|
119
|
-
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
120
|
-
const finish = (result: RepositoryRunResult): void => {
|
|
121
|
-
if (settled) return;
|
|
122
|
-
settled = true;
|
|
123
|
-
if (timeout) clearTimeout(timeout);
|
|
124
|
-
resolve(result);
|
|
125
|
-
};
|
|
126
|
-
const append = (target: 'stdout' | 'stderr', chunk: Buffer): void => {
|
|
127
|
-
if (Buffer.byteLength(stdout) + Buffer.byteLength(stderr) >= MAX_OUTPUT_BYTES) {
|
|
128
|
-
truncated = true;
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (target === 'stdout') stdout += chunk.toString('utf8');
|
|
132
|
-
else stderr += chunk.toString('utf8');
|
|
133
|
-
};
|
|
134
|
-
child.stdout?.on('data', (chunk: Buffer) => append('stdout', chunk));
|
|
135
|
-
child.stderr?.on('data', (chunk: Buffer) => append('stderr', chunk));
|
|
136
|
-
|
|
137
|
-
timeout = setTimeout(() => {
|
|
138
|
-
timedOut = true;
|
|
139
|
-
child.kill('SIGTERM');
|
|
140
|
-
if (process.platform === 'win32' && child.pid) {
|
|
141
|
-
spawnSync('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], {
|
|
142
|
-
windowsHide: true,
|
|
143
|
-
stdio: 'ignore',
|
|
144
|
-
shell: false,
|
|
145
|
-
timeout: 5_000,
|
|
146
|
-
});
|
|
147
|
-
child.stdout?.destroy();
|
|
148
|
-
child.stderr?.destroy();
|
|
149
|
-
}
|
|
150
|
-
finish({
|
|
151
|
-
repository: manifest.name,
|
|
152
|
-
command: executable,
|
|
153
|
-
args,
|
|
154
|
-
cwd: manifest.root,
|
|
155
|
-
status: 'TIMEOUT',
|
|
156
|
-
exitCode: null,
|
|
157
|
-
stdout,
|
|
158
|
-
stderr,
|
|
159
|
-
durationMs: Date.now() - started,
|
|
160
|
-
reason: 'Command exceeded the configured timeout',
|
|
161
|
-
});
|
|
162
|
-
}, options.timeoutMs ?? 15 * 60 * 1000);
|
|
163
|
-
|
|
164
|
-
child.on('error', (error) => {
|
|
165
|
-
finish({
|
|
166
|
-
repository: manifest.name,
|
|
167
|
-
command: executable,
|
|
168
|
-
args,
|
|
169
|
-
cwd: manifest.root,
|
|
170
|
-
status: 'BLOCKED',
|
|
171
|
-
exitCode: null,
|
|
172
|
-
stdout,
|
|
173
|
-
stderr,
|
|
174
|
-
durationMs: Date.now() - started,
|
|
175
|
-
reason: `Unable to start command: ${error.message}`,
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
child.on('close', (exitCode, signal) => {
|
|
180
|
-
finish({
|
|
181
|
-
repository: manifest.name,
|
|
182
|
-
command: executable,
|
|
183
|
-
args,
|
|
184
|
-
cwd: manifest.root,
|
|
185
|
-
status: timedOut ? 'TIMEOUT' : exitCode === 0 ? 'PASS' : 'FAIL',
|
|
186
|
-
exitCode,
|
|
187
|
-
signal: signal ?? undefined,
|
|
188
|
-
stdout,
|
|
189
|
-
stderr,
|
|
190
|
-
durationMs: Date.now() - started,
|
|
191
|
-
reason: truncated ? 'Output truncated at 16 MB' : undefined,
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { RepositoryManifest } from './repository-inspector.js';
|
|
4
|
+
|
|
5
|
+
export interface RepositoryRunOptions {
|
|
6
|
+
script?: string | undefined;
|
|
7
|
+
timeoutMs?: number | undefined;
|
|
8
|
+
environment?: Record<string, string> | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RepositoryRunResult {
|
|
12
|
+
repository: string;
|
|
13
|
+
command: string;
|
|
14
|
+
args: string[];
|
|
15
|
+
cwd: string;
|
|
16
|
+
status: 'PASS' | 'FAIL' | 'TIMEOUT' | 'BLOCKED';
|
|
17
|
+
exitCode: number | null;
|
|
18
|
+
signal?: string | undefined;
|
|
19
|
+
stdout: string;
|
|
20
|
+
stderr: string;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
reason?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
26
|
+
const SAFE_COMMANDS = new Set(['npm', 'pnpm', 'yarn', 'npx']);
|
|
27
|
+
|
|
28
|
+
function executableFor(manager: RepositoryManifest['packageManager']): string {
|
|
29
|
+
if (manager === 'pnpm') return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
|
|
30
|
+
if (manager === 'yarn') return process.platform === 'win32' ? 'yarn.cmd' : 'yarn';
|
|
31
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function chooseScript(manifest: RepositoryManifest, requested?: string): string | undefined {
|
|
35
|
+
if (requested) return requested;
|
|
36
|
+
const candidates = ['test:e2e', 'test:smoke', 'test:sanity', 'test'];
|
|
37
|
+
return candidates.find((candidate) => manifest.scripts[candidate]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class RepositoryRunner {
|
|
41
|
+
public static async runBaseline(manifest: RepositoryManifest, options: RepositoryRunOptions = {}): Promise<RepositoryRunResult> {
|
|
42
|
+
if (manifest.status !== 'DISCOVERED') {
|
|
43
|
+
return {
|
|
44
|
+
repository: manifest.name,
|
|
45
|
+
command: '',
|
|
46
|
+
args: [],
|
|
47
|
+
cwd: manifest.root,
|
|
48
|
+
status: 'BLOCKED',
|
|
49
|
+
exitCode: null,
|
|
50
|
+
stdout: '',
|
|
51
|
+
stderr: '',
|
|
52
|
+
durationMs: 0,
|
|
53
|
+
reason: `Repository status is ${manifest.status}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const script = chooseScript(manifest, options.script);
|
|
58
|
+
const executable = executableFor(manifest.packageManager);
|
|
59
|
+
const args = script ? ['run', script] : ['exec', 'playwright', 'test'];
|
|
60
|
+
return this.runCommand(manifest, executable, args, options);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public static runCommand(
|
|
64
|
+
manifest: RepositoryManifest,
|
|
65
|
+
executable: string,
|
|
66
|
+
args: string[],
|
|
67
|
+
options: RepositoryRunOptions = {},
|
|
68
|
+
): Promise<RepositoryRunResult> {
|
|
69
|
+
const normalizedExecutable = path.basename(executable).replace(/\.cmd$/i, '').toLowerCase();
|
|
70
|
+
if (!SAFE_COMMANDS.has(normalizedExecutable)) {
|
|
71
|
+
return Promise.resolve({
|
|
72
|
+
repository: manifest.name,
|
|
73
|
+
command: executable,
|
|
74
|
+
args,
|
|
75
|
+
cwd: manifest.root,
|
|
76
|
+
status: 'BLOCKED',
|
|
77
|
+
exitCode: null,
|
|
78
|
+
stdout: '',
|
|
79
|
+
stderr: '',
|
|
80
|
+
durationMs: 0,
|
|
81
|
+
reason: `Command '${executable}' is not in the repository evaluation allowlist`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (args.some((argument) => /[&|<>]/.test(argument))) {
|
|
86
|
+
return Promise.resolve({
|
|
87
|
+
repository: manifest.name,
|
|
88
|
+
command: executable,
|
|
89
|
+
args,
|
|
90
|
+
cwd: manifest.root,
|
|
91
|
+
status: 'BLOCKED',
|
|
92
|
+
exitCode: null,
|
|
93
|
+
stdout: '',
|
|
94
|
+
stderr: '',
|
|
95
|
+
durationMs: 0,
|
|
96
|
+
reason: 'Command arguments contain shell metacharacters',
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const isWindows = process.platform === 'win32';
|
|
101
|
+
const command = isWindows ? (process.env.ComSpec ?? 'cmd.exe') : executable;
|
|
102
|
+
const spawnArgs = isWindows
|
|
103
|
+
? ['/d', '/s', '/c', [executable, ...args].map((argument) => argument.includes(' ') ? `"${argument.replace(/"/g, '\\"')}"` : argument).join(' ')]
|
|
104
|
+
: args;
|
|
105
|
+
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
const started = Date.now();
|
|
108
|
+
const child = spawn(command, spawnArgs, {
|
|
109
|
+
cwd: manifest.root,
|
|
110
|
+
shell: false,
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
env: { ...process.env, CI: 'true', ...options.environment },
|
|
113
|
+
});
|
|
114
|
+
let stdout = '';
|
|
115
|
+
let stderr = '';
|
|
116
|
+
let truncated = false;
|
|
117
|
+
let timedOut = false;
|
|
118
|
+
let settled = false;
|
|
119
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
120
|
+
const finish = (result: RepositoryRunResult): void => {
|
|
121
|
+
if (settled) return;
|
|
122
|
+
settled = true;
|
|
123
|
+
if (timeout) clearTimeout(timeout);
|
|
124
|
+
resolve(result);
|
|
125
|
+
};
|
|
126
|
+
const append = (target: 'stdout' | 'stderr', chunk: Buffer): void => {
|
|
127
|
+
if (Buffer.byteLength(stdout) + Buffer.byteLength(stderr) >= MAX_OUTPUT_BYTES) {
|
|
128
|
+
truncated = true;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (target === 'stdout') stdout += chunk.toString('utf8');
|
|
132
|
+
else stderr += chunk.toString('utf8');
|
|
133
|
+
};
|
|
134
|
+
child.stdout?.on('data', (chunk: Buffer) => append('stdout', chunk));
|
|
135
|
+
child.stderr?.on('data', (chunk: Buffer) => append('stderr', chunk));
|
|
136
|
+
|
|
137
|
+
timeout = setTimeout(() => {
|
|
138
|
+
timedOut = true;
|
|
139
|
+
child.kill('SIGTERM');
|
|
140
|
+
if (process.platform === 'win32' && child.pid) {
|
|
141
|
+
spawnSync('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], {
|
|
142
|
+
windowsHide: true,
|
|
143
|
+
stdio: 'ignore',
|
|
144
|
+
shell: false,
|
|
145
|
+
timeout: 5_000,
|
|
146
|
+
});
|
|
147
|
+
child.stdout?.destroy();
|
|
148
|
+
child.stderr?.destroy();
|
|
149
|
+
}
|
|
150
|
+
finish({
|
|
151
|
+
repository: manifest.name,
|
|
152
|
+
command: executable,
|
|
153
|
+
args,
|
|
154
|
+
cwd: manifest.root,
|
|
155
|
+
status: 'TIMEOUT',
|
|
156
|
+
exitCode: null,
|
|
157
|
+
stdout,
|
|
158
|
+
stderr,
|
|
159
|
+
durationMs: Date.now() - started,
|
|
160
|
+
reason: 'Command exceeded the configured timeout',
|
|
161
|
+
});
|
|
162
|
+
}, options.timeoutMs ?? 15 * 60 * 1000);
|
|
163
|
+
|
|
164
|
+
child.on('error', (error) => {
|
|
165
|
+
finish({
|
|
166
|
+
repository: manifest.name,
|
|
167
|
+
command: executable,
|
|
168
|
+
args,
|
|
169
|
+
cwd: manifest.root,
|
|
170
|
+
status: 'BLOCKED',
|
|
171
|
+
exitCode: null,
|
|
172
|
+
stdout,
|
|
173
|
+
stderr,
|
|
174
|
+
durationMs: Date.now() - started,
|
|
175
|
+
reason: `Unable to start command: ${error.message}`,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
child.on('close', (exitCode, signal) => {
|
|
180
|
+
finish({
|
|
181
|
+
repository: manifest.name,
|
|
182
|
+
command: executable,
|
|
183
|
+
args,
|
|
184
|
+
cwd: manifest.root,
|
|
185
|
+
status: timedOut ? 'TIMEOUT' : exitCode === 0 ? 'PASS' : 'FAIL',
|
|
186
|
+
exitCode,
|
|
187
|
+
signal: signal ?? undefined,
|
|
188
|
+
stdout,
|
|
189
|
+
stderr,
|
|
190
|
+
durationMs: Date.now() - started,
|
|
191
|
+
reason: truncated ? 'Output truncated at 16 MB' : undefined,
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
package/src/gate/quality.ts
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
import { NamingLinter, NamingLintReport } from '../authoring/naming/naming-linter.js';
|
|
2
|
-
import type { NamePlan } from '../authoring/naming/name-plan.js';
|
|
3
|
-
|
|
4
|
-
export interface CodeQualityGateInput {
|
|
5
|
-
symbols?: NamePlan | undefined;
|
|
6
|
-
files?: Array<{ relativePath: string; content: string }> | undefined;
|
|
7
|
-
diff?: string | undefined;
|
|
8
|
-
originalSource?: string | undefined;
|
|
9
|
-
patchedSource?: string | undefined;
|
|
10
|
-
locatorPolicy?: string | undefined;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type QualityGateInput = CodeQualityGateInput;
|
|
14
|
-
|
|
15
|
-
export interface QualityGateResult {
|
|
16
|
-
passed: boolean;
|
|
17
|
-
namingReport?: NamingLintReport | undefined;
|
|
18
|
-
duplicateSelectors: string[];
|
|
19
|
-
locatorPolicyViolations: string[];
|
|
20
|
-
assertionStrengthPassed: boolean;
|
|
21
|
-
changedLines: number;
|
|
22
|
-
violations: string[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class QualityGate {
|
|
26
|
-
public static evaluate(input: QualityGateInput): QualityGateResult {
|
|
27
|
-
const violations: string[] = [];
|
|
28
|
-
|
|
29
|
-
// 1. Naming Conformance
|
|
30
|
-
let namingReport: NamingLintReport | undefined;
|
|
31
|
-
if (input.symbols && input.symbols.symbols.length > 0) {
|
|
32
|
-
namingReport = NamingLinter.lint(input.symbols);
|
|
33
|
-
if (!namingReport.passed) {
|
|
34
|
-
for (const v of namingReport.violations) {
|
|
35
|
-
violations.push(`Naming quality violation [${v.kind}]: ${v.message}`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 2. Duplicate Selectors Check
|
|
41
|
-
const duplicateSelectors: string[] = [];
|
|
42
|
-
if (input.files) {
|
|
43
|
-
for (const file of input.files) {
|
|
44
|
-
if (file.relativePath.endsWith('Page.ts')) {
|
|
45
|
-
const locatorMatches = [...file.content.matchAll(/this\.page\.(?:getByRole|getByText|getByTestId|locator)\s*\(([^)]+)\)/g)];
|
|
46
|
-
const seen = new Set<string>();
|
|
47
|
-
for (const match of locatorMatches) {
|
|
48
|
-
const locStr = match[1]!.trim();
|
|
49
|
-
if (seen.has(locStr)) {
|
|
50
|
-
duplicateSelectors.push(locStr);
|
|
51
|
-
violations.push(`Duplicate selector in '${file.relativePath}': ${locStr}`);
|
|
52
|
-
}
|
|
53
|
-
seen.add(locStr);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 3. Locator Policy Conformance (e.g. Reject raw CSS when ARIA is required)
|
|
60
|
-
const locatorPolicyViolations: string[] = [];
|
|
61
|
-
if (input.files && input.locatorPolicy === 'prefer-role-over-css') {
|
|
62
|
-
for (const file of input.files) {
|
|
63
|
-
const rawCssMatches = [...file.content.matchAll(/this\.page\.locator\s*\(\s*['"]([#.]|div|button|input)[^'"]*['"]\s*\)/g)];
|
|
64
|
-
for (const match of rawCssMatches) {
|
|
65
|
-
locatorPolicyViolations.push(match[0]);
|
|
66
|
-
violations.push(`Locator policy violation in '${file.relativePath}': raw CSS used instead of role-based locator: ${match[0]}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 4. Assertion Strength Non-Downgrade Check
|
|
72
|
-
let assertionStrengthPassed = true;
|
|
73
|
-
if (input.originalSource && input.patchedSource) {
|
|
74
|
-
const origHasExactText = /\b(toHaveText|toHaveValue|toContainText)\b/.test(input.originalSource);
|
|
75
|
-
const patchHasExactText = /\b(toHaveText|toHaveValue|toContainText)\b/.test(input.patchedSource);
|
|
76
|
-
const patchDowngradedToVisible = /\btoBeVisible\b/.test(input.patchedSource) && !/\btoBeVisible\b/.test(input.originalSource);
|
|
77
|
-
|
|
78
|
-
if (origHasExactText && !patchHasExactText && patchDowngradedToVisible) {
|
|
79
|
-
assertionStrengthPassed = false;
|
|
80
|
-
violations.push('Assertion strength regression: exact text assertion was downgraded to toBeVisible');
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// 5. Diff Minimality (Count changed lines)
|
|
85
|
-
let changedLines = 0;
|
|
86
|
-
if (input.diff) {
|
|
87
|
-
const diffLines = input.diff.split(/\r?\n/);
|
|
88
|
-
changedLines = diffLines.filter((l) => (l.startsWith('+') || l.startsWith('-')) && !l.startsWith('+++') && !l.startsWith('---')).length;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
passed: violations.length === 0,
|
|
93
|
-
namingReport,
|
|
94
|
-
duplicateSelectors,
|
|
95
|
-
locatorPolicyViolations,
|
|
96
|
-
assertionStrengthPassed,
|
|
97
|
-
changedLines,
|
|
98
|
-
violations,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
}
|
|
1
|
+
import { NamingLinter, NamingLintReport } from '../authoring/naming/naming-linter.js';
|
|
2
|
+
import type { NamePlan } from '../authoring/naming/name-plan.js';
|
|
3
|
+
|
|
4
|
+
export interface CodeQualityGateInput {
|
|
5
|
+
symbols?: NamePlan | undefined;
|
|
6
|
+
files?: Array<{ relativePath: string; content: string }> | undefined;
|
|
7
|
+
diff?: string | undefined;
|
|
8
|
+
originalSource?: string | undefined;
|
|
9
|
+
patchedSource?: string | undefined;
|
|
10
|
+
locatorPolicy?: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type QualityGateInput = CodeQualityGateInput;
|
|
14
|
+
|
|
15
|
+
export interface QualityGateResult {
|
|
16
|
+
passed: boolean;
|
|
17
|
+
namingReport?: NamingLintReport | undefined;
|
|
18
|
+
duplicateSelectors: string[];
|
|
19
|
+
locatorPolicyViolations: string[];
|
|
20
|
+
assertionStrengthPassed: boolean;
|
|
21
|
+
changedLines: number;
|
|
22
|
+
violations: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class QualityGate {
|
|
26
|
+
public static evaluate(input: QualityGateInput): QualityGateResult {
|
|
27
|
+
const violations: string[] = [];
|
|
28
|
+
|
|
29
|
+
// 1. Naming Conformance
|
|
30
|
+
let namingReport: NamingLintReport | undefined;
|
|
31
|
+
if (input.symbols && input.symbols.symbols.length > 0) {
|
|
32
|
+
namingReport = NamingLinter.lint(input.symbols);
|
|
33
|
+
if (!namingReport.passed) {
|
|
34
|
+
for (const v of namingReport.violations) {
|
|
35
|
+
violations.push(`Naming quality violation [${v.kind}]: ${v.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Duplicate Selectors Check
|
|
41
|
+
const duplicateSelectors: string[] = [];
|
|
42
|
+
if (input.files) {
|
|
43
|
+
for (const file of input.files) {
|
|
44
|
+
if (file.relativePath.endsWith('Page.ts')) {
|
|
45
|
+
const locatorMatches = [...file.content.matchAll(/this\.page\.(?:getByRole|getByText|getByTestId|locator)\s*\(([^)]+)\)/g)];
|
|
46
|
+
const seen = new Set<string>();
|
|
47
|
+
for (const match of locatorMatches) {
|
|
48
|
+
const locStr = match[1]!.trim();
|
|
49
|
+
if (seen.has(locStr)) {
|
|
50
|
+
duplicateSelectors.push(locStr);
|
|
51
|
+
violations.push(`Duplicate selector in '${file.relativePath}': ${locStr}`);
|
|
52
|
+
}
|
|
53
|
+
seen.add(locStr);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. Locator Policy Conformance (e.g. Reject raw CSS when ARIA is required)
|
|
60
|
+
const locatorPolicyViolations: string[] = [];
|
|
61
|
+
if (input.files && input.locatorPolicy === 'prefer-role-over-css') {
|
|
62
|
+
for (const file of input.files) {
|
|
63
|
+
const rawCssMatches = [...file.content.matchAll(/this\.page\.locator\s*\(\s*['"]([#.]|div|button|input)[^'"]*['"]\s*\)/g)];
|
|
64
|
+
for (const match of rawCssMatches) {
|
|
65
|
+
locatorPolicyViolations.push(match[0]);
|
|
66
|
+
violations.push(`Locator policy violation in '${file.relativePath}': raw CSS used instead of role-based locator: ${match[0]}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 4. Assertion Strength Non-Downgrade Check
|
|
72
|
+
let assertionStrengthPassed = true;
|
|
73
|
+
if (input.originalSource && input.patchedSource) {
|
|
74
|
+
const origHasExactText = /\b(toHaveText|toHaveValue|toContainText)\b/.test(input.originalSource);
|
|
75
|
+
const patchHasExactText = /\b(toHaveText|toHaveValue|toContainText)\b/.test(input.patchedSource);
|
|
76
|
+
const patchDowngradedToVisible = /\btoBeVisible\b/.test(input.patchedSource) && !/\btoBeVisible\b/.test(input.originalSource);
|
|
77
|
+
|
|
78
|
+
if (origHasExactText && !patchHasExactText && patchDowngradedToVisible) {
|
|
79
|
+
assertionStrengthPassed = false;
|
|
80
|
+
violations.push('Assertion strength regression: exact text assertion was downgraded to toBeVisible');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 5. Diff Minimality (Count changed lines)
|
|
85
|
+
let changedLines = 0;
|
|
86
|
+
if (input.diff) {
|
|
87
|
+
const diffLines = input.diff.split(/\r?\n/);
|
|
88
|
+
changedLines = diffLines.filter((l) => (l.startsWith('+') || l.startsWith('-')) && !l.startsWith('+++') && !l.startsWith('---')).length;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
passed: violations.length === 0,
|
|
93
|
+
namingReport,
|
|
94
|
+
duplicateSelectors,
|
|
95
|
+
locatorPolicyViolations,
|
|
96
|
+
assertionStrengthPassed,
|
|
97
|
+
changedLines,
|
|
98
|
+
violations,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|