# Contributing to Beacon

Thank you for your interest in contributing. Beacon is a focused project and we
want contributions to be high quality and sustainable. Please read this guide
before opening a PR.

---

## Before you start

**Open or comment on an issue before starting work.** This avoids duplicate
effort and lets maintainers flag if something is already in progress or out of
scope. For small bug fixes a brief comment is fine; for larger features please
discuss the approach first.

**One thing per PR.** Each pull request should cover one logical change — a bug
fix, a new endpoint, a refactor, a new test. PRs that touch many unrelated parts
of the codebase are hard to review and hard to revert if something goes wrong.

**No fully AI-generated contributions.** We welcome developers who use AI tools
to assist their work, but PRs should reflect the author's own understanding and
judgement. PRs that appear to be unreviewed AI output may be closed without
further comment.

---

## Branches

- `main` — stable releases only, protected. Never target this directly.
- `dev` — active development. All PRs target `dev`.

## Workflow

1. Fork or create a branch from `dev`
2. Make your changes
3. Run the checklist below
4. Open a pull request against `dev` with a clear description of what changed
   and why, referencing any related issues

---

## Checklist before opening a PR

```
go build ./...        # must compile
gofmt -l .            # must be empty (no unformatted files)
go vet ./...          # no warnings
go test ./...         # all tests pass
swag init             # if you changed any handler or api type (see below)
```

---

## Code style

- Run `gofmt -w .` before committing — CI will fail on unformatted files
- Run `go vet ./...` — no warnings
- Follow the existing patterns in each package before introducing new ones
- Keep functions small and single-purpose
- Prefer explicit error handling over panic

---

## Tests

- **Add tests for any new pure functions.** Pure functions (no DB, no network,
  no side effects) should have unit tests. See `internal/hub/hub_test.go`,
  `internal/api/nodes_test.go`, and `internal/keystore/keystore_test.go` for
  examples of the style we use.
- Integration tests (requiring a real DB) are not yet required but are welcome.
  They will be gated before release.
- Run `go test ./...` before opening a PR. All tests must pass.
- If you are fixing a bug, add a test that would have caught it.

---

## Database changes

All schema changes must include a proper migration path:

- Add a new migration file to `db/migrations/` following the existing naming
  convention (e.g. `002_add_observation_count.sql`). Do not modify existing
  migration files — append only via new files.
- Update `db/queries/queries.sql` with any new or modified queries
- Re-run `sqlc generate` to regenerate `db/sqlc/`
- Update the store layer in `db/` to expose the new functionality
- Update `internal/api/reader.go` if the change needs to be exposed via the API

Never edit files under `db/sqlc/` by hand — they are generated by sqlc and will
be overwritten. If you need to work around a sqlc limitation, document it
clearly in the query comment.

To regenerate after modifying `db/queries/queries.sql`:

```bash
sqlc generate
```

---

## API changes

Any new or modified REST endpoint must have swagger annotations and regenerated
docs:

- Add or update `// @Summary`, `// @Param`, `// @Success`, `// @Failure`, and
  `// @Router` comments on the handler function
- Response types are defined in `internal/api/` — add new types there, not
  inline in handlers
- After changing any handler or API type, regenerate the swagger docs and commit
  the updated `docs/` directory alongside your changes:

```bash
swag init -g cmd/beacon/main.go -o docs --parseInternal --parseDependency
```

Install swag if you don't have it:

```bash
go install github.com/swaggo/swag/cmd/swag@latest
```

Each handler function should have a godoc-style annotation block:

```go
// listThings godoc
//
//	@Summary	Short description shown in the UI
//	@Tags		TagName
//	@Produce	json
//	@Param		paramName	query		string	false	"Description"
//	@Param		id			path		string	true	"Resource ID"
//	@Success	200			{object}	api.MyResponseType
//	@Failure	400			{object}	handlers.APIError
//	@Failure	500			{object}	handlers.APIError
//	@Router		/things [get]
func listThings(reader api.Reader) http.HandlerFunc {
```

For paginated responses use the generic page wrapper:

```go
//	@Success	200	{object}	api.Page[api.MyType]
```

---

## Caching

Beacon uses a Redis-backed caching layer in `internal/cache`. If you add a new
REST endpoint backed by a new `api.Reader` method, consider whether it should be
cached:

- Add the method to `CachedReader` in `internal/cache/reader.go`
- Pass-through to `cr.inner` if the data is highly dynamic, paginated with many
  filter combinations, or low traffic
- Use `getOrSet` with an appropriate TTL category (`cr.ttl.Stats`,
  `cr.ttl.Reference`, `cr.ttl.Nodes`, or `cr.ttl.Observers`) for read-heavy,
  slow-changing responses
