feat(transitions): tasks_ready predicate for execution loop
New TasksReady condition driven by EvaluationContext.readyTaskCount, fed at resolve time via a kernel-declared ReadyTaskCounter interface implemented in apps/server over TaskService (keeps core:kernel decoupled from core:tasks). Loops a graph while ready>0, exits at 0.
This commit is contained in:
@@ -368,6 +368,7 @@ fun main() {
|
||||
compactionService = journalCompactionService,
|
||||
artifactKindRegistry = artifactKindRegistry,
|
||||
repoKnowledgeRetriever = repoKnowledgeRetriever,
|
||||
readyTaskCounter = com.correx.apps.server.tasks.ProjectReadyTaskCounter(eventStore, taskService),
|
||||
)
|
||||
val workflowRegistry = FileSystemWorkflowRegistry(
|
||||
InfrastructureModule.createWorkflowLoader(configArtifactKindsEarly),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.correx.apps.server.tasks
|
||||
|
||||
import com.correx.core.approvals.ProjectIdentity
|
||||
import com.correx.core.events.events.SessionWorkspaceBoundEvent
|
||||
import com.correx.core.events.stores.EventStore
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.kernel.orchestration.ReadyTaskCounter
|
||||
import com.correx.core.tasks.TaskService
|
||||
|
||||
/**
|
||||
* [ReadyTaskCounter] over the task projection. Resolves the session to its project the same way the
|
||||
* approval gate does (bound workspace root → [ProjectIdentity.of]) and counts the ready tasks there.
|
||||
* Pure read over event-derived state, so it stays replay-safe.
|
||||
*
|
||||
* Caveat for the execution loop: tasks are keyed by whatever `project` string the decomposer passes
|
||||
* to task tools; this counts the project derived from the workspace root. Those must agree for the
|
||||
* loop predicate to fire — Slice 2/4 pins that down.
|
||||
*/
|
||||
class ProjectReadyTaskCounter(
|
||||
private val eventStore: EventStore,
|
||||
private val taskService: TaskService,
|
||||
) : ReadyTaskCounter {
|
||||
override fun count(sessionId: SessionId): Int {
|
||||
val workspaceRoot = eventStore.read(sessionId)
|
||||
.mapNotNull { it.payload as? SessionWorkspaceBoundEvent }
|
||||
.lastOrNull()
|
||||
?.workspaceRoot
|
||||
?: return 0
|
||||
return taskService.ready(ProjectIdentity.of(workspaceRoot)).size
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -61,7 +61,8 @@ class DefaultSessionOrchestrator(
|
||||
private val compactionService: JournalCompactionService? = null,
|
||||
artifactKindRegistry: ArtifactKindRegistry? = null,
|
||||
repoKnowledgeRetriever: RepoKnowledgeRetriever? = null,
|
||||
) : SessionOrchestrator(repositories, engines, artifactStore, decisionJournalRepository, artifactKindRegistry = artifactKindRegistry, repoKnowledgeRetriever = repoKnowledgeRetriever), ApprovalGateway {
|
||||
readyTaskCounter: ReadyTaskCounter? = null,
|
||||
) : SessionOrchestrator(repositories, engines, artifactStore, decisionJournalRepository, artifactKindRegistry = artifactKindRegistry, repoKnowledgeRetriever = repoKnowledgeRetriever, readyTaskCounter = readyTaskCounter), ApprovalGateway {
|
||||
override val tokenizer: Tokenizer? = tokenizer
|
||||
override val cancellations: ConcurrentHashMap<SessionId, AtomicBoolean> =
|
||||
ConcurrentHashMap<SessionId, AtomicBoolean>()
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.correx.core.kernel.orchestration
|
||||
|
||||
import com.correx.core.events.types.SessionId
|
||||
|
||||
/**
|
||||
* How many tasks are ready to claim for a session's project, read at transition-resolution time
|
||||
* (invariant #9: the observation is taken when the predicate runs). Implemented in apps/server over
|
||||
* the task projection — core:kernel stays decoupled from core:tasks. Null injection ⇒ count 0, so
|
||||
* the [com.correx.core.transitions.conditions.TasksReady] predicate is simply false.
|
||||
*/
|
||||
fun interface ReadyTaskCounter {
|
||||
fun count(sessionId: SessionId): Int
|
||||
}
|
||||
+2
@@ -187,6 +187,7 @@ abstract class SessionOrchestrator(
|
||||
private val decisionJournalRenderer: DecisionJournalRenderer = DecisionJournalRenderer(),
|
||||
private val artifactKindRegistry: ArtifactKindRegistry? = null,
|
||||
private val repoKnowledgeRetriever: RepoKnowledgeRetriever? = null,
|
||||
private val readyTaskCounter: ReadyTaskCounter? = null,
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(this::class.java)
|
||||
private val eventStore: EventStore = repositories.eventStore
|
||||
@@ -1856,6 +1857,7 @@ abstract class SessionOrchestrator(
|
||||
currentStage = currentStageId,
|
||||
artifacts = artifacts,
|
||||
artifactContent = artifactContent,
|
||||
readyTaskCount = readyTaskCounter?.count(sessionId) ?: 0,
|
||||
)
|
||||
return transitionResolver.resolve(graph, ctx)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.correx.core.transitions.conditions
|
||||
|
||||
import com.correx.core.transitions.evaluation.EvaluationContext
|
||||
import com.correx.core.transitions.graph.TransitionCondition
|
||||
|
||||
/** True while the session's project still has tasks ready to claim — the execution loop's gate. */
|
||||
object TasksReady : TransitionCondition {
|
||||
override fun evaluate(context: EvaluationContext) = context.readyTaskCount > 0
|
||||
}
|
||||
+1
@@ -11,4 +11,5 @@ data class EvaluationContext(
|
||||
val artifacts: Map<ArtifactId, ArtifactState> = emptyMap(),
|
||||
val variables: Map<String, String> = emptyMap(),
|
||||
val artifactContent: Map<ArtifactId, String> = emptyMap(),
|
||||
val readyTaskCount: Int = 0,
|
||||
)
|
||||
|
||||
+12
@@ -30,6 +30,18 @@ class TransitionConditionTest {
|
||||
assertTrue(AlwaysTrue.evaluate(baseContext))
|
||||
}
|
||||
|
||||
// TasksReady
|
||||
|
||||
@Test
|
||||
fun `TasksReady true when ready count positive`() {
|
||||
assertTrue(TasksReady.evaluate(baseContext.copy(readyTaskCount = 1)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TasksReady false when no ready tasks`() {
|
||||
assertFalse(TasksReady.evaluate(baseContext))
|
||||
}
|
||||
|
||||
// ArtifactPresent
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "correx-web-ui",
|
||||
"version": "1.0.0",
|
||||
"description": "Web client for CORREX orchestration kernel",
|
||||
"main": "./src/App.tsx",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import SessionView from './components/session/SessionView';
|
||||
import TaskBoard from './components/task/TaskBoard';
|
||||
import WorkflowDebugger from './components/workflow/WorkflowDebugger';
|
||||
import EventTimeline from './components/state/EventTimeline';
|
||||
import { api } from './services/api';
|
||||
import { actions } from './services/actions';
|
||||
|
||||
const App: React.FC = () => {
|
||||
// Placeholder for routing or state management
|
||||
return (
|
||||
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
|
||||
<h1>CORREX Web Client</h1>
|
||||
<p>Welcome to the orchestration kernel interface.</p>
|
||||
|
||||
{/* Basic component placeholders */}
|
||||
<SessionView api={api} />
|
||||
<TaskBoard api={api} />
|
||||
<WorkflowDebugger api={api} />
|
||||
<EventTimeline api={api} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
const SessionView: React.FC<{ api: any }> = ({ api }) => {
|
||||
// Implementation in Step 3
|
||||
return <div>Session View Component</div>;
|
||||
};
|
||||
|
||||
export default SessionView;
|
||||
+2
@@ -10,6 +10,7 @@ import com.correx.core.transitions.conditions.ArtifactPresent
|
||||
import com.correx.core.transitions.conditions.ArtifactValidated
|
||||
import com.correx.core.transitions.conditions.FieldOperator
|
||||
import com.correx.core.transitions.conditions.Not
|
||||
import com.correx.core.transitions.conditions.TasksReady
|
||||
import com.correx.core.transitions.conditions.VariableEquals
|
||||
import com.correx.core.transitions.graph.TransitionCondition
|
||||
|
||||
@@ -20,6 +21,7 @@ fun ConditionSpec.toCondition(): TransitionCondition = when (type) {
|
||||
"artifact_validated" -> buildArtifactValidated()
|
||||
"variable_equals" -> buildVariableEquals()
|
||||
"artifact_field_equals" -> buildArtifactFieldEquals()
|
||||
"tasks_ready" -> TasksReady
|
||||
"all_of" -> AllOf(conditions.map { it.toCondition() })
|
||||
"any_of" -> AnyOf(conditions.map { it.toCondition() })
|
||||
"not" -> Not((condition ?: error("condition required for not")).toCondition())
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.correx.infrastructure.workflow
|
||||
|
||||
import com.correx.core.transitions.conditions.TasksReady
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import kotlin.test.assertSame
|
||||
|
||||
class ConditionFactoryTest {
|
||||
|
||||
@Test
|
||||
fun `tasks_ready maps to TasksReady`() {
|
||||
assertSame(TasksReady, ConditionSpec(type = "tasks_ready").toCondition())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown type rejected`() {
|
||||
assertThrows<WorkflowValidationException> { ConditionSpec(type = "nope").toCondition() }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.events.types.StageId
|
||||
import com.correx.core.transitions.conditions.Not
|
||||
import com.correx.core.transitions.conditions.TasksReady
|
||||
import com.correx.core.events.types.TransitionId
|
||||
import com.correx.core.transitions.evaluation.EvaluationContext
|
||||
import com.correx.core.transitions.evaluation.TransitionConditionEvaluator
|
||||
@@ -210,6 +212,28 @@ class DefaultTransitionResolverTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TasksReady loops while ready tasks remain and exits at zero`() {
|
||||
val graph = graph(
|
||||
start = "claim",
|
||||
transitions = setOf(
|
||||
edge(id = "loop", from = "claim", to = "claim", condition = TasksReady),
|
||||
edge(id = "exit", from = "claim", to = "done", condition = Not(TasksReady)),
|
||||
),
|
||||
stages = setOf("claim", "done"),
|
||||
)
|
||||
val base = context(currentStage = "claim")
|
||||
|
||||
assertEquals(
|
||||
StageId("claim"),
|
||||
(resolver.resolve(graph, base.copy(readyTaskCount = 2)) as TransitionDecision.Move).to,
|
||||
)
|
||||
assertEquals(
|
||||
StageId("done"),
|
||||
(resolver.resolve(graph, base.copy(readyTaskCount = 0)) as TransitionDecision.Move).to,
|
||||
)
|
||||
}
|
||||
|
||||
private fun alwaysTrue() =
|
||||
TransitionCondition { true }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user