Current section

Files

Jump to
credence lib assumptions.ex
Raw

lib/assumptions.ex

defmodule Credence.Assumptions do
@moduledoc """
Safety switches — the promises Credence is allowed to make about your data.
A **switch** (an *assumption*) is a checkable promise about the text your code
handles **while it runs** — names, chat messages, file contents — *not* about
the characters in your `.ex` source files. Some rules only give the exact same
answer as your original code when a particular promise holds; those rules
declare the promise via `assumptions/0` and run only while it is on.
The reframed invariant:
> **Credence never changes behaviour on any input your stated promises admit.**
## The two modes
- **`:strict`** — you make *no* promises, so only rules that are correct for
*every possible input* run. Bit-identical to your original code, guaranteed.
- **the helpful default** — a small curated set of promises is on, which lets
Credence also run the extra rules. Correct for the ~99% of real code whose
running data is plain, single-piece text.
## Setting switches
Pass `assumptions:` as one of three things (to `Credence.fix/2`,
`Credence.analyze/2`, etc.), or set it project-wide via
`config :credence, assumptions: ...`:
- a **small map** naming only the switches you want to change —
`%{single_codepoint_graphemes: false}`. It *patches* only the keys it names;
every switch it does not mention falls through to the layer below.
- **`:strict`** — a full reset that forces **every** switch off.
- **`:default`** — a full reset that returns **every** switch to its built-in
default (the mirror of `:strict`).
## Three places, later wins
Credence folds three layers, later beating earlier:
**call options > `config :credence` > built-in defaults.** A place that isn't
set is skipped. Mechanically it starts from the full defaults map, then folds
each later layer on top: `:strict`/`:default` overwrite *all* keys, a small map
patches *only its own* keys (never expanded with defaults — that would let an
unmentioned switch stomp the layer beneath it).
# project-wide: play it safe everywhere
config :credence, assumptions: :strict
# one trusted run re-enables just this switch, leaving the rest off
Credence.fix(code, assumptions: %{single_codepoint_graphemes: true})
## Inspecting
`Credence.Pattern.rule_status/1` lists every rule, the promises it needs,
whether it is on now, and which needed promises are off.
`Credence.Pattern.enabled_rules/1` is the on-names from that list.
## The switches
### `single_codepoint_graphemes` (on by default)
Promises that every character (grapheme) in your **running data** is a single
codepoint — a plain single-piece character. No decomposed accents (an `"e"`
plus a *separate* combining accent mark), no ZWJ emoji (`👨‍👩‍👧`), no flag
sequences (`🇵🇱`). This is about the text your program *handles*, not the
characters in your source file. It is on by default because LLM-generated
Phoenix apps overwhelmingly process exactly this kind of text; turn it off
(or use `:strict`) if your code processes arbitrary Unicode.
### `proper_lists` (on by default)
Promises that every list your **running data** builds or receives is a
*proper* list — one whose final tail is `[]`, so the tail of any cons cell is
itself a list. It rules out *improper* lists such as `[1 | 2]`, whose tail is
a non-list value. This matters for rules that drop an `is_list/1` guard on a
cons-tail variable (`[head | tail] when is_list(tail)`): the guard is only
redundant when `tail` cannot be a non-list, i.e. under this promise. It is on
by default because improper lists are rare in application code; turn it off
(or use `:strict`) if your code deliberately constructs improper lists.
"""
@type name :: atom()
@type settings :: %{name() => boolean()}
@registry %{
single_codepoint_graphemes: %{
default: true,
summary:
"Every character in your running data is a single codepoint (no decomposed " <>
"accents, ZWJ emoji, or flag sequences). About running data, not source."
},
proper_lists: %{
default: true,
summary:
"Every list your running data builds or receives is a proper list — its " <>
"tail is always a list, never an improper cons like `[1 | 2]`. About " <>
"running data, not source."
}
}
@doc "The full registry: `%{name => %{default:, summary:}}`."
@spec all() :: %{name() => %{default: boolean(), summary: String.t()}}
def all, do: @registry
@doc "The names of all known switches."
@spec names() :: [name()]
def names, do: Map.keys(@registry)
@doc "The built-in default settings: `%{name => boolean}`."
@spec defaults() :: settings()
def defaults, do: Map.new(@registry, fn {name, %{default: d}} -> {name, d} end)
@doc "Whether `name` is a known switch."
@spec known?(term()) :: boolean()
def known?(name), do: Map.has_key?(@registry, name)
@doc """
Validates a user-supplied settings map, raising `ArgumentError` if it names a
switch that does not exist. Returns the map unchanged on success.
"""
@spec validate!(map()) :: map()
def validate!(map) when is_map(map) do
case map |> Map.keys() |> Enum.reject(&known?/1) do
[] ->
map
unknown ->
raise ArgumentError,
"unknown assumption(s): #{inspect(unknown)}. " <>
"Known assumptions: #{inspect(names())}. " <>
"Pass `:strict` to turn all off or `:default` to reset to defaults."
end
end
end