2026 03 19 Platform Generation Quality V3 Design

Documentation for Plans

Platform Generation Quality v3 — Design Spec

Date: 2026-03-19 Session: S244 Status: Draft Scope: Enrich the generation pipeline to produce production-ready platforms using the full libs/core SDK, register backend schemas, seed data, and fix the Generations tab.


Problem Statement

gspace1's platform generation produces functional CRUD modules, but the generated code uses only ~30% of the available infrastructure. The 39+ libraries in libs/core (workflow-engine, event-system, media, entity-lookup, filter panels, module-base, action-registry, widget-system, mock-api) are available in the platform-base-template but the LLM is never told to use them.

Meanwhile, gmodules1 produces fully-integrated platform modules using these exact patterns — workflow state machines, filter definitions, entity lookup components, module registration, embeddable widgets. The difference isn't code quality — it's architectural completeness.

Additionally, the backend-api has a complete dynamic CRUD system (schemas, seed data, query/filter/sort/aggregate) at /api/spaces/:spaceId/universes/:universeId/data/ — but the generation pipeline doesn't register schemas or seed data during generation.

Goal: Generated platforms should be production-ready — using real APIs, real schemas, seed data, workflow transitions, filter panels, entity lookups, and module registration — matching or exceeding gmodules1 quality.


Architecture Overview

Current Flow (What Works)

User → Vitto Chat → ModuleGenPlugin.start_module_generation()
  → ENHANCE (enrich description)
  → ANALYZE (extract entities, fields, relationships)
  → DECOMPOSE (break into DDD modules, dependency graph)
  → PLAN (execution order, navigation structure)
  → EXECUTE (LLM generates Angular code via implement.prompt.md)
  → INTEGRATE (validate cross-module compatibility)
  → Frontend SSE → VFS → rspack WASM bundler → Preview SW → Angular JIT

Backend CRUD (Already Complete)

Endpoint Method Purpose
/data/:collection/query POST Query with filter, pagination, sort, text search
/data/:collection/insert POST Insert record with auto-timestamps
/data/:collection/:recordId PUT Update record
/data/:collection/:recordId DELETE Delete record
/data/:collection/seed POST Upsert seed data (idempotent)
/data/:collection/aggregate POST MongoDB aggregation pipeline
/data/:collection/export POST Export CSV/JSON/Excel
/data/schemas POST Register entity schema with validation rules
/data/schemas GET List all schemas
/collections GET List collections with stats

Collection naming: universe_{universeId}_{collectionName} (auto-created on first write).

Frontend CRUD Stack (Already Complete)

Component → useCrudList/useCrudForm/useCrudDetail (composables)
  → BaseCrudApiService<T> (abstract) or UniverseDataCrudService<T> (platform adapter)
    → UniverseDataService (injectable)
      → HttpClient → backend-api:3000

UniverseDataCrudService (in platform-base-template) bridges composables ↔ UniverseDataService:


What Changes

Change 1: Enrich implement.prompt.md — Add Building Block Patterns

File: APIs/backend-vitto-api/src/prompts/module-gen/implement.prompt.md

Add 5 sections with golden examples adapted from gmodules1's product-catalog module (the most mature). All examples use gspace1 conventions (no NGXS, @Input() not input() for JIT, UniverseDataCrudService not BaseCrudApiService).

1A. Workflow Definitions

When: Entity has a status field with defined transitions.

Generate a workflow/{entity}-workflow.definition.ts:

import { WorkflowDefinition } from '@libs-core/shared-models';

export const TASK_STATUS_WORKFLOW: WorkflowDefinition = {
  id: 'task-status',
  entityType: 'task',
  statusField: 'status',
  states: [
    { id: 'pending', label: 'Pending', type: 'initial', color: 'warning', icon: 'clock' },
    { id: 'in-progress', label: 'In Progress', type: 'intermediate', color: 'info', icon: 'play' },
    { id: 'completed', label: 'Completed', type: 'final', color: 'success', icon: 'check' },
  ],
  transitions: [
    { id: 'start', from: 'pending', to: 'in-progress', label: 'Start', icon: 'play',
      permissions: ['tasks:update'], requireConfirmation: false },
    { id: 'complete', from: 'in-progress', to: 'completed', label: 'Complete', icon: 'check',
      permissions: ['tasks:update'], requireConfirmation: true,
      confirmMessage: 'Mark this task as completed?' },
  ],
};

export function provideTaskWorkflow() {
  return [{ provide: 'TASK_WORKFLOW', useValue: TASK_STATUS_WORKFLOW }];
}

