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,467 +1,467 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { AstPatcher } from './ast-patcher.js';
|
|
4
|
-
import { StatementPatcher, StatementPatchPlan } from './statement-patcher.js';
|
|
5
|
-
import { AssertionHasher, CompositeIntentFingerprint } from './assertion-hasher.js';
|
|
6
|
-
import { IntentValidator, IntentV2ValidationResult, VerifiedExecutionProof } from './intent-validator.js';
|
|
7
|
-
import { DiffAllowlistValidator } from './diff-allowlist.js';
|
|
8
|
-
import { ApprovalService } from './approval-service.js';
|
|
9
|
-
import { RepairEvidence } from '../core/types/repair-evidence.js';
|
|
10
|
-
import { ResolvedElement } from '../trace/snapshot-resolver.js';
|
|
11
|
-
import { CandidateExtractor } from '../selector/candidate-extractor.js';
|
|
12
|
-
import { ExecutionRunner, ExecutionResult } from '../sandbox/execution-runner.js';
|
|
13
|
-
import { PROOF_VERSION } from '../core/version.js';
|
|
14
|
-
import {
|
|
15
|
-
BusinessIntent,
|
|
16
|
-
BusinessOracleResult,
|
|
17
|
-
evaluateBuiltInBusinessOracles,
|
|
18
|
-
extractBusinessIntent,
|
|
19
|
-
} from '../intelligence/business-intent.js';
|
|
20
|
-
|
|
21
|
-
export type RepairLifecycleState =
|
|
22
|
-
| 'ANALYZED'
|
|
23
|
-
| 'PROPOSED'
|
|
24
|
-
| 'PATCHED_IN_ISOLATION'
|
|
25
|
-
| 'INTENT_VALIDATED'
|
|
26
|
-
| 'TARGET_TEST_PASSED'
|
|
27
|
-
| 'AFFECTED_TESTS_PASSED'
|
|
28
|
-
| 'FULL_SUITE_PASSED'
|
|
29
|
-
| 'READY_FOR_APPROVAL'
|
|
30
|
-
| 'REJECTED';
|
|
31
|
-
|
|
32
|
-
export interface RepairProposal {
|
|
33
|
-
state: RepairLifecycleState;
|
|
34
|
-
originalActionId: string;
|
|
35
|
-
originalSelector: string;
|
|
36
|
-
replacementSelector: string;
|
|
37
|
-
candidateElement: ResolvedElement;
|
|
38
|
-
candidateScore: number;
|
|
39
|
-
testFilePath: string;
|
|
40
|
-
pomFilePath: string;
|
|
41
|
-
className: string;
|
|
42
|
-
memberName: string;
|
|
43
|
-
intendedAction: string;
|
|
44
|
-
originalCompositeFingerprint: CompositeIntentFingerprint;
|
|
45
|
-
proposedPatchedSource: string;
|
|
46
|
-
originalSource: string;
|
|
47
|
-
pattern: string;
|
|
48
|
-
fullDiff?: string | undefined;
|
|
49
|
-
diffAllowlistPassed: boolean;
|
|
50
|
-
businessIntent?: BusinessIntent | undefined;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface RepairVerificationInput {
|
|
54
|
-
proposal: RepairProposal;
|
|
55
|
-
worktreePath: string;
|
|
56
|
-
worktreeId: string;
|
|
57
|
-
workspaceRoot?: string | undefined;
|
|
58
|
-
configPath?: string | undefined;
|
|
59
|
-
project?: string | undefined;
|
|
60
|
-
businessOracleStatus?: 'verified' | 'contradicted' | 'unavailable' | undefined;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface VerifiedRepairResult {
|
|
64
|
-
state: RepairLifecycleState;
|
|
65
|
-
evidence: RepairEvidence;
|
|
66
|
-
executionResult: ExecutionResult;
|
|
67
|
-
intentValidation: IntentV2ValidationResult;
|
|
68
|
-
executionProof: VerifiedExecutionProof;
|
|
69
|
-
isReadyForApproval: boolean;
|
|
70
|
-
businessOracleResults?: BusinessOracleResult[] | undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface RepairPlanInput {
|
|
74
|
-
originalActionId: string;
|
|
75
|
-
originalSelector: string;
|
|
76
|
-
candidateElement: ResolvedElement;
|
|
77
|
-
candidateScore: number;
|
|
78
|
-
testFilePath: string;
|
|
79
|
-
pomFilePath: string;
|
|
80
|
-
className: string;
|
|
81
|
-
memberName: string;
|
|
82
|
-
intendedAction: string;
|
|
83
|
-
replacementSelector?: string | undefined;
|
|
84
|
-
worktreeId?: string | undefined;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface StatementRepairPlanInput {
|
|
88
|
-
originalActionId: string;
|
|
89
|
-
originalSelector?: string | undefined;
|
|
90
|
-
testFilePath: string;
|
|
91
|
-
plan: StatementPatchPlan;
|
|
92
|
-
candidateElement?: ResolvedElement | undefined;
|
|
93
|
-
candidateScore?: number | undefined;
|
|
94
|
-
intendedAction?: string | undefined;
|
|
95
|
-
worktreeId?: string | undefined;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export class RepairPlanner {
|
|
99
|
-
/**
|
|
100
|
-
* Generates a modern, user-centric Playwright locator expression prioritizing getByRole and getByTestId.
|
|
101
|
-
* Employs robust string escaping; avoids bare tag fallbacks.
|
|
102
|
-
*/
|
|
103
|
-
public static generateSelectorExpression(el: ResolvedElement, pageProperty = 'this.page'): string {
|
|
104
|
-
return CandidateExtractor.generateLocator(el, pageProperty);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Generates a verified in-memory statement patch proposal using StatementPatcher.
|
|
109
|
-
* Enables healing of inline test statements, timing synchronization, and sleep elimination.
|
|
110
|
-
*/
|
|
111
|
-
public static planStatementRepair(input: StatementRepairPlanInput): RepairProposal {
|
|
112
|
-
const origSource = fs.readFileSync(input.testFilePath, 'utf-8');
|
|
113
|
-
const originalCompositeFingerprint = AssertionHasher.computeCompositeFingerprint(input.testFilePath);
|
|
114
|
-
|
|
115
|
-
const patcher = new StatementPatcher();
|
|
116
|
-
const patchResult = patcher.patchStatementInMemory(origSource, input.plan, path.basename(input.testFilePath));
|
|
117
|
-
|
|
118
|
-
// Verify all original assertions are strictly preserved in post-patch
|
|
119
|
-
const origAssertions = AssertionHasher.extractAssertionRecordsFromSource(origSource);
|
|
120
|
-
const postAssertions = AssertionHasher.extractAssertionRecordsFromSource(patchResult.patchedSource);
|
|
121
|
-
const postSet = new Set(postAssertions.map((a) => JSON.stringify(a)));
|
|
122
|
-
const allOriginalAssertionsPreserved = origAssertions.every((a) => postSet.has(JSON.stringify(a)));
|
|
123
|
-
if (!allOriginalAssertionsPreserved) {
|
|
124
|
-
throw new Error('AST_ASSERTION_TAMPERING_PROHIBITED: Statement patch altered or removed test assertions.');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const dummyCandidate: ResolvedElement = input.candidateElement ?? {
|
|
128
|
-
tag: 'statement',
|
|
129
|
-
attributes: {},
|
|
130
|
-
text: patchResult.replacementStatement,
|
|
131
|
-
children: [],
|
|
132
|
-
role: 'statement',
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
state: 'PROPOSED',
|
|
137
|
-
originalActionId: input.originalActionId,
|
|
138
|
-
originalSelector: input.originalSelector ?? patchResult.originalStatement,
|
|
139
|
-
replacementSelector: patchResult.replacementStatement,
|
|
140
|
-
candidateElement: dummyCandidate,
|
|
141
|
-
candidateScore: input.candidateScore ?? 1.0,
|
|
142
|
-
testFilePath: input.testFilePath,
|
|
143
|
-
pomFilePath: input.testFilePath,
|
|
144
|
-
className: 'InlineSpec',
|
|
145
|
-
memberName: input.plan.kind,
|
|
146
|
-
intendedAction: input.intendedAction ?? input.plan.kind,
|
|
147
|
-
originalCompositeFingerprint,
|
|
148
|
-
proposedPatchedSource: patchResult.patchedSource,
|
|
149
|
-
originalSource: origSource,
|
|
150
|
-
pattern: 'statement-patch',
|
|
151
|
-
fullDiff: patchResult.diff,
|
|
152
|
-
diffAllowlistPassed: true,
|
|
153
|
-
businessIntent: extractBusinessIntent({
|
|
154
|
-
id: input.originalActionId,
|
|
155
|
-
sourceText: origSource,
|
|
156
|
-
sourcePath: input.testFilePath,
|
|
157
|
-
}),
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Stage 1 & 2: Analyzes candidate and generates an in-memory repair proposal.
|
|
163
|
-
* Does NOT modify source files on disk. Does NOT claim repair success.
|
|
164
|
-
*/
|
|
165
|
-
public static planRepair(input: RepairPlanInput): RepairProposal {
|
|
166
|
-
// 0. Read original source and discover page property identifier
|
|
167
|
-
const origSource = fs.readFileSync(input.pomFilePath, 'utf-8');
|
|
168
|
-
const pageProperty = CandidateExtractor.discoverPageProperty(origSource);
|
|
169
|
-
|
|
170
|
-
// 1. Synthesize replacement locator expression with escaping
|
|
171
|
-
const replacementLocator = input.replacementSelector ?? RepairPlanner.generateSelectorExpression(input.candidateElement, pageProperty);
|
|
172
|
-
|
|
173
|
-
// 2. Validate replacement against Playwright locator grammar
|
|
174
|
-
const grammarCheck = DiffAllowlistValidator.validateLocatorGrammar(replacementLocator);
|
|
175
|
-
if (!grammarCheck.isValid) {
|
|
176
|
-
throw new Error(`Invalid candidate locator generated: ${grammarCheck.reason}`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 3. Compute baseline composite fingerprint of the test spec
|
|
180
|
-
const originalCompositeFingerprint = AssertionHasher.computeCompositeFingerprint(input.testFilePath);
|
|
181
|
-
|
|
182
|
-
// 4. Generate in-memory AST patch without writing to disk
|
|
183
|
-
const patcher = new AstPatcher();
|
|
184
|
-
const inMemPatch = patcher.patchPageObjectInMemory(origSource, {
|
|
185
|
-
className: input.className,
|
|
186
|
-
memberName: input.memberName,
|
|
187
|
-
replacementExpression: replacementLocator,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// 5. Validate diff allowlist in-memory
|
|
191
|
-
const diffResult = DiffAllowlistValidator.validate({
|
|
192
|
-
originalSource: origSource,
|
|
193
|
-
patchedSource: inMemPatch.patchedSource,
|
|
194
|
-
targetClassName: input.className,
|
|
195
|
-
targetMemberName: input.memberName,
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
if (!diffResult.isAllowed) {
|
|
199
|
-
throw new Error(`Diff allowlist rejected proposed patch: ${diffResult.reason}`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
state: 'PROPOSED',
|
|
204
|
-
originalActionId: input.originalActionId,
|
|
205
|
-
originalSelector: input.originalSelector,
|
|
206
|
-
replacementSelector: replacementLocator,
|
|
207
|
-
candidateElement: input.candidateElement,
|
|
208
|
-
candidateScore: input.candidateScore,
|
|
209
|
-
testFilePath: input.testFilePath,
|
|
210
|
-
pomFilePath: input.pomFilePath,
|
|
211
|
-
className: input.className,
|
|
212
|
-
memberName: input.memberName,
|
|
213
|
-
intendedAction: input.intendedAction,
|
|
214
|
-
originalCompositeFingerprint,
|
|
215
|
-
proposedPatchedSource: inMemPatch.patchedSource,
|
|
216
|
-
originalSource: origSource,
|
|
217
|
-
pattern: inMemPatch.pattern,
|
|
218
|
-
fullDiff: inMemPatch.diff,
|
|
219
|
-
diffAllowlistPassed: true,
|
|
220
|
-
businessIntent: extractBusinessIntent({
|
|
221
|
-
id: input.originalActionId,
|
|
222
|
-
sourceText: fs.readFileSync(input.testFilePath, 'utf8'),
|
|
223
|
-
sourcePath: input.testFilePath,
|
|
224
|
-
}),
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Stages 3-8: Applies patch strictly inside the isolated Git worktree, runs test execution,
|
|
230
|
-
* validates Intent Invariant v2 with actual execution proof, and derives verified evidence.
|
|
231
|
-
*/
|
|
232
|
-
public static async executeAndVerifyInWorktree(
|
|
233
|
-
input: RepairVerificationInput
|
|
234
|
-
): Promise<VerifiedRepairResult> {
|
|
235
|
-
const proposal = input.proposal;
|
|
236
|
-
const rootDir = input.workspaceRoot ?? process.cwd();
|
|
237
|
-
|
|
238
|
-
// Canonical containment checks
|
|
239
|
-
const canonicalWorktree = fs.existsSync(input.worktreePath)
|
|
240
|
-
? fs.realpathSync(input.worktreePath)
|
|
241
|
-
: path.resolve(input.worktreePath);
|
|
242
|
-
|
|
243
|
-
const relPom = path.isAbsolute(proposal.pomFilePath)
|
|
244
|
-
? path.relative(rootDir, proposal.pomFilePath)
|
|
245
|
-
: proposal.pomFilePath;
|
|
246
|
-
const relSpec = path.isAbsolute(proposal.testFilePath)
|
|
247
|
-
? path.relative(rootDir, proposal.testFilePath)
|
|
248
|
-
: proposal.testFilePath;
|
|
249
|
-
|
|
250
|
-
const worktreePomPath = path.resolve(canonicalWorktree, relPom);
|
|
251
|
-
const worktreeSpecPath = path.resolve(canonicalWorktree, relSpec);
|
|
252
|
-
|
|
253
|
-
const relPomCheck = path.relative(canonicalWorktree, worktreePomPath);
|
|
254
|
-
if (relPomCheck.startsWith('..') || path.isAbsolute(relPomCheck)) {
|
|
255
|
-
throw new Error(`Security Violation: Worktree POM path '${worktreePomPath}' escapes worktree boundary '${canonicalWorktree}'.`);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const relSpecCheck = path.relative(canonicalWorktree, worktreeSpecPath);
|
|
259
|
-
if (relSpecCheck.startsWith('..') || path.isAbsolute(relSpecCheck)) {
|
|
260
|
-
throw new Error(`Security Violation: Worktree Spec path '${worktreeSpecPath}' escapes worktree boundary '${canonicalWorktree}'.`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Stage 3: Apply patch ONLY in isolated worktree copy
|
|
264
|
-
const targetFilePath = proposal.pattern === 'statement-patch' ? worktreeSpecPath : worktreePomPath;
|
|
265
|
-
await fs.promises.mkdir(path.dirname(targetFilePath), { recursive: true });
|
|
266
|
-
await fs.promises.writeFile(targetFilePath, proposal.proposedPatchedSource, 'utf-8');
|
|
267
|
-
let currentState: RepairLifecycleState = 'PATCHED_IN_ISOLATION';
|
|
268
|
-
|
|
269
|
-
// Stage 4: Execute target test inside worktree (normalize to forward slashes for Playwright regex matcher)
|
|
270
|
-
const normalizedRelSpec = relSpec.replace(/\\/g, '/');
|
|
271
|
-
const execArgs = ['playwright', 'test', normalizedRelSpec];
|
|
272
|
-
if (input.configPath) {
|
|
273
|
-
const relConfig = path.isAbsolute(input.configPath)
|
|
274
|
-
? path.relative(rootDir, input.configPath)
|
|
275
|
-
: input.configPath;
|
|
276
|
-
execArgs.push('--config', relConfig.replace(/\\/g, '/'));
|
|
277
|
-
}
|
|
278
|
-
if (input.project) {
|
|
279
|
-
execArgs.push('--project', input.project);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const execResult = await ExecutionRunner.run('npx', {
|
|
283
|
-
cwd: canonicalWorktree,
|
|
284
|
-
args: execArgs,
|
|
285
|
-
timeoutMs: 60000,
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// Verify test was actually discovered and passed
|
|
289
|
-
const passedMatch = execResult.stdout.match(/(\d+)\s+passed/i);
|
|
290
|
-
const passedCount = passedMatch ? parseInt(passedMatch[1] ?? '0', 10) : 0;
|
|
291
|
-
const executionPassed = execResult.exitCode === 0 && passedCount > 0 && !execResult.timedOut;
|
|
292
|
-
|
|
293
|
-
if (executionPassed) {
|
|
294
|
-
currentState = 'TARGET_TEST_PASSED';
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Verify assertion integrity dynamically by comparing pre-patch and post-patch assertions
|
|
298
|
-
const worktreeSpecSource = fs.existsSync(worktreeSpecPath)
|
|
299
|
-
? fs.readFileSync(worktreeSpecPath, 'utf-8')
|
|
300
|
-
: '';
|
|
301
|
-
const postExecCompositeFingerprint = AssertionHasher.computeCompositeFingerprintFromSource(worktreeSpecSource);
|
|
302
|
-
|
|
303
|
-
let assertionIntegrityValid = false;
|
|
304
|
-
if (proposal.pattern === 'statement-patch') {
|
|
305
|
-
const origAssertions = AssertionHasher.extractAssertionRecordsFromSource(proposal.originalSource);
|
|
306
|
-
const postAssertions = AssertionHasher.extractAssertionRecordsFromSource(worktreeSpecSource);
|
|
307
|
-
const postSet = new Set(postAssertions.map((a) => JSON.stringify(a)));
|
|
308
|
-
assertionIntegrityValid = origAssertions.length > 0
|
|
309
|
-
? origAssertions.every((a) => postSet.has(JSON.stringify(a)))
|
|
310
|
-
: true;
|
|
311
|
-
} else {
|
|
312
|
-
assertionIntegrityValid =
|
|
313
|
-
proposal.originalCompositeFingerprint.assertionFingerprint === postExecCompositeFingerprint.assertionFingerprint;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const executionProof: VerifiedExecutionProof = {
|
|
317
|
-
testFilePath: worktreeSpecPath,
|
|
318
|
-
targetTestIdentity: relSpec,
|
|
319
|
-
command: `npx ${execArgs.join(' ')}`,
|
|
320
|
-
worktreeId: input.worktreeId,
|
|
321
|
-
repositoryIdentity: rootDir,
|
|
322
|
-
originalSourceHash: ApprovalService.hashContent(proposal.originalSource),
|
|
323
|
-
modifiedSourceHash: ApprovalService.hashContent(proposal.proposedPatchedSource),
|
|
324
|
-
diffHash: ApprovalService.hashContent(proposal.fullDiff ?? proposal.proposedPatchedSource),
|
|
325
|
-
exitCode: execResult.exitCode,
|
|
326
|
-
stdout: execResult.stdout,
|
|
327
|
-
stderr: execResult.stderr,
|
|
328
|
-
passed: executionPassed,
|
|
329
|
-
executionOutcome: executionPassed ? 'PASS' : 'FAIL',
|
|
330
|
-
durationMs: execResult.durationMs,
|
|
331
|
-
targetTestEvidence: [`Passed tests: ${passedCount}`, `Exit code: ${execResult.exitCode}`],
|
|
332
|
-
assertionIntegrityValid,
|
|
333
|
-
timestamp: Date.now(),
|
|
334
|
-
proofVersion: PROOF_VERSION,
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
// Missing source evidence remains unavailable; it must never be treated
|
|
338
|
-
// as proof of business correctness.
|
|
339
|
-
const originalSpecPath = path.isAbsolute(proposal.testFilePath)
|
|
340
|
-
? proposal.testFilePath
|
|
341
|
-
: path.resolve(rootDir, proposal.testFilePath);
|
|
342
|
-
const originalSpecSource = fs.existsSync(originalSpecPath)
|
|
343
|
-
? fs.readFileSync(originalSpecPath, 'utf-8')
|
|
344
|
-
: undefined;
|
|
345
|
-
const currentSpecSource = fs.existsSync(worktreeSpecPath)
|
|
346
|
-
? fs.readFileSync(worktreeSpecPath, 'utf-8')
|
|
347
|
-
: undefined;
|
|
348
|
-
const builtInOracleEvaluation = evaluateBuiltInBusinessOracles({
|
|
349
|
-
intent: proposal.businessIntent ?? extractBusinessIntent({
|
|
350
|
-
id: proposal.originalActionId,
|
|
351
|
-
sourceText: originalSpecSource ?? '',
|
|
352
|
-
sourcePath: originalSpecPath,
|
|
353
|
-
}),
|
|
354
|
-
originalSourceText: originalSpecSource,
|
|
355
|
-
currentSourceText: currentSpecSource,
|
|
356
|
-
executionPassed,
|
|
357
|
-
});
|
|
358
|
-
const effectiveBusinessOracleStatus = input.businessOracleStatus === 'contradicted'
|
|
359
|
-
|| builtInOracleEvaluation.status === 'contradicted'
|
|
360
|
-
? 'contradicted'
|
|
361
|
-
: input.businessOracleStatus ?? builtInOracleEvaluation.status;
|
|
362
|
-
|
|
363
|
-
// Stage 5: Validate Intent Invariant v2 with actual execution proof
|
|
364
|
-
const intentValidation = IntentValidator.validateV2({
|
|
365
|
-
testFilePath: worktreeSpecPath,
|
|
366
|
-
originalCompositeFingerprint: proposal.originalCompositeFingerprint,
|
|
367
|
-
pageObjectOriginalSource: proposal.originalSource,
|
|
368
|
-
pageObjectPatchedSource: proposal.proposedPatchedSource,
|
|
369
|
-
pageObjectClassName: proposal.className,
|
|
370
|
-
targetMemberName: proposal.memberName,
|
|
371
|
-
candidateElement: proposal.candidateElement,
|
|
372
|
-
intendedAction: proposal.intendedAction,
|
|
373
|
-
originalSelector: proposal.originalSelector,
|
|
374
|
-
executionPassed,
|
|
375
|
-
executionProof,
|
|
376
|
-
businessOracleStatus: effectiveBusinessOracleStatus,
|
|
377
|
-
pattern: proposal.pattern,
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
const isFullyVerified = executionPassed && intentValidation.isValid;
|
|
381
|
-
currentState = isFullyVerified ? 'READY_FOR_APPROVAL' : 'REJECTED';
|
|
382
|
-
|
|
383
|
-
const evidence: RepairEvidence = {
|
|
384
|
-
originalActionId: proposal.originalActionId,
|
|
385
|
-
originalSelector: proposal.originalSelector,
|
|
386
|
-
candidateSelector: proposal.replacementSelector,
|
|
387
|
-
candidateScore: proposal.candidateScore,
|
|
388
|
-
sourceLocation: {
|
|
389
|
-
filePath: proposal.pomFilePath,
|
|
390
|
-
className: proposal.className,
|
|
391
|
-
memberName: proposal.memberName,
|
|
392
|
-
nodeKind: proposal.pattern === 'property-initializer' ? 'property' : (proposal.pattern as any),
|
|
393
|
-
line: 1,
|
|
394
|
-
},
|
|
395
|
-
targetIdentityEvidence: [
|
|
396
|
-
`Tag: ${proposal.candidateElement.tag}`,
|
|
397
|
-
`Role: ${proposal.candidateElement.role ?? 'none'}`,
|
|
398
|
-
`Name: ${proposal.candidateElement.accessibleName ?? 'none'}`,
|
|
399
|
-
],
|
|
400
|
-
assertionFingerprint: proposal.originalCompositeFingerprint.assertionFingerprint,
|
|
401
|
-
businessOracleStatus: effectiveBusinessOracleStatus,
|
|
402
|
-
verificationWorktreeId: input.worktreeId,
|
|
403
|
-
repairedTestDurationMs: execResult.durationMs,
|
|
404
|
-
timestamp: Date.now(),
|
|
405
|
-
repairedSuccessfully: isFullyVerified,
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
return {
|
|
409
|
-
state: currentState,
|
|
410
|
-
evidence,
|
|
411
|
-
executionResult: execResult,
|
|
412
|
-
intentValidation,
|
|
413
|
-
executionProof,
|
|
414
|
-
isReadyForApproval: isFullyVerified,
|
|
415
|
-
businessOracleResults: builtInOracleEvaluation.results,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Backward-compatible planAndExecute wrapper.
|
|
421
|
-
* Generates in-memory repair proposal and validates intent in-memory.
|
|
422
|
-
* Strictly PROHIBITS direct disk mutation without isolated worktree execution.
|
|
423
|
-
*/
|
|
424
|
-
public static planAndExecute(input: RepairPlanInput): RepairEvidence {
|
|
425
|
-
const proposal = RepairPlanner.planRepair(input);
|
|
426
|
-
|
|
427
|
-
// Validate Intent Invariant (assertion immutability & action semantics)
|
|
428
|
-
const validation = IntentValidator.validateProposal({
|
|
429
|
-
testFilePath: input.testFilePath,
|
|
430
|
-
originalCompositeFingerprint: proposal.originalCompositeFingerprint,
|
|
431
|
-
candidateElement: input.candidateElement,
|
|
432
|
-
intendedAction: input.intendedAction,
|
|
433
|
-
originalSelector: input.originalSelector,
|
|
434
|
-
businessOracleStatus: 'unavailable',
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
if (!validation.isValid) {
|
|
438
|
-
throw new Error(`Intent validation failed: ${validation.reason ?? 'Invalid action semantics or identity mismatch'}`);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return {
|
|
442
|
-
originalActionId: input.originalActionId,
|
|
443
|
-
originalSelector: input.originalSelector,
|
|
444
|
-
candidateSelector: proposal.replacementSelector,
|
|
445
|
-
candidateScore: input.candidateScore,
|
|
446
|
-
sourceLocation: {
|
|
447
|
-
filePath: input.pomFilePath,
|
|
448
|
-
className: input.className,
|
|
449
|
-
memberName: input.memberName,
|
|
450
|
-
nodeKind: proposal.pattern === 'property-initializer' ? 'property' : (proposal.pattern as any),
|
|
451
|
-
line: 1,
|
|
452
|
-
},
|
|
453
|
-
targetIdentityEvidence: [
|
|
454
|
-
`Tag: ${input.candidateElement.tag}`,
|
|
455
|
-
`Role: ${input.candidateElement.role ?? 'none'}`,
|
|
456
|
-
`Name: ${input.candidateElement.accessibleName ?? 'none'}`,
|
|
457
|
-
],
|
|
458
|
-
assertionFingerprint: proposal.originalCompositeFingerprint.assertionFingerprint,
|
|
459
|
-
businessOracleStatus: 'unavailable',
|
|
460
|
-
verificationWorktreeId: input.worktreeId ?? 'unverified-in-memory',
|
|
461
|
-
repairedTestDurationMs: 0,
|
|
462
|
-
timestamp: Date.now(),
|
|
463
|
-
repairedSuccessfully: false, // Direct modification prohibited; requires isolated worktree execution
|
|
464
|
-
notes: 'Direct disk mutation prohibited. Proposal generated in-memory. Use executeAndVerifyInWorktree() for isolated execution proof.',
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { AstPatcher } from './ast-patcher.js';
|
|
4
|
+
import { StatementPatcher, StatementPatchPlan } from './statement-patcher.js';
|
|
5
|
+
import { AssertionHasher, CompositeIntentFingerprint } from './assertion-hasher.js';
|
|
6
|
+
import { IntentValidator, IntentV2ValidationResult, VerifiedExecutionProof } from './intent-validator.js';
|
|
7
|
+
import { DiffAllowlistValidator } from './diff-allowlist.js';
|
|
8
|
+
import { ApprovalService } from './approval-service.js';
|
|
9
|
+
import { RepairEvidence } from '../core/types/repair-evidence.js';
|
|
10
|
+
import { ResolvedElement } from '../trace/snapshot-resolver.js';
|
|
11
|
+
import { CandidateExtractor } from '../selector/candidate-extractor.js';
|
|
12
|
+
import { ExecutionRunner, ExecutionResult } from '../sandbox/execution-runner.js';
|
|
13
|
+
import { PROOF_VERSION } from '../core/version.js';
|
|
14
|
+
import {
|
|
15
|
+
BusinessIntent,
|
|
16
|
+
BusinessOracleResult,
|
|
17
|
+
evaluateBuiltInBusinessOracles,
|
|
18
|
+
extractBusinessIntent,
|
|
19
|
+
} from '../intelligence/business-intent.js';
|
|
20
|
+
|
|
21
|
+
export type RepairLifecycleState =
|
|
22
|
+
| 'ANALYZED'
|
|
23
|
+
| 'PROPOSED'
|
|
24
|
+
| 'PATCHED_IN_ISOLATION'
|
|
25
|
+
| 'INTENT_VALIDATED'
|
|
26
|
+
| 'TARGET_TEST_PASSED'
|
|
27
|
+
| 'AFFECTED_TESTS_PASSED'
|
|
28
|
+
| 'FULL_SUITE_PASSED'
|
|
29
|
+
| 'READY_FOR_APPROVAL'
|
|
30
|
+
| 'REJECTED';
|
|
31
|
+
|
|
32
|
+
export interface RepairProposal {
|
|
33
|
+
state: RepairLifecycleState;
|
|
34
|
+
originalActionId: string;
|
|
35
|
+
originalSelector: string;
|
|
36
|
+
replacementSelector: string;
|
|
37
|
+
candidateElement: ResolvedElement;
|
|
38
|
+
candidateScore: number;
|
|
39
|
+
testFilePath: string;
|
|
40
|
+
pomFilePath: string;
|
|
41
|
+
className: string;
|
|
42
|
+
memberName: string;
|
|
43
|
+
intendedAction: string;
|
|
44
|
+
originalCompositeFingerprint: CompositeIntentFingerprint;
|
|
45
|
+
proposedPatchedSource: string;
|
|
46
|
+
originalSource: string;
|
|
47
|
+
pattern: string;
|
|
48
|
+
fullDiff?: string | undefined;
|
|
49
|
+
diffAllowlistPassed: boolean;
|
|
50
|
+
businessIntent?: BusinessIntent | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RepairVerificationInput {
|
|
54
|
+
proposal: RepairProposal;
|
|
55
|
+
worktreePath: string;
|
|
56
|
+
worktreeId: string;
|
|
57
|
+
workspaceRoot?: string | undefined;
|
|
58
|
+
configPath?: string | undefined;
|
|
59
|
+
project?: string | undefined;
|
|
60
|
+
businessOracleStatus?: 'verified' | 'contradicted' | 'unavailable' | undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface VerifiedRepairResult {
|
|
64
|
+
state: RepairLifecycleState;
|
|
65
|
+
evidence: RepairEvidence;
|
|
66
|
+
executionResult: ExecutionResult;
|
|
67
|
+
intentValidation: IntentV2ValidationResult;
|
|
68
|
+
executionProof: VerifiedExecutionProof;
|
|
69
|
+
isReadyForApproval: boolean;
|
|
70
|
+
businessOracleResults?: BusinessOracleResult[] | undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface RepairPlanInput {
|
|
74
|
+
originalActionId: string;
|
|
75
|
+
originalSelector: string;
|
|
76
|
+
candidateElement: ResolvedElement;
|
|
77
|
+
candidateScore: number;
|
|
78
|
+
testFilePath: string;
|
|
79
|
+
pomFilePath: string;
|
|
80
|
+
className: string;
|
|
81
|
+
memberName: string;
|
|
82
|
+
intendedAction: string;
|
|
83
|
+
replacementSelector?: string | undefined;
|
|
84
|
+
worktreeId?: string | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface StatementRepairPlanInput {
|
|
88
|
+
originalActionId: string;
|
|
89
|
+
originalSelector?: string | undefined;
|
|
90
|
+
testFilePath: string;
|
|
91
|
+
plan: StatementPatchPlan;
|
|
92
|
+
candidateElement?: ResolvedElement | undefined;
|
|
93
|
+
candidateScore?: number | undefined;
|
|
94
|
+
intendedAction?: string | undefined;
|
|
95
|
+
worktreeId?: string | undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class RepairPlanner {
|
|
99
|
+
/**
|
|
100
|
+
* Generates a modern, user-centric Playwright locator expression prioritizing getByRole and getByTestId.
|
|
101
|
+
* Employs robust string escaping; avoids bare tag fallbacks.
|
|
102
|
+
*/
|
|
103
|
+
public static generateSelectorExpression(el: ResolvedElement, pageProperty = 'this.page'): string {
|
|
104
|
+
return CandidateExtractor.generateLocator(el, pageProperty);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generates a verified in-memory statement patch proposal using StatementPatcher.
|
|
109
|
+
* Enables healing of inline test statements, timing synchronization, and sleep elimination.
|
|
110
|
+
*/
|
|
111
|
+
public static planStatementRepair(input: StatementRepairPlanInput): RepairProposal {
|
|
112
|
+
const origSource = fs.readFileSync(input.testFilePath, 'utf-8');
|
|
113
|
+
const originalCompositeFingerprint = AssertionHasher.computeCompositeFingerprint(input.testFilePath);
|
|
114
|
+
|
|
115
|
+
const patcher = new StatementPatcher();
|
|
116
|
+
const patchResult = patcher.patchStatementInMemory(origSource, input.plan, path.basename(input.testFilePath));
|
|
117
|
+
|
|
118
|
+
// Verify all original assertions are strictly preserved in post-patch
|
|
119
|
+
const origAssertions = AssertionHasher.extractAssertionRecordsFromSource(origSource);
|
|
120
|
+
const postAssertions = AssertionHasher.extractAssertionRecordsFromSource(patchResult.patchedSource);
|
|
121
|
+
const postSet = new Set(postAssertions.map((a) => JSON.stringify(a)));
|
|
122
|
+
const allOriginalAssertionsPreserved = origAssertions.every((a) => postSet.has(JSON.stringify(a)));
|
|
123
|
+
if (!allOriginalAssertionsPreserved) {
|
|
124
|
+
throw new Error('AST_ASSERTION_TAMPERING_PROHIBITED: Statement patch altered or removed test assertions.');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const dummyCandidate: ResolvedElement = input.candidateElement ?? {
|
|
128
|
+
tag: 'statement',
|
|
129
|
+
attributes: {},
|
|
130
|
+
text: patchResult.replacementStatement,
|
|
131
|
+
children: [],
|
|
132
|
+
role: 'statement',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
state: 'PROPOSED',
|
|
137
|
+
originalActionId: input.originalActionId,
|
|
138
|
+
originalSelector: input.originalSelector ?? patchResult.originalStatement,
|
|
139
|
+
replacementSelector: patchResult.replacementStatement,
|
|
140
|
+
candidateElement: dummyCandidate,
|
|
141
|
+
candidateScore: input.candidateScore ?? 1.0,
|
|
142
|
+
testFilePath: input.testFilePath,
|
|
143
|
+
pomFilePath: input.testFilePath,
|
|
144
|
+
className: 'InlineSpec',
|
|
145
|
+
memberName: input.plan.kind,
|
|
146
|
+
intendedAction: input.intendedAction ?? input.plan.kind,
|
|
147
|
+
originalCompositeFingerprint,
|
|
148
|
+
proposedPatchedSource: patchResult.patchedSource,
|
|
149
|
+
originalSource: origSource,
|
|
150
|
+
pattern: 'statement-patch',
|
|
151
|
+
fullDiff: patchResult.diff,
|
|
152
|
+
diffAllowlistPassed: true,
|
|
153
|
+
businessIntent: extractBusinessIntent({
|
|
154
|
+
id: input.originalActionId,
|
|
155
|
+
sourceText: origSource,
|
|
156
|
+
sourcePath: input.testFilePath,
|
|
157
|
+
}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Stage 1 & 2: Analyzes candidate and generates an in-memory repair proposal.
|
|
163
|
+
* Does NOT modify source files on disk. Does NOT claim repair success.
|
|
164
|
+
*/
|
|
165
|
+
public static planRepair(input: RepairPlanInput): RepairProposal {
|
|
166
|
+
// 0. Read original source and discover page property identifier
|
|
167
|
+
const origSource = fs.readFileSync(input.pomFilePath, 'utf-8');
|
|
168
|
+
const pageProperty = CandidateExtractor.discoverPageProperty(origSource);
|
|
169
|
+
|
|
170
|
+
// 1. Synthesize replacement locator expression with escaping
|
|
171
|
+
const replacementLocator = input.replacementSelector ?? RepairPlanner.generateSelectorExpression(input.candidateElement, pageProperty);
|
|
172
|
+
|
|
173
|
+
// 2. Validate replacement against Playwright locator grammar
|
|
174
|
+
const grammarCheck = DiffAllowlistValidator.validateLocatorGrammar(replacementLocator);
|
|
175
|
+
if (!grammarCheck.isValid) {
|
|
176
|
+
throw new Error(`Invalid candidate locator generated: ${grammarCheck.reason}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 3. Compute baseline composite fingerprint of the test spec
|
|
180
|
+
const originalCompositeFingerprint = AssertionHasher.computeCompositeFingerprint(input.testFilePath);
|
|
181
|
+
|
|
182
|
+
// 4. Generate in-memory AST patch without writing to disk
|
|
183
|
+
const patcher = new AstPatcher();
|
|
184
|
+
const inMemPatch = patcher.patchPageObjectInMemory(origSource, {
|
|
185
|
+
className: input.className,
|
|
186
|
+
memberName: input.memberName,
|
|
187
|
+
replacementExpression: replacementLocator,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// 5. Validate diff allowlist in-memory
|
|
191
|
+
const diffResult = DiffAllowlistValidator.validate({
|
|
192
|
+
originalSource: origSource,
|
|
193
|
+
patchedSource: inMemPatch.patchedSource,
|
|
194
|
+
targetClassName: input.className,
|
|
195
|
+
targetMemberName: input.memberName,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (!diffResult.isAllowed) {
|
|
199
|
+
throw new Error(`Diff allowlist rejected proposed patch: ${diffResult.reason}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
state: 'PROPOSED',
|
|
204
|
+
originalActionId: input.originalActionId,
|
|
205
|
+
originalSelector: input.originalSelector,
|
|
206
|
+
replacementSelector: replacementLocator,
|
|
207
|
+
candidateElement: input.candidateElement,
|
|
208
|
+
candidateScore: input.candidateScore,
|
|
209
|
+
testFilePath: input.testFilePath,
|
|
210
|
+
pomFilePath: input.pomFilePath,
|
|
211
|
+
className: input.className,
|
|
212
|
+
memberName: input.memberName,
|
|
213
|
+
intendedAction: input.intendedAction,
|
|
214
|
+
originalCompositeFingerprint,
|
|
215
|
+
proposedPatchedSource: inMemPatch.patchedSource,
|
|
216
|
+
originalSource: origSource,
|
|
217
|
+
pattern: inMemPatch.pattern,
|
|
218
|
+
fullDiff: inMemPatch.diff,
|
|
219
|
+
diffAllowlistPassed: true,
|
|
220
|
+
businessIntent: extractBusinessIntent({
|
|
221
|
+
id: input.originalActionId,
|
|
222
|
+
sourceText: fs.readFileSync(input.testFilePath, 'utf8'),
|
|
223
|
+
sourcePath: input.testFilePath,
|
|
224
|
+
}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Stages 3-8: Applies patch strictly inside the isolated Git worktree, runs test execution,
|
|
230
|
+
* validates Intent Invariant v2 with actual execution proof, and derives verified evidence.
|
|
231
|
+
*/
|
|
232
|
+
public static async executeAndVerifyInWorktree(
|
|
233
|
+
input: RepairVerificationInput
|
|
234
|
+
): Promise<VerifiedRepairResult> {
|
|
235
|
+
const proposal = input.proposal;
|
|
236
|
+
const rootDir = input.workspaceRoot ?? process.cwd();
|
|
237
|
+
|
|
238
|
+
// Canonical containment checks
|
|
239
|
+
const canonicalWorktree = fs.existsSync(input.worktreePath)
|
|
240
|
+
? fs.realpathSync(input.worktreePath)
|
|
241
|
+
: path.resolve(input.worktreePath);
|
|
242
|
+
|
|
243
|
+
const relPom = path.isAbsolute(proposal.pomFilePath)
|
|
244
|
+
? path.relative(rootDir, proposal.pomFilePath)
|
|
245
|
+
: proposal.pomFilePath;
|
|
246
|
+
const relSpec = path.isAbsolute(proposal.testFilePath)
|
|
247
|
+
? path.relative(rootDir, proposal.testFilePath)
|
|
248
|
+
: proposal.testFilePath;
|
|
249
|
+
|
|
250
|
+
const worktreePomPath = path.resolve(canonicalWorktree, relPom);
|
|
251
|
+
const worktreeSpecPath = path.resolve(canonicalWorktree, relSpec);
|
|
252
|
+
|
|
253
|
+
const relPomCheck = path.relative(canonicalWorktree, worktreePomPath);
|
|
254
|
+
if (relPomCheck.startsWith('..') || path.isAbsolute(relPomCheck)) {
|
|
255
|
+
throw new Error(`Security Violation: Worktree POM path '${worktreePomPath}' escapes worktree boundary '${canonicalWorktree}'.`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const relSpecCheck = path.relative(canonicalWorktree, worktreeSpecPath);
|
|
259
|
+
if (relSpecCheck.startsWith('..') || path.isAbsolute(relSpecCheck)) {
|
|
260
|
+
throw new Error(`Security Violation: Worktree Spec path '${worktreeSpecPath}' escapes worktree boundary '${canonicalWorktree}'.`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Stage 3: Apply patch ONLY in isolated worktree copy
|
|
264
|
+
const targetFilePath = proposal.pattern === 'statement-patch' ? worktreeSpecPath : worktreePomPath;
|
|
265
|
+
await fs.promises.mkdir(path.dirname(targetFilePath), { recursive: true });
|
|
266
|
+
await fs.promises.writeFile(targetFilePath, proposal.proposedPatchedSource, 'utf-8');
|
|
267
|
+
let currentState: RepairLifecycleState = 'PATCHED_IN_ISOLATION';
|
|
268
|
+
|
|
269
|
+
// Stage 4: Execute target test inside worktree (normalize to forward slashes for Playwright regex matcher)
|
|
270
|
+
const normalizedRelSpec = relSpec.replace(/\\/g, '/');
|
|
271
|
+
const execArgs = ['playwright', 'test', normalizedRelSpec];
|
|
272
|
+
if (input.configPath) {
|
|
273
|
+
const relConfig = path.isAbsolute(input.configPath)
|
|
274
|
+
? path.relative(rootDir, input.configPath)
|
|
275
|
+
: input.configPath;
|
|
276
|
+
execArgs.push('--config', relConfig.replace(/\\/g, '/'));
|
|
277
|
+
}
|
|
278
|
+
if (input.project) {
|
|
279
|
+
execArgs.push('--project', input.project);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const execResult = await ExecutionRunner.run('npx', {
|
|
283
|
+
cwd: canonicalWorktree,
|
|
284
|
+
args: execArgs,
|
|
285
|
+
timeoutMs: 60000,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Verify test was actually discovered and passed
|
|
289
|
+
const passedMatch = execResult.stdout.match(/(\d+)\s+passed/i);
|
|
290
|
+
const passedCount = passedMatch ? parseInt(passedMatch[1] ?? '0', 10) : 0;
|
|
291
|
+
const executionPassed = execResult.exitCode === 0 && passedCount > 0 && !execResult.timedOut;
|
|
292
|
+
|
|
293
|
+
if (executionPassed) {
|
|
294
|
+
currentState = 'TARGET_TEST_PASSED';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Verify assertion integrity dynamically by comparing pre-patch and post-patch assertions
|
|
298
|
+
const worktreeSpecSource = fs.existsSync(worktreeSpecPath)
|
|
299
|
+
? fs.readFileSync(worktreeSpecPath, 'utf-8')
|
|
300
|
+
: '';
|
|
301
|
+
const postExecCompositeFingerprint = AssertionHasher.computeCompositeFingerprintFromSource(worktreeSpecSource);
|
|
302
|
+
|
|
303
|
+
let assertionIntegrityValid = false;
|
|
304
|
+
if (proposal.pattern === 'statement-patch') {
|
|
305
|
+
const origAssertions = AssertionHasher.extractAssertionRecordsFromSource(proposal.originalSource);
|
|
306
|
+
const postAssertions = AssertionHasher.extractAssertionRecordsFromSource(worktreeSpecSource);
|
|
307
|
+
const postSet = new Set(postAssertions.map((a) => JSON.stringify(a)));
|
|
308
|
+
assertionIntegrityValid = origAssertions.length > 0
|
|
309
|
+
? origAssertions.every((a) => postSet.has(JSON.stringify(a)))
|
|
310
|
+
: true;
|
|
311
|
+
} else {
|
|
312
|
+
assertionIntegrityValid =
|
|
313
|
+
proposal.originalCompositeFingerprint.assertionFingerprint === postExecCompositeFingerprint.assertionFingerprint;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const executionProof: VerifiedExecutionProof = {
|
|
317
|
+
testFilePath: worktreeSpecPath,
|
|
318
|
+
targetTestIdentity: relSpec,
|
|
319
|
+
command: `npx ${execArgs.join(' ')}`,
|
|
320
|
+
worktreeId: input.worktreeId,
|
|
321
|
+
repositoryIdentity: rootDir,
|
|
322
|
+
originalSourceHash: ApprovalService.hashContent(proposal.originalSource),
|
|
323
|
+
modifiedSourceHash: ApprovalService.hashContent(proposal.proposedPatchedSource),
|
|
324
|
+
diffHash: ApprovalService.hashContent(proposal.fullDiff ?? proposal.proposedPatchedSource),
|
|
325
|
+
exitCode: execResult.exitCode,
|
|
326
|
+
stdout: execResult.stdout,
|
|
327
|
+
stderr: execResult.stderr,
|
|
328
|
+
passed: executionPassed,
|
|
329
|
+
executionOutcome: executionPassed ? 'PASS' : 'FAIL',
|
|
330
|
+
durationMs: execResult.durationMs,
|
|
331
|
+
targetTestEvidence: [`Passed tests: ${passedCount}`, `Exit code: ${execResult.exitCode}`],
|
|
332
|
+
assertionIntegrityValid,
|
|
333
|
+
timestamp: Date.now(),
|
|
334
|
+
proofVersion: PROOF_VERSION,
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Missing source evidence remains unavailable; it must never be treated
|
|
338
|
+
// as proof of business correctness.
|
|
339
|
+
const originalSpecPath = path.isAbsolute(proposal.testFilePath)
|
|
340
|
+
? proposal.testFilePath
|
|
341
|
+
: path.resolve(rootDir, proposal.testFilePath);
|
|
342
|
+
const originalSpecSource = fs.existsSync(originalSpecPath)
|
|
343
|
+
? fs.readFileSync(originalSpecPath, 'utf-8')
|
|
344
|
+
: undefined;
|
|
345
|
+
const currentSpecSource = fs.existsSync(worktreeSpecPath)
|
|
346
|
+
? fs.readFileSync(worktreeSpecPath, 'utf-8')
|
|
347
|
+
: undefined;
|
|
348
|
+
const builtInOracleEvaluation = evaluateBuiltInBusinessOracles({
|
|
349
|
+
intent: proposal.businessIntent ?? extractBusinessIntent({
|
|
350
|
+
id: proposal.originalActionId,
|
|
351
|
+
sourceText: originalSpecSource ?? '',
|
|
352
|
+
sourcePath: originalSpecPath,
|
|
353
|
+
}),
|
|
354
|
+
originalSourceText: originalSpecSource,
|
|
355
|
+
currentSourceText: currentSpecSource,
|
|
356
|
+
executionPassed,
|
|
357
|
+
});
|
|
358
|
+
const effectiveBusinessOracleStatus = input.businessOracleStatus === 'contradicted'
|
|
359
|
+
|| builtInOracleEvaluation.status === 'contradicted'
|
|
360
|
+
? 'contradicted'
|
|
361
|
+
: input.businessOracleStatus ?? builtInOracleEvaluation.status;
|
|
362
|
+
|
|
363
|
+
// Stage 5: Validate Intent Invariant v2 with actual execution proof
|
|
364
|
+
const intentValidation = IntentValidator.validateV2({
|
|
365
|
+
testFilePath: worktreeSpecPath,
|
|
366
|
+
originalCompositeFingerprint: proposal.originalCompositeFingerprint,
|
|
367
|
+
pageObjectOriginalSource: proposal.originalSource,
|
|
368
|
+
pageObjectPatchedSource: proposal.proposedPatchedSource,
|
|
369
|
+
pageObjectClassName: proposal.className,
|
|
370
|
+
targetMemberName: proposal.memberName,
|
|
371
|
+
candidateElement: proposal.candidateElement,
|
|
372
|
+
intendedAction: proposal.intendedAction,
|
|
373
|
+
originalSelector: proposal.originalSelector,
|
|
374
|
+
executionPassed,
|
|
375
|
+
executionProof,
|
|
376
|
+
businessOracleStatus: effectiveBusinessOracleStatus,
|
|
377
|
+
pattern: proposal.pattern,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const isFullyVerified = executionPassed && intentValidation.isValid;
|
|
381
|
+
currentState = isFullyVerified ? 'READY_FOR_APPROVAL' : 'REJECTED';
|
|
382
|
+
|
|
383
|
+
const evidence: RepairEvidence = {
|
|
384
|
+
originalActionId: proposal.originalActionId,
|
|
385
|
+
originalSelector: proposal.originalSelector,
|
|
386
|
+
candidateSelector: proposal.replacementSelector,
|
|
387
|
+
candidateScore: proposal.candidateScore,
|
|
388
|
+
sourceLocation: {
|
|
389
|
+
filePath: proposal.pomFilePath,
|
|
390
|
+
className: proposal.className,
|
|
391
|
+
memberName: proposal.memberName,
|
|
392
|
+
nodeKind: proposal.pattern === 'property-initializer' ? 'property' : (proposal.pattern as any),
|
|
393
|
+
line: 1,
|
|
394
|
+
},
|
|
395
|
+
targetIdentityEvidence: [
|
|
396
|
+
`Tag: ${proposal.candidateElement.tag}`,
|
|
397
|
+
`Role: ${proposal.candidateElement.role ?? 'none'}`,
|
|
398
|
+
`Name: ${proposal.candidateElement.accessibleName ?? 'none'}`,
|
|
399
|
+
],
|
|
400
|
+
assertionFingerprint: proposal.originalCompositeFingerprint.assertionFingerprint,
|
|
401
|
+
businessOracleStatus: effectiveBusinessOracleStatus,
|
|
402
|
+
verificationWorktreeId: input.worktreeId,
|
|
403
|
+
repairedTestDurationMs: execResult.durationMs,
|
|
404
|
+
timestamp: Date.now(),
|
|
405
|
+
repairedSuccessfully: isFullyVerified,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
state: currentState,
|
|
410
|
+
evidence,
|
|
411
|
+
executionResult: execResult,
|
|
412
|
+
intentValidation,
|
|
413
|
+
executionProof,
|
|
414
|
+
isReadyForApproval: isFullyVerified,
|
|
415
|
+
businessOracleResults: builtInOracleEvaluation.results,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Backward-compatible planAndExecute wrapper.
|
|
421
|
+
* Generates in-memory repair proposal and validates intent in-memory.
|
|
422
|
+
* Strictly PROHIBITS direct disk mutation without isolated worktree execution.
|
|
423
|
+
*/
|
|
424
|
+
public static planAndExecute(input: RepairPlanInput): RepairEvidence {
|
|
425
|
+
const proposal = RepairPlanner.planRepair(input);
|
|
426
|
+
|
|
427
|
+
// Validate Intent Invariant (assertion immutability & action semantics)
|
|
428
|
+
const validation = IntentValidator.validateProposal({
|
|
429
|
+
testFilePath: input.testFilePath,
|
|
430
|
+
originalCompositeFingerprint: proposal.originalCompositeFingerprint,
|
|
431
|
+
candidateElement: input.candidateElement,
|
|
432
|
+
intendedAction: input.intendedAction,
|
|
433
|
+
originalSelector: input.originalSelector,
|
|
434
|
+
businessOracleStatus: 'unavailable',
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
if (!validation.isValid) {
|
|
438
|
+
throw new Error(`Intent validation failed: ${validation.reason ?? 'Invalid action semantics or identity mismatch'}`);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
originalActionId: input.originalActionId,
|
|
443
|
+
originalSelector: input.originalSelector,
|
|
444
|
+
candidateSelector: proposal.replacementSelector,
|
|
445
|
+
candidateScore: input.candidateScore,
|
|
446
|
+
sourceLocation: {
|
|
447
|
+
filePath: input.pomFilePath,
|
|
448
|
+
className: input.className,
|
|
449
|
+
memberName: input.memberName,
|
|
450
|
+
nodeKind: proposal.pattern === 'property-initializer' ? 'property' : (proposal.pattern as any),
|
|
451
|
+
line: 1,
|
|
452
|
+
},
|
|
453
|
+
targetIdentityEvidence: [
|
|
454
|
+
`Tag: ${input.candidateElement.tag}`,
|
|
455
|
+
`Role: ${input.candidateElement.role ?? 'none'}`,
|
|
456
|
+
`Name: ${input.candidateElement.accessibleName ?? 'none'}`,
|
|
457
|
+
],
|
|
458
|
+
assertionFingerprint: proposal.originalCompositeFingerprint.assertionFingerprint,
|
|
459
|
+
businessOracleStatus: 'unavailable',
|
|
460
|
+
verificationWorktreeId: input.worktreeId ?? 'unverified-in-memory',
|
|
461
|
+
repairedTestDurationMs: 0,
|
|
462
|
+
timestamp: Date.now(),
|
|
463
|
+
repairedSuccessfully: false, // Direct modification prohibited; requires isolated worktree execution
|
|
464
|
+
notes: 'Direct disk mutation prohibited. Proposal generated in-memory. Use executeAndVerifyInWorktree() for isolated execution proof.',
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|