@startuml
skinparam style strictuml
skinparam defaultFontName "DM Sans"
skinparam defaultFontSize 14
skinparam titleFontName "Volkhov"
skinparam titleFontSize 20
skinparam backgroundColor #FAFCF9
skinparam defaultFontColor #28342A
skinparam titleFontColor #28342A
skinparam ArrowColor #628A5B
skinparam NoteBackgroundColor #EAF0E8
skinparam NoteBorderColor #547461
skinparam ActivityBackgroundColor #FAFCF9
skinparam ActivityBorderColor #547461
skinparam ActivityDiamondBackgroundColor #FAFCF9
skinparam ActivityDiamondBorderColor #628A5B
skinparam ActivityBarColor #628A5B
skinparam SwimlaneBorderColor #547461
skinparam SwimlaneBorderThickness 0.3

title The Biergarten Data Pipeline (Current — Synchronous Data Path)

|#F2F6F0|main.cc|
start
:Create BoundedChannel<LogEntry> log_channel;
:Spawn log dispatcher thread\n(LogDispatcher::Run());
note right
  The only concurrency in the current pipeline:
  a dedicated thread drains log_channel and
  forwards entries to spdlog for the entire run.
  Joined during shutdown after log_channel closes.
end note
:ParseArguments(argc, argv);
if (Are arguments valid?) then (no)
  :Log error / usage info;
  stop
else (yes)
endif

:Init OpenSSL global state & LlamaBackendState;
:di::make_injector(...);
note right
  Binds ILogger, IEnrichmentService, DataGenerator,
  IExportService, IPromptFormatter, WebClient based
  on --mocked vs. model-backed mode.
end note
:injector.create<std::unique_ptr<BiergartenPipelineOrchestrator>>();
:BiergartenPipelineOrchestrator::Run();
note right
  Run() itself is synchronous — sampling, enrichment,
  generation, and export all happen on this thread.
end note

|#EAF0E8|BiergartenPipelineOrchestrator|
:Initialize SQLite export;

|#E0EAE0|SqliteExportService|
:GetUtcTimestamp() from SystemDateTimeProvider;
:Initialize();
note right
  Builds a fresh biergarten_seed_<UTC datetime>.sqlite filename
  Appends a numeric suffix if the timestamp already exists
  Opens DB Connection
  Executes Schema DDL
  Begins Transaction
end note

|#EAF0E8|BiergartenPipelineOrchestrator|
:QueryCitiesWithCountries();

|#E2EBDC|JsonLoader|
:JsonLoader::LoadLocations("locations.json");
:std::ranges::sample(all_locations, --location-count);
note right
  --location-count defaults to 10.
end note

|#EAF0E8|BiergartenPipelineOrchestrator|
while (For each sampled Location?) is (Remaining cities)
  |#DCE8D8|IEnrichmentService|
  :GetLocationContext(loc);
  note right
    WikipediaEnrichmentService fetches a generic
    "brewing" extract and a "beer in {country}" extract
    (not a city/region-specific page). MockEnrichmentService
    (--mocked) returns an empty string instead.
  end note
  |#EAF0E8|BiergartenPipelineOrchestrator|
  if (Lookup failed?) then (yes)
    :Log warning, skip city;
  else (no)
    :Store EnrichedCity{Location, region_context};
  endif
endwhile (Done)

|#EAF0E8|BiergartenPipelineOrchestrator|
:GenerateUsers(enriched_cities);

|#E2EBDC|JsonLoader|
:LoadPersonas("personas.json");
:LoadForenamesByCountry("forenames-by-country.json");
:LoadSurnamesByCountry("surnames-by-country.json");
note right
  Each call is cached after its first parse.
  MockCuratedDataService (--mocked) returns a
  fixed in-memory dataset instead and skips
  these three JSON files entirely.
end note

|#EAF0E8|BiergartenPipelineOrchestrator|
while (For each EnrichedCity?) is (Remaining cities)
  :SampleName(forenames_by_country,\n  surnames_by_country, location.iso3166_1);
  if (Names available for country?) then (no)
    :Log warning, skip city;
  else (yes)
    :Sample UserPersona (uniform random);

    |#E5EDE1|DataGenerator|
    if (Generator Mode) then (MockGenerator)
      :DeterministicHash & Format;
    else (LlamaGenerator)
      :prompt_directory_->Load("USER_GENERATION");
      note right
        Resolves to USER_GENERATION.md inside --prompt-dir.
      end note
      repeat
        :Infer(system_prompt, user_prompt, max_tokens, kUserJsonGrammar);
        :ValidateUserJson(raw, user);
        if (Is JSON Valid?) then (yes)
          break
        else (no)
          :Attempt++;
          :Append validation error to retry prompt;
        endif
      repeat while (Attempt < 3?) is (yes)
    endif

    |#EAF0E8|BiergartenPipelineOrchestrator|
    if (Generation successful?) then (yes)
      :BuildEmail(name, used_email_local_parts);
      :GenerateDateOfBirth(rng);
      note right
        email and date_of_birth are generated
        programmatically, never LLM-authored.
      end note

      |#E0EAE0|SqliteExportService|
      :ProcessRecord(UserRecord);
      if (Location in cache?) then (yes)
        :Reuse location_id;
      else (no)
        :Insert Location & Cache ID;
      endif
      :Insert User (FK: location_id);

      if (Exception caught during insert?) then (yes)
        |#EAF0E8|BiergartenPipelineOrchestrator|
        :Log warning "SQLite export failed";
      else (no)
      endif
    else (no)
      :Log warning "Generation failed, skipping...";
    endif
  endif
endwhile (Done)

|#EAF0E8|BiergartenPipelineOrchestrator|
:GenerateBreweries(enriched_cities);

|#E5EDE1|DataGenerator|
while (For each EnrichedCity?) is (Remaining cities)
  if (Generator Mode) then (MockGenerator)
    :DeterministicHash & Format;
  else (LlamaGenerator)
    :PrepareRegionContext;
    :prompt_directory_->Load("BREWERY_GENERATION");
    note right
      Resolves to BREWERY_GENERATION.md inside --prompt-dir.
    end note
    repeat
      :Infer(system_prompt, user_prompt, max_tokens, kBreweryJsonGrammar);
      :ValidateBreweryJson(raw, brewery);
      if (Is JSON Valid?) then (yes)
        break
      else (no)
        :Attempt++;
        :Append validation error to retry prompt;
      endif
    repeat while (Attempt < 3?) is (yes)
    note right
      max_tokens is fixed across retries —
      it is not raised on truncation.
    end note
  endif

  |#EAF0E8|BiergartenPipelineOrchestrator|
  if (Generation successful?) then (yes)
    |#E0EAE0|SqliteExportService|
    :ProcessRecord(GeneratedBrewery);
    if (Location in cache?) then (yes)
      :Reuse location_id;
    else (no)
      :Insert Location & Cache ID;
    endif
    :Insert Brewery (FK: location_id);

    if (Exception caught during insert?) then (yes)
      |#EAF0E8|BiergartenPipelineOrchestrator|
      :Log warning "SQLite export failed";
      note right
        Data loss is prevented per-record.
        The pipeline continues running.
      end note
    else (no)
    endif
  else (no)
    :Log warning "Generation failed, skipping...";
  endif
  |#E5EDE1|DataGenerator|
endwhile (Done)

|#E0EAE0|SqliteExportService|
:Finalize();
note right
  Commits Transaction
  Closes Database Connection
end note

|#F2F6F0|main.cc|
:Close log_channel;
:Join log dispatcher thread;
:Return 0;
stop

@enduml
