# task-012

status: done

## goal
Implement RouterContextBuilder — assembles a budget-constrained ContextPack from L2 memory (stage summaries), workflow status events, and conversation history, respecting L0 immutability and L2 eviction ordering.

## target artifact
core/router/src/main/kotlin/com/correx/core/router/RouterContextBuilder.kt

## execution steps
1. **Create `RouterContextBuilder` interface** in `RouterContextBuilder.kt` at `core/router/src/main/kotlin/com/correx/core/router/RouterContextBuilder.kt`
   - Interface signature: `fun build(state: RouterState, budget: TokenBudget): ContextPack`
   - Package: `com.correx.core.router`
   - Imports: `RouterState` from `com.correx.core.router.model`, `TokenBudget` and `ContextPack` from `com.correx.core.context.model`

2. **Create `DefaultRouterContextBuilder` class** in the same file
   - Constructor receives: `RouterConfig` (inject dependency, never instantiate)
   - `build()` assembles a `Map<ContextLayer, List<ContextEntry>>` layer by layer:
     a. **L0 entries** (always included, never dropped under budget pressure):
        - System prompt entry: `layer = L0`, `sourceType = "systemPrompt"`, content = `RouterContextBuilder.SYSTEM_PROMPT` (private const String)
        - Workflow status entry: `layer = L0`, `sourceType = "workflowStatus"`, content = `"Status: ${state.workflowStatus}, Stage: ${state.currentStageId ?: "none"}"`
        - Compute combined L0 token estimate; deduct from budget (remaining = max(0, budget.limit - l0Tokens))
     b. **L1 entries** (conversation history):
        - Take `state.conversationHistory.takeLast(config.keepLast)`
        - Convert each `RouterTurn` to a `ContextEntry` with `layer = L1`, `sourceType = "conversation"`
        - Content format: `"[${role.name}] ${content}"`
        - Deduct L1 token estimates from remaining budget, stop if budget exhausted
     c. **L2 entries** (stage summaries from L2 memory):
        - Iterate `state.l2Memory` in insertion order (oldest first)
        - Convert each `RouterL2Entry` to a `ContextEntry` with `layer = L2`, `sourceType = "stageSummary"`
        - Content format: `"Stage ${stageId} (${outcome}): ${summary}"`
        - Budget enforcement: if an entry's `tokenEstimate` exceeds remaining budget, skip it and continue (oldest-first eviction)
   - Assemble `ContextPack` with:
     - `id` = new `ContextPackId` (generate a deterministic ID from sessionId + "router")
     - `sessionId` = from state
     - `stageId` = from state (currentStageId, or empty StageId if null)
     - `layers` = the grouped Map<ContextLayer, List<ContextEntry>>
     - `budgetUsed` = sum of all retained entries' tokenEstimate
     - `budgetLimit` = budget.limit
     - `compressionMetadata` = `CompressionMetadata(appliedStrategies = listOf("L0Immutable", "Conversation"), truncatedLayers = emptyList(), entriesDropped = droppedCount)`
   - Token estimation per entry:
     - System prompt: `SYSTEM_PROMPT.length / 2` (rough heuristic: ~2 chars per token)
     - Workflow status: content length / 2
     - Conversation: content length / 2
     - Stage summary: content length / 2
     - Use `estimateTokens(content: String): Int` helper method for consistency

3. **Verify no raw event payload, artifact content, or tool outputs** enter the context pack
   - The builder only consumes `RouterState` (which is derived from events by the reducer) and produces clean `ContextEntry` objects
   - No direct `EventPayload` references in the builder

4. **Verify L0 entries are never dropped**
   - L0 entries are computed first, budget is deducted, then L1/L2 entries are added only if budget permits
   - L0 entries remain regardless of budget exhaustion

5. **Verify compilation** with `./gradlew :core:router:compileKotlin`

## changed artifacts
- `core/router/src/main/kotlin/com/correx/core/router/RouterContextBuilder.kt` — new file (interface + implementation)
- `core/router/build.gradle` — added Kover plugin and disabled KoverVerify (no tests yet; tests are task-022)

## blockers
- None — task-006 (RouterState) and task-007 (RouterConfig) are complete and the artifact files exist

## notes
- The epic spec (epic-14, task 3) suggests placing files under `context/` subdirectory, but the task plan file specifies the root `core/router/.../RouterContextBuilder.kt` path. Following the task plan path.
- The builder does NOT implement inference routing or event writing — those are task-013 (RouterFacade) concerns.
- The `SYSTEM_PROMPT` constant should be concise and non-authoritative, reflecting a conversational assistant role. Example: `"You are a routing assistant. Provide guidance based on workflow state and conversation context."`
- The `ContextPackId` for router packs should be deterministic and traceable — use `ContextPackId("${state.sessionId?.value ?: "unknown"}-router-pack")` for testability.
- If `state.sessionId` is null (initial state), the build should still succeed — use a fallback ID component like `"unknown"` for the pack ID.
