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:

  1. Surface generations to users — branch names are opaque timestamps; no UI lists them
  2. Activate a specific generation — auto-merge to develop fails silently when develop has diverged; generations become invisible orphans
  3. Separate manual edits from generated code — edits go directly to develop, conflicting with new generations
  4. 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:

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:


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?"

Q2: Does this universe have an active generation?

NO → Path 1: First Generation

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

Path B: Evolve

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


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:

  1. Vitto calls the publish-draft endpoint → MR created
  2. If conflicts: user resolves in GitLab → Vitto waits
  3. After merge confirmed: Vitto re-enters the generation decision flow This cannot be a single publishDraftFirst flag 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:

  1. User opens Studio → VFS loads from develop (read-only context)
  2. User edits a file → VfsSyncService detects no active draft
  3. VfsSyncService creates draft/edit-{universeSlug}-{Date.now()} from develop via GitLab API
  4. VfsSyncService switches repoContext to the new draft branch
  5. Commit goes to the draft branch
  6. Subsequent edits commit to the same draft branch
  7. DraftSession document created in MongoDB

What counts as an "edit"

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

  1. User clicks "Publish Draft" in Studio
  2. System creates GitLab MR: draft/edit-xxx → develop
  3. If no conflicts: auto-merge (or user confirms)
  4. If conflicts: show MR URL, user resolves in GitLab
  5. Draft manifest: active → published
  6. 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:

  1. Pre-check: Verify branch exists in GitLab (GET /projects/{id}/repository/branches/{name})
  2. Merge: Call GitLab merge API (PUT /projects/{id}/repository/branches/{name}/merge) or create+merge MR
  3. On success:
    • Store mergeCommitSha in 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
  4. On conflict:
    • Create draft MR in GitLab
    • Store mrUrl in manifest
    • Show: "Activation has conflicts. Review and merge the MR: {mrUrl}"
    • Manifest stays ready until 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:

  1. Active generation (highlighted): title, module count, activated date
  2. Ready generations (can activate): title, modules, "Activate" button
  3. Generating (in progress): phase, progress bar
  4. Archived: collapsed list, can re-activate
  5. 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

  1. On first commit: check if active draft exists → if not, create draft branch + DraftSession
  2. repoContext.branch = draft branch name (not develop)
  3. On Studio load: check for active draft → resume it; or show develop (read-only)

StudioDockShellComponent changes

  1. Load generations on init (alongside existing module loading)
  2. Subscribe to generation.activated SSE event → reload VFS
  3. Show branch indicator in toolbar

Notification System

When a generation completes (status: generating → ready):

  1. SSE event through existing VittoChatEngineService: generation.ready
  2. Studio banner: "New generation ready: {title} ({N} modules). [Activate] [View]"
  3. If first generation: auto-activate, no banner needed

When a generation is activated:

  1. SSE event: generation.activated
  2. Studio reloads: VFS switches to develop, preview recompiles
  3. 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:

  1. Remove the mergeBranch() call from the DEPLOY phase
  2. Instead, after the commit, save a GenerationManifest with status: 'ready'
  3. For first generation (no prior active manifest for this universe): auto-activate (preserves current seamless UX)
  4. 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

  1. User generates a platform → manifest appears in Generations panel with status 'ready'
  2. User clicks "Activate" → develop updated → Studio reloads with new content → preview compiles
  3. User edits code → commits go to auto-created draft branch (not develop)
  4. User publishes draft → MR to develop → changes merged
  5. User generates again (Fresh) → new generation on separate branch → old generation archived
  6. User sees all generations with metadata: title, modules, date, status
  7. Studio toolbar shows current branch name at all times
  8. Stale drafts detected and warned on Studio load