1 Commits

Author SHA1 Message Date
Aaron Po
532cb234fa Update title of architecture diagram 2026-04-20 10:50:28 -04:00
6 changed files with 314 additions and 260 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,5 @@
@startuml future_possible_activity @startuml
skinparam style strictuml
skinparam defaultFontName "DM Sans" skinparam defaultFontName "DM Sans"
skinparam defaultFontSize 13 skinparam defaultFontSize 13
skinparam titleFontName "Volkhov" skinparam titleFontName "Volkhov"
@@ -7,186 +8,270 @@ skinparam backgroundColor #FAFCF9
skinparam defaultFontColor #28342A skinparam defaultFontColor #28342A
skinparam titleFontColor #28342A skinparam titleFontColor #28342A
skinparam ArrowColor #628A5B skinparam ArrowColor #628A5B
skinparam ActivityBackgroundColor #EAF0E8 skinparam SequenceLifeLineBorderColor #547461
skinparam ActivityBorderColor #547461 skinparam SequenceParticipantBorderColor #547461
skinparam ActivityDiamondBackgroundColor #DCE8D8 skinparam SequenceParticipantBackgroundColor #EAF0E8
skinparam ActivityDiamondBorderColor #547461 skinparam SequenceBoxBorderColor #547461
skinparam NoteBackgroundColor #EAF0E8 skinparam NoteBackgroundColor #EAF0E8
skinparam NoteBorderColor #547461 skinparam NoteBorderColor #547461
skinparam SequenceDividerBackgroundColor #EAF0E8
skinparam SequenceDividerBorderColor #547461
title The Biergarten Data Pipeline — Activity Diagram title The Biergarten Data Pipeline — Sequence Diagram v4 (Unified Orchestrator)
|Main| participant "main.cc" as main #F2F6F0
start participant "Orchestrator" as orch #EAF0E8
:ParseArguments(argc, argv); participant "Thread U1\nUserProducer" as u1 #DCE8D8
if (Invalid args?) then (yes) participant "Thread U2\nUserExportConsumer" as u2 #E0EAE0
:spdlog::error; participant "Thread B1\nEnrichmentProducer" as b1 #DCE8D8
stop participant "Thread B2\nBreweryGenerationConsumer" as b2 #E5EDE1
else (no) participant "Thread B3\nBreweryExportConsumer" as b3 #E0EAE0
endif participant "Thread R1\nBeerGenerationProducer" as r1 #DCE8D8
:Init CurlGlobalState & LlamaBackendState; participant "Thread R2\nBeerExportConsumer" as r2 #E0EAE0
:Build DI injector; participant "Thread C1\nCheckinGenerationProducer" as c1 #DCE8D8
participant "Thread C2\nCheckinExportConsumer" as c2 #E0EAE0
participant "Thread G1\nRatingGenerationProducer" as g1 #DCE8D8
participant "Thread G2\nRatingExportConsumer" as g2 #E0EAE0
:JsonLoader::LoadLocations("locations.json"); ' ─────────────────────────────────────────────
:JsonLoader::LoadBeerStyles("beer-styles.json"); ' STARTUP
' ─────────────────────────────────────────────
main -> main : ParseArguments(argc, argv)
alt Invalid args
main -> main : spdlog::error; stop
end
:EnrichmentService::PreWarmBeerStyleCache(beer_styles); main -> main : Init CurlGlobalState & LlamaBackendState
note right main -> main : Build DI injector
**NEW**: Beer styles do not need location context. note right of main
Wikipedia summaries for the entire palette are All dependencies bound with unique_ptr.
fetched and cached globally at startup. LlamaConfig or RestConfig injected
instead of ApplicationOptions.
end note end note
:Initialize SqliteExportService; main -> orch : exporter->Initialize()
note right note right of orch
Opens SQLite connection. Opens SQLite connection.
Begins a single transaction Creates schema for all five fixture types
covering all five fixture types. in one DDL pass. Begins IMMEDIATE TRANSACTION.
end note
:BiergartenPipelineOrchestrator::Run();
' ═══════════════════════════════════════════
' PHASE 0 — USER GENERATION
' ═══════════════════════════════════════════
|Orchestrator|
:RunUserPhase(sampled_locations);
:Create BoundedChannels\n(user_llm_ch, user_exp_ch);
fork
|Orchestrator|
:Loop: Send Locations → user_llm_ch;
:Close user_llm_ch;
fork again
|LLM Worker|
while (user_llm_ch has items?) is (yes)
:Receive Location;
:GenerateUser(location)\nvia DataGenerator;
:Send GeneratedUser → user_exp_ch;
endwhile (no)
:Close user_exp_ch;
fork again
|SQLite Worker|
while (user_exp_ch has items?) is (yes)
:Receive GeneratedUser;
:ProcessUser(user) → sqlite3_int64;
:Append → user_pool_;
endwhile (no)
end fork
|Orchestrator|
:Join LLM Worker, SQLite Worker;
' ═══════════════════════════════════════════
' PHASE 1 — BREWERY & BEER GENERATION
' Combined into a single dependent unit of work.
' ═══════════════════════════════════════════
:RunBreweryAndBeerPhase(sampled_locations);
:Create BoundedChannels\n(loc_ch, llm_ch, exp_ch);
fork
|Orchestrator|
:Loop: Send Locations → loc_ch;
:Close loc_ch;
fork again
|Enrichment Workers (xN)|
while (loc_ch has items?) is (yes)
:Receive Location;
:GetLocationContext(location,\nBreweryContextStrategy);
:Send EnrichedCity → llm_ch;
endwhile (no)
|Orchestrator|
:Join Enrichment Workers;
:Close llm_ch;
fork again
|LLM Worker|
while (llm_ch has items?) is (yes)
:Receive EnrichedCity;
:GenerateBrewery(location, context)\nvia DataGenerator;
:IBeerSelectionStrategy::SelectStyles(\n brewery, beer_style_palette_);
while (For each selected BeerStyle?) is (remaining)
:GetStyleContextFromCache(style);
note right
Guaranteed cache hit from startup.
end note
:GenerateBeer(brewery, style_context)\nvia DataGenerator;
:Attach GeneratedBeer to Brewery bundle;
endwhile (done)
:Send BreweryWithBeers Bundle → exp_ch;
note right
The next generation of a brewery is
entirely dependent on the current
brewery and its beers completing.
end note
endwhile (no)
:Close exp_ch;
fork again
|SQLite Worker|
while (exp_ch has items?) is (yes)
:Receive BreweryWithBeers Bundle;
:ProcessBrewery(brewery) → brewery_id;
:Append → brewery_pool_;
while (For each beer in bundle?) is (remaining)
:Set beer.brewery_id = brewery_id;
:ProcessBeer(beer) → sqlite3_int64;
:Append → beer_pool_;
endwhile (done)
endwhile (no)
end fork
|Orchestrator|
:Join LLM Worker, SQLite Worker;
note right
Both brewery_pool_ and beer_pool_
are now completely populated.
end note end note
' ═══════════════════════════════════════════ main -> orch : JsonLoader::LoadLocations("locations.json")
' PHASE 2 — CHECKIN GENERATION main -> orch : ISamplingStrategy::Sample(all_locations)
' Sequential now that Breweries/Beers are done. main -> orch : BiergartenPipelineOrchestrator::Run()
' ═══════════════════════════════════════════
:RunCheckinPhase();
:ICheckinDistributionStrategy::\nAssignActivityWeights(user_pool_);
while (For each GeneratedUser in user_pool_?) is (remaining) ' ─────────────────────────────────────────────
:CheckinsForUser(user, brewery_pool_.size()); ' PHASE 1 — USERS
while (For each checkin index?) is (remaining) ' ─────────────────────────────────────────────
:TimestampFor(user, index); == Phase 1 — Users (no FK dependencies) ==
:Select brewery from brewery_pool_;
:GenerateCheckin(user, brewery, timestamp)\nvia DataGenerator;
:ProcessCheckin(checkin) → sqlite3_int64;
:Append → checkin_pool_;
endwhile (done)
endwhile (done)
' ═══════════════════════════════════════════ orch -> u1 : spawn
' PHASE 3 — RATING GENERATION orch -> u2 : spawn
' ═══════════════════════════════════════════
:RunRatingPhase();
while (For each GeneratedCheckin in checkin_pool_?) is (remaining) loop For each Location
:Match brewery_id → select beer\nfrom beer_pool_ (same brewery_id); u1 -> u1 : generator->GenerateUser(location)
if (Beer exists for brewery?) then (yes) u1 -> u2 : user_channel_.Send(GeneratedUser)
:GenerateRating(user, beer, checkin_id)\nvia DataGenerator; end
:ProcessRating(rating); u1 -> u2 : user_channel_.Close()
else (no)
:Skip — brewery has no beers;
endif
endwhile (done)
' ═══════════════════════════════════════════ loop user_channel_.Receive()
u2 -> u2 : exporter->ProcessUser(user) : sqlite3_int64
note right of u2
Returns committed row ID.
Stored on GeneratedUser.user_id.
end note
u2 -> orch : Append to user_pool_
end
orch -> orch : join(U1, U2)
note right of orch
── BARRIER 1 ──
user_pool_ is now fully committed.
Phase 2 begins.
end note
' ─────────────────────────────────────────────
' PHASE 2 — BREWERIES
' ─────────────────────────────────────────────
== Phase 2 — Breweries (depends on locations only) ==
orch -> b1 : spawn
orch -> b2 : spawn
orch -> b3 : spawn
loop For each Location
b1 -> b1 : BreweryContextStrategy::QueriesFor(location)
b1 -> b1 : WikipediaService::GetLocationContext(location,\nbrewery_context_strategy_)
alt failure
b1 -> b1 : LocationContext{ Absent }
else truncated
b1 -> b1 : LocationContext{ Partial }
else success
b1 -> b1 : LocationContext{ Full }
end
b1 -> b2 : enrichment_channel_.Send(EnrichedCity)
note right of b1
Blocks if channel full.
Back-pressure against GPU consumer.
end note
end
b1 -> b2 : enrichment_channel_.Close()
loop enrichment_channel_.Receive()
alt context.completeness == Absent
b2 -> b2 : spdlog::warn — proceeding with minimal prompt
end
b2 -> b2 : generator->GenerateBrewery(location, context)
b2 -> b3 : brewery_channel_.Send(GeneratedBrewery)
end
b2 -> b3 : brewery_channel_.Close()
loop brewery_channel_.Receive()
b3 -> b3 : exporter->ProcessBrewery(brewery) : sqlite3_int64
note right of b3
Row ID stored on GeneratedBrewery.brewery_id.
No brewery_cache_ needed — orchestrator
threads the ID forward directly.
end note
b3 -> orch : Append to brewery_pool_
end
orch -> orch : join(B1, B2, B3)
note right of orch
── BARRIER 2 ──
brewery_pool_ is now fully committed
with live brewery_id values.
Phase 3 begins.
end note
' ─────────────────────────────────────────────
' PHASE 3 — BEERS
' ─────────────────────────────────────────────
== Phase 3 — Beers (depends on brewery_pool_) ==
orch -> r1 : spawn
orch -> r2 : spawn
loop For each GeneratedBrewery in brewery_pool_
r1 -> r1 : BeerContextStrategy::QueriesFor(location)
r1 -> r1 : WikipediaService::GetLocationContext(location,\nbeer_context_strategy_)
r1 -> r1 : generator->GenerateBeer(brewery.brewery_id,\nlocation, context)
r1 -> r2 : beer_channel_.Send(GeneratedBeer)
end
r1 -> r2 : beer_channel_.Close()
loop beer_channel_.Receive()
r2 -> r2 : exporter->ProcessBeer(beer) : sqlite3_int64
note right of r2
Row ID stored on GeneratedBeer.beer_id.
end note
r2 -> orch : Append to beer_pool_
end
orch -> orch : join(R1, R2)
note right of orch
── BARRIER 3 ──
beer_pool_ is fully committed.
All three upstream pools ready.
end note
' ─────────────────────────────────────────────
' CHECKIN WEIGHT ASSIGNMENT
' ─────────────────────────────────────────────
== Checkin Weight Assignment ==
orch -> orch : ICheckinDistributionStrategy::\nAssignActivityWeights(user_pool_)
note right of orch
J-curve weights written onto
GeneratedUser.user.activity_weight.
Small cohort gets high weight;
long tail gets low weight.
Requires the full pool — this is why
users were committed first.
end note
' ─────────────────────────────────────────────
' PHASE 4 — CHECKINS
' ─────────────────────────────────────────────
== Phase 4 — Check-ins (depends on user_pool_ + brewery_pool_) ==
orch -> c1 : spawn
orch -> c2 : spawn
loop For each GeneratedUser in user_pool_
c1 -> c1 : strategy->CheckinsForUser(user,\nbrewery_pool_.size())
loop For each checkin index
c1 -> c1 : strategy->TimestampFor(user, index)
note right of c1
Bursty weekend / evening
distribution applied here.
end note
c1 -> c1 : Select brewery from brewery_pool_\n(weighted random by activity_weight)
c1 -> c1 : generator->GenerateCheckin(user, brewery, timestamp)
c1 -> c2 : checkin_channel_.Send(GeneratedCheckin)
end
end
c1 -> c2 : checkin_channel_.Close()
loop checkin_channel_.Receive()
c2 -> c2 : exporter->ProcessCheckin(checkin) : sqlite3_int64
note right of c2
Row ID stored on GeneratedCheckin.checkin_id.
end note
c2 -> orch : Append to checkin_pool_
end
orch -> orch : join(C1, C2)
note right of orch
── BARRIER 4 ──
checkin_pool_ is fully committed.
All FK dependencies for ratings satisfied.
Phase 5 begins.
end note
' ─────────────────────────────────────────────
' PHASE 5 — RATINGS
' ─────────────────────────────────────────────
== Phase 5 — Ratings (depends on user_pool_ + beer_pool_ + checkin_pool_) ==
orch -> g1 : spawn
orch -> g2 : spawn
loop For each GeneratedCheckin in checkin_pool_
g1 -> g1 : Resolve GeneratedUser from user_pool_\n(match user_id)
g1 -> g1 : Resolve GeneratedBeer from beer_pool_\n(match brewery_id, select one)
alt Beer found for this brewery
g1 -> g1 : generator->GenerateRating(user, beer,\ncheckin.checkin_id)
note right of g1
Strong positive skew applied
inside GenerateRating.
end note
g1 -> g2 : rating_channel_.Send(GeneratedRating)
else No beer found
g1 -> g1 : spdlog::warn — no beer for brewery,\nskipping rating
end
end
g1 -> g2 : rating_channel_.Close()
loop rating_channel_.Receive()
g2 -> g2 : exporter->ProcessRating(rating)
end
orch -> orch : join(G1, G2)
' ─────────────────────────────────────────────
' TEARDOWN ' TEARDOWN
' ═══════════════════════════════════════════ ' ─────────────────────────────────────────────
|Main| == Teardown ==
:Finalize SqliteExportService;
note right orch -> main : return
COMMIT covers all five fixture types. main -> main : exporter->Finalize()
note right of main
Single COMMIT covers all five fixture types:
users, breweries, beers, checkins, ratings.
All-or-nothing consistency per run.
end note end note
:spdlog::info "Pipeline complete in X ms"; main -> main : spdlog::info "Pipeline complete in X ms"
stop main -> main : return 0
@enduml @enduml