Import note: WorkflowDefinition is in @libs-core/shared-models, NOT @libs-core/workflow-engine. The engine service consumes these types.

Template usage (in detail component — actual lib-status-transition component API):

<lib-status-transition
  [entityType]="'task'"
  [entity]="crud.entity()"
  (transitionCompleted)="onStatusChanged($event)"
/>

Selector: lib-status-transition (not ggs-status-transition). Inputs: entityType: string (required), entity: Record<string, unknown> (required). Output: transitionCompleted.

1B. Filter Definitions

When: Always. Every list component should have filterable columns.

readonly crud = useCrudList<Task>({
  loadFn: (params) => this.taskApi.getList(params),
  basePath: '/admin/task-catalog',
  entityName: 'Task',
  deleteFn: (ids) => this.taskApi.delete(ids[0]),
  filterDefinitions: [
    { field: 'status', label: 'Status', type: 'select',
      options: [
        { value: 'pending', label: 'Pending' },
        { value: 'in-progress', label: 'In Progress' },
        { value: 'completed', label: 'Completed' },
      ] },
    { field: 'priority', label: 'Priority', type: 'select',
      options: [
        { value: 'low', label: 'Low' },
        { value: 'medium', label: 'Medium' },
        { value: 'high', label: 'High' },
      ] },
  ],
  syncWithUrl: true,
  initialPageSize: 10,
});

Template (place filter panel ABOVE the shell — no filter-area slot exists in GgsCrudListShellComponent):

<!-- Filter panel sits outside the shell -->
<ggs-filter-panel
  [filters]="crud.filterDefinitions()"
  [activeFilters]="crud.activeFilters()"
  (filtersChange)="crud.setFilters($event)"
  (clearAll)="crud.clearFilters()"
/>
<ggs-filter-chips
  [activeFilters]="crud.activeFilters()"
  (removeFilter)="crud.removeFilter($event.field)"
  (clearAll)="crud.clearFilters()"
/>

<ggs-crud-list-shell ...>
  <!-- table/grid content -->
</ggs-crud-list-shell>

API note: GgsFilterPanelComponent input is filters (not definitions), output is filtersChange (not filterChange). GgsFilterChipsComponent input is activeFilters (not filters), output is removeFilter (not remove). useCrudList.setFilters() takes an array, setFilter() takes a single filter.

1C. Entity Lookup for Foreign Keys

When: Entity has a field referencing another entity (e.g., categoryId, assigneeId).

// In form component — use getList with search param (UniverseDataCrudService has no search() method)
readonly categoryLoadFn = (query: string): Observable<Category[]> =>
  this.categoryApi.getList({ page: 1, limit: 20, search: query }).pipe(map(r => r.items));

readonly categoryDisplayFn = (cat: Category): string => cat.name;

readonly categoryInitialLoadFn = (): Observable<Category[]> =>
  this.categoryApi.getList({ page: 1, limit: 20 }).pipe(map(r => r.items));
<div class="form-group">
  <label for="category-lookup">Category <span class="required">*</span></label>
  <ggs-entity-lookup
    id="category-lookup"
    [loadFn]="categoryLoadFn"
    [displayFn]="categoryDisplayFn"
    [initialLoadFn]="categoryInitialLoadFn"
    placeholder="Search categories..."
    (selectionChange)="onCategorySelected($event)"
  />
</div>

Rule: If an entity has a *Id field pointing to another entity in the same system, generate entity lookup. If the referenced entity is in a different module, generate a search function using that module's API service.

1D. Module Registration

When: Always. Every generated module must register itself.

import { ModuleDecorator, BrowserModuleContract, ModuleManifest } from '@libs-core/module-base';

