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,300 +1,300 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { DiagnosticContext, FailureCategory } from '../core/types/diagnostic-context.js';
|
|
5
|
-
import { EvidenceBundle, EvidenceBundleSchema } from './llm-proposal.js';
|
|
6
|
-
import { EvidenceRedactor } from '../evidence/evidence-redactor.js';
|
|
7
|
-
import type { BrownfieldRepositoryGraph } from './repository-profile.js';
|
|
8
|
-
|
|
9
|
-
export interface EvidenceBundleBuildInput {
|
|
10
|
-
executionId: string;
|
|
11
|
-
testIdentity: string;
|
|
12
|
-
context: DiagnosticContext;
|
|
13
|
-
failureCategory?: FailureCategory | string | undefined;
|
|
14
|
-
sourceContext?: EvidenceBundle['sourceContext'] | undefined;
|
|
15
|
-
artifacts?: EvidenceBundle['artifacts'] | undefined;
|
|
16
|
-
redactionComplete: boolean;
|
|
17
|
-
limitations?: string[] | undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Builds the only payload that may cross the model-provider boundary. The
|
|
22
|
-
* builder carries references to artifacts rather than reading or uploading
|
|
23
|
-
* trace archives, and requires the caller to explicitly attest redaction.
|
|
24
|
-
*/
|
|
25
|
-
export function buildEvidenceBundle(input: EvidenceBundleBuildInput): EvidenceBundle {
|
|
26
|
-
const limitations = [...(input.limitations ?? [])];
|
|
27
|
-
if (!input.context.evidenceCompleteness.domAvailable) limitations.push('DOM/accessibility snapshot is unavailable');
|
|
28
|
-
if (!input.context.evidenceCompleteness.networkAvailable) limitations.push('Network timeline is unavailable');
|
|
29
|
-
if (!input.context.evidenceCompleteness.sourceAvailable) limitations.push('Source location is unavailable');
|
|
30
|
-
if (!input.redactionComplete) limitations.push('Caller has not attested that evidence is redaction-complete');
|
|
31
|
-
|
|
32
|
-
const redact = (value: string | undefined): string | undefined => value === undefined ? undefined : EvidenceRedactor.redactText(value).redactedText;
|
|
33
|
-
const diagnostic = {
|
|
34
|
-
playwrightVersion: input.context.playwrightVersion,
|
|
35
|
-
traceFormatVersion: input.context.traceFormatVersion,
|
|
36
|
-
failedAction: {
|
|
37
|
-
callId: input.context.failedAction.callId,
|
|
38
|
-
action: input.context.failedAction.action,
|
|
39
|
-
...(redact(input.context.failedAction.selector) ? { selector: redact(input.context.failedAction.selector) } : {}),
|
|
40
|
-
...(redact(input.context.failedAction.sourceFile) ? { sourceFile: redact(input.context.failedAction.sourceFile) } : {}),
|
|
41
|
-
...(input.context.failedAction.line !== undefined ? { line: input.context.failedAction.line } : {}),
|
|
42
|
-
...(input.context.failedAction.column !== undefined ? { column: input.context.failedAction.column } : {}),
|
|
43
|
-
...(redact(input.context.failedAction.errorMessage) ? { errorMessage: redact(input.context.failedAction.errorMessage) } : {}),
|
|
44
|
-
...(redact(input.context.failedAction.errorStack) ? { errorStack: redact(input.context.failedAction.errorStack) } : {}),
|
|
45
|
-
...(input.context.failedAction.durationMs !== undefined ? { durationMs: input.context.failedAction.durationMs } : {}),
|
|
46
|
-
...(input.context.failedAction.logs ? { logs: input.context.failedAction.logs.map((log) => EvidenceRedactor.redactText(log).redactedText) } : {}),
|
|
47
|
-
},
|
|
48
|
-
snapshots: {
|
|
49
|
-
...(input.context.snapshots.before ? { before: EvidenceRedactor.redactDomContent(input.context.snapshots.before) } : {}),
|
|
50
|
-
...(input.context.snapshots.action ? { action: EvidenceRedactor.redactDomContent(input.context.snapshots.action) } : {}),
|
|
51
|
-
...(input.context.snapshots.after ? { after: EvidenceRedactor.redactDomContent(input.context.snapshots.after) } : {}),
|
|
52
|
-
},
|
|
53
|
-
networkEvents: input.context.networkEvents.slice(0, 100).map((event) => ({
|
|
54
|
-
method: event.method,
|
|
55
|
-
url: EvidenceRedactor.redactText(event.url).redactedText,
|
|
56
|
-
status: event.status,
|
|
57
|
-
...(event.statusText ? { statusText: EvidenceRedactor.redactText(event.statusText).redactedText } : {}),
|
|
58
|
-
...(event.errorText ? { errorText: EvidenceRedactor.redactText(event.errorText).redactedText } : {}),
|
|
59
|
-
})),
|
|
60
|
-
consoleEvents: input.context.consoleEvents.slice(0, 100).map((event) => ({
|
|
61
|
-
type: event.type,
|
|
62
|
-
text: EvidenceRedactor.redactText(event.text).redactedText,
|
|
63
|
-
})),
|
|
64
|
-
evidenceCompleteness: input.context.evidenceCompleteness,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return EvidenceBundleSchema.parse({
|
|
68
|
-
bundleId: `bundle-${input.executionId}`,
|
|
69
|
-
executionId: input.executionId,
|
|
70
|
-
testIdentity: input.testIdentity,
|
|
71
|
-
failedActionId: input.context.failedAction.callId,
|
|
72
|
-
failureCategory: input.failureCategory,
|
|
73
|
-
sourceContext: (input.sourceContext ?? []).map((source) => ({
|
|
74
|
-
...source,
|
|
75
|
-
content: EvidenceRedactor.redactText(source.content).redactedText,
|
|
76
|
-
})),
|
|
77
|
-
artifacts: input.artifacts ?? [],
|
|
78
|
-
diagnostic,
|
|
79
|
-
redactionComplete: input.redactionComplete,
|
|
80
|
-
limitations: [...new Set(limitations)],
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export const VerificationLayerStatusSchema = z.enum([
|
|
85
|
-
'PASS',
|
|
86
|
-
'FAIL',
|
|
87
|
-
'NOT_VERIFIED',
|
|
88
|
-
'NOT_APPLICABLE',
|
|
89
|
-
]);
|
|
90
|
-
export type VerificationLayerStatus = z.infer<typeof VerificationLayerStatusSchema>;
|
|
91
|
-
|
|
92
|
-
export const ExplorationFindingCategorySchema = z.enum([
|
|
93
|
-
'SPEC_VIOLATION',
|
|
94
|
-
'RUNTIME_ERROR',
|
|
95
|
-
'AUTOMATION_GAP',
|
|
96
|
-
'OBSERVED_BEHAVIOR',
|
|
97
|
-
'NOT_VERIFIED',
|
|
98
|
-
]);
|
|
99
|
-
export type ExplorationFindingCategory = z.infer<typeof ExplorationFindingCategorySchema>;
|
|
100
|
-
|
|
101
|
-
export const VerifiedLocatorEvidenceSchema = z.object({
|
|
102
|
-
id: z.string().min(1),
|
|
103
|
-
route: z.string().min(1),
|
|
104
|
-
strategy: z.enum(['role', 'label', 'placeholder', 'testId', 'text', 'css', 'frame']),
|
|
105
|
-
expression: z.string().min(1),
|
|
106
|
-
role: z.string().optional(),
|
|
107
|
-
accessibleName: z.string().optional(),
|
|
108
|
-
label: z.string().optional(),
|
|
109
|
-
placeholder: z.string().optional(),
|
|
110
|
-
testId: z.string().optional(),
|
|
111
|
-
frameSelector: z.string().optional(),
|
|
112
|
-
matchCount: z.number().int().min(0),
|
|
113
|
-
visible: z.boolean(),
|
|
114
|
-
enabled: z.boolean(),
|
|
115
|
-
confidence: z.number().min(0).max(1),
|
|
116
|
-
timestamp: z.string(),
|
|
117
|
-
});
|
|
118
|
-
export type VerifiedLocatorEvidence = z.infer<typeof VerifiedLocatorEvidenceSchema>;
|
|
119
|
-
|
|
120
|
-
export const ExplorationNetworkRecordSchema = z.object({
|
|
121
|
-
id: z.string().min(1),
|
|
122
|
-
scenarioId: z.string().optional(),
|
|
123
|
-
method: z.string(),
|
|
124
|
-
url: z.string(),
|
|
125
|
-
status: z.number().int(),
|
|
126
|
-
durationMs: z.number().optional(),
|
|
127
|
-
isError: z.boolean(),
|
|
128
|
-
resourceType: z.string().optional(),
|
|
129
|
-
errorText: z.string().optional(),
|
|
130
|
-
timestamp: z.string(),
|
|
131
|
-
});
|
|
132
|
-
export type ExplorationNetworkRecord = z.infer<typeof ExplorationNetworkRecordSchema>;
|
|
133
|
-
|
|
134
|
-
export const ExplorationActionRecordSchema = z.object({
|
|
135
|
-
id: z.string().min(1),
|
|
136
|
-
scenarioId: z.string().min(1),
|
|
137
|
-
stepIndex: z.number().int().min(0),
|
|
138
|
-
action: z.string().min(1),
|
|
139
|
-
targetLocatorId: z.string().optional(),
|
|
140
|
-
targetExpression: z.string().optional(),
|
|
141
|
-
valueRedacted: z.string().optional(),
|
|
142
|
-
routeBefore: z.string(),
|
|
143
|
-
routeAfter: z.string(),
|
|
144
|
-
domBeforeSummary: z.string(),
|
|
145
|
-
domAfterSummary: z.string(),
|
|
146
|
-
domDiff: z.array(z.string()).default([]),
|
|
147
|
-
networkIds: z.array(z.string()).default([]),
|
|
148
|
-
consoleErrors: z.array(z.string()).default([]),
|
|
149
|
-
screenshotPath: z.string().optional(),
|
|
150
|
-
status: z.enum(['EXECUTED', 'FAILED', 'BLOCKED_BY_POLICY', 'SKIPPED']),
|
|
151
|
-
timestamp: z.string(),
|
|
152
|
-
});
|
|
153
|
-
export type ExplorationActionRecord = z.infer<typeof ExplorationActionRecordSchema>;
|
|
154
|
-
|
|
155
|
-
export const ExplorationFindingSchema = z.object({
|
|
156
|
-
id: z.string().min(1),
|
|
157
|
-
category: ExplorationFindingCategorySchema,
|
|
158
|
-
scenarioId: z.string().optional(),
|
|
159
|
-
evidenceIds: z.array(z.string()).default([]),
|
|
160
|
-
expectedSource: z.string().optional(),
|
|
161
|
-
expectedBehavior: z.string().optional(),
|
|
162
|
-
actualBehavior: z.string().min(1),
|
|
163
|
-
confidence: z.number().min(0).max(1),
|
|
164
|
-
severity: z.enum(['critical', 'high', 'medium', 'low', 'info']).default('medium'),
|
|
165
|
-
});
|
|
166
|
-
export type ExplorationFinding = z.infer<typeof ExplorationFindingSchema>;
|
|
167
|
-
|
|
168
|
-
export const ScenarioExplorationStatusSchema = z.object({
|
|
169
|
-
scenarioId: z.string().min(1),
|
|
170
|
-
status: z.enum(['VERIFIED', 'BLOCKED', 'DEFECT_DETECTED', 'NOT_VERIFIED', 'PARTIAL']),
|
|
171
|
-
verificationLayers: z.object({
|
|
172
|
-
UI: VerificationLayerStatusSchema,
|
|
173
|
-
API: VerificationLayerStatusSchema,
|
|
174
|
-
CLIENT_RELOAD: VerificationLayerStatusSchema,
|
|
175
|
-
DB: VerificationLayerStatusSchema,
|
|
176
|
-
}),
|
|
177
|
-
reason: z.string().optional(),
|
|
178
|
-
evidenceIds: z.array(z.string()).default([]),
|
|
179
|
-
});
|
|
180
|
-
export type ScenarioExplorationStatus = z.infer<typeof ScenarioExplorationStatusSchema>;
|
|
181
|
-
|
|
182
|
-
export const ExplorationRouteObservationSchema = z.object({
|
|
183
|
-
route: z.string().min(1),
|
|
184
|
-
title: z.string().optional(),
|
|
185
|
-
discoveredAt: z.string(),
|
|
186
|
-
locatorIds: z.array(z.string()).default([]),
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
export const EvidenceIndexEntrySchema = z.object({
|
|
190
|
-
id: z.string().min(1),
|
|
191
|
-
kind: z.string().min(1),
|
|
192
|
-
source: z.string().min(1),
|
|
193
|
-
timestamp: z.string(),
|
|
194
|
-
confidence: z.number().min(0).max(1),
|
|
195
|
-
scenarioIds: z.array(z.string()).default([]),
|
|
196
|
-
description: z.string(),
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
export const ExplorationContextBundleSchema = z.object({
|
|
200
|
-
bundleVersion: z.literal('1.0').default('1.0'),
|
|
201
|
-
bundleId: z.string().min(1),
|
|
202
|
-
createdAt: z.string(),
|
|
203
|
-
topology: z.enum(['monorepo', 'standalone', 'source-only']),
|
|
204
|
-
runtimeVerified: z.boolean(),
|
|
205
|
-
baseUrl: z.string().optional(),
|
|
206
|
-
authentication: z.object({
|
|
207
|
-
configured: z.boolean(),
|
|
208
|
-
method: z.enum(['none', 'storage-state', 'login-flow', 'existing-fixture']),
|
|
209
|
-
authenticated: z.boolean(),
|
|
210
|
-
secretsExposed: z.literal(false).default(false),
|
|
211
|
-
}),
|
|
212
|
-
requirements: z.object({
|
|
213
|
-
featureName: z.string().min(1),
|
|
214
|
-
requirementIds: z.array(z.string()).default([]),
|
|
215
|
-
scenarioIds: z.array(z.string()).default([]),
|
|
216
|
-
workflowPlanHash: z.string().optional(),
|
|
217
|
-
}),
|
|
218
|
-
routes: z.array(ExplorationRouteObservationSchema).default([]),
|
|
219
|
-
locators: z.array(VerifiedLocatorEvidenceSchema).default([]),
|
|
220
|
-
actions: z.array(ExplorationActionRecordSchema).default([]),
|
|
221
|
-
network: z.array(ExplorationNetworkRecordSchema).default([]),
|
|
222
|
-
consoleErrors: z.array(z.string()).default([]),
|
|
223
|
-
persistenceObservations: z.array(z.object({
|
|
224
|
-
scenarioId: z.string(),
|
|
225
|
-
clientReloadPreserved: z.boolean().optional(),
|
|
226
|
-
dbStatus: VerificationLayerStatusSchema,
|
|
227
|
-
note: z.string(),
|
|
228
|
-
})).default([]),
|
|
229
|
-
scenarioStatuses: z.array(ScenarioExplorationStatusSchema).default([]),
|
|
230
|
-
findings: z.array(ExplorationFindingSchema).default([]),
|
|
231
|
-
repositoryGraph: z.custom<BrownfieldRepositoryGraph>().optional(),
|
|
232
|
-
evidenceIndex: z.array(EvidenceIndexEntrySchema).default([]),
|
|
233
|
-
limitations: z.array(z.string()).default([]),
|
|
234
|
-
redactionComplete: z.literal(true).default(true),
|
|
235
|
-
});
|
|
236
|
-
export type ExplorationContextBundle = z.infer<typeof ExplorationContextBundleSchema>;
|
|
237
|
-
|
|
238
|
-
export function sanitizeUrlSecrets(rawUrl: string): string {
|
|
239
|
-
const redactedText = EvidenceRedactor.redactText(rawUrl).redactedText;
|
|
240
|
-
try {
|
|
241
|
-
const parsed = new URL(redactedText);
|
|
242
|
-
const sensitiveParamRe = /^(?:token|access_token|id_token|refresh_token|auth|secret|password|passwd|api_?key|key|session|jwt|code)$/i;
|
|
243
|
-
let changed = false;
|
|
244
|
-
for (const paramKey of [...parsed.searchParams.keys()]) {
|
|
245
|
-
if (sensitiveParamRe.test(paramKey)) {
|
|
246
|
-
parsed.searchParams.set(paramKey, '[REDACTED]');
|
|
247
|
-
changed = true;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return changed ? parsed.toString() : redactedText;
|
|
251
|
-
} catch {
|
|
252
|
-
return redactedText.replace(/([?&](?:token|access_token|id_token|refresh_token|auth|secret|password|api_?key|key|session|jwt)=)[^&\s]+/gi, '$1[REDACTED]');
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export function redactExplorationBundle(bundle: ExplorationContextBundle): ExplorationContextBundle {
|
|
257
|
-
const redact = (value: string): string => EvidenceRedactor.redactText(value).redactedText;
|
|
258
|
-
return ExplorationContextBundleSchema.parse({
|
|
259
|
-
...bundle,
|
|
260
|
-
...(bundle.baseUrl ? { baseUrl: sanitizeUrlSecrets(bundle.baseUrl) } : {}),
|
|
261
|
-
authentication: {
|
|
262
|
-
...bundle.authentication,
|
|
263
|
-
secretsExposed: false,
|
|
264
|
-
},
|
|
265
|
-
actions: bundle.actions.map((a) => ({
|
|
266
|
-
...a,
|
|
267
|
-
domBeforeSummary: EvidenceRedactor.redactDomContent(a.domBeforeSummary),
|
|
268
|
-
domAfterSummary: EvidenceRedactor.redactDomContent(a.domAfterSummary),
|
|
269
|
-
domDiff: a.domDiff.map(redact),
|
|
270
|
-
consoleErrors: a.consoleErrors.map(redact),
|
|
271
|
-
...(a.valueRedacted ? { valueRedacted: redact(a.valueRedacted) } : {}),
|
|
272
|
-
})),
|
|
273
|
-
network: bundle.network.map((n) => ({
|
|
274
|
-
...n,
|
|
275
|
-
url: sanitizeUrlSecrets(n.url),
|
|
276
|
-
...(n.errorText ? { errorText: redact(n.errorText) } : {}),
|
|
277
|
-
})),
|
|
278
|
-
consoleErrors: bundle.consoleErrors.map(redact),
|
|
279
|
-
findings: bundle.findings.map((f) => ({
|
|
280
|
-
...f,
|
|
281
|
-
actualBehavior: redact(f.actualBehavior),
|
|
282
|
-
...(f.expectedBehavior ? { expectedBehavior: redact(f.expectedBehavior) } : {}),
|
|
283
|
-
})),
|
|
284
|
-
redactionComplete: true,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
export async function saveExplorationContextBundle(
|
|
289
|
-
bundlePath: string,
|
|
290
|
-
bundle: ExplorationContextBundle,
|
|
291
|
-
): Promise<void> {
|
|
292
|
-
const clean = redactExplorationBundle(bundle);
|
|
293
|
-
await fs.mkdir(path.dirname(bundlePath), { recursive: true });
|
|
294
|
-
await fs.writeFile(bundlePath, `${JSON.stringify(clean, null, 2)}\n`, 'utf8');
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export async function loadExplorationContextBundle(bundlePath: string): Promise<ExplorationContextBundle> {
|
|
298
|
-
const raw = await fs.readFile(bundlePath, 'utf8');
|
|
299
|
-
return ExplorationContextBundleSchema.parse(JSON.parse(raw));
|
|
300
|
-
}
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { DiagnosticContext, FailureCategory } from '../core/types/diagnostic-context.js';
|
|
5
|
+
import { EvidenceBundle, EvidenceBundleSchema } from './llm-proposal.js';
|
|
6
|
+
import { EvidenceRedactor } from '../evidence/evidence-redactor.js';
|
|
7
|
+
import type { BrownfieldRepositoryGraph } from './repository-profile.js';
|
|
8
|
+
|
|
9
|
+
export interface EvidenceBundleBuildInput {
|
|
10
|
+
executionId: string;
|
|
11
|
+
testIdentity: string;
|
|
12
|
+
context: DiagnosticContext;
|
|
13
|
+
failureCategory?: FailureCategory | string | undefined;
|
|
14
|
+
sourceContext?: EvidenceBundle['sourceContext'] | undefined;
|
|
15
|
+
artifacts?: EvidenceBundle['artifacts'] | undefined;
|
|
16
|
+
redactionComplete: boolean;
|
|
17
|
+
limitations?: string[] | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds the only payload that may cross the model-provider boundary. The
|
|
22
|
+
* builder carries references to artifacts rather than reading or uploading
|
|
23
|
+
* trace archives, and requires the caller to explicitly attest redaction.
|
|
24
|
+
*/
|
|
25
|
+
export function buildEvidenceBundle(input: EvidenceBundleBuildInput): EvidenceBundle {
|
|
26
|
+
const limitations = [...(input.limitations ?? [])];
|
|
27
|
+
if (!input.context.evidenceCompleteness.domAvailable) limitations.push('DOM/accessibility snapshot is unavailable');
|
|
28
|
+
if (!input.context.evidenceCompleteness.networkAvailable) limitations.push('Network timeline is unavailable');
|
|
29
|
+
if (!input.context.evidenceCompleteness.sourceAvailable) limitations.push('Source location is unavailable');
|
|
30
|
+
if (!input.redactionComplete) limitations.push('Caller has not attested that evidence is redaction-complete');
|
|
31
|
+
|
|
32
|
+
const redact = (value: string | undefined): string | undefined => value === undefined ? undefined : EvidenceRedactor.redactText(value).redactedText;
|
|
33
|
+
const diagnostic = {
|
|
34
|
+
playwrightVersion: input.context.playwrightVersion,
|
|
35
|
+
traceFormatVersion: input.context.traceFormatVersion,
|
|
36
|
+
failedAction: {
|
|
37
|
+
callId: input.context.failedAction.callId,
|
|
38
|
+
action: input.context.failedAction.action,
|
|
39
|
+
...(redact(input.context.failedAction.selector) ? { selector: redact(input.context.failedAction.selector) } : {}),
|
|
40
|
+
...(redact(input.context.failedAction.sourceFile) ? { sourceFile: redact(input.context.failedAction.sourceFile) } : {}),
|
|
41
|
+
...(input.context.failedAction.line !== undefined ? { line: input.context.failedAction.line } : {}),
|
|
42
|
+
...(input.context.failedAction.column !== undefined ? { column: input.context.failedAction.column } : {}),
|
|
43
|
+
...(redact(input.context.failedAction.errorMessage) ? { errorMessage: redact(input.context.failedAction.errorMessage) } : {}),
|
|
44
|
+
...(redact(input.context.failedAction.errorStack) ? { errorStack: redact(input.context.failedAction.errorStack) } : {}),
|
|
45
|
+
...(input.context.failedAction.durationMs !== undefined ? { durationMs: input.context.failedAction.durationMs } : {}),
|
|
46
|
+
...(input.context.failedAction.logs ? { logs: input.context.failedAction.logs.map((log) => EvidenceRedactor.redactText(log).redactedText) } : {}),
|
|
47
|
+
},
|
|
48
|
+
snapshots: {
|
|
49
|
+
...(input.context.snapshots.before ? { before: EvidenceRedactor.redactDomContent(input.context.snapshots.before) } : {}),
|
|
50
|
+
...(input.context.snapshots.action ? { action: EvidenceRedactor.redactDomContent(input.context.snapshots.action) } : {}),
|
|
51
|
+
...(input.context.snapshots.after ? { after: EvidenceRedactor.redactDomContent(input.context.snapshots.after) } : {}),
|
|
52
|
+
},
|
|
53
|
+
networkEvents: input.context.networkEvents.slice(0, 100).map((event) => ({
|
|
54
|
+
method: event.method,
|
|
55
|
+
url: EvidenceRedactor.redactText(event.url).redactedText,
|
|
56
|
+
status: event.status,
|
|
57
|
+
...(event.statusText ? { statusText: EvidenceRedactor.redactText(event.statusText).redactedText } : {}),
|
|
58
|
+
...(event.errorText ? { errorText: EvidenceRedactor.redactText(event.errorText).redactedText } : {}),
|
|
59
|
+
})),
|
|
60
|
+
consoleEvents: input.context.consoleEvents.slice(0, 100).map((event) => ({
|
|
61
|
+
type: event.type,
|
|
62
|
+
text: EvidenceRedactor.redactText(event.text).redactedText,
|
|
63
|
+
})),
|
|
64
|
+
evidenceCompleteness: input.context.evidenceCompleteness,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return EvidenceBundleSchema.parse({
|
|
68
|
+
bundleId: `bundle-${input.executionId}`,
|
|
69
|
+
executionId: input.executionId,
|
|
70
|
+
testIdentity: input.testIdentity,
|
|
71
|
+
failedActionId: input.context.failedAction.callId,
|
|
72
|
+
failureCategory: input.failureCategory,
|
|
73
|
+
sourceContext: (input.sourceContext ?? []).map((source) => ({
|
|
74
|
+
...source,
|
|
75
|
+
content: EvidenceRedactor.redactText(source.content).redactedText,
|
|
76
|
+
})),
|
|
77
|
+
artifacts: input.artifacts ?? [],
|
|
78
|
+
diagnostic,
|
|
79
|
+
redactionComplete: input.redactionComplete,
|
|
80
|
+
limitations: [...new Set(limitations)],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const VerificationLayerStatusSchema = z.enum([
|
|
85
|
+
'PASS',
|
|
86
|
+
'FAIL',
|
|
87
|
+
'NOT_VERIFIED',
|
|
88
|
+
'NOT_APPLICABLE',
|
|
89
|
+
]);
|
|
90
|
+
export type VerificationLayerStatus = z.infer<typeof VerificationLayerStatusSchema>;
|
|
91
|
+
|
|
92
|
+
export const ExplorationFindingCategorySchema = z.enum([
|
|
93
|
+
'SPEC_VIOLATION',
|
|
94
|
+
'RUNTIME_ERROR',
|
|
95
|
+
'AUTOMATION_GAP',
|
|
96
|
+
'OBSERVED_BEHAVIOR',
|
|
97
|
+
'NOT_VERIFIED',
|
|
98
|
+
]);
|
|
99
|
+
export type ExplorationFindingCategory = z.infer<typeof ExplorationFindingCategorySchema>;
|
|
100
|
+
|
|
101
|
+
export const VerifiedLocatorEvidenceSchema = z.object({
|
|
102
|
+
id: z.string().min(1),
|
|
103
|
+
route: z.string().min(1),
|
|
104
|
+
strategy: z.enum(['role', 'label', 'placeholder', 'testId', 'text', 'css', 'frame']),
|
|
105
|
+
expression: z.string().min(1),
|
|
106
|
+
role: z.string().optional(),
|
|
107
|
+
accessibleName: z.string().optional(),
|
|
108
|
+
label: z.string().optional(),
|
|
109
|
+
placeholder: z.string().optional(),
|
|
110
|
+
testId: z.string().optional(),
|
|
111
|
+
frameSelector: z.string().optional(),
|
|
112
|
+
matchCount: z.number().int().min(0),
|
|
113
|
+
visible: z.boolean(),
|
|
114
|
+
enabled: z.boolean(),
|
|
115
|
+
confidence: z.number().min(0).max(1),
|
|
116
|
+
timestamp: z.string(),
|
|
117
|
+
});
|
|
118
|
+
export type VerifiedLocatorEvidence = z.infer<typeof VerifiedLocatorEvidenceSchema>;
|
|
119
|
+
|
|
120
|
+
export const ExplorationNetworkRecordSchema = z.object({
|
|
121
|
+
id: z.string().min(1),
|
|
122
|
+
scenarioId: z.string().optional(),
|
|
123
|
+
method: z.string(),
|
|
124
|
+
url: z.string(),
|
|
125
|
+
status: z.number().int(),
|
|
126
|
+
durationMs: z.number().optional(),
|
|
127
|
+
isError: z.boolean(),
|
|
128
|
+
resourceType: z.string().optional(),
|
|
129
|
+
errorText: z.string().optional(),
|
|
130
|
+
timestamp: z.string(),
|
|
131
|
+
});
|
|
132
|
+
export type ExplorationNetworkRecord = z.infer<typeof ExplorationNetworkRecordSchema>;
|
|
133
|
+
|
|
134
|
+
export const ExplorationActionRecordSchema = z.object({
|
|
135
|
+
id: z.string().min(1),
|
|
136
|
+
scenarioId: z.string().min(1),
|
|
137
|
+
stepIndex: z.number().int().min(0),
|
|
138
|
+
action: z.string().min(1),
|
|
139
|
+
targetLocatorId: z.string().optional(),
|
|
140
|
+
targetExpression: z.string().optional(),
|
|
141
|
+
valueRedacted: z.string().optional(),
|
|
142
|
+
routeBefore: z.string(),
|
|
143
|
+
routeAfter: z.string(),
|
|
144
|
+
domBeforeSummary: z.string(),
|
|
145
|
+
domAfterSummary: z.string(),
|
|
146
|
+
domDiff: z.array(z.string()).default([]),
|
|
147
|
+
networkIds: z.array(z.string()).default([]),
|
|
148
|
+
consoleErrors: z.array(z.string()).default([]),
|
|
149
|
+
screenshotPath: z.string().optional(),
|
|
150
|
+
status: z.enum(['EXECUTED', 'FAILED', 'BLOCKED_BY_POLICY', 'SKIPPED']),
|
|
151
|
+
timestamp: z.string(),
|
|
152
|
+
});
|
|
153
|
+
export type ExplorationActionRecord = z.infer<typeof ExplorationActionRecordSchema>;
|
|
154
|
+
|
|
155
|
+
export const ExplorationFindingSchema = z.object({
|
|
156
|
+
id: z.string().min(1),
|
|
157
|
+
category: ExplorationFindingCategorySchema,
|
|
158
|
+
scenarioId: z.string().optional(),
|
|
159
|
+
evidenceIds: z.array(z.string()).default([]),
|
|
160
|
+
expectedSource: z.string().optional(),
|
|
161
|
+
expectedBehavior: z.string().optional(),
|
|
162
|
+
actualBehavior: z.string().min(1),
|
|
163
|
+
confidence: z.number().min(0).max(1),
|
|
164
|
+
severity: z.enum(['critical', 'high', 'medium', 'low', 'info']).default('medium'),
|
|
165
|
+
});
|
|
166
|
+
export type ExplorationFinding = z.infer<typeof ExplorationFindingSchema>;
|
|
167
|
+
|
|
168
|
+
export const ScenarioExplorationStatusSchema = z.object({
|
|
169
|
+
scenarioId: z.string().min(1),
|
|
170
|
+
status: z.enum(['VERIFIED', 'BLOCKED', 'DEFECT_DETECTED', 'NOT_VERIFIED', 'PARTIAL']),
|
|
171
|
+
verificationLayers: z.object({
|
|
172
|
+
UI: VerificationLayerStatusSchema,
|
|
173
|
+
API: VerificationLayerStatusSchema,
|
|
174
|
+
CLIENT_RELOAD: VerificationLayerStatusSchema,
|
|
175
|
+
DB: VerificationLayerStatusSchema,
|
|
176
|
+
}),
|
|
177
|
+
reason: z.string().optional(),
|
|
178
|
+
evidenceIds: z.array(z.string()).default([]),
|
|
179
|
+
});
|
|
180
|
+
export type ScenarioExplorationStatus = z.infer<typeof ScenarioExplorationStatusSchema>;
|
|
181
|
+
|
|
182
|
+
export const ExplorationRouteObservationSchema = z.object({
|
|
183
|
+
route: z.string().min(1),
|
|
184
|
+
title: z.string().optional(),
|
|
185
|
+
discoveredAt: z.string(),
|
|
186
|
+
locatorIds: z.array(z.string()).default([]),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
export const EvidenceIndexEntrySchema = z.object({
|
|
190
|
+
id: z.string().min(1),
|
|
191
|
+
kind: z.string().min(1),
|
|
192
|
+
source: z.string().min(1),
|
|
193
|
+
timestamp: z.string(),
|
|
194
|
+
confidence: z.number().min(0).max(1),
|
|
195
|
+
scenarioIds: z.array(z.string()).default([]),
|
|
196
|
+
description: z.string(),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
export const ExplorationContextBundleSchema = z.object({
|
|
200
|
+
bundleVersion: z.literal('1.0').default('1.0'),
|
|
201
|
+
bundleId: z.string().min(1),
|
|
202
|
+
createdAt: z.string(),
|
|
203
|
+
topology: z.enum(['monorepo', 'standalone', 'source-only']),
|
|
204
|
+
runtimeVerified: z.boolean(),
|
|
205
|
+
baseUrl: z.string().optional(),
|
|
206
|
+
authentication: z.object({
|
|
207
|
+
configured: z.boolean(),
|
|
208
|
+
method: z.enum(['none', 'storage-state', 'login-flow', 'existing-fixture']),
|
|
209
|
+
authenticated: z.boolean(),
|
|
210
|
+
secretsExposed: z.literal(false).default(false),
|
|
211
|
+
}),
|
|
212
|
+
requirements: z.object({
|
|
213
|
+
featureName: z.string().min(1),
|
|
214
|
+
requirementIds: z.array(z.string()).default([]),
|
|
215
|
+
scenarioIds: z.array(z.string()).default([]),
|
|
216
|
+
workflowPlanHash: z.string().optional(),
|
|
217
|
+
}),
|
|
218
|
+
routes: z.array(ExplorationRouteObservationSchema).default([]),
|
|
219
|
+
locators: z.array(VerifiedLocatorEvidenceSchema).default([]),
|
|
220
|
+
actions: z.array(ExplorationActionRecordSchema).default([]),
|
|
221
|
+
network: z.array(ExplorationNetworkRecordSchema).default([]),
|
|
222
|
+
consoleErrors: z.array(z.string()).default([]),
|
|
223
|
+
persistenceObservations: z.array(z.object({
|
|
224
|
+
scenarioId: z.string(),
|
|
225
|
+
clientReloadPreserved: z.boolean().optional(),
|
|
226
|
+
dbStatus: VerificationLayerStatusSchema,
|
|
227
|
+
note: z.string(),
|
|
228
|
+
})).default([]),
|
|
229
|
+
scenarioStatuses: z.array(ScenarioExplorationStatusSchema).default([]),
|
|
230
|
+
findings: z.array(ExplorationFindingSchema).default([]),
|
|
231
|
+
repositoryGraph: z.custom<BrownfieldRepositoryGraph>().optional(),
|
|
232
|
+
evidenceIndex: z.array(EvidenceIndexEntrySchema).default([]),
|
|
233
|
+
limitations: z.array(z.string()).default([]),
|
|
234
|
+
redactionComplete: z.literal(true).default(true),
|
|
235
|
+
});
|
|
236
|
+
export type ExplorationContextBundle = z.infer<typeof ExplorationContextBundleSchema>;
|
|
237
|
+
|
|
238
|
+
export function sanitizeUrlSecrets(rawUrl: string): string {
|
|
239
|
+
const redactedText = EvidenceRedactor.redactText(rawUrl).redactedText;
|
|
240
|
+
try {
|
|
241
|
+
const parsed = new URL(redactedText);
|
|
242
|
+
const sensitiveParamRe = /^(?:token|access_token|id_token|refresh_token|auth|secret|password|passwd|api_?key|key|session|jwt|code)$/i;
|
|
243
|
+
let changed = false;
|
|
244
|
+
for (const paramKey of [...parsed.searchParams.keys()]) {
|
|
245
|
+
if (sensitiveParamRe.test(paramKey)) {
|
|
246
|
+
parsed.searchParams.set(paramKey, '[REDACTED]');
|
|
247
|
+
changed = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return changed ? parsed.toString() : redactedText;
|
|
251
|
+
} catch {
|
|
252
|
+
return redactedText.replace(/([?&](?:token|access_token|id_token|refresh_token|auth|secret|password|api_?key|key|session|jwt)=)[^&\s]+/gi, '$1[REDACTED]');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function redactExplorationBundle(bundle: ExplorationContextBundle): ExplorationContextBundle {
|
|
257
|
+
const redact = (value: string): string => EvidenceRedactor.redactText(value).redactedText;
|
|
258
|
+
return ExplorationContextBundleSchema.parse({
|
|
259
|
+
...bundle,
|
|
260
|
+
...(bundle.baseUrl ? { baseUrl: sanitizeUrlSecrets(bundle.baseUrl) } : {}),
|
|
261
|
+
authentication: {
|
|
262
|
+
...bundle.authentication,
|
|
263
|
+
secretsExposed: false,
|
|
264
|
+
},
|
|
265
|
+
actions: bundle.actions.map((a) => ({
|
|
266
|
+
...a,
|
|
267
|
+
domBeforeSummary: EvidenceRedactor.redactDomContent(a.domBeforeSummary),
|
|
268
|
+
domAfterSummary: EvidenceRedactor.redactDomContent(a.domAfterSummary),
|
|
269
|
+
domDiff: a.domDiff.map(redact),
|
|
270
|
+
consoleErrors: a.consoleErrors.map(redact),
|
|
271
|
+
...(a.valueRedacted ? { valueRedacted: redact(a.valueRedacted) } : {}),
|
|
272
|
+
})),
|
|
273
|
+
network: bundle.network.map((n) => ({
|
|
274
|
+
...n,
|
|
275
|
+
url: sanitizeUrlSecrets(n.url),
|
|
276
|
+
...(n.errorText ? { errorText: redact(n.errorText) } : {}),
|
|
277
|
+
})),
|
|
278
|
+
consoleErrors: bundle.consoleErrors.map(redact),
|
|
279
|
+
findings: bundle.findings.map((f) => ({
|
|
280
|
+
...f,
|
|
281
|
+
actualBehavior: redact(f.actualBehavior),
|
|
282
|
+
...(f.expectedBehavior ? { expectedBehavior: redact(f.expectedBehavior) } : {}),
|
|
283
|
+
})),
|
|
284
|
+
redactionComplete: true,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function saveExplorationContextBundle(
|
|
289
|
+
bundlePath: string,
|
|
290
|
+
bundle: ExplorationContextBundle,
|
|
291
|
+
): Promise<void> {
|
|
292
|
+
const clean = redactExplorationBundle(bundle);
|
|
293
|
+
await fs.mkdir(path.dirname(bundlePath), { recursive: true });
|
|
294
|
+
await fs.writeFile(bundlePath, `${JSON.stringify(clean, null, 2)}\n`, 'utf8');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function loadExplorationContextBundle(bundlePath: string): Promise<ExplorationContextBundle> {
|
|
298
|
+
const raw = await fs.readFile(bundlePath, 'utf8');
|
|
299
|
+
return ExplorationContextBundleSchema.parse(JSON.parse(raw));
|
|
300
|
+
}
|