2026 03 17 Generation Versioning Drafts Design
Documentation for Plans
Platform Generation Versioning & Draft System
Date: 2026-03-17 Session: S231 Status: Approved for implementation Approach: Option B (Full — Generation Registry + Draft Branch System + Notifications)
Problem Statement
The platform generation pipeline creates feature/generation-{timestamp} branches in GitLab but has no mechanism to:
- Surface generations to users — branch names are opaque timestamps; no UI lists them
- Activate a specific generation — auto-merge to develop fails silently when develop has diverged; generations become invisible orphans
- Separate manual edits from generated code — edits go directly to develop, conflicting with new generations
- Navigate version history — no way to see what was generated, when, or switch between versions
Industry research (GitHub Copilot Agent, Replit Agent Checkpoints, Lovable.dev) converges on a single principle: AI-generated code and human-edited code are different artifacts tracked separately, merged only with explicit human approval.
Core Principle
develop (sacred)
= latest activated generation + published drafts only
= never written to directly by Studio or generation pipeline
feature/generation-{ts}
= immutable AI-generated platform snapshot
= one per generation run
= activated (merged to develop) by explicit user action
draft/edit-{universeSlug}-{ts}
= user's manual work (code edits, Vitto module-level changes)
= created on first commit, not on Studio open
= unlimited per universe
= published to develop via MR
= marked stale when develop changes (new generation activated)
Data Model
GenerationManifest (MongoDB collection: generation_manifests)
{
generationId: string; // 'platform-{uuid}' from PlatformGenerationService
spaceId: string; // ObjectId ref
universeId: string; // ObjectId ref
branchName: string; // 'feature/generation-1773785812251'
baseBranch: string; // 'develop'
baseCommitSha: string; // SHA of develop when generation started
// Human-readable
title: string; // 'Parental Management Platform'
commitMessage: string; // 'feat: generate 5 modules (...)'
description: string; // Original user prompt to Vitto
// Content
modules: [{
slug: string; // 'family-household'
displayName: string; // 'Family & Household'
entityCount: number;
componentCount: number;
}];
totalFiles: number;
totalModules: number;
// Lifecycle
status: 'generating' | 'ready' | 'active' | 'archived' | 'failed';
activatedAt?: Date;
activatedBy?: string; // userId
mergeCommitSha?: string; // SHA on develop after activation
mrUrl?: string; // GitLab MR URL if merge-conflict fallback
failureReason?: string;
createdAt: Date;
updatedAt: Date;
}
Constraints:
- Exactly ONE manifest per universe can have
status: 'active'at any time - Zero is valid (fresh universe, no generation activated yet)
- Stale check:
status: 'generating'ANDcreatedAt > 30 minutes ago→ auto-markfailed
DraftSession (MongoDB collection: draft_sessions)
{
draftId: string; // 'draft-{uuid}'
spaceId: string;
universeId: string;
branchName: string; // 'draft/edit-gatito-uni-1773790000000'
baseBranch: string; // 'develop'
baseCommitSha: string; // SHA of develop when draft was created
userId: string;
editCount: number; // incremented on each commit
lastEditAt: Date;
lastEditFile?: string; // path of most recently edited file
status: 'active' | 'published' | 'stale' | 'deleted';
publishedMrUrl?: string; // GitLab MR URL when published
createdAt: Date;
updatedAt: Date;
}
Constraints:
- Unlimited active + stale drafts per universe (no cap)
- Per-user drafts — each user gets their own draft branches
- Never auto-deleted; user decides cleanup
State Machines
GenerationManifest Lifecycle
generating → ready (generation completed)
generating → failed (error or timeout > 30 min)
ready → active (user clicks Activate)
ready → archived (different generation activated)
active → archived (newer generation activated)
failed → [removed] (user deletes — DELETE endpoint)
archived → [removed] (user cleans up old generations — DELETE endpoint)
Note: archived → active (re-activation of old generation) is deferred to v2. Merging an old generation branch into a develop that has newer content creates frequent conflicts. Users should re-generate using the old description if they want a previous platform shape.
DraftSession Lifecycle
active → published (MR merged to develop)
active → stale (develop changed — generation activated)
active → deleted (user discards)
stale → deleted (user cleans up)
published → [removed] (auto-cleanup after merge)
Generation Decision Tree
When a user requests "generate a platform," Vitto assesses the current state:
Q1: Is a generation currently running?
YES → "A generation is in progress ({phase}, {N}/{total} modules). Wait for completion, or cancel it?"
[Wait]→ Vitto monitors and reports when done[Cancel & Start New]→ cancel running generation (mark manifestfailed), then proceed to Q2
Q2: Does this universe have an active generation?
NO → Path 1: First Generation
- Generate → branch from develop → auto-activate (no conflicts, develop = template base)
- No user decision needed
YES → Q3
Q3: Does the user have active draft edits?
NO → Vitto asks:
"You have a {title} platform with {N} modules. Would you like to:"
- [A] Start Fresh — new platform, replaces everything
- [B] Evolve — keep existing modules, add new ones
- [C] Cancel
YES → Vitto asks:
"You have a {title} platform with {N} modules, AND {editCount} unpublished edits on your draft. Would you like to:"
- [A] Start Fresh — new platform (drafts become stale)
- [B] Evolve — keep modules, add new ones (drafts stay valid)
- [C] Publish drafts first — save your work to develop, then generate
- [D] Cancel
Path A: Start Fresh
- Generation produces a COMPLETE platform (all modules, all routes)
- Branches from develop, replaces
modules/andapp.routes.ts - On activation: merge to develop (existing content overwritten)
- Old generation manifest:
active → archived - Existing drafts:
active → stale
Path B: Evolve
- Generation produces a COMPLETE platform that INCLUDES existing modules + new ones
- The LLM receives existing module specs as context (names, entities, routes)
- Same branching/activation as Fresh — the difference is purely in the LLM prompt
- On activation: merge to develop (same process as Fresh)
- Existing drafts: still valid if new content is additive; marked stale if develop diverged significantly
Important: "Evolve" does NOT do partial generation. The LLM produces a complete platform. The only difference from "Fresh" is the prompt context telling the LLM to preserve existing modules. Risk: LLM might subtly modify existing modules. Mitigation: Generations panel shows module diff summary before activation.
Path C: Publish Drafts First
- Step 1: Create MR from
draft/edit-xxx→ develop. User reviews and merges. - Step 2: Re-enter generation decision (Q2) with clean state.
Generation Intent Flags
Passed from Vitto's decision logic to the generation pipeline:
interface GenerationIntent {
mode: 'fresh' | 'evolve';
autoActivate: boolean; // true for first-gen only
existingModules?: { // populated for 'evolve' mode
slug: string;
displayName: string;
entities: string[];
}[];
}
Note: "Publish drafts first" (Path C) is a multi-step user-driven flow, not a pipeline flag. Vitto guides the user:
- Vitto calls the publish-draft endpoint → MR created
- If conflicts: user resolves in GitLab → Vitto waits
- After merge confirmed: Vitto re-enters the generation decision flow
This cannot be a single
publishDraftFirstflag because MR merge is async and may require manual conflict resolution.
Draft Branch System
When drafts are created
A draft branch is created on first VfsSyncService commit to GitLab, not on Studio open or first keystroke. This avoids empty branches.
Flow:
- User opens Studio → VFS loads from develop (read-only context)
- User edits a file → VfsSyncService detects no active draft
- VfsSyncService creates
draft/edit-{universeSlug}-{Date.now()}from develop via GitLab API - VfsSyncService switches
repoContextto the new draft branch - Commit goes to the draft branch
- Subsequent edits commit to the same draft branch
- DraftSession document created in MongoDB
What counts as an "edit"
- Manual file edit in Code panel → draft commit
- Vitto module-level regeneration via chat ("improve the Task Manager") → draft commit
- Any
VfsSyncService.syncChangeToBackend()that would write to GitLab
Draft branch indicator
Studio toolbar shows: "Branch: draft/edit-gatito-uni-xxx" or "Branch: develop (viewing activated generation)" so the user always knows where they are.
Publishing a draft
- User clicks "Publish Draft" in Studio
- System creates GitLab MR:
draft/edit-xxx → develop - If no conflicts: auto-merge (or user confirms)
- If conflicts: show MR URL, user resolves in GitLab
- Draft manifest:
active → published - Studio reloads VFS from develop
Stale draft detection
On every Studio load, two-tier check:
Tier 1 (fast): SHA comparison
if (draft.baseCommitSha !== currentDevelopHead) → draft MIGHT be stale
Tier 2 (accurate, runs only if Tier 1 triggers): File overlap check via GitLab compare API
changedOnDevelop = GitLab compare(draft.baseCommitSha, developHead).changedFiles
changedOnDraft = GitLab compare(draft.baseCommitSha, draftHead).changedFiles
overlap = intersection(changedOnDevelop, changedOnDraft)
if (overlap.length > 0) {
draft.status = 'stale';
show banner: "Your draft edits {overlap} conflict with the activated generation."
} else {
// develop moved but no file overlap — draft is safe
show info: "A new generation was activated. Your edits don't conflict."
}
This prevents false stale warnings when a generation adds Module B but the user's draft only edits Module A (no file overlap = no conflict).
Activation Flow
When user clicks "Activate" on a generation in the Generations panel:
- Pre-check: Verify branch exists in GitLab (
GET /projects/{id}/repository/branches/{name}) - Merge: Call GitLab merge API (
PUT /projects/{id}/repository/branches/{name}/merge) or create+merge MR - On success:
- Store
mergeCommitShain manifest - Set manifest
status: 'active',activatedAt: now - Archive the previously active generation (if any)
- Mark all active drafts for this universe as
stale - Emit SSE event:
generation.activated - Studio reloads VFS from develop
- Store
- On conflict:
- Create draft MR in GitLab
- Store
mrUrlin manifest - Show: "Activation has conflicts. Review and merge the MR: {mrUrl}"
- Manifest stays
readyuntil MR is merged
First generation auto-activation
When status: 'ready' AND no other generation is active for this universe → auto-activate. No user action needed. This handles the first-generation case cleanly.
API Endpoints
backend-api (port 3000)
GET /api/spaces/:spaceId/universes/:universeId/generations
→ List all generation manifests for this universe
GET /api/spaces/:spaceId/universes/:universeId/generations/active
→ Get the currently active generation (or null)
POST /api/spaces/:spaceId/universes/:universeId/generations
→ Create a new generation manifest (called by vitto-api DEPLOY phase)
PATCH /api/spaces/:spaceId/universes/:universeId/generations/:id
→ Update manifest (status changes, activation)
POST /api/spaces/:spaceId/universes/:universeId/generations/:id/activate
→ Activate a generation (merge to develop, update statuses)
GET /api/spaces/:spaceId/universes/:universeId/drafts
→ List all draft sessions for this universe
POST /api/spaces/:spaceId/universes/:universeId/drafts
→ Create a new draft session
PATCH /api/spaces/:spaceId/universes/:universeId/drafts/:id
→ Update draft (editCount, status)
POST /api/spaces/:spaceId/universes/:universeId/drafts/:id/publish
→ Publish draft (create MR to develop)
DELETE /api/spaces/:spaceId/universes/:universeId/generations/:id
→ Remove a generation manifest (only failed/archived)
DELETE /api/spaces/:spaceId/universes/:universeId/drafts/:id
→ Remove a draft session (deletes GitLab branch if exists)
backend-vitto-api (port 3003) — existing endpoint changes
module-gen-phase.executor.ts DEPLOY phase:
→ After branch creation + commit, call backend-api to create GenerationManifest
→ Pass: generationId, branchName, modules, totalFiles, commitMessage, description
Frontend Components
StudioGenerationsPanelComponent (new dock panel)
Registers as studio-generations in STUDIO_DOCK_PANELS. Shows:
- Active generation (highlighted): title, module count, activated date
- Ready generations (can activate): title, modules, "Activate" button
- Generating (in progress): phase, progress bar
- Archived: collapsed list, can re-activate
- Failed: error message, retry/delete options
GenerationService (new frontend service)
@Injectable({ providedIn: 'root' })
export class GenerationService {
// Signals — universe-scoped. Cleared on universe change via clearForUniverse().
readonly generations = signal<GenerationManifest[]>([]);
readonly activeGeneration = signal<GenerationManifest | null>(null);
readonly drafts = signal<DraftSession[]>([]);
readonly activeDraft = signal<DraftSession | null>(null);
private currentUniverseId: string | null = null;
// All methods take spaceId + universeId — service clears signals on universe change
loadGenerations(spaceId: string, universeId: string): void;
activateGeneration(spaceId: string, universeId: string, genId: string): Observable<void>;
loadDrafts(spaceId: string, universeId: string): void;
createDraft(spaceId: string, universeId: string): Observable<DraftSession>;
publishDraft(spaceId: string, universeId: string, draftId: string): Observable<void>;
deleteDraft(spaceId: string, universeId: string, draftId: string): Observable<void>;
deleteGeneration(spaceId: string, universeId: string, genId: string): Observable<void>;
// Called by StudioDockShellComponent on init — clears stale state if universe changed
clearForUniverse(universeId: string): void {
if (this.currentUniverseId !== universeId) {
this.generations.set([]);
this.activeGeneration.set(null);
this.drafts.set([]);
this.activeDraft.set(null);
this.currentUniverseId = universeId;
}
}
}
Note: providedIn: 'root' is kept (consistent with other studio services like VfsService, PreviewService). Universe-scoping is handled by clearForUniverse() called on init, not by component-level providers.
VfsSyncService changes
- On first commit: check if active draft exists → if not, create draft branch + DraftSession
repoContext.branch= draft branch name (not develop)- On Studio load: check for active draft → resume it; or show develop (read-only)
StudioDockShellComponent changes
- Load generations on init (alongside existing module loading)
- Subscribe to
generation.activatedSSE event → reload VFS - Show branch indicator in toolbar
Notification System
When a generation completes (status: generating → ready):
- SSE event through existing VittoChatEngineService:
generation.ready - Studio banner: "New generation ready: {title} ({N} modules). [Activate] [View]"
- If first generation: auto-activate, no banner needed
When a generation is activated:
- SSE event:
generation.activated - Studio reloads: VFS switches to develop, preview recompiles
- Draft stale warning: if user has active drafts
Files to Create/Modify
New files (~10)
| File | Purpose |
|---|---|
APIs/backend-api/src/app/generations/generation-manifest.schema.ts |
Mongoose schema |
APIs/backend-api/src/app/generations/draft-session.schema.ts |
Mongoose schema |
APIs/backend-api/src/app/generations/generations.service.ts |
CRUD + activation logic |
APIs/backend-api/src/app/generations/generations.controller.ts |
REST endpoints |
APIs/backend-api/src/app/generations/generations.module.ts |
NestJS module |
libs/libs-structural/vitto-studio/src/lib/services/generation.service.ts |
Frontend API client |
libs/libs-structural/vitto-studio/src/lib/panels/generations-panel.component.ts |
Dock panel |
Modified files (~8)
| File | Change |
|---|---|
APIs/backend-api/src/app/app.module.ts |
Import GenerationsModule |
APIs/backend-vitto-api/.../module-gen-phase.executor.ts |
Save manifest on DEPLOY |
libs/.../vitto-studio/.../vfs-sync.service.ts |
Draft branch auto-creation |
libs/.../vitto-studio/.../studio-dock-shell.component.ts |
Load generations, subscribe to events |
libs/.../vitto-studio/.../config/studio-dock-panels.config.ts |
Register generations panel |
libs/.../vitto-studio/.../services/studio-api.service.ts |
Add generation/draft API methods |
libs/.../vitto-studio/.../components/studio-toolbar/studio-toolbar.component.ts |
Branch indicator |
libs/.../vitto-studio/.../services/studio-generation-listener.service.ts |
Handle generation.ready + generation.activated SSE events |
Migration from Current Behavior
Auto-merge removal
The current module-gen-phase.executor.ts (lines 625-662) auto-merges feature/generation-{ts} to develop immediately on completion. This spec removes that behavior.
Migration:
- Remove the
mergeBranch()call from the DEPLOY phase - Instead, after the commit, save a
GenerationManifestwithstatus: 'ready' - For first generation (no prior active manifest for this universe): auto-activate (preserves current seamless UX)
- For subsequent generations: leave on feature branch, user activates via Generations panel
This is a deliberate workflow change. The rationale: once a user has an active platform, replacing it without review is destructive. First generation remains instant.
Existing generationManifest field on Universe documents
The current structure-builder.service.ts writes a generationManifest field directly onto Universe documents via PUT /spaces/:spaceId/universes/:universeId. This contains modules, navigation, files, quality, and deployment info.
Relationship: The new generation_manifests collection is the canonical registry. The existing generationManifest field on Universe documents remains as a denormalized snapshot of the active generation for backward compatibility. When a generation is activated, the Universe document's generationManifest field is updated to match. Both coexist; the collection is the source of truth for history, the document field is the source of truth for "current."
Cross-service dependency (vitto-api → backend-api)
The DEPLOY phase runs on vitto-api (port 3003). The new GenerationManifest lives on backend-api (port 3000).
Resolution: vitto-api already has BackendApiClientService (in connectivity/) that calls backend-api. The manifest creation call uses this existing client: POST /api/spaces/:spaceId/universes/:universeId/generations.
What is NOT in scope (v1)
| Feature | Reason |
|---|---|
| Rollback to previous generation | Complex merge conflicts; users can re-generate. archived → active is removed from v1 state machine |
| Rebasing stale drafts | Too risky; create new draft instead |
| Conflict resolution UI | GitLab handles this; surface MR URL |
| Multi-user approval workflows | No multi-user yet |
| Code diff between generations | Future enhancement for Generations panel |
| Auto-expire stale drafts | Users decide cleanup |
| Partial "Evolve" generation | Full generation with context is simpler |
| Cancel running generation | No abort signal in pipeline; manifest can be marked failed but pipeline runs to completion |
Success Criteria
- User generates a platform → manifest appears in Generations panel with status 'ready'
- User clicks "Activate" → develop updated → Studio reloads with new content → preview compiles
- User edits code → commits go to auto-created draft branch (not develop)
- User publishes draft → MR to develop → changes merged
- User generates again (Fresh) → new generation on separate branch → old generation archived
- User sees all generations with metadata: title, modules, date, status
- Studio toolbar shows current branch name at all times
- Stale drafts detected and warned on Studio load