Skip to content

Commit d451d59

Browse files
authored
feat(agen): detailed guideline about realize phase. (#1283)
* feat(agen): detailed guideline about realize phase. * add test cases * fix format * fix orchestrateRealizeCorrectWithRetry
1 parent 03d17fe commit d451d59

28 files changed

Lines changed: 906 additions & 100 deletions

packages/agent/prompts/REALIZE_OPERATION_CORRECT.md

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export namespace IAutoBeRealizeOperationCorrectApplication {
4646

4747
## 4. Common Error Patterns
4848

49-
> **Reuse first**: When errors concentrate in `data:` or `select`/transform blocks, call `getRealizeCollectors`/`getRealizeTransformers` first. If a Transformer exists for the model you're querying, use `Transformer.select()` + `Transformer.transform()` — this is the **MOST COMMON fix** for "missing properties" errors. Similarly, `Collector.collect(...)` eliminates write-side errors.
49+
> **Reuse first — THE #1 FIX**: Call `getRealizeCollectors`/`getRealizeTransformers` BEFORE patching any error manually. If a matching Transformer or Collector exists, use it — see **4.11** and **4.12**.
5050
5151
> **Nullable relation access**: If you see `'X.Y' is possibly 'null'`, add a null guard (`if (!X.Y) throw new HttpException(...)`) or use optional chaining (`X.Y?.prop ?? null`) before accessing properties of nullable Prisma relations.
5252
@@ -290,6 +290,49 @@ data: { parent_category_id: null }
290290
data: { metadata: Prisma.DbNull } // only valid for Json? type
291291
```
292292

293+
### 4.11. Transformer Available but Not Used
294+
295+
If TS2322 errors concentrate in the return object and the code has no `Transformer.select()`/`.transform()` calls — **compare with the template code** provided above. The template already shows which Transformers to use. Rewrite the read side to follow the template's Transformer pattern instead of patching manual mapping line by line.
296+
297+
```typescript
298+
// ❌ WRONG — manual mapping ignoring template's Transformer guidance
299+
return {
300+
task: timer.task ? { project: timer.task.project ? {...} : null } : null,
301+
} satisfies IHrmPlatformTimer.ISummary; // TS2322: nullable vs non-nullable
302+
303+
// ✅ FIX — Follow the template: use Transformer.select() + .transform()
304+
const timer = await MyGlobal.prisma.hrm_platform_timers.findUniqueOrThrow({
305+
where: { id: timerId },
306+
...HrmPlatformTimerAtSummaryTransformer.select(),
307+
});
308+
return await HrmPlatformTimerAtSummaryTransformer.transform(timer);
309+
```
310+
311+
For **composite responses** (e.g., dashboard), the template maps each property to its neighbor Transformer — follow that mapping.
312+
313+
### 4.12. Transformer select/transform Pairing Violation
314+
315+
`Transformer.select()` and `Transformer.transform()` **MUST** always be paired. `select()` shapes the Prisma payload for `transform()` — using one without the other causes type mismatches.
316+
317+
```typescript
318+
// ❌ select() without transform() — manual mapping of transformer-shaped data
319+
return { id: record.id, name: record.name };
320+
321+
// ❌ transform() without select() — wrong data shape
322+
const record = await MyGlobal.prisma.users.findUniqueOrThrow({
323+
where: { id },
324+
select: { id: true, name: true }, // Manual select
325+
});
326+
return await UserAtSummaryTransformer.transform(record); // Type mismatch
327+
328+
// ✅ Always pair both
329+
const record = await MyGlobal.prisma.users.findUniqueOrThrow({
330+
where: { id },
331+
...UserAtSummaryTransformer.select(),
332+
});
333+
return await UserAtSummaryTransformer.transform(record);
334+
```
335+
293336
## 5. Unrecoverable Errors
294337

295338
When schema-API mismatch is fundamental:
@@ -309,6 +352,9 @@ export async function method__path(props: {...}): Promise<IResponse> {
309352

310353
| Error | First Try | Alternative |
311354
|-------|-----------|-------------|
355+
| 2322 (deep type mismatch in return) + no Transformer calls | **Follow the template's Transformer pattern** | - |
356+
| select() without transform() | Add matching `.transform()` call | - |
357+
| transform() without select() | Add matching `.select()` call | - |
312358
| 2353 (field doesn't exist) | DELETE the field | Use correct field name |
313359
| 2322 (null → string) | Add `?? ""` | Check if optional |
314360
| 2322 (Date → string) | `.toISOString()` | - |
@@ -329,7 +375,9 @@ export async function method__path(props: {...}): Promise<IResponse> {
329375
## 7. Final Checklist
330376

331377
### Reuse Check
332-
- [ ] Errors in `data:` or `select`/transform: checked for matching Collector/Transformer before patching
378+
- [ ] Compared current code with the template — if template uses Transformers/Collectors, code follows the same pattern
379+
- [ ] Every `Transformer.select()` has a matching `.transform()` call (and vice versa)
380+
- [ ] Composite responses: each nested DTO uses its neighbor Transformer per the template
333381

334382
### Compiler Authority
335383
- [ ] NO compiler errors remain
Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AutoBeRealizeFunction } from "@autobe/interface";
22

3+
import { AutoBeConfigConstant } from "../../../constants/AutoBeConfigConstant";
34
import { IAutoBeRealizeFunctionResult } from "../structures/IAutoBeRealizeFunctionResult";
45

56
export async function orchestrateRealizeCorrectWithRetry<
@@ -22,28 +23,38 @@ export async function orchestrateRealizeCorrectWithRetry<
2223
return await props.correctOverall(casted);
2324
};
2425

25-
//----
26-
// FIRST ATTEMPT
27-
//----
28-
// write functions
29-
const results: IAutoBeRealizeFunctionResult<Func>[] = await process(
30-
await props.write(),
26+
const partition = (
27+
results: IAutoBeRealizeFunctionResult<Func>[],
28+
): IPartition<Func> => ({
29+
success: results.filter((r) => r.success).map((r) => r.function),
30+
failures: results.filter((r) => !r.success).map((r) => r.function),
31+
});
32+
33+
// first attempt
34+
const initial: IPartition<Func> = partition(
35+
await process(await props.write()),
3136
);
37+
const success: Func[] = initial.success;
38+
let failures: Func[] = initial.failures;
3239

33-
// filter success and failures
34-
const filter = (v: boolean) =>
35-
results.filter((r) => r.success === v).map((r) => r.function);
36-
const success: Func[] = filter(true);
37-
const failures: Func[] = filter(false);
40+
// retry loop
41+
const limit: number = Math.max(
42+
Math.floor(AutoBeConfigConstant.COMPILER_RETRY / 2),
43+
1,
44+
);
45+
for (let i: number = 0; failures.length !== 0 && i < limit; i++) {
46+
const retry: IPartition<Func> = partition(
47+
await process(await props.rewrite(failures)),
48+
);
49+
success.push(...retry.success);
50+
failures = retry.failures;
51+
}
3852

39-
// no failures, return success
40-
if (failures.length === 0) return success;
53+
// remaining failures are included as-is
54+
return [...success, ...failures];
55+
}
4156

42-
//----
43-
// RETRY
44-
//----
45-
const retryResults: IAutoBeRealizeFunctionResult<Func>[] = await process(
46-
await props.rewrite(failures),
47-
);
48-
return [...success, ...retryResults.map((r) => r.function)];
57+
interface IPartition<Func extends AutoBeRealizeFunction> {
58+
success: Func[];
59+
failures: Func[];
4960
}

packages/agent/src/orchestrate/realize/orchestrateRealizeOperationCorrectOverall.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,12 @@ export const orchestrateRealizeOperationCorrectOverall = async (
101101
(c) =>
102102
c.plan.dtoTypeName === scenario.operation.requestBody?.typeName,
103103
),
104-
realizeTransformers: props.transformers.filter(
105-
(t) =>
106-
t.plan.dtoTypeName ===
107-
scenario.operation.responseBody?.typeName.replace(/^IPage/, ""),
108-
),
104+
realizeTransformers:
105+
AutoBeRealizeOperationProgrammer.getLocalTransformers({
106+
operation: scenario.operation,
107+
schemas: document.components.schemas,
108+
transformers: props.transformers,
109+
}),
109110
},
110111
});
111112
},
@@ -143,13 +144,21 @@ export const orchestrateRealizeOperationCorrectOverall = async (
143144
thinking: result.data.thinking,
144145
request: result.data.request,
145146
});
146-
const errors: IValidation.IError[] = validateEmptyCode({
147-
name: next.function.name,
148-
draft: result.data.request.draft,
149-
revise: result.data.request.revise,
150-
path: "$input.request",
151-
asynchronous: true,
152-
});
147+
const errors: IValidation.IError[] = [
148+
...validateEmptyCode({
149+
name: next.function.name,
150+
draft: result.data.request.draft,
151+
revise: result.data.request.revise,
152+
path: "$input.request",
153+
asynchronous: true,
154+
}),
155+
...AutoBeRealizeOperationProgrammer.validateSelectTransformContract(
156+
{
157+
draft: result.data.request.draft,
158+
revise: result.data.request.revise,
159+
},
160+
),
161+
];
153162
return errors.length
154163
? {
155164
success: false,

packages/agent/src/orchestrate/realize/orchestrateRealizeOperationWrite.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,12 @@ async function process(
145145
(c) =>
146146
c.plan.dtoTypeName === props.scenario.operation.requestBody?.typeName,
147147
),
148-
realizeTransformers: props.transformers.filter(
149-
(t) =>
150-
t.plan.dtoTypeName ===
151-
props.scenario.operation.responseBody?.typeName.replace(/^IPage/, ""),
152-
),
148+
realizeTransformers:
149+
AutoBeRealizeOperationProgrammer.getLocalTransformers({
150+
operation: props.scenario.operation,
151+
schemas: props.document.components.schemas,
152+
transformers: props.transformers,
153+
}),
153154
analysisSections: ragSections,
154155
},
155156
});
@@ -254,13 +255,19 @@ function createController(props: {
254255
request: result.data.request,
255256
});
256257

257-
const errors: IValidation.IError[] = validateEmptyCode({
258-
name: props.functionName,
259-
draft: result.data.request.draft,
260-
revise: result.data.request.revise,
261-
path: "$input.request",
262-
asynchronous: true,
263-
});
258+
const errors: IValidation.IError[] = [
259+
...validateEmptyCode({
260+
name: props.functionName,
261+
draft: result.data.request.draft,
262+
revise: result.data.request.revise,
263+
path: "$input.request",
264+
asynchronous: true,
265+
}),
266+
...AutoBeRealizeOperationProgrammer.validateSelectTransformContract({
267+
draft: result.data.request.draft,
268+
revise: result.data.request.revise,
269+
}),
270+
];
264271
return errors.length
265272
? {
266273
success: false,

packages/agent/src/orchestrate/realize/programmers/AutoBeRealizeOperationProgrammer.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import {
55
AutoBeRealizeTransformerFunction,
66
IAutoBeCompiler,
77
} from "@autobe/interface";
8-
import { StringUtil } from "@autobe/utils";
8+
import { AutoBeOpenApiTypeChecker, StringUtil } from "@autobe/utils";
99
import { OpenApiTypeChecker } from "@typia/utils";
1010
import { IValidation } from "typia";
1111

1212
import { AutoBeContext } from "../../../context/AutoBeContext";
1313
import { IAutoBeRealizeScenarioResult } from "../structures/IAutoBeRealizeScenarioResult";
1414
import { AutoBeRealizeCollectorProgrammer } from "./AutoBeRealizeCollectorProgrammer";
1515
import { AutoBeRealizeTransformerProgrammer } from "./AutoBeRealizeTransformerProgrammer";
16+
import { resolvePropertyTransformer } from "./internal/resolvePropertyTransformer";
1617
import { writeRealizeOperationTemplate } from "./internal/writeRealizeOperationTemplate";
1718

1819
export namespace AutoBeRealizeOperationProgrammer {
@@ -177,6 +178,43 @@ export namespace AutoBeRealizeOperationProgrammer {
177178
);
178179
}
179180

181+
/**
182+
* Resolves transformers relevant to an operation, including neighbor
183+
* transformers for composite response types (e.g., dashboard endpoints).
184+
* Falls back to direct top-level match for simple response types.
185+
*/
186+
export function getLocalTransformers(props: {
187+
operation: AutoBeOpenApi.IOperation;
188+
schemas: Record<string, AutoBeOpenApi.IJsonSchemaDescriptive>;
189+
transformers: AutoBeRealizeTransformerFunction[];
190+
}): AutoBeRealizeTransformerFunction[] {
191+
const responseTypeName = props.operation.responseBody?.typeName;
192+
if (!responseTypeName) return [];
193+
194+
const innerTypeName = responseTypeName.replace(/^IPage/, "");
195+
196+
// Direct match (covers simple and paginated types)
197+
const direct = props.transformers.filter(
198+
(t) => t.plan.dtoTypeName === innerTypeName,
199+
);
200+
if (direct.length > 0) return direct;
201+
202+
// Composite: resolve each property's transformer
203+
const schema = props.schemas[innerTypeName];
204+
if (!schema || !AutoBeOpenApiTypeChecker.isObject(schema)) return [];
205+
206+
const results: AutoBeRealizeTransformerFunction[] = [];
207+
for (const value of Object.values(schema.properties ?? {})) {
208+
const resolved = resolvePropertyTransformer({
209+
schema: value as AutoBeOpenApi.IJsonSchemaProperty,
210+
transformers: props.transformers,
211+
});
212+
if (resolved && !results.includes(resolved.transformer))
213+
results.push(resolved.transformer);
214+
}
215+
return results;
216+
}
217+
180218
export function validateEmptyCode(props: {
181219
functionName: string;
182220
draft: string;
@@ -204,6 +242,81 @@ export namespace AutoBeRealizeOperationProgrammer {
204242
});
205243
return errors;
206244
}
245+
246+
/**
247+
* Validates that Transformer.select() and Transformer.transform() are always
248+
* used as a pair in operation code. Using one without the other causes type
249+
* mismatches: select() shapes the Prisma payload for transform(), so they
250+
* must appear together.
251+
*/
252+
export function validateSelectTransformContract(props: {
253+
draft: string;
254+
revise: {
255+
final: string | null;
256+
};
257+
}): IValidation.IError[] {
258+
const errors: IValidation.IError[] = [];
259+
validateSelectTransformContractForCode({
260+
content: props.draft,
261+
path: "$input.request.draft",
262+
errors,
263+
});
264+
if (props.revise.final !== null) {
265+
validateSelectTransformContractForCode({
266+
content: props.revise.final,
267+
path: "$input.request.revise.final",
268+
errors,
269+
});
270+
}
271+
return errors;
272+
}
273+
}
274+
275+
function validateSelectTransformContractForCode(props: {
276+
content: string;
277+
path: string;
278+
errors: IValidation.IError[];
279+
}): void {
280+
const selectUsers: Set<string> = new Set();
281+
const transformUsers: Set<string> = new Set();
282+
const selectRegex: RegExp = /(\w+Transformer)\.select/g;
283+
const transformRegex: RegExp = /(\w+Transformer)\.transform/g;
284+
let match: RegExpExecArray | null;
285+
while ((match = selectRegex.exec(props.content)) !== null)
286+
selectUsers.add(match[1]!);
287+
while ((match = transformRegex.exec(props.content)) !== null)
288+
transformUsers.add(match[1]!);
289+
for (const name of transformUsers) {
290+
if (selectUsers.has(name) === false)
291+
props.errors.push({
292+
path: props.path,
293+
expected: `${name}.select() must appear in the query when ${name}.transform() is used.`,
294+
value: props.content,
295+
description: StringUtil.trim`
296+
You call ${name}.transform() but never include ${name}.select()
297+
in your Prisma query. The Payload type of ${name}.transform()
298+
is derived from ${name}.select() — without it, the data shape
299+
will not match and you will get type mismatch compile errors.
300+
Add \`...${name}.select()\` to your Prisma query's select/spread.
301+
`,
302+
});
303+
}
304+
for (const name of selectUsers) {
305+
if (transformUsers.has(name) === false)
306+
props.errors.push({
307+
path: props.path,
308+
expected: `${name}.transform() must be called when ${name}.select() is used.`,
309+
value: props.content,
310+
description: StringUtil.trim`
311+
You include ${name}.select() in your query but never call
312+
${name}.transform() to convert the result. The data fetched
313+
via ${name}.select() is a raw Prisma payload shaped for
314+
${name}.transform() — assigning it directly to a DTO field
315+
or transforming it inline will cause type mismatches. Call
316+
${name}.transform() on the fetched data.
317+
`,
318+
});
319+
}
207320
}
208321

209322
function writeImportStatements(props: {

0 commit comments

Comments
 (0)