feat(server,kernel): bind ProjectProfile per session and inject as L0 context
This commit is contained in:
@@ -11,10 +11,12 @@ import com.correx.core.approvals.DefaultApprovalReducer
|
||||
import com.correx.core.approvals.DefaultApprovalRepository
|
||||
import com.correx.core.artifactstore.ArtifactStore
|
||||
import com.correx.core.config.OperatorProfile
|
||||
import com.correx.core.config.ProjectProfileLoader
|
||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||
import com.correx.core.events.events.EventMetadata
|
||||
import com.correx.core.events.events.NewEvent
|
||||
import com.correx.core.events.events.OperatorProfileBoundEvent
|
||||
import com.correx.core.events.events.ProjectProfileBoundEvent
|
||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||
import com.correx.core.events.events.WorkflowFailedEvent
|
||||
import com.correx.core.events.stores.EventStore
|
||||
@@ -217,6 +219,35 @@ class ServerModule(
|
||||
),
|
||||
)
|
||||
}
|
||||
// Bind the per-repo project profile (curated .correx/project.toml) as an event so
|
||||
// stages and replay read the recorded snapshot, never the live file (invariants #8/#9).
|
||||
val workspaceRoot = runCatching {
|
||||
sessionRepository.getSession(sessionId).state.boundWorkspace?.workspaceRoot
|
||||
}.getOrNull() ?: projectMemory?.repoRoot()
|
||||
workspaceRoot?.let { root ->
|
||||
val projectProfile = ProjectProfileLoader.load(root)
|
||||
if (!projectProfile.isEmpty()) {
|
||||
eventStore.append(
|
||||
NewEvent(
|
||||
metadata = EventMetadata(
|
||||
eventId = EventId(java.util.UUID.randomUUID().toString()),
|
||||
sessionId = sessionId,
|
||||
timestamp = Clock.System.now(),
|
||||
schemaVersion = 1,
|
||||
causationId = null,
|
||||
correlationId = null,
|
||||
),
|
||||
payload = ProjectProfileBoundEvent(
|
||||
sessionId = sessionId,
|
||||
workspaceRoot = root,
|
||||
about = projectProfile.about,
|
||||
conventions = projectProfile.conventions,
|
||||
commands = projectProfile.commands,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
runCatching {
|
||||
orchestrator.run(sessionId, graph, sessionConfig)
|
||||
// After a successful freestyle planning run, lock the plan and execute phase 2.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.correx.core.kernel.orchestration
|
||||
|
||||
// Context-entry builders kept top-level (not orchestrator members) for unit-testability
|
||||
// and to stay under detekt's function-count threshold (same pattern as isBackEdge).
|
||||
|
||||
import com.correx.core.context.model.ContextEntry
|
||||
import com.correx.core.context.model.ContextLayer
|
||||
import com.correx.core.context.model.EntryRole
|
||||
import com.correx.core.events.types.ContextEntryId
|
||||
import com.correx.core.sessions.BoundProjectProfile
|
||||
import java.util.UUID
|
||||
|
||||
fun buildProjectProfileEntry(profile: BoundProjectProfile): ContextEntry {
|
||||
val content = buildString {
|
||||
append("## Project profile\n")
|
||||
if (profile.about.isNotBlank()) append(profile.about).append("\n")
|
||||
if (profile.conventions.isNotEmpty()) {
|
||||
append("### Conventions\n")
|
||||
profile.conventions.forEach { append("- ").append(it).append("\n") }
|
||||
}
|
||||
if (profile.commands.isNotEmpty()) {
|
||||
append("### Commands\n")
|
||||
profile.commands.forEach { (name, cmd) -> append(name).append(": ").append(cmd).append("\n") }
|
||||
}
|
||||
}.trimEnd()
|
||||
return ContextEntry(
|
||||
id = ContextEntryId(UUID.randomUUID().toString()),
|
||||
layer = ContextLayer.L0,
|
||||
content = content,
|
||||
sourceType = "projectProfile",
|
||||
sourceId = "project-profile",
|
||||
tokenEstimate = content.length / 4,
|
||||
role = EntryRole.SYSTEM,
|
||||
)
|
||||
}
|
||||
+4
-2
@@ -363,9 +363,11 @@ abstract class SessionOrchestrator(
|
||||
),
|
||||
)
|
||||
} ?: emptyList()
|
||||
val projectProfileEntries = session.state.boundProjectProfile
|
||||
?.let { listOf(buildProjectProfileEntry(it)) } ?: emptyList()
|
||||
var accumulatedEntries =
|
||||
systemPrompt + profileEntries + journalEntries + repoMapEntries + needsEntries +
|
||||
schemaEntries + promptEntries + steeringEntries
|
||||
systemPrompt + profileEntries + projectProfileEntries + journalEntries + repoMapEntries +
|
||||
needsEntries + schemaEntries + promptEntries + steeringEntries
|
||||
val contextPack = contextPackBuilder.build(
|
||||
id = ContextPackId(UUID.randomUUID().toString()),
|
||||
sessionId = sessionId,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import com.correx.core.context.model.ContextLayer
|
||||
import com.correx.core.context.model.EntryRole
|
||||
import com.correx.core.kernel.orchestration.buildProjectProfileEntry
|
||||
import com.correx.core.sessions.BoundProjectProfile
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ContextFeedbackTest {
|
||||
|
||||
@Test
|
||||
fun `project profile renders as single L0 entry`() {
|
||||
val entry = buildProjectProfileEntry(
|
||||
BoundProjectProfile(
|
||||
about = "Event-sourced kernel",
|
||||
conventions = listOf("no bare try-catch"),
|
||||
commands = mapOf("test" to "./gradlew check"),
|
||||
),
|
||||
)
|
||||
assertEquals(ContextLayer.L0, entry.layer)
|
||||
assertEquals(EntryRole.SYSTEM, entry.role)
|
||||
assertTrue(entry.content.startsWith("## Project profile"), "content: ${entry.content}")
|
||||
assertTrue(entry.content.contains("- no bare try-catch"), "content: ${entry.content}")
|
||||
assertTrue(entry.content.contains("test: ./gradlew check"), "content: ${entry.content}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `project profile with only about renders without conventions or commands sections`() {
|
||||
val entry = buildProjectProfileEntry(
|
||||
BoundProjectProfile(
|
||||
about = "Simple project",
|
||||
conventions = emptyList(),
|
||||
commands = emptyMap(),
|
||||
),
|
||||
)
|
||||
assertTrue(entry.content.contains("Simple project"), "content: ${entry.content}")
|
||||
assertTrue(!entry.content.contains("### Conventions"), "should not contain Conventions: ${entry.content}")
|
||||
assertTrue(!entry.content.contains("### Commands"), "should not contain Commands: ${entry.content}")
|
||||
assertEquals("projectProfile", entry.sourceType)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user