Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Clickwrap's value is evidence that is still true and still verifiable years afte
3. **Released evidence formats are permanent.** Every released receipt schema, canonicalization profile, digest field, event action, and lifecycle meaning gets a golden fixture, and new versions must keep verifying old receipts. A format change means a new explicit schema and verifier, never a silent reinterpretation. Do not edit a released migration underneath an installed application; add an upgrade migration.
4. **Required writes cannot be error-isolated.** Evidence and the protected database action commit together or not at all. Optional after-commit hooks, analytics, and notifications are isolated and can never undo a committed action or stand in for one.
5. **The browser is not a policy author.** Policy key, revision, document versions, validity, subject binding, retention, and request-evidence fields are resolved server-side and rechecked at submit. Never add a hidden field, parameter, or header that lets a client choose any of them.
6. **Collecting nothing is the gem's default; collecting the ordinary trio is one honest switch.** Every `record_*` flag ships false. Turning on the coarse trio — IP address, browser user agent, and a country/region/city estimate — is `config.record_request_evidence_by_default = true` and nothing else: the purpose and the disposal answer have gem-supplied defaults (`Vocabulary::DEFAULT_REQUEST_EVIDENCE_PURPOSE`, and no clock means it keeps pace with the evidence it corroborates), the per-field flags and per-policy `record_ip_address(...)`/`do_not_record_ip_address` still override it, and every finer geolocation field stays its own separately named one-liner. Never add a switch whose NAME hides what it collects (`gdpr_compliant_mode`, `full_evidence`, `maximum_evidence`, `legal_proof`) — that prohibition is about dishonest names and legal claims, and it survives; `record_request_evidence_by_default` says exactly what it records. **Owner directive, 2026-08-20 (v0.3.0), deliberately reversing the previous "every field needs a written purpose and a retention decision or boot fails" rule:** the ceremony was pushing integrators to collect nothing, and no evidence is worse than un-reviewed evidence. Do not "fix" this back. What is still refused: scaffolding text (`TODO`) standing in for a purpose the host actually wrote, a deletion clock set alongside a keep-indefinitely declaration for the same category, and turning encryption off without `deliberately_store_request_evidence_unencrypted!(because:)`.
6. **The host's privacy policy owns the why; the gem records the what, honestly.** Clickwrap is nobody's nanny. Its job is evidence mechanics, not gatekeeping collection: it never refuses to record something because a developer did not write a sentence justifying it. Every `record_*` flag still ships false, and the gem's own default is still record-nothing — but turning collection on is frictionless in every form. `config.record_request_evidence_by_default = true` records the coarse trio (IP address, browser user agent, country/region/city). `record_ip_address`, `record_browser_user_agent`, and `record_ip_geolocation` all take zero keyword arguments in a policy. Purposes, `legal_basis_reference:`, DPIA references, deletion clocks, `trusted_proxy_configuration_digest`, and the `because:` on `keep_recorded_*_indefinitely!` and `deliberately_store_request_evidence_unencrypted!` are ALL optional; gem-supplied honest defaults (`Vocabulary::DEFAULT_REQUEST_EVIDENCE_PURPOSE` and friends) fill every purpose field so no record ever carries a blank, and the privacy inventory marks each one `gem_default` or `host` so the gem's sentence never passes for a reviewed one. What survives, and is not negotiable: never add a switch whose NAME hides what it collects (`gdpr_compliant_mode`, `full_evidence`, `maximum_evidence`, `legal_proof`); never overclaim (rule 1); never blur `not_configured` / `unavailable` / `recorded` / `deleted_after_retention`; encryption stays on by default and `= false` still requires the named `deliberately_store_request_evidence_unencrypted!` call; scaffolding text (`TODO`) a host actually supplied is still rejected rather than recorded as a purpose; and a deletion clock declared alongside a keep-indefinitely for the same category is still refused as a contradiction. **Owner directive, 2026-08-20 (v0.3.0), deliberately reversing the previous "every field needs a written purpose and a retention decision or boot fails" rule:** the ceremony was pushing integrators to collect nothing, and no evidence is worse than un-reviewed evidence. Do not "fix" this back.
7. **Names read aloud.** Complete verb-and-noun names, positive booleans, destructive methods that say exactly what they delete, `ip_address` not `ip`, `browser_user_agent` not `ua`, `ip_geolocation` not `location`, `http_request` not `context`, `recorded_at_by_server` not `signed_at`. If an example does not make sense read aloud by a developer who has never seen the gem, the name is wrong.

