clickwrap 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +90 -0
  3. data/CHANGELOG.md +650 -0
  4. data/README.md +830 -1204
  5. data/SECURITY.md +33 -0
  6. data/app/assets/stylesheets/clickwrap.css +245 -0
  7. data/app/controllers/clickwrap/application_controller.rb +79 -0
  8. data/app/controllers/clickwrap/captures_controller.rb +145 -0
  9. data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
  10. data/app/controllers/clickwrap/receipts_controller.rb +115 -0
  11. data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
  12. data/app/helpers/clickwrap/engine_helper.rb +97 -0
  13. data/app/views/clickwrap/captures/show.html.erb +34 -0
  14. data/app/views/clickwrap/receipts/index.html.erb +38 -0
  15. data/app/views/clickwrap/receipts/show.html.erb +91 -0
  16. data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
  17. data/app/views/clickwrap/shared/_fields.html.erb +100 -0
  18. data/app/views/clickwrap/shared/_statement.html.erb +105 -0
  19. data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
  20. data/config/locales/en.yml +160 -0
  21. data/config/locales/es.yml +154 -0
  22. data/config/routes.rb +41 -0
  23. data/exe/clickwrap +374 -0
  24. data/guides/README.md +30 -0
  25. data/guides/accessibility.md +249 -0
  26. data/guides/consent-and-lifecycle.md +299 -0
  27. data/guides/integrating.md +610 -0
  28. data/guides/integrity.md +212 -0
  29. data/guides/migrating.md +335 -0
  30. data/guides/naming.md +320 -0
  31. data/guides/organizations.md +320 -0
  32. data/guides/receipts-and-verification.md +415 -0
  33. data/guides/request-evidence.md +512 -0
  34. data/guides/retention-and-legal-holds.md +438 -0
  35. data/lib/clickwrap/actor_proxy.rb +147 -0
  36. data/lib/clickwrap/anonymous_actor.rb +47 -0
  37. data/lib/clickwrap/authority.rb +174 -0
  38. data/lib/clickwrap/canonical_json.rb +216 -0
  39. data/lib/clickwrap/capture/event_builder.rb +220 -0
  40. data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
  41. data/lib/clickwrap/capture.rb +650 -0
  42. data/lib/clickwrap/configuration.rb +1129 -0
  43. data/lib/clickwrap/controller_helpers.rb +758 -0
  44. data/lib/clickwrap/current_state.rb +282 -0
  45. data/lib/clickwrap/digest.rb +125 -0
  46. data/lib/clickwrap/doctor.rb +418 -0
  47. data/lib/clickwrap/document_definition.rb +255 -0
  48. data/lib/clickwrap/document_renderer.rb +83 -0
  49. data/lib/clickwrap/document_renderers/markdown.rb +175 -0
  50. data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
  51. data/lib/clickwrap/dsl/policy_builder.rb +462 -0
  52. data/lib/clickwrap/dsl/retention_builder.rb +89 -0
  53. data/lib/clickwrap/durable_commit_callback.rb +37 -0
  54. data/lib/clickwrap/engine.rb +187 -0
  55. data/lib/clickwrap/errors.rb +181 -0
  56. data/lib/clickwrap/form_builder_extensions.rb +341 -0
  57. data/lib/clickwrap/front_matter.rb +67 -0
  58. data/lib/clickwrap/identifier.rb +112 -0
  59. data/lib/clickwrap/import/external_receipt.rb +241 -0
  60. data/lib/clickwrap/import/fine_print.rb +290 -0
  61. data/lib/clickwrap/import/legacy.rb +450 -0
  62. data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
  63. data/lib/clickwrap/integrity/anchor.rb +130 -0
  64. data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
  65. data/lib/clickwrap/integrity/attestor.rb +221 -0
  66. data/lib/clickwrap/integrity/chain.rb +313 -0
  67. data/lib/clickwrap/integrity/timestamp.rb +143 -0
  68. data/lib/clickwrap/ip_geolocation/location.rb +112 -0
  69. data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
  70. data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
  71. data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
  72. data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
  73. data/lib/clickwrap/ip_geolocation.rb +16 -0
  74. data/lib/clickwrap/lifecycle.rb +534 -0
  75. data/lib/clickwrap/linter.rb +382 -0
  76. data/lib/clickwrap/localized_text.rb +101 -0
  77. data/lib/clickwrap/macros.rb +203 -0
  78. data/lib/clickwrap/models/application_record.rb +20 -0
  79. data/lib/clickwrap/models/chain_head.rb +79 -0
  80. data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
  81. data/lib/clickwrap/models/disposition_plan.rb +208 -0
  82. data/lib/clickwrap/models/document.rb +46 -0
  83. data/lib/clickwrap/models/document_version.rb +163 -0
  84. data/lib/clickwrap/models/event.rb +743 -0
  85. data/lib/clickwrap/models/event_document.rb +79 -0
  86. data/lib/clickwrap/models/event_statement.rb +92 -0
  87. data/lib/clickwrap/models/external_action.rb +150 -0
  88. data/lib/clickwrap/models/integrity_attestation.rb +90 -0
  89. data/lib/clickwrap/models/legal_hold.rb +81 -0
  90. data/lib/clickwrap/models/policy_revision.rb +115 -0
  91. data/lib/clickwrap/models/presentation.rb +59 -0
  92. data/lib/clickwrap/models/receipt_access.rb +53 -0
  93. data/lib/clickwrap/models/recording_sequence.rb +21 -0
  94. data/lib/clickwrap/models/request_evidence.rb +378 -0
  95. data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
  96. data/lib/clickwrap/models/statement_state.rb +130 -0
  97. data/lib/clickwrap/pending_receipt.rb +177 -0
  98. data/lib/clickwrap/policy.rb +283 -0
  99. data/lib/clickwrap/presentation_manifest.rb +210 -0
  100. data/lib/clickwrap/presenter.rb +729 -0
  101. data/lib/clickwrap/privacy.rb +419 -0
  102. data/lib/clickwrap/protected_outcome.rb +120 -0
  103. data/lib/clickwrap/receipt.rb +606 -0
  104. data/lib/clickwrap/receipt_html.rb +235 -0
  105. data/lib/clickwrap/receipt_verifier.rb +978 -0
  106. data/lib/clickwrap/reference.rb +44 -0
  107. data/lib/clickwrap/registration.rb +236 -0
  108. data/lib/clickwrap/registry.rb +54 -0
  109. data/lib/clickwrap/remediation_token.rb +155 -0
  110. data/lib/clickwrap/request_evidence_extractor.rb +590 -0
  111. data/lib/clickwrap/request_evidence_policy.rb +261 -0
  112. data/lib/clickwrap/retention/applier.rb +231 -0
  113. data/lib/clickwrap/retention/disposition.rb +221 -0
  114. data/lib/clickwrap/retention/planner.rb +502 -0
  115. data/lib/clickwrap/retention_class.rb +97 -0
  116. data/lib/clickwrap/reviewed_text.rb +28 -0
  117. data/lib/clickwrap/schema_requirements.rb +196 -0
  118. data/lib/clickwrap/services/authorize_external_action.rb +149 -0
  119. data/lib/clickwrap/services/load_policies.rb +69 -0
  120. data/lib/clickwrap/services/publish_documents.rb +251 -0
  121. data/lib/clickwrap/services/validate_policy_references.rb +166 -0
  122. data/lib/clickwrap/statement.rb +248 -0
  123. data/lib/clickwrap/subject_fingerprint.rb +28 -0
  124. data/lib/clickwrap/submission.rb +169 -0
  125. data/lib/clickwrap/system_actor.rb +31 -0
  126. data/lib/clickwrap/test_helpers.rb +676 -0
  127. data/lib/clickwrap/testing.rb +211 -0
  128. data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
  129. data/lib/clickwrap/verification.rb +504 -0
  130. data/lib/clickwrap/version.rb +12 -1
  131. data/lib/clickwrap/view_helpers.rb +190 -0
  132. data/lib/clickwrap/vocabulary.rb +294 -0
  133. data/lib/clickwrap.rb +497 -7
  134. data/lib/generators/clickwrap/document_generator.rb +164 -0
  135. data/lib/generators/clickwrap/hardening_generator.rb +177 -0
  136. data/lib/generators/clickwrap/install_generator.rb +1287 -0
  137. data/lib/generators/clickwrap/link_generator.rb +56 -0
  138. data/lib/generators/clickwrap/policy_generator.rb +118 -0
  139. data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
  140. data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
  141. data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
  142. data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
  143. data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
  144. data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
  145. data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
  146. data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
  147. data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
  148. data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
  149. data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
  150. data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
  151. data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
  152. data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
  153. data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
  154. data/lib/generators/clickwrap/views_generator.rb +101 -0
  155. data/lib/tasks/clickwrap.rake +569 -0
  156. metadata +311 -16
