# Changelog

All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2026-09-02

Ambient shipped four built-in values. Two of them – `Ambient.Env` and
`Ambient.Random` – turned out not to earn their surface, and the flagship one
couldn't express the way real config is written. This release cuts the first
two, fixes the third, and repositions the library around what it is actually
for: a per-process `Application.get_env` layer, plus `Ambient.Value` for
ambient values of your own.

### Removed
- **BREAKING: `Ambient.Env` and `Ambient.Credo.NoDirectEnv`.** An override can
  only affect a read that happens at runtime, and idiomatic Elixir reads env
  vars in `config/runtime.exs` – before any override can exist – then puts them
  into app config. `Ambient.Env`'s own moduledoc said so and the README pointed
  readers at `Ambient.Config` instead. Use `Ambient.Config`; for a read that
  genuinely happens at runtime, `use Ambient.Value` wraps `System.get_env/2` in
  about ten lines.
- **BREAKING: `Ambient.Random` and `Ambient.Credo.NoDirectRandom`.** It cost a
  fork-vs-shared-stream semantics table, a crypto contract, and a "seedable in
  tests, `:crypto.strong_rand_bytes/1` in production" pitch guarding a function
  with no production callers – the real credential sites in the app this was
  built for all call `:crypto.strong_rand_bytes/1` directly, which is the right
  answer. The machinery it leaned on – `get_and_update/3` and shared-mode
  atomicity – stays and serves any read-modify-write value, but the README now
  argues a seeded RNG is usually the *wrong* thing to build on it: fixture
  generation is single-process and wants `:rand.seed/2`, and production
  randomness is jitter, where a test should pin the delay rather than replay a
  stream.

### Added
- **`Ambient.mode/1`**, which reports `:private` or `{:shared, pid}` for a value
  module or facade, like `set_shared/2` and `set_private/1` take.
  `Ambient.ProcessOverride.mode/1` is the same query against a raw table atom –
  passing it a module used to return a silent `:private`.
- **Nested config keys.** `config :my_app, :oauth, client_id: "…"` reads back as
  a keyword list, so the call site is
  `Application.get_env(:my_app, :oauth)[:client_id]` – which the flat accessor
  could only override wholesale. That is why nested reads never migrated in the
  app this was built for. `get/2`, `put/2`, `revert/1` and `overridden?/1` now
  take a path:

      MyApp.Config.get([:oauth, :client_id], "default")
      MyApp.Config.put([:oauth, :client_id], "test-client")

  Paths step through keyword lists and maps to any depth, and resolve
  longest-prefix-first, so an existing wholesale `put(:oauth, …)` stays visible
  to a leaf read. A one-element path is the same key as the bare atom. The
  disabled build resolves paths straight out of app env with no ETS lookup.
- **`fetch/1` and `fetch!/1` on a generated config accessor**, the
  `Application.fetch_env/2` shape – so "absent" stays distinguishable from "set
  to `nil`", and code doing `fetch_env` doesn't have to stay on the global
  reader. Both take paths and both compile out of a disabled build.

### Fixed
- **A raising `get_and_update/3` callback took down the table's `Server`.** The
  callback runs inside the `Server` in shared mode, so an exception in it killed
  the table owner: the caller saw an `:exit` instead of its own error, and the
  restart handed back an *empty* ETS table, silently voiding every override in
  flight for every process. The exception is now caught, shipped back, and
  re-raised in the caller, so it behaves exactly like the private-mode path.
- **An `allow` cycle discarded a valid `$callers` answer.** Hitting an
  already-visited pid returned `nil` from the whole resolution rather than
  falling through to the caller chain, so a cycle anywhere in the grants could
  hide an override an ancestor really owned.
- **A generated config module's raw writers ignored path keys.** `put_override/2`
  and `delete_override/1` come from `Ambient.Value` and stored under the term as
  given, while `put/2`, `revert/1` and `overridden?/1` normalize – so
  `put_override([:port], v)` wrote a row the same module's `overridden?([:port])`
  reported as absent and `revert([:port])` could not clear. Both now normalize,
  so `[:port]` and `:port` are interchangeable across the whole generated
  surface, as the `key` typedoc says.
