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:
- Maps
PaginationRequest→QueryFilter(page/pageSize/sort/search/filters) - Maps
ActiveFilter[]operators → MongoDB operators (eq→direct,contains→$regex,in→$in) - Returns
CrudPaginatedResult<T>→{ items: T[], totalItems: number }
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:
WorkflowDefinitionis 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(notggs-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:
GgsFilterPanelComponentinput isfilters(notdefinitions), output isfiltersChange(notfilterChange).GgsFilterChipsComponentinput isactiveFilters(notfilters), output isremoveFilter(notremove).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:
BrowserModuleContractrequires amanifest: ModuleManifestproperty ANDconfigure<T>()takes a generic config parameter. The manifest in@ModuleDecoratoris 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:
- Field named
statuswith enum values →hasWorkflow: true - Field named
*Idreferencing another entity →foreignKeys[] - Field named
image*,photo*,media*,file*,attachment*→hasMedia: true - Fields with enum/select values →
filterableFields[]
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:
GenerationService.generationssignal may not be populated from SSE events- The
module-gen.completedevent may not write to the generations manifest - The tab component may read from a different source than where generations are stored
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
- 6-phase pipeline structure — ENHANCE→ANALYZE→DECOMPOSE→PLAN→EXECUTE→INTEGRATE is correct. No need for gmodules1's 13 phases.
- UniverseDataService — Already complete with caching, offline, optimistic updates. No changes.
- Backend-api dynamic CRUD — Already complete with schemas, seed, query, aggregate. No changes.
- VFS/Bundler/Preview — Working correctly. No changes.
- NGXS — Do NOT adopt from gmodules1. Keep signals-only.
input()signal inputs — Do NOT use in generated code. Use@Input()decorators for JIT compatibility. Note: SDK components (GgsFilterPanel, GgsEntityLookup, etc.) already use signalinput()internally — this is acceptable since they are pre-compiled libraries, not JIT-compiled generated code.
What DOES Need Changes (Prerequisite)
- Platform-base-template:
@libs-core/workflow-engineis NOT in the template'stsconfig.base.json. Must add the library and its path mapping before generated code can importWorkflowEngineServiceorGgsStatusTransitionComponent. Also verify@libs-core/event-systemis mapped. - JIT compatibility verification: Before adding building block components to the prompt, verify that
lib-status-transition,ggs-filter-panel,ggs-filter-chips, andggs-entity-lookuprender correctly under Angular JIT in the rspack WASM preview. These components use signalinput()internally — if JIT can't handle them, we need alternative approaches (inline status badges, simple select dropdowns, etc.).
Success Criteria
- Generated modules use
UniverseDataCrudService<T>(not rawUniverseDataService) - Generated modules include
@ModuleDecorator+BrowserModuleContract - Entities with status fields get
WorkflowDefinition+GgsStatusTransitionComponent - List components include
filterDefinitionswith appropriate field filters - Foreign key fields use
GgsEntityLookupComponentinstead of text inputs - Schema registered in backend-api during INTEGRATE phase
- 3-5 seed records per entity, seeded via
/data/:collection/seed - Generations tab shows completed generations with module list
- Generated code compiles in rspack WASM preview without errors
- 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
- Backend-api running on port 3000 (already standard for dev)
- Backend-vitto-api running on port 3003 (already standard for dev)
- Platform-base-template on GitLab — NEEDS UPDATE: add
@libs-core/workflow-enginepath mapping and library files - gmodules1 patterns as reference (read-only, no code changes there)
- JIT compatibility test — Must verify signal-input SDK components work in preview before shipping
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 |