23 Commits

Author SHA1 Message Date
Aaron Po
5abb3f2e24 Add mock enrichment process 2026-05-14 13:49:59 -04:00
Aaron Po
a057b9197f Add location count to application options and as a cli arg 2026-05-13 22:04:48 -04:00
Aaron Po
773e7c774b Add timeout for enrichment, refactor json deserialization 2026-05-13 12:44:30 -04:00
b7c0b1c8d4 Fix mistake in .gitattributes
archive/* is incorrect as it will ignore sub-dirs
2026-05-12 01:05:07 -04:00
b8ebe03921 Pipeline: Add Runpod docker configuration (#222)
* Begin work on Runpod docker config

* Reduce docker image size

* Create .dockerignore
2026-05-12 00:44:09 -04:00
26635ace84 Organize and consolidate header files (#220) 2026-05-03 21:44:37 -04:00
031be8ad5d Pipeline: Remove CURL as a dependency, add new HTTP module (#219)
Rationale: 

HTTP is a supporting concern in the pipeline, used only for Wikipedia enrichment calls. libcurl's C API required significant boilerplate to wrap safely. cpp-httplib is a header-only library that covers the same functionality with far less overhead and no manual resource management.
2026-05-03 13:35:58 -04:00
f316fabcb0 Update CMakeLists.txt (#218) 2026-05-02 19:27:44 -04:00
b1dc8e0b5d refactor(pipeline): restructure config, add PromptDirectory, consolidate SQLite layer (#217)
* Refactor ApplicationOptions to separate config concerns

* add prompt dir app option

* readability updates: remove magic numbers, update comments

* codebase formatting

* Update docs

* Extract argument parsing, timer out of
2026-05-02 18:27:14 -04:00
641a479b6a Refactor SQLite Export Service and ProcessRecord Method Signatures (#216)
* Helper cleanup

update bind to use dto for params
consolidate translation units


* Update planned class diagram
2026-04-30 19:03:45 -04:00
d80e15b55e Add docs symlink to top level docs/pipeline (#214) 2026-04-30 18:35:46 -04:00
Aaron Po
1fcaf6e174 misc deletions 2026-04-27 18:47:39 -04:00
Aaron Po
ef97bf0a75 remove misc directory 2026-04-27 18:47:39 -04:00
Aaron Po
4c8a8e43ed update readmes 2026-04-27 18:47:39 -04:00
Aaron Po
8db6992296 move diagrams and documentation 2026-04-27 18:47:39 -04:00
Aaron Po
7925fc6caf Format all markdown files in active directories 2026-04-27 18:47:39 -04:00
Aaron Po
b1f4ff2641 remove legacy doc 2026-04-27 18:47:39 -04:00
Aaron Po
a852beff21 add git attributes to ignore archive dir 2026-04-27 18:47:39 -04:00
Aaron Po
e17afe909f move docker compose files 2026-04-27 18:47:39 -04:00
Aaron Po
9ed37806dd Move pipeline directory 2026-04-27 18:47:39 -04:00
Aaron Po
5a21589029 Move website directory 2026-04-27 18:47:39 -04:00
Aaron Po
189bce040b Move dotnet api into new directory 2026-04-27 18:47:39 -04:00
e8c5b8a80c Add future plans, new design, and ethics document updates to Pipeline (#212) 2026-04-27 15:41:28 -04:00
336 changed files with 4402 additions and 169499 deletions

View File

@@ -1,13 +0,0 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.2.1",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
archive/** linguist-vendored

View File

@@ -7,6 +7,7 @@ assignees: []
--- ---
## User Story ## User Story
**As a** (who wants to accomplish something) **As a** (who wants to accomplish something)
**I want to** (what they want to accomplish) **I want to** (what they want to accomplish)
**So that** (why they want to accomplish that thing) **So that** (why they want to accomplish that thing)
@@ -15,29 +16,18 @@ assignees: []
### Scenario 1 ### Scenario 1
Given ... When ... Then ...
Given ...
When ...
Then ...
### Scenario 2 ### Scenario 2
Given ... When ... Then ...
Given ...
When ...
Then ...
### Scenario 3 ### Scenario 3
Given ... When ... Then ...
Given ...
When ...
Then ...
## Subtasks ## Subtasks
- [ ] Task 1 - [ ] Task 1
- [ ] Task 2 - [ ] Task 2
- [ ] Task 3 - [ ] Task 3

File diff suppressed because it is too large Load Diff

161
README.md
View File

@@ -1,40 +1,56 @@
# The Biergarten App # The Biergarten App
The Biergarten App is a multi-project monorepo with a .NET backend and an active React The Biergarten App is a full-stack directory and discovery platform for
Router frontend in `src/Website`. The current website focuses on account flows, theme breweries. It features a robust user authentication system, a searchable
switching, shared UI components, Storybook coverage, and integration with the API. database of brewery locations, and a custom offline data-generation pipeline
that uses LLMs (Llama.cpp) and Wikipedia to synthesize realistic seed data.
## Documentation It features:
- [Getting Started](docs/getting-started.md) - Local setup for backend and active website - A .NET backend (Web API + database migrations/seed) under `web/backend/`
- [Architecture](docs/architecture.md) - Current backend and frontend architecture - A server-rendered React website (React Router + Vite) under `web/frontend/`
- [Docker Guide](docs/docker.md) - Container-based backend development and testing - A C++20 “pipeline” CLI for generating seed data under `tooling/pipeline/`
- [Testing](docs/testing.md) - Backend and frontend test commands
- [Environment Variables](docs/environment-variables.md) - Active configuration reference Specialized documentation (setup, architecture, docker, testing, diagrams, and
- [Token Validation](docs/token-validation.md) - JWT validation architecture pipeline notes) lives under `docs/`.
- [Legacy Website Archive](docs/archive/legacy-website-v1.md) - Archived notes for the old Next.js frontend
## Documentation (Start Here)
Website + backend (active stack):
- [Getting Started](docs/website/getting-started.md)
- [Architecture](docs/architecture.md)
- [Docker Guide](docs/website/docker.md)
- [Testing](docs/website/testing.md)
- [Environment Variables](docs/website/environment-variables.md)
- [Token Validation](docs/website/token-validation.md)
Data generation pipeline (C++):
- [Pipeline README](docs/pipeline/README.md)
- [Ethics & Known Issues](docs/pipeline/ETHICS-AND-KNOWN-ISSUES.md)
## Diagrams ## Diagrams
- [Architecture](docs/diagrams-out/architecture.svg) - Layered architecture - [Architecture](docs/website/diagrams-out/architecture.svg)
- [Deployment](docs/diagrams-out/deployment.svg) - Docker topology - [Deployment](docs/website/diagrams-out/deployment.svg)
- [Authentication Flow](docs/diagrams-out/authentication-flow.svg) - Auth sequence - [Authentication Flow](docs/website/diagrams-out/authentication-flow.svg)
- [Database Schema](docs/diagrams-out/database-schema.svg) - Entity relationships - [Database Schema](docs/website/diagrams-out/database-schema.svg)
## Current Status ## Current Status
Active areas in the repository: Active areas in the repository:
- .NET 10 backend with layered architecture and SQL Server - .NET 10 backend (layered architecture) + SQL Server
- React Router 7 website in `src/Website` - React 19 website (React Router 7 + Vite)
- Shared Biergarten theme system with a theme guide route - Shared Biergarten theme system + Storybook coverage
- Storybook stories and browser-based checks for shared UI - Auth flows and account/email integration (local Mailpit in dev compose)
- Auth demo flows for home, login, register, dashboard, logout, and confirmation - Data generation pipeline with C++ and Llama.cpp
- Toast-based feedback for auth outcomes
Legacy area retained for reference: Archived/reference areas:
- `src/Website-v1` contains the archived Next.js frontend and is no longer the active website - `archive/next-js-web-app/` contains an older Next.js frontend retained for
reference
## Tech Stack ## Tech Stack
@@ -43,36 +59,43 @@ Legacy area retained for reference:
- **UI Documentation**: Storybook 10, Vitest browser mode, Playwright - **UI Documentation**: Storybook 10, Vitest browser mode, Playwright
- **Testing**: xUnit, Reqnroll (BDD), FluentAssertions, Moq - **Testing**: xUnit, Reqnroll (BDD), FluentAssertions, Moq
- **Infrastructure**: Docker, Docker Compose - **Infrastructure**: Docker, Docker Compose
- **Security**: Argon2id password hashing, JWT access/refresh/confirmation tokens - **Security**: Argon2id password hashing, JWT access/refresh/confirmation
tokens
- **Data Pipeline**: C++20, CMake, Boost, libcurl, SQLite, llama.cpp
## Quick Start ## Quick Start
### Backend For full setup details, use [Getting Started](docs/website/getting-started.md).
This section is the shortest path to a working dev environment.
### Backend (Docker)
```bash ```bash
git clone https://github.com/aaronpo97/the-biergarten-app git clone https://github.com/aaronpo97/the-biergarten-app
cd the-biergarten-app cd the-biergarten-app
cp .env.example .env.dev
docker compose -f docker-compose.dev.yaml up -d cp web/.env.example web/.env.dev
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up --build -d
``` ```
Backend access: Backend access:
- API Swagger: http://localhost:8080/swagger - API Swagger: http://localhost:8080/swagger
- Health Check: http://localhost:8080/health - Health Check: http://localhost:8080/health
- Mailpit UI (dev SMTP): http://localhost:8025
### Frontend ### Frontend (Node)
```bash ```bash
cd src/Website cd web/frontend
npm install npm install
API_BASE_URL=http://localhost:8080 SESSION_SECRET=dev-secret npm run dev API_BASE_URL=http://localhost:8080 SESSION_SECRET=dev-secret-change-me npm run dev
``` ```
Optional frontend tools: Optional frontend tools:
```bash ```bash
cd src/Website cd web/frontend
npm run storybook npm run storybook
npm run test:storybook npm run test:storybook
npm run test:storybook:playwright npm run test:storybook:playwright
@@ -81,62 +104,42 @@ npm run test:storybook:playwright
## Repository Structure ## Repository Structure
```text ```text
src/Core/ Backend projects (.NET) web/
src/Website/ Active React Router frontend backend/ .NET API + domain/service/infrastructure + DB projects
src/Website-v1/ Archived legacy Next.js frontend frontend/ React Router website + Storybook + Playwright/Vitest
docs/ Active project documentation
docs/archive/ Archived legacy documentation tooling/
pipeline/ C++20 seed-data generation CLI (CMake)
docs/
architecture.md High-level architecture overview
website/ Backend/frontend setup, docker, testing, diagrams
pipeline/ Pipeline docs, ethics notes, PlantUML diagrams
archive/
next-js-web-app/ Older Next.js frontend (reference only)
``` ```
## Key Features
Implemented today:
- User registration and login against the API
- JWT-based auth with access, refresh, and confirmation flows
- SQL Server migrations and seed projects
- Shared form components and auth screens
- Theme switching with Lager, Stout, Cassis, and Weizen variants
- Storybook documentation and automated story interaction tests
- Toast feedback for auth-related outcomes
Planned next:
- Brewery discovery and management
- Beer reviews and ratings
- Social follow relationships
- Geospatial brewery experiences
- Additional frontend routes beyond the auth demo
## Testing ## Testing
Backend suites: Run the backend test stack with Docker:
- `API.Specs` - integration tests
- `Infrastructure.Repository.Tests` - repository unit tests
- `Service.Auth.Tests` - service unit tests
Frontend suites:
- Storybook interaction tests via Vitest
- Storybook browser regression checks via Playwright
Run all backend tests with Docker:
```bash ```bash
docker compose -f docker-compose.test.yaml up --abort-on-container-exit docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up --abort-on-container-exit
``` ```
See [Testing](docs/testing.md) for the full command list. See [Testing](docs/website/testing.md) for the full command list.
## Configuration ## Configuration
Common active variables: Common active variables:
- Backend: `DB_SERVER`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET` - Backend/Docker: `DB_SERVER`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`,
- Frontend: `API_BASE_URL`, `SESSION_SECRET`, `NODE_ENV` `ACCESS_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `CONFIRMATION_TOKEN_SECRET`,
`WEBSITE_BASE_URL`
- Frontend runtime: `API_BASE_URL`, `SESSION_SECRET`, `NODE_ENV`
See [Environment Variables](docs/environment-variables.md) for details. See [Environment Variables](docs/website/environment-variables.md) for details.
## Contributing ## Contributing
@@ -145,15 +148,3 @@ See [Environment Variables](docs/environment-variables.md) for details.
3. Commit your changes (`git commit -m 'Add amazing feature'`) 3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`) 4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request 5. Open a Pull Request
### Development Workflow
1. Start development environment: `docker compose -f docker-compose.dev.yaml up -d`
2. Make changes to code
3. Run tests: `docker compose -f docker-compose.test.yaml up --abort-on-container-exit`
4. Rebuild if needed: `docker compose -f docker-compose.dev.yaml up -d --build api.core`
## Support
- **Documentation**: [docs/](docs/)
- **Architecture**: See [Architecture Guide](docs/architecture.md)

View File

@@ -4,24 +4,28 @@ This document describes the active architecture of The Biergarten App.
## High-Level Overview ## High-Level Overview
The Biergarten App is a monorepo with a clear split between the backend and the active The Biergarten App is a monorepo with a clear split between the backend and the
website: active website:
- **Backend**: .NET 10 Web API with SQL Server and a layered architecture - **Backend**: .NET 10 Web API with SQL Server and a layered architecture
- **Frontend**: React 19 + React Router 7 website in `src/Website` - **Frontend**: React 19 + React Router 7 website in `src/Website`
- **Architecture Style**: Layered backend plus server-rendered React frontend - **Architecture Style**: Layered backend plus server-rendered React frontend
The legacy Next.js frontend has been retained in `src/Website-v1` for reference only and is The legacy Next.js frontend has been retained in `src/Website-v1` for reference
documented in [archive/legacy-website-v1.md](archive/legacy-website-v1.md). only and is documented in
[archive/legacy-website-v1.md](archive/legacy-website-v1.md).
## Diagrams ## Diagrams
For visual representations, see: For visual representations, see:
- [architecture.svg](diagrams-out/architecture.svg) - Layered architecture diagram - [architecture.svg](diagrams-out/architecture.svg) - Layered architecture
diagram
- [deployment.svg](diagrams-out/deployment.svg) - Docker deployment diagram - [deployment.svg](diagrams-out/deployment.svg) - Docker deployment diagram
- [authentication-flow.svg](diagrams-out/authentication-flow.svg) - Authentication workflow - [authentication-flow.svg](diagrams-out/authentication-flow.svg) -
- [database-schema.svg](diagrams-out/database-schema.svg) - Database relationships Authentication workflow
- [database-schema.svg](diagrams-out/database-schema.svg) - Database
relationships
## Backend Architecture ## Backend Architecture
@@ -218,7 +222,8 @@ public interface IAuthRepository
### Active Website (`src/Website`) ### Active Website (`src/Website`)
The current website is a React Router 7 application with server-side rendering enabled. The current website is a React Router 7 application with server-side rendering
enabled.
```text ```text
src/Website/ src/Website/
@@ -244,20 +249,22 @@ src/Website/
### Theme System ### Theme System
The active website uses semantic DaisyUI theme tokens backed by four Biergarten themes: The active website uses semantic DaisyUI theme tokens backed by four Biergarten
themes:
- Biergarten Lager - Biergarten Lager
- Biergarten Stout - Biergarten Stout
- Biergarten Cassis - Biergarten Cassis
- Biergarten Weizen - Biergarten Weizen
All component styling should prefer semantic tokens such as `primary`, `success`, All component styling should prefer semantic tokens such as `primary`,
`surface`, and `highlight` instead of hard-coded color values. `success`, `surface`, and `highlight` instead of hard-coded color values.
### Legacy Frontend ### Legacy Frontend
The previous Next.js frontend has been archived at `src/Website-v1`. Active product and The previous Next.js frontend has been archived at `src/Website-v1`. Active
engineering documentation should point to `src/Website`, while legacy notes live in product and engineering documentation should point to `src/Website`, while
legacy notes live in
[archive/legacy-website-v1.md](archive/legacy-website-v1.md). [archive/legacy-website-v1.md](archive/legacy-website-v1.md).
## Security Architecture ## Security Architecture
@@ -387,8 +394,8 @@ For details, see [Docker Guide](docker.md).
### Health Checks ### Health Checks
**SQL Server**: Validates database connectivity **API**: Checks service health and **SQL Server**: Validates database connectivity **API**: Checks service health
dependencies and dependencies
**Configuration**: **Configuration**:

View File

@@ -1,56 +0,0 @@
# Legacy Website Archive (`src/Website-v1`)
This archive captures high-level notes about the previous Biergarten frontend so active
project documentation can focus on the current website in `src/Website`.
## Status
- `src/Website-v1` is retained for historical reference only
- It is not the active frontend used by current setup, docs, or testing guidance
- New product and engineering work should target `src/Website`
## Legacy Stack Summary
The archived frontend used a different application model from the current website:
- Next.js 14
- React 18
- Prisma
- Postgres / Neon-hosted database workflows
- Next.js API routes and server-side controllers
- Additional third-party integrations such as Cloudinary, Mapbox, and SparkPost
## Why It Was Archived
The active website moved to a React Router-based frontend that talks directly to the .NET
API. As part of that shift, the main docs were updated to describe:
- `src/Website` as the active frontend
- React Router route modules and server rendering
- Storybook-based component documentation and tests
- Current frontend runtime variables: `API_BASE_URL`, `SESSION_SECRET`, and `NODE_ENV`
## Legacy Documentation Topics Moved Out of Active Docs
The following categories were removed from active documentation and intentionally archived:
- Next.js application structure guidance
- Prisma and Postgres frontend setup
- Legacy frontend environment variables
- External service setup that only applied to `src/Website-v1`
- Old frontend local setup instructions
## When To Use This Archive
Use this file only if you need to:
- inspect the historical frontend implementation
- compare old flows against the current website
- migrate or recover legacy logic from `src/Website-v1`
For all active work, use:
- [Getting Started](../getting-started.md)
- [Architecture](../architecture.md)
- [Environment Variables](../environment-variables.md)
- [Testing](../testing.md)

View File

@@ -0,0 +1,336 @@
# Ethics, Bias, and Known Issues
This document covers the ethical context of the Biergarten Pipeline's output,
the model's biases, and known issues including hallucinated brewing science and
low-resource language failures.
> Note that all testing was used using `google_gemma-4-E4B-it-Q6_K.gguf`.
## Table of Contents
- [What This Dataset Is](#what-this-dataset-is)
- [What This Dataset Is Not](#what-this-dataset-is-not)
- [Model Bias and Language Quality](#model-bias-and-language-quality)
- [Western and Eurocentric Lens](#western-and-eurocentric-lens)
- [Wikipedia Enrichment](#wikipedia-enrichment)
- [The "Avoid AI Phrases" Prompt Instruction](#the-avoid-ai-phrases-prompt-instruction)
- [Known Issues](#known-issues)
- [Hallucinated Brewing Techniques](#hallucinated-brewing-techniques)
- [Low-Resource Language Hallucination](#low-resource-language-hallucination)
---
## What This Dataset Is
This is AI-generated fixture data for a proof-of-concept version of The
Biergarten App. Anyone who interacts with an application seeded from this
pipeline must be told upfront that the content is AI-generated.
---
## What This Dataset Is Not
The pipeline is not intended to produce accurate brewing science, faithful
cultural representation, or reliable local-language text. Hallucinations such as
invented fermentation techniques, or incoherent local-language prose, are
expected, observed, and partially documented in [Known Issues](#known-issues)
below.
Human control sits at the context layer (i.e. prompt design, Wikipedia
enrichment). Statistical output shapes in future pipeline stages (check-in
distributions, rating skews, activity profiles) will be handled the same way.
**Treat this data as an exercise in prompt engineering and model behaviour, not
as a source of truth for brewing techniques or cultural representation.**
**Natural language processing, although a powerful tool for data analysis and
generation is to be taken with scrutiny. Human language is not simply just data
points to be analyzed, but it also carries deep cultural and human meaning that
artificial intelligence is incapable of.**
---
## Model Bias and Language Quality
The underlying model's training biases surface within this pipeline. Output
quality tracks with how well a language is represented in the training corpus:
standard French (`fr-FR`) produces coherent text; regional variants like `fr-CD`
and `fr-CI` are noticeably weaker; low-resource languages like Welsh, Māori, and
Sicilian produce output that is syntactically plausible but often semantically
broken.
This is a property of the training distribution, not something that can be
mitigated through prompt design. This is a well-documented characteristic of
large language models trained predominantly on English-language
material.[^llm-bias]
Mitigations are documented in
[Known Issues: Low-Resource Language Hallucination](#low-resource-language-hallucination).
### Western and Eurocentric Lens
The model's training data skews heavily Western and North American. When
generating brewery descriptions for Kinshasa, Abidjan, or Osaka, for example, it
defaults to framing and cultural reference points drawn from that perspective
rather than from the lived context of those cities. Wikipedia enrichment grounds
some generation in city-specific material, but it does not eliminate the skew.
**Output should be read with an understanding of this bias.**
---
## Wikipedia Enrichment
City and beer context is fetched from the Wikipedia API. Wikipedia text is
co-licensed under the **Creative Commons Attribution-ShareAlike 4.0
International License (CC BY-SA 4.0)** and the **GNU Free Documentation License
(GFDL)**.[^wp-license]
Wikipedia's own accuracy limitations and editorial biases can propagate into
generated descriptions.
---
## The "Avoid AI Phrases" Prompt Instruction
The system prompt instructs the model to avoid common AI-generated phrasing
patterns. This is a prompt engineering experiment:
> How far can a model be pushed against its own stylistic defaults?
This is not an attempt to disguise the content as human-written. All downstream
consumers are informed of the AI-generated origin before engagement.
---
## Known Issues
### Hallucinated Brewing Techniques
When forced by the system prompt to generate a "highly specialized technical
brewing detail," the model frequently hallucinates fermentation science and
brewing chemistry. While the resulting sounds confident, it will be nonsensical
to reader with brewing and/or scientific expertise.
Small-parameter models such as Gemma 4 E4B cannot reliably satisfy prompts
calling for specialist brewing detail. This is consistent with the **CHOKE**
failure mode (Certain Hallucinations Overriding Known Evidence) [^llm-choke]
where a specific-sounding prompt causes a model to produce confident, coherent,
and factually wrong output.
#### Example — Osaka, produced using Gemma 4 E4B
```log
[2026-04-21 15:04:40.258] [info] 11. city="Osaka" country="Japan" state="Osaka" iso3166_2=JP-27 lat=34.6937 lon=135.5023
[2026-04-21 15:04:40.258] [info] brewery_name_en="Kani-no-Kuni Brewing"
[2026-04-21 15:04:40.258] [info] brewery_description_en="The humid scent of takoyaki and savory grilled squid always settles over the neighborhood early in the morning, mingling strangely with the metallic tang of spent grain outside our drying shed. We decided to build this place here because Osaka is a city that never pauses, a place built around the constant exchange of goods and tastes, and that is what we want our beer to reflect. Many people here are accustomed to the dependable, clean taste of the major pale lagers, but we are interested in exploring the deeper, more textured expressions of fermentation. Our base malts are specifically crafted by incorporating strains of local rice, like Yamada Nishiki, which we malt and use not for sake, but for its complex, buttery flavor profile during the mash. This combination, followed by a slow, anaerobic aging process, gives our beers a profound, satisfying depth that stands apart from the hurried mass-market style. The heavy, deep red brick of the original warehouse wall has absorbed decades of Kansai humidity and seasonal rains, creating a patina that tells the exact story of this district's tireless movement. We chose this structure not for its charm, but for its resilience and the sheer density of the local history held within its mortar. Our goal is simply to serve a drink worthy of this powerful trading city. If you are looking for a quiet spot away from the main thoroughfare, look for us just off the side street near Shinsekai."
[2026-04-21 15:04:40.258] [info] brewery_name_local="カニの国ブルワリー"
[2026-04-21 15:04:40.258] [info] brewery_description_local="早朝の、たこ焼きや香ばしいイカ焼きの湿った匂いは、いつも乾燥小屋の外にある使用済み麦芽の金属的な匂いと奇妙に混ざり合って近隣に漂います。私たちはこの場所に店を構えることを決めたのです。なぜなら、大阪は決して止まることのない都市であり、商品と味が絶え間なく交換されることで築かれた場所だからです。地元の多くの方々は、信頼できる大規模な淡麗ラガーの味が習慣になっていますが、私たちは発酵の、より深く、より複雑な表現を探求することに関心があります。私たちのベースモルトは、山田錦のような地元の米の品種を意図的に組み込んで作られています。この米を酒ではなく、麦芽として、仕込みの最中にその複雑でバターのような風味を引き出すために使用しています。この組み合わせを、ゆっくりとした嫌気的な熟成プロセスに続けることで、私たちのビールは、慌ただしい市場のスタイルとは一線を画す、深みのある、満足感のある複雑さを持っています。オリジナルの倉庫の重く深紅のレンガ壁は、関西特有の湿気と季節の雨を何十年も吸収し、この地区の絶え間ない動きの正確な物語を語るような古色を帯びています。私たちはこの構造物を、その魅力のためではなく、その回復力とモルタルに込められた地域の歴史の密度ゆえに選びました。私たちの目標は、ただこの力強い交易都市に値する飲み物を提供することだけです。もしメインの通りから離れた静かな場所をお探しなら、新世界近くの脇道にある私たちを探してください。"
```
A review of the following text for brewing techniques reveals several
inaccuracies, and no comments could be made on the local-language version due to
my own lack of proficiency in Japanese:
#### 1. "Buttery flavours" framed as a desirable malt-derived flavour
**Incorrect.**
Diacetyl is a fermentation byproduct of yeast metabolism, not a malt-derived
compound.[^diacetyl-source] Diacetyl produces a buttery or butterscotch
off-flavour and is carefully managed in many beer styles, in particular lighter
beers, through a process called a _diacetyl rest_. In this process, fermentation
temperature is briefly raised to allow yeast to reabsorb the compound before
packaging.[^diacetyl-rest]
The Oxford Companion to Beer claims that, while low levels are tolerable in some
ales and stouts, diacetyl is considered undesirable at any perceptible
concentration when it results from bacterial contamination or stressed
fermentation.[^oxford-beer]
#### 2. Yamada Nishiki sake rice described as a self-saccharifying base malt
**Incorrect.**
Yamada Nishiki (_山田錦_) is a short-grain Japanese rice bred specifically for
sake production.[^yn-wiki] Its value lies in its large starchy core
(_shinpaku_), low protein content, and amenability to _koji_ mold penetration
during saccharification.[^yn-sakestreet] Sake brewing does not use the grain's
own enzymatic activity for saccharification — it relies on _Aspergillus oryzae_
(koji mold) grown on a portion of the steamed rice to convert starches to
fermentable sugars.[^yn-sakeonline]
#### 3. "Anaerobic aging" presented as a differentiating technique
**Misleading**
Anaerobic conditions during packaging and aging are not differentiating
technique. Anaerobic conditions are the standard baseline for all commercial
beer production. Breweries exclude oxygen as a top priority for packaging and
shelf stability; published research in _Microbiology Spectrum_ confirms that
packaged beer constitutes an anaerobic environment by definition.[^anaerobic]
Professional packaging lines use CO_2 purges and closed transfers specifically
to maintain this state.[^packaging] Framing anaerobic aging as a distinctive
practice is misleading and suggests hallucinated output.
### Low-Resource Language Hallucination
The generation pipeline passes local language codes to the model to retrieve a
translated `description_local`. Output quality is reliable for high-resource
languages such as French, though it may struggle with regional variants and
idiomatic phrasing.
```json
[
{
"city": "Kinshasa",
"state_province": "Kinshasa",
"iso3166_2": "CD-KN",
"country": "Democratic Republic of the Congo",
"iso3166_1": "CD",
"latitude": -4.4419,
"longitude": 15.2663,
"local_languages": ["fr-CD", "ln"]
},
{
"city": "Paris",
"state_province": "Île-de-France",
"iso3166_2": "FR-IDF",
"country": "France",
"iso3166_1": "FR",
"latitude": 48.8566,
"longitude": 2.3522,
"local_languages": ["fr-FR"]
},
{
"city": "Abidjan",
"state_province": "Abidjan",
"iso3166_2": "CI-AB",
"country": "Ivory Coast",
"iso3166_1": "CI",
"latitude": 5.36,
"longitude": -4.0083,
"local_languages": ["fr-CI"]
},
{
"city": "Montreal",
"state_province": "Quebec",
"iso3166_2": "CA-QC",
"country": "Canada",
"iso3166_1": "CA",
"latitude": 45.5017,
"longitude": -73.5673,
"local_languages": ["fr-CA"]
},
{
"city": "Brussels",
"state_province": "Brussels-Capital Region",
"iso3166_2": "BE-BRU",
"country": "Belgium",
"iso3166_1": "BE",
"latitude": 50.8503,
"longitude": 4.3517,
"local_languages": ["fr-BE", "nl-BE"]
}
]
```
This dataset, when fed into the pipeline will often times reason that a local
variant of French is needed, but will often times just default to a standardized
dialect of French, devoid of any cultural or linguistic nuance.
For languages such as Welsh (Wales), Māori (Aotearoa/New Zealand), or Sicilian
(Sicily, Italy), the model can generate text that looks syntactically plausible
but is semantically incoherent. This comes from limited training-data coverage
rather than prompt engineering.
Output sample:
[./out-sample/french-cities.example](out-sample/french-cities.example)
#### Proposed Mitigations
- **Prevention via allowlist:** introduce a high-resource language allowlist. If
a location's code is unlisted, skip `description_local` generation and fall
back to English.
- **Upstream sanitization:** strip known low-resource language codes from the
`locations.json` payload before generation.
- **Downstream flagging:** add a `description_local_confidence` column to the
SQLite schema so downstream applications can filter or flag potentially
hallucinated text by language tier.
---
## Footnotes
[^llm-choke]:
CHOKE (Certain Hallucinations Overriding Known Evidence) is a hallucination
failure mode defined by Simhi et al. (2025), in which a model that can
consistently answer a question correctly produces a confident, wrong
response when the prompt is trivially perturbed. Source: Trust Me, I'm
Wrong: LLMs Hallucinate with Certainty Despite Knowing the Answer — Adi
Simhi, Itay Itzhak, Fazl Barez, Gabriel Stanovsky, Yonatan Belinkov.
[^llm-bias]:
e.g., Blasi et al. (2022), "Systematic Inequalities in Language Technology
Performance across the World's Languages," _ACL Anthology_. The pattern is
consistent with models trained predominantly on English-language web
corpora.
[^wp-license]:
Source:
[Wikipedia:FAQ/Copyright](https://en.wikipedia.org/wiki/Wikipedia:FAQ/Copyright).
[^cc-sa]:
Creative Commons CC BY-SA 4.0 deed: "If you remix, transform, or build upon
the material, you must distribute your contributions under the same license
as the original." Source:
[creativecommons.org/licenses/by-sa/4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en).
[^diacetyl-source]:
White Labs confirms that diacetyl is a yeast-derived fermentation byproduct:
specifically, a compound produced during amino acid metabolism that leaks
out of the yeast cell and oxidises into its characteristic buttery
off-flavour. It is generally considered undesirable at any perceived level
in most styles, though low levels are tolerated in some English ales and
European lagers. Source:
[whitelabs.com — Compound Spotlight: Diacetyl](https://www.whitelabs.com/news-update-detail?id=54).
[^diacetyl-rest]:
Brewing Science Institute: diacetyl "is produced during the fermentation
process, primarily as a byproduct of yeast metabolism… generally considered
a flaw in most beer styles." Source:
[brewingscience.com — Diacetyl: Understanding Its Role as an Off-Flavor in Beer](https://brewingscience.com/diacetyl-understanding-its-role-as-an-off-flavor-in-beer/).
[^oxford-beer]:
Oxford Companion to Beer via _Beer & Brewing_: "At low to moderate levels,
diacetyl can be perceived as a positive flavor characteristic in some ales
and stouts" but "particularly unwelcome in lager-style beers." Source:
[beerandbrewing.com — diacetyl](https://www.beerandbrewing.com/dictionary/48TDqQibPi).
[^yn-wiki]:
Wikipedia: "Yamada Nishiki (山田錦) is a short-grain Japanese rice famous
for its use in high-quality sake." Source:
[en.wikipedia.org/wiki/Yamada_Nishiki](https://en.wikipedia.org/wiki/Yamada_Nishiki).
[^yn-sakestreet]:
Sake Street: Yamadanishiki's large _shinpaku_ allows koji mold to penetrate
to the centre of the rice grain, making it "particularly suitable for
producing good koji." Source:
[sakestreet.com — What is Yamadanishiki?](https://sakestreet.com/en/media/what-is-yamadanishiki).
[^yn-sakeonline]:
Sake Online: "Steamed rice is added to make koji (rice malt) and yeast
starter, which promotes alcohol fermentation." Source:
[sakeonline.com.au — Types of Sake Rice: Yamada Nishiki](https://sakeonline.com.au/blogs/news/types-of-sake-rice-yamada-nishiki-and-its-characteristics).
[^anaerobic]:
Pai et al. (2022): "Breweries have recognized oxygen exclusion as a top
priority for the proper packaging and aging of beer… packaged beer is an
anaerobic environment." _Microbiology Spectrum._ Source:
[journals.asm.org](https://journals.asm.org/doi/10.1128/spectrum.02656-22).
[^packaging]:
Beer Production Processes (oboe.com): Professional packaging lines use
double CO_2 pre-evacuation cycles and closed transfers "so the beer moves in
a completely anaerobic environment." Source:
[oboe.com — Flavor Quality Control](https://oboe.com/learn/beer-production-processes-308lmf/flavor-quality-control-4).

439
docs/pipeline/README.md Normal file
View File

@@ -0,0 +1,439 @@
# Biergarten Pipeline
A C++20 command-line pipeline that samples city records from local JSON,
enriches each with Wikipedia context, and generates bilingual brewery names and
descriptions via a local GGUF model or a deterministic mock.
> **This pipeline produces AI-generated data.** It is not a source of truth for
> brewing techniques, cultural representation, or local-language accuracy. See
> [ETHICS-AND-KNOWN-ISSUES.md](./ETHICS-AND-KNOWN-ISSUES.md) for a full
> documentation of limitations, hallucination patterns, and bias.
---
## Table of Contents
- [How It Fits The Main App](#how-it-fits-the-main-app)
- [Quick Start](#quick-start)
- [Build](#build)
- [Model](#model)
- [Run](#run)
- [Docker / RunPod](#docker--runpod)
- [Architecture](#architecture)
- [Pipeline Stages](#pipeline-stages)
- [Key Components](#key-components)
- [Runtime Behaviour](#runtime-behaviour)
- [Generated Output](#generated-output)
- [Tech Stack](#tech-stack)
- [Tested Hardware](#tested-hardware)
- [Fixture Strategy](#fixture-strategy)
- [Repo Layout](#repo-layout)
- [Code Tour](#code-tour)
- [Next Steps](#next-steps)
---
## How It Fits The Main App
The pipeline is a data ingestion layer. It sits outside the web app runtime and
produces seed records the app imports at startup or during a dedicated seed
step.
| Planned app area | Pipeline contribution |
| -------------------------------- | ------------------------------------------------------------------ |
| Brewery discovery and management | Sampled city records, localized names, long-form descriptions |
| Beer reviews and ratings | Stable brewery fixtures with enough context to anchor review pages |
| Social follow relationships | Repeatable brewery entities for feeds, follows, and saved lists |
| Geospatial brewery experiences | Latitude, longitude, and country-level metadata |
---
## Quick Start
### Build
Requirements: C++20 compiler, CMake 3.31+, OpenSSL, Boost (JSON and
ProgramOptions). SQLite is fetched from the upstream amalgamation, so no system
SQLite package is required.
```bash
cmake -S . -B build
cmake --build build
```
CMake automatically detects whether a compatible llama.cpp installation is
present on the system (`libllama`, `libggml`, `libggml-base`, and `llama.h`
visible on the default search paths). If found, it links against those
libraries and skips the FetchContent build. If not found, it fetches and builds
llama.cpp from source at tag `b9012`. No additional flags are required in
either case.
Metal is enabled automatically on Apple Silicon. CUDA or HIP/ROCm is detected
automatically on Linux when the relevant toolkit is present.
### Model
> Skip this step if you only need `--mocked`.
```bash
mkdir -p models
curl -L \
-o models/google_gemma-4-E4B-it-Q6_K.gguf \
https://huggingface.co/bartowski/google_gemma-4-E4B-it-GGUF/resolve/main/google_gemma-4-E4B-it-Q6_K.gguf?download=true
```
### Run
Run from `build/` so the copied `locations.json` and `prompts/` are available.
Each run writes a fresh dated SQLite file such as
`biergarten_seed_2026-04-19T15-30-45.123456Z.sqlite` into the working directory.
```bash
./biergarten-pipeline --mocked
./biergarten-pipeline \
--model ../models/google_gemma-4-E4B-it-Q6_K.gguf \
--prompt-dir prompts \
--temperature 1.0 --top-p 0.95 --top-k 64 --n-ctx 8192 --seed -1
```
#### CLI Flags
| Flag | Purpose |
| --------------- | ---------------------------------------------------------------------------------------------------- |
| `--mocked` | Deterministic mock generator, no model required. |
| `--model, -m` | Path to a GGUF file. Required unless `--mocked` is set. |
| `--prompt-dir` | Directory containing prompt files (e.g. `BREWERY_GENERATION.md`). Required unless `--mocked` is set. |
| `--output, -o` | Directory for generated SQLite artifacts. Default: `output`. |
| `--log-path` | Path for application logs. Default: `pipeline.log`. |
| `--temperature` | Sampling temperature. Default: `1.0`. |
| `--top-p` | Nucleus sampling. Default: `0.95`. |
| `--top-k` | Top-k sampling. Default: `64`. |
| `--n-ctx` | Context window size. Default: `8192`. |
| `--seed` | Random seed. Default: `-1` (random at runtime). |
| `--help, -h` | Print usage and exit. |
`--mocked` and `--model` are mutually exclusive. Omitting both exits with an
error before the pipeline starts. Sampling flags are ignored when `--mocked` is
set.
The post-build step copies `prompts/` into `build/prompts/`. Rebuild after
editing any prompt file.
---
## Docker / RunPod
The `tooling/pipeline/runpod/` directory contains a GPU-ready container
configuration for running the pipeline on RunPod or any Docker host with an
NVIDIA GPU.
### How it works
The container uses a two-stage build. The first stage pulls prebuilt
`libllama`, `libggml`, and backend plugin libraries (including `libggml-cuda.so`
and the CPU variant plugins) from `ghcr.io/ggml-org/llama.cpp:full-cuda`. The
second stage copies those libraries into `/usr/local/lib` and runs `ldconfig` so
the dynamic linker and `dlopen` calls from `ggml_backend_load_all()` can resolve
the CUDA backend plugin at runtime. llama.cpp headers are cloned at the matching
tag and installed into `/usr/local/include`. CMake auto-detects both and skips
the FetchContent source build entirely, keeping image build times short.
`GGML_BACKEND_PATH` is set to `/usr/local/lib` so llama.cpp knows where to scan
for backend plugins.
### Build the image
Run from the `tooling/pipeline/` directory (the CMake project root), not from
inside `runpod/`, so the `COPY . .` step picks up the full project context.
```bash
docker build -t biergarten-pipeline:latest -f runpod/Dockerfile .
```
To monitor the full build output and confirm CMake selects the system llama.cpp:
```bash
docker build \
--progress=plain \
--no-cache \
-t biergarten-pipeline:latest \
-f runpod/Dockerfile \
. 2>&1 | tee build.log
```
Look for `[biergarten] Found system llama.cpp — skipping FetchContent` in the
output to confirm the fast path was taken.
### Run in mocked mode
No model or GPU required. Useful for validating the pipeline logic and SQLite
export path.
```bash
docker run --rm \
-e BIERGARTEN_MODE=mocked \
-v "$PWD/output:/workspace/output" \
-v "$PWD/logs:/workspace/logs" \
biergarten-pipeline:latest
```
### Run in live mode
Mount your GGUF model before starting. The container validates the model path
before launching the binary.
```bash
docker run --rm \
--runtime=nvidia \
-e BIERGARTEN_MODE=live \
-e GGML_BACKEND_PATH="/usr/local/lib/libggml-cuda.so" \
-v "$PWD/models:/workspace/models" \
-v "$PWD/output:/workspace/output" \
-v "$PWD/logs:/workspace/logs" \
biergarten-pipeline:latest
```
The model must be present at `./models/google_gemma-4-E4B-it-Q6_K.gguf` on the
host. See [Model](#model) above for the download command.
### RunPod deployment
Use a GPU pod template. Mount persistent storage for `/workspace/models`,
`/workspace/output`, and `/workspace/logs`. Set `BIERGARTEN_MODE=live` in the
template environment. See `tooling/pipeline/runpod/pod-template.yaml` for a
starter template.
---
## Architecture
### Pipeline Stages
| Stage | Implementation |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Load | `JsonLoader::LoadLocations()` reads `locations.json` into typed `Location` records. |
| Sample | `BiergartenDataGenerator::QueryCitiesWithCountries()` samples up to 50 locations per run. |
| Enrich | `WikipediaService` fetches city and beer context. Keeps going when a lookup fails. |
| Generate | `MockGenerator` or `LlamaGenerator` produces brewery names and descriptions in English and the local language. |
| Store | `SqliteExportService` writes each successful brewery into a fresh dated `.sqlite` database with normalized location and brewery tables. |
| Log | `spdlog` writes results and warnings to the console. |
If enrichment or generation fails for a city, that city is skipped and the
pipeline continues.
### Key Components
- `src/main.cc` — argument parsing and Boost.DI composition root.
- `JsonLoader` — validates curated location input.
- `WikipediaService` — queries Wikipedia extracts, caches results, returns empty
context on failure.
- `LlamaGenerator` — formats prompts for Gemma 4, validates JSON output, retries
malformed responses up to three times. If output looks truncated, the retry
raises the token budget before trying again.
- `MockGenerator` — stable hash-based output so the same city input always
produces the same brewery.
- `SqliteExportService` — creates a dated SQLite file per run and persists each
successful brewery into normalized tables.
- Brewery payloads include English and local-language name and description
fields.
### Runtime Behaviour
`WikipediaService` queries city, country, and beer-related Wikipedia extracts
using its configured lookup, then caches the first successful response per query
string. The fetched extract text is included in the prompt as context for
generation.
`GetLocationContext()` returns an empty string when the web client is
unavailable or when lookup/parsing fails.
`LlamaGenerator` validates model output as structured JSON. The retry path
exists as a safety hatch for cases where the reasoning block consumes available
token budget and compresses the JSON output space. All runs to date have
produced valid output on the first pass; the path is kept for resilience.
`MockGenerator` uses stable hashes for repeatable output in demos and Storybook
runs.
### Process Flow - Activity Diagram
![An activity diagram](./diagrams/current/output/activity.svg)
### Architectural Overview - Class Diagram
![A class diagram](./diagrams/current/output/class.svg)
---
## Generated Output
Each successful run stores a `GeneratedBrewery` pair with the source location
and a `BreweryResult` payload. The same generated records are also written to a
fresh SQLite export file named with the current UTC timestamp.
| Field | Meaning |
| ------------------- | ------------------------------------------ |
| `name_en` | Brewery name in English. |
| `description_en` | Brewery description in English. |
| `name_local` | Brewery name in the local language. |
| `description_local` | Brewery description in the local language. |
The log dump also includes city, country, state or province, ISO subdivision
code, latitude, and longitude for each entry.
### Consumer Data Shape
| Field | Why it matters |
| ----------------------------------- | ------------------------------------------------ |
| `city`, `state_province`, `country` | Human-readable location labels and page headings |
| `iso3166_1`, `iso3166_2` | Filtering, regional grouping, locale matching |
| `latitude`, `longitude` | Map pins and nearby brewery views |
| `local_languages` | Locale-aware copy selection |
| `name_en`, `description_en` | Default English display content |
| `name_local`, `description_local` | Local-language display content |
| `region_context` | Richer copy for cards and detail pages |
---
## Tech Stack
- C++20
- CMake 3.31+
- Boost.JSON, Boost.ProgramOptions, Boost.DI
- spdlog
- cpp-httplib (with OpenSSL)
- SQLite amalgamation fetched and compiled via CMake FetchContent
- llama.cpp (auto-detected from system install or fetched via FetchContent)
- Docker with NVIDIA CUDA 12.6 base image for GPU container builds
- RunPod for cloud GPU inference
The build fetches Boost.DI, spdlog, and SQLite via CMake. llama.cpp is fetched
only when a system installation is not detected. Metal is enabled on Apple
Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
> **Code Style:** Modern C++20 throughout — RAII for ownership,
> `std::unique_ptr` for injected dependencies, `std::optional` for parse
> outcomes, `std::span` for read-only views over generated city data, structured
> bindings in pipeline loops. Formatting follows the Google C++ Style Guide via
> `.clang-format` with a narrow column limit and two-space indentation.
---
## Tested Hardware
### ARM macOS — M1 Pro
| | |
| --------- | --------------------------------- |
| Host | MacBook Pro 14" (2021) |
| CPU | Apple M1 Pro (8-core) |
| GPU | Apple M1 Pro (14-core integrated) |
| Memory | 16 GB |
| Model | Gemma 4 E4B |
| Inference | llama.cpp with Metal |
### x86_64 Linux — NVIDIA RTX 2000
| | |
| --------- | ------------------------------ |
| Host | ThinkPad P1 Gen 7 (Fedora 43) |
| CPU | Intel Core Ultra 7 155H |
| GPU | NVIDIA RTX 2000 Ada Generation |
| Memory | 32 GB |
| Model | Gemma 4 E4B |
| Inference | llama.cpp with CUDA 12.x |
### x86_64 Linux — Docker / RunPod (NVIDIA CUDA)
| | |
| --------- | ------------------------------------------- |
| Host | RunPod GPU pod |
| Base | nvidia/cuda:12.6.3-devel-ubuntu24.04 |
| Model | Gemma 4 E4B Q6_K |
| Inference | llama.cpp prebuilt CUDA backends via dlopen |
---
## Fixture Strategy
- `--mocked` for stable fixtures, repeatable screenshots, and Storybook runs.
- `--model` when geographically grounded content matters for demos.
- Keep `locations.json` structured enough to support discovery and future
filtering.
- Treat SQLite output as seed material for the app's brewery domain, not
production data.
---
## Repo Layout
| Path | Purpose |
| ---------------------------- | -------------------------------------------------- |
| `includes/` | Public headers and shared models. |
| `src/` | Implementation files. |
| `locations.json` | Curated city input copied into the build tree. |
| `prompts/` | System prompts used by the model-backed path. |
| `diagrams/` | Architecture and pipeline diagrams. |
| `tooling/pipeline/runpod/` | Dockerfile, launcher, and RunPod pod template. |
| `ETHICS-AND-KNOWN-ISSUES.md` | Ethics, bias, hallucination analysis, mitigations. |
---
## Code Tour
- `src/main.cc` — argument parsing and DI composition root.
- `src/biergarten_data_generator/` — orchestration, sampling, logging, and
export.
- `src/services/wikipedia/` — enrichment service and cache.
- `src/services/sqlite/` — SQLite export implementation.
- `src/data_generation/llama/` — local inference, prompt loading, output
validation.
- `src/data_generation/mock/` — deterministic fallback.
- `tooling/pipeline/runpod/` — container build and runtime launcher.
---
## Next Steps
The pipeline currently produces city-aware brewery records and dated SQLite
exports. The next passes add additional fixture types so the app can exercise
the full brewery domain without live data.
### Testing — Very High Priority
- Unit test JSON validation and retry logic against malformed, truncated, and
empty model outputs.
- Integration test the enrichment pipeline with missing context, short context,
and fake context inputs.
- Adversarial context tests: feed plausible but geographically incorrect
Wikipedia extracts and verify the model does not silently blend them with
training data.
- Verify bilingual enrichment behaviour when only an English extract is
available versus when both extracts are present.
- Confirm the retry path is reachable when the reasoning block consumes
available token budget.
### Beer Generation
Generate catalog entries with style, ABV, IBU, color, aroma notes, and food
pairing hints. Link beers back to breweries and cities. Keep style coverage wide
enough to exercise search, sort, and category filters.
### User Generation
Generate user profiles with stable names, bios, locale hints, and preference
signals. Include stable IDs for downstream fixture joins. Keep output
deterministic for screenshots while allowing larger randomized batches.
### Check-In System
Produce timestamped check-in events between users and breweries. Use a J-curve
activity profile — a small set of users accounts for most check-ins, the rest
appear occasionally. Add bursty behaviour around weekends and travel periods.
### Beer Ratings
Generate rating events with a strong positive skew and a long tail of lower
scores. Avoid uniform distributions. Attach timestamps and user IDs so the app
can compute averages, trends, and per-style comparisons.

View File

@@ -0,0 +1,34 @@
skinparam shadowing false
skinparam backgroundColor #FCFCF7
skinparam defaultFontName "DM Sans"
skinparam defaultFontColor #14180C
skinparam titleFontName "Volkhov"
skinparam titleFontColor #14180C
skinparam ArrowColor #656F33
skinparam NoteBackgroundColor #DBEEDD
skinparam NoteFontColor #14180C
skinparam NoteBorderColor #4A5837
skinparam SwimlaneBorderColor #4A5837
skinparam SwimlaneBorderThickness 1
skinparam activityStartColor #EBECE3
skinparam activityEndColor #4A5837
skinparam activityStopColor #4A5837
skinparam ActivityBackgroundColor #EBECE3
skinparam ActivityBorderColor #4A5837
skinparam ActivityDiamondBackgroundColor #CBD2B5
skinparam ActivityDiamondBorderColor #4A5837
skinparam packageStyle rectangle
skinparam packageBackgroundColor #F1F3EA
skinparam packageBorderColor #4A5837
skinparam packageFontColor #14180C
skinparam classBackgroundColor #EBECE3
skinparam classBorderColor #4A5837
skinparam classFontColor #14180C
skinparam classAttributeFontColor #3F4724
skinparam classStereotypeFontColor #4A5837
skinparam interfaceBackgroundColor #DBEEDD
skinparam interfaceBorderColor #4A5837
skinparam interfaceFontColor #14180C
skinparam enumBackgroundColor #E4E6D8
skinparam enumBorderColor #4A5837
skinparam enumFontColor #14180C

View File

@@ -29,7 +29,7 @@ if (Are arguments valid?) then (no)
else (yes) else (yes)
endif endif
:Init CurlGlobalState & LlamaBackendState; :Init OpenSSL global state & LlamaBackendState;
:di::make_injector(...); :di::make_injector(...);
:injector.create<std::unique_ptr<BiergartenDataGenerator>>(); :injector.create<std::unique_ptr<BiergartenDataGenerator>>();
:BiergartenDataGenerator::Run(); :BiergartenDataGenerator::Run();

View File

@@ -52,7 +52,7 @@ interface WebClient <<interface>> {
+ UrlEncode(value : const std::string&) : std::string + UrlEncode(value : const std::string&) : std::string
} }
class CURLWebClient { class HttpWebClient {
+ Get(url : const std::string&) : std::string + Get(url : const std::string&) : std::string
+ UrlEncode(value : const std::string&) : std::string + UrlEncode(value : const std::string&) : std::string
} }
@@ -130,7 +130,7 @@ BiergartenDataGenerator *-- IExportService : owns
IEnrichmentService <|.. WikipediaService : implements IEnrichmentService <|.. WikipediaService : implements
WikipediaService *-- WebClient : owns WikipediaService *-- WebClient : owns
WebClient <|.. CURLWebClient : implements WebClient <|.. HttpWebClient : implements
DataGenerator <|.. MockGenerator : implements DataGenerator <|.. MockGenerator : implements
DataGenerator <|.. LlamaGenerator : implements DataGenerator <|.. LlamaGenerator : implements

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,360 @@
@startuml biergarten_activity
!include ../biergarten-weizen-theme.puml
skinparam defaultFontSize 13
skinparam titleFontSize 20
title The Biergarten Data Pipeline — Activity Diagram
|Main|
start
:ParseArguments(argc, argv);
if (Invalid args?) then (yes)
:spdlog::error;
stop
else (no)
endif
:Init OpenSSL global state & LlamaBackendState;
:Build DI injector;
:Initialize SqliteExportService;
note right
Opens SQLite connection.
(Transactions are now managed
per-phase via batching).
end note
:Create BoundedChannel<LogEntry> log_ch;
:Spawn Log Worker thread;
note right
Log worker drains log_ch for the
entire pipeline lifetime.
All workers emit LogEntry structs
via PipelineLogger -- never spdlog directly.
end note
:BiergartenPipelineOrchestrator::Run();
|BiergartenPipelineOrchestrator::Run()|
fork
:JsonLoader::LoadBeerStyles("beer-styles.json");
:EnrichmentService::PreWarmBeerStyleCache(beer_styles);
fork again
:JsonLoader::LoadLocations("locations.json");
:EnrichmentService::PreWarmLocationCache(sampled_locations);
end fork
fork
:JsonLoader::LoadNamesByCountry("names-by-country.json");
fork again
:JsonLoader::LoadPersonas("personas.json");
end fork
' ═══════════════════════════════════════════
' PHASE 0 — USER GENERATION
' ═══════════════════════════════════════════
|Orchestrator|
:RunUserPhase(sampled_locations);
:Create BoundedChannels\n(loc_ch, exp_ch);
fork
|Orchestrator|
:Loop: Send Locations -> loc_ch;
:Close loc_ch;
note right
Producer closes loc_ch.
LLM Worker while loop
terminates on empty + closed.
end note
fork again
|LLM Worker|
while (loc_ch has items?) is (yes)
:Receive Location;
:GetLocationContextFromCache(location);
note right
Guaranteed cache hit from startup.
end note
:IPersonaSelectionStrategy::SelectPersona(\n personas_palette_);
note right
Guaranteed cache hit from startup.
Returns a Persona struct carrying
style_affinities, abv_range,
ibu_preference, checkin_weight.
end note
:NamesByCountry::SampleName(\n location.iso3166_1);
note right
Deterministic lookup -- no LLM involved.
Name selected from pre-keyed table
and passed into the generation prompt.
end note
:GenerateUser(enriched_city, persona, sampled_name)\nvia DataGenerator;
note right
LLM receives: EnrichedCity context + persona
description + sampled name. Generates
bio and preference signals grounded
in locale and persona.
end note
:PipelineLogger::Log(Info, UserGeneration,\n city, user_id, "llm");
:Send GeneratedUser -> exp_ch;
endwhile (no)
:Close exp_ch;
note right
Producer closes exp_ch.
SQLite Worker while loop
terminates on empty + closed.
end note
fork again
|SQLite Worker|
:BEGIN TRANSACTION;
while (exp_ch has items?) is (yes)
:Receive GeneratedUser;
:ProcessUser(user);
:PipelineLogger::Log(Info, UserGeneration,\n city, user_id, "sqlite");
:Append -> user_pool_;
if (Batch size reached?) then (yes)
:COMMIT & BEGIN;
else (no)
endif
endwhile (no)
:COMMIT (Final);
end fork
|Orchestrator|
:Join LLM Worker, SQLite Worker;
' ═══════════════════════════════════════════
' PHASE 1a — BREWERY GENERATION
' ═══════════════════════════════════════════
:RunBreweryPhase(sampled_locations);
:Create BoundedChannels\n(loc_ch, exp_ch);
fork
|Orchestrator|
:Loop: Sample User from user_pool_
and pair with Location;
:Send BreweryTask(Location, User) -> loc_ch;
:Close loc_ch;
fork again
|LLM Worker|
while (loc_ch has items?) is (yes)
:Receive BreweryTask(Location, User);
:GetLocationContextFromCache(task.location);
note right
Guaranteed cache hit from startup.
end note
:GenerateBrewery(enriched_city, context, task.user)\nvia DataGenerator;
note right
KV cache stays warm.
Brewery is linked to the sampled owner_user_id.
end note
:PipelineLogger::Log(Info,\n BreweryGeneration,\n city, brewery_id, "llm");
:Send GeneratedBrewery -> exp_ch;
endwhile (no)
:Close exp_ch;
fork again
|SQLite Worker|
:BEGIN TRANSACTION;
while (exp_ch has items?) is (yes)
:Receive GeneratedBrewery;
:ProcessBrewery(brewery);
:PipelineLogger::Log(Info,\n BreweryGeneration,\n city, brewery_id, "sqlite");
:Append -> brewery_pool_;
if (Batch size reached?) then (yes)
:COMMIT & BEGIN;
else (no)
endif
endwhile (no)
:COMMIT (Final);
end fork
|Orchestrator|
:Join LLM Worker, SQLite Worker;
note right
brewery_pool_ is now fully populated.
Phase 1b may begin.
end note
' ═══════════════════════════════════════════
' PHASE 1b — BEER GENERATION
' ═══════════════════════════════════════════
:RunBeerPhase();
:Create BoundedChannels\n(brew_ch, exp_ch);
fork
|Orchestrator|
:Loop: Send Breweries -> brew_ch;
:Close brew_ch;
fork again
|LLM Worker|
while (brew_ch has items?) is (yes)
:Receive GeneratedBrewery;
:IBeerSelectionStrategy::SelectStyles(\n brewery, beer_style_palette_);
while (For each selected BeerStyle?) is (remaining)
:GetStyleContextFromCache(style);
note right
Guaranteed cache hit from startup.
KV cache stays warm across all
beer generations -- system prompt
does not change within this phase.
end note
:GenerateBeer(brewery, style_context)\nvia DataGenerator;
:Attach GeneratedBeer to bundle;
endwhile (done)
:PipelineLogger::Log(Info,\n BeerGeneration,\n city, brewery_id, "llm");
:Send BeersBundle -> exp_ch;
endwhile (no)
:Close exp_ch;
fork again
|SQLite Worker|
:BEGIN TRANSACTION;
while (exp_ch has items?) is (yes)
:Receive BeersBundle;
while (For each beer in bundle?) is (remaining)
:Set beer.brewery_id from bundle;
:ProcessBeer(beer);
:Append -> beer_pool_;
endwhile (done)
:PipelineLogger::Log(Info,\n BeerGeneration,\n city, brewery_id, "sqlite");
if (Batch size reached?) then (yes)
:COMMIT & BEGIN;
else (no)
endif
endwhile (no)
:COMMIT (Final);
end fork
|Orchestrator|
:Join LLM Worker, SQLite Worker;
note right
Both brewery_pool_ and beer_pool_
are now completely populated.
Checkin and Follow phases may
now run in parallel.
end note
' ═══════════════════════════════════════════
' PHASE 2 — CHECKIN + FOLLOW GENERATION
' (parallel — both depend only on user_pool_
' and brewery_pool_ being fully populated)
' ═══════════════════════════════════════════
fork
|Orchestrator|
:RunCheckinPhase();
:ICheckinDistributionStrategy::\nAssignActivityWeights(user_pool_);
note right
Weights seeded from each user's
persona.checkin_weight. J-curve profile
emerges from persona distribution.
end note
:BEGIN TRANSACTION;
while (For each GeneratedUser in user_pool_?) is (remaining)
:CheckinsForUser(user, brewery_pool_.size());
while (For each checkin index?) is (remaining)
:TimestampFor(user, index);
:Select brewery from brewery_pool_;
:GenerateCheckin(user, brewery, timestamp)\nvia DataGenerator;
:ProcessCheckin(checkin);
:PipelineLogger::Log(Info, CheckinGeneration,\n nullopt, checkin_id, "sqlite");
:Append -> checkin_pool_;
if (Batch size reached?) then (yes)
:COMMIT & BEGIN;
else (no)
endif
endwhile (done)
endwhile (done)
:COMMIT (Final);
fork again
|Orchestrator|
:RunFollowPhase();
:IFollowGenerationStrategy::\nAssignFollowWeights(user_pool_);
note right
For RandomFollowStrategy, weights
are uniform. For ActivityWeightedFollowStrategy,
weights derived from user.activity_weight
so high-activity users attract more followers.
end note
:BEGIN TRANSACTION;
:IFollowGenerationStrategy::\nGenerateFollows(user_pool_);
note right
Self-follow constraint (follower_id != followed_id)
enforced here and at the DB schema level.
end note
while (For each GeneratedFollow?) is (remaining)
:ProcessFollow(follow);
:PipelineLogger::Log(Info, FollowGeneration,\n nullopt, follower_id, "sqlite");
:Append -> follow_pool_;
if (Batch size reached?) then (yes)
:COMMIT & BEGIN;
else (no)
endif
endwhile (done)
:COMMIT (Final);
end fork
|Orchestrator|
:Join CheckinPhase, FollowPhase;
note right
checkin_pool_ and follow_pool_
are now fully populated.
Rating phase may begin.
end note
' ═══════════════════════════════════════════
' PHASE 3 — RATING GENERATION
' ═══════════════════════════════════════════
:RunRatingPhase();
note right
Beer selection biased by
user.persona.style_affinities and abv_range.
Rating skew modulated per persona.
end note
:BEGIN TRANSACTION;
while (For each GeneratedCheckin in checkin_pool_?) is (remaining)
:Match brewery_id, select beer from beer_pool_\n(same brewery_id, biased by persona affinities);
if (Beer exists for brewery?) then (yes)
:GenerateRating(user, beer, checkin_id)\nvia DataGenerator;
:ProcessRating(rating);
:PipelineLogger::Log(Info, RatingGeneration,\n nullopt, rating_id, "sqlite");
if (Batch size reached?) then (yes)
:COMMIT & BEGIN;
else (no)
endif
else (no)
:PipelineLogger::Log(Warn, RatingGeneration,\n nullopt, brewery_id, "sqlite");
:Skip -- brewery has no beers;
endif
endwhile (done)
:COMMIT (Final);
' ═══════════════════════════════════════════
' TEARDOWN
' ═══════════════════════════════════════════
|Orchestrator|
:Finalize SqliteExportService;
note right
Safely closes the DB connection.
end note
:Close log_ch;
|Main|
:spdlog::info "Pipeline complete in X ms";
:Join Log Worker;
note right
Drain guarantees no LogEntry is
dropped at shutdown.
end note
stop
@enduml

View File

@@ -1,51 +1,14 @@
@startuml future_possible_architecture @startuml
' ========================================== ' ==========================================
' CONFIGURATION & STYLING ' CONFIGURATION & STYLING
' ========================================== ' ==========================================
left to right direction !include ../biergarten-weizen-theme.puml
skinparam linetype ortho skinparam classAttributeFontSize 9
skinparam defaultFontSize 25
skinparam titleFontSize 30
' --- Typography --- package "Domain: Models" {
skinparam defaultFontName "DM Sans"
skinparam defaultFontSize 14
skinparam titleFontName "Volkhov"
skinparam titleFontSize 20
' --- Global Colors ---
skinparam backgroundColor #FCFCF7
skinparam defaultFontColor #14180C
skinparam titleFontColor #14180C
skinparam ArrowColor #656F33
skinparam class {
BackgroundColor #EBECE3
HeaderBackgroundColor #CBD2B5
BorderColor #4A5837
ArrowColor #656F33
FontColor #14180C
}
skinparam package {
BackgroundColor #DBEEDD
BorderColor #4A5837
FontColor #14180C
}
skinparam note {
BackgroundColor #DBEEDD
BorderColor #4A5837
FontColor #14180C
}
skinparam monochrome reverse
title The Biergarten Data Pipeline — Planned Architecture
' ==========================================
' DOMAIN MODELS
' ==========================================
package "Domain Models" {
class Location { class Location {
+ city : std::string + city : std::string
@@ -62,8 +25,9 @@ package "Domain Models" {
+ text : std::string + text : std::string
+ completeness : Completeness + completeness : Completeness
+ char_count : size_t + char_count : size_t
-- }
<<enum>> Completeness
enum Completeness {
Full Full
Partial Partial
Absent Absent
@@ -116,46 +80,69 @@ package "Domain Models" {
+ note : std::string + note : std::string
} }
class GenerationMetadata {
+ generation_id : uint64_t
+ generated_time : std::string
+ context_provided : bool
+ generated_with : std::string
}
class GeneratedBrewery { class GeneratedBrewery {
+ brewery_id : sqlite3_int64 + brewery_id : uint64_t
+ location : Location + location : Location
+ brewery : BreweryResult + brewery : BreweryResult
+ context_completeness : LocationContext::Completeness + context_completeness : LocationContext::Completeness
+ generated_at : std::string + metadata : GenerationMetadata
} }
class GeneratedBeer { class GeneratedBeer {
+ beer_id : sqlite3_int64 + beer_id : uint64_t
+ brewery_id : sqlite3_int64 + brewery_id : uint64_t
+ location : Location + location : Location
+ style : BeerStyle + style : BeerStyle
+ beer : BeerResult + beer : BeerResult
+ generated_at : std::string + metadata : GenerationMetadata
} }
class GeneratedUser { class GeneratedUser {
+ user_id : sqlite3_int64 + user_id : uint64_t
+ location : Location + location : Location
+ user : UserResult + user : UserResult
+ generated_at : std::string + metadata : GenerationMetadata
} }
class GeneratedCheckin { class GeneratedCheckin {
+ checkin_id : sqlite3_int64 + checkin_id : uint64_t
+ user_id : sqlite3_int64 + user_id : uint64_t
+ brewery_id : sqlite3_int64 + brewery_id : uint64_t
+ checkin : CheckinResult + checkin : CheckinResult
+ generated_at : std::string + metadata : GenerationMetadata
} }
class GeneratedRating { class GeneratedRating {
+ user_id : sqlite3_int64 + user_id : uint64_t
+ beer_id : sqlite3_int64 + beer_id : uint64_t
+ checkin_id : sqlite3_int64 + checkin_id : uint64_t
+ rating : RatingResult + rating : RatingResult
+ generated_at : std::string + metadata : GenerationMetadata
} }
class GeneratedFollow {
+ follower_id : uint64_t
+ followed_id : uint64_t
+ metadata : GenerationMetadata
}
class UserPersona {
+ name: std::string
+ description: std::string
+ style_affinities: std::vector<std::string>
}
LocationContext *-- Completeness
}
@startuml
package "Domain: Application Configuration" {
class SamplingOptions { class SamplingOptions {
+ temperature: float = 1.0F + temperature: float = 1.0F
+ top_p: float = 0.95F + top_p: float = 0.95F
@@ -167,7 +154,7 @@ package "Domain Models" {
class GeneratorOptions { class GeneratorOptions {
+ model_path: std::filesystem::path + model_path: std::filesystem::path
+ use_mocked: bool = false + use_mocked: bool = false
+ sampling : SamplingOptions + sampling: std::optional<SamplingOptions>
} }
class PipelineOptions { class PipelineOptions {
@@ -183,71 +170,11 @@ package "Domain Models" {
' --- Domain Model Relationships --- ' --- Domain Model Relationships ---
ApplicationOptions *-- GeneratorOptions ApplicationOptions *-- GeneratorOptions
ApplicationOptions *-- PipelineOptions ApplicationOptions *-- PipelineOptions
GeneratorOptions *-- SamplingOptions GeneratorOptions o-- SamplingOptions
LocationContext *-- Completeness
} }
@endum
package "Domain: Policy" {
' ==========================================
' LOGGING
' ==========================================
package "Logging" {
enum LogLevel {
Debug
Info
Warn
Error
}
enum PipelinePhase {
Startup
UserGeneration
BreweryAndBeerGeneration
CheckinGeneration
RatingGeneration
Teardown
}
class LogEntry {
+ timestamp : std::chrono::system_clock::time_point
+ level : LogLevel
+ phase : PipelinePhase
+ message : std::string
+ city : std::optional<std::string>
+ entity_id : std::optional<std::string>
+ worker : std::optional<std::string>
}
interface Logger <<interface>> {
+ Log(level, phase, message,\n city, entity_id, worker) : void
}
class PipelineLogger {
- log_ch_ : BoundedChannel<LogEntry>&
+ Log(level, phase, message,\n city, entity_id, worker) : void
}
class LogWorker {
- log_ch_ : BoundedChannel<LogEntry>&
+ Run() : void
- FormatTimestamp(tp) : std::string
- ToSpdlogLevel(level) : spdlog::level::level_enum
- ToString(phase) : std::string
}
' --- Logging Relationships ---
LogEntry *-- LogLevel
LogEntry *-- PipelinePhase
PipelineLogger ..> LogEntry : emits
LogWorker ..> LogEntry : consumes
}
' ==========================================
' DOMAIN POLICY
' ==========================================
package "Domain Policy" {
interface ContextStrategy <<interface>> { interface ContextStrategy <<interface>> {
+ QueriesFor(loc : const Location&) : std::vector<std::string> + QueriesFor(loc : const Location&) : std::vector<std::string>
@@ -297,13 +224,103 @@ package "Domain Policy" {
+ TimestampFor(user : const GeneratedUser&,\n index : size_t) : std::string + TimestampFor(user : const GeneratedUser&,\n index : size_t) : std::string
} }
class RandomCheckinStrategy {
- rng_ : std::mt19937
- min_checkins_ : size_t
- max_checkins_ : size_t
+ AssignActivityWeights(users : std::vector<GeneratedUser>&) : void
+ CheckinsForUser(user : const GeneratedUser&,\n brewery_count : size_t) : size_t
+ TimestampFor(user : const GeneratedUser&,\n index : size_t) : std::string
} }
interface FollowGenerationStrategy <<interface>> {
+ GenerateFollows(users : const std::vector<GeneratedUser>&) : std::vector<GeneratedFollow>
}
' ========================================== class RandomFollowStrategy {
' ORCHESTRATION - rng_ : std::mt19937
' ========================================== - min_follows_ : size_t
package "Orchestration" { - max_follows_ : size_t
+ GenerateFollows(users : const std::vector<GeneratedUser>&) : std::vector<GeneratedFollow>
}
class ActivityWeightedFollowStrategy {
- rng_ : std::mt19937
- min_follows_ : size_t
- max_follows_ : size_t
+ GenerateFollows(users : const std::vector<GeneratedUser>&) : std::vector<GeneratedFollow>
}
}
package "Infrastructure: Logging" {
enum LogLevel {
Debug
Info
Warn
Error
}
enum PipelinePhase {
Startup
UserGeneration
BreweryAndBeerGeneration
CheckinGeneration
RatingGeneration
FollowGeneration
Teardown
}
class LogEntry {
+ timestamp : std::chrono::system_clock::time_point
+ level : LogLevel
+ phase : PipelinePhase
+ message : std::string
+ city : std::optional<std::string>
+ entity_id : std::optional<std::string>
+ worker : std::optional<std::string>
}
interface Logger <<interface>> {
+ Log(level, phase, message,\n city, entity_id, worker) : void
}
class PipelineLogger {
- log_ch_ : BoundedChannel<LogEntry>&
+ Log(level, phase, message,\n city, entity_id, worker) : void
}
class LogWorker {
- log_ch_ : BoundedChannel<LogEntry>&
+ Run() : void
- FormatTimestamp(tp) : std::string
- ToSpdlogLevel(level) : spdlog::level::level_enum
- ToString(phase) : std::string
}
' --- Logging Relationships ---
LogEntry *-- LogLevel
LogEntry *-- PipelinePhase
PipelineLogger ..> LogEntry : emits
LogWorker ..> LogEntry : consumes
}
package "Infrastructure: Pipeline Channel" {
class "BoundedChannel<T>" as BoundedChannel {
- queue_ : std::queue<T>
- mutex_ : std::mutex
- not_full_ : std::condition_variable
- not_empty_ : std::condition_variable
- capacity_ : size_t
- closed_ : bool
+ Send(item : T) : void
+ Receive() : std::optional<T>
+ Close() : void
}
}
package "Infrastructure: Data Preloading" {
interface DataPreloader <<interface>> { interface DataPreloader <<interface>> {
+ LoadLocations(filepath : const std::filesystem::path&) : std::vector<Location> + LoadLocations(filepath : const std::filesystem::path&) : std::vector<Location>
@@ -312,38 +329,6 @@ package "Orchestration" {
+ LoadNamesByCountry(filepath : const std::filesystem::path&) : NamesByCountry + LoadNamesByCountry(filepath : const std::filesystem::path&) : NamesByCountry
} }
class BiergartenPipelineOrchestrator {
- preloader_ : std::unique_ptr<DataPreloader>
- enrichment_service_ : std::unique_ptr<EnrichmentService>
- generator_ : std::unique_ptr<DataGenerator>
- logger_ : std::unique_ptr<Logger>
- exporter_ : std::unique_ptr<ExportService>
- brewery_context_strategy_ : std::unique_ptr<ContextStrategy>
- sampling_strategy_ : std::unique_ptr<SamplingStrategy>
- beer_selection_strategy_ : std::unique_ptr<BeerSelectionStrategy>
- checkin_strategy_ : std::unique_ptr<CheckinDistributionStrategy>
- beer_style_palette_ : std::vector<BeerStyle>
- options_ : ApplicationOptions
--
- user_pool_ : std::vector<GeneratedUser>
- brewery_pool_ : std::vector<GeneratedBrewery>
- beer_pool_ : std::vector<GeneratedBeer>
- checkin_pool_ : std::vector<GeneratedCheckin>
--
+ Run() : bool
- RunUserPhase(locations : const std::vector<Location>&) : void
- RunBreweryAndBeerPhase(locations : const std::vector<Location>&) : void
- RunCheckinPhase() : void
- RunRatingPhase() : void
}
}
' ==========================================
' INFRASTRUCTURE: PRELOADING
' ==========================================
package "Infrastructure: Preloading" {
class JsonLoader { class JsonLoader {
+ LoadLocations(filepath : const std::filesystem::path&) : std::vector<Location> + LoadLocations(filepath : const std::filesystem::path&) : std::vector<Location>
+ LoadBeerStyles(filepath : const std::filesystem::path&) : std::vector<BeerStyle> + LoadBeerStyles(filepath : const std::filesystem::path&) : std::vector<BeerStyle>
@@ -353,10 +338,6 @@ package "Infrastructure: Preloading" {
} }
' ==========================================
' INFRASTRUCTURE: ENRICHMENT
' ==========================================
package "Infrastructure: Enrichment" { package "Infrastructure: Enrichment" {
interface EnrichmentService <<interface>> { interface EnrichmentService <<interface>> {
@@ -375,25 +356,21 @@ package "Infrastructure: Enrichment" {
+ UrlEncode(value : const std::string&) : std::string + UrlEncode(value : const std::string&) : std::string
} }
class CURLWebClient { class HttpWebClient {
+ Get(url : const std::string&) : std::string + Get(url : const std::string&) : std::string
+ UrlEncode(value : const std::string&) : std::string + UrlEncode(value : const std::string&) : std::string
} }
} }
package "Infrastructure: Data Generation" {
' ==========================================
' INFRASTRUCTURE: GENERATION
' ==========================================
package "Infrastructure: Generation" {
interface DataGenerator <<interface>> { interface DataGenerator <<interface>> {
+ GenerateBrewery(location : const Location&,\n context : const LocationContext&) : BreweryResult + GenerateBrewery(location : const Location&,\n 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 : uint64_t,\n location : const Location&,\n context : const LocationContext&,\n style : const BeerStyle&) : 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&,\n brewery : const GeneratedBrewery&,\n timestamp : const std::string&) : CheckinResult
+ GenerateRating(user : const GeneratedUser&,\n beer : const GeneratedBeer&,\n checkin_id : sqlite3_int64) : RatingResult + GenerateRating(user : const GeneratedUser&,\n beer : const GeneratedBeer&,\n checkin_id : uint64_t) : RatingResult
} }
class MockGenerator { class MockGenerator {
@@ -432,39 +409,16 @@ package "Infrastructure: Generation" {
} }
package "Infrastructure: Data Export" {
' ==========================================
' INFRASTRUCTURE: PIPELINE CHANNEL
' ==========================================
package "Infrastructure: Pipeline Channel" {
class "BoundedChannel<T>" as BoundedChannel {
- queue_ : std::queue<T>
- mutex_ : std::mutex
- not_full_ : std::condition_variable
- not_empty_ : std::condition_variable
- capacity_ : size_t
- closed_ : bool
+ Send(item : T) : void
+ Receive() : std::optional<T>
+ Close() : void
}
}
' ==========================================
' INFRASTRUCTURE: EXPORT
' ==========================================
package "Infrastructure: Export" {
interface ExportService <<interface>> { interface ExportService <<interface>> {
+ Initialize() : void + Initialize() : void
+ ProcessBrewery(brewery : const GeneratedBrewery&) : sqlite3_int64 + ProcessBrewery(brewery : const GeneratedBrewery&) : uint64_t
+ ProcessBeer(beer : const GeneratedBeer&) : sqlite3_int64 + ProcessBeer(beer : const GeneratedBeer&) : uint64_t
+ ProcessUser(user : const GeneratedUser&) : sqlite3_int64 + ProcessUser(user : const GeneratedUser&) : uint64_t
+ ProcessCheckin(checkin : const GeneratedCheckin&) : sqlite3_int64 + ProcessCheckin(checkin : const GeneratedCheckin&) : uint64_t
+ ProcessRating(rating : const GeneratedRating&) : void + ProcessRating(rating : const GeneratedRating&) : void
+ ProcessFollow(follow : const GeneratedFollow&) : void
+ Finalize() : void + Finalize() : void
} }
@@ -477,15 +431,17 @@ package "Infrastructure: Export" {
- insert_user_stmt_ : SqliteStatementHandle - insert_user_stmt_ : SqliteStatementHandle
- insert_checkin_stmt_ : SqliteStatementHandle - insert_checkin_stmt_ : SqliteStatementHandle
- insert_rating_stmt_ : SqliteStatementHandle - insert_rating_stmt_ : SqliteStatementHandle
- insert_follow_stmt_ : SqliteStatementHandle
- transaction_open_ : bool - transaction_open_ : bool
- location_cache_ : std::unordered_map<std::string, sqlite3_int64> - location_cache_ : std::unordered_map<std::string, uint64_t>
- brewery_cache_ : std::unordered_map<std::string, sqlite3_int64> - brewery_cache_ : std::unordered_map<std::string, uint64_t>
+ Initialize() : void + Initialize() : void
+ ProcessBrewery(brewery : const GeneratedBrewery&) : sqlite3_int64 + ProcessRecord(brewery : const GeneratedBrewery&) : uint64_t
+ ProcessBeer(beer : const GeneratedBeer&) : sqlite3_int64 + ProcessRecord(beer : const GeneratedBeer&) : uint64_t
+ ProcessUser(user : const GeneratedUser&) : sqlite3_int64 + ProcessRecord(user : const GeneratedUser&) : uint64_t
+ ProcessCheckin(checkin : const GeneratedCheckin&) : sqlite3_int64 + ProcessRecord(checkin : const GeneratedCheckin&) : uint64_t
+ ProcessRating(rating : const GeneratedRating&) : void + ProcessRecord(rating : const GeneratedRating&) : void
+ ProcessRecord(follow : const GeneratedFollow&) : void
+ Finalize() : void + Finalize() : void
- InitializeSchema() : void - InitializeSchema() : void
- PrepareStatements() : void - PrepareStatements() : void
@@ -504,9 +460,34 @@ package "Infrastructure: Export" {
} }
' ==========================================
' GLOBAL RELATIONSHIPS class BiergartenPipelineOrchestrator {
' ========================================== - preloader_ : std::unique_ptr<DataPreloader>
- enrichment_service_ : std::unique_ptr<EnrichmentService>
- generator_ : std::unique_ptr<DataGenerator>
- logger_ : std::unique_ptr<Logger>
- exporter_ : std::unique_ptr<ExportService>
- brewery_context_strategy_ : std::unique_ptr<ContextStrategy>
- sampling_strategy_ : std::unique_ptr<SamplingStrategy>
- beer_selection_strategy_ : std::unique_ptr<BeerSelectionStrategy>
- checkin_strategy_ : std::unique_ptr<CheckinDistributionStrategy>
- follow_strategy_ : std::unique_ptr<FollowGenerationStrategy>
- beer_style_palette_ : std::vector<BeerStyle>
- options_ : ApplicationOptions
--
- user_pool_ : std::vector<GeneratedUser>
- brewery_pool_ : std::vector<GeneratedBrewery>
- beer_pool_ : std::vector<GeneratedBeer>
- checkin_pool_ : std::vector<GeneratedCheckin>
- follow_pool_ : std::vector<GeneratedFollow>
--
+ Run() : bool
- RunUserPhase(locations : const std::vector<Location>&) : void
- RunBreweryAndBeerPhase(locations : const std::vector<Location>&) : void
- RunCheckinPhase() : void
- RunRatingPhase() : void
- RunFollowPhase() : void
}
' --- Orchestration Aggregations (Services & Strategies) --- ' --- Orchestration Aggregations (Services & Strategies) ---
BiergartenPipelineOrchestrator *-- DataPreloader BiergartenPipelineOrchestrator *-- DataPreloader
@@ -514,6 +495,7 @@ BiergartenPipelineOrchestrator *-- EnrichmentService
BiergartenPipelineOrchestrator *-- DataGenerator BiergartenPipelineOrchestrator *-- DataGenerator
BiergartenPipelineOrchestrator *-- ExportService BiergartenPipelineOrchestrator *-- ExportService
BiergartenPipelineOrchestrator *-- CheckinDistributionStrategy BiergartenPipelineOrchestrator *-- CheckinDistributionStrategy
BiergartenPipelineOrchestrator *-- FollowGenerationStrategy
BiergartenPipelineOrchestrator *-- SamplingStrategy BiergartenPipelineOrchestrator *-- SamplingStrategy
BiergartenPipelineOrchestrator *-- BeerSelectionStrategy BiergartenPipelineOrchestrator *-- BeerSelectionStrategy
BiergartenPipelineOrchestrator *-- ApplicationOptions BiergartenPipelineOrchestrator *-- ApplicationOptions
@@ -524,6 +506,7 @@ BiergartenPipelineOrchestrator *-- "0..*" GeneratedUser : user_pool_
BiergartenPipelineOrchestrator *-- "0..*" GeneratedBrewery : brewery_pool_ BiergartenPipelineOrchestrator *-- "0..*" GeneratedBrewery : brewery_pool_
BiergartenPipelineOrchestrator *-- "0..*" GeneratedBeer : beer_pool_ BiergartenPipelineOrchestrator *-- "0..*" GeneratedBeer : beer_pool_
BiergartenPipelineOrchestrator *-- "0..*" GeneratedCheckin : checkin_pool_ BiergartenPipelineOrchestrator *-- "0..*" GeneratedCheckin : checkin_pool_
BiergartenPipelineOrchestrator *-- "0..*" GeneratedFollow : follow_pool_
' --- Interfaces & Implementations --- ' --- Interfaces & Implementations ---
DataPreloader <|.. JsonLoader DataPreloader <|.. JsonLoader
@@ -533,8 +516,11 @@ ContextStrategy <|.. BeerContextStrategy
SamplingStrategy <|.. UniformSamplingStrategy SamplingStrategy <|.. UniformSamplingStrategy
BeerSelectionStrategy <|.. RandomBeerSelectionStrategy BeerSelectionStrategy <|.. RandomBeerSelectionStrategy
CheckinDistributionStrategy <|.. JCurveCheckinStrategy CheckinDistributionStrategy <|.. JCurveCheckinStrategy
CheckinDistributionStrategy <|.. RandomCheckinStrategy
FollowGenerationStrategy <|.. RandomFollowStrategy
FollowGenerationStrategy <|.. ActivityWeightedFollowStrategy
EnrichmentService <|.. WikipediaService EnrichmentService <|.. WikipediaService
WebClient <|.. CURLWebClient WebClient <|.. HttpWebClient
DataGenerator <|.. MockGenerator DataGenerator <|.. MockGenerator
DataGenerator <|.. LlamaGenerator DataGenerator <|.. LlamaGenerator
PromptFormatter <|.. Gemma4JinjaPromptFormatter PromptFormatter <|.. Gemma4JinjaPromptFormatter
@@ -557,12 +543,18 @@ EnrichedCity *-- Location
EnrichedCity *-- LocationContext EnrichedCity *-- LocationContext
GeneratedBrewery *-- Location GeneratedBrewery *-- Location
GeneratedBrewery *-- BreweryResult GeneratedBrewery *-- BreweryResult
GeneratedBrewery *-- GenerationMetadata
GeneratedBeer *-- Location GeneratedBeer *-- Location
GeneratedBeer *-- BeerStyle GeneratedBeer *-- BeerStyle
GeneratedBeer *-- BeerResult GeneratedBeer *-- BeerResult
GeneratedBeer *-- GenerationMetadata
GeneratedUser *-- Location GeneratedUser *-- Location
GeneratedUser *-- UserResult GeneratedUser *-- UserResult
GeneratedUser *-- GenerationMetadata
GeneratedCheckin *-- CheckinResult GeneratedCheckin *-- CheckinResult
GeneratedCheckin *-- GenerationMetadata
GeneratedRating *-- RatingResult GeneratedRating *-- RatingResult
GeneratedRating *-- GenerationMetadata
GeneratedFollow *-- GenerationMetadata
@enduml @enduml

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,7 +1,7 @@
# Docker Guide # Docker Guide
This document covers Docker deployment, configuration, and troubleshooting for The This document covers Docker deployment, configuration, and troubleshooting for
Biergarten App. The Biergarten App.
## Overview ## Overview
@@ -13,7 +13,8 @@ The project uses Docker Compose to orchestrate multiple services:
- .NET API - .NET API
- Test runners - Test runners
See the [deployment diagram](diagrams/pdf/deployment.pdf) for visual representation. See the [deployment diagram](diagrams/pdf/deployment.pdf) for visual
representation.
## Docker Compose Environments ## Docker Compose Environments
@@ -144,7 +145,11 @@ api.core / tests (start when ready)
```yaml ```yaml
healthcheck: healthcheck:
test: ['CMD-SHELL', "sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1'"] test:
[
"CMD-SHELL",
"sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1'",
]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 12 retries: 12
@@ -209,16 +214,16 @@ Each environment uses isolated bridge networks:
All containers are configured via environment variables from `.env` files: All containers are configured via environment variables from `.env` files:
```yaml ```yaml
env_file: '.env.dev' # or .env.test, .env.prod env_file: ".env.dev" # or .env.test, .env.prod
environment: environment:
ASPNETCORE_ENVIRONMENT: 'Development' ASPNETCORE_ENVIRONMENT: "Development"
DOTNET_RUNNING_IN_CONTAINER: 'true' DOTNET_RUNNING_IN_CONTAINER: "true"
DB_SERVER: '${DB_SERVER}' DB_SERVER: "${DB_SERVER}"
DB_NAME: '${DB_NAME}' DB_NAME: "${DB_NAME}"
DB_USER: '${DB_USER}' DB_USER: "${DB_USER}"
DB_PASSWORD: '${DB_PASSWORD}' DB_PASSWORD: "${DB_PASSWORD}"
JWT_SECRET: '${JWT_SECRET}' JWT_SECRET: "${JWT_SECRET}"
``` ```
For complete list, see [Environment Variables](environment-variables.md). For complete list, see [Environment Variables](environment-variables.md).

View File

@@ -1,7 +1,7 @@
# Environment Variables # Environment Variables
This document covers the active environment variables used by the current Biergarten This document covers the active environment variables used by the current
stack. Biergarten stack.
## Overview ## Overview
@@ -19,8 +19,8 @@ Direct environment variable access via `Environment.GetEnvironmentVariable()`.
### Frontend (`src/Website`) ### Frontend (`src/Website`)
The active website reads runtime values from the server environment for its auth and API The active website reads runtime values from the server environment for its auth
integration. and API integration.
### Docker ### Docker
@@ -54,14 +54,15 @@ Provide complete connection string:
DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;" DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;"
``` ```
**Priority**: `DB_CONNECTION_STRING` is checked first. If not found, connection string is **Priority**: `DB_CONNECTION_STRING` is checked first. If not found, connection
built from components. string is built from components.
**Implementation**: See `DefaultSqlConnectionFactory.cs` **Implementation**: See `DefaultSqlConnectionFactory.cs`
### JWT Authentication Secrets (Backend) ### JWT Authentication Secrets (Backend)
The backend uses separate secrets for different token types to enable independent key rotation and validation isolation. The backend uses separate secrets for different token types to enable
independent key rotation and validation isolation.
```bash ```bash
# Access token secret (1-hour tokens) # Access token secret (1-hour tokens)
@@ -131,8 +132,8 @@ DOTNET_RUNNING_IN_CONTAINER=true # Flag for container execution
## Frontend Variables (`src/Website`) ## Frontend Variables (`src/Website`)
The active website does not use the old Next.js/Prisma environment model. Its core runtime The active website does not use the old Next.js/Prisma environment model. Its
variables are: core runtime variables are:
```bash ```bash
API_BASE_URL=http://localhost:8080 # Base URL for the .NET API API_BASE_URL=http://localhost:8080 # Base URL for the .NET API
@@ -208,9 +209,10 @@ cp .env.example .env.dev
## Legacy Frontend Variables ## Legacy Frontend Variables
Variables for the archived Next.js frontend (`src/Website-v1`) have been removed from this Variables for the archived Next.js frontend (`src/Website-v1`) have been removed
active reference. See [archive/legacy-website-v1.md](archive/legacy-website-v1.md) if you from this active reference. See
need the legacy Prisma, Cloudinary, Mapbox, or SparkPost notes. [archive/legacy-website-v1.md](archive/legacy-website-v1.md) if you need the
legacy Prisma, Cloudinary, Mapbox, or SparkPost notes.
**Docker Compose Mapping**: **Docker Compose Mapping**:
@@ -243,8 +245,8 @@ need the legacy Prisma, Cloudinary, Mapbox, or SparkPost notes.
| `MSSQL_PID` | | | ✓ | No | SQL Server edition | | `MSSQL_PID` | | | ✓ | No | SQL Server edition |
| `DOTNET_RUNNING_IN_CONTAINER` | ✓ | | ✓ | No | Container flag | | `DOTNET_RUNNING_IN_CONTAINER` | ✓ | | ✓ | No | Container flag |
\* Either `DB_CONNECTION_STRING` OR the component variables (`DB_SERVER`, `DB_NAME`, \* Either `DB_CONNECTION_STRING` OR the component variables (`DB_SERVER`,
`DB_USER`, `DB_PASSWORD`) must be provided. `DB_NAME`, `DB_USER`, `DB_PASSWORD`) must be provided.
## Validation ## Validation
@@ -258,8 +260,8 @@ Variables are validated at startup:
### Frontend Validation ### Frontend Validation
The active website relies on runtime defaults for local development and the surrounding The active website relies on runtime defaults for local development and the
server environment in deployed environments. surrounding server environment in deployed environments.
- `API_BASE_URL` defaults to `http://localhost:8080` - `API_BASE_URL` defaults to `http://localhost:8080`
- `SESSION_SECRET` falls back to a development-only local secret - `SESSION_SECRET` falls back to a development-only local secret

View File

@@ -1,7 +1,7 @@
# Getting Started # Getting Started
This guide covers local setup for the current Biergarten stack: the .NET backend in This guide covers local setup for the current Biergarten stack: the .NET backend
`src/Core` and the active React Router frontend in `src/Website`. in `src/Core` and the active React Router frontend in `src/Website`.
## Prerequisites ## Prerequisites
@@ -128,8 +128,9 @@ dotnet run --project API/API.Core/API.Core.csproj
## Legacy Frontend Note ## Legacy Frontend Note
The previous Next.js frontend now lives in `src/Website-v1` and is not the active website. The previous Next.js frontend now lives in `src/Website-v1` and is not the
Legacy setup details have been moved to [docs/archive/legacy-website-v1.md](archive/legacy-website-v1.md). active website. Legacy setup details have been moved to
[docs/archive/legacy-website-v1.md](archive/legacy-website-v1.md).
## Next Steps ## Next Steps

View File

@@ -1,6 +1,7 @@
# Testing # Testing
This document describes the testing strategy and how to run tests for The Biergarten App. This document describes the testing strategy and how to run tests for The
Biergarten App.
## Overview ## Overview
@@ -9,13 +10,15 @@ The project uses a multi-layered testing approach across backend and frontend:
- **API.Specs** - BDD integration tests using Reqnroll (Gherkin) - **API.Specs** - BDD integration tests using Reqnroll (Gherkin)
- **Infrastructure.Repository.Tests** - Unit tests for data access layer - **Infrastructure.Repository.Tests** - Unit tests for data access layer
- **Service.Auth.Tests** - Unit tests for authentication business logic - **Service.Auth.Tests** - Unit tests for authentication business logic
- **Storybook Vitest project** - Browser-based interaction tests for shared website stories - **Storybook Vitest project** - Browser-based interaction tests for shared
- **Storybook Playwright suite** - Browser checks against Storybook-rendered components website stories
- **Storybook Playwright suite** - Browser checks against Storybook-rendered
components
## Running Tests with Docker (Recommended) ## Running Tests with Docker (Recommended)
The easiest way to run all tests is using Docker Compose, which sets up an isolated test The easiest way to run all tests is using Docker Compose, which sets up an
environment: isolated test environment:
```bash ```bash
docker compose -f docker-compose.test.yaml up --abort-on-container-exit docker compose -f docker-compose.test.yaml up --abort-on-container-exit
@@ -98,7 +101,8 @@ npm run test:storybook
**Purpose**: **Purpose**:
- Verifies shared stories such as form fields, submit buttons, navbar states, toasts, and the theme gallery - Verifies shared stories such as form fields, submit buttons, navbar states,
toasts, and the theme gallery
- Runs in browser mode via Vitest and Storybook integration - Runs in browser mode via Vitest and Storybook integration
### Frontend Playwright Storybook Tests ### Frontend Playwright Storybook Tests
@@ -113,7 +117,8 @@ npm run test:storybook:playwright
- Storybook dependencies installed - Storybook dependencies installed
- Playwright browser dependencies installed - Playwright browser dependencies installed
- The command will start or reuse the Storybook server defined in `playwright.storybook.config.ts` - The command will start or reuse the Storybook server defined in
`playwright.storybook.config.ts`
## Test Coverage ## Test Coverage
@@ -278,7 +283,8 @@ Scenario: User login with valid credentials
## Continuous Integration ## Continuous Integration
Tests run automatically in CI/CD pipelines using the test Docker Compose configuration: Tests run automatically in CI/CD pipelines using the test Docker Compose
configuration:
```bash ```bash
# CI/CD command # CI/CD command
@@ -292,7 +298,8 @@ Exit codes:
- `0` - All tests passed - `0` - All tests passed
- Non-zero - Test failures occurred - Non-zero - Test failures occurred
Frontend UI checks should also be included in CI for the active website workspace: Frontend UI checks should also be included in CI for the active website
workspace:
```bash ```bash
cd src/Website cd src/Website

View File

@@ -2,11 +2,14 @@
## Overview ## Overview
The Core project implements comprehensive JWT token validation across three token types: The Core project implements comprehensive JWT token validation across three
token types:
- **Access Tokens**: Short-lived (1 hour) tokens for API authentication - **Access Tokens**: Short-lived (1 hour) tokens for API authentication
- **Refresh Tokens**: Long-lived (21 days) tokens for obtaining new access tokens - **Refresh Tokens**: Long-lived (21 days) tokens for obtaining new access
- **Confirmation Tokens**: Short-lived (30 minutes) tokens for email confirmation tokens
- **Confirmation Tokens**: Short-lived (30 minutes) tokens for email
confirmation
## Components ## Components
@@ -17,10 +20,13 @@ The Core project implements comprehensive JWT token validation across three toke
Low-level JWT operations. Low-level JWT operations.
**Methods:** **Methods:**
- `GenerateJwt()` - Creates signed JWT tokens - `GenerateJwt()` - Creates signed JWT tokens
- `ValidateJwtAsync()` - Validates token signature, expiration, and format - `ValidateJwtAsync()` - Validates token signature, expiration, and format
**Implementation:** [JwtInfrastructure.cs](Infrastructure.Jwt/JwtInfrastructure.cs) **Implementation:**
[JwtInfrastructure.cs](Infrastructure.Jwt/JwtInfrastructure.cs)
- Uses Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler - Uses Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler
- Algorithm: HS256 (HMAC-SHA256) - Algorithm: HS256 (HMAC-SHA256)
- Validates token lifetime, signature, and well-formedness - Validates token lifetime, signature, and well-formedness
@@ -32,16 +38,20 @@ Low-level JWT operations.
High-level token validation with context (token type, user extraction). High-level token validation with context (token type, user extraction).
**Methods:** **Methods:**
- `ValidateAccessTokenAsync(string token)` - Validates access tokens - `ValidateAccessTokenAsync(string token)` - Validates access tokens
- `ValidateRefreshTokenAsync(string token)` - Validates refresh tokens - `ValidateRefreshTokenAsync(string token)` - Validates refresh tokens
- `ValidateConfirmationTokenAsync(string token)` - Validates confirmation tokens - `ValidateConfirmationTokenAsync(string token)` - Validates confirmation tokens
**Returns:** `ValidatedToken` record containing: **Returns:** `ValidatedToken` record containing:
- `UserId` (Guid) - `UserId` (Guid)
- `Username` (string) - `Username` (string)
- `Principal` (ClaimsPrincipal) - Full JWT claims - `Principal` (ClaimsPrincipal) - Full JWT claims
**Implementation:** [TokenValidationService.cs](Service.Auth/TokenValidationService.cs) **Implementation:**
[TokenValidationService.cs](Service.Auth/TokenValidationService.cs)
- Reads token secrets from environment variables - Reads token secrets from environment variables
- Extracts and validates claims (Sub, UniqueName) - Extracts and validates claims (Sub, UniqueName)
- Throws `UnauthorizedException` on validation failure - Throws `UnauthorizedException` on validation failure
@@ -51,15 +61,18 @@ High-level token validation with context (token type, user extraction).
Token generation (existing service extended). Token generation (existing service extended).
**Methods:** **Methods:**
- `GenerateAccessToken(UserAccount)` - Creates 1-hour access token - `GenerateAccessToken(UserAccount)` - Creates 1-hour access token
- `GenerateRefreshToken(UserAccount)` - Creates 21-day refresh token - `GenerateRefreshToken(UserAccount)` - Creates 21-day refresh token
- `GenerateConfirmationToken(UserAccount)` - Creates 30-minute confirmation token - `GenerateConfirmationToken(UserAccount)` - Creates 30-minute confirmation
token
### Integration Points ### Integration Points
#### [ConfirmationService](Service.Auth/IConfirmationService.cs) #### [ConfirmationService](Service.Auth/IConfirmationService.cs)
**Flow:** **Flow:**
1. Receives confirmation token from user 1. Receives confirmation token from user
2. Calls `TokenValidationService.ValidateConfirmationTokenAsync()` 2. Calls `TokenValidationService.ValidateConfirmationTokenAsync()`
3. Extracts user ID from validated token 3. Extracts user ID from validated token
@@ -69,6 +82,7 @@ Token generation (existing service extended).
#### [RefreshTokenService](Service.Auth/RefreshTokenService.cs) #### [RefreshTokenService](Service.Auth/RefreshTokenService.cs)
**Flow:** **Flow:**
1. Receives refresh token from user 1. Receives refresh token from user
2. Calls `TokenValidationService.ValidateRefreshTokenAsync()` 2. Calls `TokenValidationService.ValidateRefreshTokenAsync()`
3. Retrieves user account via `AuthRepository.GetUserByIdAsync()` 3. Retrieves user account via `AuthRepository.GetUserByIdAsync()`
@@ -78,6 +92,7 @@ Token generation (existing service extended).
#### [AuthController](API.Core/Controllers/AuthController.cs) #### [AuthController](API.Core/Controllers/AuthController.cs)
**Endpoints:** **Endpoints:**
- `POST /api/auth/register` - Register new user - `POST /api/auth/register` - Register new user
- `POST /api/auth/login` - Authenticate user - `POST /api/auth/login` - Authenticate user
- `POST /api/auth/confirm?token=...` - Confirm email - `POST /api/auth/confirm?token=...` - Confirm email
@@ -88,11 +103,13 @@ Token generation (existing service extended).
### Token Secrets ### Token Secrets
Three independent secrets enable: Three independent secrets enable:
- **Key rotation** - Rotate each secret type independently - **Key rotation** - Rotate each secret type independently
- **Isolation** - Compromise of one secret doesn't affect others - **Isolation** - Compromise of one secret doesn't affect others
- **Different expiration** - Different token types can expire at different rates - **Different expiration** - Different token types can expire at different rates
**Environment Variables:** **Environment Variables:**
```bash ```bash
ACCESS_TOKEN_SECRET=... # Signs 1-hour access tokens ACCESS_TOKEN_SECRET=... # Signs 1-hour access tokens
REFRESH_TOKEN_SECRET=... # Signs 21-day refresh tokens REFRESH_TOKEN_SECRET=... # Signs 21-day refresh tokens
@@ -111,6 +128,7 @@ Each token is validated for:
### Error Handling ### Error Handling
Validation failures return HTTP 401 Unauthorized: Validation failures return HTTP 401 Unauthorized:
- Invalid signature → "Invalid token" - Invalid signature → "Invalid token"
- Expired token → "Invalid token" (message doesn't reveal reason for security) - Expired token → "Invalid token" (message doesn't reveal reason for security)
- Missing claims → "Invalid token" - Missing claims → "Invalid token"
@@ -149,16 +167,19 @@ Validation failures return HTTP 401 Unauthorized:
### Unit Tests ### Unit Tests
**TokenValidationService.test.cs** **TokenValidationService.test.cs**
- Happy path: Valid token extraction - Happy path: Valid token extraction
- Error cases: Invalid, expired, malformed tokens - Error cases: Invalid, expired, malformed tokens
- Missing/invalid claims scenarios - Missing/invalid claims scenarios
**RefreshTokenService.test.cs** **RefreshTokenService.test.cs**
- Successful refresh with valid token - Successful refresh with valid token
- Invalid/expired refresh token rejection - Invalid/expired refresh token rejection
- Non-existent user handling - Non-existent user handling
**ConfirmationService.test.cs** **ConfirmationService.test.cs**
- Successful confirmation with valid token - Successful confirmation with valid token
- Token validation failures - Token validation failures
- User not found scenarios - User not found scenarios
@@ -166,16 +187,19 @@ Validation failures return HTTP 401 Unauthorized:
### BDD Tests (Reqnroll) ### BDD Tests (Reqnroll)
**TokenRefresh.feature** **TokenRefresh.feature**
- Successful token refresh - Successful token refresh
- Invalid/expired token rejection - Invalid/expired token rejection
- Missing token validation - Missing token validation
**Confirmation.feature** **Confirmation.feature**
- Successful email confirmation - Successful email confirmation
- Expired/tampered token rejection - Expired/tampered token rejection
- Missing token validation - Missing token validation
**AccessTokenValidation.feature** **AccessTokenValidation.feature**
- Protected endpoint access token validation - Protected endpoint access token validation
- Invalid/expired access token rejection - Invalid/expired access token rejection
- Token type mismatch (refresh used as access token) - Token type mismatch (refresh used as access token)

File diff suppressed because it is too large Load Diff

View File

@@ -1,559 +0,0 @@
,name,city,state
0,NorthGate Brewing ,Minneapolis, MN
1,Against the Grain Brewery,Louisville, KY
2,Jack's Abby Craft Lagers,Framingham, MA
3,Mike Hess Brewing Company,San Diego, CA
4,Fort Point Beer Company,San Francisco, CA
5,COAST Brewing Company,Charleston, SC
6,Great Divide Brewing Company,Denver, CO
7,Tapistry Brewing,Bridgman, MI
8,Big Lake Brewing,Holland, MI
9,The Mitten Brewing Company,Grand Rapids, MI
10,Brewery Vivant,Grand Rapids, MI
11,Petoskey Brewing,Petoskey, MI
12,Blackrocks Brewery,Marquette, MI
13,Perrin Brewing Company,Comstock Park, MI
14,Witch's Hat Brewing Company,South Lyon, MI
15,Founders Brewing Company,Grand Rapids, MI
16,Flat 12 Bierwerks,Indianapolis, IN
17,Tin Man Brewing Company,Evansville, IN
18,Black Acre Brewing Co.,Indianapolis, IN
19,Brew Link Brewing,Plainfield, IN
20,Bare Hands Brewery,Granger, IN
21,Three Pints Brewing,Martinsville, IN
22,Four Fathers Brewing ,Valparaiso, IN
23,Indiana City Brewing,Indianapolis, IN
24,Burn 'Em Brewing,Michigan City, IN
25,Sun King Brewing Company,Indianapolis, IN
26,Evil Czech Brewery,Mishawaka, IN
27,450 North Brewing Company,Columbus, IN
28,Taxman Brewing Company,Bargersville, IN
29,Cedar Creek Brewery,Seven Points, TX
30,SanTan Brewing Company,Chandler, AZ
31,Boulevard Brewing Company,Kansas City, MO
32,James Page Brewing Company,Stevens Point, WI
33,The Dudes' Brewing Company,Torrance, CA
34,Ballast Point Brewing Company,San Diego, CA
35,Anchor Brewing Company,San Francisco, CA
36,Figueroa Mountain Brewing Company,Buellton, CA
37,Avery Brewing Company,Boulder, CO
38,Twisted X Brewing Company,Dripping Springs, TX
39,Gonzo's BiggDogg Brewing,Kalamazoo, MI
40,Big Muddy Brewing,Murphysboro, IL
41,Lost Nation Brewing,East Fairfield, VT
42,Rising Tide Brewing Company,Portland, ME
43,Rivertowne Brewing Company,Export, PA
44,Revolution Brewing Company,Chicago, IL
45,Tallgrass Brewing Company,Manhattan, KS
46,Sixpoint Craft Ales,Brooklyn, NY
47,White Birch Brewing,Hooksett, NH
48,Firestone Walker Brewing Company,Paso Robles, CA
49,SweetWater Brewing Company,Atlanta, GA
50,Flying Mouse Brewery,Troutville, VA
51,Upslope Brewing Company,Boulder, CO
52,Pipeworks Brewing Company,Chicago, IL
53,Bent Brewstillery,Roseville, MN
54,Flesk Brewing Company,Lombard, IL
55,Pollyanna Brewing Company,Lemont, IL
56,BuckleDown Brewing,Lyons, IL
57,Destihl Brewery,Bloomington, IL
58,Summit Brewing Company,St. Paul, MN
59,Latitude 42 Brewing Company,Portage, MI
60,4 Hands Brewing Company,Saint Louis, MO
61,Surly Brewing Company,Brooklyn Center, MN
62,Against The Grain Brewery,Louisville, KY
63,Crazy Mountain Brewing Company,Edwards, CO
64,SlapShot Brewing Company,Chicago, IL
65,Mikerphone Brewing,Chicago, IL
66,Freetail Brewing Company,San Antonio, TX
67,3 Daughters Brewing,St Petersburg, FL
68,Red Shedman Farm Brewery and Hop...,Mt. Airy, MD
69,Appalachian Mountain Brewery,Boone, NC
70,Birdsong Brewing Company,Charlotte, NC
71,Union Craft Brewing,Baltimore, MD
72,Atwater Brewery,Detroit, MI
73,Ale Asylum,Madison, WI
74,Two Brothers Brewing Company,Warrenville, IL
75,Bent Paddle Brewing Company,Duluth, MN
76,Bell's Brewery,Kalamazoo, MI
77,Blue Owl Brewing,Austin, TX
78,Speakasy Ales & Lagers,San Francisco, CA
79,Black Tooth Brewing Company,Sheridan, WY
80,Hopworks Urban Brewery,Portland, OR
81,Epic Brewing,Denver, CO
82,New Belgium Brewing Company,Fort Collins, CO
83,Sierra Nevada Brewing Company,Chico, CA
84,Keweenaw Brewing Company,Houghton, MI
85,Brewery Terra Firma,Traverse City, MI
86,Grey Sail Brewing Company,Westerly, RI
87,Kirkwood Station Brewing Company,Kirkwood, MO
88,Goose Island Brewing Company,Chicago, IL
89,Broad Brook Brewing LLC,East Windsor, CT
90,The Lion Brewery,Wilkes-Barre, PA
91,Madtree Brewing Company,Cincinnati, OH
92,Jackie O's Pub & Brewery,Athens, OH
93,Rhinegeist Brewery,Cincinnati, OH
94,Warped Wing Brewing Company,Dayton, OH
95,Blackrocks Brewery,Marquette, MA
96,Catawba Valley Brewing Company,Morganton, NC
97,Tröegs Brewing Company,Hershey, PA
98,Mission Brewery,San Diego, CA
99,Christian Moerlein Brewing Company,Cincinnati, OH
100,West Sixth Brewing,Lexington, KY
101,Coastal Extreme Brewing Company,Newport, RI
102,King Street Brewing Company,Anchorage, AK
103,Beer Works Brewery,Lowell, MA
104,Lone Tree Brewing Company,Lone Tree, CO
105,Four String Brewing Company,Columbus, OH
106,Glabrous Brewing Company,Pineland, ME
107,Bonfire Brewing Company,Eagle, CO
108,Thomas Hooker Brewing Company,Bloomfield, CT
109,"Woodstock Inn, Station & Brewery",North Woodstock, NH
110,Renegade Brewing Company,Denver, CO
111,Mother Earth Brew Company,Vista, CA
112,Black Market Brewing Company,Temecula, CA
113,Vault Brewing Company,Yardley, PA
114,Jailbreak Brewing Company,Laurel, MD
115,Smartmouth Brewing Company,Norfolk, VA
116,Base Camp Brewing Co.,Portland, OR
117,Alameda Brewing,Portland, OR
118,Southern Star Brewing Company,Conroe, TX
119,Steamworks Brewing Company,Durango, CO
120,Horny Goat Brew Pub,Milwaukee, WI
121,Cheboygan Brewing Company,Cheboygan, MI
122,Center of the Universe Brewing C...,Ashland, VA
123,Ipswich Ale Brewery,Ipswich, MA
124,Griffin Claw Brewing Company,Birmingham, MI
125,Karbach Brewing Company,Houston, TX
126,Uncle Billy's Brewery and Smokeh...,Austin, TX
127,Deep Ellum Brewing Company,Dallas, TX
128,Real Ale Brewing Company,Blanco, TX
129,Straub Brewery,St Mary's, PA
130,Shebeen Brewing Company,Wolcott, CT
131,Stevens Point Brewery,Stevens Point, WI
132,Weston Brewing Company,Weston, MO
133,Southern Prohibition Brewing Com...,Hattiesburg, MS
134,Minhas Craft Brewery,Monroe, WI
135,Pug Ryan's Brewery,Dillon, CO
136,Hops & Grains Brewing Company,Austin, TX
137,Sietsema Orchards and Cider Mill,Ada, MI
138,Summit Brewing Company,St Paul, MN
139,Core Brewing & Distilling Company,Springdale, AR
140,Independence Brewing Company,Austin, TX
141,Cigar City Brewing Company,Tampa, FL
142,Third Street Brewhouse,Cold Spring, MN
143,Narragansett Brewing Company,Providence, RI
144,Grimm Brothers Brewhouse,Loveland, CO
145,Cisco Brewers,Nantucket, MA
146,Angry Minnow,Hayward, WI
147,Platform Beer Company,Cleveland, OH
148,Odyssey Beerwerks,Arvada, CO
149,Lonerider Brewing Company,Raleigh, NC
150,Oakshire Brewing,Eugene, OR
151,Fort Pitt Brewing Company,Latrobe, PA
152,Tin Roof Brewing Company,Baton Rouge, LA
153,Three Creeks Brewing,Sisters, OR
154,2 Towns Ciderhouse,Corvallis, OR
155,Caldera Brewing Company,Ashland, OR
156,Greenbrier Valley Brewing Company,Lewisburg, WV
157,Phoenix Ale Brewery,Phoenix, AZ
158,Lumberyard Brewing Company,Flagstaff, AZ
159,Uinta Brewing Company,Salt Lake City, UT
160,Four Peaks Brewing Company,Tempe, AZ
161,Martin House Brewing Company,Fort Worth, TX
162,Right Brain Brewery,Traverse City, MI
163,Sly Fox Brewing Company,Phoenixville, PA
164,Round Guys Brewing,Lansdale, PA
165,Great Crescent Brewery,Aurora, IN
166,Oskar Blues Brewery,Longmont, CO
167,Boxcar Brewing Company,West Chester, PA
168,High Hops Brewery,Windsor, CO
169,Crooked Fence Brewing Company,Garden City, ID
170,Everybody's Brewing,White Salmon, WA
171,Anderson Valley Brewing Company,Boonville, CA
172,Fiddlehead Brewing Company,Shelburne, VT
173,Evil Twin Brewing,Brooklyn, NY
174,New Orleans Lager & Ale Brewing ...,New Orleans, LA
175,Spiteful Brewing Company,Chicago, IL
176,Rahr & Sons Brewing Company,Fort Worth, TX
177,18th Street Brewery,Gary, IN
178,Cambridge Brewing Company,Cambridge, MA
179,Carolina Brewery,Pittsboro, NC
180,Frog Level Brewing Company,Waynesville, NC
181,Wild Wolf Brewing Company,Nellysford, VA
182,COOP Ale Works,Oklahoma City, OK
183,Seventh Son Brewing Company,Columbus, OH
184,Oasis Texas Brewing Company,Austin, TX
185,Vander Mill Ciders,Spring Lake, MI
186,St. Julian Winery,Paw Paw, MI
187,Pedernales Brewing Company,Fredericksburg, TX
188,Mother's Brewing,Springfield, MO
189,Modern Monks Brewery,Lincoln, NE
190,Two Beers Brewing Company,Seattle, WA
191,Snake River Brewing Company,Jackson, WY
192,Capital Brewery,Middleton, WI
193,Anthem Brewing Company,Oklahoma City, OK
194,Goodlife Brewing Co.,Bend, OR
195,Breakside Brewery,Portland, OR
196,Goose Island Brewery Company,Chicago, IL
197,Burnside Brewing Co.,Portland, OR
198,Hop Valley Brewing Company,Springfield, OR
199,Worthy Brewing Company,Bend, OR
200,Occidental Brewing Company,Portland, OR
201,Fearless Brewing Company,Estacada, OR
202,Upland Brewing Company,Bloomington, IN
203,Mehana Brewing Co.,Hilo, HI
204,Hawai'i Nui Brewing Co.,Hilo, HI
205,People's Brewing Company,Lafayette, IN
206,Fort George Brewery,Astoria, OR
207,Branchline Brewing Company,San Antonio, TX
208,Kalona Brewing Company,Kalona, IA
209,Modern Times Beer,San Diego, CA
210,Temperance Beer Company,Evanston, IL
211,Wisconsin Brewing Company,Verona, WI
212,Crow Peak Brewing Company,Spearfish, SD
213,Grapevine Craft Brewery,Farmers Branch, TX
214,Buffalo Bayou Brewing Company,Houston, TX
215,Texian Brewing Co.,Richmond, TX
216,Orpheus Brewing,Atlanta, GA
217,Forgotten Boardwalk,Cherry Hill, NJ
218,Laughing Dog Brewing Company,Ponderay, ID
219,Bozeman Brewing Company,Bozeman, MT
220,Big Choice Brewing,Broomfield, CO
221,Big Storm Brewing Company,Odessa, FL
222,Carton Brewing Company,Atlantic Highlands, NJ
223,Midnight Sun Brewing Company,Anchorage, AK
224,Fat Head's Brewery,Middleburg Heights, OH
225,Refuge Brewery,Temecula, CA
226,Chatham Brewing,Chatham, NY
227,DC Brau Brewing Company,Washington, DC
228,Geneva Lake Brewing Company,Lake Geneva, WI
229,Rochester Mills Brewing Company,Rochester, MI
230,Cape Ann Brewing Company,Gloucester, MA
231,Borderlands Brewing Company,Tucson, AZ
232,College Street Brewhouse and Pub,Lake Havasu City, AZ
233,Joseph James Brewing Company,Henderson, NV
234,Harpoon Brewery,Boston, MA
235,Back East Brewing Company,Bloomfield, CT
236,Champion Brewing Company,Charlottesville, VA
237,Devil's Backbone Brewing Company,Lexington, VA
238,Newburgh Brewing Company,Newburgh, NY
239,Wiseacre Brewing Company,Memphis, TN
240,Golden Road Brewing,Los Angeles, CA
241,New Republic Brewing Company,College Station, TX
242,Infamous Brewing Company,Austin, TX
243,Two Henrys Brewing Company,Plant City, FL
244,Lift Bridge Brewing Company,Stillwater, MN
245,Lucky Town Brewing Company,Jackson, MS
246,Quest Brewing Company,Greenville, SC
247,Creature Comforts,Athens, GA
248,Half Full Brewery,Stamford, CT
249,Southampton Publick House,Southampton, NY
250,Chapman's Brewing,Angola, IN
251,Barrio Brewing Company,Tucson, AZ
252,Santa Cruz Mountain Brewing,Santa Cruz, CA
253,Frankenmuth Brewery,Frankenmuth, MI
254,Meckley's Cidery,Somerset Center, MI
255,Stillwater Artisanal Ales,Baltimore, MD
256,Finch's Beer Company,Chicago, IL
257,South Austin Brewery,South Austin, TX
258,Bauhaus Brew Labs,Minneapolis, MN
259,Ozark Beer Company,Rogers, AR
260,Mountain Town Brewing Company ,Mount Pleasant, MI
261,Otter Creek Brewing,Waterbury, VT
262,The Brewer's Art,Baltimore, MD
263,Denver Beer Company,Denver, CO
264,Ska Brewing Company,Durango, CO
265,Tractor Brewing Company,Albuquerque, NM
266,Peak Organic Brewing Company,Portland, ME
267,Cape Cod Beer,Hyannis, MA
268,Long Trail Brewing Company,Bridgewater Corners, VT
269,Great Raft Brewing Company,Shreveport, LA
270,Alaskan Brewing Company,Juneau, AK
271,Notch Brewing Company,Ipswich, MA
272,The Alchemist,Waterbury, VT
273,Three Notch'd Brewing Company,Charlottesville, VA
274,Portside Brewery,Cleveland, OH
275,Otter Creek Brewing,Middlebury, VT
276,Montauk Brewing Company,Montauk, NY
277,Indeed Brewing Company,Minneapolis, MN
278,Berkshire Brewing Company,South Deerfield, MA
279,Foolproof Brewing Company,Pawtucket, RI
280,Headlands Brewing Company,Mill Valley, CA
281,Bolero Snort Brewery,Ridgefield Park, NJ
282,Thunderhead Brewing Company,Kearney, NE
283,Defiance Brewing Company,Hays, KS
284,Milwaukee Brewing Company,Milwaukee, WI
285,Catawba Island Brewing,Port Clinton, OH
286,Back Forty Beer Company,Gadsden, AL
287,Four Corners Brewing Company,Dallas, TX
288,Saint Archer Brewery,San Diego, CA
289,Rogue Ales,Newport, OR
290,Hale's Ales,Seattle, WA
291,Tommyknocker Brewery,Idaho Springs, CO
292,Baxter Brewing Company,Lewiston, ME
293,Northampton Brewery,Northamtpon, MA
294,Black Shirt Brewing Company,Denver, CO
295,Wachusett Brewing Company,Westminster, MA
296,Widmer Brothers Brewing Company,Portland, OR
297,Hop Farm Brewing Company,Pittsburgh, PA
298,Liquid Hero Brewery,York, PA
299,Matt Brewing Company,Utica, NY
300,Boston Beer Company,Boston, MA
301,Old Forge Brewing Company,Danville, PA
302,Utah Brewers Cooperative,Salt Lake City, UT
303,Magic Hat Brewing Company,South Burlington, VT
304,Blue Hills Brewery,Canton, MA
305,Night Shift Brewing,Everett, MA
306,Beach Brewing Company,Virginia Beach, VA
307,Payette Brewing Company,Garden City, ID
308,Brew Bus Brewing,Tampa, FL
309,Sockeye Brewing Company,Boise, ID
310,Pine Street Brewery,San Francisco, CA
311,Dirty Bucket Brewing Company,Woodinville, WA
312,Jackalope Brewing Company,Nashville, TN
313,Slanted Rock Brewing Company,Meridian, ID
314,Piney River Brewing Company,Bucryus, MO
315,Cutters Brewing Company,Avon, IN
316,Iron Hill Brewery & Restaurant,Wilmington, DE
317,Marshall Wharf Brewing Company,Belfast, ME
318,Banner Beer Company,Williamsburg, MA
319,Dick's Brewing Company,Centralia, WA
320,Claremont Craft Ales,Claremont, CA
321,Rivertown Brewing Company,Lockland, OH
322,Voodoo Brewery,Meadville, PA
323,D.L. Geary Brewing Company,Portland, ME
324,Pisgah Brewing Company,Black Mountain, NC
325,Neshaminy Creek Brewing Company,Croydon, PA
326,Morgan Street Brewery,Saint Louis, MO
327,Half Acre Beer Company,Chicago, IL
328,The Just Beer Project,Burlington, VT
329,The Bronx Brewery,Bronx, NY
330,Dead Armadillo Craft Brewing,Tulsa, OK
331,Catawba Brewing Company,Morganton, NC
332,La Cumbre Brewing Company,Albuquerque, NM
333,David's Ale Works,Diamond Springs, CA
334,The Traveler Beer Company,Burlington, VT
335,Fargo Brewing Company,Fargo, ND
336,Big Sky Brewing Company,Missoula, MT
337,Nebraska Brewing Company,Papillion, NE
338,Uncle John's Fruit House Winery,St. John's, MI
339,Wormtown Brewery,Worcester, MA
340,Due South Brewing Company,Boynton Beach, FL
341,Palisade Brewing Company,Palisade, CO
342,KelSo Beer Company,Brooklyn, NY
343,Hardywood Park Craft Brewery,Richmond, VA
344,Wolf Hills Brewing Company,Abingdon, VA
345,Lavery Brewing Company,Erie, PA
346,Manzanita Brewing Company,Santee, CA
347,Fullsteam Brewery,Durham, NC
348,Four Horsemen Brewing Company,South Bend, IN
349,Hinterland Brewery,Green Bay, WI
350,Central Coast Brewing Company,San Luis Obispo, CA
351,Westfield River Brewing Company,Westfield, MA
352,Elevator Brewing Company,Columbus, OH
353,Aslan Brewing Company,Bellingham, WA
354,Kulshan Brewery,Bellingham, WA
355,Pikes Peak Brewing Company,Monument, CO
356,Manayunk Brewing Company,Philadelphia, PA
357,Buckeye Brewing,Cleveland, OH
358,Daredevil Brewing Company,Shelbyville, IN
359,NoDa Brewing Company,Charlotte, NC
360,Aviator Brewing Company,Fuquay-Varina, NC
361,Wild Onion Brewing Company,Lake Barrington, IL
362,Hilliard's Beer,Seattle, WA
363,Mikkeller,Pottstown, PA
364,Bohemian Brewery,Midvale, UT
365,Great River Brewery,Davenport, IA
366,Mustang Brewing Company,Mustang, OK
367,Airways Brewing Company,Kent, WA
368,21st Amendment Brewery,San Francisco, CA
369,Eddyline Brewery & Restaurant,Buena Vista, CO
370,Pizza Port Brewing Company,Carlsbad, CA
371,Sly Fox Brewing Company,Pottstown, PA
372,Spring House Brewing Company,Conestoga, PA
373,7venth Sun,Dunedin, FL
374,Astoria Brewing Company,Astoria, OR
375,Maui Brewing Company,Lahaina, HI
376,RoughTail Brewing Company,Midwest City, OK
377,Lucette Brewing Company,Menominee, WI
378,Bold City Brewery,Jacksonville, FL
379,Grey Sail Brewing of Rhode Island,Westerly, RI
380,Blue Blood Brewing Company,Lincoln, NE
381,Swashbuckler Brewing Company,Manheim, PA
382,Blue Mountain Brewery,Afton, VA
383,Starr Hill Brewery,Crozet, VA
384,Westbrook Brewing Company,Mt. Pleasant, SC
385,Shipyard Brewing Company,Portland, ME
386,Revolution Brewing,Paonia, CO
387,Natian Brewery,Portland, OR
388,Alltech's Lexington Brewing Company,Lexington, KY
389,Oskar Blues Brewery (North Carol...,Brevard, NC
390,Orlison Brewing Company,Airway Heights, WA
391,Breckenridge Brewery,Denver, CO
392,Santa Fe Brewing Company,Santa Fe, NM
393,Miami Brewing Company,Miami, FL
394,Schilling & Company,Seattle, WA
395,Hops & Grain Brewery,Austin, TX
396,White Flame Brewing Company,Hudsonville, MI
397,Ruhstaller Beer Company,Sacramento, CA
398,Saugatuck Brewing Company,Douglas, MI
399,Moab Brewery,Moab, UT
400,Macon Beer Company,Macon, GA
401,Amnesia Brewing Company,Washougal, WA
402,Wolverine State Brewing Company,Ann Arbor, MI
403,Red Tank Cider Company,Bend, OR
404,Cascadia Ciderworks United,Portland, OR
405,Fate Brewing Company,Boulder, CO
406,Lazy Monk Brewing,Eau Claire, WI
407,Bitter Root Brewing,Hamilton, MT
408,10 Barrel Brewing Company,Bend, OR
409,Tamarack Brewing Company,Lakeside, MT
410,New England Brewing Company,Woodbridge, CT
411,Seattle Cider Company,Seattle, WA
412,Straight to Ale,Huntsville, AL
413,Austin Beerworks,Austin, TX
414,Blue Mountain Brewery,Arrington, VA
415,Coastal Empire Beer Company,Savannah, GA
416,Jack's Hard Cider (Hauser Estate...,Biglerville, PA
417,Boulder Beer Company,Boulder, CO
418,Coalition Brewing Company,Portland, OR
419,Sanitas Brewing Company,Boulder, CO
420,Gore Range Brewery,Edwards, CO
421,Redstone Meadery,Boulder, CO
422,Blue Dog Mead,Eugene, OR
423,Hess Brewing Company,San Diego, CA
424,Wynkoop Brewing Company,Denver, CO
425,Ciderboys,Stevens Point, WI
426,Armadillo Ale Works,Denton, TX
427,Roanoke Railhouse Brewery,Roanoke, VA
428,Schlafly Brewing Company,Saint Louis, MO
429,Asher Brewing Company,Boulder, CO
430,Lost Rhino Brewing Company,Ashburn, VA
431,North Country Brewing Company,Slippery Rock, PA
432,Seabright Brewery,Santa Cruz, CA
433,French Broad Brewery,Asheville, NC
434,Angry Orchard Cider Company,Cincinnati, OH
435,Two Roads Brewing Company,Stratford, CT
436,Southern Oregon Brewing Company,Medford, OR
437,Brooklyn Brewery,Brooklyn, NY
438,The Right Brain Brewery,Traverse City, MI
439,Kona Brewing Company,Kona, HI
440,MillKing It Productions,Royal Oak, MI
441,Pateros Creek Brewing Company,Fort Collins, CO
442,O'Fallon Brewery,O'Fallon, MO
443,Marble Brewery,Albuquerque, NM
444,Big Wood Brewery,Vadnais Heights, MN
445,Howard Brewing Company,Lenoir, NC
446,Downeast Cider House,Leominster, MA
447,Swamp Head Brewery,Gainesville, FL
448,Mavericks Beer Company,Half Moon Bay, CA
449,TailGate Beer,San Diego, CA
450,Northwest Brewing Company,Pacific, WA
451,Dad & Dude's Breweria,Aurora, CO
452,Centennial Beer Company,Edwards, CO
453,Denali Brewing Company,Talkeetna, AK
454,Deschutes Brewery,Bend, OR
455,Sunken City Brewing Company,Hardy, VA
456,Lucette Brewing Company,Menominie, WI
457,The Black Tooth Brewing Company,Sheridan, WY
458,Kenai River Brewing Company,Soldotna, AK
459,River North Brewery,Denver, CO
460,Fremont Brewing Company,Seattle, WA
461,Armstrong Brewing Company,South San Francisco, CA
462,AC Golden Brewing Company,Golden, CO
463,Big Bend Brewing Company,Alpine, TX
464,Good Life Brewing Company,Bend, OR
465,Engine 15 Brewing,Jacksonville Beach, FL
466,Green Room Brewing,Jacksonville, FL
467,Brindle Dog Brewing Company,Tampa Bay, FL
468,Peace Tree Brewing Company,Knoxville, IA
469,Terrapin Brewing Company,Athens, GA
470,Pete's Brewing Company,San Antonio, TX
471,Okoboji Brewing Company,Spirit Lake, IA
472,Crystal Springs Brewing Company,Boulder, CO
473,Engine House 9,Tacoma, WA
474,Tonka Beer Company,Minnetonka, MN
475,Red Hare Brewing Company,Marietta, GA
476,Hangar 24 Craft Brewery,Redlands, CA
477,Big Elm Brewing,Sheffield, MA
478,Good People Brewing Company,Birmingham, AL
479,Heavy Seas Beer,Halethorpe, MD
480,Telluride Brewing Company,Telluride, CO
481,7 Seas Brewing Company,Gig Harbor, WA
482,Confluence Brewing Company,Des Moines, IA
483,Bale Breaker Brewing Company,Yakima, WA
484,The Manhattan Brewing Company,New York, NY
485,MacTarnahans Brewing Company,Portland, OR
486,Stillmank Beer Company,Green Bay, WI
487,Redhook Brewery,Woodinville, WA
488,Dock Street Brewery,Philadelphia, PA
489,Blue Point Brewing Company,Patchogue, NY
490,Tampa Bay Brewing Company,Tampa, FL
491,Devil's Canyon Brewery,Belmont, CA
492,Stone Coast Brewing Company,Portland, ME
493,Broken Tooth Brewing Company,Anchorage, AK
494,Seven Brides Brewery,Silverton, OR
495,Newburyport Brewing Company,Newburyport, MA
496,Dry Dock Brewing Company,Aurora, CO
497,Cans Bar and Canteen,Charlotte, NC
498,Sprecher Brewing Company,Glendale, WI
499,Wildwood Brewing Company,Stevensville, MT
500,High Noon Saloon And Brewery,Leavenworth, KS
501,Woodchuck Hard Cider,Middlebury, VT
502,Sea Dog Brewing Company,Portland, ME
503,Oskar Blues Brewery,Lyons, CO
504,Carolina Beer & Beverage,Mooresville, NC
505,Krebs Brewing Company (Pete's Pl...,Krebs, OK
506,Warbird Brewing Company,Fort Wayne, IN
507,Mudshark Brewing Company,Lake Havasu City, AZ
508,Spilker Ales,Cortland, NE
509,Wingman Brewers,Tacoma, WA
510,Kettle House Brewing Company,Missoula, MT
511,Sherwood Forest Brewers,Marlborough, MA
512,Cottrell Brewing,Pawcatuck, CT
513,Arctic Craft Brewery,Colorado Springs, CO
514,Monkey Paw Pub & Brewery,San Diego, CA
515,Crabtree Brewing Company,Greeley, CO
516,Emerald City Beer Company,Seattle, WA
517,Butcher's Brewing,Carlsbad, CA
518,New South Brewing Company,Myrtle Beach, SC
519,Big River Brewing Company,Chattanooga, TN
520,Twisted Pine Brewing Company,Boulder, CO
521,Flying Dog Brewery,Frederick, MD
522,Uncommon Brewers,Santa Cruz, CA
523,Aspen Brewing Company,Aspen, CO
524,Triangle Brewing Company,Durham, NC
525,Bomb Beer Company,New York, NY
526,Churchkey Can Company,Seattle, WA
527,Intuition Ale Works,Jacksonville, FL
528,Asheville Brewing Company,Asheville, NC
529,Northwoods Brewpub,Eau Claire, WI
530,Buckbean Brewing Company,Reno, NV
531,Dolores River Brewery,Dolores, CO
532,Flat Rock Brewing Company,Smithton, PA
533,Abita Brewing Company,Abita Springs, LA
534,Mammoth Brewing Company,Mammoth Lakes, CA
535,Harvest Moon Brewing Company,Belt, MT
536,Grand Canyon Brewing Company,Williams, AZ
537,Lewis and Clark Brewing Company,Helena, MT
538,Dundee Brewing Company,Rochester, NY
539,Twin Lakes Brewing Company,Greenville, DE
540,Mother Earth Brewing Company,Kinston, NC
541,Arcadia Brewing Company,Battle Creek, MI
542,Angry Minnow Brewing Company,Hayward, WI
543,Great Northern Brewing Company,Whitefish, MT
544,Pyramid Breweries,Seattle, WA
545,Lancaster Brewing Company,Lancaster, PA
546,Upstate Brewing Company,Elmira, NY
547,Moat Mountain Smoke House & Brew...,North Conway, NH
548,Prescott Brewing Company,Prescott, AZ
549,Mogollon Brewing Company,Flagstaff, AZ
550,Wind River Brewing Company,Pinedale, WY
551,Silverton Brewery,Silverton, CO
552,Mickey Finn's Brewery,Libertyville, IL
553,Covington Brewhouse,Covington, LA
554,Dave's Brewfarm,Wilson, WI
555,Ukiah Brewing Company,Ukiah, CA
556,Butternuts Beer and Ale,Garrattsville, NY
557,Sleeping Lady Brewing Company,Anchorage, AK
1 name city state
2 0 NorthGate Brewing Minneapolis MN
3 1 Against the Grain Brewery Louisville KY
4 2 Jack's Abby Craft Lagers Framingham MA
5 3 Mike Hess Brewing Company San Diego CA
6 4 Fort Point Beer Company San Francisco CA
7 5 COAST Brewing Company Charleston SC
8 6 Great Divide Brewing Company Denver CO
9 7 Tapistry Brewing Bridgman MI
10 8 Big Lake Brewing Holland MI
11 9 The Mitten Brewing Company Grand Rapids MI
12 10 Brewery Vivant Grand Rapids MI
13 11 Petoskey Brewing Petoskey MI
14 12 Blackrocks Brewery Marquette MI
15 13 Perrin Brewing Company Comstock Park MI
16 14 Witch's Hat Brewing Company South Lyon MI
17 15 Founders Brewing Company Grand Rapids MI
18 16 Flat 12 Bierwerks Indianapolis IN
19 17 Tin Man Brewing Company Evansville IN
20 18 Black Acre Brewing Co. Indianapolis IN
21 19 Brew Link Brewing Plainfield IN
22 20 Bare Hands Brewery Granger IN
23 21 Three Pints Brewing Martinsville IN
24 22 Four Fathers Brewing Valparaiso IN
25 23 Indiana City Brewing Indianapolis IN
26 24 Burn 'Em Brewing Michigan City IN
27 25 Sun King Brewing Company Indianapolis IN
28 26 Evil Czech Brewery Mishawaka IN
29 27 450 North Brewing Company Columbus IN
30 28 Taxman Brewing Company Bargersville IN
31 29 Cedar Creek Brewery Seven Points TX
32 30 SanTan Brewing Company Chandler AZ
33 31 Boulevard Brewing Company Kansas City MO
34 32 James Page Brewing Company Stevens Point WI
35 33 The Dudes' Brewing Company Torrance CA
36 34 Ballast Point Brewing Company San Diego CA
37 35 Anchor Brewing Company San Francisco CA
38 36 Figueroa Mountain Brewing Company Buellton CA
39 37 Avery Brewing Company Boulder CO
40 38 Twisted X Brewing Company Dripping Springs TX
41 39 Gonzo's BiggDogg Brewing Kalamazoo MI
42 40 Big Muddy Brewing Murphysboro IL
43 41 Lost Nation Brewing East Fairfield VT
44 42 Rising Tide Brewing Company Portland ME
45 43 Rivertowne Brewing Company Export PA
46 44 Revolution Brewing Company Chicago IL
47 45 Tallgrass Brewing Company Manhattan KS
48 46 Sixpoint Craft Ales Brooklyn NY
49 47 White Birch Brewing Hooksett NH
50 48 Firestone Walker Brewing Company Paso Robles CA
51 49 SweetWater Brewing Company Atlanta GA
52 50 Flying Mouse Brewery Troutville VA
53 51 Upslope Brewing Company Boulder CO
54 52 Pipeworks Brewing Company Chicago IL
55 53 Bent Brewstillery Roseville MN
56 54 Flesk Brewing Company Lombard IL
57 55 Pollyanna Brewing Company Lemont IL
58 56 BuckleDown Brewing Lyons IL
59 57 Destihl Brewery Bloomington IL
60 58 Summit Brewing Company St. Paul MN
61 59 Latitude 42 Brewing Company Portage MI
62 60 4 Hands Brewing Company Saint Louis MO
63 61 Surly Brewing Company Brooklyn Center MN
64 62 Against The Grain Brewery Louisville KY
65 63 Crazy Mountain Brewing Company Edwards CO
66 64 SlapShot Brewing Company Chicago IL
67 65 Mikerphone Brewing Chicago IL
68 66 Freetail Brewing Company San Antonio TX
69 67 3 Daughters Brewing St Petersburg FL
70 68 Red Shedman Farm Brewery and Hop... Mt. Airy MD
71 69 Appalachian Mountain Brewery Boone NC
72 70 Birdsong Brewing Company Charlotte NC
73 71 Union Craft Brewing Baltimore MD
74 72 Atwater Brewery Detroit MI
75 73 Ale Asylum Madison WI
76 74 Two Brothers Brewing Company Warrenville IL
77 75 Bent Paddle Brewing Company Duluth MN
78 76 Bell's Brewery Kalamazoo MI
79 77 Blue Owl Brewing Austin TX
80 78 Speakasy Ales & Lagers San Francisco CA
81 79 Black Tooth Brewing Company Sheridan WY
82 80 Hopworks Urban Brewery Portland OR
83 81 Epic Brewing Denver CO
84 82 New Belgium Brewing Company Fort Collins CO
85 83 Sierra Nevada Brewing Company Chico CA
86 84 Keweenaw Brewing Company Houghton MI
87 85 Brewery Terra Firma Traverse City MI
88 86 Grey Sail Brewing Company Westerly RI
89 87 Kirkwood Station Brewing Company Kirkwood MO
90 88 Goose Island Brewing Company Chicago IL
91 89 Broad Brook Brewing LLC East Windsor CT
92 90 The Lion Brewery Wilkes-Barre PA
93 91 Madtree Brewing Company Cincinnati OH
94 92 Jackie O's Pub & Brewery Athens OH
95 93 Rhinegeist Brewery Cincinnati OH
96 94 Warped Wing Brewing Company Dayton OH
97 95 Blackrocks Brewery Marquette MA
98 96 Catawba Valley Brewing Company Morganton NC
99 97 Tröegs Brewing Company Hershey PA
100 98 Mission Brewery San Diego CA
101 99 Christian Moerlein Brewing Company Cincinnati OH
102 100 West Sixth Brewing Lexington KY
103 101 Coastal Extreme Brewing Company Newport RI
104 102 King Street Brewing Company Anchorage AK
105 103 Beer Works Brewery Lowell MA
106 104 Lone Tree Brewing Company Lone Tree CO
107 105 Four String Brewing Company Columbus OH
108 106 Glabrous Brewing Company Pineland ME
109 107 Bonfire Brewing Company Eagle CO
110 108 Thomas Hooker Brewing Company Bloomfield CT
111 109 Woodstock Inn, Station & Brewery North Woodstock NH
112 110 Renegade Brewing Company Denver CO
113 111 Mother Earth Brew Company Vista CA
114 112 Black Market Brewing Company Temecula CA
115 113 Vault Brewing Company Yardley PA
116 114 Jailbreak Brewing Company Laurel MD
117 115 Smartmouth Brewing Company Norfolk VA
118 116 Base Camp Brewing Co. Portland OR
119 117 Alameda Brewing Portland OR
120 118 Southern Star Brewing Company Conroe TX
121 119 Steamworks Brewing Company Durango CO
122 120 Horny Goat Brew Pub Milwaukee WI
123 121 Cheboygan Brewing Company Cheboygan MI
124 122 Center of the Universe Brewing C... Ashland VA
125 123 Ipswich Ale Brewery Ipswich MA
126 124 Griffin Claw Brewing Company Birmingham MI
127 125 Karbach Brewing Company Houston TX
128 126 Uncle Billy's Brewery and Smokeh... Austin TX
129 127 Deep Ellum Brewing Company Dallas TX
130 128 Real Ale Brewing Company Blanco TX
131 129 Straub Brewery St Mary's PA
132 130 Shebeen Brewing Company Wolcott CT
133 131 Stevens Point Brewery Stevens Point WI
134 132 Weston Brewing Company Weston MO
135 133 Southern Prohibition Brewing Com... Hattiesburg MS
136 134 Minhas Craft Brewery Monroe WI
137 135 Pug Ryan's Brewery Dillon CO
138 136 Hops & Grains Brewing Company Austin TX
139 137 Sietsema Orchards and Cider Mill Ada MI
140 138 Summit Brewing Company St Paul MN
141 139 Core Brewing & Distilling Company Springdale AR
142 140 Independence Brewing Company Austin TX
143 141 Cigar City Brewing Company Tampa FL
144 142 Third Street Brewhouse Cold Spring MN
145 143 Narragansett Brewing Company Providence RI
146 144 Grimm Brothers Brewhouse Loveland CO
147 145 Cisco Brewers Nantucket MA
148 146 Angry Minnow Hayward WI
149 147 Platform Beer Company Cleveland OH
150 148 Odyssey Beerwerks Arvada CO
151 149 Lonerider Brewing Company Raleigh NC
152 150 Oakshire Brewing Eugene OR
153 151 Fort Pitt Brewing Company Latrobe PA
154 152 Tin Roof Brewing Company Baton Rouge LA
155 153 Three Creeks Brewing Sisters OR
156 154 2 Towns Ciderhouse Corvallis OR
157 155 Caldera Brewing Company Ashland OR
158 156 Greenbrier Valley Brewing Company Lewisburg WV
159 157 Phoenix Ale Brewery Phoenix AZ
160 158 Lumberyard Brewing Company Flagstaff AZ
161 159 Uinta Brewing Company Salt Lake City UT
162 160 Four Peaks Brewing Company Tempe AZ
163 161 Martin House Brewing Company Fort Worth TX
164 162 Right Brain Brewery Traverse City MI
165 163 Sly Fox Brewing Company Phoenixville PA
166 164 Round Guys Brewing Lansdale PA
167 165 Great Crescent Brewery Aurora IN
168 166 Oskar Blues Brewery Longmont CO
169 167 Boxcar Brewing Company West Chester PA
170 168 High Hops Brewery Windsor CO
171 169 Crooked Fence Brewing Company Garden City ID
172 170 Everybody's Brewing White Salmon WA
173 171 Anderson Valley Brewing Company Boonville CA
174 172 Fiddlehead Brewing Company Shelburne VT
175 173 Evil Twin Brewing Brooklyn NY
176 174 New Orleans Lager & Ale Brewing ... New Orleans LA
177 175 Spiteful Brewing Company Chicago IL
178 176 Rahr & Sons Brewing Company Fort Worth TX
179 177 18th Street Brewery Gary IN
180 178 Cambridge Brewing Company Cambridge MA
181 179 Carolina Brewery Pittsboro NC
182 180 Frog Level Brewing Company Waynesville NC
183 181 Wild Wolf Brewing Company Nellysford VA
184 182 COOP Ale Works Oklahoma City OK
185 183 Seventh Son Brewing Company Columbus OH
186 184 Oasis Texas Brewing Company Austin TX
187 185 Vander Mill Ciders Spring Lake MI
188 186 St. Julian Winery Paw Paw MI
189 187 Pedernales Brewing Company Fredericksburg TX
190 188 Mother's Brewing Springfield MO
191 189 Modern Monks Brewery Lincoln NE
192 190 Two Beers Brewing Company Seattle WA
193 191 Snake River Brewing Company Jackson WY
194 192 Capital Brewery Middleton WI
195 193 Anthem Brewing Company Oklahoma City OK
196 194 Goodlife Brewing Co. Bend OR
197 195 Breakside Brewery Portland OR
198 196 Goose Island Brewery Company Chicago IL
199 197 Burnside Brewing Co. Portland OR
200 198 Hop Valley Brewing Company Springfield OR
201 199 Worthy Brewing Company Bend OR
202 200 Occidental Brewing Company Portland OR
203 201 Fearless Brewing Company Estacada OR
204 202 Upland Brewing Company Bloomington IN
205 203 Mehana Brewing Co. Hilo HI
206 204 Hawai'i Nui Brewing Co. Hilo HI
207 205 People's Brewing Company Lafayette IN
208 206 Fort George Brewery Astoria OR
209 207 Branchline Brewing Company San Antonio TX
210 208 Kalona Brewing Company Kalona IA
211 209 Modern Times Beer San Diego CA
212 210 Temperance Beer Company Evanston IL
213 211 Wisconsin Brewing Company Verona WI
214 212 Crow Peak Brewing Company Spearfish SD
215 213 Grapevine Craft Brewery Farmers Branch TX
216 214 Buffalo Bayou Brewing Company Houston TX
217 215 Texian Brewing Co. Richmond TX
218 216 Orpheus Brewing Atlanta GA
219 217 Forgotten Boardwalk Cherry Hill NJ
220 218 Laughing Dog Brewing Company Ponderay ID
221 219 Bozeman Brewing Company Bozeman MT
222 220 Big Choice Brewing Broomfield CO
223 221 Big Storm Brewing Company Odessa FL
224 222 Carton Brewing Company Atlantic Highlands NJ
225 223 Midnight Sun Brewing Company Anchorage AK
226 224 Fat Head's Brewery Middleburg Heights OH
227 225 Refuge Brewery Temecula CA
228 226 Chatham Brewing Chatham NY
229 227 DC Brau Brewing Company Washington DC
230 228 Geneva Lake Brewing Company Lake Geneva WI
231 229 Rochester Mills Brewing Company Rochester MI
232 230 Cape Ann Brewing Company Gloucester MA
233 231 Borderlands Brewing Company Tucson AZ
234 232 College Street Brewhouse and Pub Lake Havasu City AZ
235 233 Joseph James Brewing Company Henderson NV
236 234 Harpoon Brewery Boston MA
237 235 Back East Brewing Company Bloomfield CT
238 236 Champion Brewing Company Charlottesville VA
239 237 Devil's Backbone Brewing Company Lexington VA
240 238 Newburgh Brewing Company Newburgh NY
241 239 Wiseacre Brewing Company Memphis TN
242 240 Golden Road Brewing Los Angeles CA
243 241 New Republic Brewing Company College Station TX
244 242 Infamous Brewing Company Austin TX
245 243 Two Henrys Brewing Company Plant City FL
246 244 Lift Bridge Brewing Company Stillwater MN
247 245 Lucky Town Brewing Company Jackson MS
248 246 Quest Brewing Company Greenville SC
249 247 Creature Comforts Athens GA
250 248 Half Full Brewery Stamford CT
251 249 Southampton Publick House Southampton NY
252 250 Chapman's Brewing Angola IN
253 251 Barrio Brewing Company Tucson AZ
254 252 Santa Cruz Mountain Brewing Santa Cruz CA
255 253 Frankenmuth Brewery Frankenmuth MI
256 254 Meckley's Cidery Somerset Center MI
257 255 Stillwater Artisanal Ales Baltimore MD
258 256 Finch's Beer Company Chicago IL
259 257 South Austin Brewery South Austin TX
260 258 Bauhaus Brew Labs Minneapolis MN
261 259 Ozark Beer Company Rogers AR
262 260 Mountain Town Brewing Company Mount Pleasant MI
263 261 Otter Creek Brewing Waterbury VT
264 262 The Brewer's Art Baltimore MD
265 263 Denver Beer Company Denver CO
266 264 Ska Brewing Company Durango CO
267 265 Tractor Brewing Company Albuquerque NM
268 266 Peak Organic Brewing Company Portland ME
269 267 Cape Cod Beer Hyannis MA
270 268 Long Trail Brewing Company Bridgewater Corners VT
271 269 Great Raft Brewing Company Shreveport LA
272 270 Alaskan Brewing Company Juneau AK
273 271 Notch Brewing Company Ipswich MA
274 272 The Alchemist Waterbury VT
275 273 Three Notch'd Brewing Company Charlottesville VA
276 274 Portside Brewery Cleveland OH
277 275 Otter Creek Brewing Middlebury VT
278 276 Montauk Brewing Company Montauk NY
279 277 Indeed Brewing Company Minneapolis MN
280 278 Berkshire Brewing Company South Deerfield MA
281 279 Foolproof Brewing Company Pawtucket RI
282 280 Headlands Brewing Company Mill Valley CA
283 281 Bolero Snort Brewery Ridgefield Park NJ
284 282 Thunderhead Brewing Company Kearney NE
285 283 Defiance Brewing Company Hays KS
286 284 Milwaukee Brewing Company Milwaukee WI
287 285 Catawba Island Brewing Port Clinton OH
288 286 Back Forty Beer Company Gadsden AL
289 287 Four Corners Brewing Company Dallas TX
290 288 Saint Archer Brewery San Diego CA
291 289 Rogue Ales Newport OR
292 290 Hale's Ales Seattle WA
293 291 Tommyknocker Brewery Idaho Springs CO
294 292 Baxter Brewing Company Lewiston ME
295 293 Northampton Brewery Northamtpon MA
296 294 Black Shirt Brewing Company Denver CO
297 295 Wachusett Brewing Company Westminster MA
298 296 Widmer Brothers Brewing Company Portland OR
299 297 Hop Farm Brewing Company Pittsburgh PA
300 298 Liquid Hero Brewery York PA
301 299 Matt Brewing Company Utica NY
302 300 Boston Beer Company Boston MA
303 301 Old Forge Brewing Company Danville PA
304 302 Utah Brewers Cooperative Salt Lake City UT
305 303 Magic Hat Brewing Company South Burlington VT
306 304 Blue Hills Brewery Canton MA
307 305 Night Shift Brewing Everett MA
308 306 Beach Brewing Company Virginia Beach VA
309 307 Payette Brewing Company Garden City ID
310 308 Brew Bus Brewing Tampa FL
311 309 Sockeye Brewing Company Boise ID
312 310 Pine Street Brewery San Francisco CA
313 311 Dirty Bucket Brewing Company Woodinville WA
314 312 Jackalope Brewing Company Nashville TN
315 313 Slanted Rock Brewing Company Meridian ID
316 314 Piney River Brewing Company Bucryus MO
317 315 Cutters Brewing Company Avon IN
318 316 Iron Hill Brewery & Restaurant Wilmington DE
319 317 Marshall Wharf Brewing Company Belfast ME
320 318 Banner Beer Company Williamsburg MA
321 319 Dick's Brewing Company Centralia WA
322 320 Claremont Craft Ales Claremont CA
323 321 Rivertown Brewing Company Lockland OH
324 322 Voodoo Brewery Meadville PA
325 323 D.L. Geary Brewing Company Portland ME
326 324 Pisgah Brewing Company Black Mountain NC
327 325 Neshaminy Creek Brewing Company Croydon PA
328 326 Morgan Street Brewery Saint Louis MO
329 327 Half Acre Beer Company Chicago IL
330 328 The Just Beer Project Burlington VT
331 329 The Bronx Brewery Bronx NY
332 330 Dead Armadillo Craft Brewing Tulsa OK
333 331 Catawba Brewing Company Morganton NC
334 332 La Cumbre Brewing Company Albuquerque NM
335 333 David's Ale Works Diamond Springs CA
336 334 The Traveler Beer Company Burlington VT
337 335 Fargo Brewing Company Fargo ND
338 336 Big Sky Brewing Company Missoula MT
339 337 Nebraska Brewing Company Papillion NE
340 338 Uncle John's Fruit House Winery St. John's MI
341 339 Wormtown Brewery Worcester MA
342 340 Due South Brewing Company Boynton Beach FL
343 341 Palisade Brewing Company Palisade CO
344 342 KelSo Beer Company Brooklyn NY
345 343 Hardywood Park Craft Brewery Richmond VA
346 344 Wolf Hills Brewing Company Abingdon VA
347 345 Lavery Brewing Company Erie PA
348 346 Manzanita Brewing Company Santee CA
349 347 Fullsteam Brewery Durham NC
350 348 Four Horsemen Brewing Company South Bend IN
351 349 Hinterland Brewery Green Bay WI
352 350 Central Coast Brewing Company San Luis Obispo CA
353 351 Westfield River Brewing Company Westfield MA
354 352 Elevator Brewing Company Columbus OH
355 353 Aslan Brewing Company Bellingham WA
356 354 Kulshan Brewery Bellingham WA
357 355 Pikes Peak Brewing Company Monument CO
358 356 Manayunk Brewing Company Philadelphia PA
359 357 Buckeye Brewing Cleveland OH
360 358 Daredevil Brewing Company Shelbyville IN
361 359 NoDa Brewing Company Charlotte NC
362 360 Aviator Brewing Company Fuquay-Varina NC
363 361 Wild Onion Brewing Company Lake Barrington IL
364 362 Hilliard's Beer Seattle WA
365 363 Mikkeller Pottstown PA
366 364 Bohemian Brewery Midvale UT
367 365 Great River Brewery Davenport IA
368 366 Mustang Brewing Company Mustang OK
369 367 Airways Brewing Company Kent WA
370 368 21st Amendment Brewery San Francisco CA
371 369 Eddyline Brewery & Restaurant Buena Vista CO
372 370 Pizza Port Brewing Company Carlsbad CA
373 371 Sly Fox Brewing Company Pottstown PA
374 372 Spring House Brewing Company Conestoga PA
375 373 7venth Sun Dunedin FL
376 374 Astoria Brewing Company Astoria OR
377 375 Maui Brewing Company Lahaina HI
378 376 RoughTail Brewing Company Midwest City OK
379 377 Lucette Brewing Company Menominee WI
380 378 Bold City Brewery Jacksonville FL
381 379 Grey Sail Brewing of Rhode Island Westerly RI
382 380 Blue Blood Brewing Company Lincoln NE
383 381 Swashbuckler Brewing Company Manheim PA
384 382 Blue Mountain Brewery Afton VA
385 383 Starr Hill Brewery Crozet VA
386 384 Westbrook Brewing Company Mt. Pleasant SC
387 385 Shipyard Brewing Company Portland ME
388 386 Revolution Brewing Paonia CO
389 387 Natian Brewery Portland OR
390 388 Alltech's Lexington Brewing Company Lexington KY
391 389 Oskar Blues Brewery (North Carol... Brevard NC
392 390 Orlison Brewing Company Airway Heights WA
393 391 Breckenridge Brewery Denver CO
394 392 Santa Fe Brewing Company Santa Fe NM
395 393 Miami Brewing Company Miami FL
396 394 Schilling & Company Seattle WA
397 395 Hops & Grain Brewery Austin TX
398 396 White Flame Brewing Company Hudsonville MI
399 397 Ruhstaller Beer Company Sacramento CA
400 398 Saugatuck Brewing Company Douglas MI
401 399 Moab Brewery Moab UT
402 400 Macon Beer Company Macon GA
403 401 Amnesia Brewing Company Washougal WA
404 402 Wolverine State Brewing Company Ann Arbor MI
405 403 Red Tank Cider Company Bend OR
406 404 Cascadia Ciderworks United Portland OR
407 405 Fate Brewing Company Boulder CO
408 406 Lazy Monk Brewing Eau Claire WI
409 407 Bitter Root Brewing Hamilton MT
410 408 10 Barrel Brewing Company Bend OR
411 409 Tamarack Brewing Company Lakeside MT
412 410 New England Brewing Company Woodbridge CT
413 411 Seattle Cider Company Seattle WA
414 412 Straight to Ale Huntsville AL
415 413 Austin Beerworks Austin TX
416 414 Blue Mountain Brewery Arrington VA
417 415 Coastal Empire Beer Company Savannah GA
418 416 Jack's Hard Cider (Hauser Estate... Biglerville PA
419 417 Boulder Beer Company Boulder CO
420 418 Coalition Brewing Company Portland OR
421 419 Sanitas Brewing Company Boulder CO
422 420 Gore Range Brewery Edwards CO
423 421 Redstone Meadery Boulder CO
424 422 Blue Dog Mead Eugene OR
425 423 Hess Brewing Company San Diego CA
426 424 Wynkoop Brewing Company Denver CO
427 425 Ciderboys Stevens Point WI
428 426 Armadillo Ale Works Denton TX
429 427 Roanoke Railhouse Brewery Roanoke VA
430 428 Schlafly Brewing Company Saint Louis MO
431 429 Asher Brewing Company Boulder CO
432 430 Lost Rhino Brewing Company Ashburn VA
433 431 North Country Brewing Company Slippery Rock PA
434 432 Seabright Brewery Santa Cruz CA
435 433 French Broad Brewery Asheville NC
436 434 Angry Orchard Cider Company Cincinnati OH
437 435 Two Roads Brewing Company Stratford CT
438 436 Southern Oregon Brewing Company Medford OR
439 437 Brooklyn Brewery Brooklyn NY
440 438 The Right Brain Brewery Traverse City MI
441 439 Kona Brewing Company Kona HI
442 440 MillKing It Productions Royal Oak MI
443 441 Pateros Creek Brewing Company Fort Collins CO
444 442 O'Fallon Brewery O'Fallon MO
445 443 Marble Brewery Albuquerque NM
446 444 Big Wood Brewery Vadnais Heights MN
447 445 Howard Brewing Company Lenoir NC
448 446 Downeast Cider House Leominster MA
449 447 Swamp Head Brewery Gainesville FL
450 448 Mavericks Beer Company Half Moon Bay CA
451 449 TailGate Beer San Diego CA
452 450 Northwest Brewing Company Pacific WA
453 451 Dad & Dude's Breweria Aurora CO
454 452 Centennial Beer Company Edwards CO
455 453 Denali Brewing Company Talkeetna AK
456 454 Deschutes Brewery Bend OR
457 455 Sunken City Brewing Company Hardy VA
458 456 Lucette Brewing Company Menominie WI
459 457 The Black Tooth Brewing Company Sheridan WY
460 458 Kenai River Brewing Company Soldotna AK
461 459 River North Brewery Denver CO
462 460 Fremont Brewing Company Seattle WA
463 461 Armstrong Brewing Company South San Francisco CA
464 462 AC Golden Brewing Company Golden CO
465 463 Big Bend Brewing Company Alpine TX
466 464 Good Life Brewing Company Bend OR
467 465 Engine 15 Brewing Jacksonville Beach FL
468 466 Green Room Brewing Jacksonville FL
469 467 Brindle Dog Brewing Company Tampa Bay FL
470 468 Peace Tree Brewing Company Knoxville IA
471 469 Terrapin Brewing Company Athens GA
472 470 Pete's Brewing Company San Antonio TX
473 471 Okoboji Brewing Company Spirit Lake IA
474 472 Crystal Springs Brewing Company Boulder CO
475 473 Engine House 9 Tacoma WA
476 474 Tonka Beer Company Minnetonka MN
477 475 Red Hare Brewing Company Marietta GA
478 476 Hangar 24 Craft Brewery Redlands CA
479 477 Big Elm Brewing Sheffield MA
480 478 Good People Brewing Company Birmingham AL
481 479 Heavy Seas Beer Halethorpe MD
482 480 Telluride Brewing Company Telluride CO
483 481 7 Seas Brewing Company Gig Harbor WA
484 482 Confluence Brewing Company Des Moines IA
485 483 Bale Breaker Brewing Company Yakima WA
486 484 The Manhattan Brewing Company New York NY
487 485 MacTarnahans Brewing Company Portland OR
488 486 Stillmank Beer Company Green Bay WI
489 487 Redhook Brewery Woodinville WA
490 488 Dock Street Brewery Philadelphia PA
491 489 Blue Point Brewing Company Patchogue NY
492 490 Tampa Bay Brewing Company Tampa FL
493 491 Devil's Canyon Brewery Belmont CA
494 492 Stone Coast Brewing Company Portland ME
495 493 Broken Tooth Brewing Company Anchorage AK
496 494 Seven Brides Brewery Silverton OR
497 495 Newburyport Brewing Company Newburyport MA
498 496 Dry Dock Brewing Company Aurora CO
499 497 Cans Bar and Canteen Charlotte NC
500 498 Sprecher Brewing Company Glendale WI
501 499 Wildwood Brewing Company Stevensville MT
502 500 High Noon Saloon And Brewery Leavenworth KS
503 501 Woodchuck Hard Cider Middlebury VT
504 502 Sea Dog Brewing Company Portland ME
505 503 Oskar Blues Brewery Lyons CO
506 504 Carolina Beer & Beverage Mooresville NC
507 505 Krebs Brewing Company (Pete's Pl... Krebs OK
508 506 Warbird Brewing Company Fort Wayne IN
509 507 Mudshark Brewing Company Lake Havasu City AZ
510 508 Spilker Ales Cortland NE
511 509 Wingman Brewers Tacoma WA
512 510 Kettle House Brewing Company Missoula MT
513 511 Sherwood Forest Brewers Marlborough MA
514 512 Cottrell Brewing Pawcatuck CT
515 513 Arctic Craft Brewery Colorado Springs CO
516 514 Monkey Paw Pub & Brewery San Diego CA
517 515 Crabtree Brewing Company Greeley CO
518 516 Emerald City Beer Company Seattle WA
519 517 Butcher's Brewing Carlsbad CA
520 518 New South Brewing Company Myrtle Beach SC
521 519 Big River Brewing Company Chattanooga TN
522 520 Twisted Pine Brewing Company Boulder CO
523 521 Flying Dog Brewery Frederick MD
524 522 Uncommon Brewers Santa Cruz CA
525 523 Aspen Brewing Company Aspen CO
526 524 Triangle Brewing Company Durham NC
527 525 Bomb Beer Company New York NY
528 526 Churchkey Can Company Seattle WA
529 527 Intuition Ale Works Jacksonville FL
530 528 Asheville Brewing Company Asheville NC
531 529 Northwoods Brewpub Eau Claire WI
532 530 Buckbean Brewing Company Reno NV
533 531 Dolores River Brewery Dolores CO
534 532 Flat Rock Brewing Company Smithton PA
535 533 Abita Brewing Company Abita Springs LA
536 534 Mammoth Brewing Company Mammoth Lakes CA
537 535 Harvest Moon Brewing Company Belt MT
538 536 Grand Canyon Brewing Company Williams AZ
539 537 Lewis and Clark Brewing Company Helena MT
540 538 Dundee Brewing Company Rochester NY
541 539 Twin Lakes Brewing Company Greenville DE
542 540 Mother Earth Brewing Company Kinston NC
543 541 Arcadia Brewing Company Battle Creek MI
544 542 Angry Minnow Brewing Company Hayward WI
545 543 Great Northern Brewing Company Whitefish MT
546 544 Pyramid Breweries Seattle WA
547 545 Lancaster Brewing Company Lancaster PA
548 546 Upstate Brewing Company Elmira NY
549 547 Moat Mountain Smoke House & Brew... North Conway NH
550 548 Prescott Brewing Company Prescott AZ
551 549 Mogollon Brewing Company Flagstaff AZ
552 550 Wind River Brewing Company Pinedale WY
553 551 Silverton Brewery Silverton CO
554 552 Mickey Finn's Brewery Libertyville IL
555 553 Covington Brewhouse Covington LA
556 554 Dave's Brewfarm Wilson WI
557 555 Ukiah Brewing Company Ukiah CA
558 556 Butternuts Beer and Ale Garrattsville NY
559 557 Sleeping Lady Brewing Company Anchorage AK

File diff suppressed because it is too large Load Diff

View File

@@ -1,578 +0,0 @@
[
{
"text": "100 Acre Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/100-acre-brewing-company/"
},
{
"text": "All My Friends Beer Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/all-my-friends-beer-co/"
},
{
"text": "All or Nothing Brewhouse",
"href": "https://ontariocraftbrewers.com/brewery-profile/all-or-nothing-brewhouse/"
},
{
"text": "Anderson Craft Ales",
"href": "https://ontariocraftbrewers.com/brewery-profile/anderson-craft-ales/"
},
{
"text": "Badlands Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/badlands-brewing-company/"
},
{
"text": "Bancroft Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/bancroft-brewing-co/"
},
{
"text": "Banded Goose Brewing Comany",
"href": "https://ontariocraftbrewers.com/brewery-profile/banded-goose-brewing-comany/"
},
{
"text": "Beaus Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/beaus-brewery/"
},
{
"text": "BeerLab! London",
"href": "https://ontariocraftbrewers.com/brewery-profile/beerlab-london/"
},
{
"text": "Bellwoods Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/bellwoods-brewery/"
},
{
"text": "Bench Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/bench-brewing/"
},
{
"text": "Beyond The Pale Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/beyond-the-pale-brewing-co/"
},
{
"text": "Bicycle Craft Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/bicycle-craft-brewery/"
},
{
"text": "Big Rig Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/big-rig-brewery/"
},
{
"text": "Big Rock Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/big-rock-brewery/"
},
{
"text": "Black Gold Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/black-gold-brewery/"
},
{
"text": "Black Oak Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/black-oak-brewing-co/"
},
{
"text": "Block 3 Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/block-3-brewing-co/"
},
{
"text": "Blood Brothers Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/blood-brothers-brewing/"
},
{
"text": "Bobcaygeon Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/bobcaygeon-brewing-company/"
},
{
"text": "Boshkung Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/boshkung-brewing-co/"
},
{
"text": "Brauwerk Hoffman Rockland",
"href": "https://ontariocraftbrewers.com/brewery-profile/brauwerk-hoffman-rockland/"
},
{
"text": "Bridge Masters Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/bridge-masters-brewing-co/"
},
{
"text": "Broadhead Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/broadhead-brewery/"
},
{
"text": "Broken Rail Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/broken-rail-brewing/"
},
{
"text": "Burdock Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/burdock-brewery/"
},
{
"text": "Cest What Durham Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/cest-what-durham-brewing-company/"
},
{
"text": "Calabogie Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/calabogie-brewing/"
},
{
"text": "Camerons Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/camerons-brewing-company/"
},
{
"text": "Canvas Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/canvas-brewing-co/"
},
{
"text": "Caps Off Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/caps-off-brewing/"
},
{
"text": "Century Barn Brewing & Beverage Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/century-barn-brewing-and-beverage-company/"
},
{
"text": "Chronicle Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/chronicle-brewing-company/"
},
{
"text": "Clifford Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/clifford-brewing-co/"
},
{
"text": "Cold Bear Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/cold-bear-brewing-co/"
},
{
"text": "Collective Arts Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/collective-arts-brewing-ltd/"
},
{
"text": "Common Good Beer Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/common-good-beer-co/"
},
{
"text": "Couchiching Craft Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/couchiching-craft-brewing-company/"
},
{
"text": "Cowbell Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/cowbell-brewing-co/"
},
{
"text": "Cured Craft Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/cured-craft-brewing-co/"
},
{
"text": "Daft Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/daft-brewing-company/"
},
{
"text": "Dog House Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/dog-house-brewing-company/"
},
{
"text": "Dominion City Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/dominion-city-brewing-co/"
},
{
"text": "Eastbound Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/eastbound-brewing-co/"
},
{
"text": "Equals Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/equals-brewing-company/"
},
{
"text": "Fairweather Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/fairweather-brewing-company/"
},
{
"text": "Farm League Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/farm-league-brewing/"
},
{
"text": "Fixed Gear Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/fixed-gear-brewing-co/"
},
{
"text": "Flying Monkeys Craft Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/flying-monkeys-craft-brewery/"
},
{
"text": "Focal Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/focal-brewing-co/"
},
{
"text": "Foundry Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/foundry-brewing/"
},
{
"text": "Four Fathers Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/four-fathers-brewing-co/"
},
{
"text": "Frank Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/frank-brewing-co/"
},
{
"text": "Freddys",
"href": "https://ontariocraftbrewers.com/brewery-profile/freddys/"
},
{
"text": "Full Beard Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/1068-2/"
},
{
"text": "Furnace Room Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/furnace-room-brewery/"
},
{
"text": "Gateway City Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/gateway-city-brewery/"
},
{
"text": "Glasstown Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/glasstown-brewing-company/"
},
{
"text": "Godspeed Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/godspeed-brewery/"
},
{
"text": "Goldenfield Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/goldenfield-brewing/"
},
{
"text": "Grand River Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/grand-river-brewery/"
},
{
"text": "Granite Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/granite-brewery/"
},
{
"text": "Great Lakes Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/great-lakes-brewery/"
},
{
"text": "Haliburton Highlands Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/haliburton-highlands-brewery/"
},
{
"text": "Imperial City Brew House",
"href": "https://ontariocraftbrewers.com/brewery-profile/imperial-city-brew-house/"
},
{
"text": "Indie Ale House",
"href": "https://ontariocraftbrewers.com/brewery-profile/indie-ale-house/"
},
{
"text": "Jobsite Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/jobsite-brewing-co/"
},
{
"text": "Kichesippi Beer Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/kichesippi-beer-co/"
},
{
"text": "Kick and Push Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/kick-and-push-brewing-company/"
},
{
"text": "Lake of Bays Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/lake-of-bays-brewing-co/"
},
{
"text": "Lake Of The Woods Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/lake-of-the-woods-brewing-company/"
},
{
"text": "Left Field Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/left-field-brewery/"
},
{
"text": "Lightcaster Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/lightcaster-brewery/"
},
{
"text": "MacKinnon Brothers Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/mackinnon-brothers-brewing-co/"
},
{
"text": "Macleans Ales Inc.",
"href": "https://ontariocraftbrewers.com/brewery-profile/macleans-ales-inc/"
},
{
"text": "Magnotta Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/magnotta-brewery/"
},
{
"text": "Market Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/market-brewing/"
},
{
"text": "Mascot Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/mascot-brewery/"
},
{
"text": "Matron Fine Beer",
"href": "https://ontariocraftbrewers.com/brewery-profile/matron-fine-beer/"
},
{
"text": "Meyers Creek Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/meyers-creek-brewing-company/"
},
{
"text": "Midtown Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/midtown-brewing-co/"
},
{
"text": "Miski Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/miski-brewing/"
},
{
"text": "Muddy York Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/muddy-york-brewing-co/"
},
{
"text": "Muskoka Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/muskoka-brewery/"
},
{
"text": "Natterjack Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/natterjack-brewing-company/"
},
{
"text": "Newark Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/newark-brewing-co/"
},
{
"text": "Niagara Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/niagara-brewing-company/"
},
{
"text": "Niagara College Teaching Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/niagara-college-teaching-brewery/"
},
{
"text": "Niagara Oast House Brewers",
"href": "https://ontariocraftbrewers.com/brewery-profile/niagara-oast-house-brewers/"
},
{
"text": "Nickel Brook Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/nickel-brook-brewing-co/"
},
{
"text": "Northern Superior Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/northern-superior-brewing-co/"
},
{
"text": "Old Credit Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/old-credit-brewing-co/"
},
{
"text": "Old Flame Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/1239-2/"
},
{
"text": "Orléans Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/orleans-brewing-co/"
},
{
"text": "Overflow Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/overflow-brewing-co/"
},
{
"text": "Parsons Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/parsons-brewing-company/"
},
{
"text": "Perth Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/perth-brewery/"
},
{
"text": "Prince Eddys Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/prince-eddys-brewing-company/"
},
{
"text": "Quayles Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/quayles-brewery/"
},
{
"text": "Quetico Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/quetico-brewing-company/"
},
{
"text": "Railway City Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/railway-city-brewing-co/"
},
{
"text": "Ramblin Road Brewery Farm",
"href": "https://ontariocraftbrewers.com/brewery-profile/ramblin-road-brewery-farm/"
},
{
"text": "Red Barn Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/red-barn-brewing-company/"
},
{
"text": "Refined Fool Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/refined-fool-brewing-co/"
},
{
"text": "Rouge River Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/rouge-river-brewing/"
},
{
"text": "Royal City Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/royal-city-brewing-co/"
},
{
"text": "Sassy Britches Brewing Co Ltd",
"href": "https://ontariocraftbrewers.com/brewery-profile/sassy-britches-brewing-co-ltd/"
},
{
"text": "Sawdust City Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/sawdust-city-brewing/"
},
{
"text": "Shawn & Ed Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/shawn-ed-brewing-co/"
},
{
"text": "Silversmith Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/silversmith-brewing-company/"
},
{
"text": "Slake Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/slake-brewing/"
},
{
"text": "Sleeping Giant Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/sleeping-giant-brewing-co/"
},
{
"text": "Something in the Water Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/something-in-the-water-brewing-co/"
},
{
"text": "Sonnen Hill Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/sonnen-hill-brewing/"
},
{
"text": "Sons of Kent Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/sons-of-kent-brewing-company/"
},
{
"text": "Spark Beer",
"href": "https://ontariocraftbrewers.com/brewery-profile/spark-beer/"
},
{
"text": "Split Rail Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/split-rail-brewing-company/"
},
{
"text": "Stack Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/stack-brewing/"
},
{
"text": "Steam Whistle Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/steam-whistle-brewing/"
},
{
"text": "Steel Wheel Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/steel-wheel-brewery/"
},
{
"text": "Stonehooker Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/stonehooker-brewing-company/"
},
{
"text": "Stonepicker Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/stonepicker-brewery/"
},
{
"text": "Stray Dog Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/stray-dog-brewing-company/"
},
{
"text": "The Exchange Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/the-exchange-brewery/"
},
{
"text": "The Grove Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/the-grove-brewing-company/"
},
{
"text": "The Second Wedge Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/the-second-wedge-brewing-co/"
},
{
"text": "Thornbury Village Craft Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/thornbury-village-craft-brewery/"
},
{
"text": "Three Sheets Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/three-sheets-brewing/"
},
{
"text": "Tooth and Nail Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/tooth-and-nail-brewing-company/"
},
{
"text": "Torched Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/torched-brewing/"
},
{
"text": "Town Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/town-brewery/"
},
{
"text": "Trestle Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/trestle-brewing-company/"
},
{
"text": "True History Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/true-history-brewing/"
},
{
"text": "Upper Thames Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/upper-thames-brewing-co/"
},
{
"text": "Vimy Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/vimy-brewing-company/"
},
{
"text": "Walkerville Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/walkerville-brewery/"
},
{
"text": "Wave Maker Craft Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/wave-maker-craft-brewery/"
},
{
"text": "Wellington Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/wellington-brewery/"
},
{
"text": "Whiprsnapr Brewing Co.",
"href": "https://ontariocraftbrewers.com/brewery-profile/whiprsnapr-brewing-co/"
},
{
"text": "Whiskeyjack Beer Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/whiskeyjack-beer-company/"
},
{
"text": "Whitewater Brewing Company",
"href": "https://ontariocraftbrewers.com/brewery-profile/whitewater-brewing-company/"
},
{
"text": "Willibald Farm Distillery & Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/willibald-farm-distillery-brewery/"
},
{
"text": "Windmill Brewery",
"href": "https://ontariocraftbrewers.com/brewery-profile/windmill-brewery/"
},
{
"text": "Wishbone Brewing",
"href": "https://ontariocraftbrewers.com/brewery-profile/wishbone-brewing/"
}
]

View File

@@ -1,181 +0,0 @@
cmake_minimum_required(VERSION 3.24)
project(biergarten-pipeline)
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE)
# =============================================================================
# 1. Platform & GPU Detection
# =============================================================================
if(WIN32)
message(FATAL_ERROR "[biergarten] Windows is currently not supported. Please use Linux (Fedora 43) or macOS (M1 Pro).")
endif()
if(APPLE)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64")
message(STATUS "[biergarten] Apple Silicon detected — enabling Metal acceleration.")
set(GGML_METAL ON CACHE BOOL "Enable Metal for Apple Silicon" FORCE)
else()
message(STATUS "[biergarten] Intel Mac detected — using CPU / Accelerate framework.")
set(GGML_METAL OFF CACHE BOOL "Disable Metal for Intel Macs" FORCE)
endif()
elseif(UNIX AND NOT APPLE)
find_package(CUDAToolkit QUIET)
find_package(HIP QUIET)
if(CUDAToolkit_FOUND)
message(STATUS "[biergarten] NVIDIA GPU detected — enabling CUDA acceleration.")
set(GGML_CUDA ON CACHE BOOL "Enable CUDA for NVIDIA GPUs" FORCE)
set(CMAKE_CUDA_ARCHITECTURES native)
elseif(HIP_FOUND OR EXISTS "/opt/rocm")
message(STATUS "[biergarten] AMD GPU detected — enabling HIP/ROCm acceleration.")
set(GGML_HIPBLAS ON CACHE BOOL "Enable HIP for AMD GPUs" FORCE)
else()
message(STATUS "[biergarten] No NVIDIA or AMD GPU found — falling back to CPU.")
endif()
endif()
# =============================================================================
# 2. Project-wide Settings (Standard & Optimization)
# =============================================================================
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Release Build Optimization: Aggressive (-O3), Arch-specific, and LTO
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -march=native -flto")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Og -g")
# =============================================================================
# 3. Dependencies
# =============================================================================
include(FetchContent)
find_package(CURL QUIET)
if(NOT CURL_FOUND)
message(FATAL_ERROR "[biergarten] libcurl not found. Install it (e.g. 'sudo dnf install libcurl-devel').")
endif()
# Require system Boost for JSON and Program Options to speed up build times
find_package(Boost REQUIRED COMPONENTS json program_options)
FetchContent_Declare(
sqlite_amalgamation
URL https://www.sqlite.org/2026/sqlite-amalgamation-3530000.zip
URL_HASH SHA3_256=c2325c53b3b41761469f91cfb078e96882ac5d85bac10c11b0bd8f253b031e5b
)
FetchContent_GetProperties(sqlite_amalgamation)
if(NOT sqlite_amalgamation_POPULATED)
FetchContent_Populate(sqlite_amalgamation)
endif()
if(NOT TARGET sqlite3)
add_library(sqlite3 STATIC
${sqlite_amalgamation_SOURCE_DIR}/sqlite3.c
)
target_include_directories(sqlite3 PUBLIC
${sqlite_amalgamation_SOURCE_DIR}
)
target_compile_definitions(sqlite3 PUBLIC
SQLITE_THREADSAFE=1
)
endif()
FetchContent_Declare(
llama-cpp
GIT_REPOSITORY https://github.com/ggml-org/llama.cpp.git
GIT_TAG b8742
)
FetchContent_MakeAvailable(llama-cpp)
FetchContent_Declare(
boost-di
GIT_REPOSITORY https://github.com/boost-ext/di.git
GIT_TAG v1.3.0
)
FetchContent_MakeAvailable(boost-di)
if(TARGET Boost.DI AND NOT TARGET boost::di)
add_library(boost::di ALIAS Boost.DI)
endif()
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.15.3
)
FetchContent_MakeAvailable(spdlog)
# =============================================================================
# 4. Sources
# =============================================================================
set(SOURCES
src/main.cc
src/biergarten_data_generator/biergarten_data_generator.cc
src/biergarten_data_generator/run.cc
src/biergarten_data_generator/query_cities_with_countries.cc
src/biergarten_data_generator/generate_breweries.cc
src/biergarten_data_generator/log_results.cc
src/services/wikipedia/wikipedia_service.cc
src/services/wikipedia/get_summary.cc
src/services/wikipedia/fetch_extract.cc
src/services/sqlite/sqlite_export_service.cc
src/services/sqlite/build_database_path.cc
src/services/sqlite/build_location_key.cc
src/services/sqlite/initialize_schema.cc
src/services/sqlite/prepare_statements.cc
src/services/sqlite/initialize.cc
src/services/sqlite/process_record.cc
src/services/sqlite/finalize_statements.cc
src/services/sqlite/rollback_and_close_no_throw.cc
src/services/sqlite/finalize.cc
src/web_client/curl_global_state.cc
src/web_client/curl_web_client_get.cc
src/web_client/curl_web_client_url_encode.cc
src/data_generation/llama/llama_generator.cc
src/data_generation/llama/generate_brewery.cc
src/data_generation/llama/generate_user.cc
src/data_generation/llama/helpers.cc
src/data_generation/llama/infer.cc
src/data_generation/llama/load.cc
src/data_generation/llama/load_brewery_prompt.cc
src/data_generation/prompt_formatting/gemma4_jinja_prompt_formatter.cc
src/data_generation/mock/deterministic_hash.cc
src/data_generation/mock/generate_brewery.cc
src/data_generation/mock/generate_user.cc
src/json_handling/json_loader.cc
)
# =============================================================================
# 5. Target
# =============================================================================
add_executable(${PROJECT_NAME} ${SOURCES})
target_include_directories(${PROJECT_NAME} PRIVATE
includes
${llama-cpp_SOURCE_DIR}/include
${llama-cpp_SOURCE_DIR}/common
)
target_link_libraries(${PROJECT_NAME} PRIVATE
llama
boost::di
Boost::json
Boost::program_options
spdlog::spdlog
sqlite3
CURL::libcurl
)
# =============================================================================
# 6. Runtime Assets
# =============================================================================
configure_file(
${CMAKE_SOURCE_DIR}/locations.json
${CMAKE_BINARY_DIR}/locations.json
COPYONLY
)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/prompts
${CMAKE_BINARY_DIR}/prompts
)

View File

@@ -1,344 +0,0 @@
# Biergarten Pipeline
A C++20 command-line pipeline that samples city records from local JSON, enriches each with Wikipedia context, and generates bilingual brewery names and descriptions via a local GGUF model or a deterministic mock.
---
## Table of Contents
- [How It Fits The Main App](#how-it-fits-the-main-app)
- [Tech Stack](#tech-stack)
- [Build](#build)
- [Model](#model)
- [Run](#run)
- [Architecture](#architecture)
- [Pipeline Stages](#pipeline-stages)
- [Key Components](#key-components)
- [Runtime Behaviour](#runtime-behaviour)
- [Generated Output](#generated-output)
- [Language Generation Quality](#language-generation-quality)
- [Known Issues](#known-issues)
- [Tested Hardware](#tested-hardware)
- [Repo Layout](#repo-layout)
- [Code Tour](#code-tour)
- [Fixture Strategy](#fixture-strategy)
- [Next Steps](#next-steps)
---
## How It Fits The Main App
The pipeline is a data ingestion layer. It sits outside the web app runtime and produces seed records the app imports at startup or during a dedicated seed step.
| Planned app area | Pipeline contribution |
| -------------------------------- | ------------------------------------------------------------------ |
| Brewery discovery and management | Sampled city records, localized names, long-form descriptions |
| Beer reviews and ratings | Stable brewery fixtures with enough context to anchor review pages |
| Social follow relationships | Repeatable brewery entities for feeds, follows, and saved lists |
| Geospatial brewery experiences | Latitude, longitude, and country-level metadata |
---
## Tech Stack
- C++20
- CMake 3.24+
- Boost.JSON, Boost.ProgramOptions, Boost.DI
- spdlog
- libcurl
- SQLite amalgamation fetched and compiled via CMake FetchContent
- llama.cpp
The build fetches Boost.DI, spdlog, llama.cpp, and SQLite via CMake. Metal is enabled on Apple Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
> **Code Style:** Modern C++20 throughout - RAII for ownership, `std::unique_ptr` for injected dependencies, `std::optional` for parse outcomes, `std::span` for read-only views over generated city data, structured bindings in pipeline loops. Formatting follows the Google C++ Style Guide via `.clang-format` with a narrow column limit and two-space indentation.
---
## Build
Requirements: C++20 compiler, CMake 3.24+, libcurl, Boost (JSON and ProgramOptions).
SQLite is fetched from the upstream amalgamation, so no system SQLite package is required.
```bash
cmake -S . -B build
cmake --build build
```
---
## Model
> Skip this step if you only need `--mocked`.
```bash
mkdir -p models
curl -L \
-o models/google_gemma-4-E4B-it-Q6_K.gguf \
https://huggingface.co/bartowski/google_gemma-4-E4B-it-GGUF/resolve/main/google_gemma-4-E4B-it-Q6_K.gguf?download=true
```
---
## Run
Run from `build/` so the copied `locations.json` and `prompts/` are available. Each run also writes a fresh dated SQLite file such as `biergarten_seed_2026-04-19T15-30-45.123456Z.sqlite` into the working directory.
```bash
./biergarten-pipeline --mocked
./biergarten-pipeline --model models/google_gemma-4-E4B-it-Q6_K.gguf --temperature 1.0 --top-p 0.95 --top-k 64 --n-ctx 8192 --seed -1
```
### CLI Flags
| Flag | Purpose |
| --------------- | ------------------------------------------------------- |
| `--mocked` | Deterministic mock generator, no model required. |
| `--model, -m` | Path to a GGUF file. Required unless `--mocked` is set. |
| `--temperature` | Sampling temperature. Default: `1.0`. |
| `--top-p` | Nucleus sampling. Default: `0.95`. |
| `--top-k` | Top-k sampling. Default: `64`. |
| `--n-ctx` | Context window size. Default: `8192`. |
| `--seed` | Random seed. Default: `-1` (random at runtime). |
| `--help, -h` | Print usage and exit. |
`--mocked` and `--model` are mutually exclusive. Omitting both exits with an error before the pipeline starts. Sampling flags are ignored when `--mocked` is set.
The post-build step copies `prompts/` into `build/prompts/`. Rebuild after editing `prompts/system.md`.
---
## Architecture
### Pipeline Stages
| Stage | Implementation |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Load | `JsonLoader::LoadLocations()` reads `locations.json` into typed `Location` records. |
| Sample | `BiergartenDataGenerator::QueryCitiesWithCountries()` samples up to 50 locations per run. |
| Enrich | `WikipediaService` fetches city and beer context. Keeps going when a lookup fails. |
| Generate | `MockGenerator` or `LlamaGenerator` produces brewery names and descriptions in English and the local language. |
| Store | `SqliteExportService` writes each successful brewery into a fresh dated `.sqlite` database with normalized location and brewery tables. |
| Log | `spdlog` writes results and warnings to the console. |
If enrichment or generation fails for a city, that city is skipped and the pipeline continues.
### Key Components
- `src/main.cc` - argument parsing and Boost.DI composition root.
- `JsonLoader` - validates curated location input.
- `WikipediaService` - queries Wikipedia extracts, caches results, returns empty context on failure.
- `LlamaGenerator` - formats prompts for Gemma 4, validates JSON output, retries malformed responses up to three times. If output looks truncated, the retry raises the token budget before trying again.
- `MockGenerator` - stable hash-based output so the same city input always produces the same brewery.
- `SqliteExportService` - creates a dated SQLite file per run and persists each successful brewery into normalized tables.
- Brewery payloads include English and local-language name and description fields.
### Runtime Behaviour
`WikipediaService` queries city, country, and beer-related Wikipedia extracts using its configured lookup, then caches the first successful response per query string. The fetched extract text is included in the prompt as context for generation.
`GetLocationContext()` returns an empty string when the web client is unavailable or when lookup/parsing fails.
`LlamaGenerator` validates model output as structured JSON. The retry path exists as a safety hatch for cases where the reasoning block consumes available token budget and compresses the JSON output space. All runs to date have produced valid output on the first pass; the path is kept for resilience.
`MockGenerator` uses stable hashes for repeatable output in demos and Storybook runs.
### Process Flow - Activity Diagram
![An activity diagram](./diagrams/activity-diagram.svg)
### Architectural Overview - Class Diagram
![A class diagram](./diagrams/class-diagram.svg)
---
## Generated Output
Each successful run stores a `GeneratedBrewery` pair with the source location and a `BreweryResult` payload. The same generated records are also written to a fresh SQLite export file named with the current UTC timestamp.
| Field | Meaning |
| ------------------- | ------------------------------------------ |
| `name_en` | Brewery name in English. |
| `description_en` | Brewery description in English. |
| `name_local` | Brewery name in the local language. |
| `description_local` | Brewery description in the local language. |
The log dump also includes city, country, state or province, ISO subdivision code, latitude, and longitude for each entry.
### Consumer Data Shape
| Field | Why it matters |
| ----------------------------------- | ------------------------------------------------ |
| `city`, `state_province`, `country` | Human-readable location labels and page headings |
| `iso3166_1`, `iso3166_2` | Filtering, regional grouping, locale matching |
| `latitude`, `longitude` | Map pins and nearby brewery views |
| `local_languages` | Locale-aware copy selection |
| `name_en`, `description_en` | Default English display content |
| `name_local`, `description_local` | Local-language display content |
| `region_context` | Richer copy for cards and detail pages |
---
## Language Generation Quality
The generation pipeline passes local language codes to the model to retrieve a translated `description_local`.
Output quality is reliable for high-resource languages such as French, though it may struggle with regional variants and idiomatic phrasing. This can be seen with these data points:
```json
[
{
"city": "Kinshasa",
"state_province": "Kinshasa",
"iso3166_2": "CD-KN",
"country": "Democratic Republic of the Congo",
"iso3166_1": "CD",
"latitude": -4.4419,
"longitude": 15.2663,
"local_languages": ["fr-CD", "ln"]
},
{
"city": "Paris",
"state_province": "Île-de-France",
"iso3166_2": "FR-IDF",
"country": "France",
"iso3166_1": "FR",
"latitude": 48.8566,
"longitude": 2.3522,
"local_languages": ["fr-FR"]
},
{
"city": "Abidjan",
"state_province": "Abidjan",
"iso3166_2": "CI-AB",
"country": "Ivory Coast",
"iso3166_1": "CI",
"latitude": 5.36,
"longitude": -4.0083,
"local_languages": ["fr-CI"]
},
{
"city": "Montreal",
"state_province": "Quebec",
"iso3166_2": "CA-QC",
"country": "Canada",
"iso3166_1": "CA",
"latitude": 45.5017,
"longitude": -73.5673,
"local_languages": ["fr-CA"]
},
{
"city": "Brussels",
"state_province": "Brussels-Capital Region",
"iso3166_2": "BE-BRU",
"country": "Belgium",
"iso3166_1": "BE",
"latitude": 50.8503,
"longitude": 4.3517,
"local_languages": ["fr-BE", "nl-BE"]
}
]
```
Output sample: [./out-sample/french-cities.example](out-sample/french-cities.example)
### Known Issues
#### Low-Resource Language Hallucination
For languages such as Welsh (Wales), Maori (Aotearoa/New Zealand), or Sicilian (Sicily, Italy), the model can generate text that looks syntactically plausible but is semantically incoherent. This comes from limited training-data coverage rather than prompt engineering.
#### Proposed Mitigations
- **Prevention via allowlist:** introduce a high-resource language allowlist. If a location's code is unlisted, skip `description_local` generation and fall back to English.
- **Upstream sanitization:** strip known low-resource language codes from the `locations.json` payload before generation.
- **Downstream flagging:** add a `description_local_confidence` column to the SQLite schema so downstream applications can filter or flag potentially hallucinated text by language tier.
---
## Tested Hardware
### ARM macOS - M1 Pro
| | |
| --------- | --------------------------------- |
| Host | MacBook Pro 14" (2021) |
| CPU | Apple M1 Pro (8-core) |
| GPU | Apple M1 Pro (14-core integrated) |
| Memory | 16 GB |
| Model | Gemma 4 E4B |
| Inference | llama.cpp with Metal |
### x86_64 Linux - NVIDIA RTX 2000
| | |
| --------- | ------------------------------ |
| Host | ThinkPad P1 Gen 7 (Fedora 43) |
| CPU | Intel Core Ultra 7 155H |
| GPU | NVIDIA RTX 2000 Ada Generation |
| Memory | 32 GB |
| Model | Gemma 4 E4B |
| Inference | llama.cpp with CUDA 12.x |
---
## Repo Layout
| Path | Purpose |
| ---------------- | ---------------------------------------------- |
| `includes/` | Public headers and shared models. |
| `src/` | Implementation files. |
| `locations.json` | Curated city input copied into the build tree. |
| `prompts/` | System prompt used by the model-backed path. |
| `diagrams/` | Architecture and pipeline diagrams. |
---
## Code Tour
- `src/main.cc` - argument parsing and DI composition root.
- `src/biergarten_data_generator/` - orchestration, sampling, logging, and export.
- `src/services/wikipedia/` - enrichment service and cache.
- `src/services/sqlite/` - SQLite export implementation.
- `src/data_generation/llama/` - local inference, prompt loading, output validation.
- `src/data_generation/mock/` - deterministic fallback.
---
## Fixture Strategy
- `--mocked` for stable fixtures, repeatable screenshots, and Storybook runs.
- `--model` when geographically grounded content matters for demos.
- Keep `locations.json` structured enough to support discovery and future filtering.
- Treat SQLite output as seed material for the app's brewery domain, not production data.
---
## Next Steps
The pipeline currently produces city-aware brewery records and dated SQLite exports. The next passes add additional fixture types so the app can exercise the full brewery domain without live data.
### Testing _(Very High Importance)_
- Unit test JSON validation and retry logic against malformed, truncated, and empty model outputs.
- Integration test the enrichment pipeline with missing context, short context, and fake context inputs.
- Adversarial context tests: feed plausible but geographically incorrect Wikipedia extracts and verify the model does not silently blend them with training data.
- Verify bilingual enrichment behaviour when only an English extract is available versus when both extracts are present.
- Confirm the retry path is reachable when the reasoning block consumes available token budget.
### Beer Generation
Generate catalog entries with style, ABV, IBU, color, aroma notes, and food pairing hints. Link beers back to breweries and cities. Keep style coverage wide enough to exercise search, sort, and category filters.
### User Generation
Generate user profiles with stable names, bios, locale hints, and preference signals. Include stable IDs for downstream fixture joins. Keep output deterministic for screenshots while allowing larger randomized batches.
### Check-In System
Produce timestamped check-in events between users and breweries. Use a J-curve activity profile - a small set of users accounts for most check-ins, the rest appear occasionally. Add bursty behaviour around weekends and travel periods.
### Beer Ratings
Generate rating events with a strong positive skew and a long tail of lower scores. Avoid uniform distributions. Attach timestamps and user IDs so the app can compute averages, trends, and per-style comparisons.

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,262 +0,0 @@
@startuml biergarten_activity
skinparam defaultFontName "DM Sans"
skinparam defaultFontSize 13
skinparam titleFontName "Volkhov"
skinparam titleFontSize 20
skinparam backgroundColor #FCFCF7
skinparam defaultFontColor #14180C
skinparam titleFontColor #14180C
skinparam ArrowColor #656F33
skinparam activityStartColor #EBECE3
skinparam activityEndColor #4A5837
skinparam activityStopColor #4A5837
skinparam ActivityBackgroundColor #EBECE3
skinparam ActivityBorderColor #4A5837
skinparam ActivityDiamondBackgroundColor #CBD2B5
skinparam ActivityDiamondBorderColor #4A5837
skinparam NoteBackgroundColor #DBEEDD
skinparam NoteFontColor #14180C
skinparam NoteBorderColor #4A5837
skinparam SwimlaneBorderColor #4A5837
skinparam SwimlaneBorderThickness 1
skinparam monochrome reverse
title The Biergarten Data Pipeline — Activity Diagram
|Main|
start
:ParseArguments(argc, argv);
if (Invalid args?) then (yes)
:spdlog::error;
stop
else (no)
endif
:Init CurlGlobalState & LlamaBackendState;
:Build DI injector;
:Initialize SqliteExportService;
note right
Opens SQLite connection.
Begins a single transaction
covering all five fixture types.
end note
:Create BoundedChannel<LogEntry> log_ch;
:Spawn Log Worker thread;
note right
Log worker drains log_ch for the
entire pipeline lifetime.
All workers emit LogEntry structs
via PipelineLogger — never spdlog directly.
end note
:BiergartenPipelineOrchestrator::Run();
|BiergartenPipelineOrchestrator::Run()|
:JsonLoader::LoadLocations("locations.json");
:JsonLoader::LoadBeerStyles("beer-styles.json");
:JsonLoader::LoadPersonas("personas.json");
:JsonLoader::LoadNamesByCountry("names-by-country.json");
:EnrichmentService::PreWarmBeerStyleCache(beer_styles);
note right
Beer styles do not need location context.
Wikipedia summaries for the entire palette are
fetched and cached globally at startup.
end note
:EnrichmentService::PreWarmPersonaCache(personas);
note right
Persona descriptions do not need location context.
All persona lookups are resolved and cached
globally at startup.
end note
' ═══════════════════════════════════════════
' PHASE 0 — USER GENERATION
' ═══════════════════════════════════════════
|Orchestrator|
:RunUserPhase(sampled_locations);
:Create BoundedChannels\n(loc_ch, llm_ch, exp_ch);
fork
|Orchestrator|
:Loop: Send Locations → loc_ch;
:Close loc_ch;
fork again
|LLM Worker|
while (loc_ch has items?) is (yes)
:Receive Location;
:IPersonaSelectionStrategy::SelectPersona(\n personas_palette_);
note right
Guaranteed cache hit from startup.
Returns a Persona struct carrying
style_affinities, abv_range,
ibu_preference, checkin_weight.
end note
:NamesByCountry::SampleName(\n location.iso3166_1);
note right
Deterministic lookup — no LLM involved.
Name selected from pre-keyed table
and passed into the generation prompt.
end note
:GenerateUser(location, persona, sampled_name)\nvia DataGenerator;
note right
LLM receives: Location fields + persona
description + sampled name. Generates
bio and preference signals grounded
in locale and persona.
end note
:PipelineLogger::Log(Info, UserGeneration,\n city, user_id, "llm");
:Send GeneratedUser → llm_ch;
endwhile (no)
:Close llm_ch;
fork again
|SQLite Worker|
while (llm_ch has items?) is (yes)
:Receive GeneratedUser;
:ProcessUser(user) → sqlite3_int64;
:PipelineLogger::Log(Info, UserGeneration,\n city, user_id, "sqlite");
:Append → user_pool_;
endwhile (no)
end fork
|Orchestrator|
:Join LLM Worker, SQLite Worker;
' ═══════════════════════════════════════════
' PHASE 1 — BREWERY & BEER GENERATION
' ═══════════════════════════════════════════
: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);
:PipelineLogger::Log(Info,\n BreweryAndBeerGeneration,\n city, nullopt, "enrichment");
: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)
:PipelineLogger::Log(Info,\n BreweryAndBeerGeneration,\n city, brewery_id, "llm");
:Send BreweryWithBeers Bundle → exp_ch;
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)
:PipelineLogger::Log(Info,\n BreweryAndBeerGeneration,\n city, brewery_id, "sqlite");
endwhile (no)
end fork
|Orchestrator|
:Join LLM Worker, SQLite Worker;
note right
Both brewery_pool_ and beer_pool_
are now completely populated.
end note
' ═══════════════════════════════════════════
' PHASE 2 — CHECKIN GENERATION
' ═══════════════════════════════════════════
:RunCheckinPhase();
:ICheckinDistributionStrategy::\nAssignActivityWeights(user_pool_);
note right
Weights seeded from each user's
persona.checkin_weight. J-curve profile
emerges from persona distribution.
end note
while (For each GeneratedUser in user_pool_?) is (remaining)
:CheckinsForUser(user, brewery_pool_.size());
while (For each checkin index?) is (remaining)
:TimestampFor(user, index);
:Select brewery from brewery_pool_;
:GenerateCheckin(user, brewery, timestamp)\nvia DataGenerator;
:ProcessCheckin(checkin) → sqlite3_int64;
:PipelineLogger::Log(Info, CheckinGeneration,\n nullopt, checkin_id, "sqlite");
:Append → checkin_pool_;
endwhile (done)
endwhile (done)
' ═══════════════════════════════════════════
' PHASE 3 — RATING GENERATION
' ═══════════════════════════════════════════
:RunRatingPhase();
note right
Beer selection biased by
user.persona.style_affinities and abv_range.
Rating skew modulated per persona.
end note
while (For each GeneratedCheckin in checkin_pool_?) is (remaining)
:Match brewery_id → select beer from beer_pool_\n(same brewery_id, biased by persona affinities);
if (Beer exists for brewery?) then (yes)
:GenerateRating(user, beer, checkin_id)\nvia DataGenerator;
:ProcessRating(rating);
:PipelineLogger::Log(Info, RatingGeneration,\n nullopt, rating_id, "sqlite");
else (no)
:PipelineLogger::Log(Warn, RatingGeneration,\n nullopt, brewery_id, "sqlite");
:Skip — brewery has no beers;
endif
endwhile (done)
' ═══════════════════════════════════════════
' TEARDOWN
' ═══════════════════════════════════════════
|Main|
:Finalize SqliteExportService;
note right
COMMIT covers all five fixture types.
end note
:Close log_ch;
:Join Log Worker;
note right
Drain guarantees no LogEntry is
dropped at shutdown.
end note
:spdlog::info "Pipeline complete in X ms";
stop
@enduml

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,42 +0,0 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_APPLICATION_OPTIONS_H_
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_APPLICATION_OPTIONS_H_
/**
* @file data_model/application_options.h
* @brief Program options for the Biergarten pipeline application.
*/
#include <cstdint>
#include <string>
/**
* @brief Program options for the Biergarten pipeline application.
*/
struct ApplicationOptions {
/// @brief Path to the LLM model file (gguf format); mutually exclusive with
/// use_mocked.
std::string model_path;
/// @brief Use mocked generator instead of LLM; mutually exclusive with
/// model_path.
bool use_mocked = false;
/// @brief LLM sampling temperature (0.0 to 1.0, higher = more random).
float temperature = 1.0F;
/// @brief LLM nucleus sampling top-p parameter (0.0 to 1.0, higher = more
/// random).
float top_p = 0.95F;
/// @brief LLM top-k sampling parameter.
uint32_t top_k = 64;
/// @brief Context window size (tokens) for LLM inference. Higher values
/// support longer prompts but use more memory.
uint32_t n_ctx = 8192;
/// @brief Random seed for sampling (-1 for random, otherwise non-negative).
int seed = -1;
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_APPLICATION_OPTIONS_H_

View File

@@ -1,22 +0,0 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_BREWERY_LOCATION_H_
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_BREWERY_LOCATION_H_
/**
* @file data_model/brewery_location.h
* @brief Non-owning brewery location input.
*/
#include <string_view>
/**
* @brief Non-owning brewery location input.
*/
struct BreweryLocation {
/// @brief City name.
std::string_view city_name;
/// @brief Country name.
std::string_view country_name;
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_BREWERY_LOCATION_H_

View File

@@ -1,28 +0,0 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_BREWERY_RESULT_H_
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_BREWERY_RESULT_H_
/**
* @file data_model/brewery_result.h
* @brief Generated brewery payload.
*/
#include <string>
/**
* @brief Generated brewery payload.
*/
struct BreweryResult {
/// @brief Brewery display name in English.
std::string name_en;
/// @brief Brewery description text in English.
std::string description_en;
/// @brief Brewery display name in the local language.
std::string name_local;
/// @brief Brewery description text in the local language.
std::string description_local;
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_BREWERY_RESULT_H_

View File

@@ -1,21 +0,0 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_ENRICHED_CITY_H_
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_ENRICHED_CITY_H_
/**
* @file data_model/enriched_city.h
* @brief Enriched city data with Wikipedia context.
*/
#include <string>
#include "data_model/location.h"
/**
* @brief Enriched city data with Wikipedia context.
*/
struct EnrichedCity {
Location location;
std::string region_context{};
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_ENRICHED_CITY_H_

View File

@@ -1,20 +0,0 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATED_BREWERY_H_
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATED_BREWERY_H_
/**
* @file data_model/generated_brewery.h
* @brief Helper struct to store generated brewery data.
*/
#include "data_model/brewery_result.h"
#include "data_model/location.h"
/**
* @brief Helper struct to store generated brewery data.
*/
struct GeneratedBrewery {
Location location;
BreweryResult brewery;
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATED_BREWERY_H_

View File

@@ -1,13 +0,0 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATION_MODELS_H_
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATION_MODELS_H_
/**
* @file data_model/generation_models.h
* @brief Convenience include for shared generation payload models.
*/
#include "data_model/brewery_location.h"
#include "data_model/brewery_result.h"
#include "data_model/user_result.h"
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATION_MODELS_H_

View File

@@ -1,41 +0,0 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_LOCATION_H_
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_LOCATION_H_
/**
* @file data_model/location.h
* @brief Location data model used throughout generation pipeline.
*/
#include <string>
#include <vector>
/**
* @brief Canonical location record for city-level generation.
*/
struct Location {
/// @brief City name.
std::string city{};
/// @brief State or province name.
std::string state_province{};
/// @brief ISO 3166-2 subdivision code.
std::string iso3166_2{};
/// @brief Country name.
std::string country{};
/// @brief ISO 3166-1 country code.
std::string iso3166_1{};
/// @brief Local language codes in priority order.
std::vector<std::string> local_languages{};
/// @brief Latitude in decimal degrees.
double latitude{};
/// @brief Longitude in decimal degrees.
double longitude{};
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_LOCATION_H_

View File

@@ -1,12 +0,0 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_PIPELINE_MODELS_H_
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_PIPELINE_MODELS_H_
/**
* @file data_model/pipeline_models.h
* @brief Convenience include for pipeline-specific data models.
*/
#include "data_model/enriched_city.h"
#include "data_model/generated_brewery.h"
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_PIPELINE_MODELS_H_

View File

@@ -1,22 +0,0 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_USER_RESULT_H_
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_USER_RESULT_H_
/**
* @file data_model/user_result.h
* @brief Generated user profile payload.
*/
#include <string>
/**
* @brief Generated user profile payload.
*/
struct UserResult {
/// @brief Username handle.
std::string username{};
/// @brief Short user biography.
std::string bio{};
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_USER_RESULT_H_

View File

@@ -1,250 +0,0 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_SQLITE_EXPORT_SERVICE_HELPERS_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_SQLITE_EXPORT_SERVICE_HELPERS_H_
/**
* @file services/sqlite_export_service_helpers.h
* @brief Internal SQLite export helpers shared across per-method translation
* units.
*/
#include <sqlite3.h>
#include <boost/json.hpp>
#include <cstddef>
#include <cstring>
#include <filesystem>
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace sqlite_export_service_internal {
struct SqliteDatabaseDeleter {
void operator()(sqlite3* handle) const noexcept {
if (handle != nullptr) {
sqlite3_close(handle);
}
}
};
struct SqliteStatementDeleter {
void operator()(sqlite3_stmt* statement) const noexcept {
if (statement != nullptr) {
sqlite3_finalize(statement);
}
}
};
using SqliteDatabaseHandle = std::unique_ptr<sqlite3, SqliteDatabaseDeleter>;
using SqliteStatementHandle =
std::unique_ptr<sqlite3_stmt, SqliteStatementDeleter>;
inline constexpr std::string_view kCreateLocationsTableSql = R"sql(
CREATE TABLE IF NOT EXISTS locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
city TEXT NOT NULL,
state_province TEXT NOT NULL,
iso3166_2 TEXT NOT NULL,
country TEXT NOT NULL,
iso3166_1 TEXT NOT NULL,
local_languages_json TEXT NOT NULL,
latitude REAL NOT NULL,
longitude REAL NOT NULL,
UNIQUE(city, state_province, iso3166_2, country, latitude, longitude)
);
)sql";
inline constexpr std::string_view kCreateBreweriesTableSql = R"sql(
CREATE TABLE IF NOT EXISTS breweries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
location_id INTEGER NOT NULL,
name_en TEXT NOT NULL,
description_en TEXT NOT NULL,
name_local TEXT NOT NULL,
description_local TEXT NOT NULL,
FOREIGN KEY(location_id) REFERENCES locations(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_breweries_location_id ON breweries(location_id);
)sql";
inline constexpr std::string_view kInsertLocationSql = R"sql(
INSERT INTO locations (
city,
state_province,
iso3166_2,
country,
iso3166_1,
local_languages_json,
latitude,
longitude
) VALUES (?, ?, ?, ?, ?, ?, ?, ?);
)sql";
inline constexpr std::string_view kInsertBrewerySql = R"sql(
INSERT INTO breweries (
location_id,
name_en,
description_en,
name_local,
description_local
) VALUES (?, ?, ?, ?, ?);
)sql";
inline constexpr int kLocationCityBindIndex = 1;
inline constexpr int kLocationStateProvinceBindIndex = 2;
inline constexpr int kLocationIso31662BindIndex = 3;
inline constexpr int kLocationCountryBindIndex = 4;
inline constexpr int kLocationIso31661BindIndex = 5;
inline constexpr int kLocationLanguagesBindIndex = 6;
inline constexpr int kLocationLatitudeBindIndex = 7;
inline constexpr int kLocationLongitudeBindIndex = 8;
inline constexpr int kBreweryLocationIdBindIndex = 1;
inline constexpr int kBreweryEnglishNameBindIndex = 2;
inline constexpr int kBreweryEnglishDescriptionBindIndex = 3;
inline constexpr int kBreweryLocalNameBindIndex = 4;
inline constexpr int kBreweryLocalDescriptionBindIndex = 5;
inline void ThrowSqliteError(sqlite3* db_handle, std::string_view action) {
const std::string message =
db_handle != nullptr ? sqlite3_errmsg(db_handle) : "unknown SQLite error";
throw std::runtime_error(std::string(action) + ": " + message);
}
inline SqliteDatabaseHandle OpenDatabase(const std::filesystem::path& path) {
sqlite3* raw_handle = nullptr;
const std::string path_string = path.string();
const int result = sqlite3_open(path_string.c_str(), &raw_handle);
SqliteDatabaseHandle handle(raw_handle);
if (result != SQLITE_OK) {
const std::string message = raw_handle != nullptr
? sqlite3_errmsg(raw_handle)
: "unknown SQLite error";
throw std::runtime_error("Failed to open SQLite export database: " +
message);
}
return handle;
}
inline void ExecSql(const SqliteDatabaseHandle& db_handle, std::string_view sql,
const char* action) {
char* error_message = nullptr;
const std::string sql_text(sql);
const int result = sqlite3_exec(db_handle.get(), sql_text.c_str(), nullptr,
nullptr, &error_message);
if (result != SQLITE_OK) {
const std::string message = error_message != nullptr
? error_message
: sqlite3_errmsg(db_handle.get());
sqlite3_free(error_message);
throw std::runtime_error(std::string(action) + ": " + message);
}
}
inline SqliteStatementHandle PrepareStatement(
const SqliteDatabaseHandle& db_handle, std::string_view sql,
const char* action) {
sqlite3_stmt* raw_statement = nullptr;
const std::string sql_text(sql);
const int result = sqlite3_prepare_v2(db_handle.get(), sql_text.c_str(), -1,
&raw_statement, nullptr);
SqliteStatementHandle statement(raw_statement);
if (result != SQLITE_OK) {
ThrowSqliteError(db_handle.get(), action);
}
return statement;
}
inline void ResetStatement(SqliteStatementHandle& statement) {
if (statement != nullptr) {
sqlite3_reset(statement.get());
sqlite3_clear_bindings(statement.get());
}
}
inline void DeleteCharArray(void* data) noexcept {
delete[] static_cast<char*>(data);
}
inline void BindText(const SqliteStatementHandle& statement, int index,
std::string_view value, const char* action) {
const auto byte_count = value.size();
if (byte_count > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
ThrowSqliteError(sqlite3_db_handle(statement.get()), action);
}
auto buffer = std::make_unique<char[]>(byte_count + 1);
std::memcpy(buffer.get(), value.data(), byte_count);
buffer[byte_count] = '\0';
char* raw_buffer = buffer.release();
const int result =
sqlite3_bind_text(statement.get(), index, raw_buffer,
static_cast<int>(byte_count), DeleteCharArray);
if (result != SQLITE_OK) {
DeleteCharArray(raw_buffer);
ThrowSqliteError(sqlite3_db_handle(statement.get()), action);
}
}
inline void BindDouble(const SqliteStatementHandle& statement, int index,
double value, std::string_view action) {
const int result = sqlite3_bind_double(statement.get(), index, value);
if (result != SQLITE_OK) {
ThrowSqliteError(sqlite3_db_handle(statement.get()), action);
}
}
inline void BindInt64(const SqliteStatementHandle& statement, int index,
sqlite3_int64 value, std::string_view action) {
const int result = sqlite3_bind_int64(statement.get(), index, value);
if (result != SQLITE_OK) {
ThrowSqliteError(sqlite3_db_handle(statement.get()), action);
}
}
inline void StepStatement(const SqliteDatabaseHandle& db_handle,
const SqliteStatementHandle& statement,
std::string_view action) {
const int result = sqlite3_step(statement.get());
if (result != SQLITE_DONE) {
ThrowSqliteError(db_handle.get(), action);
}
}
inline sqlite3_int64 LastInsertRowId(const SqliteDatabaseHandle& db_handle) {
return sqlite3_last_insert_rowid(db_handle.get());
}
inline void RollbackTransactionNoThrow(
const SqliteDatabaseHandle& db_handle) noexcept {
if (!db_handle) {
return;
}
sqlite3_exec(db_handle.get(), "ROLLBACK;", nullptr, nullptr, nullptr);
}
inline std::string SerializeLocalLanguages(
const std::vector<std::string>& local_languages) {
boost::json::array array;
array.reserve(local_languages.size());
for (const auto& language : local_languages) {
array.emplace_back(language);
}
return boost::json::serialize(array);
}
} // namespace sqlite_export_service_internal
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_SQLITE_EXPORT_SERVICE_HELPERS_H_

View File

@@ -1,54 +0,0 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_WEB_CLIENT_CURL_WEB_CLIENT_H_
#define BIERGARTEN_PIPELINE_INCLUDES_WEB_CLIENT_CURL_WEB_CLIENT_H_
/**
* @file web_client/curl_web_client.h
* @brief libcurl-based WebClient implementation.
*/
#include "web_client/web_client.h"
/**
* @brief RAII wrapper for curl_global_init and curl_global_cleanup.
*
* Create one instance in application startup before using libcurl and keep it
* alive for application lifetime.
*/
class CurlGlobalState {
public:
/// @brief Initializes global libcurl state.
CurlGlobalState();
/// @brief Cleans up global libcurl state.
~CurlGlobalState();
/// @brief Non-copyable type.
CurlGlobalState(const CurlGlobalState&) = delete;
/// @brief Non-copyable type.
CurlGlobalState& operator=(const CurlGlobalState&) = delete;
};
/**
* @brief WebClient implementation backed by libcurl.
*/
class CURLWebClient : public WebClient {
public:
/**
* @brief Executes an HTTP GET request.
*
* @param url Request URL.
* @return Response body.
*/
std::string Get(const std::string& url) override;
/**
* @brief URL-encodes a string value.
*
* @param value Raw value.
* @return URL-encoded string.
*/
std::string UrlEncode(const std::string& value) override;
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_WEB_CLIENT_CURL_WEB_CLIENT_H_

View File

@@ -1,113 +0,0 @@
# FULL SYSTEM PROMPT
You are an expert brewery copywriter, an architectural observer, and a master of zymurgy.
Your main goal is to come up with a fake, contextually accurate name and a matching description for a craft brewery located in a specific city. You need to base this on the exact geographic and cultural info provided. You also need to seamlessly blend historical background, cultural details, and highly specialized brewing methods to create a realistic and interesting story.
You will receive the inputs like this:
## CITY:
[City Name]
## COUNTRY:
[Country Name]
## LOCAL LANGUAGE CODES:
[Local language codes in priority order]
## CONTEXT:
[Information about local beer culture, history, geography, or language context]
## CRITICAL OUTPUT FORMAT (READ CAREFULLY):
ABSOLUTELY NO MARKDOWN FORMATTING. Do NOT wrap your response in json or ``` blocks.
Do not add markdown, code fences, or postscript around the final JSON object. Do not say "Here is the JSON" or "Enjoy!".
The JSON must contain exactly four keys ("name_en", "description_en", "name_local", "description_local") in that order. Do not rename or add any other keys.
ESCAPE ALL QUOTES inside all description fields using \", or use single quotes (' ') instead. This applies equally to description_en and description_local. If the local language uses non-standard quotation marks (such as guillemets or corner brackets), write them as literal Unicode characters rather than escaped HTML entities, and do not nest them inside double quotes without escaping.
DO NOT use actual line breaks (\n) inside any string. Keep all descriptions as one continuous string each.
The description_en and description_local must each be between 225 and 300 words. Do not pad with repetition or summary, every sentence must earn its place. Be concise and specific.
Expected JSON format:
```json
{
"name_en": "Fictional Local Brewery Name in English",
"description_en": "The English description goes here.",
"name_local": "Translated brewery name in the local language",
"description_local": "The localised description goes here."
}
```
## CONTENT RULES AND CONSTRAINTS:
### THE HOOK:
The first sentence must be a sensory environmental hook written as a personal observation, something the owner notices or has always noticed. It should establish the local weather, smell, or soundscape of the city. Do not open with the brewery's name or a generic welcome.
### GEOGRAPHIC & CULTURAL ANCHOR:
The story must be deeply tied to the provided geographic and cultural info. Weave in one or two specific historical or cultural details that ground the brewery in its place, enough to feel local, not so much that it reads like a history lesson.
### TECHNICAL BREWING DETAIL (VARY THIS!):
You must include one highly specialized technical brewing detail. To avoid sounding repetitive, make sure this varies a lot. Some examples: using local wild yeast (like spontaneous Brettanomyces), adjusting the water profile (like Burtonization), specific mashing techniques, or using local barrels for aging. Don't use basic concepts like generic mash temperatures.
### ARCHITECTURAL DETAIL (VARY THIS!):
You must include one specific architectural or environmental detail, highlighting the building's physical wear, structure, or history. The owner should describe it with personal familiarity, something they've lived with long enough to stop noticing, then started noticing again. Avoid overused industry clichés like repurposed dairy equipment or glycol chillers.
### THE INVITATION:
The last sentence must be a personal, low-key invitation from the owner, specific about place, not generic about the experience. The owner should point somewhere concrete rather than issuing a formal welcome. Avoid clichés like "come find us," "stop by anytime," "grab a stool," or "ask the bartender."
### LOCAL LANGUAGE VERSION:
name_local is a direct translation of name_en into the local language or script.
Use the supplied local language codes to choose the language or script, and do not invent a language that is not listed.
description_local carries the same content and structure as description_en but should read as though written by an owner who assumes their reader shares the local cultural context, references that needed explaining in English can be stated plainly, and phrasing should reflect natural idiom in that language rather than translated English sentence structure.
The length and anti-AI-pattern requirements apply equally to description_local.
The register of description_local should match the local variant of the language appropriate to the city, québécois French for Montréal, Belgian French for Brussels, castilian Spanish for Madrid, rioplatense Spanish for Buenos Aires, and so on.
### THE BLOCKLIST (FORBIDDEN CONCEPTS):
You absolutely cannot use the following words and phrases. Make sure your final output doesn't have any of these:
- "hidden gem"
- "passion"
- "authentic"
- "repurposed dairy tank"
- "repurposed industrial vat"
- "concrete eggs"
- "glycol chiller"
- "mash temperature"
- "grab a stool"
- "ask the bartender"
- "come find us"
- "stop by anytime"
#### FORBIDDEN WRITING PATTERNS
The following patterns are common AI writing pitfalls and must not appear in either description:
- Negative parallelism constructions: "It's not X, it's Y" or "We're not about X, we're about Y"
- Inflated significance phrases: "stands as a testament," "plays a vital role," "leaves a lasting impact," "watershed moment," "deeply rooted," "rich cultural heritage," "rich cultural tapestry," "enduring legacy"
- Superficial trailing analyses: sentences ending in -ing words that add opinion without content ("ensuring consistency," "reflecting the city's spirit," "highlighting our commitment")
- Promotional travel-copy tone: "breathtaking," "must-visit," "stunning," "vibrant"
- Overused conjunctive transitions used as sentence openers: "Moreover," "Furthermore," "In addition," "In contrast"
- Rule of three: do not consistently organise ideas or examples in triplets
### VOICE & PERSPECTIVE:
The description must be written in the first person, from the perspective of the brewery's owner. Favour "we" and "our" over "I" and "my." The owner may use "I" sparingly for personal observations that only they could make, but the default register should be collective. The tone should feel lived-in and a little weathered. Do not use third-person or second-person pronouns.

View File

@@ -1,55 +0,0 @@
/**
* @file data_generation/llama/load_brewery_prompt.cc
* @brief Resolves brewery system prompt content from cache or a configured
* filesystem path and provides a robust inline fallback prompt when absent.
*/
#include <spdlog/spdlog.h>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include "data_generation/llama_generator.h"
/**
* @brief Loads brewery system prompt from disk or cache.
*
* @param prompt_file_path Preferred prompt file location.
* @return Prompt text loaded from disk.
*/
std::string LlamaGenerator::LoadBrewerySystemPrompt(
const std::filesystem::path& prompt_file_path) {
// Return cached version if already loaded
if (!brewery_system_prompt_.empty()) {
return brewery_system_prompt_;
}
std::ifstream prompt_file(prompt_file_path);
if (!prompt_file.is_open()) {
spdlog::error(
"LlamaGenerator: Failed to open brewery system prompt file '{}'",
prompt_file_path.string());
throw std::runtime_error(
"LlamaGenerator: missing brewery system prompt file: " +
prompt_file_path.string());
}
const std::string prompt((std::istreambuf_iterator(prompt_file)),
std::istreambuf_iterator<char>());
prompt_file.close();
if (prompt.empty()) {
spdlog::error("LlamaGenerator: Brewery system prompt file '{}' is empty",
prompt_file_path.string());
throw std::runtime_error(
"LlamaGenerator: empty brewery system prompt file: " +
prompt_file_path.string());
}
spdlog::info(
"LlamaGenerator: Loaded brewery system prompt from '{}' ({} chars)",
prompt_file_path.string(), prompt.length());
brewery_system_prompt_ = prompt;
return brewery_system_prompt_;
}

View File

@@ -1,198 +0,0 @@
/**
* @file main.cc
* @brief Parses command-line options, validates runtime mode selection,
* initializes shared infrastructure, and executes the pipeline entry flow.
*/
#include <spdlog/spdlog.h>
#include <boost/di.hpp>
#include <boost/program_options.hpp>
#include <chrono>
#include <exception>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include "biergarten_data_generator.h"
#include "data_generation/llama_generator.h"
#include "data_generation/mock_generator.h"
#include "data_generation/prompt_formatting/gemma4_jinja_prompt_formatter.h"
#include "data_model/application_options.h"
#include "llama_backend_state.h"
#include "services/enrichment_service.h"
#include "services/export_service.h"
#include "services/sqlite_export_service.h"
#include "services/wikipedia_service.h"
#include "web_client/curl_web_client.h"
namespace prog_opts = boost::program_options;
namespace di = boost::di;
/**
* @brief Parse command-line arguments into ApplicationOptions.
*
* @param argc Command-line argument count.
* @param argv Command-line arguments.
* @return Parsed ApplicationOptions if parsing succeeded, std::nullopt
* otherwise.
*/
std::optional<ApplicationOptions> ParseArguments(const int argc, char** argv) {
prog_opts::options_description desc("Pipeline Options");
auto opt = desc.add_options();
opt("help,h", "Produce help message");
opt("mocked", prog_opts::bool_switch(),
"Use mocked generator for brewery/user data");
opt("model,m", prog_opts::value<std::string>()->default_value(""),
"Path to LLM model (gguf)");
opt("temperature", prog_opts::value<float>()->default_value(1.0F),
"Sampling temperature (higher = more random)");
opt("top-p", prog_opts::value<float>()->default_value(0.95F),
"Nucleus sampling top-p in (0,1] (higher = more random)");
opt("top-k", prog_opts::value<uint32_t>()->default_value(64),
"Top-k sampling parameter (higher = more candidate tokens)");
opt("n-ctx", prog_opts::value<uint32_t>()->default_value(8192),
"Context window size in tokens (1-32768)");
opt("seed", prog_opts::value<int>()->default_value(-1),
"Sampler seed: -1 for random, otherwise non-negative integer");
// Handle the "no arguments" or "help" case
if (argc == 1) {
spdlog::info("Biergarten Pipeline");
std::stringstream usage_stream;
usage_stream << "\nUsage: biergarten-pipeline [options]\n\n" << desc;
spdlog::info(usage_stream.str());
return std::nullopt;
}
try {
prog_opts::variables_map variables_map;
prog_opts::store(prog_opts::parse_command_line(argc, argv, desc),
variables_map);
prog_opts::notify(variables_map);
if (variables_map.contains("help")) {
std::stringstream help_stream;
help_stream << "\n" << desc;
spdlog::info(help_stream.str());
return std::nullopt;
}
const auto use_mocked = variables_map["mocked"].as<bool>();
const auto model_path = variables_map["model"].as<std::string>();
if (use_mocked && !model_path.empty()) {
spdlog::error(
"Invalid arguments: --mocked and --model are mutually exclusive");
return std::nullopt;
}
if (!use_mocked && model_path.empty()) {
spdlog::error(
"Invalid arguments: Either --mocked or --model must be specified");
return std::nullopt;
}
const bool has_llm_params = !variables_map["temperature"].defaulted() ||
!variables_map["top-p"].defaulted() ||
!variables_map["top-k"].defaulted() ||
!variables_map["seed"].defaulted();
if (use_mocked && has_llm_params) {
spdlog::warn(
"Sampling parameters (--temperature, --top-p, --top-k, --seed) are"
" ignored when using --mocked");
}
ApplicationOptions options;
options.use_mocked = use_mocked;
options.model_path = model_path;
options.temperature = variables_map["temperature"].as<float>();
options.top_p = variables_map["top-p"].as<float>();
options.top_k = variables_map["top-k"].as<uint32_t>();
options.n_ctx = variables_map["n-ctx"].as<uint32_t>();
options.seed = variables_map["seed"].as<int>();
return options;
} catch (const std::exception& exception) {
spdlog::error("Failed to parse command-line arguments: {}",
exception.what());
return std::nullopt;
} catch (...) {
spdlog::error("Failed to parse command-line arguments: unknown error");
return std::nullopt;
}
}
struct Timer {
std::chrono::steady_clock::time_point start_time =
std::chrono::steady_clock::now();
[[nodiscard]] int64_t Elapsed() const {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start_time)
.count();
}
};
int main(const int argc, char** argv) {
try {
Timer timer;
const CurlGlobalState curl_state;
const LlamaBackendState llama_backend_state;
spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v");
const auto parsed_options = ParseArguments(argc, argv);
if (!parsed_options.has_value()) {
return 0;
}
const auto options = *parsed_options;
const auto injector = di::make_injector(
di::bind<WebClient>().to<CURLWebClient>(),
di::bind<ApplicationOptions>().to(options),
di::bind<IEnrichmentService>().to<WikipediaService>(),
di::bind<IExportService>().to<SqliteExportService>(),
di::bind<IPromptFormatter>().to<Gemma4JinjaPromptFormatter>(),
di::bind<std::string>().to(options.model_path),
di::bind<DataGenerator>().to(
[options](const auto& inj) -> std::unique_ptr<DataGenerator> {
if (options.use_mocked) {
spdlog::info(
"[Generator] Using MockGenerator (no model path provided)");
return std::make_unique<MockGenerator>();
}
spdlog::info(
"[Generator] Using LlamaGenerator: {} (temperature={}, "
"top-p={}, top-k={}, n_ctx={}, seed={})",
options.model_path, options.temperature, options.top_p,
options.top_k, options.n_ctx, options.seed);
return inj.template create<std::unique_ptr<LlamaGenerator>>();
}));
auto generator =
injector.create<std::unique_ptr<BiergartenDataGenerator>>();
if (!generator->Run()) {
spdlog::error("Pipeline execution failed");
return 1;
}
spdlog::info("Pipeline executed successfully in {} ms", timer.Elapsed());
return 0;
} catch (const std::exception& exception) {
spdlog::critical("Unhandled fatal error in main: {}", exception.what());
return 1;
}
}

View File

@@ -1,24 +0,0 @@
/**
* @file services/sqlite/build_database_path.cc
* @brief SqliteExportService::BuildDatabasePath() implementation.
*/
#include <filesystem>
#include <string>
#include "services/sqlite_export_service.h"
std::filesystem::path SqliteExportService::BuildDatabasePath() const {
std::filesystem::path base_filename("biergarten_seed_" + run_timestamp_utc_ +
".sqlite");
std::filesystem::path candidate =
std::filesystem::current_path() / base_filename;
for (int suffix = 1; std::filesystem::exists(candidate); ++suffix) {
candidate = std::filesystem::current_path() /
std::filesystem::path("biergarten_seed_" + run_timestamp_utc_ +
"-" + std::to_string(suffix) + ".sqlite");
}
return candidate;
}

View File

@@ -1,28 +0,0 @@
/**
* @file services/sqlite/build_location_key.cc
* @brief SqliteExportService::BuildLocationKey() implementation.
*/
#include <iomanip>
#include <sstream>
#include "services/sqlite_export_service.h"
#include "services/sqlite_export_service_helpers.h"
constexpr int kLocationPrecision = 17;
std::string SqliteExportService::BuildLocationKey(const Location& location) {
std::ostringstream key_stream;
key_stream << location.city << '\n'
<< location.state_province << '\n'
<< location.iso3166_2 << '\n'
<< location.country << '\n'
<< location.iso3166_1 << '\n'
<< std::setprecision(kLocationPrecision) << location.latitude
<< '\n'
<< std::setprecision(kLocationPrecision) << location.longitude
<< '\n'
<< sqlite_export_service_internal::SerializeLocalLanguages(
location.local_languages);
return key_stream.str();
}

View File

@@ -1,11 +0,0 @@
/**
* @file services/sqlite/finalize_statements.cc
* @brief SqliteExportService::FinalizeStatements() implementation.
*/
#include "services/sqlite_export_service.h"
void SqliteExportService::FinalizeStatements() noexcept {
insert_brewery_stmt_.reset();
insert_location_stmt_.reset();
}

View File

@@ -1,39 +0,0 @@
/**
* @file services/sqlite/initialize.cc
* @brief SqliteExportService::Initialize() implementation.
*/
#include <filesystem>
#include <memory>
#include <stdexcept>
#include <string>
#include "services/sqlite_export_service.h"
#include "services/sqlite_export_service_helpers.h"
void SqliteExportService::Initialize() {
if (db_handle_ != nullptr) {
throw std::runtime_error("SQLite export service is already initialized");
}
run_timestamp_utc_ = date_time_provider_->GetUtcTimestamp();
database_path_ = BuildDatabasePath();
std::filesystem::create_directories(database_path_.parent_path());
db_handle_ = sqlite_export_service_internal::OpenDatabase(database_path_);
try {
sqlite_export_service_internal::ExecSql(
db_handle_, "PRAGMA foreign_keys = ON;",
"Failed to enable SQLite foreign keys");
InitializeSchema();
PrepareStatements();
sqlite_export_service_internal::ExecSql(
db_handle_, "BEGIN IMMEDIATE TRANSACTION;",
"Failed to begin SQLite transaction");
transaction_open_ = true;
} catch (...) {
RollbackAndCloseNoThrow();
throw;
}
}

View File

@@ -1,16 +0,0 @@
/**
* @file services/sqlite/initialize_schema.cc
* @brief SqliteExportService::InitializeSchema() implementation.
*/
#include "services/sqlite_export_service.h"
#include "services/sqlite_export_service_helpers.h"
void SqliteExportService::InitializeSchema() {
sqlite_export_service_internal::ExecSql(
db_handle_, sqlite_export_service_internal::kCreateLocationsTableSql,
"Failed to create SQLite locations table");
sqlite_export_service_internal::ExecSql(
db_handle_, sqlite_export_service_internal::kCreateBreweriesTableSql,
"Failed to create SQLite breweries table");
}

View File

@@ -1,16 +0,0 @@
/**
* @file services/sqlite/prepare_statements.cc
* @brief SqliteExportService::PrepareStatements() implementation.
*/
#include "services/sqlite_export_service.h"
#include "services/sqlite_export_service_helpers.h"
void SqliteExportService::PrepareStatements() {
insert_location_stmt_ = sqlite_export_service_internal::PrepareStatement(
db_handle_, sqlite_export_service_internal::kInsertLocationSql,
"Failed to prepare SQLite location insert statement");
insert_brewery_stmt_ = sqlite_export_service_internal::PrepareStatement(
db_handle_, sqlite_export_service_internal::kInsertBrewerySql,
"Failed to prepare SQLite brewery insert statement");
}

View File

@@ -1,100 +0,0 @@
/**
* @file services/sqlite/process_record.cc
* @brief SqliteExportService::ProcessRecord() implementation.
*/
#include <stdexcept>
#include <string>
#include "services/sqlite_export_service.h"
#include "services/sqlite_export_service_helpers.h"
void SqliteExportService::ProcessRecord(const GeneratedBrewery& brewery) {
if (db_handle_ == nullptr || !transaction_open_) {
throw std::runtime_error("SQLite export service is not initialized");
}
const std::string location_key = BuildLocationKey(brewery.location);
const auto cached_location = location_cache_.find(location_key);
sqlite3_int64 location_id = 0;
if (cached_location != location_cache_.end()) {
location_id = cached_location->second;
} else {
const std::string local_languages_json =
sqlite_export_service_internal::SerializeLocalLanguages(
brewery.location.local_languages);
sqlite_export_service_internal::BindText(
insert_location_stmt_,
sqlite_export_service_internal::kLocationCityBindIndex,
brewery.location.city, "Failed to bind SQLite location city");
sqlite_export_service_internal::BindText(
insert_location_stmt_,
sqlite_export_service_internal::kLocationStateProvinceBindIndex,
brewery.location.state_province,
"Failed to bind SQLite location state/province");
sqlite_export_service_internal::BindText(
insert_location_stmt_,
sqlite_export_service_internal::kLocationIso31662BindIndex,
brewery.location.iso3166_2,
"Failed to bind SQLite location ISO 3166-2 code");
sqlite_export_service_internal::BindText(
insert_location_stmt_,
sqlite_export_service_internal::kLocationCountryBindIndex,
brewery.location.country, "Failed to bind SQLite location country");
sqlite_export_service_internal::BindText(
insert_location_stmt_,
sqlite_export_service_internal::kLocationIso31661BindIndex,
brewery.location.iso3166_1,
"Failed to bind SQLite location ISO 3166-1 code");
sqlite_export_service_internal::BindText(
insert_location_stmt_,
sqlite_export_service_internal::kLocationLanguagesBindIndex,
local_languages_json, "Failed to bind SQLite location languages");
sqlite_export_service_internal::BindDouble(
insert_location_stmt_,
sqlite_export_service_internal::kLocationLatitudeBindIndex,
brewery.location.latitude, "Failed to bind SQLite location latitude");
sqlite_export_service_internal::BindDouble(
insert_location_stmt_,
sqlite_export_service_internal::kLocationLongitudeBindIndex,
brewery.location.longitude, "Failed to bind SQLite location longitude");
sqlite_export_service_internal::StepStatement(
db_handle_, insert_location_stmt_,
"Failed to insert SQLite location row");
location_id = sqlite_export_service_internal::LastInsertRowId(db_handle_);
location_cache_.emplace(location_key, location_id);
sqlite_export_service_internal::ResetStatement(insert_location_stmt_);
}
sqlite_export_service_internal::BindInt64(
insert_brewery_stmt_,
sqlite_export_service_internal::kBreweryLocationIdBindIndex, location_id,
"Failed to bind SQLite brewery location id");
sqlite_export_service_internal::BindText(
insert_brewery_stmt_,
sqlite_export_service_internal::kBreweryEnglishNameBindIndex,
brewery.brewery.name_en, "Failed to bind SQLite brewery English name");
sqlite_export_service_internal::BindText(
insert_brewery_stmt_,
sqlite_export_service_internal::kBreweryEnglishDescriptionBindIndex,
brewery.brewery.description_en,
"Failed to bind SQLite brewery English description");
sqlite_export_service_internal::BindText(
insert_brewery_stmt_,
sqlite_export_service_internal::kBreweryLocalNameBindIndex,
brewery.brewery.name_local, "Failed to bind SQLite brewery local name");
sqlite_export_service_internal::BindText(
insert_brewery_stmt_,
sqlite_export_service_internal::kBreweryLocalDescriptionBindIndex,
brewery.brewery.description_local,
"Failed to bind SQLite brewery local description");
sqlite_export_service_internal::StepStatement(
db_handle_, insert_brewery_stmt_, "Failed to insert SQLite brewery row");
sqlite_export_service_internal::ResetStatement(insert_brewery_stmt_);
}

View File

@@ -1,21 +0,0 @@
/**
* @file services/sqlite/rollback_and_close_no_throw.cc
* @brief SqliteExportService::RollbackAndCloseNoThrow() implementation.
*/
#include "services/sqlite_export_service.h"
void SqliteExportService::RollbackAndCloseNoThrow() noexcept {
if (db_handle_ == nullptr) {
return;
}
if (transaction_open_) {
sqlite_export_service_internal::RollbackTransactionNoThrow(db_handle_);
transaction_open_ = false;
}
FinalizeStatements();
db_handle_.reset();
location_cache_.clear();
}

View File

@@ -1,61 +0,0 @@
/**
* @file wikipedia/fetch_extract.cc
* @brief WikipediaService::FetchExtract() implementation.
*/
#include <spdlog/spdlog.h>
#include <boost/json.hpp>
#include <string>
#include <string_view>
#include "services/wikipedia_service.h"
std::string WikipediaService::FetchExtract(std::string_view query) {
const std::string cache_key(query);
const auto cache_it = this->extract_cache_.find(cache_key);
if (cache_it != this->extract_cache_.end()) {
return cache_it->second;
}
const std::string encoded = this->client_->UrlEncode(cache_key);
const std::string url =
"https://en.wikipedia.org/w/api.php?action=query&titles=" + encoded +
"&prop=extracts&explaintext=1&format=json";
const std::string body = this->client_->Get(url);
boost::system::error_code parse_error;
boost::json::value doc = boost::json::parse(body, parse_error);
if (!parse_error && doc.is_object()) {
try {
auto& pages = doc.at("query").at("pages").get_object();
if (!pages.empty()) {
auto& page = pages.begin()->value().get_object();
if (page.contains("extract") && page.at("extract").is_string()) {
const std::string_view extract_view = page.at("extract").as_string();
std::string extract(extract_view);
spdlog::debug("WikipediaService fetched {} chars for '{}'",
extract.size(), query);
this->extract_cache_.emplace(cache_key, extract);
return extract;
}
}
this->extract_cache_.emplace(cache_key, std::string{});
} catch (const std::exception& e) {
spdlog::warn(
"WikipediaService: failed to parse response structure for '{}': "
"{}",
query, e.what());
return {};
}
} else if (parse_error) {
spdlog::warn("WikipediaService: JSON parse error for '{}': {}", query,
parse_error.message());
}
return {};
}

View File

@@ -1,47 +0,0 @@
/**
* @file wikipedia/get_summary.cc
* @brief WikipediaService::GetLocationContext() implementation.
*/
#include <spdlog/spdlog.h>
#include <string>
#include "services/wikipedia_service.h"
std::string WikipediaService::GetLocationContext(const Location& loc) {
if (!client_) {
return {};
}
std::string result;
std::string region_query(loc.city);
if (!loc.country.empty()) {
region_query += ", ";
region_query += loc.country;
}
const std::string beer_query = "beer in " + loc.country;
const std::string city_beer_query = "beer in " + loc.city;
auto append_extract = [&result](const std::string& extract) -> void {
if (extract.empty()) {
return;
}
if (!result.empty()) {
result += "\n\n";
}
result += extract;
};
try {
append_extract(FetchExtract(region_query));
append_extract(FetchExtract(beer_query));
append_extract(FetchExtract(city_beer_query));
} catch (const std::runtime_error& e) {
spdlog::debug("WikipediaService lookup failed for '{}': {}", region_query,
e.what());
}
return result;
}

View File

@@ -1,19 +0,0 @@
/**
* @file web_client/curl_global_state.cc
* @brief CurlGlobalState constructor and destructor implementation.
*/
#include <curl/curl.h>
#include <stdexcept>
#include "web_client/curl_web_client.h"
CurlGlobalState::CurlGlobalState() {
if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) {
throw std::runtime_error(
"[CURLWebClient] Failed to initialize libcurl globally");
}
}
CurlGlobalState::~CurlGlobalState() { curl_global_cleanup(); }

View File

@@ -1,86 +0,0 @@
/**
* @file web_client/curl_web_client_get.cc
* @brief CURLWebClient::Get() implementation.
*/
#include <curl/curl.h>
#include <cstdint>
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include "web_client/curl_web_client.h"
using CurlHandle = std::unique_ptr<CURL, decltype(&curl_easy_cleanup)>;
static constexpr long kConnectionTimeout = 10;
static constexpr long kRequestTimeout = 30;
static constexpr int32_t kOkHttpStatus = 200;
static CurlHandle CreateHandle() {
CURL* handle = curl_easy_init();
if (handle == nullptr) {
throw std::runtime_error(
"[CURLWebClient] Failed to initialize libcurl handle");
}
return {handle, &curl_easy_cleanup};
}
static void SetCommonGetOptions(CURL* curl, const std::string& url) {
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_USERAGENT, "biergarten-pipeline/0.1.0");
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, kConnectionTimeout);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, kRequestTimeout);
curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
}
// curl write callback that appends response data into a std::string
static size_t WriteCallbackString(void* contents, const size_t size,
const size_t nmemb, void* userp) {
const size_t real_size = size * nmemb;
auto* str = static_cast<std::string*>(userp);
str->append(static_cast<char*>(contents), real_size);
return real_size;
}
std::string CURLWebClient::Get(const std::string& url) {
const CurlHandle curl = CreateHandle();
std::string response_string;
SetCommonGetOptions(curl.get(), url);
curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, WriteCallbackString);
curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response_string);
CURLcode curl_result = curl_easy_perform(curl.get());
if (curl_result != CURLE_OK) {
const auto error = std::string("[CURLWebClient] GET failed: ") +
curl_easy_strerror(curl_result);
throw std::runtime_error(error);
}
long curl_http_code = 0;
curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &curl_http_code);
if (curl_http_code < std::numeric_limits<int32_t>::min() ||
curl_http_code > std::numeric_limits<int32_t>::max()) {
throw std::runtime_error("[CURLWebClient] Invalid HTTP status code: " +
std::to_string(curl_http_code));
}
const int32_t http_code = static_cast<int32_t>(curl_http_code);
if (http_code != kOkHttpStatus) {
const std::string error = "[CURLWebClient] HTTP error " +
std::to_string(http_code) + " for URL " + url;
throw std::runtime_error(error);
}
return response_string;
}

View File

@@ -1,24 +0,0 @@
/**
* @file web_client/curl_web_client_url_encode.cc
* @brief CURLWebClient::UrlEncode() implementation.
*/
#include <curl/curl.h>
#include <stdexcept>
#include <string>
#include "web_client/curl_web_client.h"
std::string CURLWebClient::UrlEncode(const std::string& value) {
// A NULL handle is fine for UTF-8 encoding according to libcurl docs.
char* output = curl_easy_escape(nullptr, value.c_str(), 0);
if (!output) {
throw std::runtime_error("[CURLWebClient] curl_easy_escape failed");
}
std::string result(output);
curl_free(output);
return result;
}

View File

@@ -0,0 +1,9 @@
build/
cmake-build-debug/
.git/
.idea/
**/*.sqlite
**/*.log
**/*.sqlite3
**/*.db

View File

@@ -6,3 +6,4 @@ data
models models
*.gguf *.gguf
BiergartenPipeline.png BiergartenPipeline.png
output

View File

@@ -0,0 +1,255 @@
cmake_minimum_required(VERSION 3.31)
project(biergarten-pipeline)
# Set policy to allow FetchContent_Populate for header-only libraries
# that have outdated CMakeLists.txt files
cmake_policy(SET CMP0169 OLD)
# 1. Build Options
option(BIERGARTEN_MOCK_ONLY "Build with mock data generators only — skips llama.cpp" OFF)
if(BIERGARTEN_MOCK_ONLY)
message(STATUS "[biergarten] MOCK_ONLY build — llama.cpp will not be compiled.")
endif()
# 2. Platform & GPU Detection
if(NOT UNIX)
message(FATAL_ERROR "[biergarten] Windows is not supported. Please use Linux (Fedora 43) or macOS (M1 Pro).")
endif()
if(APPLE)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64")
message(STATUS "[biergarten] Apple Silicon detected — enabling Metal acceleration.")
set(GGML_METAL ON CACHE BOOL "Enable Metal for Apple Silicon" FORCE)
else()
message(STATUS "[biergarten] Intel Mac detected — using CPU / Accelerate framework.")
set(GGML_METAL OFF CACHE BOOL "Disable Metal for Intel Macs" FORCE)
endif()
else()
find_package(CUDAToolkit QUIET)
find_package(hip CONFIG QUIET)
if(CUDAToolkit_FOUND)
message(STATUS "[biergarten] NVIDIA GPU detected — enabling CUDA acceleration.")
set(GGML_CUDA ON CACHE BOOL "Enable CUDA for NVIDIA GPUs" FORCE)
set(CMAKE_CUDA_ARCHITECTURES native)
elseif(hip_FOUND OR DEFINED ENV{ROCM_PATH} OR EXISTS "/opt/rocm")
message(STATUS "[biergarten] AMD GPU detected — enabling HIP/ROCm acceleration.")
set(GGML_HIPBLAS ON CACHE BOOL "Enable HIP for AMD GPUs" FORCE)
else()
message(STATUS "[biergarten] No NVIDIA or AMD GPU found — falling back to CPU.")
endif()
endif()
# 3. Project-wide Settings
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -march=native -flto")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Og -g")
# 4. Dependencies
include(FetchContent)
# Boost (system install — via dnf/brew)
find_package(Boost REQUIRED COMPONENTS json program_options)
# Boost.DI (unofficial Boost extension, must declare separately from main Boost dependency)
# Header-only library, so we only fetch without invoking its CMakeLists.txt
FetchContent_Declare(
boost-di
GIT_REPOSITORY https://github.com/boost-ext/di.git
GIT_TAG v1.3.0
GIT_SHALLOW TRUE
)
FetchContent_GetProperties(boost-di)
if(NOT boost-di_POPULATED)
FetchContent_Populate(boost-di)
endif()
add_library(boost_di INTERFACE)
add_library(boost::di ALIAS boost_di)
target_include_directories(boost_di INTERFACE
$<BUILD_INTERFACE:${boost-di_SOURCE_DIR}/include>
)
# SQLite amalgamation
FetchContent_Declare(
sqlite_amalgamation
URL https://www.sqlite.org/2026/sqlite-amalgamation-3530000.zip
URL_HASH SHA3_256=c2325c53b3b41761469f91cfb078e96882ac5d85bac10c11b0bd8f253b031e5b
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(sqlite_amalgamation)
if(NOT TARGET sqlite3)
add_library(sqlite3 STATIC ${sqlite_amalgamation_SOURCE_DIR}/sqlite3.c)
target_include_directories(sqlite3 PUBLIC ${sqlite_amalgamation_SOURCE_DIR})
target_compile_definitions(sqlite3 PUBLIC SQLITE_THREADSAFE=1)
endif()
# llama.cpp — skipped for mock-only builds
if(NOT BIERGARTEN_MOCK_ONLY)
find_library(LLAMA_LIB NAMES llama)
find_library(GGML_LIB NAMES ggml)
find_library(GGML_BASE_LIB NAMES ggml-base)
find_path(LLAMA_INC_DIR NAMES llama.h PATH_SUFFIXES include)
if(LLAMA_LIB AND GGML_LIB AND GGML_BASE_LIB AND LLAMA_INC_DIR)
message(STATUS "[biergarten] Found system llama.cpp — skipping FetchContent")
add_library(llama SHARED IMPORTED)
set_target_properties(llama PROPERTIES
IMPORTED_LOCATION "${LLAMA_LIB}"
INTERFACE_INCLUDE_DIRECTORIES "${LLAMA_INC_DIR}"
INTERFACE_LINK_LIBRARIES "${GGML_LIB};${GGML_BASE_LIB}"
)
else()
message(STATUS "[biergarten] System llama.cpp not found — fetching via FetchContent")
FetchContent_Declare(
llama-cpp
GIT_REPOSITORY https://github.com/ggml-org/llama.cpp.git
GIT_TAG b9012
)
FetchContent_MakeAvailable(llama-cpp)
endif()
endif()
# spdlog
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.15.3
)
FetchContent_MakeAvailable(spdlog)
# cpp-httplib — header-only HTTP/HTTPS client replacing libcurl.
# OpenSSL is required for HTTPS (Wikipedia API). find_package locates
# libssl/libcrypto; HTTPLIB_REQUIRE_OPENSSL causes a hard build failure
# if OpenSSL is absent rather than silently producing an HTTP-only binary.
find_package(OpenSSL REQUIRED)
FetchContent_Declare(
cpp-httplib
GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git
GIT_TAG v0.43.2
GIT_SHALLOW TRUE
SYSTEM
)
set(HTTPLIB_REQUIRE_OPENSSL ON CACHE BOOL "Require OpenSSL for cpp-httplib" FORCE)
FetchContent_MakeAvailable(cpp-httplib)
# 5. Executable & Sources
add_executable(${PROJECT_NAME}
includes/services/enrichment/mock_enrichment.h)
# --- Entry point ---
target_sources(${PROJECT_NAME} PRIVATE
src/main.cc
)
# --- json_handling ---
target_sources(${PROJECT_NAME} PRIVATE
src/json_handling/json_loader.cc
)
# --- application_options ---
target_sources(${PROJECT_NAME} PRIVATE
src/application_options/parse_arguments.cc
)
# --- biergarten_data_generator ---
target_sources(${PROJECT_NAME} PRIVATE
src/biergarten_data_generator/log_results.cc
src/biergarten_data_generator/biergarten_data_generator.cc
src/biergarten_data_generator/generate_breweries.cc
src/biergarten_data_generator/run.cc
src/biergarten_data_generator/query_cities_with_countries.cc
)
# --- web_client ---
target_sources(${PROJECT_NAME} PRIVATE
src/web_client/http_web_client.cc
)
# --- data_generation: prompt_formatting ---
target_sources(${PROJECT_NAME} PRIVATE
src/data_generation/prompt_formatting/gemma4_jinja_prompt_formatter.cc
)
# --- data_generation: mock ---
target_sources(${PROJECT_NAME} PRIVATE
src/data_generation/mock/generate_brewery.cc
src/data_generation/mock/generate_user.cc
src/data_generation/mock/deterministic_hash.cc
)
# --- data_generation: llama (skipped for mock-only builds) ---
if(NOT BIERGARTEN_MOCK_ONLY)
target_sources(${PROJECT_NAME} PRIVATE
src/data_generation/llama/load.cc
src/data_generation/llama/helpers.cc
src/data_generation/llama/generate_brewery.cc
src/data_generation/llama/infer.cc
src/data_generation/llama/llama_generator.cc
src/data_generation/llama/generate_user.cc
)
endif()
# --- services: wikipedia ---
target_sources(${PROJECT_NAME} PRIVATE
src/services/enrichment/wikipedia/wikipedia_service.cc
src/services/enrichment/wikipedia/fetch_extract.cc
src/services/enrichment/wikipedia/get_summary.cc
)
# --- services: sqlite ---
target_sources(${PROJECT_NAME} PRIVATE
src/services/sqlite/process_record.cc
src/services/sqlite/sqlite_export_service.cc
src/services/sqlite/finalize.cc
src/services/sqlite/initialize.cc
src/services/sqlite/helpers/sqlite_connection_helpers.cc
src/services/sqlite/helpers/sqlite_statement_helpers.cc
)
# --- services (top-level) ---
target_sources(${PROJECT_NAME} PRIVATE
src/services/prompt_directory.cc
)
# 6. Include Directories, Link Libraries & Compile Definitions
target_include_directories(${PROJECT_NAME} PRIVATE
includes
)
target_link_libraries(${PROJECT_NAME} PRIVATE
$<$<NOT:$<BOOL:${BIERGARTEN_MOCK_ONLY}>>:llama>
boost::di
Boost::json
Boost::program_options
spdlog::spdlog
sqlite3
httplib::httplib
OpenSSL::SSL
OpenSSL::Crypto
)
target_compile_definitions(${PROJECT_NAME} PRIVATE
# Defined when -DBIERGARTEN_MOCK_ONLY=ON — skips llama.cpp entirely.
# Use #ifdef BIERGARTEN_MOCK_ONLY in source to guard llama-specific code.
$<$<BOOL:${BIERGARTEN_MOCK_ONLY}>:BIERGARTEN_MOCK_ONLY>
# Defined for Debug configuration builds.
# Use #ifdef DEBUG in source to enable debug-only behaviour (e.g. verbose logging).
$<$<CONFIG:Debug>:DEBUG>
)
# 7. Runtime Assets
configure_file(
${CMAKE_SOURCE_DIR}/locations.json
${CMAKE_BINARY_DIR}/locations.json
COPYONLY
)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/prompts
${CMAKE_BINARY_DIR}/prompts
)

1
tooling/pipeline/docs Symbolic link
View File

@@ -0,0 +1 @@
../../docs/pipeline/

View File

@@ -11,11 +11,9 @@
#include <vector> #include <vector>
#include "data_generation/data_generator.h" #include "data_generation/data_generator.h"
#include "data_model/enriched_city.h" #include "data_model/generated_models.h"
#include "data_model/generated_brewery.h" #include "services/database/export_service.h"
#include "data_model/location.h" #include "services/enrichment/enrichment_service.h"
#include "services/enrichment_service.h"
#include "services/export_service.h"
/** /**
* @brief Main data generator class for the Biergarten pipeline. * @brief Main data generator class for the Biergarten pipeline.
@@ -34,7 +32,8 @@ class BiergartenDataGenerator {
*/ */
BiergartenDataGenerator(std::unique_ptr<IEnrichmentService> context_service, BiergartenDataGenerator(std::unique_ptr<IEnrichmentService> context_service,
std::unique_ptr<DataGenerator> generator, std::unique_ptr<DataGenerator> generator,
std::unique_ptr<IExportService> exporter); std::unique_ptr<IExportService> exporter,
const ApplicationOptions& application_options);
/** /**
* @brief Run the data generation pipeline. * @brief Run the data generation pipeline.
@@ -58,12 +57,14 @@ class BiergartenDataGenerator {
/// @brief Storage backend for generated brewery records. /// @brief Storage backend for generated brewery records.
std::unique_ptr<IExportService> exporter_; std::unique_ptr<IExportService> exporter_;
const ApplicationOptions application_options_;
/** /**
* @brief Load locations from JSON and sample cities. * @brief Load locations from JSON and sample cities.
* *
* @return Vector of sampled locations capped at 50 entries. * @return Vector of sampled locations capped at 50 entries.
*/ */
static std::vector<Location> QueryCitiesWithCountries(); std::vector<Location> QueryCitiesWithCountries();
/** /**
* @brief Generate breweries for enriched cities. * @brief Generate breweries for enriched cities.

View File

@@ -8,9 +8,7 @@
#include <string> #include <string>
#include "data_model/brewery_result.h" #include "data_model/generated_models.h"
#include "data_model/location.h"
#include "data_model/user_result.h"
/** /**
* @brief Interface for data generator implementations. * @brief Interface for data generator implementations.

View File

@@ -14,9 +14,10 @@
#include <string> #include <string>
#include <string_view> #include <string_view>
#include "../services/prompting/prompt_directory.h"
#include "data_generation/data_generator.h" #include "data_generation/data_generator.h"
#include "data_generation/prompt_formatting/prompt_formatter.h" #include "data_generation/prompt_formatting/prompt_formatter.h"
#include "data_model/application_options.h" #include "data_model/models.h"
struct llama_model; struct llama_model;
struct llama_context; struct llama_context;
@@ -33,10 +34,12 @@ class LlamaGenerator final : public DataGenerator {
* @param options Parsed application options. * @param options Parsed application options.
* @param model_path Filesystem path to GGUF model assets. * @param model_path Filesystem path to GGUF model assets.
* @param prompt_formatter Formatter that produces model-specific prompts. * @param prompt_formatter Formatter that produces model-specific prompts.
* @param prompt_directory Directory service for loading named prompt files.
*/ */
LlamaGenerator(const ApplicationOptions& options, LlamaGenerator(const ApplicationOptions& options,
const std::string& model_path, const std::string& model_path,
std::unique_ptr<IPromptFormatter> prompt_formatter); std::unique_ptr<IPromptFormatter> prompt_formatter,
std::unique_ptr<IPromptDirectory> prompt_directory);
~LlamaGenerator() override; ~LlamaGenerator() override;
@@ -119,15 +122,6 @@ class LlamaGenerator final : public DataGenerator {
int max_tokens = kDefaultMaxTokens, int max_tokens = kDefaultMaxTokens,
std::string_view grammar = {}); std::string_view grammar = {});
/**
* @brief Loads the brewery system prompt from disk.
*
* @param prompt_file_path Prompt file path to try first.
* @return Loaded prompt text.
*/
std::string LoadBrewerySystemPrompt(
const std::filesystem::path& prompt_file_path);
ModelHandle model_; ModelHandle model_;
ContextHandle context_; ContextHandle context_;
float sampling_temperature_ = 1.0F; float sampling_temperature_ = 1.0F;
@@ -135,8 +129,9 @@ class LlamaGenerator final : public DataGenerator {
uint32_t sampling_top_k_ = kDefaultSamplingTopK; uint32_t sampling_top_k_ = kDefaultSamplingTopK;
std::mt19937 rng_; std::mt19937 rng_;
uint32_t n_ctx_ = kDefaultContextSize; uint32_t n_ctx_ = kDefaultContextSize;
std::string brewery_system_prompt_; int n_gpu_layers_ = 0;
std::unique_ptr<IPromptFormatter> prompt_formatter_; std::unique_ptr<IPromptFormatter> prompt_formatter_;
std::unique_ptr<IPromptDirectory> prompt_directory_;
}; };
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_LLAMA_GENERATOR_H_ #endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_LLAMA_GENERATOR_H_

View File

@@ -12,7 +12,7 @@
#include <string> #include <string>
#include <string_view> #include <string_view>
#include "data_model/brewery_result.h" #include "data_model/generated_models.h"
struct llama_vocab; struct llama_vocab;
using llama_token = int32_t; using llama_token = int32_t;

View File

@@ -44,6 +44,13 @@ class MockGenerator final : public DataGenerator {
*/ */
static size_t DeterministicHash(const Location& location); static size_t DeterministicHash(const Location& location);
// Hash stride constants for deterministic distribution across fixed-size
// arrays. These coprime strides spread hash values uniformly without
// clustering, ensuring diverse output across different hash inputs.
static constexpr size_t kNounHashStride = 7;
static constexpr size_t kDescriptionHashStride = 13;
static constexpr size_t kBioHashStride = 11;
static constexpr std::array<std::string_view, 18> kBreweryAdjectives = { static constexpr std::array<std::string_view, 18> kBreweryAdjectives = {
"Craft", "Heritage", "Local", "Artisan", "Pioneer", "Golden", "Craft", "Heritage", "Local", "Artisan", "Pioneer", "Golden",
"Modern", "Classic", "Summit", "Northern", "Riverstone", "Barrel", "Modern", "Classic", "Summit", "Northern", "Riverstone", "Barrel",

View File

@@ -1,4 +1,5 @@
#pragma once #ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_PROMPT_FORMATTING_GEMMA4_JINJA_PROMPT_FORMATTER_H_
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_PROMPT_FORMATTING_GEMMA4_JINJA_PROMPT_FORMATTER_H_
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -13,3 +14,5 @@ class Gemma4JinjaPromptFormatter final : public IPromptFormatter {
[[nodiscard]] std::string Format(std::string_view system_prompt, [[nodiscard]] std::string Format(std::string_view system_prompt,
std::string_view user_prompt) const override; std::string_view user_prompt) const override;
}; };
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_PROMPT_FORMATTING_GEMMA4_JINJA_PROMPT_FORMATTER_H_

View File

@@ -1,4 +1,5 @@
#pragma once #ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_PROMPT_FORMATTING_PROMPT_FORMATTER_H_
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_PROMPT_FORMATTING_PROMPT_FORMATTER_H_
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -15,3 +16,5 @@ class IPromptFormatter {
[[nodiscard]] virtual std::string Format( [[nodiscard]] virtual std::string Format(
std::string_view system_prompt, std::string_view user_prompt) const = 0; std::string_view system_prompt, std::string_view user_prompt) const = 0;
}; };
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_GENERATION_PROMPT_FORMATTING_PROMPT_FORMATTER_H_

View File

@@ -0,0 +1,66 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATED_MODELS_H_
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATED_MODELS_H_
/**
* @file data_model/generated_models.h
* @brief Generated output models from the pipeline: brewery/user results, enriched data,
* and complete generation results.
*/
#include <string>
#include "data_model/models.h"
// ============================================================================
// Generation Output Models
// ============================================================================
/**
* @brief Generated brewery payload.
*/
struct BreweryResult {
/// @brief Brewery display name in English.
std::string name_en;
/// @brief Brewery description text in English.
std::string description_en;
/// @brief Brewery display name in the local language.
std::string name_local;
/// @brief Brewery description text in the local language.
std::string description_local;
};
/**
* @brief Generated user profile payload.
*/
struct UserResult {
/// @brief Username handle.
std::string username{};
/// @brief Short user biography.
std::string bio{};
};
// ============================================================================
// Pipeline Data Models
// ============================================================================
/**
* @brief Enriched city data with Wikipedia context.
*/
struct EnrichedCity {
Location location;
std::string region_context{};
};
/**
* @brief Helper struct to store generated brewery data.
*/
struct GeneratedBrewery {
Location location;
BreweryResult brewery;
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_GENERATED_MODELS_H_

View File

@@ -0,0 +1,141 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_MODELS_H_
#define BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_MODELS_H_
/**
* @file data_model/models.h
* @brief Core data models: locations, application configuration, and generation
* inputs.
*/
#include <boost/program_options.hpp>
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
namespace prog_opts = boost::program_options;
// ============================================================================
// Location Models
// ============================================================================
/**
* @brief Canonical location record for city-level generation.
*/
struct Location {
/// @brief City name.
std::string city{};
/// @brief State or province name.
std::string state_province{};
/// @brief ISO 3166-2 subdivision code.
std::string iso3166_2{};
/// @brief Country name.
std::string country{};
/// @brief ISO 3166-1 country code.
std::string iso3166_1{};
/// @brief Local language codes in priority order.
std::vector<std::string> local_languages{};
/// @brief Latitude in decimal degrees.
double latitude{};
/// @brief Longitude in decimal degrees.
double longitude{};
};
/**
* @brief Non-owning brewery location input.
*/
struct BreweryLocation {
/// @brief City name.
std::string_view city_name;
/// @brief Country name.
std::string_view country_name;
};
// ============================================================================
// Configuration Models
// ============================================================================
/**
* @brief LLM sampling parameters.
*/
struct SamplingOptions {
/// @brief LLM sampling temperature (0.0 to 1.0, higher = more random).
float temperature = 1.0F;
/// @brief LLM nucleus sampling top-p parameter.
float top_p = 0.95F;
/// @brief LLM top-k sampling parameter.
uint32_t top_k = 64;
/// @brief Context window size (tokens).
uint32_t n_ctx = 8192;
/// @brief Random seed (-1 for random, otherwise non-negative).
int seed = -1;
/// @brief Number of layers to offload to GPU.
int n_gpu_layers = 0;
};
/**
* @brief Configuration for the LLM generator component.
*/
struct GeneratorOptions {
/// @brief Path to the LLM model file (gguf format).
std::filesystem::path model_path;
/// @brief Use mocked generator instead of actual LLM inference.
bool use_mocked = false;
/// @brief Specific sampling parameters for this generator.
/// If nullopt, the application should use global defaults.
std::optional<SamplingOptions> sampling;
};
/**
* @brief Configuration for the pipeline execution and output.
*/
struct PipelineOptions {
/// @brief Directory for generated artifacts.
std::filesystem::path output_path;
/// @brief Directory that contains named prompt files (e.g.
/// BREWERY_GENERATION.md).
std::filesystem::path prompt_dir;
/// @brief Path for application logs.
std::filesystem::path log_path;
/// @brief Number of locations to sample from the dataset
/// More locations -> more users/more breweries
uint32_t location_count;
};
/**
* @brief Root configuration object for the Biergarten pipeline.
*/
struct ApplicationOptions {
GeneratorOptions generator;
PipelineOptions pipeline;
};
// ============================================================================
// Function Declarations
// ============================================================================
std::optional<ApplicationOptions> ParseArguments(const int argc, char** argv);
#endif // BIERGARTEN_PIPELINE_INCLUDES_DATA_MODEL_MODELS_H_

View File

@@ -9,7 +9,7 @@
#include <filesystem> #include <filesystem>
#include <vector> #include <vector>
#include "data_model/location.h" #include "data_model/models.h"
/// @brief Loads curated world locations from a JSON file into memory. /// @brief Loads curated world locations from a JSON file into memory.
class JsonLoader { class JsonLoader {

View File

@@ -1,12 +1,14 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_EXPORT_SERVICE_H_ #ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_EXPORT_SERVICE_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_EXPORT_SERVICE_H_ #define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_EXPORT_SERVICE_H_
/** /**
* @file services/export_service.h * @file services/export_service.h
* @brief Abstraction for persisting generated brewery data. * @brief Abstraction for persisting generated brewery data.
*/ */
#include "data_model/generated_brewery.h" #include <cstdint>
#include "data_model/generated_models.h"
/** /**
* @brief Interface for services that persist generated brewery records. * @brief Interface for services that persist generated brewery records.
@@ -31,10 +33,10 @@ class IExportService {
* *
* @param brewery Generated brewery payload to store. * @param brewery Generated brewery payload to store.
*/ */
virtual void ProcessRecord(const GeneratedBrewery& brewery) = 0; virtual uint64_t ProcessRecord(const GeneratedBrewery& brewery) = 0;
/// @brief Finalizes the export destination. /// @brief Finalizes the export destination.
virtual void Finalize() = 0; virtual void Finalize() = 0;
}; };
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_EXPORT_SERVICE_H_ #endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_EXPORT_SERVICE_H_

View File

@@ -0,0 +1,30 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_CONNECTION_HELPERS_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_CONNECTION_HELPERS_H_
/**
* @file services/sqlite_connection_helpers.h
* @brief Declarations for connection-level SQLite helper functions.
*/
#include <sqlite3.h>
#include <filesystem>
#include <string>
#include <string_view>
#include "sqlite_handle_types.h"
namespace sqlite_export_service_internal {
void ThrowSqliteError(sqlite3* db_handle, std::string_view action);
SqliteDatabaseHandle OpenDatabase(const std::filesystem::path& path);
void ExecSql(const SqliteDatabaseHandle& db_handle, std::string_view sql,
const char* action);
void RollbackTransactionNoThrow(const SqliteDatabaseHandle& db_handle) noexcept;
} // namespace sqlite_export_service_internal
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_CONNECTION_HELPERS_H_

View File

@@ -1,5 +1,5 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_SQLITE_EXPORT_SERVICE_H_ #ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_EXPORT_SERVICE_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_SQLITE_EXPORT_SERVICE_H_ #define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_EXPORT_SERVICE_H_
/** /**
* @file services/sqlite_export_service.h * @file services/sqlite_export_service.h
@@ -11,16 +11,17 @@
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include "services/date_time_provider.h" #include "data_model/models.h"
#include "services/export_service.h" #include "../datetime/date_time_provider.h"
#include "services/sqlite_export_service_helpers.h" #include "export_service.h"
#include "sqlite_export_service_helpers.h"
/** /**
* @brief Persists generated brewery records into a fresh SQLite database. * @brief Persists generated brewery records into a fresh SQLite database.
*/ */
class SqliteExportService final : public IExportService { class SqliteExportService final : public IExportService {
public: public:
SqliteExportService(); explicit SqliteExportService(const ApplicationOptions& options);
~SqliteExportService() override; ~SqliteExportService() override;
SqliteExportService(const SqliteExportService&) = delete; SqliteExportService(const SqliteExportService&) = delete;
@@ -29,7 +30,7 @@ class SqliteExportService final : public IExportService {
SqliteExportService& operator=(SqliteExportService&&) = delete; SqliteExportService& operator=(SqliteExportService&&) = delete;
void Initialize() override; void Initialize() override;
void ProcessRecord(const GeneratedBrewery& brewery) override; uint64_t ProcessRecord(const GeneratedBrewery& brewery) override;
void Finalize() override; void Finalize() override;
private: private:
@@ -38,15 +39,15 @@ class SqliteExportService final : public IExportService {
using SqliteStatementHandle = using SqliteStatementHandle =
sqlite_export_service_internal::SqliteStatementHandle; sqlite_export_service_internal::SqliteStatementHandle;
void InitializeSchema(); void InitializeSchema() const;
void PrepareStatements(); void PrepareStatements();
void RollbackAndCloseNoThrow() noexcept; void RollbackAndCloseNoThrow() noexcept;
void FinalizeStatements() noexcept;
[[nodiscard]] std::filesystem::path BuildDatabasePath() const; [[nodiscard]] std::filesystem::path BuildDatabasePath() const;
[[nodiscard]] static std::string BuildLocationKey(const Location& location); [[nodiscard]] static std::string BuildLocationKey(const Location& location);
std::unique_ptr<IDateTimeProvider> date_time_provider_; std::unique_ptr<IDateTimeProvider> date_time_provider_;
std::filesystem::path output_path_;
std::string run_timestamp_utc_; std::string run_timestamp_utc_;
std::filesystem::path database_path_; std::filesystem::path database_path_;
SqliteDatabaseHandle db_handle_; SqliteDatabaseHandle db_handle_;
@@ -56,4 +57,4 @@ class SqliteExportService final : public IExportService {
std::unordered_map<std::string, sqlite3_int64> location_cache_; std::unordered_map<std::string, sqlite3_int64> location_cache_;
}; };
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_SQLITE_EXPORT_SERVICE_H_ #endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_EXPORT_SERVICE_H_

View File

@@ -0,0 +1,10 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_EXPORT_SERVICE_HELPERS_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_EXPORT_SERVICE_HELPERS_H_
/* Umbrella header for backward compatibility. */
#include "sqlite_connection_helpers.h"
#include "sqlite_handle_types.h"
#include "sqlite_statement_helpers.h"
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_EXPORT_SERVICE_HELPERS_H_

View File

@@ -0,0 +1,36 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_HANDLE_TYPES_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_HANDLE_TYPES_H_
/**
* Shared handle and parameter type declarations used by SQLite helper units.
*/
#include <sqlite3.h>
#include <memory>
#include <string_view>
namespace sqlite_export_service_internal {
struct SqliteDatabaseDeleter {
void operator()(sqlite3* handle) const noexcept;
};
struct SqliteStatementDeleter {
void operator()(sqlite3_stmt* statement) const noexcept;
};
using SqliteDatabaseHandle = std::unique_ptr<sqlite3, SqliteDatabaseDeleter>;
using SqliteStatementHandle =
std::unique_ptr<sqlite3_stmt, SqliteStatementDeleter>;
template <typename T>
struct BindParam {
int index;
T value;
std::string_view action;
};
} // namespace sqlite_export_service_internal
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_HANDLE_TYPES_H_

View File

@@ -0,0 +1,116 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_STATEMENT_HELPERS_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_STATEMENT_HELPERS_H_
/**
* @file services/sqlite_statement_helpers.h
* @brief Declarations for statement-level SQLite helper functions and
* constants.
*/
#include <sqlite3.h>
#include <string>
#include <string_view>
#include <vector>
#include "sqlite_handle_types.h"
namespace sqlite_export_service_internal {
inline constexpr std::string_view kCreateLocationsTableSql = R"sql(
CREATE TABLE IF NOT EXISTS locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
city TEXT NOT NULL,
state_province TEXT NOT NULL,
iso3166_2 TEXT NOT NULL,
country TEXT NOT NULL,
iso3166_1 TEXT NOT NULL,
local_languages_json TEXT NOT NULL,
latitude REAL NOT NULL,
longitude REAL NOT NULL,
UNIQUE(city, state_province, iso3166_2, country, latitude, longitude)
);
)sql";
inline constexpr std::string_view kCreateBreweriesTableSql = R"sql(
CREATE TABLE IF NOT EXISTS breweries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
location_id INTEGER NOT NULL,
name_en TEXT NOT NULL,
description_en TEXT NOT NULL,
name_local TEXT NOT NULL,
description_local TEXT NOT NULL,
FOREIGN KEY(location_id) REFERENCES locations(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_breweries_location_id ON breweries(location_id);
)sql";
inline constexpr std::string_view kInsertLocationSql = R"sql(
INSERT INTO locations (
city,
state_province,
iso3166_2,
country,
iso3166_1,
local_languages_json,
latitude,
longitude
) VALUES (?, ?, ?, ?, ?, ?, ?, ?);
)sql";
inline constexpr std::string_view kInsertBrewerySql = R"sql(
INSERT INTO breweries (
location_id,
name_en,
description_en,
name_local,
description_local
) VALUES (?, ?, ?, ?, ?);
)sql";
inline constexpr int kLocationCityBindIndex = 1;
inline constexpr int kLocationStateProvinceBindIndex = 2;
inline constexpr int kLocationIso31662BindIndex = 3;
inline constexpr int kLocationCountryBindIndex = 4;
inline constexpr int kLocationIso31661BindIndex = 5;
inline constexpr int kLocationLanguagesBindIndex = 6;
inline constexpr int kLocationLatitudeBindIndex = 7;
inline constexpr int kLocationLongitudeBindIndex = 8;
inline constexpr int kBreweryLocationIdBindIndex = 1;
inline constexpr int kBreweryEnglishNameBindIndex = 2;
inline constexpr int kBreweryEnglishDescriptionBindIndex = 3;
inline constexpr int kBreweryLocalNameBindIndex = 4;
inline constexpr int kBreweryLocalDescriptionBindIndex = 5;
SqliteStatementHandle PrepareStatement(const SqliteDatabaseHandle& db_handle,
std::string_view sql,
const char* action);
void ResetStatement(SqliteStatementHandle& statement);
void Bind(const SqliteStatementHandle& statement,
const BindParam<std::string_view>& param);
void Bind(const SqliteStatementHandle& statement,
const BindParam<double>& param);
void Bind(const SqliteStatementHandle& statement,
const BindParam<sqlite3_int64>& param);
void StepStatement(const SqliteDatabaseHandle& db_handle,
const SqliteStatementHandle& statement,
std::string_view action);
sqlite3_int64 LastInsertRowId(const SqliteDatabaseHandle& db_handle);
std::string SerializeVector(const std::vector<std::string>& str_vec);
} // namespace sqlite_export_service_internal
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_STATEMENT_HELPERS_H_

View File

@@ -1,5 +1,5 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATE_TIME_PROVIDER_H_ #ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATETIME_DATE_TIME_PROVIDER_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATE_TIME_PROVIDER_H_ #define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATETIME_DATE_TIME_PROVIDER_H_
/** /**
* @file services/date_time_provider.h * @file services/date_time_provider.h
@@ -63,4 +63,4 @@ class SystemDateTimeProvider final : public IDateTimeProvider {
} }
}; };
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATE_TIME_PROVIDER_H_ #endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATETIME_DATE_TIME_PROVIDER_H_

View File

@@ -0,0 +1,35 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATETIME_TIMER_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATETIME_TIMER_H_
#include <chrono>
/**
* @file services/timer.h
* @brief Simple timer utility for measuring elapsed time.
*/
class Timer {
std::chrono::steady_clock::time_point start_time =
std::chrono::steady_clock::now();
public:
Timer(const Timer&) = delete;
Timer& operator=(const Timer&) = delete;
Timer(Timer&&) = delete;
Timer& operator=(Timer&&) = delete;
Timer() = default;
~Timer() = default;
[[nodiscard]] int64_t Elapsed() const {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start_time)
.count();
}
[[nodiscard]] int64_t Reset() {
auto previous_elapsed = Elapsed();
start_time = std::chrono::steady_clock::now();
return previous_elapsed;
}
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATETIME_TIMER_H_

View File

@@ -1,5 +1,5 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_SERVICE_H_ #ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_ENRICHMENT_SERVICE_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_SERVICE_H_ #define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_ENRICHMENT_SERVICE_H_
/** /**
* @file services/enrichment_service.h * @file services/enrichment_service.h
@@ -8,7 +8,7 @@
#include <string> #include <string>
#include "data_model/location.h" #include "data_model/models.h"
/** /**
* @brief Interface for services that can enrich a location with context. * @brief Interface for services that can enrich a location with context.
@@ -27,4 +27,4 @@ class IEnrichmentService {
virtual std::string GetLocationContext(const Location& loc) = 0; virtual std::string GetLocationContext(const Location& loc) = 0;
}; };
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_SERVICE_H_ #endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_ENRICHMENT_SERVICE_H_

View File

@@ -0,0 +1,17 @@
//
// Created by aaronpo on 13/05/2026.
//
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_MOCK_ENRICHMENT_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_MOCK_ENRICHMENT_H_
#include <string>
#include "enrichment_service.h"
class MockEnrichmentService final : public IEnrichmentService {
public:
std::string GetLocationContext(const Location& /*loc*/) override {
return {};
}
};
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_MOCK_ENRICHMENT_H_

View File

@@ -1,5 +1,5 @@
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_WIKIPEDIA_SERVICE_H_ #ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_WIKIPEDIA_SERVICE_H_
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_WIKIPEDIA_SERVICE_H_ #define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_WIKIPEDIA_SERVICE_H_
/** /**
* @file services/wikipedia_service.h * @file services/wikipedia_service.h
@@ -11,14 +11,14 @@
#include <string_view> #include <string_view>
#include <unordered_map> #include <unordered_map>
#include "services/enrichment_service.h" #include "enrichment_service.h"
#include "web_client/web_client.h" #include "web_client/web_client.h"
/// @brief Provides Wikipedia summary lookups backed by cached raw extracts. /// @brief Provides Wikipedia summary lookups backed by cached raw extracts.
class WikipediaService final : public IEnrichmentService { class WikipediaEnrichmentService final : public IEnrichmentService {
public: public:
/// @brief Creates a new Wikipedia service with the provided web client. /// @brief Creates a new Wikipedia service with the provided web client.
explicit WikipediaService(std::unique_ptr<WebClient> client); explicit WikipediaEnrichmentService(std::unique_ptr<WebClient> client);
/// @brief Returns the Wikipedia-derived context for a location. /// @brief Returns the Wikipedia-derived context for a location.
[[nodiscard]] std::string GetLocationContext(const Location& loc) override; [[nodiscard]] std::string GetLocationContext(const Location& loc) override;
@@ -30,4 +30,4 @@ class WikipediaService final : public IEnrichmentService {
std::unordered_map<std::string, std::string> extract_cache_; std::unordered_map<std::string, std::string> extract_cache_;
}; };
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_WIKIPEDIA_SERVICE_H_ #endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_WIKIPEDIA_SERVICE_H_

Some files were not shown because too many files have changed in this diff Show More