@ModuleDecorator({
  id: 'task-catalog',
  version: '1.0.0',
  name: 'Task Catalog',
  description: 'Manage tasks and assignments',
  basePath: 'task-catalog',
  repositories: [],
  permissions: [
    { id: 'tasks:list', name: 'tasks:list', resource: 'tasks', action: 'list' },
    { id: 'tasks:read', name: 'tasks:read', resource: 'tasks', action: 'read' },
    { id: 'tasks:create', name: 'tasks:create', resource: 'tasks', action: 'create' },
    { id: 'tasks:update', name: 'tasks:update', resource: 'tasks', action: 'update' },
    { id: 'tasks:delete', name: 'tasks:delete', resource: 'tasks', action: 'delete' },
  ],
  navigation: {
    menuItems: [{
      id: 'task-catalog-main',
      label: 'Tasks',
      icon: 'list-check',
      route: 'list',
      order: 100,
      permissions: ['tasks:list'],
    }],
  },
  tags: ['task-catalog', 'crud'],
})
@Injectable({ providedIn: 'root' })
export class TaskCatalogModule implements BrowserModuleContract {
  readonly manifest: ModuleManifest = {
    id: 'task-catalog',
    version: '1.0.0',
    name: 'Task Catalog',
    description: 'Manage tasks and assignments',
    basePath: 'task-catalog',
    permissions: [
      { id: 'tasks:list', name: 'tasks:list', resource: 'tasks', action: 'list' },
      { id: 'tasks:read', name: 'tasks:read', resource: 'tasks', action: 'read' },
      { id: 'tasks:create', name: 'tasks:create', resource: 'tasks', action: 'create' },
      { id: 'tasks:update', name: 'tasks:update', resource: 'tasks', action: 'update' },
      { id: 'tasks:delete', name: 'tasks:delete', resource: 'tasks', action: 'delete' },
    ],
    embeddableComponents: [],
  };

  readonly routes = taskCatalogRoutes;

  configure<T = Record<string, unknown>>(_config: T): Observable<void> { return of(void 0); }
  initialize(): Observable<void> { return of(void 0); }
  start(): Observable<void> { return of(void 0); }
  stop(): Observable<void> { return of(void 0); }
  destroy(): Observable<void> { return of(void 0); }
}

Contract note: BrowserModuleContract requires a manifest: ModuleManifest property AND configure<T>() takes a generic config parameter. The manifest in @ModuleDecorator is for auto-registration; the class property satisfies the interface contract.


#### 1E. Seed Data Generation

**When:** Always. Every module should generate realistic seed data for immediate usability.

