playwright-intelligence-engine 2.0.1 → 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,175 +1,175 @@
|
|
|
1
|
-
import { ScoreBreakdown } from './selector-scorer.js';
|
|
2
|
-
import { ResolvedElement } from '../trace/snapshot-resolver.js';
|
|
3
|
-
|
|
4
|
-
export interface ScoredCandidate {
|
|
5
|
-
element: ResolvedElement;
|
|
6
|
-
breakdown: ScoreBreakdown;
|
|
7
|
-
locatorExpression?: string | undefined;
|
|
8
|
-
originalSelector?: string | undefined;
|
|
9
|
-
intendedAction?: string | undefined; // e.g. "click", "fill"
|
|
10
|
-
parentScopeVerified?: boolean | undefined;
|
|
11
|
-
disambiguationStrategy?: string | undefined;
|
|
12
|
-
disambiguationExplanation?: string | undefined;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type DecisionStage =
|
|
16
|
-
| 'SUPPORTED_ELEMENT'
|
|
17
|
-
| 'TARGET_IDENTITY'
|
|
18
|
-
| 'STRICT_UNIQUENESS'
|
|
19
|
-
| 'ACTION_SEMANTICS'
|
|
20
|
-
| 'SCORE_CONFIDENCE'
|
|
21
|
-
| 'AMBIGUITY_DELTA'
|
|
22
|
-
| 'AUTHORIZED';
|
|
23
|
-
|
|
24
|
-
export interface AbstentionDecision {
|
|
25
|
-
shouldAbstain: boolean;
|
|
26
|
-
stage: DecisionStage;
|
|
27
|
-
selectedCandidate?: ScoredCandidate | undefined;
|
|
28
|
-
reason?: string | undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export class AbstentionPolicy {
|
|
32
|
-
public static readonly MINIMUM_CONFIDENCE_THRESHOLD = 0.70;
|
|
33
|
-
public static readonly AMBIGUITY_DELTA_THRESHOLD = 0.08;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Applies the formal Multi-Stage Decision Pipeline to candidate locators:
|
|
37
|
-
* 1. Supported Element? (Must be recognized interactive element)
|
|
38
|
-
* 2. Target Identity Verified? (Affinity to intended business entity)
|
|
39
|
-
* 3. Exactly One Scoped Match? (Strict Uniqueness: matchCount === 1, unless parentScopeVerified)
|
|
40
|
-
* 4. Action Semantics Preserved? (e.g. click -> clickable element, fill -> input/textarea)
|
|
41
|
-
* 5. Score & Calibrated Threshold Pass? (rawScore >= 0.70)
|
|
42
|
-
* 6. Ambiguity Delta Pass? (top.score - runnerUp.score >= 0.08)
|
|
43
|
-
*/
|
|
44
|
-
public static evaluate(candidates: ScoredCandidate[]): AbstentionDecision {
|
|
45
|
-
if (candidates.length === 0) {
|
|
46
|
-
return {
|
|
47
|
-
shouldAbstain: true,
|
|
48
|
-
stage: 'SUPPORTED_ELEMENT',
|
|
49
|
-
reason: 'Zero matching candidate elements found in DOM snapshot',
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Sort descending by score
|
|
54
|
-
const sorted = [...candidates].sort((a, b) => b.breakdown.score - a.breakdown.score);
|
|
55
|
-
const top = sorted[0]!;
|
|
56
|
-
|
|
57
|
-
// Stage 1: Supported Element Check
|
|
58
|
-
const supportedTags = new Set([
|
|
59
|
-
'button',
|
|
60
|
-
'a',
|
|
61
|
-
'input',
|
|
62
|
-
'select',
|
|
63
|
-
'textarea',
|
|
64
|
-
'option',
|
|
65
|
-
'div',
|
|
66
|
-
'span',
|
|
67
|
-
'li',
|
|
68
|
-
'td',
|
|
69
|
-
'th',
|
|
70
|
-
'p',
|
|
71
|
-
'h1',
|
|
72
|
-
'h2',
|
|
73
|
-
'h3',
|
|
74
|
-
'h4',
|
|
75
|
-
'label',
|
|
76
|
-
'img',
|
|
77
|
-
'form',
|
|
78
|
-
]);
|
|
79
|
-
if (!supportedTags.has(top.element.tag) && !top.element.role) {
|
|
80
|
-
return {
|
|
81
|
-
shouldAbstain: true,
|
|
82
|
-
stage: 'SUPPORTED_ELEMENT',
|
|
83
|
-
reason: `Unsupported DOM tag '<${top.element.tag}>' without ARIA role cannot be safely automated.`,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Stage 2: Target Identity Verification
|
|
88
|
-
if (top.breakdown.identityMatch < 0.4) {
|
|
89
|
-
return {
|
|
90
|
-
shouldAbstain: true,
|
|
91
|
-
stage: 'TARGET_IDENTITY',
|
|
92
|
-
reason: `Target identity mismatch: candidate accessible name '${top.element.accessibleName ?? 'unnamed'}' does not align with original selector intent.`,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Stage 3: Strict Uniqueness Gate (CRITICAL: matchCount !== 1 triggers ABSTAIN)
|
|
97
|
-
// Exactly 2 or more matches fail the uniqueness requirement unless parentScopeVerified is true
|
|
98
|
-
if (top.breakdown.matchCount !== 1 && !top.parentScopeVerified) {
|
|
99
|
-
return {
|
|
100
|
-
shouldAbstain: true,
|
|
101
|
-
stage: 'STRICT_UNIQUENESS',
|
|
102
|
-
reason: `NON_UNIQUE_CANDIDATE: Candidate element matches ${top.breakdown.matchCount} elements in DOM. Strict uniqueness requires exactly 1 match (U = 1.0) unless scoped by a verified parent container.`,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Stage 4: Action Semantics Check
|
|
107
|
-
if (top.intendedAction) {
|
|
108
|
-
const action = top.intendedAction.toLowerCase();
|
|
109
|
-
if (action.includes('click')) {
|
|
110
|
-
const isClickable =
|
|
111
|
-
top.element.tag === 'button' ||
|
|
112
|
-
top.element.tag === 'a' ||
|
|
113
|
-
top.element.role === 'button' ||
|
|
114
|
-
top.element.role === 'link' ||
|
|
115
|
-
top.element.role === 'tab' ||
|
|
116
|
-
top.element.role === 'menuitem' ||
|
|
117
|
-
Boolean(top.element.attributes?.['onclick']) ||
|
|
118
|
-
Boolean(top.element.attributes?.['href']);
|
|
119
|
-
|
|
120
|
-
if (!isClickable && top.element.tag !== 'div' && top.element.tag !== 'span' && top.element.tag !== 'td') {
|
|
121
|
-
return {
|
|
122
|
-
shouldAbstain: true,
|
|
123
|
-
stage: 'ACTION_SEMANTICS',
|
|
124
|
-
reason: `Action semantics violation: action '${top.intendedAction}' cannot be safely dispatched to non-clickable element <${top.element.tag}>.`,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
} else if (action.includes('fill') || action.includes('type')) {
|
|
128
|
-
const isInput =
|
|
129
|
-
top.element.tag === 'input' ||
|
|
130
|
-
top.element.tag === 'textarea' ||
|
|
131
|
-
top.element.role === 'textbox' ||
|
|
132
|
-
top.element.attributes['contenteditable'] === 'true';
|
|
133
|
-
|
|
134
|
-
if (!isInput) {
|
|
135
|
-
return {
|
|
136
|
-
shouldAbstain: true,
|
|
137
|
-
stage: 'ACTION_SEMANTICS',
|
|
138
|
-
reason: `Action semantics violation: fill/type action requires input or textbox element, but found <${top.element.tag}>.`,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Stage 5: Confidence Threshold Gate
|
|
145
|
-
if (top.breakdown.score < AbstentionPolicy.MINIMUM_CONFIDENCE_THRESHOLD) {
|
|
146
|
-
return {
|
|
147
|
-
shouldAbstain: true,
|
|
148
|
-
stage: 'SCORE_CONFIDENCE',
|
|
149
|
-
reason: `Highest candidate confidence (${(top.breakdown.score * 100).toFixed(1)}%) is below minimum safety threshold (${(AbstentionPolicy.MINIMUM_CONFIDENCE_THRESHOLD * 100).toFixed(1)}%)`,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Stage 6: Ambiguity Delta Gate (if top two candidates are neck and neck, guess is unsafe)
|
|
154
|
-
if (sorted.length > 1) {
|
|
155
|
-
const runnerUp = sorted[1]!;
|
|
156
|
-
const delta = parseFloat((top.breakdown.score - runnerUp.breakdown.score).toFixed(3));
|
|
157
|
-
const identitySeparation = top.breakdown.identityMatch - runnerUp.breakdown.identityMatch;
|
|
158
|
-
const hasStrongIdentitySeparation = top.breakdown.identityMatch >= 0.9 && identitySeparation >= 0.12;
|
|
159
|
-
if (delta < AbstentionPolicy.AMBIGUITY_DELTA_THRESHOLD && !hasStrongIdentitySeparation) {
|
|
160
|
-
return {
|
|
161
|
-
shouldAbstain: true,
|
|
162
|
-
stage: 'AMBIGUITY_DELTA',
|
|
163
|
-
reason: `Ambiguous match: top two candidate elements have nearly identical scores (${top.breakdown.score} vs ${runnerUp.breakdown.score}, delta: ${delta.toFixed(3)} < threshold ${AbstentionPolicy.AMBIGUITY_DELTA_THRESHOLD}). Abstaining to prevent incorrect action dispatch.`,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
shouldAbstain: false,
|
|
170
|
-
stage: 'AUTHORIZED',
|
|
171
|
-
selectedCandidate: top,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
1
|
+
import { ScoreBreakdown } from './selector-scorer.js';
|
|
2
|
+
import { ResolvedElement } from '../trace/snapshot-resolver.js';
|
|
3
|
+
|
|
4
|
+
export interface ScoredCandidate {
|
|
5
|
+
element: ResolvedElement;
|
|
6
|
+
breakdown: ScoreBreakdown;
|
|
7
|
+
locatorExpression?: string | undefined;
|
|
8
|
+
originalSelector?: string | undefined;
|
|
9
|
+
intendedAction?: string | undefined; // e.g. "click", "fill"
|
|
10
|
+
parentScopeVerified?: boolean | undefined;
|
|
11
|
+
disambiguationStrategy?: string | undefined;
|
|
12
|
+
disambiguationExplanation?: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type DecisionStage =
|
|
16
|
+
| 'SUPPORTED_ELEMENT'
|
|
17
|
+
| 'TARGET_IDENTITY'
|
|
18
|
+
| 'STRICT_UNIQUENESS'
|
|
19
|
+
| 'ACTION_SEMANTICS'
|
|
20
|
+
| 'SCORE_CONFIDENCE'
|
|
21
|
+
| 'AMBIGUITY_DELTA'
|
|
22
|
+
| 'AUTHORIZED';
|
|
23
|
+
|
|
24
|
+
export interface AbstentionDecision {
|
|
25
|
+
shouldAbstain: boolean;
|
|
26
|
+
stage: DecisionStage;
|
|
27
|
+
selectedCandidate?: ScoredCandidate | undefined;
|
|
28
|
+
reason?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class AbstentionPolicy {
|
|
32
|
+
public static readonly MINIMUM_CONFIDENCE_THRESHOLD = 0.70;
|
|
33
|
+
public static readonly AMBIGUITY_DELTA_THRESHOLD = 0.08;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Applies the formal Multi-Stage Decision Pipeline to candidate locators:
|
|
37
|
+
* 1. Supported Element? (Must be recognized interactive element)
|
|
38
|
+
* 2. Target Identity Verified? (Affinity to intended business entity)
|
|
39
|
+
* 3. Exactly One Scoped Match? (Strict Uniqueness: matchCount === 1, unless parentScopeVerified)
|
|
40
|
+
* 4. Action Semantics Preserved? (e.g. click -> clickable element, fill -> input/textarea)
|
|
41
|
+
* 5. Score & Calibrated Threshold Pass? (rawScore >= 0.70)
|
|
42
|
+
* 6. Ambiguity Delta Pass? (top.score - runnerUp.score >= 0.08)
|
|
43
|
+
*/
|
|
44
|
+
public static evaluate(candidates: ScoredCandidate[]): AbstentionDecision {
|
|
45
|
+
if (candidates.length === 0) {
|
|
46
|
+
return {
|
|
47
|
+
shouldAbstain: true,
|
|
48
|
+
stage: 'SUPPORTED_ELEMENT',
|
|
49
|
+
reason: 'Zero matching candidate elements found in DOM snapshot',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Sort descending by score
|
|
54
|
+
const sorted = [...candidates].sort((a, b) => b.breakdown.score - a.breakdown.score);
|
|
55
|
+
const top = sorted[0]!;
|
|
56
|
+
|
|
57
|
+
// Stage 1: Supported Element Check
|
|
58
|
+
const supportedTags = new Set([
|
|
59
|
+
'button',
|
|
60
|
+
'a',
|
|
61
|
+
'input',
|
|
62
|
+
'select',
|
|
63
|
+
'textarea',
|
|
64
|
+
'option',
|
|
65
|
+
'div',
|
|
66
|
+
'span',
|
|
67
|
+
'li',
|
|
68
|
+
'td',
|
|
69
|
+
'th',
|
|
70
|
+
'p',
|
|
71
|
+
'h1',
|
|
72
|
+
'h2',
|
|
73
|
+
'h3',
|
|
74
|
+
'h4',
|
|
75
|
+
'label',
|
|
76
|
+
'img',
|
|
77
|
+
'form',
|
|
78
|
+
]);
|
|
79
|
+
if (!supportedTags.has(top.element.tag) && !top.element.role) {
|
|
80
|
+
return {
|
|
81
|
+
shouldAbstain: true,
|
|
82
|
+
stage: 'SUPPORTED_ELEMENT',
|
|
83
|
+
reason: `Unsupported DOM tag '<${top.element.tag}>' without ARIA role cannot be safely automated.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Stage 2: Target Identity Verification
|
|
88
|
+
if (top.breakdown.identityMatch < 0.4) {
|
|
89
|
+
return {
|
|
90
|
+
shouldAbstain: true,
|
|
91
|
+
stage: 'TARGET_IDENTITY',
|
|
92
|
+
reason: `Target identity mismatch: candidate accessible name '${top.element.accessibleName ?? 'unnamed'}' does not align with original selector intent.`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Stage 3: Strict Uniqueness Gate (CRITICAL: matchCount !== 1 triggers ABSTAIN)
|
|
97
|
+
// Exactly 2 or more matches fail the uniqueness requirement unless parentScopeVerified is true
|
|
98
|
+
if (top.breakdown.matchCount !== 1 && !top.parentScopeVerified) {
|
|
99
|
+
return {
|
|
100
|
+
shouldAbstain: true,
|
|
101
|
+
stage: 'STRICT_UNIQUENESS',
|
|
102
|
+
reason: `NON_UNIQUE_CANDIDATE: Candidate element matches ${top.breakdown.matchCount} elements in DOM. Strict uniqueness requires exactly 1 match (U = 1.0) unless scoped by a verified parent container.`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Stage 4: Action Semantics Check
|
|
107
|
+
if (top.intendedAction) {
|
|
108
|
+
const action = top.intendedAction.toLowerCase();
|
|
109
|
+
if (action.includes('click')) {
|
|
110
|
+
const isClickable =
|
|
111
|
+
top.element.tag === 'button' ||
|
|
112
|
+
top.element.tag === 'a' ||
|
|
113
|
+
top.element.role === 'button' ||
|
|
114
|
+
top.element.role === 'link' ||
|
|
115
|
+
top.element.role === 'tab' ||
|
|
116
|
+
top.element.role === 'menuitem' ||
|
|
117
|
+
Boolean(top.element.attributes?.['onclick']) ||
|
|
118
|
+
Boolean(top.element.attributes?.['href']);
|
|
119
|
+
|
|
120
|
+
if (!isClickable && top.element.tag !== 'div' && top.element.tag !== 'span' && top.element.tag !== 'td') {
|
|
121
|
+
return {
|
|
122
|
+
shouldAbstain: true,
|
|
123
|
+
stage: 'ACTION_SEMANTICS',
|
|
124
|
+
reason: `Action semantics violation: action '${top.intendedAction}' cannot be safely dispatched to non-clickable element <${top.element.tag}>.`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
} else if (action.includes('fill') || action.includes('type')) {
|
|
128
|
+
const isInput =
|
|
129
|
+
top.element.tag === 'input' ||
|
|
130
|
+
top.element.tag === 'textarea' ||
|
|
131
|
+
top.element.role === 'textbox' ||
|
|
132
|
+
top.element.attributes['contenteditable'] === 'true';
|
|
133
|
+
|
|
134
|
+
if (!isInput) {
|
|
135
|
+
return {
|
|
136
|
+
shouldAbstain: true,
|
|
137
|
+
stage: 'ACTION_SEMANTICS',
|
|
138
|
+
reason: `Action semantics violation: fill/type action requires input or textbox element, but found <${top.element.tag}>.`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Stage 5: Confidence Threshold Gate
|
|
145
|
+
if (top.breakdown.score < AbstentionPolicy.MINIMUM_CONFIDENCE_THRESHOLD) {
|
|
146
|
+
return {
|
|
147
|
+
shouldAbstain: true,
|
|
148
|
+
stage: 'SCORE_CONFIDENCE',
|
|
149
|
+
reason: `Highest candidate confidence (${(top.breakdown.score * 100).toFixed(1)}%) is below minimum safety threshold (${(AbstentionPolicy.MINIMUM_CONFIDENCE_THRESHOLD * 100).toFixed(1)}%)`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Stage 6: Ambiguity Delta Gate (if top two candidates are neck and neck, guess is unsafe)
|
|
154
|
+
if (sorted.length > 1) {
|
|
155
|
+
const runnerUp = sorted[1]!;
|
|
156
|
+
const delta = parseFloat((top.breakdown.score - runnerUp.breakdown.score).toFixed(3));
|
|
157
|
+
const identitySeparation = top.breakdown.identityMatch - runnerUp.breakdown.identityMatch;
|
|
158
|
+
const hasStrongIdentitySeparation = top.breakdown.identityMatch >= 0.9 && identitySeparation >= 0.12;
|
|
159
|
+
if (delta < AbstentionPolicy.AMBIGUITY_DELTA_THRESHOLD && !hasStrongIdentitySeparation) {
|
|
160
|
+
return {
|
|
161
|
+
shouldAbstain: true,
|
|
162
|
+
stage: 'AMBIGUITY_DELTA',
|
|
163
|
+
reason: `Ambiguous match: top two candidate elements have nearly identical scores (${top.breakdown.score} vs ${runnerUp.breakdown.score}, delta: ${delta.toFixed(3)} < threshold ${AbstentionPolicy.AMBIGUITY_DELTA_THRESHOLD}). Abstaining to prevent incorrect action dispatch.`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
shouldAbstain: false,
|
|
170
|
+
stage: 'AUTHORIZED',
|
|
171
|
+
selectedCandidate: top,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|