playwright-intelligence-engine 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +229 -229
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/core/version.d.ts +3 -3
  4. package/dist/index.js +81 -24
  5. package/package.json +76 -76
  6. package/src/authoring/author.ts +81 -81
  7. package/src/authoring/generation-contract.ts +283 -283
  8. package/src/authoring/hybrid-author.ts +115 -115
  9. package/src/authoring/index.ts +7 -7
  10. package/src/authoring/llm-author.ts +282 -282
  11. package/src/authoring/naming/name-plan.ts +28 -28
  12. package/src/authoring/naming/name-planner.ts +339 -339
  13. package/src/authoring/naming/naming-linter.ts +292 -292
  14. package/src/cli/index.ts +1238 -1172
  15. package/src/collectors/git-diff-collector.ts +190 -190
  16. package/src/core/version.ts +25 -25
  17. package/src/diagnostics/diagnostic-report.ts +204 -204
  18. package/src/diagnostics/failure-classifier.ts +701 -701
  19. package/src/evaluation/repository-runner.ts +197 -197
  20. package/src/gate/quality.ts +101 -101
  21. package/src/generator/architecture-generator.ts +582 -582
  22. package/src/generator/assertion-quality-gate.ts +88 -88
  23. package/src/generator/candidate-writer.ts +288 -288
  24. package/src/generator/test-generator.ts +52 -52
  25. package/src/index.ts +107 -107
  26. package/src/intelligence/evidence-bundle.ts +300 -300
  27. package/src/intelligence/generation-agent.ts +561 -561
  28. package/src/intelligence/generation-verifier.ts +1115 -1115
  29. package/src/intelligence/locator-disambiguation-engine.ts +331 -331
  30. package/src/intelligence/project-intelligence.ts +140 -140
  31. package/src/intelligence/repository-profile.ts +796 -796
  32. package/src/intelligence/trace-proposal-workflow.ts +318 -318
  33. package/src/intelligence/workflow-planner.ts +966 -966
  34. package/src/mcp/server.ts +970 -970
  35. package/src/perception/app-model.ts +32 -32
  36. package/src/perception/crawler.ts +263 -263
  37. package/src/perception/index.ts +3 -3
  38. package/src/repair/assertion-hasher.ts +386 -386
  39. package/src/repair/diff-allowlist.ts +612 -612
  40. package/src/repair/intent-validator.ts +761 -761
  41. package/src/repair/repair-planner.ts +467 -467
  42. package/src/repair/statement-patcher.ts +544 -544
  43. package/src/reproduce/interactive-workflow-runner.ts +828 -828
  44. package/src/sandbox/execution-runner.ts +408 -408
  45. package/src/selector/abstention-policy.ts +175 -175
  46. package/src/selector/candidate-extractor.ts +343 -343
  47. package/src/selector/repair-candidate-ranker.ts +92 -92
  48. 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
+ }