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.
- 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,282 +1,282 @@
|
|
|
1
|
-
import { Project } from 'ts-morph';
|
|
2
|
-
import {
|
|
3
|
-
Author,
|
|
4
|
-
AuthorInput,
|
|
5
|
-
CandidateChange,
|
|
6
|
-
FullCodeGenerationInput,
|
|
7
|
-
FullCodeGenerationProvider,
|
|
8
|
-
} from './author.js';
|
|
9
|
-
import { HybridAuthor } from './hybrid-author.js';
|
|
10
|
-
import {
|
|
11
|
-
buildTierL2SystemPrompt,
|
|
12
|
-
FullGenerationProposal,
|
|
13
|
-
normalizeProposalPath,
|
|
14
|
-
validateFullGenerationProposal,
|
|
15
|
-
} from './generation-contract.js';
|
|
16
|
-
import type { NamePlan, SymbolEntry } from './naming/name-plan.js';
|
|
17
|
-
import { NamePlanner } from './naming/name-planner.js';
|
|
18
|
-
import { toClassName, toSlug } from '../generator/architecture-generator.js';
|
|
19
|
-
|
|
20
|
-
export interface OpenAiCompatibleFullCodeProviderOptions {
|
|
21
|
-
endpoint: string;
|
|
22
|
-
model: string;
|
|
23
|
-
apiKey?: string | undefined;
|
|
24
|
-
timeoutMs?: number | undefined;
|
|
25
|
-
fetchImpl?: typeof fetch | undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Provider-neutral Tier-L2 full-code Playwright generation transport.
|
|
30
|
-
*/
|
|
31
|
-
export class OpenAiCompatibleFullCodeProvider implements FullCodeGenerationProvider {
|
|
32
|
-
public constructor(private readonly options: OpenAiCompatibleFullCodeProviderOptions) {}
|
|
33
|
-
|
|
34
|
-
public async generate(input: FullCodeGenerationInput): Promise<unknown> {
|
|
35
|
-
const controller = new AbortController();
|
|
36
|
-
const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs ?? 90_000);
|
|
37
|
-
try {
|
|
38
|
-
const response = await (this.options.fetchImpl ?? fetch)(this.options.endpoint, {
|
|
39
|
-
method: 'POST',
|
|
40
|
-
signal: controller.signal,
|
|
41
|
-
headers: {
|
|
42
|
-
'content-type': 'application/json',
|
|
43
|
-
...(this.options.apiKey ? { authorization: `Bearer ${this.options.apiKey}` } : {}),
|
|
44
|
-
},
|
|
45
|
-
body: JSON.stringify({
|
|
46
|
-
model: this.options.model,
|
|
47
|
-
temperature: 0.1,
|
|
48
|
-
response_format: { type: 'json_object' },
|
|
49
|
-
messages: [
|
|
50
|
-
{ role: 'system', content: buildTierL2SystemPrompt() },
|
|
51
|
-
{ role: 'user', content: JSON.stringify(input) },
|
|
52
|
-
],
|
|
53
|
-
}),
|
|
54
|
-
});
|
|
55
|
-
if (!response.ok) {
|
|
56
|
-
throw new Error(`Tier-L2 generation model endpoint returned HTTP ${response.status}`);
|
|
57
|
-
}
|
|
58
|
-
const payload = (await response.json()) as {
|
|
59
|
-
choices?: Array<{ message?: { content?: string | Array<{ text?: string }> | null } }>;
|
|
60
|
-
output_text?: string;
|
|
61
|
-
};
|
|
62
|
-
const content =
|
|
63
|
-
payload.output_text ??
|
|
64
|
-
(typeof payload.choices?.[0]?.message?.content === 'string'
|
|
65
|
-
? payload.choices[0].message.content
|
|
66
|
-
: payload.choices?.[0]?.message?.content?.map((part) => part.text ?? '').join(''));
|
|
67
|
-
if (!content) {
|
|
68
|
-
throw new Error('Tier-L2 generation model returned empty content');
|
|
69
|
-
}
|
|
70
|
-
return content;
|
|
71
|
-
} finally {
|
|
72
|
-
clearTimeout(timeout);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function configuredFullCodeProvider(): FullCodeGenerationProvider | undefined {
|
|
78
|
-
const liveAllowed = !process.env.VITEST || process.env.PIE_LIVE_LLM_TESTS === '1';
|
|
79
|
-
if (!liveAllowed) return undefined;
|
|
80
|
-
const apiKey = process.env.PIE_LLM_API_KEY ?? process.env.OPENAI_API_KEY ?? process.env.DEEPSEEK_API_KEY;
|
|
81
|
-
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));
|
|
82
|
-
const model = process.env.PIE_LLM_MODEL ?? (process.env.OPENAI_API_KEY ? 'gpt-4o' : (apiKey ? 'deepseek-chat' : undefined));
|
|
83
|
-
if (!endpoint || !model) return undefined;
|
|
84
|
-
return new OpenAiCompatibleFullCodeProvider({
|
|
85
|
-
endpoint,
|
|
86
|
-
model,
|
|
87
|
-
...(apiKey ? { apiKey } : {}),
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function inferRoleSuffix(propertyName: string): string {
|
|
92
|
-
const lower = propertyName.toLowerCase();
|
|
93
|
-
if (lower.includes('input') || lower.includes('field')) return 'textbox';
|
|
94
|
-
if (lower.includes('button') || lower.includes('btn')) return 'button';
|
|
95
|
-
if (lower.includes('checkbox')) return 'checkbox';
|
|
96
|
-
if (lower.includes('select')) return 'combobox';
|
|
97
|
-
return 'locator';
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function extractNamePlanFromGeneratedFiles(featureName: string, proposal: FullGenerationProposal): NamePlan {
|
|
101
|
-
const rawBase = toClassName(featureName);
|
|
102
|
-
let pageClassName = rawBase.endsWith('Page') ? rawBase : `${rawBase}Page`;
|
|
103
|
-
const symbols: SymbolEntry[] = [];
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
const project = new Project({ useInMemoryFileSystem: true });
|
|
107
|
-
for (const file of proposal.files) {
|
|
108
|
-
if (!/\.tsx?$/i.test(file.path)) continue;
|
|
109
|
-
const sf = project.createSourceFile(file.path, file.content, { overwrite: true });
|
|
110
|
-
const classes = sf.getClasses();
|
|
111
|
-
const pageClass = classes.find((c) => c.getName()?.endsWith('Page')) ?? classes[0];
|
|
112
|
-
if (!pageClass) continue;
|
|
113
|
-
const clsName = pageClass.getName();
|
|
114
|
-
if (clsName) {
|
|
115
|
-
pageClassName = clsName.endsWith('Page') ? clsName : `${clsName}Page`;
|
|
116
|
-
}
|
|
117
|
-
const props = pageClass
|
|
118
|
-
.getProperties()
|
|
119
|
-
.map((p) => p.getName())
|
|
120
|
-
.filter((p) => p !== 'page');
|
|
121
|
-
const methods = pageClass.getMethods().filter((m) => m.getName() !== 'goto');
|
|
122
|
-
const usedPropNames = new Set<string>();
|
|
123
|
-
|
|
124
|
-
for (let i = 0; i < methods.length; i += 1) {
|
|
125
|
-
const methodDecl = methods[i]!;
|
|
126
|
-
const method = methodDecl.getName();
|
|
127
|
-
const bodyText = methodDecl.getBodyText() ?? '';
|
|
128
|
-
const matchedProp = props.find((p) => !usedPropNames.has(p) && new RegExp(`\\bthis\\.${p}\\b`).test(bodyText));
|
|
129
|
-
const property = matchedProp ?? `${method}Locator`;
|
|
130
|
-
usedPropNames.add(property);
|
|
131
|
-
const verbMatch = method.match(/^([a-z]+)/);
|
|
132
|
-
symbols.push({
|
|
133
|
-
ref: `ast-${i + 1}`,
|
|
134
|
-
role: inferRoleSuffix(property),
|
|
135
|
-
property,
|
|
136
|
-
method,
|
|
137
|
-
verb: verbMatch?.[1] ?? 'click',
|
|
138
|
-
confidence: 0.9,
|
|
139
|
-
source: 'llm',
|
|
140
|
-
assumptions: [],
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
if (symbols.length > 0) break;
|
|
144
|
-
}
|
|
145
|
-
} catch {
|
|
146
|
-
// Fallback below if AST extraction fails
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const pageVariable = pageClassName[0]!.toLowerCase() + pageClassName.slice(1);
|
|
150
|
-
return {
|
|
151
|
-
planVersion: '1',
|
|
152
|
-
pageClassName,
|
|
153
|
-
pageVariable,
|
|
154
|
-
symbols,
|
|
155
|
-
assumptions: proposal.assumptions.map((a) => a.description),
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export interface LlmAuthorOptions {
|
|
160
|
-
provider?: FullCodeGenerationProvider | undefined;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export class LlmAuthor implements Author {
|
|
164
|
-
public readonly kind = 'llm';
|
|
165
|
-
private readonly hybridFallback = new HybridAuthor();
|
|
166
|
-
|
|
167
|
-
public constructor(private readonly options: LlmAuthorOptions = {}) {}
|
|
168
|
-
|
|
169
|
-
public async propose(input: AuthorInput): Promise<CandidateChange> {
|
|
170
|
-
const provider = input.fullCodeProvider ?? this.options.provider ?? configuredFullCodeProvider();
|
|
171
|
-
if (!provider) {
|
|
172
|
-
const hybridChange = await this.hybridFallback.propose(input);
|
|
173
|
-
return {
|
|
174
|
-
...hybridChange,
|
|
175
|
-
authorKind: 'llm',
|
|
176
|
-
fullCodeAuthoring: false,
|
|
177
|
-
usedFallback: true,
|
|
178
|
-
status: 'GENERATE',
|
|
179
|
-
rationale: 'No Tier-L2 full-code provider configured; fell back to deterministic HybridAuthor.',
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const generationInput: FullCodeGenerationInput = {
|
|
184
|
-
featureName: input.featureName,
|
|
185
|
-
mode: input.mode,
|
|
186
|
-
baseUrl: input.baseUrl,
|
|
187
|
-
requirements: input.requirements,
|
|
188
|
-
steps: input.steps,
|
|
189
|
-
profile: input.profile,
|
|
190
|
-
existingFiles: input.existingFiles,
|
|
191
|
-
exemplars: input.exemplars,
|
|
192
|
-
contextPacket: input.contextPacket,
|
|
193
|
-
workflowPlan: input.workflowPlan,
|
|
194
|
-
explorationBundle: input.explorationBundle,
|
|
195
|
-
scenarios: input.scenarios,
|
|
196
|
-
existingFileSnapshots: input.existingFileSnapshots,
|
|
197
|
-
allowedEvidenceIds: input.allowedEvidenceIds,
|
|
198
|
-
feedback: input.feedback,
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
let rawProposal: unknown;
|
|
202
|
-
try {
|
|
203
|
-
rawProposal = await provider.generate(generationInput);
|
|
204
|
-
} catch (error) {
|
|
205
|
-
const hybridChange = await this.hybridFallback.propose(input);
|
|
206
|
-
return {
|
|
207
|
-
...hybridChange,
|
|
208
|
-
authorKind: 'llm',
|
|
209
|
-
fullCodeAuthoring: false,
|
|
210
|
-
usedFallback: true,
|
|
211
|
-
status: 'GENERATE',
|
|
212
|
-
validationErrors: [`Tier-L2 provider failed (${error instanceof Error ? error.message : String(error)}); fell back to HybridAuthor.`],
|
|
213
|
-
rationale: `Tier-L2 provider unavailable; fell back to HybridAuthor (${error instanceof Error ? error.message : String(error)}).`,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const validation = validateFullGenerationProposal(rawProposal, {
|
|
218
|
-
allowedEvidenceIds: input.allowedEvidenceIds ? new Set(input.allowedEvidenceIds) : undefined,
|
|
219
|
-
existingFiles: input.existingFiles ? new Set(input.existingFiles) : undefined,
|
|
220
|
-
requireEvidenceReferences: input.requireEvidenceReferences,
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
if (!validation.isValid || !validation.proposal) {
|
|
224
|
-
return {
|
|
225
|
-
files: [],
|
|
226
|
-
symbols: NamePlanner.planNames(input.featureName, input.steps ?? [], { source: 'llm' }),
|
|
227
|
-
rationale: `Tier-L2 proposal rejected by contract validation: ${validation.errors.join('; ')}`,
|
|
228
|
-
authorKind: 'llm',
|
|
229
|
-
featureName: input.featureName,
|
|
230
|
-
slug: toSlug(input.featureName),
|
|
231
|
-
mode: input.mode,
|
|
232
|
-
status: 'ABSTAIN',
|
|
233
|
-
fullCodeAuthoring: true,
|
|
234
|
-
usedFallback: false,
|
|
235
|
-
validationErrors: validation.errors,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const proposal = validation.proposal;
|
|
240
|
-
if (proposal.status !== 'GENERATE') {
|
|
241
|
-
return {
|
|
242
|
-
files: [],
|
|
243
|
-
symbols: NamePlanner.planNames(input.featureName, input.steps ?? [], { source: 'llm' }),
|
|
244
|
-
rationale: proposal.summary,
|
|
245
|
-
authorKind: 'llm',
|
|
246
|
-
featureName: input.featureName,
|
|
247
|
-
slug: toSlug(input.featureName),
|
|
248
|
-
mode: input.mode,
|
|
249
|
-
status: proposal.status,
|
|
250
|
-
fullCodeAuthoring: true,
|
|
251
|
-
usedFallback: false,
|
|
252
|
-
proposal,
|
|
253
|
-
assumptions: proposal.assumptions.map((a) => a.description),
|
|
254
|
-
requiredClarifications: proposal.requiredClarifications,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const symbols = extractNamePlanFromGeneratedFiles(input.featureName, proposal);
|
|
259
|
-
return {
|
|
260
|
-
files: proposal.files.map((f) => ({
|
|
261
|
-
relativePath: normalizeProposalPath(f.path),
|
|
262
|
-
content: f.content,
|
|
263
|
-
purpose: f.rationale,
|
|
264
|
-
action: f.action === 'modify' ? 'MODIFY' : 'CREATE',
|
|
265
|
-
rationale: f.rationale,
|
|
266
|
-
evidenceReferences: f.evidenceReferences,
|
|
267
|
-
scenarioIds: f.scenarioIds,
|
|
268
|
-
})),
|
|
269
|
-
symbols,
|
|
270
|
-
rationale: proposal.summary,
|
|
271
|
-
authorKind: 'llm',
|
|
272
|
-
featureName: input.featureName,
|
|
273
|
-
slug: toSlug(input.featureName),
|
|
274
|
-
mode: input.mode,
|
|
275
|
-
status: 'GENERATE',
|
|
276
|
-
fullCodeAuthoring: true,
|
|
277
|
-
usedFallback: false,
|
|
278
|
-
proposal,
|
|
279
|
-
assumptions: proposal.assumptions.map((a) => a.description),
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
}
|
|
1
|
+
import { Project } from 'ts-morph';
|
|
2
|
+
import {
|
|
3
|
+
Author,
|
|
4
|
+
AuthorInput,
|
|
5
|
+
CandidateChange,
|
|
6
|
+
FullCodeGenerationInput,
|
|
7
|
+
FullCodeGenerationProvider,
|
|
8
|
+
} from './author.js';
|
|
9
|
+
import { HybridAuthor } from './hybrid-author.js';
|
|
10
|
+
import {
|
|
11
|
+
buildTierL2SystemPrompt,
|
|
12
|
+
FullGenerationProposal,
|
|
13
|
+
normalizeProposalPath,
|
|
14
|
+
validateFullGenerationProposal,
|
|
15
|
+
} from './generation-contract.js';
|
|
16
|
+
import type { NamePlan, SymbolEntry } from './naming/name-plan.js';
|
|
17
|
+
import { NamePlanner } from './naming/name-planner.js';
|
|
18
|
+
import { toClassName, toSlug } from '../generator/architecture-generator.js';
|
|
19
|
+
|
|
20
|
+
export interface OpenAiCompatibleFullCodeProviderOptions {
|
|
21
|
+
endpoint: string;
|
|
22
|
+
model: string;
|
|
23
|
+
apiKey?: string | undefined;
|
|
24
|
+
timeoutMs?: number | undefined;
|
|
25
|
+
fetchImpl?: typeof fetch | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Provider-neutral Tier-L2 full-code Playwright generation transport.
|
|
30
|
+
*/
|
|
31
|
+
export class OpenAiCompatibleFullCodeProvider implements FullCodeGenerationProvider {
|
|
32
|
+
public constructor(private readonly options: OpenAiCompatibleFullCodeProviderOptions) {}
|
|
33
|
+
|
|
34
|
+
public async generate(input: FullCodeGenerationInput): Promise<unknown> {
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timeout = setTimeout(() => controller.abort(), this.options.timeoutMs ?? 90_000);
|
|
37
|
+
try {
|
|
38
|
+
const response = await (this.options.fetchImpl ?? fetch)(this.options.endpoint, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
headers: {
|
|
42
|
+
'content-type': 'application/json',
|
|
43
|
+
...(this.options.apiKey ? { authorization: `Bearer ${this.options.apiKey}` } : {}),
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
model: this.options.model,
|
|
47
|
+
temperature: 0.1,
|
|
48
|
+
response_format: { type: 'json_object' },
|
|
49
|
+
messages: [
|
|
50
|
+
{ role: 'system', content: buildTierL2SystemPrompt() },
|
|
51
|
+
{ role: 'user', content: JSON.stringify(input) },
|
|
52
|
+
],
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
throw new Error(`Tier-L2 generation model endpoint returned HTTP ${response.status}`);
|
|
57
|
+
}
|
|
58
|
+
const payload = (await response.json()) as {
|
|
59
|
+
choices?: Array<{ message?: { content?: string | Array<{ text?: string }> | null } }>;
|
|
60
|
+
output_text?: string;
|
|
61
|
+
};
|
|
62
|
+
const content =
|
|
63
|
+
payload.output_text ??
|
|
64
|
+
(typeof payload.choices?.[0]?.message?.content === 'string'
|
|
65
|
+
? payload.choices[0].message.content
|
|
66
|
+
: payload.choices?.[0]?.message?.content?.map((part) => part.text ?? '').join(''));
|
|
67
|
+
if (!content) {
|
|
68
|
+
throw new Error('Tier-L2 generation model returned empty content');
|
|
69
|
+
}
|
|
70
|
+
return content;
|
|
71
|
+
} finally {
|
|
72
|
+
clearTimeout(timeout);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function configuredFullCodeProvider(): FullCodeGenerationProvider | undefined {
|
|
78
|
+
const liveAllowed = !process.env.VITEST || process.env.PIE_LIVE_LLM_TESTS === '1';
|
|
79
|
+
if (!liveAllowed) return undefined;
|
|
80
|
+
const apiKey = process.env.PIE_LLM_API_KEY ?? process.env.OPENAI_API_KEY ?? process.env.DEEPSEEK_API_KEY;
|
|
81
|
+
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));
|
|
82
|
+
const model = process.env.PIE_LLM_MODEL ?? (process.env.OPENAI_API_KEY ? 'gpt-4o' : (apiKey ? 'deepseek-chat' : undefined));
|
|
83
|
+
if (!endpoint || !model) return undefined;
|
|
84
|
+
return new OpenAiCompatibleFullCodeProvider({
|
|
85
|
+
endpoint,
|
|
86
|
+
model,
|
|
87
|
+
...(apiKey ? { apiKey } : {}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function inferRoleSuffix(propertyName: string): string {
|
|
92
|
+
const lower = propertyName.toLowerCase();
|
|
93
|
+
if (lower.includes('input') || lower.includes('field')) return 'textbox';
|
|
94
|
+
if (lower.includes('button') || lower.includes('btn')) return 'button';
|
|
95
|
+
if (lower.includes('checkbox')) return 'checkbox';
|
|
96
|
+
if (lower.includes('select')) return 'combobox';
|
|
97
|
+
return 'locator';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function extractNamePlanFromGeneratedFiles(featureName: string, proposal: FullGenerationProposal): NamePlan {
|
|
101
|
+
const rawBase = toClassName(featureName);
|
|
102
|
+
let pageClassName = rawBase.endsWith('Page') ? rawBase : `${rawBase}Page`;
|
|
103
|
+
const symbols: SymbolEntry[] = [];
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
107
|
+
for (const file of proposal.files) {
|
|
108
|
+
if (!/\.tsx?$/i.test(file.path)) continue;
|
|
109
|
+
const sf = project.createSourceFile(file.path, file.content, { overwrite: true });
|
|
110
|
+
const classes = sf.getClasses();
|
|
111
|
+
const pageClass = classes.find((c) => c.getName()?.endsWith('Page')) ?? classes[0];
|
|
112
|
+
if (!pageClass) continue;
|
|
113
|
+
const clsName = pageClass.getName();
|
|
114
|
+
if (clsName) {
|
|
115
|
+
pageClassName = clsName.endsWith('Page') ? clsName : `${clsName}Page`;
|
|
116
|
+
}
|
|
117
|
+
const props = pageClass
|
|
118
|
+
.getProperties()
|
|
119
|
+
.map((p) => p.getName())
|
|
120
|
+
.filter((p) => p !== 'page');
|
|
121
|
+
const methods = pageClass.getMethods().filter((m) => m.getName() !== 'goto');
|
|
122
|
+
const usedPropNames = new Set<string>();
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < methods.length; i += 1) {
|
|
125
|
+
const methodDecl = methods[i]!;
|
|
126
|
+
const method = methodDecl.getName();
|
|
127
|
+
const bodyText = methodDecl.getBodyText() ?? '';
|
|
128
|
+
const matchedProp = props.find((p) => !usedPropNames.has(p) && new RegExp(`\\bthis\\.${p}\\b`).test(bodyText));
|
|
129
|
+
const property = matchedProp ?? `${method}Locator`;
|
|
130
|
+
usedPropNames.add(property);
|
|
131
|
+
const verbMatch = method.match(/^([a-z]+)/);
|
|
132
|
+
symbols.push({
|
|
133
|
+
ref: `ast-${i + 1}`,
|
|
134
|
+
role: inferRoleSuffix(property),
|
|
135
|
+
property,
|
|
136
|
+
method,
|
|
137
|
+
verb: verbMatch?.[1] ?? 'click',
|
|
138
|
+
confidence: 0.9,
|
|
139
|
+
source: 'llm',
|
|
140
|
+
assumptions: [],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (symbols.length > 0) break;
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Fallback below if AST extraction fails
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const pageVariable = pageClassName[0]!.toLowerCase() + pageClassName.slice(1);
|
|
150
|
+
return {
|
|
151
|
+
planVersion: '1',
|
|
152
|
+
pageClassName,
|
|
153
|
+
pageVariable,
|
|
154
|
+
symbols,
|
|
155
|
+
assumptions: proposal.assumptions.map((a) => a.description),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface LlmAuthorOptions {
|
|
160
|
+
provider?: FullCodeGenerationProvider | undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export class LlmAuthor implements Author {
|
|
164
|
+
public readonly kind = 'llm';
|
|
165
|
+
private readonly hybridFallback = new HybridAuthor();
|
|
166
|
+
|
|
167
|
+
public constructor(private readonly options: LlmAuthorOptions = {}) {}
|
|
168
|
+
|
|
169
|
+
public async propose(input: AuthorInput): Promise<CandidateChange> {
|
|
170
|
+
const provider = input.fullCodeProvider ?? this.options.provider ?? configuredFullCodeProvider();
|
|
171
|
+
if (!provider) {
|
|
172
|
+
const hybridChange = await this.hybridFallback.propose(input);
|
|
173
|
+
return {
|
|
174
|
+
...hybridChange,
|
|
175
|
+
authorKind: 'llm',
|
|
176
|
+
fullCodeAuthoring: false,
|
|
177
|
+
usedFallback: true,
|
|
178
|
+
status: 'GENERATE',
|
|
179
|
+
rationale: 'No Tier-L2 full-code provider configured; fell back to deterministic HybridAuthor.',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const generationInput: FullCodeGenerationInput = {
|
|
184
|
+
featureName: input.featureName,
|
|
185
|
+
mode: input.mode,
|
|
186
|
+
baseUrl: input.baseUrl,
|
|
187
|
+
requirements: input.requirements,
|
|
188
|
+
steps: input.steps,
|
|
189
|
+
profile: input.profile,
|
|
190
|
+
existingFiles: input.existingFiles,
|
|
191
|
+
exemplars: input.exemplars,
|
|
192
|
+
contextPacket: input.contextPacket,
|
|
193
|
+
workflowPlan: input.workflowPlan,
|
|
194
|
+
explorationBundle: input.explorationBundle,
|
|
195
|
+
scenarios: input.scenarios,
|
|
196
|
+
existingFileSnapshots: input.existingFileSnapshots,
|
|
197
|
+
allowedEvidenceIds: input.allowedEvidenceIds,
|
|
198
|
+
feedback: input.feedback,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
let rawProposal: unknown;
|
|
202
|
+
try {
|
|
203
|
+
rawProposal = await provider.generate(generationInput);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
const hybridChange = await this.hybridFallback.propose(input);
|
|
206
|
+
return {
|
|
207
|
+
...hybridChange,
|
|
208
|
+
authorKind: 'llm',
|
|
209
|
+
fullCodeAuthoring: false,
|
|
210
|
+
usedFallback: true,
|
|
211
|
+
status: 'GENERATE',
|
|
212
|
+
validationErrors: [`Tier-L2 provider failed (${error instanceof Error ? error.message : String(error)}); fell back to HybridAuthor.`],
|
|
213
|
+
rationale: `Tier-L2 provider unavailable; fell back to HybridAuthor (${error instanceof Error ? error.message : String(error)}).`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const validation = validateFullGenerationProposal(rawProposal, {
|
|
218
|
+
allowedEvidenceIds: input.allowedEvidenceIds ? new Set(input.allowedEvidenceIds) : undefined,
|
|
219
|
+
existingFiles: input.existingFiles ? new Set(input.existingFiles) : undefined,
|
|
220
|
+
requireEvidenceReferences: input.requireEvidenceReferences,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (!validation.isValid || !validation.proposal) {
|
|
224
|
+
return {
|
|
225
|
+
files: [],
|
|
226
|
+
symbols: NamePlanner.planNames(input.featureName, input.steps ?? [], { source: 'llm' }),
|
|
227
|
+
rationale: `Tier-L2 proposal rejected by contract validation: ${validation.errors.join('; ')}`,
|
|
228
|
+
authorKind: 'llm',
|
|
229
|
+
featureName: input.featureName,
|
|
230
|
+
slug: toSlug(input.featureName),
|
|
231
|
+
mode: input.mode,
|
|
232
|
+
status: 'ABSTAIN',
|
|
233
|
+
fullCodeAuthoring: true,
|
|
234
|
+
usedFallback: false,
|
|
235
|
+
validationErrors: validation.errors,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const proposal = validation.proposal;
|
|
240
|
+
if (proposal.status !== 'GENERATE') {
|
|
241
|
+
return {
|
|
242
|
+
files: [],
|
|
243
|
+
symbols: NamePlanner.planNames(input.featureName, input.steps ?? [], { source: 'llm' }),
|
|
244
|
+
rationale: proposal.summary,
|
|
245
|
+
authorKind: 'llm',
|
|
246
|
+
featureName: input.featureName,
|
|
247
|
+
slug: toSlug(input.featureName),
|
|
248
|
+
mode: input.mode,
|
|
249
|
+
status: proposal.status,
|
|
250
|
+
fullCodeAuthoring: true,
|
|
251
|
+
usedFallback: false,
|
|
252
|
+
proposal,
|
|
253
|
+
assumptions: proposal.assumptions.map((a) => a.description),
|
|
254
|
+
requiredClarifications: proposal.requiredClarifications,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const symbols = extractNamePlanFromGeneratedFiles(input.featureName, proposal);
|
|
259
|
+
return {
|
|
260
|
+
files: proposal.files.map((f) => ({
|
|
261
|
+
relativePath: normalizeProposalPath(f.path),
|
|
262
|
+
content: f.content,
|
|
263
|
+
purpose: f.rationale,
|
|
264
|
+
action: f.action === 'modify' ? 'MODIFY' : 'CREATE',
|
|
265
|
+
rationale: f.rationale,
|
|
266
|
+
evidenceReferences: f.evidenceReferences,
|
|
267
|
+
scenarioIds: f.scenarioIds,
|
|
268
|
+
})),
|
|
269
|
+
symbols,
|
|
270
|
+
rationale: proposal.summary,
|
|
271
|
+
authorKind: 'llm',
|
|
272
|
+
featureName: input.featureName,
|
|
273
|
+
slug: toSlug(input.featureName),
|
|
274
|
+
mode: input.mode,
|
|
275
|
+
status: 'GENERATE',
|
|
276
|
+
fullCodeAuthoring: true,
|
|
277
|
+
usedFallback: false,
|
|
278
|
+
proposal,
|
|
279
|
+
assumptions: proposal.assumptions.map((a) => a.description),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
export const SymbolSourceSchema = z.enum(['llm', 'heuristic']);
|
|
4
|
-
export type SymbolSource = z.infer<typeof SymbolSourceSchema>;
|
|
5
|
-
|
|
6
|
-
export const SymbolEntrySchema = z.object({
|
|
7
|
-
ref: z.string().min(1),
|
|
8
|
-
role: z.string().optional(),
|
|
9
|
-
accessibleName: z.string().optional(),
|
|
10
|
-
property: z.string().min(1),
|
|
11
|
-
method: z.string().min(1),
|
|
12
|
-
verb: z.string().min(1),
|
|
13
|
-
confidence: z.number().min(0).max(1),
|
|
14
|
-
source: SymbolSourceSchema,
|
|
15
|
-
assumptions: z.array(z.string()).default([]),
|
|
16
|
-
frame: z.string().optional(),
|
|
17
|
-
});
|
|
18
|
-
export type SymbolEntry = z.infer<typeof SymbolEntrySchema>;
|
|
19
|
-
|
|
20
|
-
export const NamePlanSchema = z.object({
|
|
21
|
-
planVersion: z.literal('1').default('1'),
|
|
22
|
-
pageClassName: z.string().min(1),
|
|
23
|
-
pageVariable: z.string().min(1),
|
|
24
|
-
symbols: z.array(SymbolEntrySchema),
|
|
25
|
-
assumptions: z.array(z.string()).default([]),
|
|
26
|
-
});
|
|
27
|
-
export type NamePlan = z.infer<typeof NamePlanSchema>;
|
|
28
|
-
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const SymbolSourceSchema = z.enum(['llm', 'heuristic']);
|
|
4
|
+
export type SymbolSource = z.infer<typeof SymbolSourceSchema>;
|
|
5
|
+
|
|
6
|
+
export const SymbolEntrySchema = z.object({
|
|
7
|
+
ref: z.string().min(1),
|
|
8
|
+
role: z.string().optional(),
|
|
9
|
+
accessibleName: z.string().optional(),
|
|
10
|
+
property: z.string().min(1),
|
|
11
|
+
method: z.string().min(1),
|
|
12
|
+
verb: z.string().min(1),
|
|
13
|
+
confidence: z.number().min(0).max(1),
|
|
14
|
+
source: SymbolSourceSchema,
|
|
15
|
+
assumptions: z.array(z.string()).default([]),
|
|
16
|
+
frame: z.string().optional(),
|
|
17
|
+
});
|
|
18
|
+
export type SymbolEntry = z.infer<typeof SymbolEntrySchema>;
|
|
19
|
+
|
|
20
|
+
export const NamePlanSchema = z.object({
|
|
21
|
+
planVersion: z.literal('1').default('1'),
|
|
22
|
+
pageClassName: z.string().min(1),
|
|
23
|
+
pageVariable: z.string().min(1),
|
|
24
|
+
symbols: z.array(SymbolEntrySchema),
|
|
25
|
+
assumptions: z.array(z.string()).default([]),
|
|
26
|
+
});
|
|
27
|
+
export type NamePlan = z.infer<typeof NamePlanSchema>;
|
|
28
|
+
|