data/CHANGELOG.md CHANGED
@@ -1,5 +1,655 @@
1
1
  # Changelog
2
2
 
3
+ All notable changes to this project are documented here.
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.1] - 2026-08-19
10
+
11
+ ### Fixed — the composed sentence in a language that declines its articles
12
+
13
+ - **Per-key sentence fragments.** Spanish cannot say "acepto %{documents}" for
14
+ every document — the article agrees with the noun ("los Términos", "la
15
+ Política"). The composer now looks up
16
+ `clickwrap.sentence.fragments.<kind>.<key>` before falling back to the
17
+ per-kind template, and the gem ships Spanish defaults for its standard keys.
18
+ An application adds its own in its locale files; no DSL change.
19
+ - **One opening capital.** Fragment templates are written lowercase for the
20
+ middle of a sentence; the composer capitalizes exactly one letter — the
21
+ first — once, at build time, so the signed manifest and the rendered HTML
22
+ can never disagree. The Spanish signup now reads
23
+ "Acepto los Términos y Condiciones y doy por recibida la Política de
24
+ Privacidad." instead of "Acepto Términos y Condiciones y He recibido
25
+ Política de Privacidad."
26
+ - **One voice, composed or itemized.** The Spanish composed acknowledgment now
27
+ says "doy por recibida" — the same words as the itemized default statement —
28
+ instead of the flatter "he recibido". The same act reads the same way in
29
+ either rendering. (Still never "he leído": the evidence records an
30
+ affirmative act on an offered notice, not that anyone read it. A host that
31
+ wants the first-person read-declaration owns that wording via its locale
32
+ files — see the next fix.)
33
+ - **Host locale overrides actually win now.** The engine appended its locale
34
+ files to `app.config.i18n.load_path` on top of Rails's automatic engine
35
+ locale loading. Railties paths are unshifted ahead of that list, so the
36
+ appended copy landed AFTER the host's own locale files — and every host
37
+ override of a gem key silently lost to the gem's default. The manual append
38
+ is gone; Rails::Engine's own `:add_locales` ordering (gem first, host last)
39
+ is the contract, and a test now pins it.
40
+ - **Checkbox optical alignment.** The box was mathematically centred on the
41
+ first line and still read as floating high: Latin text carries its mass
42
+ between cap-height and baseline, below the line box's midpoint. The offset
43
+ gains an optical eighth of an em, calibrated against rendered screenshots.
44
+
45
+ ## [0.1.0] - 2026-08-19
46
+
47
+ ### Changed — the signup clickwrap is one line
48
+
49
+ - **`form.clickwrap` renders ONE checkbox carrying ONE sentence** whenever every
50
+ statement in a policy is an ordinary, required, default-worded `agree_to` or
51
+ `acknowledge`:
52
+
53
+ ☐ I agree to the Terms of Service and I acknowledge the Privacy Policy.
54
+
55
+ The documents are linked inside the sentence, and the label IS the sentence —
56
+ pressing the words toggles the control and assistive technology announces both
57
+ together. Gone from the rendering: the "Required" flag, the visible "(opens in
58
+ a new tab)" hint, the version label under each link, and the list of documents
59
+ under each statement. The `required` attribute stays as progressive
60
+ enhancement (THE SERVER DECIDES is unchanged), the new-tab truth is now an
61
+ `sr-only` span rendered only when the link really does open one, and versions
62
+ stay on receipts, where somebody reading the record can act on them.
63
+
64
+ The evidence is unchanged. One control, several statements: the manifest signs
65
+ which statement keys it covers and which key it is submitted under, and the
66
+ server fans that one answer out to all of them — each recorded with its own
67
+ kind, documents, assertion, and lifecycle, exactly as before. An unticked or
68
+ absent control refuses every one of them. Whether one answer may cover several
69
+ statements is re-checked at capture against the FROZEN POLICY REVISION, not
70
+ taken on the token's word.
71
+
72
+ Nothing that a person could reasonably want to answer differently is folded
73
+ in: optional consents, recorded yes/nos, statements with a withdrawal route,
74
+ and copy an application wrote itself all keep a control of their own below the
75
+ line. A policy with nothing composable — operator attestation rails, payout
76
+ authorizations — renders exactly as it did before. `combined: false` on
77
+ `form.clickwrap` / `form.clickwrap_fields` asks for the itemized shape, and it
78
+ reaches the presenter rather than the template, so the manifest signs the
79
+ shape that was offered. An itemized manifest is byte-identical to the ones
80
+ earlier versions wrote.
81
+
82
+ The words are translations: `clickwrap.sentence.agreement`,
83
+ `clickwrap.sentence.acknowledgment`, and three connectives, with `%{documents}`
84
+ marking where the links go. A locale that has not translated them renders the
85
+ itemized shape rather than half an English sentence.
86
+
87
+ Custom surfaces iterate `presentation.itemized_statements` rather than
88
+ `statements`, and render `clickwrap_combined_sentence(presentation.combined)`
89
+ when there is one. The presentation linter keeps its preselected-control and
90
+ action-ordering rules and gains
91
+ `combined_statement_rendered_as_its_own_control`, which is where an ejected
92
+ view lands after this change.
93
+
94
+ - **Receipts record the sentence that was read.** `presentation` gained
95
+ `combined_sentence` and `combined_statements`, shown on the HTML receipt and
96
+ the engine's receipt screen. The acts say what was recorded; this says what
97
+ was on the screen. Both are absent — not null — on an itemized presentation,
98
+ so every receipt written before this change verifies byte for byte exactly as
99
+ it did.
100
+
101
+ ### Added — `link:`, the page a person actually reads a document on
102
+
103
+ - **`Clickwrap.document :terms, from: ..., link: "/legal/terms"`** presents and
104
+ signs the host's own formatted page instead of the engine's rendering of the
105
+ published bytes. It is the path rendered AND the path signed, so evidence
106
+ never cites a different target from the link somebody pressed, and the trade
107
+ is written where a reviewer sees it: a host page shows whatever is current, so
108
+ the signed path is a stable address rather than an immutable snapshot. The
109
+ bytes stay frozen, digested, and recorded either way.
110
+
111
+ Precedence: a resolver passed at the call site, then `link:`, then the render
112
+ context's default resolver (the mounted engine route, with this request's
113
+ Hotwire Native treatment attached — which therefore applies to `link:` paths
114
+ unchanged), then the engine's own routes. The refusal to sign a link an
115
+ unmounted engine cannot resolve is about the ENGINE route, so a policy whose
116
+ every document names a host page now presents with no mount at all, and the
117
+ refusal keeps firing for every document that does not.
118
+
119
+ A `link:` is checked at boot for a scheme a browser can navigate:
120
+ `javascript:`, `data:`, a bare word, and protocol-relative `//host` are
121
+ refused with the sentence that fixes them.
122
+
123
+ - The presenter's framework wiring moved from `document_version_path_with:` to
124
+ `default_document_version_path_with:`, leaving the former as what it always
125
+ read like — the host's own resolver, which still wins outright.
126
+ `clickwrap_document_version_path_for_presentation` gained a `declared_link:`
127
+ keyword.
128
+
129
+ ### Added — the quickstart runs in CI
130
+
131
+ - **The README quickstart is now executed end to end**, in a subprocess, against
132
+ a throwaway application built only from what the installer emitted: install →
133
+ migrate → declare → `has_clickwraps` → publish → render the form → submit the
134
+ token that render produced → the evidence row exists → the receipt verifies.
135
+ It is the one document every user reads and was the only one nothing checked.
136
+
137
+ It found real defects on its first run, all of them consequences of emitting
138
+ fewer tables: capture marked a presentation accepted without asking whether
139
+ the policy retains presentations, and receipts, verification, and the
140
+ retention planner read optional annex tables unconditionally. Every
141
+ association whose table is optional now answers "there is nothing here" when
142
+ the table was never created — which is not a fallback but the exact truth,
143
+ since without the table no row was ever written.
144
+
145
+ ### Changed — the emitted footprint
146
+
147
+ - **The generated initializer states decisions, not defaults.** It carried about
148
+ thirty live lines that assigned the gem's own default back to itself, so a
149
+ reader could not tell which lines somebody had chosen and which were noise —
150
+ and every one of them was a line to maintain in two places forever. Live lines
151
+ are now only what the installer actually decided or detected; every other
152
+ setting appears commented, with its default value, under prose that says what
153
+ it does. A default install drops from about thirty live settings to three,
154
+ plus the eleven request-evidence lines.
155
+
156
+ Those eleven stay live even when every answer is `false`, deliberately: each
157
+ is an answer to a question the installer asked, and "we decided not to collect
158
+ this" is a decision worth reading rather than inferring from a file that does
159
+ not mention it. The file says so where the rule is stated.
160
+ - **`clickwrap:install` emits only the tables an installation can put a row
161
+ in.** Seven of the seventeen tables — persisted presentations, the
162
+ request-evidence annex, chain heads, integrity attestations, external
163
+ actions, disposition plans, legal holds — are each gated on a configuration
164
+ that is off by default, so a default install could never write to any of
165
+ them. A schema that contains them anyway claims capabilities and data
166
+ categories the application does not have, which is exactly the impression
167
+ this gem exists not to give. Each now arrives with its own flag
168
+ (`--with-persisted-presentations`, `--with-request-evidence`,
169
+ `--with-integrity`, `--with-retention-ops`, `--with-external-actions`),
170
+ following the `clickwrap:hardening` precedent, and re-running the generator
171
+ later with a flag adds that migration then.
172
+
173
+ Enabling a request-evidence field implies `--with-request-evidence`: an
174
+ installation that records IP addresses into a table it never created is not
175
+ a schema choice, and the operator already answered the question that matters.
176
+
177
+ The one failure mode this trade creates — turning a capability on and
178
+ forgetting its migration — is caught three ways, each naming the exact
179
+ command: at boot, by `bin/rails clickwrap:doctor`, and at the entry points
180
+ no configuration announces (`authorize_external_action!`, legal holds,
181
+ disposition planning). The boot check deliberately stays quiet while
182
+ migrations are pending, because refusing to boot would make the fix
183
+ unrunnable.
184
+
185
+ ### Fixed — the two-audit adversarial review
186
+
187
+ Two independent audits read the gem as an unfamiliar developer would: one
188
+ followed the installer and the README literally, the other read the code
189
+ against its own claims. Everything below is a defect they evidenced.
190
+
191
+ - **The installer and the README never taught the auth door.** A Devise app
192
+ that followed the post-install message verbatim created accounts with no
193
+ evidence at all, silently — the gem is never called on that path, so nothing
194
+ could warn about it. The post-install message and the README quickstart now
195
+ both carry the door step, with the exact line, the host's own file path and
196
+ class name, and the `devise_for ..., controllers:` route that makes the
197
+ subclass reachable. "The installer detects your authentication stack and
198
+ generates an explicit adapter" now says what it does: it detects, and prints
199
+ the line for you to add.
200
+ - **`renew!`, `correct_declaration!`, and `change_consent_scope!` had no tests,
201
+ no documentation, and no callers.** They are the conceptual spine of the six
202
+ verbs, so they are now tested behaviorally — the event is appended, the
203
+ earlier event still says exactly what it said and still verifies, and the new
204
+ receipt verifies on its own — and the README shows each one with the sentence
205
+ that says what it means. Writing those tests surfaced the reason nobody had
206
+ used them: all three capture through a real presentation, so each needs a
207
+ `submission:` exactly as the original statement did. That is correct — a
208
+ correction, a renewal and a rescope are new statements by the same person,
209
+ not administrative flags — but it was nowhere in the docs, and the README now
210
+ says it.
211
+ - **`config.actor_class_name` now decides something.** The installer spends
212
+ sixty lines, a `--actor-class` option, and a seven-line warning on this
213
+ setting because "a wrong guess attributes evidence to the wrong kind of
214
+ record for years" — which was not true, because nothing checked. The setting
215
+ fed one error string, and `Configuration#actor_class` had no production
216
+ caller at all. Capture now asserts the recorded actor is an instance of it,
217
+ with an error that names the setting and points an organization at
218
+ `acting_for:` where it belongs. System actors, anonymous actors, and literal
219
+ references to actors in other systems are their own kinds, say so in the
220
+ receipt, and are not checked against the class.
221
+ - **The receipt list authorized after paging.** Taking a page of rows and
222
+ filtering them afterwards rendered "you have no receipts" whenever the
223
+ viewer's newest page happened to be rows the host would not show them, while
224
+ readable ones sat past the limit. Authorization now happens first, in bounded
225
+ batches, and the actor is eager-loaded — a full page is three queries instead
226
+ of fifty-two.
227
+ - **A presentation resolved its documents one statement at a time**, including
228
+ the same document twice when two statements named it, even though documents
229
+ are immutable and published and the answer cannot differ. Two queries now
230
+ resolve every document a policy references.
231
+ - **`Clickwrap.reset!` left the presentation-manifest verifier memoized**, so a
232
+ verifier built under the previous configuration survived the reset. The test
233
+ suite had been resetting it by hand, which is why nothing caught it.
234
+ - **`Clickwrap.verify(event_id, …)` silently dropped its strictness keywords.**
235
+ The event-id branch accepted `require_current_revision:` and then never
236
+ received it, so a host asking "is this old evidence still good?" about one
237
+ recorded act got an unqualified yes — worse than an error, because it looked
238
+ like an answer. Both branches now answer the whole question: the event id
239
+ compares the act's recorded policy revision against the wording compiled
240
+ today, and re-derives the subject fingerprint from the live record when
241
+ `subject:` is passed. A policy that is no longer declared answers
242
+ `:unknown_policy` rather than passing, because "we can no longer check this"
243
+ must never be spelled the same way as "this is fine". `Clickwrap.verify(event_id,
244
+ subject:, require_current_revision: true)` is therefore the complete public
245
+ form of that question, and nothing needs to reach into
246
+ `Clickwrap::PolicyRevision` or `Clickwrap::SubjectFingerprint` to ask it.
247
+ Verifying an event id also no longer writes: the revision comparison is by
248
+ digest, so a read-only question stops creating a revision row as a side
249
+ effect.
250
+ - **An unmounted engine used to sign 404 document links into evidence.** The
251
+ document link goes into the presentation manifest, into the digest, and into
252
+ the record of what was offered; built from an unmounted engine's routes it
253
+ resolves to nothing, and nothing downstream can detect that afterwards
254
+ because the digest is over the wrong link. Presenting now refuses at build
255
+ time with the mount line in the sentence, `bin/rails clickwrap:doctor`
256
+ reports the missing mount as a problem whenever any policy is compiled (it
257
+ used to speak only about `requires_clickwrap` gates), and a non-interactive
258
+ install mounts the engine and says so instead of silently taking the `[y/N]`
259
+ default — `--skip-routes` is the one flag that declines.
260
+
261
+ ### Added — the dream-API pass (driven by installing the gem into a second real application)
262
+
263
+ Two real installations wrote about a hundred lines of nearly identical glue
264
+ around this gem. That is the strongest possible evidence that the code belongs
265
+ in the gem, so each ceremony became behavior, working backwards from the API we
266
+ wished we had written.
267
+
268
+ - **`version:` is optional when the source names its own.** A document declared
269
+ `from:` a file — or with inline `content:` — whose leading YAML front matter
270
+ carries `clickwrap_version:` or `last_updated:` resolves its version label
271
+ from there, so the file that *is* the legal text also names its version and
272
+ there is no second copy of the label anywhere to drift. `clickwrap_version:`
273
+ outranks `last_updated:` on purpose: a same-day correction that still changes
274
+ bytes needs a fresh label while the date readers see stays put. Both host
275
+ applications had written the same front-matter reader module to do this
276
+ themselves; that module stops existing. The old failure mode was a label
277
+ living in `config/clickwrap.rb` and words living in a file, changed in
278
+ separate edits — which either publishes as "same label, different bytes"
279
+ (refused at publish, correctly, but a step later than it needed to be) or
280
+ quietly leaves the label describing text nobody is serving any more. A source
281
+ with no front-matter key and no `version:` is now a boot failure with the fix
282
+ in the sentence, and a `resolver:` source (whose bytes are only read at
283
+ publish time) says exactly why it cannot name its own. Clickwrap never invents a
284
+ label, because a policy that requires a current version cannot be satisfied
285
+ by a guess.
286
+ - **`save`/`save!` pairing for doors and captures.** `register_with_clickwrap`
287
+ (non-bang) absorbs a refused registration — a stale presentation, an unticked
288
+ control, a validation the account failed — into the exact human sentences the
289
+ Devise adapter paints, through one shared `Clickwrap::Registration.absorb_refusal`:
290
+ inline via `clickwrap_errors` beside the control it belongs to, once on the
291
+ record's `:base`, then `false`, ready for `render :new, status: :unprocessable_entity`.
292
+ `capture_clickwrap` and `capture_clickwrap_and` do the same for
293
+ `Clickwrap::CaptureRefused`, leaving the refusal on `clickwrap_refusal` with
294
+ its `user_facing_message`. The old failure mode was every hand-rolled door
295
+ writing its own rescue, each one slightly different, and one of them
296
+ eventually rescuing too much. Infrastructure failures still escape every
297
+ form — `Clickwrap::EventWriteFailed` is not a validation error to dress up —
298
+ and lifecycle conflicts (`ReplayRejected`, `OneTimeAuthorizationConflict`)
299
+ still raise, because "this was already done" needs a domain answer that no
300
+ generic rescue can supply honestly.
301
+ - **`config.document_renderer = :markdown_rails` renders through the
302
+ application's own renderer.** Content-file Rails apps already serve their
303
+ public legal pages through a registered markdown-rails handler; when the same
304
+ files are Clickwrap documents, what people accept and what the page serves
305
+ must be the same bytes — same renderer, same options, no second sanitization
306
+ pass changing entities behind the digest. Wiring that by hand meant a lambda,
307
+ an engine name, gem versions, and a parity test copied between applications;
308
+ this setting says it once, records honest provenance (the renderer class the
309
+ application registered, plus the markdown-rails and Markdown-engine gem
310
+ versions), and resolves the handler lazily at first render so initializer
311
+ order cannot capture the stock default renderer instead of the application's.
312
+ Its bound is stated rather than hidden: the renderer is called without a view
313
+ context, exactly how a frozen snapshot must render, so Markdown that calls
314
+ Rails view helpers fails loudly at publish and needs the explicit lambda form.
315
+ - **Publishing rides `db:prepare`.** The deploy step everyone forgets no longer
316
+ exists: an enhanced `db:prepare` publishes declared documents, so by the time
317
+ the server takes traffic every declared version has an immutable snapshot.
318
+ The old failure mode was the worst kind of quiet — presentations refuse
319
+ unpublished documents (the safe failure), so a forgotten `clickwrap:publish`
320
+ became "nobody can sign up" some hours after a deploy that looked green. It
321
+ is idempotent, silent when no documents are declared, and a sentence rather
322
+ than a crash when the tables are not migrated yet; a real refusal fails the
323
+ deploy out loud, which beats signups failing quietly later. Opt out with
324
+ `config.publish_documents_after_database_preparation = false`.
325
+ - **`config.hotwire_native_document_links` answers the native question once.**
326
+ One declarative seam does both halves coherently — the signed href
327
+ (absolutized against a validated `https` canonical host, which may be a
328
+ callable) and the navigation attributes (`target="_blank"`,
329
+ `rel="noopener"`, `data-turbo="false"` for `:external_browser`; a plain link
330
+ for `:same_screen`). It exists for one specific failure: on a native
331
+ authentication sheet, a same-host document link is routed by the app itself,
332
+ which pops the sheet and takes the half-filled signup form with it. Getting
333
+ the href right in one place and the attributes right in another is exactly
334
+ the kind of split that drifts. When set it answers native renders entirely;
335
+ `config.document_link_html_options_with` goes on answering everything else,
336
+ so an app needing different answers per screen keeps the per-request lambda.
337
+ - **The default `describe_authentication_with` no longer over-claims.** It now
338
+ reports `{ method: :authenticated_session }` only when the controller's
339
+ configured current-actor method actually returns someone, and `{}` otherwise.
340
+ A signup form is not an authenticated session, and describing every request
341
+ as authenticated merely because it passed through `ApplicationController` put
342
+ a claim in the receipt that nobody had checked. It is deliberately gentler
343
+ than the capture path's actor resolution: describing authentication is
344
+ context, not identity, so a controller with no such method is `{}`, never an
345
+ error.
346
+
347
+ ### Added — installer improvements (driven by installing the gem into a template application)
348
+
349
+ - **Non-interactive runs skip the questions instead of jumbling them.** When
350
+ `rails generate clickwrap:install` runs without a terminal (a piped or
351
+ scripted run, CI, an AI agent), it now announces that it detected a
352
+ non-interactive run and takes the same collect-nothing defaults as
353
+ `--skip-questions`, rather than streaming interactive prompts into a pipe
354
+ that would have taken every `[y/N]` default anyway.
355
+ - **Existing legal pages become the document source.** When the host already
356
+ keeps both legal documents at a recognized convention — Sitepress-style
357
+ `app/content/pages/legal/terms.html.md` + `privacy.html.md`, or a previous
358
+ install's `app/content/legal/*.md` — the generated `config/clickwrap.rb`
359
+ points its `from:` lines at those exact files and no placeholders are
360
+ written. What people accept and what the public legal routes render must be
361
+ the same bytes, and a second copy is how they silently stop being the same
362
+ document. Placeholders are still written when no convention matches, and a
363
+ convention only counts when both of its documents exist.
364
+
365
+ ### Added — the hardening pass (driven by an independent adversarial audit of the first production integration)
366
+
367
+ - **Policy-declared tenant semantics: `tenant_is :not_applicable | :optional | :required`.**
368
+ Presentation, capture, verification, and import all resolve the tenant
369
+ through the policy's declaration, and the resolved tenant is signed into
370
+ the presentation manifest — so a personal policy can never inherit an
371
+ ambient organization from the session, and an organization member can no
372
+ longer be dead-ended by a `presentation_tenant_mismatch` between a
373
+ tenant-less render and a tenant-injecting submit. Withdrawal matches each
374
+ grant under its own policy's semantics, so consent granted personally stays
375
+ withdrawable after the person joins an organization; exemptions record the
376
+ canonical tenant reference. Declare `tenant_is :not_applicable` explicitly
377
+ on personal policies — it is the difference between "this evidence never
378
+ changes identity" and "whatever organization happened to be current."
379
+ - **Import chronology and revision honesty.** An imported historical grant
380
+ can never replace a newer live state (effective-time-first comparison, with
381
+ server recording order as tie-breaker only); imported evidence carries an
382
+ explicit legacy-import revision identity that can never satisfy
383
+ `require_current_revision: true`; and **`counts_as_current: false`**
384
+ quarantines an import — it satisfies no predicate, survives projection
385
+ REBUILDS quarantined, and exists for exactly the case where counsel has not
386
+ yet blessed a legacy source (a bundled checkbox, an unverified column).
387
+ Import idempotency covers statement mappings and provenance.
388
+ - **Serialized evidence writes.** Every state-mutating path (capture, import,
389
+ exemption, lifecycle transitions, provider outcomes, projection rebuild)
390
+ takes a per-actor identity lock first and the chain head second, in one
391
+ documented order, so concurrent writers block instead of deadlocking and
392
+ lost-update races on statement states are gone. The concurrency lane runs
393
+ on PostgreSQL and MySQL — SQLite cannot express row locks.
394
+ - **`recorded_after?` is off ULIDs.** Ordering questions are answered by a
395
+ durable database sequence under a unique chain position; ULID comparison —
396
+ process-local monotonicity — is no longer presented as an ordering
397
+ guarantee anywhere. The sequence is the installation's PRIVATE order: it is
398
+ deliberately absent from canonical bodies and receipts, because publishing
399
+ a global counter would hand every receipt holder an enumerable census of
400
+ installation activity.
401
+ - **Atomic protected outcomes.** `record_protected_outcome_with:` on a
402
+ one-time authorization records the exact result of the protected action —
403
+ built via `Clickwrap.protected_outcome`, digest-covered, committed in the
404
+ same transaction, refused on recorder-version drift — so "this evidence
405
+ authorized this exact operation" names the operation's amount, record, and
406
+ state instead of implying them.
407
+ - **Immutable document navigation.** The signed manifest binds each
408
+ statement's immutable document path; choice/radio statements render their
409
+ document links (previously only checkboxes did); and
410
+ `config.document_link_html_options_with` lets hosts choose HOW links open
411
+ (evaluated in the rendering view, so `hotwire_native_app?` works) while the
412
+ href itself is refused under any capitalization. The "opens in a new tab"
413
+ hint renders only when the link actually does.
414
+ - **Engine route authorization.** Receipts and withdrawal routes require a
415
+ present actor before the host callback runs, and the generated
416
+ authorization example now guards `nil == nil` explicitly. Remediation
417
+ tokens CARRY their signed tenant into presentation and capture instead of
418
+ comparing it against an ambient value the engine's routes cannot have.
419
+ - **Evidence-contract model links.** `has_clickwrap_evidence` takes the full
420
+ contract (`policy:`, `statement:`, `actor:`, `subject:`, optional
421
+ `tenant:`/`represented_party:`/`required_for_new_records:`), validates a
422
+ linked event against it, refuses link replacement, and supports model-first
423
+ deployments (inert until the column exists; strict from then on).
424
+ - **Submission hardening.** Forged envelope fields raise instead of being
425
+ dropped; answers are bounded at `Submission::MAX_ANSWER_LENGTH` characters
426
+ and refused — never truncated — beyond it (an answer is a checkbox state or
427
+ a declared choice name, not free text).
428
+ - Atomic represented-party creation (`create_represented_party_with_clickwrap`
429
+ and `including_when_this_action_creates_the_organization:`), atomic local
430
+ projections for external actions, request-aware geolocation provenance, and
431
+ pending-receipt answer readers (`answer_for`, `answered?`, `granted?`,
432
+ `declined?`) that read the validated event being committed instead of
433
+ re-parsing browser params.
434
+
435
+ Changes driven by the first production host application:
436
+
437
+ ### Changed
438
+
439
+ - **Imported legacy evidence now satisfies the everyday predicates.**
440
+ `Clickwrap.import_legacy!` projects into current state exactly as a capture
441
+ does, so `agreed_to?`, `acknowledged?`, and `current_for?` keep answering
442
+ what the source system answered — a migration no longer implies mass forced
443
+ re-acceptance. Provenance is unchanged (`imported_legacy` event type,
444
+ `imported_provider` attribution, unknowns named in the receipt), and
445
+ `require_current_version: true` still re-prompts when documents move on.
446
+ 0.1.0 was never published, so no installed application observes a behavior
447
+ change.
448
+ - The Devise adapter refuses a submission with a missing/stale presentation or
449
+ a declined required statement on the re-rendered form, with the gem's
450
+ localized user-facing sentences (inline beside the control and on `:base`) —
451
+ never a raw exception or a developer-facing message.
452
+ - The framework-integration modules (`FormBuilderExtensions`,
453
+ `ControllerHelpers`, `Registration`, `DocumentRenderers::Markdown`) are
454
+ required at boot instead of autoloaded, so hosts whose other gems load
455
+ Action View/Action Controller first no longer fail with an uninitialized
456
+ constant.
457
+ - The install migration recognizes the PostGIS adapter as PostgreSQL (jsonb)
458
+ and Trilogy as MySQL; the generated initializer selects the `:safe_text`
459
+ renderer explicitly instead of writing `nil` (which disabled rendering).
460
+
461
+ ### Added
462
+
463
+ - **Public forms that find or create their record:**
464
+ `register!(..., actor_may_already_exist: true)`. The lead-capture /
465
+ newsletter shape — an anonymous visitor submits a public form and the host
466
+ resolves the row by typed email — is one `register_with_clickwrap` call for
467
+ both cases now. When the submission created the row, attribution stays
468
+ `account_registration`; when it matched an existing row, the receipt
469
+ records the new `public_form` attribution instead, because no account was
470
+ created by the act. Without the explicit option, a persisted prospective
471
+ actor is still refused (that is usually a bug — the host meant `capture!`
472
+ with `actor:`), and the refusal teaches the option. (Forced by migrating a
473
+ production lead-magnet funnel whose leads upsert by email.)
474
+ - **`Clickwrap::CaptureRefused` with `#user_facing_message`.** Every refusal a
475
+ person can cause from a form — a stale or missing presentation, an
476
+ unparseable submission, a declined required statement — now shares one
477
+ exception superclass carrying a localized sentence fit to show them, so a
478
+ host controller handles the whole family in one rescue:
479
+ `rescue Clickwrap::CaptureRefused => refusal; redirect_to ..., alert:
480
+ refusal.user_facing_message`. Infrastructure failures stay outside the
481
+ family and stay loud. (Extracted from a real host that had grown five
482
+ hand-written rescue sites for the same distinctions.)
483
+ - **`has_clickwrap_evidence` + `bin/rails generate clickwrap:link TABLE`.**
484
+ One macro and one generator for the row-level link between a domain record
485
+ and the capture that authorized it: the generator writes the
486
+ `clickwrap_event_id` column migration (ULID string, indexed, nullable, no
487
+ foreign key — each deliberate, and the migration says why), and the macro
488
+ gives the model `clickwrap_event` and `clickwrap_receipt`, so
489
+ `withdrawal.clickwrap_receipt.verify` is one line years later.
490
+ - **`Result#recorded_after?(other)`** on verification results — enforce
491
+ evidence ordering ("the declaration must postdate the preparation") without
492
+ hosts comparing event ids by hand; accepts another result or a bare event
493
+ id and is false whenever either side is missing.
494
+ - The installer's post-install checklist now includes the test-suite setup:
495
+ include `Clickwrap::TestHelpers`, publish once per parallel worker and once
496
+ per process, and read submissions off rendered pages with
497
+ `clickwrap_params_from` — presentation tokens are signed and session-bound,
498
+ so tests cannot fabricate them by hand (that is the point).
499
+ - **[Integrating guide](guides/integrating.md)** — the battle-tested playbook
500
+ from migrating a production application onto the gem end to end: install
501
+ order, pointing documents at real legal content, test setup, Devise
502
+ dual-write bridges, custom surfaces, money-path protection, legacy import,
503
+ request-evidence enablement, and the dual-write → dual-belt → retire
504
+ rollout doctrine.
505
+ - **View helpers for custom surfaces** (`Clickwrap::ViewHelpers`, available in
506
+ every view): `clickwrap_presentation_token_field`,
507
+ `clickwrap_statement_check_box`, `clickwrap_statement_radio_button`, and
508
+ `clickwrap_submit_button` own the three contracts a hand-written form gets
509
+ wrong silently — the envelope name, the statement control names/ids, and a
510
+ call to action worded by the signed manifest itself. The host owns every
511
+ class and wrapper around them. (Extracted from the first production host's money-path
512
+ migration, where each custom form repeated all three by hand.)
513
+ - **`Clickwrap.verify(..., require_current_revision: true)`** — opt-in
514
+ revision currency: evidence recorded under a superseded policy revision
515
+ fails with `:stale_policy_revision` (the verify-time counterpart of the
516
+ capture-time symbol), so "legal reworded the statement → re-ask" is one
517
+ keyword instead of a hand-rolled revision comparison at the host's service
518
+ boundary.
519
+ - **Predicates on verification results**, one per stable error symbol and
520
+ generated from the vocabulary so they can never drift:
521
+ `result.no_evidence?`, `result.subject_fingerprint_mismatch?`,
522
+ `result.stale_policy_revision?`, …
523
+ - `clickwrap_params_from(path, answers: {})` in TestHelpers — the one-line
524
+ integration-test pattern: GET the page, read the signed token and controls
525
+ back off it, return the POST params.
526
+ - The unknown-document boot error now mentions `document: nil` for statements
527
+ about operational facts with no published document.
528
+ - `config.document_renderer = :markdown` — real HTML through whichever
529
+ Markdown library the host already bundles (commonmarker, redcarpet, or
530
+ kramdown; no new dependency), with leading YAML front matter stripped from
531
+ the rendered representation only, the engine name and version recorded in
532
+ the receipt, and the same safe-list sanitizer as the reference renderer.
533
+ - Spanish locale (`config/locales/es.yml`).
534
+ - `clickwrap_submission_params_from(response)` test helper: host integration
535
+ tests read the signed presentation token and its controls back off the
536
+ rendered page, the way a browser does.
537
+
538
+ ## [0.1.0] - 2026-08-15
539
+
540
+ First implemented release. `clickwrap` turns terms acceptance, privacy notice
541
+ acknowledgment, consent, factual declarations, operator attestations, and
542
+ one-time authorizations into one Rails primitive: immutable versioned
543
+ documents, server-owned policies, signed presentation manifests, append-oriented
544
+ evidence events with fixed named disposition transitions, and canonical receipts that can be checked without the
545
+ application that wrote them. Required evidence and the protected database
546
+ action commit in the same transaction, so an account, payout, or handoff cannot
547
+ succeed without the evidence that authorized it. Request evidence — IP address,
548
+ browser user-agent, IP geolocation — stays off until a policy names the field,
549
+ its purpose, and its retention. The gem provides evidence mechanics only: your
550
+ application and its counsel still own the legal text, lawful basis, substantive
551
+ validity, capacity, authority, and retention periods.
552
+
553
+ ### Added
554
+
555
+ - **Immutable versioned documents.** `Clickwrap.document :terms, version:, from:`
556
+ points at the files your application already owns; `bin/rails clickwrap:publish`
557
+ freezes each version into a snapshot with a versioned SHA-256 digest of the
558
+ exact bytes, and `clickwrap:publish:plan` previews what a boot would publish.
559
+ A published version is never rewritten — editing the source file is a new
560
+ version, and receipts keep resolving the bytes they were captured against.
561
+ - **Server-owned compiled policies with six honest kinds.** `Clickwrap.policy`
562
+ and its verbal DSL — `agree_to` (agreement), `acknowledge` (acknowledgment),
563
+ `consent_to` (consent), `declare` (declaration), `attest` (attestation),
564
+ `authorize` (authorization) — give each act the lifecycle it actually needs
565
+ instead of calling every checkbox "consent." The policy compiler runs at boot
566
+ and refuses incoherent combinations in a full sentence: an indefinite one-time
567
+ authorization, consent without a withdrawal path, an expiring declaration that
568
+ would have to pretend the original statement was false. The taxonomy is
569
+ product design, not statutory vocabulary; the host picks the kind.
570
+ - **Signed presentation manifests.** Every rendered policy carries a signed,
571
+ short-lived manifest of exactly what the server generated and offered: policy key and
572
+ revision, document versions and digests, assertion and link text, choices,
573
+ submit-button text, and locale. Submission is validated against that manifest
574
+ and rechecked server-side, so render-to-submit substitution — a different
575
+ version, a different call to action, a checkbox the server never required —
576
+ is rejected rather than recorded. The browser may answer a policy; it can
577
+ never choose the policy, the version, the validity window, the subject, the
578
+ retention class, or a request-evidence field.
579
+ - **`capture!`, `capture_and!`, and `register!` with same-transaction atomicity.**
580
+ `capture_and!` yields a read-only `Clickwrap::PendingReceipt` whose stable
581
+ `event_id` the domain row can store, and commits the evidence and the
582
+ protected action together or not at all; if the transaction rolls back the
583
+ pending object becomes invalid instead of masquerading as committed evidence.
584
+ `capture!` records evidence on its own, and `register!` binds a prospective
585
+ actor to the presentation that preceded the account — the Rails-authentication
586
+ and Devise adapters are thin conveniences over it. Optional
587
+ `after_event_is_committed` hooks are error-isolated and can never undo a
588
+ committed action or stand in for one.
589
+ - **Lifecycles that stay truthful over time.** Consent can actually be
590
+ withdrawn, renewed, and scope-changed; declarations expire, get corrected, and
591
+ get superseded without rewriting what was originally stated; one-time
592
+ authorizations are locked and consumed inside the same transaction as the
593
+ action they authorize, so a stale token, a changed subject, a wrong ordering,
594
+ or a concurrent replay cannot reuse one. Every ordinary lifecycle transition
595
+ appends an event with its own predecessor link; reviewed retention uses the
596
+ separately named, fixed disposition transition rather than masquerading as an
597
+ ordinary append.
598
+ - **Canonical receipts and a standalone verifier.** Receipts use versioned
599
+ schemas serialized with the [JSON Canonicalization Scheme (RFC 8785)](https://www.rfc-editor.org/rfc/rfc8785)
600
+ plus a published Clickwrap profile for UTC timestamps, decimals, identifiers,
601
+ binary digests, absent values, and extension names — never Ruby object
602
+ serialization, YAML, or database column order. `bin/rails clickwrap:verify`
603
+ and `clickwrap:export` produce and check bundles without the host
604
+ application's source code, and golden fixtures pin every released format so
605
+ new versions keep verifying old receipts. An unknown schema version fails
606
+ honestly instead of being reinterpreted. The baseline tier verifies schema,
607
+ canonical bytes, digests, links, and bundled content consistency, and says so
608
+ precisely; it claims nothing about origin or time that it cannot show.
609
+ - **Optional request evidence, off by default and encrypted.** IP address,
610
+ browser user-agent, and each individual IP-geolocation field are collected
611
+ only when a policy names them with a plain-English purpose and a retention
612
+ decision. They live in a separate `ActiveRecord::Encryption` annex, are read
613
+ through their own authorization callback, and are separately disposable
614
+ without touching the core event. There is no `gdpr_compliant_mode`,
615
+ `full_evidence`, or `legal_proof` switch that turns on a category of personal
616
+ data as a side effect of something else — the installer's recipes write every
617
+ individual setting into the initializer and then disappear.
618
+ - **Retention classes, legal holds, and dry-run disposition.**
619
+ `Clickwrap.retention` expresses per-field retention (including event-based and
620
+ "later of" rules) as executable, auditable policy. `clickwrap:retention:plan`
621
+ produces an immutable, scoped, expiring plan that `clickwrap:retention:apply`
622
+ rechecks before touching anything, so a newly placed hold or a changed policy
623
+ stops disposition instead of deleting more than the operator reviewed.
624
+ `place_on_legal_hold!` / `release_legal_hold!` require a reason, an owner, and
625
+ a review date; placing and releasing a hold append corresponding evidence
626
+ events while the hold row remains an explicit current-state record. Destructive methods
627
+ name exactly what they remove (`delete_recorded_ip_address!`,
628
+ `delete_recorded_browser_user_agent!`, `delete_recorded_ip_geolocation!`) and
629
+ append a disposition event rather than rewriting history. Clickwrap does not
630
+ decide retention periods; it makes reviewed ones executable.
631
+ - **Generators for every step.** `clickwrap:install` detects integer versus UUID
632
+ keys, the database adapter, and Rails authentication versus Devise; it stops
633
+ and explains itself when the actor or tenant mapping is ambiguous, asks
634
+ separately about every request-evidence field, and prints a post-install
635
+ checklist. `clickwrap:policy`, `clickwrap:document`, `clickwrap:views`,
636
+ `clickwrap:hardening --database`, and `clickwrap:upgrade` cover the rest.
637
+ Upgrade generators always create new migrations; a released migration is never
638
+ silently edited underneath an installed application.
639
+ - **Importers that do not invent history.** `clickwrap:import:fine_print:plan` /
640
+ `clickwrap:import:fine_print` turn FinePrint contract versions and signatures
641
+ into explicit `imported_legacy` events, and `Clickwrap.import_legacy!` does the
642
+ same for an `accepted_terms_at` column. Fields the legacy source never
643
+ recorded — presentation manifest, IP address, call to action, protected
644
+ action — stay `unknown` or `not_collected`. Clickwrap never synthesizes
645
+ evidence it does not have.
646
+ - **A form-builder helper and ejectable reference views.**
647
+ `form.clickwrap :signup, submit: "Create account"` renders the initially
648
+ unselected controls and the bound submit button as one presentation, so the
649
+ call to action in the signed manifest is the one the user can actually press.
650
+ `rails generate clickwrap:views` ejects the reference views — including the
651
+ standalone remediation screen — for hosts that want their own markup.
652
+
3
653
  ## [0.0.0]
4
654
 
5
655
  - Name-reservation release. No implementation: no engine, no models, no