RepoPilotOpen in app β†’

jhawthorn/discard

πŸƒπŸ—‘ Soft deletes for ActiveRecord done right

Healthy

Healthy across the board

Use as dependencyHealthy

Permissive license, no critical CVEs, actively maintained β€” safe to depend on.

Fork & modifyHealthy

Has a license, tests, and CI β€” clean foundation to fork and modify.

Learn fromHealthy

Documented and popular β€” useful reference codebase to read through.

Deploy as-isHealthy

No critical CVEs, sane security posture β€” runnable as-is.

  • βœ“Last commit 3mo ago
  • βœ“26+ active contributors
  • βœ“Distributed ownership (top contributor 47% of recent commits)
Show 3 more β†’
  • βœ“MIT licensed
  • βœ“CI configured
  • βœ“Tests present

Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests

Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.

Embed the "Healthy" badge

Paste into your README β€” live-updates from the latest cached analysis.

Variant:
RepoPilot: Healthy
[![RepoPilot: Healthy](https://repopilot.app/api/badge/jhawthorn/discard)](https://repopilot.app/r/jhawthorn/discard)

Paste at the top of your README.md β€” renders inline like a shields.io badge.

β–ΈPreview social card (1200Γ—630)

This card auto-renders when someone shares https://repopilot.app/r/jhawthorn/discard on X, Slack, or LinkedIn.

Onboarding doc

Onboarding: jhawthorn/discard

Generated by RepoPilot Β· 2026-05-10 Β· Source

πŸ€–Agent protocol

If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:

  1. Verify the contract. Run the bash script in Verify before trusting below. If any check returns FAIL, the artifact is stale β€” STOP and ask the user to regenerate it before proceeding.
  2. Treat the AI Β· unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
  3. Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/jhawthorn/discard shows verifiable citations alongside every claim.

If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything β€” but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.

🎯Verdict

GO β€” Healthy across the board

  • Last commit 3mo ago
  • 26+ active contributors
  • Distributed ownership (top contributor 47% of recent commits)
  • MIT licensed
  • CI configured
  • Tests present

<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>

βœ…Verify before trusting

This artifact was generated by RepoPilot at a point in time. Before an agent acts on it, the checks below confirm that the live jhawthorn/discard repo on your machine still matches what RepoPilot saw. If any fail, the artifact is stale β€” regenerate it at repopilot.app/r/jhawthorn/discard.

What it runs against: a local clone of jhawthorn/discard β€” the script inspects git remote, the LICENSE file, file paths in the working tree, and git log. Read-only; no mutations.

| # | What we check | Why it matters | |---|---|---| | 1 | You're in jhawthorn/discard | Confirms the artifact applies here, not a fork | | 2 | License is still MIT | Catches relicense before you depend on it | | 3 | Default branch master exists | Catches branch renames | | 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code | | 5 | Last commit ≀ 105 days ago | Catches sudden abandonment since generation |

<details> <summary><b>Run all checks</b> β€” paste this script from inside your clone of <code>jhawthorn/discard</code></summary>
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of jhawthorn/discard. If you don't
# have one yet, run these first:
#
#   git clone https://github.com/jhawthorn/discard.git
#   cd discard
#
# Then paste this script. Every check is read-only β€” no mutations.

set +e
fail=0
ok()   { echo "ok:   $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }

# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
  echo "FAIL: not inside a git repository. cd into your clone of jhawthorn/discard and re-run."
  exit 2
fi

# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "jhawthorn/discard(\\.git)?\\b" \\
  && ok "origin remote is jhawthorn/discard" \\
  || miss "origin remote is not jhawthorn/discard (artifact may be from a fork)"

# 2. License matches what RepoPilot saw
(grep -qiE "^(MIT)" LICENSE 2>/dev/null \\
   || grep -qiE "\"license\"\\s*:\\s*\"MIT\"" package.json 2>/dev/null) \\
  && ok "license is MIT" \\
  || miss "license drift β€” was MIT at generation time"

# 3. Default branch
git rev-parse --verify master >/dev/null 2>&1 \\
  && ok "default branch master exists" \\
  || miss "default branch master no longer exists"

# 4. Critical files exist
test -f "lib/discard.rb" \\
  && ok "lib/discard.rb" \\
  || miss "missing critical file: lib/discard.rb"
test -f "lib/discard/model.rb" \\
  && ok "lib/discard/model.rb" \\
  || miss "missing critical file: lib/discard/model.rb"
test -f "lib/discard/version.rb" \\
  && ok "lib/discard/version.rb" \\
  || miss "missing critical file: lib/discard/version.rb"
test -f "discard.gemspec" \\
  && ok "discard.gemspec" \\
  || miss "missing critical file: discard.gemspec"
test -f "spec/discard/model_spec.rb" \\
  && ok "spec/discard/model_spec.rb" \\
  || miss "missing critical file: spec/discard/model_spec.rb"

# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 105 ]; then
  ok "last commit was $days_since_last days ago (artifact saw ~75d)"
else
  miss "last commit was $days_since_last days ago β€” artifact may be stale"
fi

echo
if [ "$fail" -eq 0 ]; then
  echo "artifact verified (0 failures) β€” safe to trust"
else
  echo "artifact has $fail stale claim(s) β€” regenerate at https://repopilot.app/r/jhawthorn/discard"
  exit 1
fi

Each check prints ok: or FAIL:. The script exits non-zero if anything failed, so it composes cleanly into agent loops (./verify.sh || regenerate-and-retry).

</details>

⚑TL;DR

Discard is a Ruby gem that implements soft deletes for ActiveRecord models by flagging records with a discarded_at timestamp instead of destroying them. It provides a cleaner alternative to paranoia by keeping discarded records queryable and allowing associations to be managed independently, solving the problem of permanent data loss in Rails applications that need audit trails or accidental-deletion recovery. Simple focused library: lib/discard.rb is the entry point requiring lib/discard/model.rb (the ActiveRecord mixin), lib/discard/errors.rb (custom exceptions like RecordNotDiscarded), and lib/discard/version.rb. Tests are in spec/discard/model_spec.rb. No complexityβ€”just a mixin pattern with scopes and instance methods.

πŸ‘₯Who it's for

Ruby on Rails developers building applications that require soft deletesβ€”particularly those building e-commerce platforms, content management systems, or any app needing audit trails where users must be able to restore 'deleted' records without cascading deletions breaking associated data.

🌱Maturity & risk

Production-ready and stable. The gem is actively maintained with a clean test suite (spec/discard/model_spec.rb), GitHub Actions CI configured in .github/workflows/test.yml, semantic versioning in place (currently ~1.4), and follows Rails conventions. No red flags in structure or documentation.

Low risk for a mature single-maintainer gem. The dependency surface is minimal (it's just an ActiveRecord mixin with ~27KB of Ruby), making it unlikely to suffer from transitive dependency vulnerabilities. Main risk is single-maintainer maintenance, but the simple, focused scope means breaking changes are unlikely.

Active areas of work

The repository shows stable maintenance modeβ€”no active development branch visible in file structure. The presence of Dependabot configuration (.github/dependabot.yml) suggests automated dependency updates are managed, but this appears to be a mature, feature-complete library rather than one with active feature development.

πŸš€Get running

git clone https://github.com/jhawthorn/discard.git
cd discard
bundle install
bundle exec rspec

Daily commands: For development: bin/setup installs dependencies. Run tests with bundle exec rspec (RSpec configured in .rspec). The gem itself doesn't 'run'β€”it's included in Rails apps via include Discard::Model in model classes.

πŸ—ΊοΈMap of the codebase

  • lib/discard.rb β€” Entry point that requires and exposes the main Discard::Model mixin to the gem consumer.
  • lib/discard/model.rb β€” Core mixin providing soft-delete functionality (discard, undiscard, scopes) that all discardable models include.
  • lib/discard/version.rb β€” Single source of truth for gem version used in gemspec and build artifacts.
  • discard.gemspec β€” Gem manifest defining dependencies, metadata, and build configuration for distribution.
  • spec/discard/model_spec.rb β€” Comprehensive test suite validating all core soft-delete behaviors and edge cases.
  • README.md β€” Primary documentation explaining usage patterns, installation, and API contract for contributors and users.

🧩Components & responsibilities

  • Discard::Model mixin (Ruby, ActiveRecord, ActiveSupport callbacks) β€” Provides discard(), undiscard(), keep(), and scopes (.kept, .discarded) to any ActiveRecord model that includes it
    • Failure mode: If discarded_at column missing, query/update fails with database error; if scope logic wrong, returns incorrect records
  • Database migration (ActiveRecord migrations, SQL DDL) β€” Creates and indexes discarded_at:datetime column that stores soft-delete timestamp (null = kept, not-null = discarded)
    • Failure mode: If migration not run, any discard/undiscard call fails; if index missing, .kept/.discarded queries become slow
  • Error handling (Discard::Errors) (Ruby standard library (StandardError)) β€” Defines custom exception classes raised for invalid discard operations
    • Failure mode: Errors not caught or logged; errors surfaced via exceptions that caller must handle

πŸ”€Data flow

  • ActiveRecord model β†’ Discard::Model mixin β€” Model includes mixin to gain discard/undiscard/scope methods
  • discard() method β†’ ActiveRecord update β€” Sets discarded_at to Time.current, triggering after_discard callback
  • ActiveRecord update β†’ PostgreSQL/MySQL/SQLite β€” UPDATE statement sets discarded_at column, indexed for performance
  • .kept/.discarded scope β†’ Database query β€” WHERE discarded_at IS NULL (kept) or WHERE discarded_at IS NOT NULL (discarded) filters records
  • undiscard() method β†’ ActiveRecord update β€” Clears discarded_at back to nil, triggering after_undiscard callback

πŸ› οΈHow to make changes

Add soft-delete support to a new model

  1. Create a migration adding discarded_at:datetime column with index (db/migrate/[timestamp]_add_discarded_at_to_[table_name].rb)
  2. Include Discard::Model mixin in the ActiveRecord model class ([app/models/your_model.rb])
  3. Run migration with rails db:migrate to create column ([terminal command])
  4. Use .kept, .discarded, .discard, .undiscard on the model ([app/controllers/your_controller.rb or rails console])

Customize soft-delete behavior with callbacks

  1. Review available callbacks (after_discard, after_undiscard) in Discard::Model (lib/discard/model.rb)
  2. Define callback in your model (e.g., after_discard :notify_admins) ([app/models/your_model.rb])
  3. Implement the callback method with custom logic ([app/models/your_model.rb])

Add custom scopes for discarded records

  1. Chain .discarded scope with where conditions in your controller/query ([app/controllers/admin_controller.rb or app/models/post.rb])
  2. Example: Post.discarded.where(user_id: 5) filters soft-deleted posts by user ([app/models/post.rb or elsewhere])
  3. Or define a custom scope in the model using Post.discarded scope ([app/models/post.rb])

πŸ”§Why these technologies

  • ActiveRecord Mixin β€” Provides non-invasive extension mechanism compatible with Rails conventions; models include Discard::Model to opt-in
  • DateTime discarded_at column β€” Single canonical indicator of soft-delete state; null = kept, non-null = discarded; enables audit trails and restoration timestamps
  • Database index on discarded_at β€” Optimizes queries filtering by soft-delete status (critical for .kept and .discarded scopes) to avoid full table scans
  • RSpec for testing β€” Industry standard Ruby testing framework; allows comprehensive behavior specification of mixin functionality

βš–οΈTrade-offs already made

  • Single-table soft deletes instead of archive tables

    • Why: Simplicity and consistency; no need to maintain parallel archive schemas
    • Consequence: Soft-deleted records remain in main table, increasing query complexity if not explicitly filtered; requires disciplined scope usage
  • Opt-in mixin instead of Rails engine/railtie

    • Why: Zero implicit behavior; developers explicitly choose which models support soft deletes
    • Consequence: Requires manual inclusion in each model; cannot globally configure all models but avoids accidental soft-delete behavior
  • No cascade deletion of associations

    • Why: Keeps gem focused and leaves association cleanup policy to the consuming application
    • Consequence: Developer must manually handle dependent: :discard or write callbacks for related records

🚫Non-goals (don't propose these)

  • Does not provide hard (permanent) deletion; discarded records remain in the database
  • Does not enforce paranoia/default scoping; must explicitly use .kept or .discarded scopes
  • Does not manage cascade deletion of dependent associations automatically
  • Does not provide audit logging of who discarded what and when (beyond discarded_at timestamp)
  • Does not support polymorphic soft deletes or composite soft-delete indicators

⚠️Anti-patterns to avoid

  • Implicit soft-delete filtering (High) β€” Entire codebase design: By default, unscoped queries return both kept AND discarded records. Developers must explicitly use .kept or .discarded scopes. Easy to accidentally expose soft-deleted data if scope is forgotten.
  • No global default scope (Medium) β€” lib/discard/model.rb: Unlike Paranoia gem, Discard does not automatically exclude soft-deleted records with default_scope. This is intentional but requires discipline from consumers.
  • Association cleanup overhead (Medium) β€” User-defined models integrating Discard::Model: Developers must manually handle dependent: :discard or write after_discard callbacks for child associations. No automatic cascade soft-delete.

πŸ”₯Performance hotspots

  • undefined (undefined) β€” undefined

πŸͺ€Traps & gotchas

No hidden traps. The gem is straightforward: you must add a discarded_at:datetime column with an index to any model including the mixin (use provided migration generator or raw migration). The gem does NOT automatically cascade discards to associationsβ€”this is intentional and a feature, not a bug. Ensure your database supports datetime indexing. No environment variables, external services, or version constraints beyond standard Rails compatibility.

πŸ—οΈArchitecture

πŸ’‘Concepts to learn

  • Soft deletes / Logical deletes β€” This is the entire premise of Discardβ€”you need to understand why flagging records with a timestamp is better than hard-deleting them for audit trails, recovery, and data integrity
  • ActiveRecord scopes and query filtering β€” Discard relies heavily on .kept and .discarded scopes to filter records; understanding how scopes work is central to using and extending this gem
  • Rails mixins and module inclusion β€” The entire gem is a mixin (include Discard::Model)β€”you need to understand how Ruby modules extend classes to comprehend the design pattern
  • ActiveRecord callbacks and state transitions β€” Discard implements discard/undiscard as state transitions; understanding Rails callbacks (before_discard, after_discard hooks) is useful for extending the gem
  • Database indexing on datetime columns β€” The migration adds an index on discarded_at to make filtering soft-deleted records performant; this is why the migration step is non-optional
  • Temporal queries in SQL β€” Discard filters via discarded_at IS NULL (kept) vs discarded_at IS NOT NULL (discarded)β€”understanding null-safe filtering in SQL is important for debugging queries
  • Exception-driven design with bang methods β€” Discard follows Rails conventions with discard/undiscard (safe, return true/false) vs discard!/undiscard! (bang, raise exceptions)β€”understanding this pattern helps with API design
  • rubysherpas/paranoia β€” Direct predecessor and alternative soft-delete gem; Discard was built to avoid Paranoia's cascading-delete and restoration-timestamp complexity
  • actsaparanoid/acts-as-paranoid β€” Another ActiveRecord soft-delete library; less actively maintained but shows alternative implementation patterns
  • rails/rails β€” Core Rails framework that Discard integrates with; understanding ActiveRecord::Base, scopes, and callbacks is essential context
  • rspec/rspec-rails β€” Testing framework used in this gem's test suite; necessary for contributing tests and understanding the spec structure
  • thoughtbot/factory_bot β€” Common companion gem in Rails projects using Discard for generating test fixtures and mocking model instances

πŸͺ„PR ideas

To work on one of these in Claude Code or Cursor, paste: Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.

Add comprehensive association handling tests in spec/discard/model_spec.rb

The repo provides soft delete functionality via Discard::Model, but there are no visible tests for how discarded records interact with ActiveRecord associations (has_many, belongs_to, has_and_belongs_to_many). This is critical because soft deletes can create unexpected behavior with dependent: :destroy and association queries. Adding tests would ensure the gem handles these edge cases correctly.

  • [ ] Add test cases in spec/discard/model_spec.rb for associations with discarded parent records
  • [ ] Test behavior of dependent: :destroy, dependent: :delete_all, dependent: :restrict_with_error with discarded records
  • [ ] Test that association queries (e.g., post.comments) properly filter out discarded records by default
  • [ ] Test counter_cache behavior with discarded associated records
  • [ ] Document findings in comments for future maintainers

Add GitHub Actions workflow for testing multiple Ruby/Rails versions in .github/workflows/

The existing test.yml workflow is minimal. The repo should test against multiple Ruby versions (2.7, 3.0, 3.1, 3.2+) and Rails versions (5.0, 6.0, 6.1, 7.0+) to ensure compatibility across the ActiveRecord ecosystem. This prevents silent breakage for users on different versions.

  • [ ] Create .github/workflows/matrix-test.yml with Ruby and Rails version matrix
  • [ ] Use matrix strategy to test combinations: Ruby 2.7-3.2 Γ— Rails 5.0-7.1
  • [ ] Configure gemfile selection to test against different Gemfile.rails-*.lock files
  • [ ] Add matrix test results to README.md badge section for visibility

Add integration test demonstrating real-world usage patterns in spec/discard/integration_spec.rb

The existing spec/discard/model_spec.rb likely tests isolated unit functionality, but there are no visible integration tests showing real-world scenarios like: auditing discarded records, restoring with callbacks, querying across discarded/kept records, and handling concurrent deletes. A new integration spec file would provide concrete examples for both users and maintainers.

  • [ ] Create spec/discard/integration_spec.rb with end-to-end scenarios
  • [ ] Add test for restore workflow with ActiveRecord callbacks (before_restore, after_restore)
  • [ ] Add test for complex queries combining kept and discarded record scopes
  • [ ] Add test for soft-delete behavior in transaction rollbacks
  • [ ] Add test demonstrating audit trail pattern (capturing who discarded what and when)

🌿Good first issues

  • Add support for custom discarded_at column names (e.g., discard_column: :deleted_at) as a configuration option in the mixinβ€”requires modifying lib/discard/model.rb to make the column name parameterizable instead of hardcoded
  • Document association handling patterns in README.md with concrete examples (e.g., a has_many :comments relationship where you want comments to remain visible after post is discarded, vs. a pattern where comments should be hidden)β€”helps users avoid the paranoia gotcha
  • Add a .with_discarded scope to lib/discard/model.rb that returns all records including discarded ones (complement to .kept/.discarded)β€”useful for admin dashboards and audit trails

⭐Top contributors

Click to expand

πŸ“Recent commits

Click to expand
  • 7a9417d β€” Merge pull request #116 from okuramasafumi/fix-ci (jarednorman)
  • f7dd775 β€” Merge pull request #118 from SuperGoodSoft/fix-tests (jarednorman)
  • 26f3113 β€” Update readme to have correct test command (AlistairNorman)
  • 4340d3f β€” Fix tests (AlistairNorman)
  • bc45ffc β€” Merge pull request #117 from rud/patch-1 (jarednorman)
  • 561f6c4 β€” Remove Rails 6 from the text matrix (rud)
  • 39e07c8 β€” Add Rails 8.1 on Ruby 4.0 to test matrix (rud)
  • b70455d β€” Pin concurrent-ruby to 1.3.4 to fix an error (okuramasafumi)
  • a652b8c β€” Merge pull request #115 from jhawthorn/dependabot/github_actions/actions/checkout-6 (jarednorman)
  • 68dfc9c β€” Bump actions/checkout from 5 to 6 (dependabot[bot])

πŸ”’Security observations

The Discard gem appears to be a well-maintained ActiveRecord mixin for soft deletes with good GitHub workflow automation (Dependabot configured). No critical or high-severity vulnerabilities were identified in the static analysis. The codebase follows standard Ruby gem conventions with proper configuration files (.rspec, .ruby-version, Rakefile). Primary recommendations: (1) Review Gemfile.lock against known CVEs using bundle audit, (2) Ensure comprehensive security documentation around soft delete implications (e.g., privacy, data retention, cascade behaviors), and (3) Verify that soft-deleted records are properly excluded from queries by default to prevent unintended data exposure. The gem's simplicity (primarily a mixin) reduces attack surface compared to more complex libraries.

  • Low Β· Missing dependency lock file details β€” Gemfile / Gemfile.lock. The dependency/package file content was not provided in the security context. Without visibility into Gemfile.lock or specific gem versions, it's not possible to identify known vulnerabilities in dependencies. Fix: Provide Gemfile.lock content and run 'bundle audit' regularly to check for known vulnerabilities in gems. Consider using Dependabot (already configured in .github/dependabot.yml) to automatically check and update dependencies.
  • Low Β· Incomplete README truncation β€” README.md. The README.md file appears to be incomplete or truncated in the provided content, which may indicate documentation gaps or potential incomplete security guidance. Fix: Ensure complete and up-to-date documentation including security considerations for soft deletes. Document any caveats about querying discarded records.

LLM-derived; treat as a starting point, not a security audit.


Generated by RepoPilot. Verdict based on maintenance signals β€” see the live page for receipts. Re-run on a new commit to refresh.

Healthy signals Β· jhawthorn/discard β€” RepoPilot