## Working here
Expand Down
91 changes: 91 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,97 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.1] - 2026-08-20

### Changed — the rest of the collection friction, and the principle behind removing it

0.3.0 made *enabling* request evidence one switch and gave the initializer
defaults honest gem-supplied purposes. It left the friction standing
everywhere else: a policy-level `record_ip_address` still read as though it
wanted a sentence, `record_ip_geolocation` refused to do anything without a
field list, and the encryption escape hatch still demanded a reason be
phrased twice. This finishes the job the owner asked for.

The principle, in their words: **the host application and its privacy policy
own *why* data is collected; this gem records *what* was collected, honestly,
and is nobody's nanny.** Its job is evidence mechanics, not gatekeeping
collection. Everything below now works exactly as written:

```ruby
Clickwrap.configure do |config|
config.record_request_evidence_by_default = true
config.keep_recorded_ip_addresses_indefinitely!
config.deliberately_store_request_evidence_unencrypted!
end

Clickwrap.policy :anything do
agree_to :terms
record_ip_address
record_browser_user_agent
record_ip_geolocation
end
```

- **All three `record_` verbs take zero keyword arguments.** `because:`,
`legal_basis_reference:`, `data_protection_impact_assessment_reference:`,
`delete_after:`, `retain_until:`, and `encrypted:` are optional in fact and
not merely in the signature — nothing downstream refuses their absence.
(`legal_basis_reference` and the DPIA reference never were required anywhere
in the gem; a test now pins that they never become so.)
- **`record_ip_geolocation` with no field named records the coarse trio** —
country, region, city — the same set `record_request_evidence_by_default`
turns on, and nothing finer. The field keywords moved from `false` to `nil`
defaults so "did not mention this field" is distinguishable from "named it
and turned it off". Naming even one field means you are choosing the set
yourself, and the set is exactly what you named. Naming every field `false`
is still refused, because calling the verb and disabling everything cannot
mean anything; `do_not_record_ip_geolocation` is how to say that.
- **`keep_recorded_{ip_addresses,browser_user_agents,ip_geolocation}_indefinitely!`
take no arguments at all.**
- **`deliberately_store_request_evidence_unencrypted!` no longer needs a
`because:`.** The method NAME is the ceremony: `encrypt_recorded_* = false`
still cannot be reached without writing that line, and a reviewer still
finds it in the diff. When the host writes no reason the gem records
`Vocabulary::DEFAULT_REASON_FOR_STORING_REQUEST_EVIDENCE_UNENCRYPTED`.
Encryption itself is untouched — on by default for all three categories,
with a test pinning that the one switch does not weaken it.
- **The install generator stops refusing an incomplete category.**
`--record-ip-addresses-by-default` with no reason and no period writes the
file and simply omits those two lines, so the gem's own defaults apply.
- The `ReviewedText` placeholder check now only ever applies to text a host
actually supplied. Absence is never scaffolding.

### Unchanged, deliberately

- Reasons that are not about *collection* keep their required `because:`:
`delete_recorded_ip_address!` and its siblings, `dispose_core_event!`,
`place_on_legal_hold!` / `release_legal_hold!`, `plan_disposition_for`,
unredacted receipt export, and the lifecycle verbs. Those record a
destructive act, an access, or a state change — there the audit trail *is*
the reason, and there is no honest default for "why did somebody delete
this".
- Still refused, because each is the host contradicting themselves rather than
leaving a blank: scaffolding text the host actually wrote standing in for a
purpose (including in the installer, where a `TODO` in a shipped initializer
is worse than no line and the gem would reject it at boot anyway), a
negative deletion period passed to the installer, a deletion clock declared
alongside `keep_recorded_..._indefinitely!` for the same category, and
`record_ip_geolocation` with every field explicitly off.
- The gem's code default is still record-nothing. Claim boundaries, receipt
state labeling (`not_configured` / `unavailable` / `recorded` /
`deleted_after_retention`), and every released receipt format are untouched.

### Documentation

- README, `guides/request-evidence.md`, and `guides/naming.md` show the
zero-keyword forms as the ordinary way to write a policy, with purposes,
legal bases, and clocks as the upgrade path.
- `CLAUDE.md` / `AGENTS.md` rule 6 is rewritten around the principle above and
records the owner directive and its date. What it keeps as non-negotiable:
never a switch whose NAME hides what it collects, never an overclaim, never
a blurred receipt state, encryption on by default behind its named call,
host-supplied scaffolding rejected, and contradictions refused.

## [0.3.0] - 2026-08-20

### Changed — recording request evidence is one switch, not a checklist
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Clickwrap's value is evidence that is still true and still verifiable years afte
3. **Released evidence formats are permanent.** Every released receipt schema, canonicalization profile, digest field, event action, and lifecycle meaning gets a golden fixture, and new versions must keep verifying old receipts. A format change means a new explicit schema and verifier, never a silent reinterpretation. Do not edit a released migration underneath an installed application; add an upgrade migration.
4. **Required writes cannot be error-isolated.** Evidence and the protected database action commit together or not at all. Optional after-commit hooks, analytics, and notifications are isolated and can never undo a committed action or stand in for one.
5. **The browser is not a policy author.** Policy key, revision, document versions, validity, subject binding, retention, and request-evidence fields are resolved server-side and rechecked at submit. Never add a hidden field, parameter, or header that lets a client choose any of them.
6. **Collecting nothing is the gem's default; collecting the ordinary trio is one honest switch.** Every `record_*` flag ships false. Turning on the coarse trio — IP address, browser user agent, and a country/region/city estimate — is `config.record_request_evidence_by_default = true` and nothing else: the purpose and the disposal answer have gem-supplied defaults (`Vocabulary::DEFAULT_REQUEST_EVIDENCE_PURPOSE`, and no clock means it keeps pace with the evidence it corroborates), the per-field flags and per-policy `record_ip_address(...)`/`do_not_record_ip_address` still override it, and every finer geolocation field stays its own separately named one-liner. Never add a switch whose NAME hides what it collects (`gdpr_compliant_mode`, `full_evidence`, `maximum_evidence`, `legal_proof`) — that prohibition is about dishonest names and legal claims, and it survives; `record_request_evidence_by_default` says exactly what it records. **Owner directive, 2026-08-20 (v0.3.0), deliberately reversing the previous "every field needs a written purpose and a retention decision or boot fails" rule:** the ceremony was pushing integrators to collect nothing, and no evidence is worse than un-reviewed evidence. Do not "fix" this back. What is still refused: scaffolding text (`TODO`) standing in for a purpose the host actually wrote, a deletion clock set alongside a keep-indefinitely declaration for the same category, and turning encryption off without `deliberately_store_request_evidence_unencrypted!(because:)`.
6. **The host's privacy policy owns the why; the gem records the what, honestly.** Clickwrap is nobody's nanny. Its job is evidence mechanics, not gatekeeping collection: it never refuses to record something because a developer did not write a sentence justifying it. Every `record_*` flag still ships false, and the gem's own default is still record-nothing — but turning collection on is frictionless in every form. `config.record_request_evidence_by_default = true` records the coarse trio (IP address, browser user agent, country/region/city). `record_ip_address`, `record_browser_user_agent`, and `record_ip_geolocation` all take zero keyword arguments in a policy. Purposes, `legal_basis_reference:`, DPIA references, deletion clocks, `trusted_proxy_configuration_digest`, and the `because:` on `keep_recorded_*_indefinitely!` and `deliberately_store_request_evidence_unencrypted!` are ALL optional; gem-supplied honest defaults (`Vocabulary::DEFAULT_REQUEST_EVIDENCE_PURPOSE` and friends) fill every purpose field so no record ever carries a blank, and the privacy inventory marks each one `gem_default` or `host` so the gem's sentence never passes for a reviewed one. What survives, and is not negotiable: never add a switch whose NAME hides what it collects (`gdpr_compliant_mode`, `full_evidence`, `maximum_evidence`, `legal_proof`); never overclaim (rule 1); never blur `not_configured` / `unavailable` / `recorded` / `deleted_after_retention`; encryption stays on by default and `= false` still requires the named `deliberately_store_request_evidence_unencrypted!` call; scaffolding text (`TODO`) a host actually supplied is still rejected rather than recorded as a purpose; and a deletion clock declared alongside a keep-indefinitely for the same category is still refused as a contradiction. **Owner directive, 2026-08-20 (v0.3.0), deliberately reversing the previous "every field needs a written purpose and a retention decision or boot fails" rule:** the ceremony was pushing integrators to collect nothing, and no evidence is worse than un-reviewed evidence. Do not "fix" this back.
7. **Names read aloud.** Complete verb-and-noun names, positive booleans, destructive methods that say exactly what they delete, `ip_address` not `ip`, `browser_user_agent` not `ua`, `ip_geolocation` not `location`, `http_request` not `context`, `recorded_at_by_server` not `signed_at`. If an example does not make sense read aloud by a developer who has never seen the gem, the name is wrong.

