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:


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:

  1. Angular Material Ban — @angular/material is NOT installed. No mat-* components, no Mat*Module imports. Use plain HTML + SCSS + Ggs-prefixed components. The LLM will default to Material if not explicitly banned.

  2. Promise-to-Observable conversion — from(somePromise).pipe(...) NOT somePromise.pipe(...). Common LLM mistake.

  3. async ngOnInit() is FORBIDDEN — ngOnInit MUST be void, not Promise<void>. Use .subscribe() or effect().

  4. .subscribe() without cleanup — MUST include takeUntilDestroyed(this.destroyRef). The LLM generates memory leaks without this rule.

  5. firstValueFrom() in components is FORBIDDEN — Use .pipe(tap(), takeUntilDestroyed()).subscribe().

  6. No patch() on DataSourceService — Only get, post, put, delete. The LLM invents patch().

  7. Icon syntax — <fa-icon [icon]="['fas', 'icon-name']"> (tuple binding), NOT <fa-icon icon="name"> (string — renders literal text).

  8. Functional Completeness: Zero Dead UI — Every (click) handler MUST have a real implementation. No // TODO, no empty method bodies, no console.log('clicked'). If you can't implement it, DON'T create the button. This single rule eliminates the of(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:

  1. 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
  2. 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.files is a non-empty array, each file has path, content, moduleName.

  3. 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:

  1. Fix the ANALYZE phase prompt to output rich field types, OR
  2. 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 → number
    • is*, has*, checked*, active, enabled → boolean
    • status → 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.

  1. Add this.logger.log('RAW LLM CODE RESPONSE (first 2000 chars): ' + response.content?.substring(0, 2000)) in generateCode() BEFORE calling extractCodeFiles().
  2. Fix parseLlmJson() to handle text-wrapped JSON (strip everything before first {).
  3. Replace Strategy 2's indexOf('{')/lastIndexOf('}') with the brace-depth counter (see section 2B above).
  4. Increase maxTokens from 16384 to 32768 (line 299).
  5. 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.

  1. In generateScaffoldFiles(), deduplicate fields before adding id, createdAt, updatedAt.
  2. Map field types from entity spec: number for counts/amounts, boolean for flags, union types for status fields.
  3. Fix service method casing: createClassBooking() not createclassbooking().
  4. Add typed method signatures: createClassBooking(data: Partial<ClassBooking>): Observable<ClassBooking>.
  5. 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.

  1. Create the partials/ directory and the 6 partial files listed in 2A.
  2. Add partial injection to PromptBuilderService.
  3. Add the AI-RULES (section 2E) to implement.prompt.md.
  4. Add 1-2 golden file examples (section 2F).
  5. Add "Functional Completeness: Zero Dead UI" section from sys-paks (section 2E point 8).
  6. Add "Simplicity Rules" from sys-paks implement.prompt.md (no extra components, 10 mock records not 20, flat models).
  7. 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.

  1. Add duplicate field detection to auto-correction.service.ts.
  2. Add null-service detection (every method of(null) → flag as invalid).
  3. Add template-TS consistency check: parse .html for (click)="someMethod()" and verify someMethod exists in the .ts.
  4. Add Angular Material import detection (flag any @angular/material import 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.

  1. Wire TemplateRegistryService into the scaffold fallback path.
  2. Ensure file path consistency between LLM output, scaffold output, and writeGeneratedFilesToDisk().
  3. Run a full 5-module generation test (Fitness Studio template).
  4. 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


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:

  1. A full Fitness Studio generation (5 modules) completes with EXECUTE.output.files containing >20 files (not null).
  2. Generated model files have zero duplicate fields and use proper TypeScript types (number, boolean, union types).
  3. Generated service files have real method signatures with typed parameters and return values (no of(null)).
  4. Generated components use ChangeDetectionStrategy.OnPush, inject(), and Signals.
  5. No @angular/material imports anywhere in generated code.
  6. All (click) handlers in .html templates have corresponding method implementations in .ts files.
  7. The scaffold fallback (when LLM parsing fails) produces code that passes all quality checks in items 2-6.
  8. Backend survives the full generation without crashing.
  9. Generated files are written to the correct, canonical module directory.

10. Quick Start Checklist

Before starting implementation: