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,318 +1,318 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import fsSync from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { ArchiveReader } from '../trace/archive-reader.js';
|
|
5
|
-
import { TraceParser } from '../trace/trace-parser.js';
|
|
6
|
-
import { EvidenceCorrelator } from '../diagnostics/evidence-correlator.js';
|
|
7
|
-
import { parseExternalFailureEvidence, supplementDiagnosticContext } from '../diagnostics/external-failure-evidence.js';
|
|
8
|
-
import { buildEvidenceBundle } from './evidence-bundle.js';
|
|
9
|
-
import { runRepairProposalAgent, RepairAgentResult } from './repair-agent.js';
|
|
10
|
-
import { OpenAiCompatibleProposalProvider, ProposalProvider } from './model-provider.js';
|
|
11
|
-
import { EvidenceBundle } from './llm-proposal.js';
|
|
12
|
-
import { CorrelatedEvidence } from '../diagnostics/evidence-correlator.js';
|
|
13
|
-
import { DiagnosticContext } from '../core/types/diagnostic-context.js';
|
|
14
|
-
import { bootstrapProjectIntelligence, getHistoricalSelectorScore } from './project-intelligence.js';
|
|
15
|
-
import { DiagnosticReportGenerator } from '../diagnostics/diagnostic-report.js';
|
|
16
|
-
import { discoverRepairSourceMapping } from '../repair/source-mapper.js';
|
|
17
|
-
import { DirectBrowserInvestigator } from '../reproduce/direct-browser-investigator.js';
|
|
18
|
-
import { CandidateExtractor } from '../selector/candidate-extractor.js';
|
|
19
|
-
import { RepairCandidateRanker } from '../selector/repair-candidate-ranker.js';
|
|
20
|
-
import { AbstentionPolicy } from '../selector/abstention-policy.js';
|
|
21
|
-
import { EvidenceRedactor } from '../evidence/evidence-redactor.js';
|
|
22
|
-
import { ExecutionRunner } from '../sandbox/execution-runner.js';
|
|
23
|
-
|
|
24
|
-
export interface TraceProposalWorkflowInput {
|
|
25
|
-
tracePath: string;
|
|
26
|
-
workspaceRoot?: string | undefined;
|
|
27
|
-
errorContextPath?: string | undefined;
|
|
28
|
-
executionId?: string | undefined;
|
|
29
|
-
testIdentity?: string | undefined;
|
|
30
|
-
enableBrowserInvestigation?: boolean | undefined;
|
|
31
|
-
browserUrl?: string | undefined;
|
|
32
|
-
browserAllowedHosts?: string[] | undefined;
|
|
33
|
-
enableTestExecution?: boolean | undefined;
|
|
34
|
-
provider?: ProposalProvider | undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface TraceProposalWorkflowResult {
|
|
38
|
-
context: DiagnosticContext;
|
|
39
|
-
correlated: CorrelatedEvidence;
|
|
40
|
-
bundle: EvidenceBundle;
|
|
41
|
-
agent: RepairAgentResult;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function assertWorkspacePath(root: string, target: string, label: string): string {
|
|
45
|
-
const canonicalRoot = fsSync.realpathSync(root);
|
|
46
|
-
const resolved = path.resolve(root, target);
|
|
47
|
-
if (!fsSync.existsSync(resolved)) throw new Error(`${label} not found: '${resolved}'`);
|
|
48
|
-
const canonicalTarget = fsSync.realpathSync(resolved);
|
|
49
|
-
const relative = path.relative(canonicalRoot, canonicalTarget);
|
|
50
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error(`${label} escapes workspace root: '${target}'`);
|
|
51
|
-
return canonicalTarget;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function resolveReadOnlyPath(root: string, target: string, label: string): string {
|
|
55
|
-
const resolved = path.isAbsolute(target) ? target : path.resolve(root, target);
|
|
56
|
-
if (!fsSync.existsSync(resolved)) throw new Error(`${label} not found: '${resolved}'`);
|
|
57
|
-
return fsSync.realpathSync(resolved);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function searchRepository(root: string, query: string): Promise<Array<{ filePath: string; line: number; snippet: string }>> {
|
|
61
|
-
const ignored = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', 'test-results', 'playwright-report', '.pie', '.pw-intelligence']);
|
|
62
|
-
const results: Array<{ filePath: string; line: number; snippet: string }> = [];
|
|
63
|
-
const walk = async (directory: string, depth: number): Promise<void> => {
|
|
64
|
-
if (depth > 6 || results.length >= 30) return;
|
|
65
|
-
let entries;
|
|
66
|
-
try { entries = await fs.readdir(directory, { withFileTypes: true }); } catch { return; }
|
|
67
|
-
for (const entry of entries) {
|
|
68
|
-
if (results.length >= 30 || ignored.has(entry.name)) continue;
|
|
69
|
-
const full = path.join(directory, entry.name);
|
|
70
|
-
if (entry.isDirectory()) { await walk(full, depth + 1); continue; }
|
|
71
|
-
if (!/\.(?:ts|tsx|js|jsx|json|md|yaml|yml)$/i.test(entry.name)) continue;
|
|
72
|
-
let content: string;
|
|
73
|
-
try { content = await fs.readFile(full, 'utf8'); } catch { continue; }
|
|
74
|
-
const lines = content.split(/\r?\n/);
|
|
75
|
-
lines.forEach((line, index) => {
|
|
76
|
-
if (results.length >= 30 || !line.toLowerCase().includes(query.toLowerCase())) return;
|
|
77
|
-
results.push({ filePath: path.relative(root, full), line: index + 1, snippet: EvidenceRedactor.redactText(line.slice(0, 500)).redactedText });
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
await walk(root, 0);
|
|
82
|
-
return results;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function configuredProvider(): ProposalProvider | undefined {
|
|
86
|
-
const apiKey = process.env.PIE_LLM_API_KEY ?? process.env.OPENAI_API_KEY ?? process.env.DEEPSEEK_API_KEY;
|
|
87
|
-
const endpoint = process.env.PIE_LLM_ENDPOINT ?? (process.env.OPENAI_API_KEY ? 'https://api.openai.com/v1/chat/completions' : (apiKey ? 'https://api.deepseek.com/chat/completions' : undefined));
|
|
88
|
-
const model = process.env.PIE_LLM_MODEL ?? (process.env.OPENAI_API_KEY ? 'gpt-4o' : (apiKey ? 'deepseek-chat' : undefined));
|
|
89
|
-
if (!endpoint || !model) return undefined;
|
|
90
|
-
const timeoutRaw = process.env.PIE_LLM_TIMEOUT_MS;
|
|
91
|
-
const timeoutMs = timeoutRaw ? Number(timeoutRaw) : undefined;
|
|
92
|
-
return new OpenAiCompatibleProposalProvider({
|
|
93
|
-
endpoint,
|
|
94
|
-
model,
|
|
95
|
-
...(apiKey ? { apiKey } : {}),
|
|
96
|
-
...(Number.isFinite(timeoutMs) && timeoutMs && timeoutMs > 0 ? { timeoutMs } : {}),
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Runs one bounded, redaction-aware model proposal step from a trace archive.
|
|
102
|
-
* Deterministic classification executes before the provider and the returned
|
|
103
|
-
* proposal is never executed or promoted by this function.
|
|
104
|
-
*/
|
|
105
|
-
export async function runTraceProposalWorkflow(input: TraceProposalWorkflowInput): Promise<TraceProposalWorkflowResult> {
|
|
106
|
-
const workspaceRoot = path.resolve(input.workspaceRoot ?? process.cwd());
|
|
107
|
-
const tracePath = resolveReadOnlyPath(workspaceRoot, input.tracePath, 'Trace archive');
|
|
108
|
-
const { traceEntryName, networkEntryName, traceEntryNames, networkEntryNames } = ArchiveReader.findTraceFileEntries(tracePath);
|
|
109
|
-
if (!traceEntryName) throw new Error(`No .trace event stream found in archive '${tracePath}'`);
|
|
110
|
-
|
|
111
|
-
const traceText = await ArchiveReader.readEntriesAsText(tracePath, traceEntryNames ?? [traceEntryName]);
|
|
112
|
-
const networkText = (networkEntryNames ?? (networkEntryName ? [networkEntryName] : [])).length > 0
|
|
113
|
-
? await ArchiveReader.readEntriesAsText(tracePath, networkEntryNames ?? [networkEntryName!])
|
|
114
|
-
: '';
|
|
115
|
-
let context = TraceParser.assembleDiagnosticContext(traceText.split('\n'), networkText.split('\n'));
|
|
116
|
-
if (input.errorContextPath) {
|
|
117
|
-
const errorContextPath = resolveReadOnlyPath(workspaceRoot, input.errorContextPath, 'Error context');
|
|
118
|
-
context = supplementDiagnosticContext(context, parseExternalFailureEvidence(await fs.readFile(errorContextPath, 'utf8')));
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const correlated = EvidenceCorrelator.correlate(context);
|
|
122
|
-
const intelligence = await bootstrapProjectIntelligence(workspaceRoot);
|
|
123
|
-
const deterministicProposedLocator = DiagnosticReportGenerator.extractProposedLocator(correlated, intelligence);
|
|
124
|
-
const sourceMapping = await discoverRepairSourceMapping({ workspaceRoot, failedAction: correlated.context.failedAction });
|
|
125
|
-
const sourceContext: EvidenceBundle['sourceContext'] = [];
|
|
126
|
-
const profile = intelligence.profile;
|
|
127
|
-
const projectContext = profile
|
|
128
|
-
? {
|
|
129
|
-
preferredLocator: profile.locatorConventions.preferred,
|
|
130
|
-
usesPageObjects: profile.structure.usesPageObjects,
|
|
131
|
-
usesCustomFixtures: profile.structure.usesCustomFixtures,
|
|
132
|
-
usesStorageState: profile.structure.usesStorageState,
|
|
133
|
-
domainTerms: profile.domainTerms.slice(0, 30),
|
|
134
|
-
...(context.failedAction.selector
|
|
135
|
-
? { historicalSelectorReliability: getHistoricalSelectorScore(intelligence, context.failedAction.selector) }
|
|
136
|
-
: {}),
|
|
137
|
-
}
|
|
138
|
-
: undefined;
|
|
139
|
-
if (projectContext) {
|
|
140
|
-
sourceContext.push({ filePath: '.pie/project-intelligence.json', content: JSON.stringify(projectContext, null, 2) });
|
|
141
|
-
}
|
|
142
|
-
if (sourceMapping?.pomFilePath) {
|
|
143
|
-
try {
|
|
144
|
-
const source = await fs.readFile(sourceMapping.pomFilePath, 'utf8');
|
|
145
|
-
const lines = source.split(/\r?\n/);
|
|
146
|
-
const center = correlated.context.failedAction.line ?? 1;
|
|
147
|
-
const start = Math.max(0, center - 8);
|
|
148
|
-
const end = Math.min(lines.length, center + 8);
|
|
149
|
-
sourceContext.push({
|
|
150
|
-
filePath: path.relative(workspaceRoot, sourceMapping.pomFilePath),
|
|
151
|
-
startLine: start + 1,
|
|
152
|
-
endLine: end,
|
|
153
|
-
content: lines.slice(start, end).join('\n'),
|
|
154
|
-
});
|
|
155
|
-
} catch {
|
|
156
|
-
// Source mapping remains available as an action observation even when a snippet cannot be read.
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
const bundle = buildEvidenceBundle({
|
|
160
|
-
executionId: input.executionId ?? `trace-${Date.now()}`,
|
|
161
|
-
testIdentity: input.testIdentity ?? context.failedAction.sourceFile ?? 'unknown-test',
|
|
162
|
-
context,
|
|
163
|
-
failureCategory: correlated.classification.category,
|
|
164
|
-
redactionComplete: true,
|
|
165
|
-
sourceContext,
|
|
166
|
-
artifacts: [
|
|
167
|
-
{ kind: 'trace', reference: path.relative(workspaceRoot, tracePath), redacted: true },
|
|
168
|
-
...(networkEntryName ? [{ kind: 'network' as const, reference: path.relative(workspaceRoot, networkEntryName), redacted: true }] : []),
|
|
169
|
-
],
|
|
170
|
-
});
|
|
171
|
-
const agent = await runRepairProposalAgent({
|
|
172
|
-
context,
|
|
173
|
-
bundle,
|
|
174
|
-
provider: input.provider ?? configuredProvider(),
|
|
175
|
-
allowModelEscalation: true,
|
|
176
|
-
strategy: correlated.classification.category === 'LOCATOR_DRIFT'
|
|
177
|
-
&& correlated.classification.confidence >= 0.85
|
|
178
|
-
&& Boolean(deterministicProposedLocator)
|
|
179
|
-
? 'single-shot'
|
|
180
|
-
: 'agent',
|
|
181
|
-
maxDurationMs: (() => {
|
|
182
|
-
const raw = Number(process.env.PIE_LLM_AGENT_MAX_DURATION_MS ?? 180000);
|
|
183
|
-
return Number.isFinite(raw) && raw > 0 ? raw : 180000;
|
|
184
|
-
})(),
|
|
185
|
-
// Step budget prevents deadlocks; token limits remain provider/user controlled.
|
|
186
|
-
maxSteps: 6,
|
|
187
|
-
actionHandlers: {
|
|
188
|
-
inspect_evidence: async () => ({
|
|
189
|
-
category: correlated.classification.category,
|
|
190
|
-
failedAction: correlated.context.failedAction,
|
|
191
|
-
evidenceCompleteness: correlated.context.evidenceCompleteness,
|
|
192
|
-
networkEvents: correlated.context.networkEvents.length,
|
|
193
|
-
consoleEvents: correlated.context.consoleEvents.length,
|
|
194
|
-
}),
|
|
195
|
-
inspect_browser_state: async () => {
|
|
196
|
-
if (input.enableBrowserInvestigation !== true) {
|
|
197
|
-
return { status: 'DISABLED', reason: 'Browser investigation requires explicit opt-in.' };
|
|
198
|
-
}
|
|
199
|
-
if (!input.browserUrl) {
|
|
200
|
-
return { status: 'INSUFFICIENT_EVIDENCE', reason: 'No explicit browser URL was supplied.' };
|
|
201
|
-
}
|
|
202
|
-
let browserHost: string;
|
|
203
|
-
try {
|
|
204
|
-
browserHost = new URL(input.browserUrl).hostname;
|
|
205
|
-
} catch {
|
|
206
|
-
return { status: 'INSUFFICIENT_EVIDENCE', reason: 'The supplied browser URL is invalid.' };
|
|
207
|
-
}
|
|
208
|
-
if (!input.browserAllowedHosts?.includes(browserHost)) {
|
|
209
|
-
return { status: 'POLICY_BLOCKED', reason: 'Browser investigation requires an explicit allowed-host list.' };
|
|
210
|
-
}
|
|
211
|
-
const investigation = await DirectBrowserInvestigator.investigateUrl({
|
|
212
|
-
url: input.browserUrl,
|
|
213
|
-
executionId: `${input.executionId ?? 'trace'}-browser-investigation`,
|
|
214
|
-
timeoutMs: 10000,
|
|
215
|
-
});
|
|
216
|
-
const clip = (value: unknown, limit: number): string | undefined => {
|
|
217
|
-
const text = typeof value === 'string' ? value : value === undefined ? undefined : JSON.stringify(value);
|
|
218
|
-
if (text === undefined || text.length <= limit) return text;
|
|
219
|
-
return `${text.slice(0, limit)}\n[PIE browser evidence clipped]`;
|
|
220
|
-
};
|
|
221
|
-
return {
|
|
222
|
-
status: investigation.status,
|
|
223
|
-
pageUrl: investigation.pageUrl,
|
|
224
|
-
error: investigation.error,
|
|
225
|
-
domSnapshot: clip(investigation.domSnapshotRecord?.payload, 8000),
|
|
226
|
-
ariaSnapshot: clip(investigation.ariaSnapshotRecord?.payload, 8000),
|
|
227
|
-
recordKinds: investigation.records.map((record) => record.evidenceType).slice(0, 20),
|
|
228
|
-
};
|
|
229
|
-
},
|
|
230
|
-
inspect_repository: async () => ({
|
|
231
|
-
repository: intelligence.profile?.repository,
|
|
232
|
-
structure: intelligence.profile?.structure,
|
|
233
|
-
paths: intelligence.profile?.paths,
|
|
234
|
-
locatorConventions: intelligence.profile?.locatorConventions,
|
|
235
|
-
testConventions: intelligence.profile?.testConventions,
|
|
236
|
-
domainTerms: intelligence.profile?.domainTerms?.slice(0, 50),
|
|
237
|
-
}),
|
|
238
|
-
read_source_file: async (action) => {
|
|
239
|
-
const sourcePath = action.path ?? action.sourceFile;
|
|
240
|
-
if (!sourcePath) return { status: 'INPUT_REQUIRED', reason: 'A path or sourceFile is required.' };
|
|
241
|
-
const resolved = assertWorkspacePath(workspaceRoot, sourcePath, 'Source file');
|
|
242
|
-
const content = await fs.readFile(resolved, 'utf8');
|
|
243
|
-
const clipped = content.length > 16000 ? `${content.slice(0, 16000)}\n[PIE source evidence clipped]` : content;
|
|
244
|
-
return { status: 'FOUND', filePath: path.relative(workspaceRoot, resolved), content: EvidenceRedactor.redactText(clipped).redactedText };
|
|
245
|
-
},
|
|
246
|
-
search_repository: async (action) => {
|
|
247
|
-
if (!action.query) return { status: 'INPUT_REQUIRED', reason: 'A query is required.' };
|
|
248
|
-
return { status: 'FOUND', query: action.query, matches: await searchRepository(workspaceRoot, action.query) };
|
|
249
|
-
},
|
|
250
|
-
discover_source_mapping: async () => sourceMapping ?? {
|
|
251
|
-
status: 'NOT_FOUND',
|
|
252
|
-
reason: 'No unique source mapping was discovered.',
|
|
253
|
-
},
|
|
254
|
-
rank_locator_candidates: async () => ({
|
|
255
|
-
...(() => {
|
|
256
|
-
const domRoot = (correlated.context as unknown as { domRoot?: unknown }).domRoot;
|
|
257
|
-
if (!domRoot) return { status: 'NO_DOM_EVIDENCE', candidates: [], proposedLocator: null };
|
|
258
|
-
try {
|
|
259
|
-
const candidates = CandidateExtractor.extractCandidates(domRoot as Parameters<typeof CandidateExtractor.extractCandidates>[0], correlated.context.failedAction);
|
|
260
|
-
const ranked = RepairCandidateRanker.rank(
|
|
261
|
-
candidates,
|
|
262
|
-
correlated.context.failedAction.selector,
|
|
263
|
-
correlated.context.failedAction.action,
|
|
264
|
-
intelligence,
|
|
265
|
-
domRoot as Parameters<typeof CandidateExtractor.extractCandidates>[0],
|
|
266
|
-
correlated.context.failedAction.selector
|
|
267
|
-
);
|
|
268
|
-
const decision = AbstentionPolicy.evaluate(ranked);
|
|
269
|
-
return {
|
|
270
|
-
status: decision.shouldAbstain ? 'CANDIDATES_REQUIRE_REVIEW' : 'CANDIDATE_AVAILABLE',
|
|
271
|
-
proposedLocator: deterministicProposedLocator ?? null,
|
|
272
|
-
abstentionStage: decision.stage,
|
|
273
|
-
abstentionReason: decision.reason ?? null,
|
|
274
|
-
candidates: ranked.slice(0, 10).map((candidate) => ({
|
|
275
|
-
locator: candidate.locatorExpression,
|
|
276
|
-
score: candidate.breakdown.score,
|
|
277
|
-
identityMatch: candidate.breakdown.identityMatch,
|
|
278
|
-
matchCount: candidate.breakdown.matchCount,
|
|
279
|
-
element: {
|
|
280
|
-
tag: candidate.element.tag,
|
|
281
|
-
role: candidate.element.role,
|
|
282
|
-
accessibleName: candidate.element.accessibleName,
|
|
283
|
-
text: candidate.element.text,
|
|
284
|
-
attributes: candidate.element.attributes,
|
|
285
|
-
},
|
|
286
|
-
})),
|
|
287
|
-
};
|
|
288
|
-
} catch (error) {
|
|
289
|
-
return { status: 'CANDIDATE_EXTRACTION_FAILED', candidates: [], proposedLocator: deterministicProposedLocator ?? null, reason: error instanceof Error ? error.message : String(error) };
|
|
290
|
-
}
|
|
291
|
-
})(),
|
|
292
|
-
}),
|
|
293
|
-
build_repair_proposal: async (action) => ({
|
|
294
|
-
status: 'PROPOSAL_ONLY',
|
|
295
|
-
target: action.target ?? null,
|
|
296
|
-
locator: action.locator ?? deterministicProposedLocator ?? null,
|
|
297
|
-
message: 'PIE has not modified or executed any source. The model must return the final schema-validated proposal.',
|
|
298
|
-
}),
|
|
299
|
-
run_target_test: async (action) => {
|
|
300
|
-
if (input.enableTestExecution !== true) return { status: 'DISABLED', reason: 'Targeted test execution requires explicit opt-in.' };
|
|
301
|
-
const target = action.testFile ?? action.sourceFile;
|
|
302
|
-
if (!target) return { status: 'INPUT_REQUIRED', reason: 'testFile or sourceFile is required.' };
|
|
303
|
-
const testPath = assertWorkspacePath(workspaceRoot, target, 'Target test');
|
|
304
|
-
const relativeTest = path.relative(workspaceRoot, testPath);
|
|
305
|
-
const args = ['playwright', 'test', relativeTest, '--reporter=line'];
|
|
306
|
-
if (action.configPath) args.push('--config', path.relative(workspaceRoot, assertWorkspacePath(workspaceRoot, action.configPath, 'Playwright config')));
|
|
307
|
-
const execution = await ExecutionRunner.run('npx', { cwd: workspaceRoot, args, timeoutMs: 120000 });
|
|
308
|
-
const clip = (value: string): string => EvidenceRedactor.redactText(value.slice(0, 8000)).redactedText;
|
|
309
|
-
return { status: execution.exitCode === 0 ? 'PASSED' : 'FAILED', exitCode: execution.exitCode, timedOut: execution.timedOut, stdout: clip(execution.stdout), stderr: clip(execution.stderr), durationMs: execution.durationMs };
|
|
310
|
-
},
|
|
311
|
-
verify_repair: async () => ({
|
|
312
|
-
status: 'NOT_EXECUTED',
|
|
313
|
-
reason: 'Trace proposal workflow is proposal-only. Isolated verification requires the repair workflow and explicit execution policy.',
|
|
314
|
-
}),
|
|
315
|
-
},
|
|
316
|
-
});
|
|
317
|
-
return { context, correlated, bundle, agent };
|
|
318
|
-
}
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import fsSync from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { ArchiveReader } from '../trace/archive-reader.js';
|
|
5
|
+
import { TraceParser } from '../trace/trace-parser.js';
|
|
6
|
+
import { EvidenceCorrelator } from '../diagnostics/evidence-correlator.js';
|
|
7
|
+
import { parseExternalFailureEvidence, supplementDiagnosticContext } from '../diagnostics/external-failure-evidence.js';
|
|
8
|
+
import { buildEvidenceBundle } from './evidence-bundle.js';
|
|
9
|
+
import { runRepairProposalAgent, RepairAgentResult } from './repair-agent.js';
|
|
10
|
+
import { OpenAiCompatibleProposalProvider, ProposalProvider } from './model-provider.js';
|
|
11
|
+
import { EvidenceBundle } from './llm-proposal.js';
|
|
12
|
+
import { CorrelatedEvidence } from '../diagnostics/evidence-correlator.js';
|
|
13
|
+
import { DiagnosticContext } from '../core/types/diagnostic-context.js';
|
|
14
|
+
import { bootstrapProjectIntelligence, getHistoricalSelectorScore } from './project-intelligence.js';
|
|
15
|
+
import { DiagnosticReportGenerator } from '../diagnostics/diagnostic-report.js';
|
|
16
|
+
import { discoverRepairSourceMapping } from '../repair/source-mapper.js';
|
|
17
|
+
import { DirectBrowserInvestigator } from '../reproduce/direct-browser-investigator.js';
|
|
18
|
+
import { CandidateExtractor } from '../selector/candidate-extractor.js';
|
|
19
|
+
import { RepairCandidateRanker } from '../selector/repair-candidate-ranker.js';
|
|
20
|
+
import { AbstentionPolicy } from '../selector/abstention-policy.js';
|
|
21
|
+
import { EvidenceRedactor } from '../evidence/evidence-redactor.js';
|
|
22
|
+
import { ExecutionRunner } from '../sandbox/execution-runner.js';
|
|
23
|
+
|
|
24
|
+
export interface TraceProposalWorkflowInput {
|
|
25
|
+
tracePath: string;
|
|
26
|
+
workspaceRoot?: string | undefined;
|
|
27
|
+
errorContextPath?: string | undefined;
|
|
28
|
+
executionId?: string | undefined;
|
|
29
|
+
testIdentity?: string | undefined;
|
|
30
|
+
enableBrowserInvestigation?: boolean | undefined;
|
|
31
|
+
browserUrl?: string | undefined;
|
|
32
|
+
browserAllowedHosts?: string[] | undefined;
|
|
33
|
+
enableTestExecution?: boolean | undefined;
|
|
34
|
+
provider?: ProposalProvider | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TraceProposalWorkflowResult {
|
|
38
|
+
context: DiagnosticContext;
|
|
39
|
+
correlated: CorrelatedEvidence;
|
|
40
|
+
bundle: EvidenceBundle;
|
|
41
|
+
agent: RepairAgentResult;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function assertWorkspacePath(root: string, target: string, label: string): string {
|
|
45
|
+
const canonicalRoot = fsSync.realpathSync(root);
|
|
46
|
+
const resolved = path.resolve(root, target);
|
|
47
|
+
if (!fsSync.existsSync(resolved)) throw new Error(`${label} not found: '${resolved}'`);
|
|
48
|
+
const canonicalTarget = fsSync.realpathSync(resolved);
|
|
49
|
+
const relative = path.relative(canonicalRoot, canonicalTarget);
|
|
50
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error(`${label} escapes workspace root: '${target}'`);
|
|
51
|
+
return canonicalTarget;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveReadOnlyPath(root: string, target: string, label: string): string {
|
|
55
|
+
const resolved = path.isAbsolute(target) ? target : path.resolve(root, target);
|
|
56
|
+
if (!fsSync.existsSync(resolved)) throw new Error(`${label} not found: '${resolved}'`);
|
|
57
|
+
return fsSync.realpathSync(resolved);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function searchRepository(root: string, query: string): Promise<Array<{ filePath: string; line: number; snippet: string }>> {
|
|
61
|
+
const ignored = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', 'test-results', 'playwright-report', '.pie', '.pw-intelligence']);
|
|
62
|
+
const results: Array<{ filePath: string; line: number; snippet: string }> = [];
|
|
63
|
+
const walk = async (directory: string, depth: number): Promise<void> => {
|
|
64
|
+
if (depth > 6 || results.length >= 30) return;
|
|
65
|
+
let entries;
|
|
66
|
+
try { entries = await fs.readdir(directory, { withFileTypes: true }); } catch { return; }
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (results.length >= 30 || ignored.has(entry.name)) continue;
|
|
69
|
+
const full = path.join(directory, entry.name);
|
|
70
|
+
if (entry.isDirectory()) { await walk(full, depth + 1); continue; }
|
|
71
|
+
if (!/\.(?:ts|tsx|js|jsx|json|md|yaml|yml)$/i.test(entry.name)) continue;
|
|
72
|
+
let content: string;
|
|
73
|
+
try { content = await fs.readFile(full, 'utf8'); } catch { continue; }
|
|
74
|
+
const lines = content.split(/\r?\n/);
|
|
75
|
+
lines.forEach((line, index) => {
|
|
76
|
+
if (results.length >= 30 || !line.toLowerCase().includes(query.toLowerCase())) return;
|
|
77
|
+
results.push({ filePath: path.relative(root, full), line: index + 1, snippet: EvidenceRedactor.redactText(line.slice(0, 500)).redactedText });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
await walk(root, 0);
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function configuredProvider(): ProposalProvider | undefined {
|
|
86
|
+
const apiKey = process.env.PIE_LLM_API_KEY ?? process.env.OPENAI_API_KEY ?? process.env.DEEPSEEK_API_KEY;
|
|
87
|
+
const endpoint = process.env.PIE_LLM_ENDPOINT ?? (process.env.OPENAI_API_KEY ? 'https://api.openai.com/v1/chat/completions' : (apiKey ? 'https://api.deepseek.com/chat/completions' : undefined));
|
|
88
|
+
const model = process.env.PIE_LLM_MODEL ?? (process.env.OPENAI_API_KEY ? 'gpt-4o' : (apiKey ? 'deepseek-chat' : undefined));
|
|
89
|
+
if (!endpoint || !model) return undefined;
|
|
90
|
+
const timeoutRaw = process.env.PIE_LLM_TIMEOUT_MS;
|
|
91
|
+
const timeoutMs = timeoutRaw ? Number(timeoutRaw) : undefined;
|
|
92
|
+
return new OpenAiCompatibleProposalProvider({
|
|
93
|
+
endpoint,
|
|
94
|
+
model,
|
|
95
|
+
...(apiKey ? { apiKey } : {}),
|
|
96
|
+
...(Number.isFinite(timeoutMs) && timeoutMs && timeoutMs > 0 ? { timeoutMs } : {}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Runs one bounded, redaction-aware model proposal step from a trace archive.
|
|
102
|
+
* Deterministic classification executes before the provider and the returned
|
|
103
|
+
* proposal is never executed or promoted by this function.
|
|
104
|
+
*/
|
|
105
|
+
export async function runTraceProposalWorkflow(input: TraceProposalWorkflowInput): Promise<TraceProposalWorkflowResult> {
|
|
106
|
+
const workspaceRoot = path.resolve(input.workspaceRoot ?? process.cwd());
|
|
107
|
+
const tracePath = resolveReadOnlyPath(workspaceRoot, input.tracePath, 'Trace archive');
|
|
108
|
+
const { traceEntryName, networkEntryName, traceEntryNames, networkEntryNames } = ArchiveReader.findTraceFileEntries(tracePath);
|
|
109
|
+
if (!traceEntryName) throw new Error(`No .trace event stream found in archive '${tracePath}'`);
|
|
110
|
+
|
|
111
|
+
const traceText = await ArchiveReader.readEntriesAsText(tracePath, traceEntryNames ?? [traceEntryName]);
|
|
112
|
+
const networkText = (networkEntryNames ?? (networkEntryName ? [networkEntryName] : [])).length > 0
|
|
113
|
+
? await ArchiveReader.readEntriesAsText(tracePath, networkEntryNames ?? [networkEntryName!])
|
|
114
|
+
: '';
|
|
115
|
+
let context = TraceParser.assembleDiagnosticContext(traceText.split('\n'), networkText.split('\n'));
|
|
116
|
+
if (input.errorContextPath) {
|
|
117
|
+
const errorContextPath = resolveReadOnlyPath(workspaceRoot, input.errorContextPath, 'Error context');
|
|
118
|
+
context = supplementDiagnosticContext(context, parseExternalFailureEvidence(await fs.readFile(errorContextPath, 'utf8')));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const correlated = EvidenceCorrelator.correlate(context);
|
|
122
|
+
const intelligence = await bootstrapProjectIntelligence(workspaceRoot);
|
|
123
|
+
const deterministicProposedLocator = DiagnosticReportGenerator.extractProposedLocator(correlated, intelligence);
|
|
124
|
+
const sourceMapping = await discoverRepairSourceMapping({ workspaceRoot, failedAction: correlated.context.failedAction });
|
|
125
|
+
const sourceContext: EvidenceBundle['sourceContext'] = [];
|
|
126
|
+
const profile = intelligence.profile;
|
|
127
|
+
const projectContext = profile
|
|
128
|
+
? {
|
|
129
|
+
preferredLocator: profile.locatorConventions.preferred,
|
|
130
|
+
usesPageObjects: profile.structure.usesPageObjects,
|
|
131
|
+
usesCustomFixtures: profile.structure.usesCustomFixtures,
|
|
132
|
+
usesStorageState: profile.structure.usesStorageState,
|
|
133
|
+
domainTerms: profile.domainTerms.slice(0, 30),
|
|
134
|
+
...(context.failedAction.selector
|
|
135
|
+
? { historicalSelectorReliability: getHistoricalSelectorScore(intelligence, context.failedAction.selector) }
|
|
136
|
+
: {}),
|
|
137
|
+
}
|
|
138
|
+
: undefined;
|
|
139
|
+
if (projectContext) {
|
|
140
|
+
sourceContext.push({ filePath: '.pie/project-intelligence.json', content: JSON.stringify(projectContext, null, 2) });
|
|
141
|
+
}
|
|
142
|
+
if (sourceMapping?.pomFilePath) {
|
|
143
|
+
try {
|
|
144
|
+
const source = await fs.readFile(sourceMapping.pomFilePath, 'utf8');
|
|
145
|
+
const lines = source.split(/\r?\n/);
|
|
146
|
+
const center = correlated.context.failedAction.line ?? 1;
|
|
147
|
+
const start = Math.max(0, center - 8);
|
|
148
|
+
const end = Math.min(lines.length, center + 8);
|
|
149
|
+
sourceContext.push({
|
|
150
|
+
filePath: path.relative(workspaceRoot, sourceMapping.pomFilePath),
|
|
151
|
+
startLine: start + 1,
|
|
152
|
+
endLine: end,
|
|
153
|
+
content: lines.slice(start, end).join('\n'),
|
|
154
|
+
});
|
|
155
|
+
} catch {
|
|
156
|
+
// Source mapping remains available as an action observation even when a snippet cannot be read.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const bundle = buildEvidenceBundle({
|
|
160
|
+
executionId: input.executionId ?? `trace-${Date.now()}`,
|
|
161
|
+
testIdentity: input.testIdentity ?? context.failedAction.sourceFile ?? 'unknown-test',
|
|
162
|
+
context,
|
|
163
|
+
failureCategory: correlated.classification.category,
|
|
164
|
+
redactionComplete: true,
|
|
165
|
+
sourceContext,
|
|
166
|
+
artifacts: [
|
|
167
|
+
{ kind: 'trace', reference: path.relative(workspaceRoot, tracePath), redacted: true },
|
|
168
|
+
...(networkEntryName ? [{ kind: 'network' as const, reference: path.relative(workspaceRoot, networkEntryName), redacted: true }] : []),
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
const agent = await runRepairProposalAgent({
|
|
172
|
+
context,
|
|
173
|
+
bundle,
|
|
174
|
+
provider: input.provider ?? configuredProvider(),
|
|
175
|
+
allowModelEscalation: true,
|
|
176
|
+
strategy: correlated.classification.category === 'LOCATOR_DRIFT'
|
|
177
|
+
&& correlated.classification.confidence >= 0.85
|
|
178
|
+
&& Boolean(deterministicProposedLocator)
|
|
179
|
+
? 'single-shot'
|
|
180
|
+
: 'agent',
|
|
181
|
+
maxDurationMs: (() => {
|
|
182
|
+
const raw = Number(process.env.PIE_LLM_AGENT_MAX_DURATION_MS ?? 180000);
|
|
183
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 180000;
|
|
184
|
+
})(),
|
|
185
|
+
// Step budget prevents deadlocks; token limits remain provider/user controlled.
|
|
186
|
+
maxSteps: 6,
|
|
187
|
+
actionHandlers: {
|
|
188
|
+
inspect_evidence: async () => ({
|
|
189
|
+
category: correlated.classification.category,
|
|
190
|
+
failedAction: correlated.context.failedAction,
|
|
191
|
+
evidenceCompleteness: correlated.context.evidenceCompleteness,
|
|
192
|
+
networkEvents: correlated.context.networkEvents.length,
|
|
193
|
+
consoleEvents: correlated.context.consoleEvents.length,
|
|
194
|
+
}),
|
|
195
|
+
inspect_browser_state: async () => {
|
|
196
|
+
if (input.enableBrowserInvestigation !== true) {
|
|
197
|
+
return { status: 'DISABLED', reason: 'Browser investigation requires explicit opt-in.' };
|
|
198
|
+
}
|
|
199
|
+
if (!input.browserUrl) {
|
|
200
|
+
return { status: 'INSUFFICIENT_EVIDENCE', reason: 'No explicit browser URL was supplied.' };
|
|
201
|
+
}
|
|
202
|
+
let browserHost: string;
|
|
203
|
+
try {
|
|
204
|
+
browserHost = new URL(input.browserUrl).hostname;
|
|
205
|
+
} catch {
|
|
206
|
+
return { status: 'INSUFFICIENT_EVIDENCE', reason: 'The supplied browser URL is invalid.' };
|
|
207
|
+
}
|
|
208
|
+
if (!input.browserAllowedHosts?.includes(browserHost)) {
|
|
209
|
+
return { status: 'POLICY_BLOCKED', reason: 'Browser investigation requires an explicit allowed-host list.' };
|
|
210
|
+
}
|
|
211
|
+
const investigation = await DirectBrowserInvestigator.investigateUrl({
|
|
212
|
+
url: input.browserUrl,
|
|
213
|
+
executionId: `${input.executionId ?? 'trace'}-browser-investigation`,
|
|
214
|
+
timeoutMs: 10000,
|
|
215
|
+
});
|
|
216
|
+
const clip = (value: unknown, limit: number): string | undefined => {
|
|
217
|
+
const text = typeof value === 'string' ? value : value === undefined ? undefined : JSON.stringify(value);
|
|
218
|
+
if (text === undefined || text.length <= limit) return text;
|
|
219
|
+
return `${text.slice(0, limit)}\n[PIE browser evidence clipped]`;
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
status: investigation.status,
|
|
223
|
+
pageUrl: investigation.pageUrl,
|
|
224
|
+
error: investigation.error,
|
|
225
|
+
domSnapshot: clip(investigation.domSnapshotRecord?.payload, 8000),
|
|
226
|
+
ariaSnapshot: clip(investigation.ariaSnapshotRecord?.payload, 8000),
|
|
227
|
+
recordKinds: investigation.records.map((record) => record.evidenceType).slice(0, 20),
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
inspect_repository: async () => ({
|
|
231
|
+
repository: intelligence.profile?.repository,
|
|
232
|
+
structure: intelligence.profile?.structure,
|
|
233
|
+
paths: intelligence.profile?.paths,
|
|
234
|
+
locatorConventions: intelligence.profile?.locatorConventions,
|
|
235
|
+
testConventions: intelligence.profile?.testConventions,
|
|
236
|
+
domainTerms: intelligence.profile?.domainTerms?.slice(0, 50),
|
|
237
|
+
}),
|
|
238
|
+
read_source_file: async (action) => {
|
|
239
|
+
const sourcePath = action.path ?? action.sourceFile;
|
|
240
|
+
if (!sourcePath) return { status: 'INPUT_REQUIRED', reason: 'A path or sourceFile is required.' };
|
|
241
|
+
const resolved = assertWorkspacePath(workspaceRoot, sourcePath, 'Source file');
|
|
242
|
+
const content = await fs.readFile(resolved, 'utf8');
|
|
243
|
+
const clipped = content.length > 16000 ? `${content.slice(0, 16000)}\n[PIE source evidence clipped]` : content;
|
|
244
|
+
return { status: 'FOUND', filePath: path.relative(workspaceRoot, resolved), content: EvidenceRedactor.redactText(clipped).redactedText };
|
|
245
|
+
},
|
|
246
|
+
search_repository: async (action) => {
|
|
247
|
+
if (!action.query) return { status: 'INPUT_REQUIRED', reason: 'A query is required.' };
|
|
248
|
+
return { status: 'FOUND', query: action.query, matches: await searchRepository(workspaceRoot, action.query) };
|
|
249
|
+
},
|
|
250
|
+
discover_source_mapping: async () => sourceMapping ?? {
|
|
251
|
+
status: 'NOT_FOUND',
|
|
252
|
+
reason: 'No unique source mapping was discovered.',
|
|
253
|
+
},
|
|
254
|
+
rank_locator_candidates: async () => ({
|
|
255
|
+
...(() => {
|
|
256
|
+
const domRoot = (correlated.context as unknown as { domRoot?: unknown }).domRoot;
|
|
257
|
+
if (!domRoot) return { status: 'NO_DOM_EVIDENCE', candidates: [], proposedLocator: null };
|
|
258
|
+
try {
|
|
259
|
+
const candidates = CandidateExtractor.extractCandidates(domRoot as Parameters<typeof CandidateExtractor.extractCandidates>[0], correlated.context.failedAction);
|
|
260
|
+
const ranked = RepairCandidateRanker.rank(
|
|
261
|
+
candidates,
|
|
262
|
+
correlated.context.failedAction.selector,
|
|
263
|
+
correlated.context.failedAction.action,
|
|
264
|
+
intelligence,
|
|
265
|
+
domRoot as Parameters<typeof CandidateExtractor.extractCandidates>[0],
|
|
266
|
+
correlated.context.failedAction.selector
|
|
267
|
+
);
|
|
268
|
+
const decision = AbstentionPolicy.evaluate(ranked);
|
|
269
|
+
return {
|
|
270
|
+
status: decision.shouldAbstain ? 'CANDIDATES_REQUIRE_REVIEW' : 'CANDIDATE_AVAILABLE',
|
|
271
|
+
proposedLocator: deterministicProposedLocator ?? null,
|
|
272
|
+
abstentionStage: decision.stage,
|
|
273
|
+
abstentionReason: decision.reason ?? null,
|
|
274
|
+
candidates: ranked.slice(0, 10).map((candidate) => ({
|
|
275
|
+
locator: candidate.locatorExpression,
|
|
276
|
+
score: candidate.breakdown.score,
|
|
277
|
+
identityMatch: candidate.breakdown.identityMatch,
|
|
278
|
+
matchCount: candidate.breakdown.matchCount,
|
|
279
|
+
element: {
|
|
280
|
+
tag: candidate.element.tag,
|
|
281
|
+
role: candidate.element.role,
|
|
282
|
+
accessibleName: candidate.element.accessibleName,
|
|
283
|
+
text: candidate.element.text,
|
|
284
|
+
attributes: candidate.element.attributes,
|
|
285
|
+
},
|
|
286
|
+
})),
|
|
287
|
+
};
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return { status: 'CANDIDATE_EXTRACTION_FAILED', candidates: [], proposedLocator: deterministicProposedLocator ?? null, reason: error instanceof Error ? error.message : String(error) };
|
|
290
|
+
}
|
|
291
|
+
})(),
|
|
292
|
+
}),
|
|
293
|
+
build_repair_proposal: async (action) => ({
|
|
294
|
+
status: 'PROPOSAL_ONLY',
|
|
295
|
+
target: action.target ?? null,
|
|
296
|
+
locator: action.locator ?? deterministicProposedLocator ?? null,
|
|
297
|
+
message: 'PIE has not modified or executed any source. The model must return the final schema-validated proposal.',
|
|
298
|
+
}),
|
|
299
|
+
run_target_test: async (action) => {
|
|
300
|
+
if (input.enableTestExecution !== true) return { status: 'DISABLED', reason: 'Targeted test execution requires explicit opt-in.' };
|
|
301
|
+
const target = action.testFile ?? action.sourceFile;
|
|
302
|
+
if (!target) return { status: 'INPUT_REQUIRED', reason: 'testFile or sourceFile is required.' };
|
|
303
|
+
const testPath = assertWorkspacePath(workspaceRoot, target, 'Target test');
|
|
304
|
+
const relativeTest = path.relative(workspaceRoot, testPath);
|
|
305
|
+
const args = ['playwright', 'test', relativeTest, '--reporter=line'];
|
|
306
|
+
if (action.configPath) args.push('--config', path.relative(workspaceRoot, assertWorkspacePath(workspaceRoot, action.configPath, 'Playwright config')));
|
|
307
|
+
const execution = await ExecutionRunner.run('npx', { cwd: workspaceRoot, args, timeoutMs: 120000 });
|
|
308
|
+
const clip = (value: string): string => EvidenceRedactor.redactText(value.slice(0, 8000)).redactedText;
|
|
309
|
+
return { status: execution.exitCode === 0 ? 'PASSED' : 'FAILED', exitCode: execution.exitCode, timedOut: execution.timedOut, stdout: clip(execution.stdout), stderr: clip(execution.stderr), durationMs: execution.durationMs };
|
|
310
|
+
},
|
|
311
|
+
verify_repair: async () => ({
|
|
312
|
+
status: 'NOT_EXECUTED',
|
|
313
|
+
reason: 'Trace proposal workflow is proposal-only. Isolated verification requires the repair workflow and explicit execution policy.',
|
|
314
|
+
}),
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
return { context, correlated, bundle, agent };
|
|
318
|
+
}
|