Skip to content

Latest commit

 

History

History
316 lines (271 loc) · 28.7 KB

architecture.md

File metadata and controls

316 lines (271 loc) · 28.7 KB

Architecture

The CLI is a hexagonal application: a pure domain core (problems, attempts, scheduling, comparison, complexity analysis) surrounded by adapters for the filesystem, SQLite, editors and one subprocess runner per runtime. Runners are deliberately dumb: they deserialize inputs, call user code and serialize outputs. All judging happens in the core, written once in Python.

Components

Component Path Language Responsibility
CLI cli/ Python 3.13, uv Commands, domain core, adapters, codegen, comparators, scheduling, bench analysis
Python runner runners/python/ Python Packages cs_runner (runner) and cs_support (ListNode, TreeNode, GraphNode)
TypeScript runner runners/typescript/ TypeScript on Node 24 runner.ts and support.ts (the node classes and Decimal); erasable syntax only
JVM runner runners/jvm/ Java, compiled at release 21; Gradle included build cs.runner.Main and cs.support.*; serves Java and Kotlin. The module cs.coroutines, in Kotlin, is the bridge to the coroutine test scheduler that grades Kotlin projects on the virtual clock
Content problems/, tracks/ YAML, JSON, Markdown, code Problems, tests, references, explanations, generators
Web UI cli/src/coding_study/entrypoints/web/, sources in tools/web-ui/ Python (Starlette on uvicorn); TypeScript and CodeMirror 6, built and committed cstudy serve: the local page, a second driving adapter (below, web-ui)
Schemas schemas/ JSON Schema meta.schema.json, tests.schema.json, item.schema.json, conformance.schema.json; hand-maintained source of truth for validation (jsonschema) and editor completion. events.schema.json describes the lines of cstudy test --events and cstudy submit --events (event stream) for programs that read them, and web-api.schema.json the web API's own documents
Workspace templates cli/src/coding_study/adapters/workspace_template/ 3 real projects What cstudy init copies into a workspace
Workspace anywhere, outside this repository (installation-and-workspaces) 4 real projects The user's code, notes and own problems, versioned on their own
Progress <workspace>/progress.db SQLite Attempts, test runs, schedule, bench runs
Cache <workspace>/.cs/ mixed Index, generated cases, calibration, build scratch, runner runtime

Hexagonal boundaries

flowchart LR
  subgraph Driving
    CLI[Typer CLI entrypoint]
    WEB[Web adapter: cstudy serve]
  end
  subgraph Application
    UC[Use cases]
  end
  subgraph Domain
    D[Problem, Signature, Case, Attempt, Schedule, Comparators, StressExpander, Invariants, Scheduler, Selector, ComplexityAnalyzer]
  end
  subgraph Driven adapters
    FS[FsProblemRepository]
    DB[SqliteProgressStore]
    RUN[SubprocessRunner + LanguageRunner x4]
    GEN[Stub generators x4]
    WS[WorkspaceFs]
    PY[ContentCodeLoader]
    ED[EditorOpener]
    CLK[SystemClock]
  end
  CLI --> UC --> D
  WEB --> UC
  WEB -->|child cstudy per run| CLI
  UC --> FS & DB & RUN & GEN & WS & PY & ED & CLK
Loading

