Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
67ffb32
Implement the evidence-and-assent engine
rameerez Aug 15, 2026
3bcf626
Add the web layer, generators, ops tooling, and the test suite
rameerez Aug 15, 2026
c166f6c
Cover the gem to 88% lines and 63% branches, and fix what that surfaced
rameerez Aug 15, 2026
62489da
Close the gaps between what the README promises and what the code does
rameerez Aug 15, 2026
405dd86
Make the MySQL lane real, and isolate the test that needs DDL
rameerez Aug 15, 2026
f5e7804
Tell "already withdrawn" apart from "never granted"
rameerez Aug 15, 2026
7578700
Fix three bugs a real host application found that the dummy could not
rameerez Aug 15, 2026
607a02e
Prepare Clickwrap for production release
rameerez Aug 15, 2026
f4783e5
Survive the first real host: what dogfooding CarHey forced
rameerez Aug 16, 2026
3b5af12
Add the custom-surface ergonomics the money-path migration asked for
rameerez Aug 16, 2026
07468c8
One rescue for human refusals, one line to link a row to its evidence
rameerez Aug 16, 2026
4b3331b
Let public forms find or create their record, attributed honestly
rameerez Aug 16, 2026
62ceaee
Harden clickwrap evidence and integration contracts
rameerez Aug 16, 2026
b186df5
Keep recording order numeric in UUID applications
rameerez Aug 16, 2026
6fb0579
Preserve historical receipts when adding recording order
rameerez Aug 16, 2026
242eeea
Let hosts customize immutable document navigation
rameerez Aug 16, 2026
040f7af
Expose immutable document navigation in host forms
rameerez Aug 16, 2026
bb0610b
Select exact Clickwrap forms in integration tests
rameerez Aug 16, 2026
2fffb47
Submit rendered Clickwrap choice values in tests
rameerez Aug 16, 2026
7c8e654
Preserve idempotency after subject state changes
rameerez Aug 16, 2026
cd99ed4
Support model-first evidence link deployments
rameerez Aug 16, 2026
b8f1886
Support atomic local projections for external actions
rameerez Aug 16, 2026
a9b4c37
Support atomic represented-party creation
rameerez Aug 16, 2026
bbb64c1
Let creation services return represented parties
rameerez Aug 16, 2026
bb03536
Reject forged submission envelope fields
rameerez Aug 16, 2026
d870c6f
Preserve request-aware geolocation provenance
rameerez Aug 16, 2026
c98a38a
Load geolocation adapters before host initializers
rameerez Aug 16, 2026
c1a76f5
Let protected work read accepted answers off the pending receipt
rameerez Aug 16, 2026
b032c4f
Defuse the absolute-date time bomb in the recording-order test
rameerez Aug 16, 2026
7d00eda
Evaluate the document-navigation hook in the rendering view
rameerez Aug 16, 2026
061778a
Make the whole CI matrix honest: fix its two first-run discoveries
rameerez Aug 16, 2026
2ef90db
Carry every hardening mechanism to every path (adversarial review rou…
rameerez Aug 16, 2026
00fae74
Close the engine authority hole, ungate the review-date check, allow …
rameerez Aug 16, 2026
86c61d2
Let a renderer's declared sanitation decision survive publishing
rameerez Aug 16, 2026
e6d9e1a
Remove the Claude review workflow
rameerez Aug 16, 2026
d95b04c
Teach the installer about pipes and about legal pages the app already…
rameerez Aug 16, 2026
763f521
Keep the review checklist honest when no placeholders were written
rameerez Aug 16, 2026
c14c753
Let the concurrency test do what the deadlock error tells hosts to do
rameerez Aug 16, 2026
4496c9e
The dream API: five ceremonies become gem behavior
rameerez Aug 17, 2026
912e416
The installer, the generators, and every public doc teach the dream API
rameerez Aug 17, 2026
82a48ab
Teach the auth door in the two places a new host actually reads
rameerez Aug 17, 2026
13bc463
Refuse to sign a document link that resolves to nothing
rameerez Aug 17, 2026
d88042f
Teach the install that actually installs this gem
rameerez Aug 17, 2026
65fa103
Make both ends of `verify` answer the same question
rameerez Aug 17, 2026
1dc7843
Let `reset!` clear the presentation verifier too
rameerez Aug 17, 2026
c6f0b8b
Authorize the receipt list before paging it, and stop the per-row act…
rameerez Aug 17, 2026
d25589a
Resolve a presentation's documents once, in two queries
rameerez Aug 17, 2026
bc62a6e
Make `config.actor_class_name` mean what the installer says it means
rameerez Aug 17, 2026
2858150
Test the three lifecycle verbs nobody had ever called
rameerez Aug 17, 2026
e6c6be0
Delete two errors nothing raises, and pin two nothing tested
rameerez Aug 17, 2026
35168d3
Genericize the example domain out of every public surface
rameerez Aug 17, 2026
8c98e20
One name for the registering record, and real signatures for nine for…
rameerez Aug 17, 2026
6fec45d
Give the test factory its own verb: `submit_clickwrap`
rameerez Aug 17, 2026
6eeecd0
Let a host word its own submit button from the signed presentation
rameerez Aug 17, 2026
b0e53bd
Let `open_in:` answer per request, the way `canonical_host:` already can
rameerez Aug 17, 2026
a4fe438
Stop remembering three option names nobody ever wrote
rameerez Aug 17, 2026
286b93c
Emit only the tables an installation can put a row in
rameerez Aug 17, 2026
289b2c2
Let the generated initializer state decisions instead of defaults
rameerez Aug 17, 2026
6fcb19a
One geolocation flag instead of nine
rameerez Aug 17, 2026
80129df
Give the presentation linter a switch and a paragraph
rameerez Aug 17, 2026
7615d36
Declare the two gems this gem has always required at boot
rameerez Aug 17, 2026
b32368a
Lock the gate registry, like every other registry in this gem
rameerez Aug 17, 2026
3737ac1
Say what `recorded_after?` false means, and document FrontMatter at all
rameerez Aug 17, 2026
308766f
Run the README quickstart in CI, and fix what it found
rameerez Aug 17, 2026
b05fd67
Make the autoload test check every file, and fix what that found
rameerez Aug 17, 2026
694d4fe
Raise the coverage floors to where they actually mean something
rameerez Aug 17, 2026
70bd49b
Let a document say where a person actually reads it
rameerez Aug 17, 2026
9036689
Teach the presenter to say two ordinary things in one sentence
rameerez Aug 17, 2026
3404b63
Make the one-line offer the default a signup renders
rameerez Aug 17, 2026
7d2acec
Describe the shape the gem actually renders now
rameerez Aug 17, 2026
64ff82f
Raise the coverage floors to where they mean something again
rameerez Aug 17, 2026
a3c5104
Render one control one way, whether it stands for one act or three
rameerez Aug 17, 2026
45ed570
Put the sentence somebody read into the receipt
rameerez Aug 17, 2026
692f546
Say how to measure the coverage the floors are set against
rameerez Aug 17, 2026
ab270c2
The checkbox sits with the sentence, and the links read as part of it
rameerez Aug 17, 2026
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
41 changes: 41 additions & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Claude Code

on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]

jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
181 changes: 181 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
name: Tests

on:
pull_request:
push:
branches:
- main

jobs:
# Static gates: style, autoload health, and a buildable gem — fast,
# independent of the database matrix.
lint:
runs-on: ubuntu-latest

env:
RAILS_ENV: test

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true

- name: RuboCop
run: bundle exec rubocop

- name: Prepare dummy database
run: bundle exec rake db:create db:migrate

- name: Zeitwerk eager-load check
working-directory: test/dummy
run: bundle exec rails zeitwerk:check

- name: Gem builds
run: gem build clickwrap.gemspec

# Main test suite - tests Ruby versions and Rails compatibility with SQLite
sqlite:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
ruby_version: ["3.2", "3.3", "3.4", "4.0"]
gemfile:
- Gemfile
- gemfiles/rails_7.1.gemfile
- gemfiles/rails_7.2.gemfile
- gemfiles/rails_8.0.gemfile
- gemfiles/rails_8.1.gemfile

env:
RAILS_ENV: test
BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby ${{ matrix.ruby_version }}
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby_version }}
bundler-cache: true

- name: Prepare database and run tests
# Exercise the real migration path in SQLite too so dummy/test schema
# drift is caught in the default matrix, not only adapter-specific jobs.
#
# Use `db:create db:migrate` rather than `db:migrate:reset`: the reset macro
# runs db:drop + db:create + db:migrate IN ONE PROCESS, and on SQLite the
# db:migrate step then writes through a stale connection to the just-dropped
# file, so nothing persists and the suite boots into "pending migrations"
# (PG/MySQL survive it because the DB server reconnects). CI runners start
# fresh, so no drop is needed.
run: bundle exec rake db:create db:migrate test

- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-results-sqlite-ruby-${{ matrix.ruby_version }}-${{ strategy.job-index }}
path: test/reports/
retention-days: 7

# PostgreSQL compatibility tests
postgres:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
ruby_version: ["3.4"]

services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: clickwrap_test
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=10s
--health-timeout=5s
--health-retries=5

env:
RAILS_ENV: test
DATABASE_URL: postgres://postgres:postgres@localhost:5432/clickwrap_test

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby ${{ matrix.ruby_version }}
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby_version }}
bundler-cache: true

- name: Prepare database
# Use db:migrate:reset instead of db:test:prepare to avoid loading schema.rb
# which has SQLite-specific defaults that fail on PostgreSQL.
# db:migrate:reset does: db:drop, db:create, db:migrate (using migrations, not schema.rb)
run: bundle exec rake db:migrate:reset

- name: Run tests
run: bundle exec rake test

# MySQL compatibility tests
mysql:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
ruby_version: ["3.4"]

