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,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
+