View File

@@ -1,4 +1,4 @@
@startuml future_possible_architecture @startuml
skinparam style strictuml skinparam style strictuml
skinparam defaultFontName "DM Sans" skinparam defaultFontName "DM Sans"
skinparam defaultFontSize 14 skinparam defaultFontSize 14
@@ -29,7 +29,7 @@ skinparam package {
FontColor #28342A FontColor #28342A
} }
title The Biergarten Data Pipeline — Architecture title The Biergarten Data Pipeline — Architecture (Unified Orchestrator)
' ───────────────────────────────────────────── ' ─────────────────────────────────────────────
' DOMAIN: VALUE OBJECTS ' DOMAIN: VALUE OBJECTS
@@ -63,24 +63,6 @@ package "Domain: Value Objects & Contracts" {
+ context : LocationContext + context : LocationContext
} }
class BeerStyle {
+ name : std::string
+ description : std::string
+ min_abv : float
+ max_abv : float
+ min_ibu : int
+ max_ibu : int
}
note right of BeerStyle
Loaded once at startup from
beer-styles.json via JsonLoader.
Passed as std::span<const BeerStyle>
to IBeerSelectionStrategy.
Generator receives the selected
style as a parameter — it never
reads the palette directly.
end note
class BreweryResult { class BreweryResult {
+ name_en : std::string + name_en : std::string
+ description_en : std::string + description_en : std::string
@@ -143,11 +125,6 @@ package "Domain: Value Objects & Contracts" {
+ user : UserResult + user : UserResult
+ generated_at : std::string + generated_at : std::string
} }
note right of GeneratedUser
user_id populated after SQLite
insert. Live FK carried in pool
for checkin and rating references.
end note
class GeneratedCheckin { class GeneratedCheckin {
+ checkin_id : sqlite3_int64 + checkin_id : sqlite3_int64
@@ -196,47 +173,27 @@ package "Domain Policy" {
+ Sample(locations : const std::vector<Location>&) : std::vector<Location> + Sample(locations : const std::vector<Location>&) : std::vector<Location>
} }
interface IBeerSelectionStrategy <<interface>> {
+ SelectStyles(brewery : const GeneratedBrewery&,\n palette : std::span<const BeerStyle>) : std::vector<BeerStyle>
}
note right of IBeerSelectionStrategy
Decides how many beers a brewery
gets and which styles are selected.
Count distribution and style
deduplication logic live here,
not in the orchestrator or generator.
end note
class RandomBeerSelectionStrategy {
- rng_ : std::mt19937
- min_beers_ : size_t
- max_beers_ : size_t
+ SelectStyles(brewery : const GeneratedBrewery&,\n palette : std::span<const BeerStyle>) : std::vector<BeerStyle>
}
note right of RandomBeerSelectionStrategy
Draws a random count in [min, max].
Samples without replacement from
palette to avoid duplicate styles
per brewery.
end note
interface ICheckinDistributionStrategy <<interface>> { interface ICheckinDistributionStrategy <<interface>> {
+ AssignActivityWeights(users : std::vector<GeneratedUser>&) : void + AssignActivityWeights(users : std::vector<GeneratedUser>&) : void
+ CheckinsForUser(user : const GeneratedUser&,\n brewery_count : size_t) : size_t + CheckinsForUser(user : const GeneratedUser&, brewery_count : size_t) : size_t
+ TimestampFor(user : const GeneratedUser&,\n index : size_t) : std::string + TimestampFor(user : const GeneratedUser&, index : size_t) : std::string
} }
note right of ICheckinDistributionStrategy note right of ICheckinDistributionStrategy
Injected into the orchestrator.
Owns all statistical policy: Owns all statistical policy:
J-curve weight assignment, J-curve weight assignment,
bursty weekend timestamps, bursty weekend timestamps,
per-user checkin volume. per-user checkin volume.
No mediator required to hold this —
the orchestrator calls it directly
before the checkin phase opens.
end note end note
class JCurveCheckinStrategy { class JCurveCheckinStrategy {
- rng_ : std::mt19937 - rng_ : std::mt19937
+ AssignActivityWeights(users : std::vector<GeneratedUser>&) : void + AssignActivityWeights(users : std::vector<GeneratedUser>&) : void
+ CheckinsForUser(user : const GeneratedUser&,\n brewery_count : size_t) : size_t + CheckinsForUser(user : const GeneratedUser&, brewery_count : size_t) : size_t
+ TimestampFor(user : const GeneratedUser&,\n index : size_t) : std::string + TimestampFor(user : const GeneratedUser&, index : size_t) : std::string
} }
} }
@@ -247,21 +204,15 @@ package "Domain Policy" {
package "Infrastructure: Enrichment" { package "Infrastructure: Enrichment" {
interface IEnrichmentService <<interface>> { interface IEnrichmentService <<interface>> {
+ GetLocationContext(loc : const Location&,\n strategy : const IContextStrategy&) : LocationContext + GetLocationContext(loc : const Location&, strategy : const IContextStrategy&) : LocationContext
} }
class WikipediaService { class WikipediaService {
- client_ : std::unique_ptr<WebClient> - client_ : std::unique_ptr<WebClient>
- extract_cache_ : std::unordered_map<std::string, std::string> - extract_cache_ : std::unordered_map<std::string, std::string>
+ GetLocationContext(loc : const Location&,\n strategy : const IContextStrategy&) : LocationContext + GetLocationContext(loc : const Location&, strategy : const IContextStrategy&) : LocationContext
- FetchExtract(query : std::string_view) : std::string - FetchExtract(query : std::string_view) : std::string
} }
note right of WikipediaService
extract_cache_ keyed by query string.
Beer pass gets near-100% cache hits
since locations were already fetched
during the brewery pass.
end note
interface WebClient <<interface>> { interface WebClient <<interface>> {
+ Get(url : const std::string&) : std::string + Get(url : const std::string&) : std::string
@@ -281,18 +232,12 @@ package "Infrastructure: Enrichment" {
package "Infrastructure: Generation" { package "Infrastructure: Generation" {
interface DataGenerator <<interface>> { interface DataGenerator <<interface>> {
+ GenerateBrewery(location : const Location&,\n context : const LocationContext&) : BreweryResult + GenerateBrewery(location : const Location&, context : const LocationContext&) : BreweryResult
+ GenerateBeer(brewery_id : sqlite3_int64,\n location : const Location&,\n context : const LocationContext&,\n style : const BeerStyle&) : BeerResult + GenerateBeer(brewery_id : sqlite3_int64, location : const Location&, context : const LocationContext&) : BeerResult
+ GenerateUser(location : const Location&) : UserResult + GenerateUser(location : const Location&) : UserResult
+ GenerateCheckin(user : const GeneratedUser&,\n brewery : const GeneratedBrewery&,\n timestamp : const std::string&) : CheckinResult + GenerateCheckin(user : const GeneratedUser&, brewery : const GeneratedBrewery&, timestamp : const std::string&) : CheckinResult
+ GenerateRating(user : const GeneratedUser&,\n beer : const GeneratedBeer&,\n checkin_id : sqlite3_int64) : RatingResult + GenerateRating(user : const GeneratedUser&, beer : const GeneratedBeer&, checkin_id : sqlite3_int64) : RatingResult
} }
note right of DataGenerator
GenerateBeer receives BeerStyle
as a parameter. Style selection
and count decisions live in
IBeerSelectionStrategy, not here.
end note
class MockGenerator { class MockGenerator {
+ GenerateBrewery(...) : BreweryResult + GenerateBrewery(...) : BreweryResult
@@ -315,12 +260,26 @@ package "Infrastructure: Generation" {
+ GenerateCheckin(...) : CheckinResult + GenerateCheckin(...) : CheckinResult
+ GenerateRating(...) : RatingResult + GenerateRating(...) : RatingResult
- Load(config : const LlamaConfig&) : void - Load(config : const LlamaConfig&) : void
- Infer(system_prompt, user_prompt,\n max_tokens, grammar) : std::string - Infer(system_prompt, user_prompt, max_tokens, grammar) : std::string
- ValidateModelArchitecture() : void - ValidateModelArchitecture() : void
} }
class RestGenerator {
- config_ : RestConfig
+ GenerateBrewery(...) : BreweryResult
+ GenerateBeer(...) : BeerResult
+ GenerateUser(...) : UserResult
+ GenerateCheckin(...) : CheckinResult
+ GenerateRating(...) : RatingResult
}
note right of RestGenerator
Future REST-backed implementation.
Slots in at the DI root with zero
changes to orchestration logic.
end note
interface IPromptFormatter <<interface>> { interface IPromptFormatter <<interface>> {
+ Format(system_prompt : std::string_view,\n user_prompt : std::string_view) : std::string + Format(system_prompt : std::string_view, user_prompt : std::string_view) : std::string
+ ExpectedArchitecture() : std::string_view + ExpectedArchitecture() : std::string_view
} }
@@ -338,6 +297,12 @@ package "Infrastructure: Generation" {
+ seed : int + seed : int
} }
class RestConfig {
+ endpoint : std::string
+ api_key : std::string
+ timeout : std::chrono::milliseconds
}
} }
' ───────────────────────────────────────────── ' ─────────────────────────────────────────────
@@ -357,12 +322,12 @@ package "Infrastructure: Pipeline Channel" {
+ Close() : void + Close() : void
} }
note right of BoundedChannel note right of BoundedChannel
Used for user, brewery, and Used within each phase to
checkin/rating phases. decouple production from export.
Beer phase uses a simple Phase boundaries are explicit
sequential loop — enrichment sequential barriers in the
is all cache hits, no fan-out orchestrator's Run() method —
needed. not channel-mediated.
end note end note
} }
@@ -381,6 +346,13 @@ package "Infrastructure: Export" {
+ ProcessRating(rating : const GeneratedRating&) : void + ProcessRating(rating : const GeneratedRating&) : void
+ Finalize() : void + Finalize() : void
} }
note right of IExportService
Process* methods return
sqlite3_int64 row IDs.
Orchestrator uses these to
populate FK fields on all
downstream fixture types.
end note
class SqliteExportService { class SqliteExportService {
- date_time_provider_ : std::unique_ptr<IDateTimeProvider> - date_time_provider_ : std::unique_ptr<IDateTimeProvider>
@@ -393,7 +365,6 @@ package "Infrastructure: Export" {
- insert_rating_stmt_ : SqliteStatementHandle - insert_rating_stmt_ : SqliteStatementHandle
- transaction_open_ : bool - transaction_open_ : bool
- location_cache_ : std::unordered_map<std::string, sqlite3_int64> - location_cache_ : std::unordered_map<std::string, sqlite3_int64>
- brewery_cache_ : std::unordered_map<std::string, sqlite3_int64>
+ Initialize() : void + Initialize() : void
+ ProcessBrewery(brewery : const GeneratedBrewery&) : sqlite3_int64 + ProcessBrewery(brewery : const GeneratedBrewery&) : sqlite3_int64
+ ProcessBeer(beer : const GeneratedBeer&) : sqlite3_int64 + ProcessBeer(beer : const GeneratedBeer&) : sqlite3_int64
@@ -407,11 +378,11 @@ package "Infrastructure: Export" {
- FinalizeStatements() : void - FinalizeStatements() : void
} }
note right of SqliteExportService note right of SqliteExportService
brewery_cache_ restored. brewery_cache_ removed — row IDs
Keyed by location string for are now carried on GeneratedBrewery
location deduplication, and and GeneratedBeer value objects
by brewery identity for beer and threaded through by the
FK resolution without re-querying. orchestrator directly.
end note end note
interface IDateTimeProvider <<interface>> { interface IDateTimeProvider <<interface>> {
@@ -436,9 +407,7 @@ package "Orchestration" {
- brewery_context_strategy_ : std::unique_ptr<IContextStrategy> - brewery_context_strategy_ : std::unique_ptr<IContextStrategy>
- beer_context_strategy_ : std::unique_ptr<IContextStrategy> - beer_context_strategy_ : std::unique_ptr<IContextStrategy>
- sampling_strategy_ : std::unique_ptr<ISamplingStrategy> - sampling_strategy_ : std::unique_ptr<ISamplingStrategy>
- beer_selection_strategy_ : std::unique_ptr<IBeerSelectionStrategy>
- checkin_strategy_ : std::unique_ptr<ICheckinDistributionStrategy> - checkin_strategy_ : std::unique_ptr<ICheckinDistributionStrategy>
- beer_style_palette_ : std::vector<BeerStyle>
-- --
- user_pool_ : std::vector<GeneratedUser> - user_pool_ : std::vector<GeneratedUser>
- brewery_pool_ : std::vector<GeneratedBrewery> - brewery_pool_ : std::vector<GeneratedBrewery>
@@ -453,25 +422,29 @@ package "Orchestration" {
- RunRatingPhase() : void - RunRatingPhase() : void
} }
note right of BiergartenPipelineOrchestrator note right of BiergartenPipelineOrchestrator
beer_style_palette_ loaded once Single component owns all
at startup from beer-styles.json. sequencing. Run() reads as a
Passed as std::span<const BeerStyle> linear narrative:
to IBeerSelectionStrategy per brewery. 1. RunUserPhase
RunBeerPhase() is a sequential loop — 2. RunBreweryPhase
no channels, no fan-out. Enrichment 3. RunBeerPhase
is cache hits; LLM is the only cost. 4. checkin_strategy_->AssignActivityWeights
5. RunCheckinPhase
6. RunRatingPhase
The checkin gate is an explicit
sequential barrier between steps
3 and 5 — not a hidden internal
trigger in a separate object.
Pools are members: each phase
appends to them and the next
phase reads from them directly.
No mediator. No shared_ptr.
Ownership is unambiguous.
end note end note
class JsonLoader { class JsonLoader {
+ {static} LoadLocations(filepath : const std::filesystem::path&) : std::vector<Location> + {static} LoadLocations(filepath : const std::filesystem::path&) : std::vector<Location>
+ {static} LoadBeerStyles(filepath : const std::filesystem::path&) : std::vector<BeerStyle>
} }
note right of JsonLoader
LoadBeerStyles() added.
Reads beer-styles.json once
at startup into the palette
held by the orchestrator.
end note
} }
@@ -485,14 +458,12 @@ BiergartenPipelineOrchestrator *-- DataGenerator : owns
BiergartenPipelineOrchestrator *-- IExportService : owns BiergartenPipelineOrchestrator *-- IExportService : owns
BiergartenPipelineOrchestrator *-- ICheckinDistributionStrategy : owns BiergartenPipelineOrchestrator *-- ICheckinDistributionStrategy : owns
BiergartenPipelineOrchestrator *-- ISamplingStrategy : owns BiergartenPipelineOrchestrator *-- ISamplingStrategy : owns
BiergartenPipelineOrchestrator *-- IBeerSelectionStrategy : owns
BiergartenPipelineOrchestrator ..> JsonLoader : uses BiergartenPipelineOrchestrator ..> JsonLoader : uses
' Policy implementations ' Policy implementations
IContextStrategy <|.. BreweryContextStrategy : implements IContextStrategy <|.. BreweryContextStrategy : implements
IContextStrategy <|.. BeerContextStrategy : implements IContextStrategy <|.. BeerContextStrategy : implements
ISamplingStrategy <|.. UniformSamplingStrategy : implements ISamplingStrategy <|.. UniformSamplingStrategy : implements
IBeerSelectionStrategy <|.. RandomBeerSelectionStrategy : implements
ICheckinDistributionStrategy <|.. JCurveCheckinStrategy : implements ICheckinDistributionStrategy <|.. JCurveCheckinStrategy : implements
' Enrichment ' Enrichment
@@ -504,8 +475,10 @@ WebClient <|.. CURLWebClient : implements
' Generation ' Generation
DataGenerator <|.. MockGenerator : implements DataGenerator <|.. MockGenerator : implements
DataGenerator <|.. LlamaGenerator : implements DataGenerator <|.. LlamaGenerator : implements
DataGenerator <|.. RestGenerator : implements
LlamaGenerator *-- IPromptFormatter : owns LlamaGenerator *-- IPromptFormatter : owns
LlamaGenerator ..> LlamaConfig : constructed with LlamaGenerator ..> LlamaConfig : constructed with
RestGenerator ..> RestConfig : constructed with
IPromptFormatter <|.. Gemma4JinjaPromptFormatter : implements IPromptFormatter <|.. Gemma4JinjaPromptFormatter : implements
' Export ' Export
@@ -513,7 +486,7 @@ IExportService <|.. SqliteExportService : implements
SqliteExportService *-- IDateTimeProvider : owns SqliteExportService *-- IDateTimeProvider : owns
IDateTimeProvider <|.. SystemDateTimeProvider : implements IDateTimeProvider <|.. SystemDateTimeProvider : implements
' Domain containment ' Data flow
EnrichedCity *-- Location : contains EnrichedCity *-- Location : contains
EnrichedCity *-- LocationContext : contains EnrichedCity *-- LocationContext : contains
GeneratedBrewery *-- Location : contains GeneratedBrewery *-- Location : contains

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long