Dependencies point inward. Ports are Python Protocol classes in cli/src/coding_study/application/ports.py; adapters implement them; each command module in entrypoints/commands/ wires them, and entrypoints/cli.py registers the commands. Shared pure rules live in the domain: naming (per-language symbol names), layout (stub, statement, archive and reference paths, run targets), runs (runner request and per-case outcomes) and jdk (the supported JDK range, a problem's effective range and whether the selected JDK can run it, ADR 0016).

Port Adapter Notes
ProblemRepository FsProblemRepository Reads problems/, validates with JSON Schema, caches the validated meta.yaml documents in .cs/index.json (index)
ItemRepository FsItemRepository Reads the non-autograded items in tracks/<track>/<slug>/ and validates item.yaml with JSON Schema, through the same .cs/index.json
StudyGuides FsStudyGuides Locates the user's own study guides under config study_guides.root for item statements (study guides)
ProgressStore SqliteProgressStore Schema in progress-and-scheduling
LanguageRunner one per language (adapters/languages/<python,typescript,jvm,go,rust>/; the JVM adapter serves Java and Kotlin) prepare(scope, notify) returns Prepared (launch command) or BuildFailed, building when needed; scope BenchCode prepares the user's code and the references for one bench process. Several cstudy processes may share a workspace: the JVM, Go and Rust adapters build under a file lock and launch from a private copy of the build output, which RunnerProcess removes when the run ends (RunnerLaunch.scratch). The JVM adapter copies its class output (JVM build details); Go and Rust share adapters/languages/compiled_builds.py (lock, write-on-change generated files, abandoned-run cleanup) and each run gets its own scratch directory .cs/build/<lang>/run/<pid>-<random>/ holding a copy of the binary (Go, Rust)
RunnerProcess SubprocessRunner (one for every language) run(launch, request) -> RunReport and bench(launch, request) -> BenchReport: request file, event file, hang guard, restarts; protocol in runners
JdkSelector EnvJdkSelector selected() -> SelectedJdk | None: the JDK that JAVA_HOME names, else the JDK of the first java on PATH, with its version read from the release file in the JDK's home, so listing problems starts no JVM (the selected JDK); unusable_java_home() -> str | None: the JAVA_HOME as it is set when it holds no JDK, in which case nothing is selected (the selection does not fall back to PATH); selected_home() -> Path | None: the selected JDK's home, which doctor --fix passes to Gradle. Environment.jdk holds it: the use cases that start, test, submit, bench, list, pick or verify a problem ask it and apply the domain's range rules (domain/jdk_fit.py); the JVM adapter builds and launches with the same JDK
MachineProbe SystemMachine CPU model, power source, load average and core count for a bench run's environment record
StubGenerator one per language generate(meta) -> text; mapping table in problem-format
Workspace WorkspaceFs Stub paths, archive, statement rendering
SandboxEngine DockerSandbox (adapters/docker_sandbox.py) The container engine of service projects: its state, images, stacks that a guard takes down however cstudy ends, and leftovers by label. Only a service project reaches it
GradedStack, OpenStackClients SandboxGradedStack (application/graded_stack.py), open_stack_clients (adapters/http_grader/clients.py) What the HTTP grader asks of a ready stack and of the network
ContentCode ContentCodeLoader Imports generator.py, checker.py and stress_checker.py in-process (stdlib-only content code)
EditorOpener EnvEditorOpener Runs $VISUAL or $EDITOR
LanguageServerLocator LspDirectory (adapters/language_servers/) For each server of the code-intelligence catalog (domain/code_intelligence.py), whether .cs/lsp/ holds it, or PATH for rust-analyzer, and the version its version command prints, or, for JDT LS, whether the core plugin of its pinned milestone is unpacked, and for the Kotlin LSP the version, last day and license that the record of its fetched build names; today's date, for that last day; whether the workspace's marker records a server's license as accepted; and the selected Go's release, which pins gopls (web-ui)
LanguageServerInstaller LspDirectory Copies the shipped package.json and pnpm-lock.yaml into .cs/lsp/ and runs pnpm install --frozen-lockfile --ignore-scripts there; downloads an archive server such as JDT LS, checks its SHA-256 and unpacks it, a tar or a zip with its Unix modes and links, in place of its directory; finds the Kotlin LSP's newest build for this machine in its GitHub releases, with the SHA-256 JetBrains publishes beside it and the license at its tag, then downloads it the same way, asks it its last day and records it; builds a server that Go builds, gopls, with go install into .cs/lsp/bin/; records an accepted license in the workspace's marker; and runs a server's warm-up, the Kotlin LSP's bin/warmup.py under cstudy's Python (layout)
PreferencesStore FsPreferencesStore (adapters/preferences_fs.py) The web UI's .cs/serve/preferences.json; cstudy doctor reads code_intelligence from it
ProblemSetStore YamlProblemSets (adapters/yaml_problem_sets.py) The workspace's sets.yaml: read and validated, and changed by the lines a change needs, keeping the user's comments (ADR 0023)
WorkspaceSetup FsWorkspaceSetup The workspace template, the marker, the runner runtime (RunnerRuntime) and git init, for cstudy init and cstudy doctor
Clock, Random system implementations Injected for deterministic tests
Layer Package May import
Domain coding_study.domain stdlib only (no os, subprocess, sqlite3, pathlib I/O)
Application coding_study.application domain, ports
Adapters coding_study.adapters application, domain, third-party libs
Entrypoints coding_study.entrypoints everything

An import-linter contract in cli/.importlinter enforces the table (NFR-8).

Web adapter

cstudy serve is a second driving adapter beside the Typer CLI (ADR 0015, web-ui). It reaches the application in two ways: reads and small writes call use cases in-process, through one worker thread; runs and long jobs start a child python -m coding_study … and relay its output, so the terminal and the page run the same command.

Rule Detail
Package coding_study.entrypoints.web, beside entrypoints.commands; it may import every layer, as entrypoints do
Lazy import entrypoints/commands/serve.py imports entrypoints.web inside the command function only, so no other command loads Starlette or uvicorn (NFR-2); a test checks sys.modules after importing coding_study.entrypoints.cli
Import contracts starlette, uvicorn and wsproto join the forbidden modules of the domain and application contracts, and a new contract forbids them to coding_study.adapters
One WebSocket GET /api/v1/code-intelligence?attempt_id=N, for language-server sessions only, on uvicorn's wsproto implementation; every other client action stays an ordinary request (ADR 0018, web-ui). The route, its sessions, its server processes and its broker live in entrypoints/web/ (api_code_intelligence.py, code_intelligence.py, language_server.py, language_broker.py, documentation.py, library_files.py), where runs already start child processes: each session runs one language server as a child, in a session of its own, at most four at once
Code intelligence The catalog of servers is domain data (domain/code_intelligence.py: per language the command, version command, root rule, initialization options, client capabilities, settings and idle time). application/code_intelligence.py holds language_server_states, for the doctor rows and the Settings view, and install_language_servers, download_language_server and install_language_server_with_go, for cstudy doctor --fix, behind LanguageServerLocator, LanguageServerInstaller and PreferencesStore. Like highlighting, it grades nothing and records nothing
New use cases application/attempt_files.py (an attempt's files: tree, read, save, create, move, delete), application/attempt_changes.py (what the notifications watch, and what changed between two snapshots of it) and application/problem_history.py (shared with cstudy history); project_status grows into attempt_status for every kind
New port methods Workspace.read_within, delete_within and move_within, beside write_within, none following a link; Workspace.locked_with_deadline, the project lock that gives up after a deadline, so a page change answers 409 locked instead of waiting; ProgressStore.attempt, an attempt by its id
No new judging The adapter never runs user code and never compares an output; it relays --events lines unchanged

Languages

Language has six members. ALL_LANGUAGES is what a problem lists by default and what every "all languages" default, coverage line and IDE action means; a problem lists fewer only where a language cannot express it (check S13), on the concurrency and debugging tracks (LANGUAGE_SUBSET_TRACKS) or in the security, identity and AI domains (LANGUAGE_SUBSET_DOMAINS, ADR 0020); a project problem lists exactly one (ADR 0014), and code that branches on ProblemKind uses the same exhaustive match. CORE_LANGUAGES (Python, TypeScript, Java, Kotlin) are the languages whose toolchains every workspace needs: cstudy doctor fails without them, and only warns about Go and Rust (ADR 0012). Code that branches on a language uses an exhaustive match ending in assert_never, and tests/test_language_tables.py fails when a table keyed by Language lacks a row.

The adapter registry holds one stub generator and one runner per language that has an adapter package. Every language has its adapters now. A language without them would raise LanguageNotSetUpError, which the CLI reports as error: rust stub generator is not set up yet with exit code 4, as it does for any part that is not built yet; cstudy start --lang rust has already rejected any problem that does not list Rust with exit code 3. cstudy doctor handshakes the runners of the core languages only; for Go and Rust it checks the toolchains and debuggers and warns when one is missing.

Use cases

Use case Command
PickNext cstudy next
StartAttempt cstudy start
RunTests cstudy test
RevealHint, RevealSolution cstudy hint, cstudy solution
CompleteAttempt cstudy done
RunBench cstudy bench, cstudy test --perf
ListProblems, ShowStats cstudy list, cstudy stats
VerifyContent, ScaffoldProblem cstudy verify, cstudy new
StartMock cstudy mock
ExportProgress, ImportProgress cstudy export, cstudy import
InitWorkspace cstudy init
AttemptStatus, ProblemHistory cstudy status, cstudy history
AttemptFiles The web UI's editor and file tree

CLI libraries

Need Library
Commands Typer
Terminal output Rich
YAML PyYAML (safe_load)
Schema validation jsonschema
File watching watchfiles
Web server (cstudy serve only) Starlette on uvicorn, without uvicorn's standard extra; wsproto for the code-intelligence WebSocket
Markdown to HTML (cstudy serve only) markdown-it-py and Pygments, declared directly although Rich installs them too
Tests, lint, types pytest, ruff, pyright, import-linter; httpx2 for Starlette's test client and Playwright in an optional group, for the web UI

Data flow: cstudy start two-sum --lang kotlin

sequenceDiagram
  participant U as User
  participant C as CLI
  participant R as ProblemRepository
  participant G as KotlinStubGenerator
  participant W as Workspace
  participant S as ProgressStore
  participant J as JdkSelector
  U->>C: cstudy start two-sum --lang kotlin
  C->>R: load("two-sum")
  C->>J: selected JDK within the problem's range?
  Note over C,J: outside it: error, exit 4, nothing written
  C->>W: stub exists?
  alt exists and not --fresh
    C->>W: keep file
  else new or --fresh
    W->>W: archive old file to .archive/
    C->>G: generate(signature)
    G-->>W: write Solution.kt
  end
  C->>W: render .statements/two-sum.md
  C->>S: insert attempt(status=in_progress)
  C->>U: print paths, open $EDITOR
Loading

Data flow: cstudy test

sequenceDiagram
  participant C as CLI core
  participant R as ProblemRepository
  participant X as ContentCode
  participant L as LanguageRunner
  participant S as ProgressStore
  participant J as JdkSelector
  C->>S: active attempt (slug, lang)
  C->>R: load meta, tests
  C->>J: selected JDK within the problem's range? (Java, Kotlin)
  C->>X: materialize generated cases (cached)
  C->>L: prepare (JVM: Gradle compile of one package)
  C->>L: run(request without expected values)
  L-->>C: JSONL events: case_start, case_result(output)
  C->>C: compare(output, expected) via comparator
  C->>C: apply time limit if enabled
  C->>S: insert test_run
  C-->>C: render report, exit code
Loading

The runner never receives expected values. Comparison, time-limit verdicts and reporting are core logic.

Data flow: a test run from the page

sequenceDiagram
  participant T as Browser tab
  participant S as cstudy serve
  participant C as Child cstudy
  participant R as Runner
  T->>S: POST /api/v1/runs (attempt, test, options)
  S->>S: Pre-checks in-process; take the attempt's run slot
  S->>C: python -m coding_study test SLUG --lang L --events
  S-->>T: 201 with the run id
  T->>S: GET /api/v1/runs/ID/events
  C->>R: The job, as cstudy test runs it
  R-->>C: case_start, case_result
  C-->>S: One event line per start and judged result
  S-->>T: One server-sent event per line, data unchanged
  C-->>S: run_end, then exit
  S-->>T: closed, with the exit code; the slot frees
Loading

Cancel sends the child SIGINT, which ends the run as Ctrl-C does (web-ui).

Repository layout

coding-study/
  pyproject.toml, uv.lock   # the installable `coding-study` package (hatchling): cli/src plus the content
  .python-version           # 3.13
  cli/                      # the CLI's sources, tests and tool settings, entry point `cstudy`
    src/coding_study/{domain,application,adapters,entrypoints}/
    src/coding_study/adapters/templates/            # `cstudy new` templates
    src/coding_study/adapters/workspace_template/   # what `cstudy init` copies
    src/coding_study/adapters/ide/                  # VS Code and IntelliJ renderers, generated-file store, launcher jar
    tests/
  runners/
    python/                 # uv project: cs_runner, cs_support
    typescript/             # runner.ts, support.ts
    jvm/                    # Gradle included build: support + runner (Java)
    conformance/            # fixture problems for every DSL type and error path, expectations.json
  schemas/                  # meta, tests, item, conformance, events and web API JSON Schemas
  tools/ide-launcher/       # source and reproducible build of the IntelliJ open-file launcher (its jar ships in adapters/ide/)
  tools/web-ui/             # sources, lockfile and build of the web UI front end (its bundle ships in entrypoints/web/static/)
  problems/<id>-<slug>/     # autograded content (format: specs/problem-format.md)
  tracks/                   # non-autograded content (system design, code review, behavioral)

The repository holds no workspace. A workspace, created by cstudy init anywhere else, holds the practice projects, the project problems' folders in projects/, notes/, .archive/, .statements/, its own problems/, .cs/ and progress.db (layout).

Workspace decisions

Decision Choice Reason
Where is the workspace? Outside this repository, in its own directory and optionally its own git repository (ADR 0008) Solutions must never be committed with the public content; the workspace moves between machines on its own
Commit practice files? Tracked in the workspace Attempt history in git is valuable and the user owns the workspace. Tradeoff: noisier history; mitigated by committing only when the user chooses (Q3 in the PRD).
Repeat attempts cstudy start --fresh (and review attempts by default) move the old file to .archive/<slug>/<attempt_id>-<lang>/ before writing a new stub Re-solving from scratch is the point of spaced repetition; nothing is lost
progress.db Ignored by the workspace's .gitignore Binary churn; cstudy export covers backup (PRD Q2)
Hiding references References live only in the content's problems/*/solutions/ Open a workspace project (python/, typescript/, jvm/) as the editor root so file pickers never index references
Statements Rendered to .statements/<slug>.md Excludes hints and locked progressive levels

Per-language workspace projects

Language Project root User file Tooling
Python python/ practice/<snake_slug>.py pyproject.toml (requires-python >= 3.13), .venv via uv sync, cs-support as editable path dependency on ../.cs/runtime/runners/python, ruff and pyright configured
TypeScript typescript/ practice/<slug>.ts package.json with "type": "module" and "imports": {"#support": "./support.ts"}; support.ts re-exports ../.cs/runtime/runners/typescript/support.ts; tsconfig.json with noEmit, allowImportingTsExtensions, erasableSyntaxOnly, verbatimModuleSyntax, strict; dev deps typescript, @types/node via pnpm
Java jvm/java/ src/main/java/practice/<snake_slug>/Solution.java (or <ClassName>.java) Gradle subproject; no toolchain: compiles with the selected JDK at its release level, capped at 26 (runners)
Kotlin jvm/kotlin/ src/main/kotlin/practice/<snake_slug>/Solution.kt (or <ClassName>.kt) Gradle subproject; Kotlin JVM plugin; jvmTarget and -Xjdk-release at the same release level as Java
Go go/ practice/<snake_slug>/<snake_slug>.go go.mod (module workspace, Go 1.24) with one package per problem; replace cstudy => ../.cs/runtime/runners/go provides cstudy/support. The root go.work uses ./go. Built offline (ADR 0012)
Rust rust/ practice/<snake_slug>.rs Cargo.toml (package practice, edition 2021) with one [[example]] built as a library per started problem, which cstudy start adds, cs_support from ../.cs/runtime/runners/rust/cs_support as a dev-dependency, and an empty lib.rs. For the editor only: cstudy builds solutions offline in .cs/build/rust (ADR 0012)

Project roots are relative to the workspace. jvm/settings.gradle.kts uses includeBuild("../.cs/runtime/runners/jvm"); both subprojects depend on the runner and support artifacts. IntelliJ opens jvm/ as one Gradle project. Every path into the runners is relative and points at the workspace's runner runtime, so a workspace survives a move or a clone on another machine.

Toolchain

Tool Version Role Needed by
Python 3.13 CLI, Python runner Everyone (uv python install 3.13)
uv 0.10 or newer CLI install (uv tool install git+https://github.com/bkuberek/coding-study, or --editable . in a checkout), venvs Everyone
Node 24 or newer TypeScript runner via type stripping TypeScript practice
pnpm 9 or newer TypeScript workspace dev deps TypeScript practice
OpenJDK 21 to 26; 25 is the one to install Java and Kotlin compile and run, on the JDK that JAVA_HOME or PATH selects Java and Kotlin practice
Gradle Homebrew, used once Generates the wrapper in the workspace template's jvm/ Maintainers regenerating the wrapper (brew install gradle)
Gradle wrapper pinned in gradle-wrapper.properties All JVM builds Nobody: it ships in the workspace template, and cstudy init copies it
Kotlin compiler pinned Kotlin Gradle plugin in gradle/libs.versions.toml Fetched by Gradle; no global kotlinc Nobody: Gradle fetches it
Go 1.25 or newer Go runner and the go/ practice project, built offline Go practice (optional; cstudy doctor warns when missing)
Rust rustc 1.80 or newer, through rustup Rust runner and the rust/ practice project, built offline Rust practice (optional; cstudy doctor warns when missing)

cstudy doctor checks every row and cstudy doctor --fix restores missing workspace files, refreshes the runner runtime, then runs uv sync, pnpm install and a Gradle warm-up build.

Cache directory .cs/

The workspace's .cs/ (ignored by its .gitignore, except the workspace.toml marker). Outside a workspace, cstudy list and cstudy show cache in $XDG_CACHE_HOME/coding-study/catalog/ instead.

Path Content Invalidation
.cs/index.json Parsed and validated meta.yaml and item.yaml documents (index) Per file: modification time (ns) and size of the file and of its schema
.cs/cases/<slug>/<hash>.json Generated cases with expected values Hash of generator, tests, expected-source approach
.cs/calibration/<lang>/<slug>.json Reference per-case timings for time limits Hash of reference file, tests, runtime version, machine id
.cs/build/<lang>/ Materialized references for verify and bench; in Go and Rust also the generated build and its runner binaries (Go, Rust) Rebuilt on change
.cs/runs/<run_id>/ Request and result files Keep last 20
.cs/serve/ The web UI's preferences.json, and the redirect file <port>.html that cstudy serve --open writes (web-ui) Preferences kept; the redirect file removed when the server stops
.cs/lsp/ The web UI's language servers: package.json and pnpm-lock.yaml from cstudy's package data, and node_modules/; JDT LS in jdtls/ and its data, per root and session, in jdtls-data/; the Kotlin LSP in kotlin-lsp/ and its data in kotlin-lsp-data/; gopls in bin/ (layout) Rewritten and reinstalled by cstudy doctor --fix while the code_intelligence preference is on, when the shipped manifest differs
.cs/runtime/ The runner runtime: a copy of the content's runners/{python,typescript,jvm,go} with its build output, and stamp.json Digest of the copied sources; refreshed before the first runner of a command, by cstudy init and by cstudy doctor --fix

Index cache

cstudy list, cstudy next and cstudy stats read every meta.yaml and item.yaml; parsing and schema-validating them dominated their time (about 0.8 s with 207 problems, against NFR-2's 500 ms). The index stores each file's validated document so a warm run only stats the files.

{
  "version": 2,
  "entries": {
    "/home/me/coding-study/problems/0001-two-sum/meta.yaml": {"stamp": [1761350000123456789, 1520, 1761340000000000000, 9480], "document": {"id": "0001", "slug": "two-sum"}}
  }
}
Rule Detail
Key Absolute path of the YAML file, so the catalog and a workspace's own problems/ share one index
stamp [file mtime ns, file size, schema mtime ns, schema size]: an entry is a hit only when all four equal the current values, so editing a file or its schema is a miss. Nothing is hashed
Stored Only documents with no schema violation, so invalid content is reported on every run
Not stored tests.json, solutions and statements: cstudy start, cstudy test and cstudy verify read what they need directly. The reference languages of a problem still come from a directory listing each run
Housekeeping A full listing drops the entries of files that no longer exist under its directory (problems/ or tracks/) and rewrites the index only when it changed (write to a temporary file, then rename)
Failure A missing, corrupt or other-version index, or one that cannot be written, is a cold read: never an error
Loader yaml.CSafeLoader when PyYAML has libyaml, else yaml.SafeLoader; the schema validators are built on first use