- **A slow `get_and_update/3` callback could consume a value nobody received.**
  In shared mode the callback runs inside the `Server`, and the `GenServer.call`
  took the default 5s timeout – so a caller could give up while the Server still
  applied the write. It now waits `:infinity`; this is test-only infrastructure
  with no liveness requirement.
- **`Ambient.Credo.NoDirectConfig` missed `Application.get_env(@otp_app, :key)`.**
  It matched the app argument as a literal atom, so one of the commonest
  spellings of the banned call was invisible to it. Module attributes are now
  flagged. This matters because "Credo pins it" is the whole answer to Ambient
  asking you to change every read site.

### Changed
- **"Build your own" no longer promotes an anti-pattern.** The worked example
  was a `MyApp.Tenant` whose fallback was a stub module – which in a production
  build is all that remains, so the example described a value that is a constant
  in production and only real in tests. It is now a feature flag whose fallback
  is the real flag-service lookup, alongside the rule that separates the two:
  the fallback must be the real production implementation. The docs also now say not
  to make the acting user or current tenant ambient – they decide what a request
  may see, so a leaked override is a data-exposure bug, not a wrong timestamp.
- The package description and README are rebuilt around `Ambient.Config` and
  `use Ambient.Value`, with `Ambient.Clock` as the worked example. The
  comparison section is a third of its former length, and a new "What it costs
  to adopt" section states plainly what the migration does not reach.
- The Credo checks' alias/`apply` blind spot is now documented rather than
  implied away.

## [0.1.1] - 2026-07-29

