Design¶
How marola-app is put together: three sbt modules, the pipeline that answers "what's the best hour
tomorrow to swim nearby?", the two places it uses AI and why they stay apart, and where the effect
boundary sits. The siblings go deeper: Effects map (what is pure, what is
< Sync), Heuristics (the scoring internals) and
Integrations (each pluggable backend). How marola fits with the other
repos is the umbrella's Architecture.
Three modules¶
Traits live in core, local implements them, and cli is the one place that wires both
together (ADR-0001 has why).
The DSPy compile, the fine-tune and the benchmark gate live in marola-ml, which runs this repo's image rather than reading its tree.
Module map¶
Every source, regenerated from find core local cli -name '*.scala': the main sources by
package, then the tests.
core/src/main/scala/marola
Recommender.scala the pipeline, from origin to ranked hours
beaches
BeachFinder.scala named beaches from Overpass, across three mirrors
BeachSnapshot.scala beach lists on disk (MAROLA_BEACHES_DIR)
Facilities.scala Facility, Facilities and the AccessibilityClient trait (MIP-0021)
NoopAccessibilityClient.scala MAROLA_FACILITIES=off: no call, no data
OverpassAccessibilityClient.scala amenities within 300 m of each beach, one Overpass query
conditions
OpenMeteoClient.scala hourly weather and marine data, in the beach's timezone
Tides.scala tide turns from the hourly sea level (pure)
http
Http.scala java.net.http at the Sync boundary, with retries
json
Json.scala the hand-rolled JSON reader and writer
knowledge
Corpus.scala corpus loading, chunking, cosine
FileKnowledgeStore.scala the JSON vector index under data/
KnowledgeStore.scala the Embedder and KnowledgeStore traits, Passage
OceanQa.scala grounded Q&A: retrieve, answer from the passages, fall back
SafetyFooter.scala the emergency footer (MIP-0022)
ledger
RunLedger.scala the RunLedger trait and its Noop (MIP-0010)
llm
CompiledPrompt.scala replays a DSPy-compiled artifact as chat messages
LlmClient.scala the LlmClient trait, ChatMessage
Reviewer.scala the second LLM pass that grades the draft
TracedLlmClient.scala one llm.<model> span per call
location
IpGeolocation.scala origin from three IP providers, medoid vote
log
Log.scala the SLF4J wrapper
lore
SeaLore.scala the sourced "did you know?" paragraph
model
Models.scala Coordinates, Beach, HourlyConditions, BestHour, the enums
observability
Tracing.scala the Tracing trait and its Noop
scoring
Note.scala a scoring reason as a code plus arguments (MIP-0054)
Swimability.scala the score, the heuristics, the water verdict (pure)
sightings
Sighting.scala Sighting, SightingKind
SightingStore.scala the SightingStore trait
site
Board.scala the per-area, per-day board JSON the map renders (pure)
trails
TrailFinder.scala named paths and tracks near the beaches (MIP-0030)
vision
VisionClient.scala the VisionClient trait
water
WaterQuality.scala samples, points, freshness, the WaterQualityClient trait
WaterQualityMatcher.scala agency points → OSM beaches (pure)
local/src/main/scala/marola
knowledge
OllamaEmbedder.scala embeddings from Ollama's native /api/embed
ledger
MlflowApi.scala the MLflow REST calls the ledger and the tracer share
MlflowRunLedger.scala benchmark runs into a local MLflow server
llm
LocalLlmClient.scala any OpenAI-compatible chat endpoint (Ollama by default)
observability
MlflowTracing.scala OTLP/HTTP spans into MLflow
sightings
LocalFileSightingStore.scala JSON lines in data/sightings.jsonl
vision
LocalVisionClient.scala a multimodal Ollama model
water
CachedWaterQualityClient.scala the last good fetch when the agency is down
FallbackWaterQualityClient.scala a primary source, then a backup when it gives nothing
ImaScPdfParser.scala IMA/SC's bulletin rows
ImaScPdfWaterQualityClient.scala IMA/SC's bulletin, joined to the feed's coordinates
ImaScWaterQualityClient.scala IMA/SC's JSON feed
IneaPdfParser.scala INEA/RJ's bulletin table
IneaRjWaterQualityClient.scala INEA/RJ's latest PDF per zone
InemaBaWaterQualityClient.scala INEMA/BA's bulletin PDF
InemaPdfParser.scala INEMA/BA's bulletin table
PdfLines.scala PDF text with its geometry, for both parsers
SamplingPointCoordinates.scala bundled point → coordinate tables for RJ and BA
cli/src/main/scala/marola
AppConfig.scala MAROLA_* settings and one factory per integration
Main.scala the CLI (KyoApp): every mode in the CLI reference
Report.scala the text output: ranked list, detailed block, lore, answers (pure)
agent
ChatServer.scala --serve-chat: /health and /ask for the map's chat widget
SwimConditionsMcpServer.scala the four MCP tools over stdio
bench
BenchmarkLedger.scala a benchmark report as an MLflow run
OceanBenchmark.scala --benchmark: 22 questions, three arms
site
SiteBuilder.scala --site: the boards for every area of an --areas file
The tests, one spec per class or concern:
core/src/test/scala/marola
beaches AccessibilitySpec, BeachSnapshotSpec
conditions TidesSpec
http HttpSpec
knowledge CorpusSpec, RagOfflineSpec, SafetyFooterSpec
ledger NoopRunLedgerSpec
llm SummarizeFlowSpec, TracedLlmClientSpec
location IpGeolocationSpec
lore SeaLoreSpec
model CoordinatesSpec
observability TracingSpec
scoring NoteSpec, SwimabilitySpec, WaterVerdictSpec
trails TrailFinderSpec
water WaterQualityMatcherSpec
local/src/test/scala/marola
knowledge OllamaEmbedderSpec
ledger MlflowRunLedgerSpec
observability MlflowTracingSpec
sightings LocalFileSightingStoreSpec
water CachedWaterQualityClientSpec, ImaScPdfParserSpec, ImaScPdfWaterQualityClientSpec,
ImaScWaterQualityClientSpec, IneaPdfParserSpec, IneaRjWaterQualityClientSpec,
InemaBaWaterQualityClientSpec, InemaPdfParserSpec, SamplingPointCoordinatesSpec
cli/src/test/scala/marola
AppConfigSpec, E2ESpec, PipelineGoldenSpec, ReportFacilitiesSpec
agent ChatServerSpec
bench BenchmarkLedgerSpec
site BoardSpec, SiteBuilderSpec
The pipeline¶
Recommender finds the nearest named beaches
around the origin (six by default; each --site area sets its own), fetches the region's water
quality and the beaches' facilities once each, then one forecast per beach, and scores every hour
that falls on tomorrow in that beach's own timezone.
Hours rank by score, ties by distance from 10:00; bestPerBeachTomorrow keeps each beach's best
hour. scoreDays serves --site: today and tomorrow from one forecast per beach. The clock is
injected (today: ZoneId => LocalDate), the one clock read in the pipeline, so
PipelineGoldenSpec can pin "tomorrow" to its recorded fixtures.
A failing water or facilities call logs a warning and leaves every beach with "no data"; Overpass failing on all three mirrors, or Open-Meteo failing, fails the run. Where the origin comes from (flags, a Maps URL, env vars, IP geolocation) is in the CLI reference.
Two uses of AI, kept apart¶
marola runs AI in two places that answer to different rules. The split decides what may be wrong and how you would find out. A third, forecasting with time-series models, is designed but not built (Architecture).
The map is deterministic: no model writes any number a visitor sees.
SiteBuilder has no LLM reference. Every
value on a board comes from a measured source or a pure function over one: Open-Meteo for sea and
weather, Overpass for beaches, trails and facilities, the agency feeds and bulletins for water
quality, Tides for the tide curve. The score and the water sentence come from
Swimability.score, a function with no
effect type: the same inputs give the same board. A wrong number there is a bug with a stack trace,
and SwimabilitySpec can pin it.
The chat is a generative model, fenced in.
ChatServer answers through
config.llmClient, grounded on config.knowledgeStore (RAG over the corpus). The model can be
marola's own marola-sea (MIP-0025, marola-ml's
fine-tune): point
MAROLA_LOCAL_LLM_MODEL at it. Around it sit checks, not trust:
Revieweris a second, separate LLM pass that grades the summarizer's draft before a user sees it (LLM-as-judge).SwimConditionsMcpServerhands the deterministic half to agents as MCP tools, so an assistant gets measured data through a tool call rather than from the model's recollection.- The safety footer, and the corpus's "sourced or clearly labelled" rule.
When something looks wrong, the split says where to look: a bad map value is a parser or scoring bug; a bad chat answer is the model, retrieval or the prompt. It is also why the map needs no GPU and no token.
Beach distance¶
BeachFinder measures distance as the crow flies (Coordinates.distanceKm, haversine) to each
beach's OSM centre (out center). There is no routing backend, so a beach across a bay counts as
near. What that means for a user is in
Limitations.
The effect boundary¶
Kyo effects mark real I/O: every HTTP, file and model call is < Sync (or < Async in Main),
and the scoring, matching, tides, lore, board and report code is plain functions with no effect
type. The one deliberate escape is the MCP server, whose Java SDK takes synchronous callbacks; the
hidden effect that matters most is AppConfig.fromEnv. Both are in the
Effects map.