- If the data is mutated by the ingest path, add an invalidation call in
  `internal/ingest/side_effects.go` via the `onNodeUpsert` or `onObserverUpsert`
  callbacks, or add a new callback following the same pattern

Cache keys live as constants at the top of `reader.go`. Use the `beacon:`
namespace prefix and include all parameters that affect the response in the key.

---

## Updating the IATA database

Beacon includes a static IATA → country/continent mapping compiled into the
binary, generated from the [OurAirports](https://ourairports.com/data/) public
dataset.

To refresh it with the latest airport data:

```bash
rm internal/iatadb/gen/airports.csv
go generate ./internal/iatadb/
```

This fetches a fresh `airports.csv` from OurAirports, saves it locally, and
regenerates `internal/iatadb/db.go`. Commit both files.

To use a local CSV instead (e.g. in a restricted network environment):

```bash
AIRPORTS_CSV=/path/to/airports.csv go run ./internal/iatadb/gen
```

---

## Adding a new dependency

- Run `go get <module>` and `go mod tidy`
- Add an entry to [SHOULDERS.md](SHOULDERS.md) with a brief description of what
  the dependency does and why it was added

---

## Commit messages

Use the conventional commits format:

```
feat(routes): add observation_count to known routes response
fix(ingest): correct lat/lon divisor for advert payloads
refactor(hub): collapse RegionIATAs into IATAs in Scope
test(api): add unit tests for NodeTypeName and NodeTypeFromString
chore: update airports.csv
docs: expand CONTRIBUTING.md
```

Scopes are optional but helpful for larger codebases. Common scopes: `api`,
`db`, `ingest`, `hub`, `ws`, `handlers`, `config`, `keystore`.

---

## Project structure

```
cmd/beacon/          — main entry point, wiring, startup
db/                  — store layer: sqlc-generated code + thin mapping layer
  migrations/        — SQL schema (single file, append only)
  queries/           — SQL queries (input to sqlc)
  sqlc/              — generated Go code (do not edit by hand)
internal/
  api/               — response types and Reader interface
    handlers/        — HTTP handlers (validation, routing, response)
    router/          — chi router wiring
  config/            — config loading and scope key derivation
  hub/               — WebSocket fan-out broker
  ingest/            — MQTT packet ingestion and side effects
  iatadb/            — in-memory IATA airport lookup
  keystore/          — channel key lookup
  scopestore/        — transport scope key lookup
  ws/                — WebSocket connection handling
docs/                — generated swagger docs (do not edit by hand)
```

Key patterns to understand before contributing:

- **Store layer** (`db/`): thin wrappers around sqlc-generated queries. Each
  method maps between the ingest/api param structs and sqlc param structs. Never
  put business logic here.
- **Ingest layer** (`internal/ingest/`): processes raw MQTT packets, calls the
  store, and broadcasts hub events. The `DB` interface in `ingest.go` defines
  exactly what the ingest layer needs from the store — keep it minimal.
- **Hub** (`internal/hub/`): pure fan-out broker. Events are pre-serialised JSON
  before entering the hub so the broadcast loop never touches encoding.
- **Reader interface** (`internal/api/reader.go`): defines everything the API
  layer can read. The store implements it. All handler tests use a stub reader.

---

## Releases

Merges from `dev` to `main` are done by maintainers and represent a versioned
release. Do not open PRs directly against `main`.

### Cutting a release

> NOTE: release are done manually, not from GH. Commits for releases should be
> signed.

1. Ensure all changes are committed and CI is green on `dev`
2. Bump the version string in the `@version` swagger annotation in
   `cmd/beacon/main.go`
3. Regenerate swagger docs:

```bash
swag init -g cmd/beacon/main.go -o docs --parseInternal --parseDependency
```

4. Commit the version bump and updated docs:

```
chore: bump version to vX.Y.Z
```

5. Merge `dev` into `main` (fast-forward only):

```bash
git checkout main
git merge --ff-only dev
```

6. Tag the release and push:

```bash
git tag vX.Y.Z
git push origin main --tags
```

Pushing the tag triggers the release CI workflow, which builds binaries for all
supported platforms and attaches them to a draft GitHub release.

7. Open the draft release on GitHub, paste the release notes, and publish.

8. Rebase `dev` on `main` to keep histories in sync for future releases:

```bash
git checkout dev
git rebase main
```

## Recognition

If you'd like to be listed as a contributor, add yourself to
[CONTRIBUTORS.md](CONTRIBUTORS.md) in your PR.