```typescript
// mocks/task-seed-data.ts
import { Task } from '../models/task.model';

export const SEED_TASKS: Partial<Task>[] = [
  {
    name: 'Review quarterly report',
    description: 'Review and approve the Q1 financial report',
    status: 'pending',
    priority: 'high',
    assigneeId: '',
    dueDate: '2026-04-01T00:00:00.000Z',
  },
  {
    name: 'Update team documentation',
    description: 'Refresh onboarding docs with latest process changes',
    status: 'in-progress',
    priority: 'medium',
    assigneeId: '',
    dueDate: '2026-03-25T00:00:00.000Z',
  },
  // 3-5 more realistic records per entity
];

Rule: Seed data is POSTed to /data/:collection/seed during the INTEGRATE phase. Each seed record MUST include a stable _id field (e.g., seed-task-001) because the seed endpoint upserts by _id only — without stable IDs, repeated calls create duplicates.


Change 2: Enrich system-analyze.prompt.md — Detect Building Block Requirements

File: APIs/backend-vitto-api/src/prompts/module-gen/system-analyze.prompt.md

Add output fields to the entity analysis:

{
  "coreEntities": [
    {
      "name": "Task",
      "attributes": [...],
      "relationships": [...],
      "buildingBlocks": {
        "hasWorkflow": true,
        "workflowField": "status",
        "workflowStates": ["pending", "in-progress", "completed", "cancelled"],
        "foreignKeys": [
          { "field": "assigneeId", "targetEntity": "User", "targetModule": "user-management" },
          { "field": "categoryId", "targetEntity": "Category", "targetModule": "same" }
        ],
        "hasMedia": false,
        "filterableFields": ["status", "priority", "assigneeId", "dueDate"]
      }
    }
  ]
}

Detection rules for the LLM:


Change 3: Pass Building Block Metadata to EXECUTE Phase

File: APIs/backend-vitto-api/src/app/module-generation/phases/execute.phase.ts

Currently, the EXECUTE phase passes entity names, fields, and relationships to the LLM. Add the buildingBlocks metadata from ANALYZE:

const specJson = JSON.stringify({
  ...moduleSpec,
  entities: moduleSpec.entities.map(e => ({
    ...e,
    buildingBlocks: analyzeResult.coreEntities
      .find(ce => ce.name === e.name)?.buildingBlocks || {},
  })),
});

This tells the LLM: "Task has a workflow (status field), has foreign keys (assigneeId → User), needs filters on status/priority/dueDate."


Change 4: Schema Registration + Seed Data in INTEGRATE Phase

File: APIs/backend-vitto-api/src/app/module-generation/phases/integrate.phase.ts

After code generation succeeds, register schemas and seed data with backend-api:

// For each entity in the generated module:
for (const entity of generatedEntities) {
  // 1. Register schema
  await this.httpService.post(
    `${API_BASE_URL}/api/spaces/${spaceId}/universes/${universeId}/data/schemas`,
    {
      collection: entity.collectionName,    // e.g., 'tasks'
      displayName: entity.displayName,       // e.g., 'Tasks'
      fields: entity.fields.map(f => ({
        name: f.name,
        type: f.type,                        // 'string' | 'number' | 'boolean' | 'date' | 'object' | 'array' | 'reference' (lowercase!)
        required: f.required,
        label: f.label,
        // Enum fields use type: 'string' with validation.enum (NOT type: 'enum')
        ...(f.enumValues ? { validation: { enum: f.enumValues } } : {}),
        ...(f.min !== undefined ? { validation: { ...( f.enumValues ? { enum: f.enumValues } : {}), min: f.min } } : {}),
        ...(f.max !== undefined ? { validation: { ...( f.enumValues ? { enum: f.enumValues } : {}), max: f.max } } : {}),
      })),
    }
  );

  // 2. Seed initial data
  if (entity.seedData?.length) {
    await this.httpService.post(
      `${API_BASE_URL}/api/spaces/${spaceId}/universes/${universeId}/data/${entity.collectionName}/seed`,
      { records: entity.seedData }
    );
  }
}

Why this matters: Without schema registration, the backend accepts any data shape. With schemas, the backend validates types, required fields, enum values, and creates indexes. Without seed data, the generated platform shows empty lists on first load — poor first impression.


Change 5: LLM-Generated Seed Data Extraction

Update implement.prompt.md output format to include seed data:

{
  "files": [...],
  "summary": {...},
  "schemas": [
    {
      "collection": "tasks",
      "displayName": "Tasks",
      "fields": [
        { "name": "name", "type": "string", "required": true, "label": "Task Name" },
        { "name": "status", "type": "string", "required": true, "label": "Status",
          "validation": { "enum": ["pending", "in-progress", "completed", "cancelled"] } },
        { "name": "priority", "type": "string", "label": "Priority",
          "validation": { "enum": ["low", "medium", "high"] } },
        { "name": "dueDate", "type": "date", "label": "Due Date" }
      ]
    }
  ],
  "seedData": {
    "tasks": [
      { "name": "Review quarterly report", "status": "pending", "priority": "high",
        "dueDate": "2026-04-01T00:00:00.000Z" },
      { "name": "Update team documentation", "status": "in-progress", "priority": "medium",
        "dueDate": "2026-03-25T00:00:00.000Z" }
    ]
  }
}

Change 6: Fix Generations Tab

File: libs/libs-ai/gograb-ai-lib/src/lib/components/ or libs/libs-structural/vitto-studio/src/lib/services/generation.service.ts

Bug: The Generations tab shows "No generations yet" after a successful generation completes and the preview renders.

Root Cause Investigation:

Fix: Trace the SSE event flow from module-gen.completed → StudioGenerationListenerService → GenerationService.generations signal → Generations tab component. Ensure the completed generation is persisted and displayed.


Updated File Structure (per generated module)

modules/{{category}}/{{moduleSlug}}/
├── src/
│   ├── models/
│   │   ├── {{entity}}.model.ts              # extends BaseEntity + permissions + status types
│   │   └── index.ts
│   ├── services/
│   │   ├── {{module}}-api.service.ts         # extends UniverseDataCrudService<T>
│   │   └── index.ts
│   ├── components/
│   │   ├── {{entity}}-list/
│   │   │   ├── {{entity}}-list.component.ts  # useCrudList + filterDefinitions
│   │   │   ├── {{entity}}-list.component.html # GgsCrudListShell + filter panel
│   │   │   └── {{entity}}-list.component.scss
│   │   ├── {{entity}}-detail/
│   │   │   ├── {{entity}}-detail.component.ts  # useCrudDetail + workflow transitions
│   │   │   ├── {{entity}}-detail.component.html # GgsCrudDetailShell + status transition
│   │   │   └── {{entity}}-detail.component.scss
│   │   └── {{entity}}-form/
│   │       ├── {{entity}}-form.component.ts    # useCrudForm + entity lookups
│   │       ├── {{entity}}-form.component.html  # GgsCrudFormShell + lookup fields
│   │       └── {{entity}}-form.component.scss
│   ├── workflow/                               # NEW
│   │   └── {{entity}}-workflow.definition.ts   # WorkflowDefinition (if hasWorkflow)
│   ├── events/
│   │   └── {{entity}}.events.ts
│   ├── mocks/                                  # NEW
│   │   └── {{entity}}-seed-data.ts             # Seed data for initial population
│   ├── {{module}}.routes.ts
│   ├── {{module}}.module.ts                    # NEW: @ModuleDecorator + BrowserModuleContract
│   └── index.ts
└── project.json

What We Explicitly Do NOT Change

  1. 6-phase pipeline structure — ENHANCE→ANALYZE→DECOMPOSE→PLAN→EXECUTE→INTEGRATE is correct. No need for gmodules1's 13 phases.
  2. UniverseDataService — Already complete with caching, offline, optimistic updates. No changes.
  3. Backend-api dynamic CRUD — Already complete with schemas, seed, query, aggregate. No changes.
  4. VFS/Bundler/Preview — Working correctly. No changes.
  5. NGXS — Do NOT adopt from gmodules1. Keep signals-only.
  6. input() signal inputs — Do NOT use in generated code. Use @Input() decorators for JIT compatibility. Note: SDK components (GgsFilterPanel, GgsEntityLookup, etc.) already use signal input() internally — this is acceptable since they are pre-compiled libraries, not JIT-compiled generated code.

What DOES Need Changes (Prerequisite)

  1. Platform-base-template: @libs-core/workflow-engine is NOT in the template's tsconfig.base.json. Must add the library and its path mapping before generated code can import WorkflowEngineService or GgsStatusTransitionComponent. Also verify @libs-core/event-system is mapped.
  2. JIT compatibility verification: Before adding building block components to the prompt, verify that lib-status-transition, ggs-filter-panel, ggs-filter-chips, and ggs-entity-lookup render correctly under Angular JIT in the rspack WASM preview. These components use signal input() internally — if JIT can't handle them, we need alternative approaches (inline status badges, simple select dropdowns, etc.).

Success Criteria

  1. Generated modules use UniverseDataCrudService<T> (not raw UniverseDataService)
  2. Generated modules include @ModuleDecorator + BrowserModuleContract
  3. Entities with status fields get WorkflowDefinition + GgsStatusTransitionComponent
  4. List components include filterDefinitions with appropriate field filters
  5. Foreign key fields use GgsEntityLookupComponent instead of text inputs
  6. Schema registered in backend-api during INTEGRATE phase
  7. 3-5 seed records per entity, seeded via /data/:collection/seed
  8. Generations tab shows completed generations with module list
  9. Generated code compiles in rspack WASM preview without errors
  10. Full CRUD cycle works: Create → Read → Update → Delete against backend-api:3000

Risks & Mitigations

Risk Mitigation
Prompt grows too long → LLM quality degrades Use conditional sections: only include workflow example if hasWorkflow: true in spec
Schema registration fails silently Add error handling in INTEGRATE phase, emit SSE error event
Entity lookup references cross-module entities that don't exist yet Generate lookup with graceful fallback (text input if lookup service unavailable)
JIT incompatibility with signal-input SDK components BLOCKER CHECK: Test lib-status-transition, ggs-filter-panel, ggs-entity-lookup in preview before adding to prompt. Fallback: use inline status badges + simple selects
Seed data doesn't match schema Generate seed data and schema from same entity spec (single source of truth)
Seed data creates duplicates on re-generation Seed records MUST include stable _id values (e.g., seed-{entity}-001). Backend upserts by _id only
workflow-engine missing from template Add library to platform-base-template BEFORE prompt changes

Dependencies


Files to Modify

File Change
repo-templates/platform-base-template/tsconfig.base.json Add @libs-core/workflow-engine path mapping
repo-templates/platform-base-template/libs/core/workflow-engine/ Add workflow-engine library (copy from gspace1)
APIs/backend-vitto-api/src/prompts/module-gen/implement.prompt.md Add 5 building block sections with verified golden examples
APIs/backend-vitto-api/src/prompts/module-gen/system-analyze.prompt.md Add buildingBlocks output to entity analysis
APIs/backend-vitto-api/src/app/module-generation/phases/execute.phase.ts Pass building block metadata to LLM
APIs/backend-vitto-api/src/app/module-generation/phases/integrate.phase.ts Add schema registration + seed data POST
APIs/backend-vitto-api/src/app/module-generation/module-generation.types.ts Add buildingBlocks, schemas, seedData types
Generations tab component (TBD after investigation) Fix "No generations yet" display bug