Skip to content

Latest commit

 

History

History
87 lines (59 loc) · 7.58 KB

File metadata and controls

87 lines (59 loc) · 7.58 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project

SwiftStanServer is a non-sandboxed macOS app that hosts an HTTP server exposing SwiftStan commands over an OpenAPI interface. It is the execution tier: it owns cmdstan and the ~/Documents/StanCases filesystem, and clients (notably SwiftStanApp, and potentially SwiftStats) talk to it over HTTP. The HTTP layer is generated by Apple's Swift OpenAPI Generator with a Hummingbird transport. macOS 26+; Swift 6.

Three independent projects, coupled only by contracts:

  • SwiftStanLibrary — the distributable library at https://github.com/SwiftProjectOrganization/SwiftStanLibrary. This is what the server depends on.
  • SwiftStanServer (this) — links SwiftStanLibrary and serves its public commands.
  • SwiftStanApp (client) — has no build dependency on this project; talks to the server purely over HTTP, generating its client from a copy of openapi.yaml.

The full design rationale lives in ../SwiftStan/Docs/SwiftStanServer-Plan.md.

Getting started

The repo includes the .xcodeproj with all source files, the OpenAPI spec, entitlements, and SPM dependencies already wired.

  1. Clone https://github.com/SwiftProjectOrganization/SwiftStanServer and open SwiftStanServer.xcodeproj in Xcode.
  2. Trust the build plugin when prompted (OpenAPIGenerator). The first build generates APIProtocol/Types from openapi.yaml.
  3. Set your cmdstan path in the GUI or via the $CMDSTAN environment variable.
  4. Build and run.

Package dependencies (for reference)

Package Product Purpose
https://github.com/SwiftProjectOrganization/SwiftStanLibrary SwiftStan Core Stan library
https://github.com/apple/swift-openapi-generator ≥ 1.6.0 OpenAPIGenerator plugin Code generation at build time
https://github.com/apple/swift-openapi-runtime ≥ 1.8.0 OpenAPIRuntime Runtime types
https://github.com/hummingbird-project/swift-openapi-hummingbird ≥ 2.0.1 OpenAPIHummingbird Transport adapter
https://github.com/hummingbird-project/hummingbird ≥ 2.5.0 Hummingbird HTTP server

Architecture

HTTP contract

OpenAPI/openapi.yaml is the canonical spec (SwiftStanApp keeps a byte-identical copy). 14 POST operations plus GET /v1/health. dsl2stan is not included — SwiftStanLibrary dropped it because it requires recompiling the package source tree via swiftc (not viable in a distributed library; the in-process stancode covers alist→Stan without swiftc).

Every command returns 200 with a CommandResult { status, error, outputPath? }:

  • status — the human-readable status line (library tuple .0, or "Wrote <file>" for file ops).
  • error — library tuple .1; "" on success. Non-empty = logical failure at 200. Real 4xx/5xx reserved for transport faults.
  • outputPath — set by file-translation ops; omitted for cmdstan ops.

stanCases per-request override

Every request accepts an optional stanCases field (e.g. "StanCases", "SR2Cases", "ARM/Chapter3"). The server resolves it by appending to ~/Documents, with a path-traversal guard. When absent, falls back to $STAN_CASES or ~/Documents/StanCases. The resolved URL is passed directly to the library's caseRoot: parameter.

Three operation families (mirror SwiftStanLibrary)

  • Model listing (models) → lists subdirectories of the resolved StanCases root. Request carries optional stanCases.
  • cmdstan-backed (compile, sample, optimize, pathfinder, laplace, generated_quantities, stansummary, ulam) → call library funcs returning (String, String). Requests carry model, arguments (verbatim cmdstan key=value), optional cmdstan override, stanCases, verbose, plus per-op flags (compile: install/force; sample: install/nosummary + structured sampling params; ulam: force).
  • pure-Swift file translation (csv2json, alist2dsl, stancode, stan2alist, runinfo) → call library funcs that throw/return URL. Requests carry model (required), stanCases, verbose (+ force for stan2alist).

SampleRequest structured sampling parameters

sample accepts first-class fields that are translated to cmdstan tokens by sampleTokens() before being merged with arguments: num_samples, num_warmup, thin, adapt_delta, max_treedepth, num_chains, seed. These map to the cmdstan hierarchical parser's expected key=value tokens under the sample subcommand.

Files

  • StanAPIHandler.swiftstruct StanAPIHandler: APIProtocol; one method per operation forwarding to the matching SwiftStan function. Synchronous library calls (blocking Process.run()) are wrapped in Task.detached via offload(_:) so the event loop isn't starved.
  • ServerController.swift@Observable @MainActor class owning the Hummingbird Application lifecycle (start()/stop()), binding to 0.0.0.0:<port> (all interfaces, reachable over the local network).
  • ServerSettings.swift — cmdstan path (UserDefaults "cmdstanPath"$CMDSTAN → hardcoded default), port (UserDefaults "serverPort", default 8080), StanCases root, and per-request resolveStanCasesRoot(_:).
  • RequestLog.swift@Observable @MainActor bounded log (max 200 entries) of requests handled by StanAPIHandler; owned by ServerController, read by ContentView.
  • SwiftStanServerApp.swift@main App; starts the server on appear.
  • ContentView.swift — Liquid Glass GUI: running indicator with hostname URL, port, cmdstan path, Start/Stop, and a scrollable recent-requests log.

Long-running operations

Calls are synchronous (one blocking HTTP request per command — localhost, generous timeouts). cmdstan sampling/compiling can take minutes; the server keeps the response open until the library returns. If timeouts bite, raise Hummingbird's response/idle timeout in ServerController.

Key constraints

  • macOS 26+; Swift 6. Non-sandboxed (shells out to cmdstan, writes ~/Documents/StanCases). Developer-ID / direct distribution.
  • Depends on SwiftStanLibrary (https://github.com/SwiftProjectOrganization/SwiftStanLibrary). Do not add a dependency on the original SwiftStan package (the one with the CLI) or on SwiftStanApp.
  • Required server-side env: CMDSTAN (or set in the GUI / UserDefaults). STAN_CASES optional (defaults to ~/Documents/StanCases). SWIFTSTAN_PROJECT_ROOT is no longer needed (dsl2stan is not in the API).
  • Keep openapi.yaml byte-identical to the copy in SwiftStanApp (canonical spec, copied).
  • The server binds to 0.0.0.0 (all interfaces). Clients on the same LAN can reach it — not just localhost.

Verification

  1. Build in Xcode (after first-time setup). Confirm the plugin emitted APIProtocol/Types and StanAPIHandler compiles.
  2. Run; the GUI shows "Running on http://<hostname>:8080". curl http://127.0.0.1:8080/v1/health → JSON with resolved cmdstan + StanCases paths.
  3. curl -XPOST http://127.0.0.1:8080/v1/stancode -H 'Content-Type: application/json' -d '{"model":"bernoulli"}'{"status":"Wrote bernoulli.stan","error":"","outputPath":"..."}.
  4. curl -XPOST .../v1/compile -d '{"model":"bernoulli"}' then .../v1/sampleerror:"" and bernoulli.samples.csv appears under ~/Documents/StanCases/bernoulli/Results/.
  5. curl -XPOST http://127.0.0.1:8080/v1/models -H 'Content-Type: application/json' -d '{}'{"models":["bernoulli",...],"root":"...","error":""}.