feat(tasks): markdown export projection

A disposable Markdown view of the board, grouped by status with checkboxes
and claimants — rendered from the event log, never a source of truth.
TaskMarkdown.render (pure) backs GET /tasks/export[?project=].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 07:17:06 +00:00
parent 8d84ccf92f
commit 8a6678f171
4 changed files with 111 additions and 0 deletions
@@ -11,14 +11,17 @@ import com.correx.core.events.types.TaskNoteAuthor
import com.correx.core.events.types.TaskTargetKind
import com.correx.core.tasks.Task
import com.correx.core.tasks.TaskContextAssembler
import com.correx.core.tasks.TaskMarkdown
import com.correx.core.tasks.TaskSearch
import com.correx.core.tasks.TaskService
import com.correx.core.tasks.TaskStatus
import com.correx.core.tasks.TaskTargetKinds
import com.correx.core.utils.TypeId
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.RoutingContext
import io.ktor.server.routing.delete
@@ -150,6 +153,12 @@ private fun Route.taskCollectionRoutes(service: TaskService) {
)
call.respond(HttpStatusCode.Created, task.toResponse())
}
// Disposable Markdown projection of the board (whole board, or one project with ?project=).
get("/export") {
val project = call.request.queryParameters["project"]
val tasks = if (project != null) service.list(ProjectId(project)) else service.listAll()
call.respondText(TaskMarkdown.render(tasks), ContentType.Text.Plain)
}
}
// Git-driven status: a commit mention advances a task to IN_PROGRESS, a closing keyword
@@ -227,6 +227,21 @@ class TaskRoutesTest {
}
}
@Test
fun `GET tasks export renders the board as markdown`(@TempDir tempDir: Path) {
testApplication {
application { configureServer(buildModule(tempDir)) }
val client = createClient {}
client.createTask() // demo-1
val res = client.get("/tasks/export")
assertEquals(HttpStatusCode.OK, res.status)
val md = res.bodyAsText()
assertTrue(md.startsWith("# Tasks"))
assertTrue(md.contains("**demo-1**"))
}
}
@Test
fun `context bundle is served for a known task`(@TempDir tempDir: Path) {
testApplication {