## Working here
Expand Down
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,7 @@ end

That records, on every policy: the IP address the request arrived from, the browser user agent it sent, and a coarse country / region / city estimate for that address. Add [`trackdown`](https://github.com/rameerez/trackdown) 0.4+ to your Gemfile and the geolocation half resolves itself — Clickwrap picks up the official adapter with no wiring line, so trackdown plus Cloudflare genuinely is "bundle it and flip the switch".

Nothing else is required, because Clickwrap supplies honest defaults for the parts you did not write:
Nothing else is required — anywhere, at either level. **Your privacy policy owns the why; the gem records the what, honestly, and is nobody's nanny.** Clickwrap supplies its own defaults for every part you did not write:

- **Purpose.** Every recorded field carries one into the receipt. Yours if you wrote one, otherwise Clickwrap's: *"Corroborate who performed each recorded act, from where, on what client — to defend the recorded agreement itself."* The [privacy inventory](#operations) marks which of the two it is reading back (`"purpose_source": "gem_default"` vs `"host"`), so a gem sentence never passes for a decision your team reviewed.
- **How long.** No clock means it keeps pace with the evidence it corroborates — the same posture core evidence has had since 0.2.0. A corroboration scheduled to expire before the agreement it corroborates is a scheduled weakening of the record.
Expand Down Expand Up @@ -772,7 +772,23 @@ end

Two things are still refused, and both are you contradicting yourself rather than leaving a blank: scaffolding text (`"TODO: ask legal"`) standing in for a purpose, and a deletion clock set alongside `keep_recorded_..._indefinitely!` for the same category.

A single regulated surface can also name a field per policy instead of by default:
### Per policy, with as much or as little as you want

A single surface can name the fields itself instead of inheriting the default. The frictionless form takes no arguments at all:

```ruby
Clickwrap.policy :withdrawal_authorization do
authorize :regulated_action, one_time: true, valid_for: 10.minutes

record_ip_address
record_browser_user_agent
record_ip_geolocation
end
```

`record_ip_geolocation` with no field named records the same coarse trio as the switch — country, region, city. Name even one field and you are choosing the set yourself, and the set is exactly what you named.

The same declarations with the full record a reviewed team would want:

```ruby
Clickwrap.policy :regulated_authorization do
Expand All @@ -791,9 +807,11 @@ end
Recorded values live in a separately encrypted annex with their own retention, so
they can be deleted later without rewriting the core event payload. Core payloads
have their own reviewed disposition path and leave a digest-linked tombstone.
Encryption is on by default and turning it off keeps its own ceremony —
`config.deliberately_store_request_evidence_unencrypted!(because: "…")` — because
that one is a real hazard, not paperwork.
Encryption is on by default for all three categories. Turning it off keeps its
own ceremony — you cannot reach `encrypt_recorded_ip_addresses = false` without
first writing `config.deliberately_store_request_evidence_unencrypted!` — but the
ceremony is the method name a reviewer finds in the diff, not a sentence the gem
makes you type. `because:` there is optional too.

For IP geolocation, [`trackdown`](https://github.com/rameerez/trackdown) 0.4 or newer is the official resolver, and Clickwrap uses it automatically when your bundle has it and you named no resolver of your own. Set it explicitly when you want a different provider per policy, or when you are wiring Trackdown's per-request CDN trust:

Expand Down
2 changes: 1 addition & 1 deletion guides/naming.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ object.
| `chain_event_history_with`, `anchor_event_history_with`, `timestamp_receipts_with` | one `integrity_level` setting | Three different mechanisms making three different claims. One setting would let a reader infer the strongest from the presence of the weakest |
| `after_event_is_committed` | `after_commit` | Says which commit, and reads as a sentence |
| `authorize_unredacted_request_evidence_access_with` | `access_control` | Long, and correct. It names exactly which access it authorizes |
| `deliberately_store_request_evidence_unencrypted!(because:)` | `encryption: false` | Turning encryption off should be a sentence a reviewer can find in a diff, with the host's own reason attached — not a `false` |
| `deliberately_store_request_evidence_unencrypted!` | `encryption: false` | Turning encryption off should be a sentence a reviewer can find in a diff — not a `false`. The method NAME is the ceremony; its `because:` is optional |

The last row is the pattern worth copying. When an option has a consequence somebody should
have to think about, make the name carry the thinking.
Expand Down
Loading
Loading