Skip to content

Repository files navigation

Atlas — Country Info Chat

CI-iOS CI-Modules

A multi-platform (iOS + CLI) chat app that answers questions about countries in free text. Built with the Essential Developer method — TDD, small always-green commits, modular design, and CI. One framework-free domain core, reused verbatim by two thin composition roots (an iOS app and a CLI).

Ask it anything, however you phrase it:

> whats the capitol of belgum
Brussels is the capital of Belgium.

> countries starting with New
Countries starting with “New”: New Caledonia, New Zealand

> iso code for viet nam
The ISO alpha-2 code for Vietnam is VN.

> show me brazils flag
The flag of Brazil is 🇧🇷

Runs with zero setup — no API key, no account, no signup.

One caveat if you run the iOS app: the simulator's iOS version must match your macOS version, or the on-device model is silently unavailable and the app falls back to its rule-based interpreter. Details in Simulator and macOS versions must match. The CLI has no such constraint.

Architecture

Atlas country chat — one core, two apps

Layered Swift packages under Modules/ (one package each), consumed by two roots:

Layer Package Role
Domain CountryFoundation Country, CountryQuery, Answer value types, the pure AnswerResolver, and the CountryLoader / QuestionInterpreter seams. Foundation-only — no UI or networking framework, and depends on nothing.
Infrastructure CountryAPI HTTPClientProtocol seam + URLSessionHTTPClient adapter, countries endpoint + mapper, RemoteCountryLoader.
Infrastructure CountryInterpreter Free text → CountryQuery. A deterministic rule-based interpreter, plus an on-device Apple Foundation Models interpreter, behind a fallback composite.
Orchestration CountryChat AnswerService (interpret → load → resolve) + caching and retry decorators.
Presentation CountryPresentation MVVM ChatViewModel emitting AsyncStream<ChatState> (Foundation-only).
Root (iOS) CountryChatApp SwiftUI ChatView + composition root.
Root (CLI) country-cli Console chat loop + composition root.

Every dependency points down to the framework-free domain core; only the two roots know concrete types. The core is compiled and tested on Linux in CI to prove it stays platform-agnostic. See docs/ for the light/dark diagrams.

Behaviour (BDD)

Story: A user wants quick answers about countries

Narrative #1 — Happy path
As a user
I want to ask about a country in my own words
So I get an answer without learning a command syntax

  Given the user has connectivity
   When they ask for a capital, an ISO alpha-2 code, a flag,
        or which countries start with some letters
   Then the app replies with the answer
    And it understands the question however it is phrased or misspelled

Narrative #2 — Unrecognised country
As a user
I want to be told when a country isn't recognised
So I can correct myself instead of trusting a wrong answer

  Given the user names something that is not a country
   When nothing matches closely enough
   Then the app says it doesn't know that country
    And it does not guess

Narrative #3 — Network failure
As a user
I want to retry when the lookup fails
So a dropped connection doesn't end the conversation

  Given the country list has not loaded yet
   When the request fails
   Then the app shows an error with a retry action
    And retrying re-runs the same question
    And the error clears as soon as a request succeeds

How a question becomes an answer

free text
   │  QuestionInterpreter          (on-device model, falling back to rules)
   ▼
CountryQuery  .capital | .countriesStartingWith | .isoCode | .flag
   │  AnswerService                (loads every country once, then caches)
   ▼
AnswerResolver                     (pure: normalize, fuzzy-match, prefix-filter)
   │  AnswerFormatter              (localized)
   ▼
chat reply

Interpretation is the only non-deterministic step, and it sits behind a single seam. Everything downstream — including the misspelling tolerance that turns belgum into Belgium — is deterministic and fully unit-tested.

Data source

mledoze/countries (MIT) served over jsDelivr, pinned to a release tag:

https://cdn.jsdelivr.net/gh/mledoze/countries@v4.1.1/countries.json

Fetched once per session and cached. No key, no account, no signup.

