Skip to main content

Contributing to Constellation Engine

Thank you for your interest in contributing to Constellation Engine! This guide will help you get started with development.

First-Time Contributors

New to the codebase? Start with issues labeled good first issue on GitHub. These are self-contained tasks that introduce you to the project structure without requiring deep domain knowledge.

Table of Contents​

  1. Prerequisites
  2. Quick Start
  3. Development Workflow
  4. Project Structure
  5. Running Tests
  6. VSCode Setup
  7. Common Tasks
  8. Troubleshooting

Prerequisites​

Before you begin, ensure you have:

  • Java 17+ (JDK 17 or later)
  • SBT 1.10+ (Scala Build Tool)
  • Node.js 18+ and npm
  • VSCode (recommended for extension development)

Verify Installation​

java -version    # Should show Java 17+
sbt --version # Should show SBT 1.10+
node --version # Should show Node 18+
npm --version

Quick Start​

1. Clone and Install Dependencies​

git clone https://github.com/VledicFranco/constellation-engine.git
cd constellation-engine

# Install all dependencies
make install
# Or manually:
sbt update
cd vscode-extension && npm install && cd ..

2. Start Development Environment​

Option A: Using Make (recommended)

make dev    # Starts server + TypeScript watch

Option B: Using Scripts

# Windows PowerShell
.\scripts\dev.ps1

# Unix/Mac
./scripts/dev.sh

Option C: Manual

# Terminal 1: Start server
sbt "exampleApp/runMain io.constellation.examples.app.server.ExampleServer"

# Terminal 2: Watch TypeScript
cd vscode-extension && npm run watch

3. Launch VSCode Extension​

  1. Open VSCode in the project root
  2. Press F5 to launch the extension in a new window
  3. Open a .cst file and start coding!

Development Workflow​

Make Commands​

CommandDescription
make devStart full dev environment (server + watch)
make serverStart HTTP/LSP server only
make watchWatch Scala and recompile on changes
make ext-watchWatch TypeScript and recompile
make testRun all tests
make compileCompile all modules
make cleanClean build artifacts
make assemblyBuild fat JAR for deployment
make docker-buildBuild Docker image
make docker-runRun in Docker container

Hot Reload (Server Auto-Restart)​

For automatic server restart on code changes:

make server-rerun
# Or: sbt "~exampleApp/reStart"

This requires the sbt-revolver plugin (already configured).

VSCode Tasks​

Press Ctrl+Shift+P → "Tasks: Run Task" to see available tasks:

  • Compile All - Compile Scala modules
  • Run Tests - Run all tests
  • Start Server (ExampleLib) - Start the HTTP/LSP server
  • Watch Compile - Watch mode for Scala
  • Full Dev Setup - Start server + extension watch

Project Structure​

constellation-engine/
├── modules/
│ ├── core/ # Type system (CType, CValue)
│ ├── runtime/ # Execution engine, ModuleBuilder
│ ├── lang-ast/ # AST definitions
│ ├── lang-parser/ # Parser (cats-parse)
│ ├── lang-compiler/ # Type checker, DAG compiler
│ ├── lang-stdlib/ # Standard library functions
│ ├── lang-lsp/ # Language Server Protocol
│ ├── http-api/ # HTTP server (http4s)
│ └── example-app/ # Example application
├── vscode-extension/ # VSCode extension (TypeScript)
├── docs/ # Documentation
├── scripts/ # Development scripts
├── Makefile # Build automation
└── build.sbt # SBT configuration

Module Dependencies​

core → runtime → lang-compiler → http-api
↓ ↓
lang-ast lang-stdlib, lang-lsp, example-app
↓
lang-parser

Rule: Modules can only depend on modules above them. No circular dependencies.


Running Tests​

All Tests​

Test Before Committing

Always run make test before pushing. CI runs the full test suite, and PRs with failing tests cannot be merged.

make test
# Or: sbt test

Module-Specific Tests​

make test-core       # Test core module
make test-compiler # Test compiler
make test-lsp # Test LSP server
make test-parser # Test parser

Watch Mode (Continuous Testing)​

sbt "~test"          # Re-run tests on changes
sbt "~testQuick" # Only re-run failed tests

VSCode Setup​

  • Scala (Metals) - Scala language support
  • Scala Syntax - Syntax highlighting

Settings​

The project includes .vscode/tasks.json with pre-configured tasks.

Debugging the Extension​

  1. Open the vscode-extension/ folder in VSCode
  2. Press F5 to launch
  3. A new VSCode window opens with the extension loaded
  4. Set breakpoints in TypeScript files

Debugging the Scala Backend​

  1. Start the server with debug port:
    sbt -jvm-debug 5005 "exampleApp/run"
  2. Attach a debugger to port 5005

Common Tasks​

Adding a New Module Function​

  1. Define the module in modules/example-app/src/main/scala/.../modules/:

    case class MyInput(value: String)
    case class MyOutput(result: String)

    val myModule: Module.Uninitialized = ModuleBuilder
    .metadata("MyModule", "Description", 1, 0)
    .implementationPure[MyInput, MyOutput] { input =>
    MyOutput(input.value.toUpperCase)
    }
    .build
  2. Add signature to ExampleLib in ExampleLib.scala:

    private val myModuleSig = FunctionSignature(
    name = "MyModule",
    params = List("value" -> SemanticType.SString),
    returns = SemanticType.SString,
    moduleName = "MyModule"
    )
  3. Register in allSignatures and add module to allModules

  4. Restart server to pick up changes

Testing a Constellation-Lang Pipeline​

Create a .cst file:

in text: String
result = MyModule(text)
out result

Run with Ctrl+Shift+R in VSCode or via HTTP:

curl -X POST http://localhost:8080/compile \
-H "Content-Type: application/json" \
-d '{"source": "in x: String\nresult = Uppercase(x)\nout result", "dagName": "test"}'

Troubleshooting​

Server won't start​

  1. Check if port 8080 is in use:
    netstat -an | grep 8080
  2. Kill existing Java processes:
    # Windows
    taskkill /F /IM java.exe

    # Unix
    pkill -f "java.*constellation"

SBT compilation errors​

  1. Clean and recompile:
    make clean
    make compile
  2. Update dependencies:
    sbt update

Extension not connecting to LSP​

  1. Ensure server is running on port 8080
  2. Check VSCode output panel for errors
  3. Verify WebSocket URL in settings: ws://localhost:8080/lsp

TypeScript compilation errors​

cd vscode-extension
rm -rf node_modules out
npm install
npm run compile

Code Style​

Formatting Requirements

All Scala code must pass scalafmt checks before merging. Run sbt scalafmtCheck locally to verify formatting. PRs with formatting violations will fail CI.

  • Scala: Follow standard Scala 3 conventions
  • TypeScript: Follow existing patterns in the codebase
  • Commits: Use conventional commit messages
  • Tests: Add tests for new functionality

Error Handling Patterns​

The codebase uses standardized error handling patterns for consistency and maintainability.

HTTP Routes (EitherT Pattern)​

For HTTP endpoints that execute multiple operations, use EitherT to chain operations cleanly:

import cats.data.EitherT
import io.constellation.errors.{ApiError, ErrorHandling}

// Define a chain of operations using EitherT
private def executeByRef(req: ExecuteRequest): EitherT[IO, ApiError, DataSignature] =
for {
image <- EitherT(constellation.pipelineStore.getByName(req.ref).map {
case Some(img) => Right(img)
case None => Left(ApiError.NotFoundError("Pipeline", req.ref))
})
loaded = LoadedPipeline(image, Map.empty)
inputs <- ErrorHandling.liftIO(convertInputs(req.inputs))(t => ApiError.InputError(t.getMessage))
sig <- ErrorHandling.liftIO(constellation.run(loaded, inputs))(t => ApiError.ExecutionError(t.getMessage))
} yield sig

// In the route handler, pattern match on the result
case req @ POST -> Root / "execute" =>
(for {
execReq <- req.as[ExecuteRequest]
result <- executeStoredDag(execReq).value
response <- result match {
case Right(outputs) => Ok(ExecuteResponse(success = true, outputs = outputs))
case Left(ApiError.NotFoundError(_, name)) => NotFound(...)
case Left(ApiError.InputError(msg)) => BadRequest(...)
case Left(error) => InternalServerError(...)
}
} yield response).handleErrorWith { error =>
InternalServerError(...) // Catch-all for unexpected errors
}

Key principles:

  • Use ApiError sealed trait for typed errors
  • Use ErrorHandling.liftIO to wrap IO operations with error mapping
  • Keep outer handleErrorWith as catch-all for unexpected errors
  • Pattern match on specific error types to return appropriate HTTP status codes

LSP Notification Handlers (Logging Pattern)​

For LSP notification handlers (didOpen, didChange, didClose), log errors instead of silently swallowing:

private def handleDidOpen(notification: Notification): IO[Unit] = {
for {
params <- IO.fromEither(...)
_ <- documentManager.openDocument(...)
_ <- validateDocument(...)
} yield ()
}.handleErrorWith(e => logger.warn(s"Error in didOpen: ${e.getMessage}"))

Key principles:

  • Never silently swallow errors with handleErrorWith(_ => IO.unit)
  • Log errors with context (operation name, error message)
  • Notification handlers should complete successfully even on error (LSP protocol requirement)
  • Use logger.warn for recoverable errors

API Error Types​

The io.constellation.errors.ApiError sealed trait provides typed errors:

sealed trait ApiError { def message: String }

object ApiError {
case class InputError(message: String) extends ApiError // JSON -> CValue conversion errors
case class ExecutionError(message: String) extends ApiError // DAG execution errors
case class OutputError(message: String) extends ApiError // CValue -> JSON conversion errors
case class NotFoundError(resource: String, name: String) extends ApiError
case class CompilationError(errors: List[String]) extends ApiError
}

ErrorHandling Utilities​

The io.constellation.errors.ErrorHandling object provides helpers:

// Wrap an IO operation, mapping exceptions to typed errors
ErrorHandling.liftIO(riskyOperation)(t => ApiError.InputError(t.getMessage))

// Convert Either to EitherT
ErrorHandling.fromEither(either)

// Convert Option to EitherT with error for None
ErrorHandling.fromOption(opt, ApiError.NotFoundError("DAG", name))

Getting Help​

  • Check existing issues on GitHub
  • Review documentation in /docs
  • See docs/architecture.md for comprehensive architecture guide

Happy coding! 🚀