HANDOFF S138.10 Code Gen Quality Fix
Documentation for Handoffs
HANDOFF S138.10 — Code Generation Quality Fix
Date: 2026-02-24
Author: Claude (Session 138.10, Cowork analysis)
Audience: Coding agents about to implement fix_code_generation_quality_856dce71.plan.md
Scope: Fix the module code generation pipeline so it produces gmodules1-quality code
Concurrent Sessions: This session (138.10) runs alongside other 138.x subSessions. Always use subSession 138.10+ for MCP operations.
READ THIS BEFORE WRITING A SINGLE LINE OF CODE
This handoff supplements the plan at .cursor/plans/fix_code_generation_quality_856dce71.plan.md. The plan is mostly correct but has critical gaps that will cause you to ship a fix that still produces garbage. Read this entire document first.
1. What the Plan Gets Right
The plan correctly identifies:
extractCodeFiles()is the #1 blocker — the LLM IS generating code (~90s/module in Test 8) but the 3-strategy parser returns empty arrays. This is confirmed across 8 tests with 0 code files extracted.generateScaffoldFiles()is the fallback producing garbage — duplicateidfields,of(null)services, no casing on method names.- sys-paks is the right approach over agent-paks — template-free prompts with rich context beat hard-coded scaffolding.
- The 5-phase fix order (extract → scaffold → prompt → validation → wire templates) is correct.
2. What the Plan is MISSING (You Must Also Do These)
2A. Port the sys-paks Prompt Partials System — Not Just the Prompt Text
The plan says "upgrade implement.prompt.md with gmodules1 sys-paks template content." This is too vague. Here's what you actually need:
gmodules1's sys-paks uses a partial template system. The implement.prompt.md references partials like {{> event-system-api}}, {{> context-service-api}}, {{> core-library-api}} which inject complete API documentation for every core library. Without these, the LLM has to GUESS the API surface of BaseRepository, EventBusService, AbstractModule, etc.
You must create these partial files for gspace1:
| Partial File | Content | Why It Matters |
|---|---|---|
core-library-api.partial.md |
API signatures for BaseRepository, EventBusService, AbstractModule, createCrudMockApi, ActionRegistry | Without this, the LLM generates of(null) services because it doesn't know what methods exist |
ui-framework-components.partial.md |
Complete list of Ggs-prefixed components with their inputs/outputs | Without this, the LLM imports non-existent MatTableModule, ButtonComponent, etc. |
mock-api-route-ordering.partial.md |
Route ordering rules for mock APIs | Prevents greedy-match bugs where GET /api/:id catches GET /api/search |
datasource-api.partial.md |
DataSourceService methods: get, post, put, delete — NO patch(), NO getBlob() |
Prevents LLM from inventing methods |
event-system-api.partial.md |
EventBusService.publish/subscribe API, DomainEvent interface | Prevents incorrect event bus usage |
context-service-api.partial.md |
currentContext() Signal, tenant isolation | Prevents getCurrentContext() calls that don't exist |
Where to put them: apis/backend-vitto-api/src/prompts/module-gen/partials/
How to wire them: Modify PromptBuilderService.buildModuleGenPrompt() to load and inject these partials where {{> partial-name}} appears in the prompt template. If the current PromptBuilderService doesn't support partials, add a simple replacePartials() step that reads .partial.md files from the partials directory and replaces {{> name}} tokens.
2B. The parseLlmJson() Function Has a Critical Bug
The plan mentions fixing it but doesn't identify the actual bug. Look at module-generation.types.ts lines 15-45:
export function parseLlmJson<T = any>(content: string): T {
let cleaned = content
.replace(/^```json\s*/m, '') // ← Only strips FIRST ```json fence
.replace(/^```\s*/m, '') // ← Only strips FIRST ``` fence
.replace(/```\s*$/m, '') // ← Only strips LAST ``` fence
.trim();
Bug: The m flag makes ^ match start-of-line, not start-of-string. If the LLM returns:
Here are the files for booking-system:
```json
{"files": [...]}
The regex strips the fences but leaves "Here are the files for booking-system:" as a prefix, which makes `JSON.parse()` fail. The repair logic then adds closing brackets to garbage, producing invalid JSON.
**Fix:** Before stripping fences, find the FIRST `{` that starts a JSON object and extract from there. Or use a more aggressive cleaner:
```typescript
// Strip everything before the first { and after the last }
const jsonStart = cleaned.indexOf('{');
const jsonEnd = cleaned.lastIndexOf('}');
if (jsonStart >= 0 && jsonEnd > jsonStart) {
cleaned = cleaned.substring(jsonStart, jsonEnd + 1);
}
Wait — that's exactly what Strategy 2 in extractCodeFiles() does. So why does Strategy 2 also fail? Because the content values inside the JSON contain { and } characters (it's source code!), so lastIndexOf('}') lands inside a code string, not at the actual JSON closing brace.
The real fix for Strategy 2: Use a proper brace-depth counter instead of indexOf/lastIndexOf:
function findOutermostJsonObject(text: string): string | null {
const start = text.indexOf('{');
if (start < 0) return null;
let depth = 0;
let inString = false;
let escape = false;
for (let i = start; i < text.length; i++) {
const ch = text[i];
if (escape) { escape = false; continue; }
if (ch === '\\') { escape = true; continue; }
if (ch === '"') { inString = !inString; continue; }
if (inString) continue;
if (ch === '{') depth++;
if (ch === '}') { depth--; if (depth === 0) return text.substring(start, i + 1); }
}
return null;
}
2C. maxTokens: 16384 is NOT Enough — Must Increase to 32768+
The plan doesn't mention this. The implement.prompt.md requires 14+ files per module. At ~200 lines of real code per module (model + 3 components with .ts/.html/.scss + repository + 3 actions + widget + events + mocks + service + routes + module + index), you need ~60K characters of output. That's roughly 20K tokens. With JSON wrapping and escaped content, you're looking at 25K+ tokens. 16384 will truncate mid-file, and ALL three extraction strategies fail on truncated JSON.
Fix in execute.phase.ts line 299:
// BEFORE
maxTokens: 16384,
// AFTER
maxTokens: 32768,
Also consider: if the module has many entities, split generation into multiple LLM calls (one per entity's components) rather than one giant call for all 14+ files. The gmodules1 sys-paks does this — it generates per-entity, not per-module.
2D. The Scaffold Fallback Writes Files to the WRONG Location
The plan doesn't mention this. Look at writeGeneratedFilesToDisk() (line 683):
const moduleDir = path.join(workspaceRoot, 'modules', modName);
But the implement.prompt.md tells the LLM to generate paths like:
libs/libs-modules/{{moduleName}}/src/lib/models/{singular}.model.ts
These are TWO DIFFERENT LOCATIONS. The scaffold writes to modules/booking-system/ while the prompt expects libs/libs-modules/booking-system/. The nx workspace project.json reference sourceRoot: modules/${slug}/src also differs from the standard gmodules1 path.
You must decide which is canonical and make everything consistent. In gmodules1, modules live in libs/libs-modules/. The gspace1 codebase currently uses modules/. we must use the /modules like we do inside our own codebase in writeGeneratedFilesToDisk(), project.json, and the tsconfig paths.
2E. Add the "AI-RULES.md" System from gmodules1
gmodules1's sys-paks has docs/ia/AI-RULES.md with 100+ critical rules organized by priority (MUST/SHOULD/MAY). The gspace1 implement.prompt.md has a small "CRITICAL RULES" table, but it's missing at least 30 rules that are essential. The most impactful missing rules:
Angular Material Ban —
@angular/materialis NOT installed. Nomat-*components, noMat*Moduleimports. Use plain HTML + SCSS + Ggs-prefixed components. The LLM will default to Material if not explicitly banned.Promise-to-Observable conversion —
from(somePromise).pipe(...)NOTsomePromise.pipe(...). Common LLM mistake.async ngOnInit()is FORBIDDEN — ngOnInit MUST bevoid, notPromise<void>. Use.subscribe()oreffect()..subscribe()without cleanup — MUST includetakeUntilDestroyed(this.destroyRef). The LLM generates memory leaks without this rule.firstValueFrom()in components is FORBIDDEN — Use.pipe(tap(), takeUntilDestroyed()).subscribe().No
patch()on DataSourceService — Onlyget,post,put,delete. The LLM inventspatch().Icon syntax —
<fa-icon [icon]="['fas', 'icon-name']">(tuple binding), NOT<fa-icon icon="name">(string — renders literal text).Functional Completeness: Zero Dead UI — Every
(click)handler MUST have a real implementation. No// TODO, no empty method bodies, noconsole.log('clicked'). If you can't implement it, DON'T create the button. This single rule eliminates theof(null)services problem.
2F. Add "Golden File" Examples to the Prompt
The plan says "add component templates." Go further — inject 1-2 complete, real examples from gmodules1 directly into the prompt. The LLM performs drastically better with concrete examples than with abstract rules.
Best candidates from gmodules1:
For the model: Use libs/core/user-management/src/lib/models/user.model.ts — it shows proper type unions, JSDoc comments, as const patterns, and shared-model extension.
For the service: Show a real repository usage pattern — inject via inject(), call findAll(), handle errors with catchError(), return properly typed Observables.
For the mock data: Show a real MOCK_USERS array with 10 realistic records using different statuses, dates, and relationships.
Embed these as ## Example: High-Quality Model File sections in implement.prompt.md. Keep them under 50 lines each.
2G. Test Strategy is Missing from the Plan
The plan has no testing step. After fixing the pipeline, how do you verify it works? Define this:
Unit test for
extractCodeFiles()— Create test cases with:- Clean JSON (happy path)
- JSON wrapped in markdown fences
- JSON with prefix text ("Here are the files:")
- Truncated JSON (token limit hit mid-file)
- JSON with escaped content containing
{,},",\n - Each test should assert the correct number of files extracted with valid content
Integration test — Run a single-module generation (not 5 modules) with a simple spec:
POST /module-generation/generate-single { description: "Simple task manager with tasks and categories", outputMode: "code" }Verify:
EXECUTE.output.filesis a non-empty array, each file haspath,content,moduleName.Quality gate — After extraction, validate:
- No duplicate fields in model interfaces
- No
of(null)in services - All
(click)handlers in .html have corresponding methods in .ts - All imports resolve to real modules (no
@angular/material)
2H. The specJson Entities Lack Type Information
The plan doesn't address this. In execute.phase.ts line 275-282:
const specJson = {
entities: entities.map(e => ({
name: e.name,
description: e.description,
fields: e.fields, // ← What's in fields?
})),
features: (moduleSpec as unknown as { features?: string[] }).features || [],
};
The fields come from the ANALYZE phase. Check what the ANALYZE phase actually returns for each field — does it include type (number, boolean, string, Date)? Does it include required? Does it include relationship info?
If the ANALYZE phase only returns field names without types, the LLM will default everything to string. You need to either:
- Fix the ANALYZE phase prompt to output rich field types, OR
- Add a field-type inference step between ANALYZE and EXECUTE that maps common field names to types:
*Id→string*Date,*At,*Time→string(ISO)count,total,capacity,attended,age,price,amount→numberis*,has*,checked*,active,enabled→booleanstatus→ union type (infer from context)email→string(with validation pattern)
3. Architecture Decisions Already Made (DO NOT CHANGE)
| Decision | Choice | Rationale |
|---|---|---|
| Model | Sonnet 4.6 (claude-sonnet-4-6) |
Stable, survives 18.5min generation. Haiku 4.5 also works but slower. |
| Output mode | both (visual + code) |
Visual path works, code path is what we're fixing. |
| Approach | sys-paks (template-free prompts) | Better code quality than agent-paks hard-scaffold approach. |
| Module path | Currently modules/ — align with whatever is canonical |
Must be consistent across prompt, scaffold, writer, nx config. |
| Generation granularity | Per-module (current) | Consider splitting to per-entity if token limits are hit. |
4. Files You Will Modify (Complete List)
| File | What to Change | Phase |
|---|---|---|
apis/backend-vitto-api/src/app/module-generation/phases/execute.phase.ts |
Fix extractCodeFiles(), add raw response logging, increase maxTokens to 32768 |
1 |
apis/backend-vitto-api/src/app/module-generation/module-generation.types.ts |
Fix parseLlmJson() fence-stripping bug |
1 |
apis/backend-vitto-api/src/app/module-generation/phases/module-gen-phase.executor.ts |
Fix generateScaffoldFiles() deduplication, type mapping, casing; fix writeGeneratedFilesToDisk() path consistency |
2 |
apis/backend-vitto-api/src/prompts/module-gen/implement.prompt.md |
Major rewrite — add partials, golden examples, AI-RULES, functional completeness rules, Angular Material ban | 3 |
apis/backend-vitto-api/src/prompts/module-gen/partials/ (NEW) |
Create 6 partial files for core library APIs | 3 |
apis/backend-vitto-api/src/app/prompts/prompt-builder.service.ts |
Add replacePartials() to load and inject partial templates |
3 |
apis/backend-vitto-api/src/app/module-generation/validation/auto-correction.service.ts |
Add duplicate field detection, null-service detection, type quality checks | 4 |
apis/backend-vitto-api/src/app/module-generation/templates/template-registry.service.ts |
Verify templates match quality target, wire into scaffold fallback | 5 |
5. Execution Order (MANDATORY — Follow This Sequence)
Phase 1: Make extractCodeFiles() Actually Extract Files
Goal: After this phase, EXECUTE.output.files should be a non-empty array.
- Add
this.logger.log('RAW LLM CODE RESPONSE (first 2000 chars): ' + response.content?.substring(0, 2000))ingenerateCode()BEFORE callingextractCodeFiles(). - Fix
parseLlmJson()to handle text-wrapped JSON (strip everything before first{). - Replace Strategy 2's
indexOf('{')/lastIndexOf('}')with the brace-depth counter (see section 2B above). - Increase
maxTokensfrom 16384 to 32768 (line 299). - TEST: Run a single generation. Check backend logs for
RAW LLM CODE RESPONSE. Verify files are extracted.
Exit criteria: extractCodeFiles() returns >0 files for at least 1 module.
Phase 2: Fix the Scaffold Fallback
Goal: Even when LLM parsing fails, the fallback produces decent code.
- In
generateScaffoldFiles(), deduplicate fields before addingid,createdAt,updatedAt. - Map field types from entity spec:
numberfor counts/amounts,booleanfor flags, union types for status fields. - Fix service method casing:
createClassBooking()notcreateclassbooking(). - Add typed method signatures:
createClassBooking(data: Partial<ClassBooking>): Observable<ClassBooking>. - Make file paths consistent with the canonical module location.
Exit criteria: Scaffold output has 0 duplicate fields, 0 of(null) methods, correct casing.
Phase 3: Upgrade the Prompt to gmodules1 Quality
Goal: When the LLM generates code, it produces code matching gmodules1 standards.
- Create the
partials/directory and the 6 partial files listed in 2A. - Add partial injection to PromptBuilderService.
- Add the AI-RULES (section 2E) to implement.prompt.md.
- Add 1-2 golden file examples (section 2F).
- Add "Functional Completeness: Zero Dead UI" section from sys-paks (section 2E point 8).
- Add "Simplicity Rules" from sys-paks implement.prompt.md (no extra components, 10 mock records not 20, flat models).
- Update JSON output example to show proper escaped content with newlines.
Exit criteria: implement.prompt.md is 300+ lines with all critical rules embedded.
Phase 4: Add Quality Validation
Goal: Catch regressions automatically.
- Add duplicate field detection to auto-correction.service.ts.
- Add null-service detection (every method
of(null)→ flag as invalid). - Add template-TS consistency check: parse .html for
(click)="someMethod()"and verifysomeMethodexists in the .ts. - Add Angular Material import detection (flag any
@angular/materialimport as error).
Exit criteria: Validation catches all 4 issue types and either auto-fixes or logs clear errors.
Phase 5: Wire Templates + Final Integration
Goal: The full pipeline works end-to-end.
- Wire TemplateRegistryService into the scaffold fallback path.
- Ensure file path consistency between LLM output, scaffold output, and
writeGeneratedFilesToDisk(). - Run a full 5-module generation test (Fitness Studio template).
- Verify: files on disk, no duplicates, no
of(null), components have real implementations.
Exit criteria: Full generation produces code that a human would review as "acceptable" — not perfect, but functional and correctly typed.
6. Critical Reference: gmodules1 sys-paks Key Patterns
BaseRepository Return Types (Agents get this wrong 90% of the time)
// CORRECT — BaseRepository returns entities DIRECTLY, not wrapped in ApiResponse
findById(id: string): Observable<T | null> // returns entity directly
findAll(): Observable<T[]> // returns array directly
create(entity: Partial<T>): Observable<T> // returns created entity
update(id: string, entity: Partial<T>): Observable<T> // returns updated entity
delete(id: string): Observable<boolean> // returns boolean
// WRONG — DO NOT wrap in ApiResponse
findById(id: string): Observable<ApiResponse<T>> // ❌ WRONG
Component Pattern (OnPush + Signals + inject)
@Component({
selector: 'app-student-list',
standalone: true,
imports: [CommonModule, GgsTableComponent, GgsButtonComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './student-list.component.html',
styleUrl: './student-list.component.scss',
})
export class StudentListComponent implements OnInit {
private readonly repository = inject(StudentRepository);
private readonly destroyRef = inject(DestroyRef);
readonly students = signal<Student[]>([]);
readonly loading = signal(false);
readonly error = signal<string | null>(null);
ngOnInit(): void { // MUST be sync void, NOT async
this.loadStudents();
}
private loadStudents(): void {
this.loading.set(true);
this.repository.findAll().pipe(
takeUntilDestroyed(this.destroyRef),
).subscribe({
next: (data) => { this.students.set(data); this.loading.set(false); },
error: (err) => { this.error.set(err.message); this.loading.set(false); },
});
}
}
Model Pattern (Proper Types, Not All Strings)
import { BaseEntity } from '@libs-core/shared-models';
export type BookingStatus = 'confirmed' | 'pending' | 'cancelled' | 'waitlisted';
export interface ClassBooking extends BaseEntity {
memberId: string;
classId: string;
bookingDate: string; // ISO date string
status: BookingStatus; // union type, NOT string
checkedIn: boolean; // boolean, NOT string
checkinTime?: string; // optional ISO datetime
capacity: number; // number, NOT string
attended: number; // number, NOT string
}
export const BOOKING_PERMISSIONS = {
VIEW: 'bookings:view',
CREATE: 'bookings:create',
UPDATE: 'bookings:update',
DELETE: 'bookings:delete',
CHECKIN: 'bookings:checkin',
} as const;
Mock Data Pattern (Realistic, Typed, 10 Records)
import { ClassBooking } from '../models/class-booking.model';
export const MOCK_BOOKINGS: ClassBooking[] = [
{
id: 'bk-001',
memberId: 'mem-101',
classId: 'cls-yoga-am',
bookingDate: '2026-02-24',
status: 'confirmed',
checkedIn: true,
checkinTime: '2026-02-24T08:55:00Z',
capacity: 20,
attended: 18,
createdAt: '2026-02-20T10:00:00Z',
updatedAt: '2026-02-24T08:55:00Z',
},
// ... 9 more records with varied statuses, dates, and values
];
7. MCP / Concurrency Rules
- This analysis was done in session 138.10. Your work should use subSession 138.11+.
- Always increment subSessionNumber:
session { action: "set", sessionNumber: 138, subSessionNumber: N+1 }. - DO NOT reset or overwrite existing session state.
- Log decisions with
log_decisionfor any architectural choices you make during implementation. - Log outcomes with
outcome { action: "report" }after each phase completes.
8. Risk Matrix
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| maxTokens increase causes backend OOM | Low | High | Monitor memory during generation; consider per-entity splitting |
| Prompt changes cause LLM to return different JSON structure | Medium | High | Test extractCodeFiles() with the ACTUAL new response format before merging |
| Partial template injection breaks existing prompts | Low | Medium | Only inject partials in implement.prompt.md, not other phase prompts |
| TemplateRegistryService templates are stale/wrong | Medium | Medium | Review templates against gmodules1 reference before wiring |
| File path inconsistency between prompt and writer | High (it exists NOW) | High | Decide canonical path FIRST, then update all references |
9. Definition of Done
The fix is complete when:
- A full Fitness Studio generation (5 modules) completes with
EXECUTE.output.filescontaining >20 files (not null). - Generated model files have zero duplicate fields and use proper TypeScript types (number, boolean, union types).
- Generated service files have real method signatures with typed parameters and return values (no
of(null)). - Generated components use
ChangeDetectionStrategy.OnPush,inject(), and Signals. - No
@angular/materialimports anywhere in generated code. - All
(click)handlers in .html templates have corresponding method implementations in .ts files. - The scaffold fallback (when LLM parsing fails) produces code that passes all quality checks in items 2-6.
- Backend survives the full generation without crashing.
- Generated files are written to the correct, canonical module directory.
10. Quick Start Checklist
Before starting implementation:
- Read this entire document
- Read the plan:
.cursor/plans/fix_code_generation_quality_856dce71.plan.md - Read the test results:
docs/english/Handoffs/HANDOFF-S138-test-results.md - Read
execute.phase.ts(the main file you're fixing) - Read
module-generation.types.ts(containsparseLlmJson) - Read
module-gen-phase.executor.ts(containsgenerateScaffoldFiles) - Read
implement.prompt.md(the prompt you're upgrading) - Read gmodules1 reference:
gmodules1/libs/core/user-management/src/lib/models/(what "good" looks like) - Set your MCP session:
session { action: "set", sessionNumber: 138, subSessionNumber: 11 } - Run
git statusto verify you're on the correct branch