feat(server): activate architect contradiction-check hook (B§4)
Wire the display-only ArchitectContradictionChecker (eae0a0c) live: ServerModule.start()
subscribes to the architect stage's `design` ArtifactCreatedEvent, resolves the decision
text (prefers the design schema's `approach` field; falls back to the capped artifact
text), runs checker.check(...), and appends the non-null PossibleContradictionFlaggedEvent.
Live-only and never halts a stage (runCatching). Checker built in Main.kt gated on
project.enabled (same L3 "project:" namespace ProjectMemoryService.persist populates) and
threaded into ServerModule as a nullable param. Replaces the standing TODO(wiring) in Main.kt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -380,16 +380,16 @@ fun main() {
|
|||||||
privilegedLocations = privilegedLocations,
|
privilegedLocations = privilegedLocations,
|
||||||
allowedWorkspaceRoots = allowedWorkspaceRoots,
|
allowedWorkspaceRoots = allowedWorkspaceRoots,
|
||||||
)
|
)
|
||||||
// TODO(wiring): ArchitectContradictionChecker (BACKLOG §B-§4) is complete but not yet live.
|
// Display-only architect contradiction surfacing (BACKLOG §B-§4): now live via the
|
||||||
// `embedder` + `l3MemoryStore` are in scope here, but two preconditions are missing:
|
// ServerModule.start() subscription on the architect's `design` ArtifactCreatedEvent (see
|
||||||
// (1) no server-side hook exposes the architect's decision *text* with an event-emit path
|
// ServerModule.handleArchitectArtifact). Gated on project.enabled because the checker queries the
|
||||||
// (architect is a workflow-defined role/stage, not a known stage id; decisions become
|
// same L3 "project:<repoRoot>" namespace that ProjectMemoryService.persist populates.
|
||||||
// DecisionRecords via the journal reducer from generic events), and
|
val architectContradictionChecker: com.correx.apps.server.memory.ArchitectContradictionChecker? =
|
||||||
// (2) decisions are only embedded into L3 at session end (ProjectMemoryService.persist),
|
if (correxConfig.project.enabled) {
|
||||||
// under turnId "project:<repoRoot>", so an in-session architect run has nothing of its
|
com.correx.apps.server.memory.ArchitectContradictionChecker(embedder, l3MemoryStore)
|
||||||
// own to retrieve. Wiring this would mean inventing an architect-stage subscriber +
|
} else {
|
||||||
// emit path — deliberately left out per scope. To go live: subscribe to the architect
|
null
|
||||||
// decision, call ArchitectContradictionChecker.check(...), and emit the non-null result.
|
}
|
||||||
// Built from a config snapshot and reused by ConfigService's rebuild hook so toggling
|
// Built from a config snapshot and reused by ConfigService's rebuild hook so toggling
|
||||||
// project.enabled / personalization.* applies live to the next session.
|
// project.enabled / personalization.* applies live to the next session.
|
||||||
fun buildProjectMemory(cfg: CorrexConfig): com.correx.apps.server.memory.ProjectMemoryService? =
|
fun buildProjectMemory(cfg: CorrexConfig): com.correx.apps.server.memory.ProjectMemoryService? =
|
||||||
@@ -477,6 +477,7 @@ fun main() {
|
|||||||
workspaceResolver = workspaceResolver,
|
workspaceResolver = workspaceResolver,
|
||||||
narrationMaxPerRun = correxConfig.router.narration.maxPerRun,
|
narrationMaxPerRun = correxConfig.router.narration.maxPerRun,
|
||||||
projectMemory = projectMemory,
|
projectMemory = projectMemory,
|
||||||
|
architectContradictionChecker = architectContradictionChecker,
|
||||||
configHolder = configHolder,
|
configHolder = configHolder,
|
||||||
freestyleDriver = freestyleDriver,
|
freestyleDriver = freestyleDriver,
|
||||||
operatorProfile = operatorProfile,
|
operatorProfile = operatorProfile,
|
||||||
|
|||||||
@@ -13,11 +13,15 @@ import com.correx.core.approvals.DefaultApprovalRepository
|
|||||||
import com.correx.core.artifactstore.ArtifactStore
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
import com.correx.core.config.OperatorProfile
|
import com.correx.core.config.OperatorProfile
|
||||||
import com.correx.core.config.ProjectProfileLoader
|
import com.correx.core.config.ProjectProfileLoader
|
||||||
|
import com.correx.apps.server.memory.ArchitectContradictionChecker
|
||||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||||
|
import com.correx.core.events.events.ArtifactContentStoredEvent
|
||||||
|
import com.correx.core.events.events.ArtifactCreatedEvent
|
||||||
import com.correx.core.events.events.EventMetadata
|
import com.correx.core.events.events.EventMetadata
|
||||||
import com.correx.core.events.events.NewEvent
|
import com.correx.core.events.events.NewEvent
|
||||||
import com.correx.core.events.events.ExecutionPlanLockedEvent
|
import com.correx.core.events.events.ExecutionPlanLockedEvent
|
||||||
import com.correx.core.events.events.OperatorProfileBoundEvent
|
import com.correx.core.events.events.OperatorProfileBoundEvent
|
||||||
|
import com.correx.core.events.events.PossibleContradictionFlaggedEvent
|
||||||
import com.correx.core.events.events.ProjectProfileBoundEvent
|
import com.correx.core.events.events.ProjectProfileBoundEvent
|
||||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||||
import com.correx.core.events.events.OrchestrationResumedEvent
|
import com.correx.core.events.events.OrchestrationResumedEvent
|
||||||
@@ -26,6 +30,9 @@ import com.correx.core.events.stores.EventStore
|
|||||||
import com.correx.core.events.types.EventId
|
import com.correx.core.events.types.EventId
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
import com.correx.core.kernel.execution.WorkflowResult
|
import com.correx.core.kernel.execution.WorkflowResult
|
||||||
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
|
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
|
||||||
import com.correx.core.kernel.orchestration.OrchestrationConfig
|
import com.correx.core.kernel.orchestration.OrchestrationConfig
|
||||||
@@ -87,6 +94,10 @@ class ServerModule(
|
|||||||
// Cross-session, repo-scoped memory. Null disables the hooks entirely (tests / static path).
|
// Cross-session, repo-scoped memory. Null disables the hooks entirely (tests / static path).
|
||||||
// Rebuilt by ConfigService when project.enabled is toggled live.
|
// Rebuilt by ConfigService when project.enabled is toggled live.
|
||||||
projectMemory: com.correx.apps.server.memory.ProjectMemoryService? = null,
|
projectMemory: com.correx.apps.server.memory.ProjectMemoryService? = null,
|
||||||
|
// Display-only architect contradiction surfacing (BACKLOG §B-§4). Null disables the hook
|
||||||
|
// (tests / project.enabled=false). Unlike projectMemory there is no live config-rebuild hook:
|
||||||
|
// a server restart re-reads project.enabled, which is enough for this informational flag.
|
||||||
|
private val architectContradictionChecker: ArchitectContradictionChecker? = null,
|
||||||
// Live, swappable config. Null only in tests that don't exercise config editing; defaults to a
|
// Live, swappable config. Null only in tests that don't exercise config editing; defaults to a
|
||||||
// holder seeded from defaults so callers always have a value to read.
|
// holder seeded from defaults so callers always have a value to read.
|
||||||
val configHolder: com.correx.core.config.ConfigHolder =
|
val configHolder: com.correx.core.config.ConfigHolder =
|
||||||
@@ -187,6 +198,80 @@ class ServerModule(
|
|||||||
// Continuous, edge-triggered health watch (observability-spec §4). Live-only like narration:
|
// Continuous, edge-triggered health watch (observability-spec §4). Live-only like narration:
|
||||||
// probes read the environment and record degraded/restored events; replay reads those facts.
|
// probes read the environment and record degraded/restored events; replay reads those facts.
|
||||||
healthMonitor?.start(moduleScope)
|
healthMonitor?.start(moduleScope)
|
||||||
|
|
||||||
|
// Display-only architect contradiction surfacing (BACKLOG §B-§4). Live-only like the hooks
|
||||||
|
// above: subscribeAll() replays nothing and ServerModule is never built under replay, so a
|
||||||
|
// restart never re-fires the flag (invariant #8). Never halts a stage — failures are logged
|
||||||
|
// and swallowed (runCatching) and the flag is emitted purely for the operator to eyeball.
|
||||||
|
architectContradictionChecker?.let { checker ->
|
||||||
|
eventStore.subscribeAll()
|
||||||
|
.filter {
|
||||||
|
it.payload is ArtifactCreatedEvent &&
|
||||||
|
(it.payload as ArtifactCreatedEvent).stageId.value == ARCHITECT_STAGE_ID
|
||||||
|
}
|
||||||
|
.onEach { stored ->
|
||||||
|
runCatching {
|
||||||
|
handleArchitectArtifact(checker, stored.payload as ArtifactCreatedEvent)
|
||||||
|
}.onFailure { log.warn("architect contradiction check failed: {}", it.message) }
|
||||||
|
}
|
||||||
|
.launchIn(moduleScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the architect contradiction check for a freshly-created `design` artifact and, when the
|
||||||
|
* checker surfaces a non-null flag, appends the [PossibleContradictionFlaggedEvent]. Display-only:
|
||||||
|
* the flag is informational and never halts the stage. Factored out of the subscription so the
|
||||||
|
* extraction/dispatch path is unit-testable without a live event flow.
|
||||||
|
*
|
||||||
|
* @return the appended flag, or null when the artifact text could not be resolved or the checker
|
||||||
|
* found nothing near the architect's decision.
|
||||||
|
*/
|
||||||
|
internal suspend fun handleArchitectArtifact(
|
||||||
|
checker: ArchitectContradictionChecker,
|
||||||
|
event: ArtifactCreatedEvent,
|
||||||
|
): PossibleContradictionFlaggedEvent? {
|
||||||
|
val decisionText = resolveArchitectDecisionText(event) ?: return null
|
||||||
|
val flag = checker.check(event.sessionId, event.stageId, decisionText) ?: return null
|
||||||
|
eventStore.append(
|
||||||
|
NewEvent(
|
||||||
|
metadata = EventMetadata(
|
||||||
|
eventId = EventId(java.util.UUID.randomUUID().toString()),
|
||||||
|
sessionId = event.sessionId,
|
||||||
|
timestamp = Clock.System.now(),
|
||||||
|
schemaVersion = 1,
|
||||||
|
causationId = null,
|
||||||
|
correlationId = null,
|
||||||
|
),
|
||||||
|
payload = flag,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return flag
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the architect's decision text for [event] from the CAS-stored `design` artifact.
|
||||||
|
* Finds the content hash by scanning the session for the [ArtifactContentStoredEvent] matching
|
||||||
|
* the artifact id (emitted at inference time, before [ArtifactCreatedEvent]), fetches the bytes,
|
||||||
|
* and prefers the design schema's `approach` field ("the chosen approach and why") as the
|
||||||
|
* decision summary; falls back to the whole artifact text, capped at [DECISION_TEXT_CAP] chars.
|
||||||
|
*
|
||||||
|
* @return the decision text, or null when no content hash, bytes, or text could be resolved.
|
||||||
|
*/
|
||||||
|
private suspend fun resolveArchitectDecisionText(event: ArtifactCreatedEvent): String? {
|
||||||
|
val contentHash = eventStore.read(event.sessionId)
|
||||||
|
.asSequence()
|
||||||
|
.map { it.payload }
|
||||||
|
.filterIsInstance<ArtifactContentStoredEvent>()
|
||||||
|
.firstOrNull { it.artifactId == event.artifactId }
|
||||||
|
?.contentHash ?: return null
|
||||||
|
val text = artifactStore.get(contentHash)?.toString(Charsets.UTF_8)?.takeIf { it.isNotBlank() }
|
||||||
|
?: return null
|
||||||
|
val approach = runCatching {
|
||||||
|
(lenientJson.parseToJsonElement(text).jsonObject["approach"] as? JsonPrimitive)
|
||||||
|
?.takeIf { it.isString }?.content
|
||||||
|
}.getOrNull()?.takeIf { it.isNotBlank() }
|
||||||
|
return (approach ?: text).take(DECISION_TEXT_CAP)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -438,4 +523,14 @@ class ServerModule(
|
|||||||
.forEach { req -> approvalCoordinator.registerPendingRequest(req.id, sessionId) }
|
.forEach { req -> approvalCoordinator.registerPendingRequest(req.id, sessionId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Workflow stage id that produces the `design` artifact (examples/workflows/role_pipeline.toml). */
|
||||||
|
const val ARCHITECT_STAGE_ID = "architect"
|
||||||
|
|
||||||
|
/** Cap on the architect decision text embedded when no `approach` field is present. */
|
||||||
|
private const val DECISION_TEXT_CAP = 4000
|
||||||
|
|
||||||
|
private val lenientJson = Json { ignoreUnknownKeys = true; isLenient = true }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
package com.correx.apps.server
|
||||||
|
|
||||||
|
import com.correx.apps.server.memory.ArchitectContradictionChecker
|
||||||
|
import com.correx.apps.server.registry.ProviderRegistry
|
||||||
|
import com.correx.apps.server.registry.WorkflowRegistry
|
||||||
|
import com.correx.apps.server.registry.WorkflowSummary
|
||||||
|
import com.correx.apps.server.undo.SessionUndoService
|
||||||
|
import com.correx.core.artifactstore.ArtifactStore
|
||||||
|
import com.correx.core.context.builder.DefaultContextPackBuilder
|
||||||
|
import com.correx.core.context.compression.DefaultContextCompressor
|
||||||
|
import com.correx.core.events.EventDispatcher
|
||||||
|
import com.correx.core.events.events.ArtifactContentStoredEvent
|
||||||
|
import com.correx.core.events.events.ArtifactCreatedEvent
|
||||||
|
import com.correx.core.events.events.EventMetadata
|
||||||
|
import com.correx.core.events.events.EventPayload
|
||||||
|
import com.correx.core.events.events.NewEvent
|
||||||
|
import com.correx.core.events.events.PossibleContradictionFlaggedEvent
|
||||||
|
import com.correx.core.events.stores.EventStore
|
||||||
|
import com.correx.core.events.types.ArtifactId
|
||||||
|
import com.correx.core.events.types.EventId
|
||||||
|
import com.correx.core.events.types.ProviderId
|
||||||
|
import com.correx.core.events.types.SessionId
|
||||||
|
import com.correx.core.events.types.StageId
|
||||||
|
import com.correx.core.inference.Embedder
|
||||||
|
import com.correx.core.inference.InferenceProjector
|
||||||
|
import com.correx.core.inference.InferenceProvider
|
||||||
|
import com.correx.core.inference.InferenceRepository
|
||||||
|
import com.correx.core.inference.ProviderHealth
|
||||||
|
import com.correx.core.kernel.orchestration.DefaultOrchestrationReducer
|
||||||
|
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
|
||||||
|
import com.correx.core.kernel.orchestration.OrchestrationConfig
|
||||||
|
import com.correx.core.kernel.orchestration.OrchestrationProjector
|
||||||
|
import com.correx.core.kernel.orchestration.OrchestrationRepository
|
||||||
|
import com.correx.core.kernel.orchestration.OrchestratorEngines
|
||||||
|
import com.correx.core.kernel.orchestration.OrchestratorRepositories
|
||||||
|
import com.correx.core.kernel.retry.DefaultRetryCoordinator
|
||||||
|
import com.correx.core.risk.DefaultRiskAssessor
|
||||||
|
import com.correx.core.router.l3.L3Hit
|
||||||
|
import com.correx.core.router.l3.L3MemoryEntry
|
||||||
|
import com.correx.core.router.l3.L3MemoryStore
|
||||||
|
import com.correx.core.router.l3.L3Query
|
||||||
|
import com.correx.core.router.model.RouterConfig
|
||||||
|
import com.correx.core.sessions.DefaultSessionReducer
|
||||||
|
import com.correx.core.sessions.DefaultSessionRepository
|
||||||
|
import com.correx.core.sessions.SessionProjector
|
||||||
|
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
|
||||||
|
import com.correx.core.transitions.graph.WorkflowGraph
|
||||||
|
import com.correx.core.transitions.resolution.DefaultTransitionResolver
|
||||||
|
import com.correx.core.validation.pipeline.ValidationPipeline
|
||||||
|
import com.correx.infrastructure.InfrastructureModule
|
||||||
|
import com.correx.infrastructure.inference.commons.UnavailableProbe
|
||||||
|
import com.correx.infrastructure.persistence.InMemoryEventStore
|
||||||
|
import com.correx.infrastructure.tools.FileEditConfig
|
||||||
|
import com.correx.infrastructure.tools.FileReadConfig
|
||||||
|
import com.correx.infrastructure.tools.FileWriteConfig
|
||||||
|
import com.correx.infrastructure.tools.ShellConfig
|
||||||
|
import com.correx.infrastructure.tools.ToolConfig
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.io.TempDir
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/** Always returns the same vector so the canned L3 store fully controls the scores. */
|
||||||
|
private class OnesEmbedder(override val dimension: Int = 8) : Embedder {
|
||||||
|
override suspend fun embed(text: String): FloatArray = FloatArray(dimension) { 1f }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns canned hits regardless of the query vector. */
|
||||||
|
private class CannedL3MemoryStore(private val hits: List<L3Hit>) : L3MemoryStore {
|
||||||
|
override suspend fun store(entry: L3MemoryEntry) = Unit
|
||||||
|
override suspend fun query(query: L3Query): List<L3Hit> = hits.sortedByDescending { it.score }.take(query.k)
|
||||||
|
override suspend fun existsByTurnIdPrefix(prefix: String): Boolean = hits.any { it.entry.turnId.startsWith(prefix) }
|
||||||
|
override suspend fun close() = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minimal in-memory CAS keyed by content hash. */
|
||||||
|
private class MapArtifactStore(private val bytesByHash: Map<String, ByteArray>) : ArtifactStore {
|
||||||
|
override suspend fun put(bytes: ByteArray): ArtifactId = ArtifactId("00".repeat(32))
|
||||||
|
override suspend fun get(id: ArtifactId): ByteArray? = bytesByHash[id.value]
|
||||||
|
override suspend fun flushBefore(commit: suspend () -> Unit) { commit() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exercises [ServerModule.handleArchitectArtifact]: the extraction/dispatch helper that backs the
|
||||||
|
* display-only architect contradiction subscription (BACKLOG §B-§4). A full subscription/integration
|
||||||
|
* test is not required — these assert the helper fetches the design artifact text and emits (or
|
||||||
|
* skips) the flag. The surrounding module is built with in-memory infra (no real model needed).
|
||||||
|
*/
|
||||||
|
class ArchitectContradictionHookTest {
|
||||||
|
|
||||||
|
private val session = SessionId("new-session")
|
||||||
|
private val priorSession = SessionId("prior-session")
|
||||||
|
private val architectStage = StageId("architect")
|
||||||
|
private val designArtifact = ArtifactId("design")
|
||||||
|
private val contentHash = ArtifactId("cafebabe")
|
||||||
|
|
||||||
|
private fun append(es: EventStore, payload: EventPayload, sid: SessionId = session) = runBlocking {
|
||||||
|
es.append(
|
||||||
|
NewEvent(
|
||||||
|
EventMetadata(EventId(UUID.randomUUID().toString()), sid, Clock.System.now(), 1, null, null),
|
||||||
|
payload,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun priorDecisionHit(text: String, score: Float) = L3Hit(
|
||||||
|
entry = L3MemoryEntry(
|
||||||
|
id = UUID.randomUUID().toString(),
|
||||||
|
sessionId = priorSession,
|
||||||
|
turnId = "project:/repo",
|
||||||
|
text = text,
|
||||||
|
vector = FloatArray(8) { 1f },
|
||||||
|
timestampMs = 0L,
|
||||||
|
),
|
||||||
|
score = score,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun flagsIn(es: EventStore): List<PossibleContradictionFlaggedEvent> =
|
||||||
|
es.read(session).map { it.payload }.filterIsInstance<PossibleContradictionFlaggedEvent>()
|
||||||
|
|
||||||
|
private fun buildModule(
|
||||||
|
eventStore: EventStore,
|
||||||
|
artifactStore: ArtifactStore,
|
||||||
|
checker: ArchitectContradictionChecker?,
|
||||||
|
tempDir: Path,
|
||||||
|
): ServerModule {
|
||||||
|
val provider = InfrastructureModule.createLlamaCppProvider(
|
||||||
|
modelId = "test-model",
|
||||||
|
modelPath = "/dev/null",
|
||||||
|
baseUrl = "http://127.0.0.1:1",
|
||||||
|
)
|
||||||
|
val providerRegistry = InfrastructureModule.createProviderRegistry(listOf(provider))
|
||||||
|
val inferenceRouter = com.correx.core.inference.DefaultInferenceRouter(
|
||||||
|
providerRegistry,
|
||||||
|
com.correx.infrastructure.inference.FirstAvailableRoutingStrategy(),
|
||||||
|
)
|
||||||
|
val toolConfig = ToolConfig(
|
||||||
|
shell = ShellConfig(enabled = false, allowedExecutables = emptySet(), workingDir = tempDir),
|
||||||
|
fileRead = FileReadConfig(enabled = false, allowedPaths = setOf(tempDir)),
|
||||||
|
fileWrite = FileWriteConfig(enabled = false, allowedPaths = setOf(tempDir), workingDir = tempDir),
|
||||||
|
fileEdit = FileEditConfig(enabled = false, allowedPaths = setOf(tempDir), workingDir = tempDir),
|
||||||
|
)
|
||||||
|
val toolRegistry = InfrastructureModule.createToolRegistry(toolConfig)
|
||||||
|
val toolExecutor = InfrastructureModule.createToolExecutor(
|
||||||
|
registry = toolRegistry,
|
||||||
|
eventDispatcher = EventDispatcher(eventStore),
|
||||||
|
workDir = tempDir,
|
||||||
|
artifactStore = null,
|
||||||
|
)
|
||||||
|
val engines = OrchestratorEngines(
|
||||||
|
transitionResolver = DefaultTransitionResolver { condition, ctx -> condition.evaluate(ctx) },
|
||||||
|
contextPackBuilder = DefaultContextPackBuilder(DefaultContextCompressor()),
|
||||||
|
inferenceRouter = inferenceRouter,
|
||||||
|
validationPipeline = ValidationPipeline(validators = emptyList()),
|
||||||
|
approvalEngine = com.correx.core.approvals.domain.DefaultApprovalEngine(),
|
||||||
|
riskAssessor = DefaultRiskAssessor(),
|
||||||
|
toolRegistry = toolRegistry,
|
||||||
|
toolExecutor = toolExecutor,
|
||||||
|
)
|
||||||
|
val repositories = OrchestratorRepositories(
|
||||||
|
eventStore = eventStore,
|
||||||
|
inferenceRepository = InferenceRepository(DefaultEventReplayer(eventStore, InferenceProjector())),
|
||||||
|
orchestrationRepository = OrchestrationRepository(
|
||||||
|
DefaultEventReplayer(eventStore, OrchestrationProjector(DefaultOrchestrationReducer())),
|
||||||
|
),
|
||||||
|
sessionRepository = DefaultSessionRepository(
|
||||||
|
DefaultEventReplayer(eventStore, SessionProjector(DefaultSessionReducer())),
|
||||||
|
),
|
||||||
|
artifactRepository = InfrastructureModule.createArtifactRepository(eventStore),
|
||||||
|
approvalRepository = InfrastructureModule.createApprovalRepository(eventStore),
|
||||||
|
)
|
||||||
|
val orchestrator = DefaultSessionOrchestrator(
|
||||||
|
repositories = repositories,
|
||||||
|
engines = engines,
|
||||||
|
retryCoordinator = DefaultRetryCoordinator(eventStore),
|
||||||
|
artifactStore = artifactStore,
|
||||||
|
tokenizer = provider.tokenizer,
|
||||||
|
decisionJournalRepository = InfrastructureModule.createDecisionJournalRepository(eventStore),
|
||||||
|
)
|
||||||
|
val routerFacade = InfrastructureModule.createRouterFacade(
|
||||||
|
eventStore = eventStore,
|
||||||
|
inferenceRouter = inferenceRouter,
|
||||||
|
config = RouterConfig(),
|
||||||
|
tokenizer = provider.tokenizer,
|
||||||
|
)
|
||||||
|
val noopProviderRegistry = object : ProviderRegistry {
|
||||||
|
override fun listAll() = emptyList<InferenceProvider>()
|
||||||
|
override suspend fun healthCheckAll() = emptyMap<ProviderId, ProviderHealth>()
|
||||||
|
}
|
||||||
|
val noopWorkflowRegistry = object : WorkflowRegistry {
|
||||||
|
override fun listAll(): List<WorkflowSummary> = emptyList()
|
||||||
|
override fun find(workflowId: String): WorkflowGraph? = null
|
||||||
|
}
|
||||||
|
return ServerModule(
|
||||||
|
orchestrator = orchestrator,
|
||||||
|
eventStore = eventStore,
|
||||||
|
artifactStore = artifactStore,
|
||||||
|
sessionRepository = repositories.sessionRepository,
|
||||||
|
workflowRegistry = noopWorkflowRegistry,
|
||||||
|
providerRegistry = noopProviderRegistry,
|
||||||
|
defaultOrchestrationConfig = OrchestrationConfig(sandboxRoot = tempDir),
|
||||||
|
routerFacade = routerFacade,
|
||||||
|
orchestrationRepository = repositories.orchestrationRepository,
|
||||||
|
approvalRepository = repositories.approvalRepository,
|
||||||
|
toolRegistry = toolRegistry,
|
||||||
|
sessionUndoService = SessionUndoService(eventStore, artifactStore, bootRoots = setOf(tempDir)),
|
||||||
|
resourceProbe = UnavailableProbe,
|
||||||
|
architectContradictionChecker = checker,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `emits the flag when the checker surfaces a near prior decision`(@TempDir tempDir: Path) = runBlocking {
|
||||||
|
val es = InMemoryEventStore()
|
||||||
|
val designJson = """{"approach":"Use Postgres for the event store.","components":["db.kt"]}"""
|
||||||
|
val artifacts = MapArtifactStore(mapOf(contentHash.value to designJson.toByteArray()))
|
||||||
|
// ArtifactContentStored precedes ArtifactCreated for the same artifactId (inference-time).
|
||||||
|
append(es, ArtifactContentStoredEvent(designArtifact, contentHash, session, architectStage))
|
||||||
|
val created = ArtifactCreatedEvent(designArtifact, session, architectStage, schemaVersion = 1)
|
||||||
|
append(es, created)
|
||||||
|
val checker = ArchitectContradictionChecker(
|
||||||
|
OnesEmbedder(),
|
||||||
|
CannedL3MemoryStore(listOf(priorDecisionHit("Decided to use SQLite for the event store.", 0.9f))),
|
||||||
|
)
|
||||||
|
|
||||||
|
val flag = buildModule(es, artifacts, checker, tempDir).handleArchitectArtifact(checker, created)
|
||||||
|
|
||||||
|
assertNotNull(flag, "expected a contradiction flag")
|
||||||
|
assertEquals(session, flag!!.sessionId)
|
||||||
|
assertEquals(architectStage, flag.stageId)
|
||||||
|
// The design schema's `approach` field is preferred as the decision text.
|
||||||
|
assertEquals("Use Postgres for the event store.", flag.decisionSummary)
|
||||||
|
assertEquals("Decided to use SQLite for the event store.", flag.related.single().summary)
|
||||||
|
// The flag is appended to the session stream (display-only, but recorded for the operator).
|
||||||
|
assertEquals(1, flagsIn(es).size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `returns null and appends nothing when the checker finds no related decision`(
|
||||||
|
@TempDir tempDir: Path,
|
||||||
|
) = runBlocking {
|
||||||
|
val es = InMemoryEventStore()
|
||||||
|
val designJson = """{"approach":"Use Postgres for the event store.","components":["db.kt"]}"""
|
||||||
|
val artifacts = MapArtifactStore(mapOf(contentHash.value to designJson.toByteArray()))
|
||||||
|
append(es, ArtifactContentStoredEvent(designArtifact, contentHash, session, architectStage))
|
||||||
|
val created = ArtifactCreatedEvent(designArtifact, session, architectStage, schemaVersion = 1)
|
||||||
|
append(es, created)
|
||||||
|
val checker = ArchitectContradictionChecker(OnesEmbedder(), CannedL3MemoryStore(emptyList()))
|
||||||
|
|
||||||
|
val flag = buildModule(es, artifacts, checker, tempDir).handleArchitectArtifact(checker, created)
|
||||||
|
|
||||||
|
assertNull(flag)
|
||||||
|
assertEquals(0, flagsIn(es).size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `returns null when no content hash was recorded for the artifact`(@TempDir tempDir: Path) = runBlocking {
|
||||||
|
val es = InMemoryEventStore()
|
||||||
|
val created = ArtifactCreatedEvent(designArtifact, session, architectStage, schemaVersion = 1)
|
||||||
|
append(es, created)
|
||||||
|
val checker = ArchitectContradictionChecker(
|
||||||
|
OnesEmbedder(),
|
||||||
|
CannedL3MemoryStore(listOf(priorDecisionHit("Decided to use SQLite.", 0.9f))),
|
||||||
|
)
|
||||||
|
|
||||||
|
val flag = buildModule(es, MapArtifactStore(emptyMap()), checker, tempDir)
|
||||||
|
.handleArchitectArtifact(checker, created)
|
||||||
|
|
||||||
|
assertNull(flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user