Import code review¶
A one-pass review of the whole repo at the initial import (45f5628), done before the first push to
GitHub. Every finding below was verified against the actual file, not inferred from a doc. Nothing
here was fixed as part of the review. This is the to-do list, ordered by how much each item
matters. Update or delete rows as they're addressed, same rule as every other doc under docs/.
What was verified live: just build and just test pass (12 unit tests, all green; see
D9 for why the docs say 16). The pre-commit hook's sbt Test/compile also passed on commit.
1. Code findings¶
C1. Secrets are printed to stdout — fixed (AppConfig.redacted)¶
cli/src/main/scala/marola/Main.scala:136 prints the entire AppConfig case class:
_ <- Console.printLine(s"config -> $config")
AppConfig carries telegramBotToken. Anyone who sets it and runs just run gets it echoed in
plain text, into terminal scrollback, CI logs, or wherever stdout goes. docs/1-Using-marola/RUN-LOCALLY.md:73
even shows this line as expected output. Either drop the line or print a redacted view (provider
choices and non-secret settings only).
C3. The DSPy compile step writes to a directory that no longer exists — fixed¶
dspy/compile_recommendation_prompt.py:334:
resources_dir = os.path.join(os.path.dirname(__file__), "..", "src", "main", "resources")
That resolves to <repo>/src/main/resources, a leftover from before the module hoist
(FUTURE-WORK.md §7.2). The artifacts the Scala side actually loads live in
core/src/main/resources/. Re-running the compile today does not update them. Fix: "..", "core",
"src", "main", "resources". dspy/README.md already names the correct path.
C6. Smaller correctness items¶
| Where | What | Suggested fix |
|---|---|---|
local/.../LocalFileSightingStore.scala:26 recentFor |
One malformed JSON line throws JsonParseException and fails the whole read |
Fixed — bad lines are skipped |
cli/.../SwimConditionsMcpServer.scala:40 numberArg |
s.toDouble throws NumberFormatException on non-numeric input, surfacing as an MCP error |
Fixed — toDoubleOption |
core/.../Json.scala:53 render for JNumber |
NaN/Infinity render as bare NaN/Infinity, which is not valid JSON |
Fixed — rendered as null |
cli/.../Main.scala parseOrigin |
If only one of --lat/--lon is given, it silently falls back to the default origin |
Fixed — resolveOrigin/warnHalfPair now warn and fall through to env vars / IP geolocation |
core/.../Recommender.scala:89 |
LocalDate.now(...) is a clock read inside the effect chain — harmless, but EFFECTS-MAP.md classes Recommender as pure control flow |
Note it in EFFECTS-MAP.md |
C7. BeachFinder missed every beach mapped as an OSM relation — fixed¶
Found after the review, from a live run near Praia do Campeche that returned only two beaches
within 20km. The Overpass query asked for node and way only; Campeche, Joaquina, Armação,
Matadeiro and ~40 other beaches around Florianópolis are multipolygon relations. Two further
problems compounded it: the element cap was limit * 4 (24) applied in Overpass's database order,
i.e. before marola sorts by distance, so the nearest beaches could be truncated arbitrarily; and
Http.postForm's fixed 15s timeout was below what relation-aware Overpass queries actually take
(~29s observed). All three are fixed in BeachFinder/Http.postForm; see ARCHITECTURE.md §9 for
the two residual limitations (centroid distance, Overpass slowness).
2. Documentation findings¶
D3. Broken markdown in ARCHITECTURE.md — fixed¶
docs/2-Building-marola/ARCHITECTURE.md:152 is a stray closing fence after the "Telemetry (§5f) wraps the
pipeline..." note. The mermaid block already closed at line 143, so line 152 opens a code block
that swallows the "Cross-cutting: rate limiting..." paragraph and §5's table on render. Delete it.
D4. Stale commands and paths from before the monorepo hoist¶
| File:line | Says | Should say |
|---|---|---|
docs/2-Building-marola/ARCHITECTURE.md:104-108, :227 |
just run marola -- ... |
Fixed with MIP-0001 |
docs/2-Building-marola/ARCHITECTURE.md:291 |
sbt marola/runMain marola.agent.SwimConditionsMcpServer |
Fixed with MIP-0001 |
cli/src/test/scala/marola/E2ESpec.scala:15 |
just e2e-marola |
just e2e |
cli/src/test/scala/marola/E2ESpec.scala:35 |
"the just run marola default path" |
just run |
docs/2-Building-marola/ARCHITECTURE.md:188 |
artifact at marola/src/main/resources/recommendation_prompt.json |
core/src/main/resources/... |
docs/4-Research-and-plans/FUTURE-WORK.md:211, core/src/main/scala/marola/llm/Reviewer.scala:20 |
"see dspy/review_prompt.json" |
core/src/main/resources/review_prompt.json |
D5. References to files that don't exist — fixed¶
build.sbt:14: "See flake.nix and Dockerfile — both pin 25." There is no Dockerfile.
D6. A method that doesn't exist is cited as the structured-output mechanism — fixed¶
docs/4-Research-and-plans/SKILLS.md:27 names CompiledPrompt.replay. The real method is
CompiledPrompt.buildMessages; the model reply is consumed by LlmClient.extractContent and
Reviewer.extractJsonObject.
D8. Contradictory status on the DSPy compile step — fixed¶
dspy/compile_recommendation_prompt.py:11 (module docstring) says "NOT RUN as part of writing
this" and describes the default as a paid API; dspy/README.md "Status" and main()'s own comment
say it was run, twice, against a local Ollama model, and the default is ollama_chat/llama3.2.
The docstring is stale.
D9. Numbers and ordering — fixed¶
docs/4-Research-and-plans/FUTURE-WORK.md:363: "all 16 tests pass". There are 12 unit tests (SwimabilitySpec) plus 2 E2E tests;just testreports 12.docs/4-Research-and-plans/FUTURE-WORK.md§7 runs 7.1 → 7.3 → 7.2.docs/4-Research-and-plans/SKILLS.md:37points at "ARCHITECTURE.md§5b's Status note" for the MCP JSON-RPC verification; that note is in §5c.
D10. Things the docs get right that are worth keeping¶
Not a finding, but so this file isn't only a list of gaps: the per-integration "verified live vs.
written-not-run" status notes in ARCHITECTURE.md §5 matched the code in every case checked; the
EFFECTS-MAP.md classification is accurate apart from the Recommender clock read (C6);
build.sbt's META-INF/services merge note and the Compile / run / mainClass pin are both real
and correct; .gitignore covers .idea/, .bsp/, .tmp/, and data/, and none of them leaked
into the initial commit.
C8. The "best hour" was always midnight — fixed¶
Found from a real run after merge: every beach's best hour printed as 00:00 and the LLM
rightly called it "not a good night for swimming". Swimability.score ignored isDaylight, so on
a flat day all 24 hours tied and the first one won. Fixed: a −60 "dark" deduction, and equal
scores now break ties toward 10:00 (Swimability.hourPreference): staffed lifeguard posts, best
light, calmest sea. Same run showed Tides.extrema reporting a 2cm wobble as a high/low pair;
turns now need ≥ 0.1m of range. Golden test asserts every recommended hour is daylight.
Regression mechanism added after the review¶
cli/src/test/scala/marola/PipelineGoldenSpec.scala replays recorded real responses (Overpass,
Open-Meteo, IMA: cli/src/test/resources/fixtures/) through the unchanged production code via
Http.withTransport, and core/.../llm/SummarizeFlowSpec.scala / knowledge/RagOfflineSpec.scala
script the LLM/embedder. That is the every-push regression check in ci.yml; the live E2E workflow
is manual, two-job, model-cached, and skips Ollama unless asked (marola-e2e.yml).
3. Environment notes from the review session¶
These aren't defects in marola, but they cost time and are easy to hit again:
- Inside
just jail-claude,.env.exampleand.ai-jailread as empty. ai-jail bind-mounts both read-only with no content (the.env.*mask).git statusshows them asAM,catandgit diffshow them blank. The index/HEAD copies are intact. Nevergit add -Afrom inside the jail; stage files by name. ghis unauthenticated inside the jail because~/.config/ghis not mapped (the recipe maps~/.claude,~/.claude.json, and~/.sshonly). SSH to GitHub works, sogit pushis fine, butgh repo createand any other API call need either--map ~/.config/ghadded to thejail-clauderecipe orGH_TOKENin the environment.