services:
mysql:
image: mysql:8.4
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: clickwrap_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h localhost -uroot -proot"
--health-interval=10s
--health-timeout=5s
--health-retries=5

env:
RAILS_ENV: test
DATABASE_URL: mysql2://root:root@127.0.0.1:3306/clickwrap_test

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby ${{ matrix.ruby_version }}
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby_version }}
bundler-cache: true

- name: Prepare database
# Use db:migrate:reset instead of db:test:prepare to avoid loading schema.rb
# which has SQLite-specific defaults that fail on MySQL (JSON column defaults).
# db:migrate:reset does: db:drop, db:create, db:migrate (using migrations, not schema.rb)
run: bundle exec rake db:migrate:reset

- name: Run tests
run: bundle exec rake test
34 changes: 34 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,36 @@
/.bundle/
/.yardoc/
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
/dist/
/log/

# For gems, ignore all lockfiles (root and appraisal) to allow flexible dependency resolution
# But commit the Appraisal Gemfiles themselves (not the .lock files)
Gemfile.lock
gemfiles/*.lock

test/dummy/db/*.sqlite3*
# A `db:test:prepare` roundtrip can leave an empty schema.rb here, and while it
# exists `maintain_test_schema!` loads it over the real one and the whole suite
# dies with "Migrations are pending". The migrations are the source of truth.
test/dummy/db/schema.rb
test/dummy/log/*.log*
test/dummy/tmp/
test/dummy/storage/
test/dummy/.bundle/

.DS_Store
.ruby-lsp/

TODO
.cursor/

# Owner-directed: the product research corpus stays out of the published
# repository. See docs/strategy/03-readiness-market-and-next-steps.md.
/docs/
/*.gem
90 changes: 90 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# MERGE our excludes with rubocop's defaults (vendor/, node_modules/, …) —
# a bare Exclude key would REPLACE them.
inherit_mode:
merge:
- Exclude

AllCops:
TargetRubyVersion: 3.2
NewCops: enable
SuggestExtensions: false
Exclude:
# The dummy host app mirrors what a real application looks like, including
# migrations copied verbatim from the install generator's templates —
# don't lint a generated-code mirror against the gem's own style.
- test/dummy/**/*
# Generator templates are emitted into HOST apps (host style, and some
# carry ERB tags rubocop can't parse as Ruby).
- lib/generators/clickwrap/templates/**/*
# Appraisal-generated Gemfiles.
- gemfiles/*

Style/StringLiterals:
EnforcedStyle: double_quotes

Style/StringLiteralsInInterpolation:
EnforcedStyle: double_quotes

# Hard size metrics fight readable, well-commented domain code and thorough
# test classes; we optimize for the latter. A policy compiler that explains
# every refusal in a full sentence is longer than one that raises "invalid",
# and the long version is the product.
Metrics:
Enabled: false

# Evidence code carries short, standard names for the things it records.
Naming/MethodParameterName:
AllowedNames: [at, by, id, ip, to, ua, key]

# `has_clickwraps` is the macro and the product — it matches the ecosystem
# grammar (has_sessions, has_credits, has_api_keys), not a predicate.
Naming/PredicatePrefix:
AllowedMethods: [has_clickwraps, has_clickwrap_evidence]

# Receipt and verification predicates read as questions about a recorded fact:
# `recorded_ip_address?`, `ip_geolocation_was_estimated?`,
# `browser_user_agent_was_client_supplied?`. Renaming them to satisfy a
# prefix cop would break the plain-English naming contract these methods exist
# to honor.
Naming/PredicateMethod:
Enabled: false

# %{policy} / %{document} are I18n interpolation tokens — annotated tokens
# aren't a thing in I18n templates.
Style/FormatStringToken:
EnforcedStyle: template

# Adapter contracts (IP geolocation resolvers, anchors, timestamp providers)
# fix keyword names even where a no-op implementation ignores them. The names
# ARE the documented interface.
Lint/UnusedMethodArgument:
AllowUnusedKeywordArguments: true

Lint/UnusedBlockArgument:
AllowUnusedKeywordArguments: true

# The codebase consistently names a rescued exception `error` — it reads as a
# noun in the sentences these rescue blocks are written as.
Naming/RescuedExceptionsVariableName:
PreferredName: error

# Configuration's readers are grouped by section, matching the order and shape
# of the generated initializer — which is the main thing a host ever reads
# about this gem. Collapsing them into one declaration produces a single
# thousand-character line and loses that structure entirely.
Style/AccessorGrouping:
Exclude:
- lib/clickwrap/configuration.rb

# `->(http_request) { http_request.remote_ip }` keeps the parameter name, and
# the parameter name is the point: the naming contract says `http_request`, not
# a bare `&:remote_ip` whose receiver is anybody's guess when a host overrides
# the reader.
Style/SymbolProc:
Exclude:
- lib/clickwrap/configuration.rb

Layout/LineLength:
Max: 120
Exclude:
- clickwrap.gemspec # the long-form rubygems description is one line by design
Loading
Loading