Why not restcountries.com? The brief specifies it, but as of July 2026 it can no longer satisfy a zero-setup public repo:

  • restcountries.com/v3.1/all returns 301 → "This API version has been deprecated"
  • v5 requires Authorization: Bearer <key> and a sign-up; its public demo key (rc_live_demo) returns a single canned sample object for every endpoint

Requiring a reviewer to obtain their own key — or committing a secret to a public repo — was the worse trade. mledoze/countries is the upstream dataset restcountries is itself built from, so the field names are the same ones the brief names: name, capital, cca2, flag, altSpellings. Flag images aren't in the dataset, so they're derived deterministically from the ISO code (https://flagcdn.com/w320/be.png) — the same CDN restcountries used.

Pinning to @v4.1.1 rather than @master keeps the live end-to-end tests reproducible.

Getting started

Important

Pick a simulator whose iOS version matches your macOS version. An iOS 26.3 simulator on macOS 26.3; an iOS 26.1 simulator on macOS 26.1. A mismatch silently disables the on-device model — see Simulator and macOS versions must match below. The CLI is unaffected.

# CLI — the quickest way to see it work
swift run --package-path country-cli

# Per-module tests (what CI runs)
swift test --package-path Modules/CountryFoundation

# iOS app
open Atlas.xcworkspace

The CLI is an interactive prompt. Type a question and press return; retry re-runs the last question after a failure, exit quits.

The iOS project is generated from CountryChatApp/project.yml with XcodeGen. The committed .xcodeproj is the source of truth for reviewers; regenerate with cd CountryChatApp && xcodegen generate after editing the spec.

Simulator and macOS versions must match

The on-device interpreter needs Apple's FoundationModels, which requires:

  • iOS 26 / macOS 26 or later, on Apple silicon
  • Apple Intelligence enabled on the host Mac (System Settings → Apple Intelligence & Siri)
  • the generative model assets downloaded — while they download, SystemLanguageModel.availability reports .unavailable(.modelNotReady)

The Simulator has no assets of its own; it borrows the host's. Observed on Xcode 26.3 / macOS 26.3.1:

Host Simulator On-device model
macOS 26.3.1 iOS 26.3 ✅ loads com.apple.fm.language.instruct_3b
macOS 26.3.1 iOS 26.1 Model Catalog error … there are no underlying assets

We've only verified this pairing, but the failure mode is consistent with the Simulator resolving assets against the host, so treat version parity as the safe default.

The trap is that SystemLanguageModel.isAvailable returns true on the mismatched simulator — it reports .available rather than one of the honest reasons (.deviceNotEligible, .appleIntelligenceNotEnabled, .modelNotReady) — so the app tries the model and only fails at generation time. The console then fills with Prewarm failed and ModelManagerError for every question.

The app still answers correctly — the fallback composite drops to the rule-based interpreter, which is exactly what it exists for. But you are testing the fallback, not the model. If you want to exercise the on-device path, match the versions or run the CLI, which runs natively on the host and is never affected.

Unrelated simulator noise you can ignore: IOHIDLib (HID plugin architecture) and CHHapticPattern / hapticpatternlibrary.plist (the keyboard trying to play typing haptics on hardware that doesn't exist).

CI

  • .github/workflows/test-modules.ymlswift test for every module in a matrix, plus the CLI, on Linux (swift:6.1 container). Deterministic, no network.
  • .github/workflows/test-ios.ymlxcodebuild test for the iOS app on a simulator with the Thread Sanitizer enabled.
  • Live end-to-end tests hit the real countries endpoint and live in their own package (CountryAPIEndToEndTests), run on demand so a third-party outage never blocks a PR.

License

MIT.

The country data comes from mledoze/countries, which is also MIT licensed.

About

AI chat app for iOS and the command line that answers free-text questions about countries. One framework-free domain core, two thin composition roots, on-device model with a rule-based fallback, TDD and CI.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages