CLAUDE.md

# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

A **proxy pool governor**: a control loop that adjusts traffic-routing weights across
proxy pools per service in response to observed health metrics. The decision logic lives
entirely in a **CLIPS expert system** (`gov.clp`); the C program is a thin harness that
feeds observations in and pulls actions out. A `gov-sim` binary exercises the rules either
against synthetic data (simulation) or against recorded metrics (replay).

`README.md` describes what the governor actually does today, with a clearly-labeled
"Design intent (not yet implemented)" section for the larger vision (capacity awareness,
proactive time-of-day shifts, probation/flap handling, trend detection, persistence). When
README and `gov.clp` disagree, `gov.clp` is what actually runs.

## Build & run

This targets **OpenBSD**. The Makefile uses BSD make (`bsd.prog.mk`) and the binary calls
`pledge(2)`/`unveil(2)`, so it will not build or run as-is on Linux/macOS.

```sh
# CLIPS_DIR must point at a built CLIPS core (libclips.a + clips.h).
# Override it if your CLIPS install lives elsewhere than the Makefile default.
make CLIPS_DIR=/path/to/clips/core

./gov-sim                       # simulation mode, 60 cycles (default)
./gov-sim -s 200                # simulation mode, N cycles
./gov-sim -r scenarios/foo.pps  # replay a recorded scenario
```

There is no test suite, linter, or CI. Verification is done by running `gov-sim` and reading
the `ADJUST:`/`ALERT:` output and final weight matrix.

## Architecture

**State ownership is the key design decision:** CLIPS owns all operational state (degradation
status, healthy-since timers, statistics). The C harness is stateless across cycles except for
the `effective_weights` matrix, which it carries forward.

Per-cycle loop (both `run_simulation` and `run_replay` in `main.c`):
1. `cleanup_cycle` — retract all transient facts (`current-weight`, `pool-service-stats`,
   `degradation`, `service-status`, `service-config`) from the previous cycle.
2. `inject_configs_and_weights` — assert `service-config` and `current-weight` facts (the
   latter carrying `effective_weights` forward).
3. Inject one `pool-service-stats` fact per pool×service.
4. `Run(env, -1)` — fire CLIPS rules to a quiescent state.
5. `process_weight_adjustments` / `process_alerts` — read `weight-adjustment` and `alert`
   facts CLIPS emitted, update `effective_weights`, print, and retract them.

A "cycle" corresponds to one timestamp. In replay mode, a change in `rec.timestamp` is what
delimits cycles — all records sharing a timestamp belong to the same cycle.

Note `pool-healthy-since` is **not** cleared by `cleanup_cycle`; it is the one CLIPS fact
intended to persist across cycles (it drives restoration cooldown).

### Files

- `gov.clp` — the governor. Deftemplates (the C/CLIPS interface contract), degradation
  detection rules, service-health rollup, weight reduction, and weight restoration. **This is
  where behavior changes are made.**
- `main.c` — harness: mode handling, fact injection (`inject_*`), fact extraction
  (`process_*`), defaults for simulation mode, synthetic health simulation.
- `scenario.c` / `scenario.h` — reader/writer for `.pps` scenario files.
- `scenario.R` — R tooling to read, write, generate, and analyze `.pps` files.
- `scenarios/``.pps` files live here (gitignored; only `.gitkeep` is tracked).

### `.pps` scenario file format

Plain text. Header has `@pools N` / `@services N` directives each followed by `id name` lines,
optional `@records N`, then a `---` marker, then whitespace-separated data rows. Each data row
is **14 fields**, and the C reader (`scenario_read`) requires exactly 14:

```
timestamp pool service rate_success rate_lost_race rate_302 rate_timeout \
  rate_ssl rate_other response_time avg_success avg_response_time \
  stddev_success stddev_response_time
```

The C reader/writer and `scenario.R` must agree on this layout — change all three together.
`rate_lost_race`, `rate_302`, and `rate_other` are carried through the interface but **unused**
by the current rules (see the comment in the `pool-service-stats` deftemplate).

## Conventions

- C style is **OpenBSD KNF**: tabs, `static` for file-local functions, `err`/`errx` for fatal
  errors, K&R braces. Match it.
- The C↔CLIPS contract is the set of deftemplate slot names in `gov.clp`. The `inject_*`
  functions in `main.c` build `AssertString` payloads by hand — any slot rename must be made
  in both places, and the slot *types* (SYMBOL/INTEGER/FLOAT) must match what the C side
  formats (e.g. weights are INTEGER, rates are FLOAT).
- Per-service tuning lives in `service-config` facts (seeded from `service_configs[]` /
  `init_defaults` in `main.c`). The tunable parameters the rules actually read are documented
  in `README.md`.