### Fixed
- **Every `Ambient.Random` read crashed when `Ambient.start_servers/1` hadn't
  been called**, in any build with overrides enabled. `uniform/1`, `bytes/1`,
  `shuffle/1` and friends route through
  `Ambient.ProcessOverride.get_and_update/3`, which reached `shared_owner/1` –
  a bare `:ets.lookup` – before checking the table existed, so ETS raised
  `ArgumentError` ("the table identifier does not refer to an existing ETS
  table") where a read should simply miss and fall through to `:rand`.

  It bit hardest in `:dev`, which the recommended
  `enable_overrides: config_env() != :prod` leaves enabled while nothing
  starts the servers: `iex -S mix` plus any `Ambient.Random` call crashed.
  `Ambient.Clock` and `Ambient.Config` were unaffected – they read through
  `fetch/2`, which has always guarded.

  No table now means no override, so `get_and_update/3` returns `:error` and
  the caller falls through. Writers are unchanged and still raise
  `Ambient.Error` with `:server_not_started`, which is the actionable message
  for the case that really is a mistake.

## [0.1.0] - 2026-07-29

First release.

### Added
- `Ambient.ProcessOverride` – ETS-backed process-local override store with
  `$callers` inheritance and an Ecto-Sandbox-style `allow/3`.
- `Ambient.Clock` – overridable wall clock (`set/1`, `advance/1`, `reset/0`).
- `Ambient.Random` – seedable, replayable RNG (`seed/1`, `uniform`, `shuffle`, …).
- `Ambient.Config` – `use`-able app-config accessor with a per-process override layer.
- `Ambient.start_servers/1` – one-call test setup (runs the servers under
  `Ambient.Supervisor` so a Server crash is restarted + logged, not silent).
- `Ambient.Facade` – `use Ambient.Facade, for: Ambient.Clock` to re-export a
  value module under your own module name, with compile-time-derived delegates.
- Optional Credo checks `Ambient.Credo.NoDirectClock`, `NoDirectRandom` and
  `NoDirectConfig`.
- `config :ambient, enable_overrides: config_env() != :prod` – a compile-time
  switch, **off by default**, that decides whether the override machinery is
  built at all. With it off, `Ambient.start_servers/1`,
  `ProcessOverride.Server.{start_link/1, init/1}`, `put/3` and `allow/3` all
  refuse, so no Ambient API can produce an override.
  `Ambient.ProcessOverride.enabled?/0` reports the build; compiling with the
  flag hard-coded on warns when Ambient can tell it's a prod build.
- `Ambient.Random.bytes/1` now falls through to `:crypto.strong_rand_bytes/1`
  when no seed is in scope, making it **credential-safe in production**: the
  seeded clause isn't compiled into a build that didn't opt in, so no ambient
  seed can downgrade it. It stays deterministic (and non-cryptographic) under
  `seed/1`. The rest of `Ambient.Random` remains `:rand`-backed and must never
  be used for credentials.
- **Shared mode.** `Ambient.set_shared/2` / `Ambient.set_private/1` (and
  `Ambient.ProcessOverride.set_shared/2` / `set_private/1` / `mode/1`) make one
  process's overrides the ones every process reads, for `async: false` tests
  that can't reach a process with `allow/3`. Only the shared owner may write;
  `allow/3` is refused while shared; the owner is monitored, so its exit
  returns the table to private.
- `Ambient.Error` – every Ambient misuse now raises this instead of a bare
  `ArgumentError`/`RuntimeError`, carrying a machine-readable `:reason` and the
  `:table` involved. Bad argument *values* still raise `ArgumentError`.
- `Ambient.Env` – overridable OS environment variables, so tests stop reaching
  for the VM-global `System.put_env/2`. `get/2`, `fetch/1`, `fetch!/1`,
  `put/2`, `put_all/1`, `unset/1` (override as *absent*), `revert/1` (drop the
  override), `reset/0`.
- `Ambient.Value` – the supported extension point. `use Ambient.Value,
  table: :t` generates the writers (`put_override/2`, `delete_override/1`,
  `delete_all/0`, `overridden?/1`, `allow/2`, `set_shared/1`, `set_private/0`,
  `__ambient_table__/0`, all overridable) and imports the `get_or/2` macro. The
  built-ins are built on it.
- `Ambient.Credo.NoDirectEnv` – flags `System.get_env/*` and `System.put_env/*`.
- `Ambient.ProcessOverride.delete_all/1` – drop every override the calling
  process owns in a table.
- `Ambient.ProcessOverride.get_and_update/3` – atomic read-modify-write for
  values whose reads also write, like `Ambient.Random`. A plain `put/3` would
  raise for every non-owner once a table went shared, and a `fetch/2` plus
  `put/3` would lose updates: every process shares one row in shared mode, so
  concurrent draws read the same state and overwrite each other (99 duplicates
  in 200 draws, measured). Shared mode runs the whole operation inside the
  `Server`; private mode stays client-side, where a process can't race itself.

### Fixed
- **`Ambient.Random` was unusable under shared mode.** Every draw writes its
  advanced state back, and shared mode forbids non-owner writes, so any process
  that wasn't the shared owner raised `{:not_shared_owner, pid}` – i.e. exactly
  the processes shared mode exists to reach. Writes now route through
  `get_and_update/3`, giving one globally advancing stream.
- **`allow/3` and `set_shared/2` monitored by cast, then inserted from the
  client**, so a pid dying in the gap left a row no `:DOWN` would ever clean.
  Measured over 40k attempts: 202 orphaned `allow` rows (which pid reuse then
  hands to an unrelated process – a leak in the library whose promise is no
  leaks) and 146 tables stuck shared to a dead pid, where every write raises
  until someone calls `set_private/1`. Both now monitor and insert inside the
  Server, on the same side of its mailbox as the `:DOWN`. Reproduced at 0 after.
- **`Ambient.Supervisor` used the default 3-restarts-in-5-seconds and stayed
  linked to whichever process called `start_servers/1` first.** A suite that
  restarts a Server (or `--repeat-until-failure`) exhausted it, and the
  supervisor's exit took every override table and the test run with it.
- **A non-owner could silently steal or cancel shared mode.** `set_shared/2`
  now raises `{:not_shared_owner, pid}` when the table is already shared by
  someone else. `set_private/1` stays open deliberately – `on_exit/1` runs in a
  different process from the test.
- **All four Credo checks missed piped calls** when the banned entry pinned an
  exact arity: a pipe leaves the receiver out of the call node, so
  `list |> Enum.shuffle()` – the form almost everyone writes – slipped past
  `NoDirectRandom` entirely.
- `Ambient.Value`'s `defoverridable` list omitted `__ambient_table__/0`, so
  redefining it only produced a "clause cannot match" warning while the
  generated one silently won.
- `Ambient.Facade` now passes `__ambient_table__/0` through, so a facade can be
  given to `Ambient.start_servers/1` and `set_shared/2` in place of the value module
  it wraps. It was rejected as `:not_a_value_module`.
- `Ambient.Random.normal/2`'s second argument was documented as the standard
  deviation; like `:rand.normal_s/3`, it is the **variance**.

### Changed
- `use Ambient.Config` now generates the domain verbs the other values have:
  `put/2`, `revert/1` and `reset/0`, alongside `get/2`. It was the only value module
  whose documented API was the raw `Ambient.Value` layer.
- `Ambient.start_servers/1`, `set_shared/2` and `set_private/1` accept a single
  value module as well as a list, so they no longer collide by argument shape with
  the same-named `Ambient.ProcessOverride` functions that take one raw table.
  A non-atom, non-list argument now raises `Ambient.Error` with
  `:not_a_value_module` instead of `FunctionClauseError`.
- **Production wrappers are now free.** `get_or/2` expands at compile time, so
  in a build without overrides each wrapper compiles to exactly the function it
  wraps: `Ambient.Clock.utc_now/0` to `DateTime.utc_now/0`, a generated
  `MyApp.Config.get/2` to `Application.get_env/3`, `Ambient.Env.get/2` to
  `System.get_env/2`. `Ambient.Clock` and `Ambient.Config` previously paid one
  `:ets.whereis/1` per call.
- **`Ambient.Random`'s unseeded path no longer reseeds per call.** It built a
  fresh `:rand.seed_s(:exsss)` on every call, ~12x the cost of the plain `:rand`
  function; it now delegates to `:rand.uniform/1` and friends, which seed the
  process dictionary once. Seeded behaviour is unchanged.
- `Ambient.Clock.utc_now/0` no longer re-checks that the stored override is a
  `DateTime` – `set/1` is the only writer and is typed.

### Upgrading from the git dependency

Only relevant if you tracked `main` before this release.

Add the switch to `config/config.exs` – without it `Ambient.start_servers/1`
raises and your suite won't boot:

```elixir
config :ambient, enable_overrides: config_env() != :prod
```

Derive it from `config_env/0` rather than hard-coding `true`; that's what keeps
the machinery – and the only way to downgrade `Random.bytes/1` – out of your
release. Prefer `!= :prod` over `== :test`: Dialyzer runs in `:dev`, and in a
disabled build the writers raise, so gating on `== :test` makes it report every
generated writer in your own modules as having no local return.

Also:

- If you rescue Ambient's exceptions, switch from `ArgumentError` to
  `Ambient.Error` and match on `:reason`.
- `Ambient.start_servers/1` now raises `:not_a_value_module` for a module-looking
  atom that doesn't export `__ambient_table__/0`, where it previously accepted
  it as a raw table name. Facades are fine – they now pass it through.
- Unseeded `Ambient.Random.bytes/1` changed source, from a `:rand` stream to
  `:crypto.strong_rand_bytes/1`. Output shape is identical; it is simply no
  longer predictable from a `:rand` seed.
- Unseeded `Ambient.Random` now draws from the process dictionary's `:rand`
  state rather than a fresh one per call, so a caller who seeded `:rand`
  directly will see those draws follow that seed.

[0.2.0]: https://github.com/mariuszzak/ambient/releases/tag/v0.2.0
[0.1.1]: https://github.com/mariuszzak/ambient/releases/tag/v0.1.1
[0.1.0]: https://github.com/mariuszzak/ambient/releases/tag/v0.1.0
