← Zpět na zprávu

Engineering quality, test strength and project health — auctify and folio

Defensive review for ACB (Aukční síň Galerie Kodl / acb.cz), the adopter running "Auctea" in production. Scope: engineering quality, tests, project health of the two public engines. Security and architecture are covered in sibling reports.

auctifyfolio
HEAD revieweda1b60f0 2024-11-1571084c97 2026-09-14
Ruby (app + lib)2 503 lines47 957 lines
Ruby (whole repo incl. test dummy)8 946 lines101 807 lines
Test files39 (31 engine, 8 dummy)466
Test cases180 engine / 199 total1 701
Assertion calls885 engine / 922 total5 133
Commits2276 749
Contributors all-time3 humans + dependabot33 email identities (~25 humans) + dependabot
CInone, everJenkins
Last commit22 months before this reviewsame day as this review

Bottom line up front. These are not two members of one family. folio is a healthy, actively engineered product with real release discipline and 5 991 lines of documentation. auctify is a 2 500-line domain engine that one person wrote, that has never had a continuous-integration pipeline, that declares a Ruby and a Rails version that both stopped receiving security patches years ago, and that cannot be installed today against the folio it declares as its own dependency. The money-handling code in auctify is, in isolation, better written than the average of folio's code — short methods, near-zero duplication, a genuinely strong bidding test suite. But nothing verifies it on any machine except the author's.


0. What could not be measured

No Ruby interpreter exists on this machine (command -v ruby → nothing; no /usr/lib/ruby, no rbenv/rvm/asdf, no vendored rubocop binary). Per instruction, nothing was installed. RuboCop was therefore not run and no offense counts by severity are reported. Every quantity below comes from git, grep, find, wc and a line-counting script, and is reproducible without Ruby.

Note that auctify's .rubocop.yml (both repos, 3 lines) does not contain rules at all — it is inherit_gem: folio: lib/rubocop_config.yml. So auctify's lint standard is whatever folio's lib/rubocop_config.yml says today, and that file sets TargetRubyVersion: 3.2.2 (lib/rubocop_config.yml:11) while auctify's own .ruby-version says 2.6.6. auctify is linted against a Ruby it does not claim to run.


1. Test suite

1.1 auctify

39 test files, 180 engine test cases, 885 assertion calls against 2 503 lines of app/ + lib/ — a density of 0.35 assertions per line of production code. That is three times folio's density (0.11). On raw numbers auctify looks well tested.

The raw numbers are misleading in both directions. Two corrections, one down and one up.

Correction down: 16 of the 180 tests are dead Rails scaffold output — PROVED, HIGH

test/system/auctify/ contains four files, 16 tests, 16 assertions. All four are unmodified output of bin/rails generate scaffold, committed 2021-02-12 (f4a4315) and 2021-04-16 (89e04fb) and never touched since. They test a CRUD scaffold UI that the real product does not have — click_on "New Bid", fill_in "Registration", with: @bid.registration, assert_text "Bid was successfully destroyed" (test/system/auctify/bids_test.rb:16-49).

One of them cannot even parse into a working call:

test/system/auctify/sales_test.rb:12   visitauctify_sales_url
test/system/auctify/sales_test.rb:17   visitauctify_sales_url
test/system/auctify/sales_test.rb:33   visitauctify_sales_url
test/system/auctify/sales_test.rb:49   visitauctify_sales_url

visitauctify_sales_url — the space between visit and the URL helper was lost. No such method is defined anywhere in the repository. All four tests in that file raise NoMethodError on their first line.

This is the single hardest proof in the report. A broken NoMethodError has sat in the test directory since 2021 and nobody noticed. It follows directly that bin/rails test:system has never been run green on this repository, by anyone, in four and a half years. There is no CI to have caught it (§1.3) and no human did either.

Consequence for the auction house: the browser-level tests provide zero assurance. When Auctea's own developers or a future maintainer report "the engine has system tests", that is not true.

Correction up: the bidding algorithm is genuinely, seriously tested — PROVED

Strip the scaffold and 164 real tests with 869 assertions remain, and the best of them are very good. test/services/auctify/bid_appender_test.rb is 625 lines and 14 tests, written as declarative expectation tables that walk a whole bidding war and assert the resulting database state bid by bid:

test/services/auctify/bid_appender_test.rb:77   "can handle autobidding with max_price"
test/services/auctify/bid_appender_test.rb:82-108
  { bid: { price: nil, max_price: 2_222, bidder: lucifer },
    appender: { success: true, errors: {} },
    auction_after: { current_price: 2_223, current_minimal_bid: 2_224,
                     winner: adam, bids_count: 6 } },

with the expected final bid rows written out as a comment block (test/services/auctify/bid_appender_test.rb:118-136). The named cases cover exactly the things that cost an auction house money when they are wrong:

TestFile:line
autobid / proxy-bid escalationtest/services/auctify/bid_appender_test.rb:77
reserve price not mettest/services/auctify/bid_appender_test.rb:155
bid-step ladder applied by price bandtest/services/auctify/bid_appender_test.rb:196
two identical limits — earlier bid winstest/services/auctify/bid_appender_test.rb:361
cannot overbid yourselftest/services/auctify/bid_appender_test.rb:285
can only raise your own maxtest/services/auctify/bid_appender_test.rb:302
price never pushed above the limittest/services/auctify/bid_appender_test.rb:463
fat-finger ceiling per bidtest/services/auctify/bid_appender_test.rb:505
"ultimate bidding test"test/services/auctify/bid_appender_test.rb:379

test/models/auctify/sale/auction_states_test.rb adds 14 tests over the AASM state machine including the two consistency validations (:174 winner-and-price on sold_in_auction, :205 no-winner on not_sold_in_auction). This is real domain testing, not model-in-isolation testing. Credit where it is due: whoever wrote bid_appender_test.rb understood the auction rules and pinned them down.

What is not tested — PROVED

GapEvidenceSeverity
Concurrency: nothing at all`grep -rn "Thread\parallelize\race\advisory" test/ returns only unrelated Puma comments. Zero Thread.new, zero parallelize`.CRITICAL
Bid cancellation and re-derivationcancel!/recalculate_bidding! appear on exactly one line of test: test/models/auctify/sale/auction_test.rb:145HIGH
AutobidFillerJobapp/jobs/auctify/autobid_filler_job.rb has no test file at allMEDIUM
Auctify::Sale::AuctionCallbacksapp/concerns/auctify/sale/auction_callbacks.rb — no direct test; it is the documented extension point (README.md:141-142)MEDIUM
9 tests are skippedtest/concerns/auctify/seller_test.rb:35, test/concerns/auctify/item_test.rb:12, test/controllers/auctify/sales_controller_test.rb:27 and :40, test/services/auctify/bid_appender_test.rb:73, test/jobs/auctify/bidding_is_close_to_end_notifier_job_test.rb:41, :54, :72MEDIUM

The concurrency gap is the important one. The production code takes an advisory lock when closing an auction (app/jobs/auctify/bidding_closer_job.rb:19, with_advisory_lock("closing_auction_#{auction_id}")) but takes no lock at all when placing a bid — app/models/auctify/sale/auction.rb:161 opens a bare ActiveRecord::Base.transaction with no lock!, no SELECT … FOR UPDATE, no advisory lock. Under PostgreSQL's default READ COMMITTED, two bids arriving in the same millisecond both read auction.ordered_applied_bids (app/services/auctify/bids_appender.rb:123), both compute a winner from the same stale bid set, and both write. The last write to current_price and current_winner wins.

That the design is unproven is not itself proof of a defect. But the test suite cannot tell you whether it is a defect, and there is no observation point that would: the only metric that would reveal it, diff_in_closing_time_seconds, measures closing latency, not bid interleaving.

Concrete consequence: at this house hammer prices reach millions of CZK. A lost update on the final two bids of a lot means the wrong bidder is recorded as winner, or the winning price is recorded one ladder step low. On a 3 000 000 CZK lot with a 100 000 CZK top ladder step (README.md:58), one dropped step is 100 000 CZK of lost hammer plus commission. The legal exposure is worse than the money: under Czech auction practice the house must be able to evidence the bid sequence that produced the hammer. An unserialised write path means the stored sequence can disagree with what bidders saw on screen, and there is no test, no log and no metric that would let you reconstruct which is right.

What would confirm it: a test that opens two database connections, starts two auction.bid! calls on the same auction, and asserts both bids land with a strictly increasing price. Roughly 40 lines. It has never been written.

The skip at test/concerns/auctify/item_test.rb:12 deserves quoting — SUSPECTED, HIGH

skip "Because `auctify_as :item` changes association in Auctify::Sale::Base,
      next lines randomly breaks full test suite run"

The author disabled a test because the engine's own class-macro mutates global state and makes the suite non-deterministic. That is a test hiding a design problem, and the problem — auctify_as rewriting associations on a shared superclass at load time — is still in the code (app/models/auctify/behaviors.rb:19, app/concerns/auctify/behavior/item.rb). What would confirm the production impact: booting a Rails console in an app that auctifies two different item classes and checking Auctify::Sale::Base.reflect_on_association(:item). Cannot be done here — no Ruby.

1.2 folio

466 test files, 1 701 test cases, 5 133 assertion calls. Coverage is broad and follows the code:

app areaproduction filestest filesratio
components12310888 %
cells916571 %
controllers905966 %
jobs372362 %
mailers4375 %
models1246250 %
services6350 %
lib521529 %
serializers7229 %
helpers27519 %

Three things folio's test infrastructure does that auctify's does not:

  1. Parallel execution, deliberately tuned. test/test_helper_base.rb:75-96 reads

TEST_MAX_WORKERS (default 8) and a threshold (default 100 loaded test methods) and calls parallelize(**parallelize_options). The change is documented in CHANGELOG.md under 7.7.2. auctify has no parallelize call anywhere.

  1. VCR with credential scrubbing. test/test_helper_base.rb:21-57 filters

OPENAI_API_KEY, ANTHROPIC_API_KEY, FOLIO_AI_*, plus Openai-Organization, X-Request-Id and Set-Cookie response headers before a cassette is written, and drops failed AI-provider interactions entirely. This is careful work; most projects commit their API keys into VCR cassettes.

  1. skip means "feature disabled", not "abandoned". 36 skips, and the overwhelming

majority are conditional guards such as skip unless Rails.application.config.folio_users_publicly_invitable — the test runs when the feature is on. Contrast auctify's skip "TODO" and skip "is this required?".

folio also has zero concurrency tests (same grep, same empty result). For a CMS that matters far less than it does for a bidding engine.

1.3 CI — the sharpest difference between the two repos

auctify has never had any CI configuration, on any branch, in its entire history — PROVED, CRITICAL.

git log --all --diff-filter=ADM -- Jenkinsfile .github .travis.yml .gitlab-ci.yml .circleci
→ (empty)
git log --all --pretty=format: --name-only --diff-filter=A | grep -iE 'jenkins|github|travis|circle'
→ (empty)

No file matching a CI configuration has ever been added to this repository. There is no .github/, no Jenkinsfile, nothing. The only automation present is a Guardfile (rubocop + slim-lint on save, developer-local) and lib/tasks/test_tasks.rake.

folio has a Jenkinsfile. Jenkinsfile:1-31, one stage:

Jenkinsfile:5    stage('Rails Tests')
Jenkinsfile:20     sh '/var/lib/jenkins/test_rails.sh'
Jenkinsfile:24   post { success: slackSend(color:'good', …)
Jenkinsfile:27          failure: slackSend(color:'danger', …) }

It computes a commit link, a Blue Ocean build link and the commit author, runs one shell script, and posts pass/fail to Slack with the elapsed time.

Two caveats the adopter should know:

Consequence for the auction house: every line of the code that computes who won a lot and at what price is merged on a developer's word that the tests passed locally. The visitauctify_sales_url typo proves that word is not always reliable.


2. Code health

2.1 Method and class size

Measured by a brace-matching line counter over app/ and lib/.

auctify — this is the best part of the repository.

bucketmethods
1–10 lines158
11–25 lines26
26–50 lines0
51+ lines0

188 methods across app/ + lib/, and not one exceeds 25 lines. The longest is 21 (app/jobs/auctify/bidding_is_close_to_end_notifier_job.rb:7). The five longest classes:

linesfile:lineclass
374app/models/auctify/sale/auction.rb:5Auctify::Sale::Auction
249app/services/auctify/bids_appender.rb:4Auctify::BidsAppender
159app/models/auctify/sale/base.rb:5Auctify::Sale::Base
110app/models/auctify/sales_pack.rb:4Auctify::SalesPack
84app/controllers/auctify/api/v1/auctions_controller.rb:6Api::V1::AuctionsController

A 374-line model and a 249-line service are large but not out of line for a Rails domain core, and both decompose into small, single-purpose private methods — solve_winner dispatching to solve_limits_fight / increase_bid_price / duplicate_increased_win_bid (app/services/auctify/bids_appender.rb:179-214) is a clean four-case table. This is disciplined code. State it plainly: the author knew what they were doing.

folio — normal for a 45 000-line CMS, with identifiable hotspots.

bucketmethods
1–10 lines2 325
11–25 lines752
26–50 lines193
51–100 lines58
100+ lines8

3 336 methods; 7.5 % exceed 25 lines. The eight worst:

linesfile:line
207app/lib/folio/tiptap/node_builder.rb:279 setup_structure_for_folio_attachment
183app/jobs/folio/generate_thumbnail_job.rb:74 make_thumb
159app/controllers/folio/console/api/autocompletes_controller.rb:257 react_select
143app/helpers/folio/console/react_helper.rb:138 react_ordered_multiselect
125app/components/folio/console/links/modal/list_component.rb:14 records_data
108app/controllers/concerns/folio/console/api/file_controller_base.rb:250 update_thumbnails_crop
107app/components/folio/console/ui/image_component.rb:54 set_data
103app/cells/folio/cookie_consent_cell.rb:7 self.known_cookies

Largest classes: Folio::Tiptap::NodeBuilder 805 lines (app/lib/folio/tiptap/node_builder.rb:3), Folio::Console::CatalogueCell 626, Folio::Console::Api::FileControllerBase 574, Folio::File 552, Folio::User 500. None of these sit on the auction money path.

2.2 Duplication

Measured as identical 8-line windows (comments and blank lines stripped), app/ only, test dummies excluded.

2.3 Specific code-health defects found by reading

These are engineering-quality findings. They are reported here because they are defects a maintainer inherits, not because this is the security report.

H-1. current_winner has two sources of truth — PROVED, HIGH

app/models/auctify/sale/auction.rb:25 declares belongs_to :current_winner, polymorphic: true, optional: true and the schema has current_winner_type / current_winner_id (:414-415). app/models/auctify/sale/auction.rb:177 writes to that column (self.current_winner = winner inside succesfull_bid!).

But app/models/auctify/sale/auction.rb:202-204 overrides the association reader:

def current_winner
  bidding_result.winner
end

and bidding_result (:198-200) re-runs the entire BidsAppender over all bids on every call. Consequences:

  1. The stored column is effectively write-only in Ruby. Every Ruby read recomputes.
  2. SQL reads use the column. scope :where_current_winner_is, ->(bidder) { where(current_winner: bidder) }

(:30) queries the stored value. A query and an attribute read on the same auction can disagree if the column was ever written by a path that later diverged.

  1. close_bidding copies one to the other: self.winner = current_winner (:71) — the

recomputed value into the persisted winner column. So the authoritative winner of a lot is decided by an unlocked recomputation at close time, not by the value the bidders were shown while bidding.

  1. Performance: buyer_vs_bidding_consistence (:274) calls bidding_result a second

time within the same validation pass, unmemoized. current_winner, current_winning_bid (:206) and current_minimal_bid (:218) each trigger a further full recomputation. Rendering a list of 50 lots re-derives 50 bid ladders.

Consequence for the auction house: money. The winner shown on screen during bidding and the winner written at close are produced by two separate recomputations over an unlocked table. If a bid commits between them the house issues the invoice to the wrong bidder. Recovering from that after a 2 000 000 CZK hammer is a lawyer's problem, not a developer's.

H-2. A failed bid can commit partial writes — PROVED, HIGH

app/models/auctify/sale/auction.rb:161-169:

ActiveRecord::Base.transaction do
  bid.created_at ||= Time.current
  bap = Auctify::BidsAppender.call(auction: self, bid: bid)
  bap.success? ? after_bid_appended(bap) : after_bid_not_appended(bap)
  bap.success?
end

BidsAppender signals failure with fail! (app/services/auctify/service_base.rb:65-67), which sets a boolean. It does not raise. Returning false from a transaction block does not roll back in Rails — only raising ActiveRecord::Rollback or an exception does. And append_bids! (app/services/auctify/bids_appender.rb:74-81) continues after a failed save:

fail! unless bid.save
if @updated_win_bid.present?
  fail! unless @updated_win_bid.save     # still attempted
end

So if the incoming bid fails to save and the generated autobid succeeds, the autobid is committed without the bid that triggered it. The auction's price rises with no corresponding manual bid in the history.

Reachability is narrow — approved_bid? (:58-72) already ran bid.valid?, so save fails only on a database constraint or a state change between validation and save, which is exactly what the missing bid-path lock (§1.1) makes possible. The two findings compound.

Consequence: a price step in the bid history with no bidder behind it. If a bidder disputes the hammer and asks to see the bid log, the house cannot explain that row.

M-1. Unbounded ladder walk with an unvalidated input path — PROVED, MEDIUM

app/services/auctify/bids_appender.rb:229-239:

def increase_price_to(overcome:, ceil:)
  running_price = current_price
  while running_price <= overcome
    running_price = increase_price(running_price)
  end
  [running_price, ceil].min
end

increase_price (:225-227) calls auction.minimal_bid_increase_amount_at(price, respect_first_bid: false), which at app/models/auctify/sale/auction.rb:226 does:

_range, increase_step = bid_steps_ladder.detect { |range, step| range.cover?(price) }

If no configured range covers price, detect returns nil, destructuring gives increase_step = nil, and price + nil raises inside the loop.

Whether that is reachable depends on how the ladder was written. The numeric and string input paths are safe: MinimalBidsLadderType#cast_from_min_numbers (app/types/auctify/minimal_bids_ladder_type.rb:63-75) always closes with an open-ended top range at :72, Range.new(previous_step.first, nil, false). But the Range input path does no validation at all:

app/types/auctify/minimal_bids_ladder_type.rb:52-54
def cast_from_ranges(hash)
  hash # desired format
end

An operator who configures bid_steps_ladder directly with a bounded top range — e.g. (2_000_000...5_000_000) => 100_000 instead of the README's open (2_000_000..) => 100_000 (README.md:58) — gets a NoMethodError the first time a lot passes 5 000 000 CZK, mid-auction, inside the bid path. The bid is rejected; the bidder sees an error; the lot stalls.

Secondary: the loop starts at current_price, not at overcome, so a proxy-bid war between two high limits walks the entire ladder from the current price upward on every single bid. Bounded in practice by the ladder's step sizes, but it is O(ladder distance) per bid inside a request. SUSPECTED performance issue at auction-close peak; confirm by timing BidsAppender.call against a 3 000 000 CZK lot with the README ladder — needs a Ruby environment.

Third: MinimalBidsLadderType#type returns :jsonb (app/types/auctify/minimal_bids_ladder_type.rb:9) but the column is annotated bid_steps_ladder :json (app/models/auctify/sale/auction.rb:399). Cosmetic today.

M-2. Validation error added in a setter is silently discarded — PROVED, MEDIUM

app/models/auctify/bid.rb:51-55:

def bidder=(auctified_model)
  errors.add(:bidder, :not_auctified) unless auctified_model.class.included_modules.include?(Auctify::Behavior::Buyer)
  ...
end

BidsAppender#approved_bid? calls bid.valid? as its first action (app/services/auctify/bids_appender.rb:60), and valid? clears errors before re-running validations. The :not_auctified error added at assignment time is therefore always wiped before it is read. The check is dead code. In practice check_bidder (:126-133) catches the case by a different route, so this is a latent trap rather than an open hole — but a maintainer reading bidder= will believe a guard exists that does not.

L-1. Duplicated schema annotation block — PROVED, LOW

app/models/auctify/sale/auction.rb:419-430 and :432-444 are the same # Indexes block twice. annotate ran, its output was committed, it ran again and the second copy was committed too. Harmless; indicative of how much review these commits got.

L-2. Three typos in the README's executable example — PROVED, LOW (but see §4)

README.md:181, :190, :198 all read aucion.bid! (missing t). Nobody ever ran the documented example.


3. API and upgrade safety

3.1 Does auctify version its public interface? Partly, and badly.

lib/auctify/version.rb:4 → VERSION = "1.1.5". Three git tags exist:

tagdatecommit
1.0.02021-06-25cc7d014
1.1.02022-01-21a96750e
v1.1.52023-09-05782e4d1

Note the inconsistent naming — two bare, one v-prefixed.

The CHANGELOG names eight versions: 1.0.2, 1.0.3, 1.1.0, 1.1.3, 1.1.4, 1.1.5, plus an untitled 2024-11-15 entry, plus 1.0.0. Five of those were never tagged. 1.0.2, 1.0.3, 1.1.3 and 1.1.4 exist only as CHANGELOG headings. A consumer cannot check out the code that corresponds to "version 1.1.3". This is the opposite of versioning a public interface.

Worse: HEAD is 14 months past the last tag and contains a dependency removal with no version bump. CHANGELOG.md:4-6:

### 2024-11-15
- added `sale_counts_by_state` metric to Yabeda
- removed `yabeda-prometheus` gem dependency (now only `yabeda` is required).
  Other yabeda gems should be in main app.

Removing a runtime dependency is a breaking change for any consumer that relied on it being pulled in transitively. It shipped on master under the same VERSION = "1.1.5" that was tagged 14 months earlier. Anyone consuming auctify the way auctify itself consumes folio — branch: "master" — took that change silently.

3.2 Breaking changes between the tags

The CHANGELOG does mark them, and clearly. Credit for this:

versionbreaking changeCHANGELOG line
1.0.3 (2021-09-17)auction_prolonging_limit renamed to auction_prolonging_limit_in_secondsmarked #### BREAKING
1.1.0 (2022-01-21)BiddingCloserJob no longer self-enqueues; the host app must now run EnsureAuctionsClosingJob periodicallymarked #### BREAKING

The 1.1.0 entry is a good piece of technical writing — it explains the new invariant ("interval between runs must be shorter than auction_prolonging_limit_in_seconds") and warns to run it at boot. That change is the difference between lots closing and lots not closing, and the author documented it properly.

The two breaking changes that are not marked: the 2024-11-15 dependency removal above, and c8f50ad (2024-11-05) adding Auctify.configuration.maximal_increase_of_price_proc — additive with a safe default (lib/auctify/configuration.rb:28 returns nil = no restriction), so not breaking. Fine.

3.3 Is the CHANGELOG maintained?

auctify: barely. CHANGELOG.md is 1 820 bytes, 8 entries, covering 227 commits over 3.5 years. Entire feature branches (§6) have no entry. The most recent entry has no version heading at all.

folio: exemplary. CHANGELOG.md is 67 591 bytes — 36 released versions in Keep-a-Changelog format with ### Added / ### Changed / ### Fixed, plus an [Unreleased] section that is actually kept current. Entries cross-reference UPGRADING.md by name. One entry even documents an optimisation that was considered and rejected, with the benchmark numbers that justified rejecting it (PostgreSQL 16.14, 300 000 files, 600 000 placements, medians under 2 s) and the design that would be needed if production ever looks different. That is better release documentation than most commercial products produce.

3.4 What happens to a consumer on a Rails upgrade — the finding that matters most

auctify cannot be installed today. PROVED, CRITICAL.

Three declarations, all in the repository, mutually unsatisfiable:

file:linedeclares
auctify.gemspec:31spec.add_dependency "rails", "~> 6.1.4"
auctify/.ruby-version:12.6.6
auctify/Gemfile:14gem "folio", github: "sinfin/folio", branch: "master"
folio/folio.gemspec:68s.add_dependency "rails", "~> 8.0"
folio/.ruby-version:13.3.3

auctify's Gemfile pulls folio from master. folio's master today requires Rails ~> 8.0. auctify requires Rails ~> 6.1.4. ~> 6.1.4 and ~> 8.0 have no intersection. bundle install in a clean checkout of auctify cannot resolve. Its own test suite cannot be booted without first editing the gemspec.

auctify's Gemfile.lock:3 still pins folio at c743af4b63cd3a2fa9780cf68e572ff8f4d73e51 — a commit from the Rails 6.1 era. The lock file is the only thing that ever made this repository buildable, and it is frozen in 2024.

And Ruby 2.6.6 cannot run Rails 8 at all. Ruby 2.6 reached end of life on 2022-03-31 — three and a half years of unpatched Ruby. Rails 6.1's security support ended 2024-10-01 — roughly a year of unpatched Rails, and the last auctify commit (2024-11-15) postdates that.

Consequence for the auction house:

  1. You cannot run auctify's tests as published. Any claim about auctify's quality that

rests on "the tests pass" is unverifiable by an outside party today.

  1. Auctea must already be running a private fork. Auctea runs folio (actively

developed, Rails 8 era) and auctify together. Published auctify cannot coexist with published folio. Therefore the auctify running in production is not the code in this repository. SUSPECTED, and this is the most commercially important suspicion in this report. The public repo is a snapshot, not the product. Every finding above about auctify's test coverage is a finding about code that may no longer be the code you run. What would confirm it: ask Sinfin for bundle list | grep auctify and the resolved git SHA from an Auctea production deploy, or for the Gemfile.lock GIT block. One line of output settles it.

  1. On the next Rails upgrade you are on your own. There is no upstream maintainer

moving auctify forward. Whoever holds the private fork carries the whole Rails-version migration for the bidding engine alone, with no CI.

A second, narrower upgrade landmine, in case anyone does try to move auctify forward: test/test_helper.rb:22-29 guards fixture loading on if ActiveSupport::TestCase.respond_to?(:fixture_path=). fixture_path= was deprecated in Rails 7.1 and removed in 7.2 (renamed fixture_paths). On Rails 7.2+ that guard evaluates false, fixtures :all at :28 never runs, and every fixture-dependent test — which is nearly all of them — fails against an empty database with no explanation pointing at the cause. MEDIUM; a day lost, not money lost, but it will be the first wall anyone hits.

3.5 folio's upgrade story

Strong. UPGRADING.md (10 179 bytes) documents breaking changes per version with before/after code and an explicit "Action required:" list. Example, UPGRADING.md:20-40:

BREAKING CHANGE: has_folio_tiptap? has been changed from a class method to an instance method. … Search your codebase for \.has_folio_tiptap\?

It even gives the grep. The ## Unreleased section at the top is kept current (UPGRADING.md:3-17, the AASM email-modal cell → ViewComponent migration).

The one real gap: 36 CHANGELOG releases, 2 git tags (v7.4.1 2026-03-11, v7.6.2 2026-04-20). You cannot git checkout v7.7.2. Consumers pin to master or to a raw SHA. Combined with ## [Unreleased] currently holding about seven weeks of changes (last release 7.7.2 dated 2026-07-23; HEAD 2026-09-14), anyone tracking branch: master — which is what auctify does at Gemfile:14, and probably what Auctea does — is running unreleased code by design. MEDIUM for folio; the CHANGELOG discipline largely compensates, but a tag is checkable and a heading is not.


4. Documentation

4.1 auctify

artifactstate
README.md245 lines
doc/two PNG state diagrams only — auction_state_diagram.png, retail_sale_state_diagram.png. No prose docs at all.
UPGRADING.mddoes not exist
CHANGELOG.md8 entries, see §3.3
inline comments in app/372 lines against 2 345 lines of code, but roughly 150 of those are annotate-generated schema blocks

The README is better than nothing and worse than it looks. What it does well:

What breaks it:

The single end-to-end worked example is wrong. PROVED, HIGH. README.md:145-219 is the only place a new developer can learn to drive an auction from start to finish. Trace it:

The documented price should be 151. A developer following the README to close a lot hits a raised exception at the most consequential step in the system and has no documentation telling them why.

## Features required (README.md:45-76) mixes built with wished-for. It is a requirements list, not documentation, and nothing marks which items exist. Items in that list with no implementation in app/: auction-number format configuration (:63), follow-an-auction / follow-an-author notifications (:64), item categories with category-filtered browsing (:65). A reader cannot distinguish "the engine does this" from "we wanted this in 2021". ## TODO at :31-32 and ## Contributing at :239-240 ("Contribution directions go here.") are unfinished template text.

Could a new developer operate an auction night from the documentation alone? No. Answered concretely — here is what an auction-night operator needs and where the docs are silent:

Operational questionDocumented?
How do I close a lot manually if a job fails?No. No manual-close path exists on master at all; it sits unmerged on origin/petr/manual-closing (§6).
How do I cancel a bid entered in error?No. Bid#cancel! exists (app/models/auctify/bid.rb:23-27) and is mentioned nowhere in the README.
What happens if two bidders hit the same limit?No. The rule ("first placed bid wins") exists only as a comment inside a test, test/services/auctify/bid_appender_test.rb:361 and :100.
What is the difference between winner and buyer?No. Both appear in the example (README.md:196, :207-208) with no explanation of why they differ.
What does reserve price do to a closing auction?No. reserve_price never appears in the README. Behaviour is only in overcame_reserve_price? (app/services/auctify/bids_appender.rb:111-116).
How do I recover if BiddingCloserJob dies mid-auction?Partly. README.md:124-125 says to run EnsureAuctionsClosingJob periodically and states the interval invariant. Nothing on what to do after a failure.
Is there a runbook, an on-call page, an incident procedure?No.

The operational knowledge exists — in the author's head and in bid_appender_test.rb. For a business that runs live auction nights where a stalled lot is a room full of waiting bidders, that is the wrong place for it.

4.2 folio

artifactstate
docs/29 files, 5 991 lines
UPGRADING.md10 179 bytes, per-version, with "Action required"
README.md7 089 bytes — setup, system requirements, image tooling, Yarn, quick install
ROADMAP.md11 492 bytes
AGENTS.md8 874 bytes
.skills/21 topic directories for AI-assisted development
CHANGELOG.md67 591 bytes, 36 versions

The docs/ set covers architecture, atoms, cache, components, concerns, configuration, emails, embed, extending, faq, files, forms, jobs, overview, packs, sanitization, seeding, sitemap, testing, tiptap, troubleshooting, upgrade, plus design documents (docs/design/cra-encoding-system.md) and implementation plans (docs/plans/image_metadata_extraction_plan.md).

docs/testing.md and docs/troubleshooting.md have no counterpart anywhere in auctify. The 21 .skills/ directories (folio-testing, folio-view-component-migration-from-cells, folio-stimulus-migration-from-legacy-js, code-review, …) show a team that has invested in making its conventions transferable. A new developer could become productive in folio from the repository alone. They could not do the same for auctify.


5. Project health from git

5.1 Commits per year

yearauctifyfolio
2017—269
2018—671
2019—621
2020—561
2021197688
202223599
20233807
202441 107
202501 051
2026 (to 09-15)0375
total2276 749

auctify: 197 of 227 commits (87 %) landed in its first year. Since 2022-12-31: seven commits, then twenty-two months of nothing. folio: never below 269 commits a year in ten years, and its two busiest years are 2024 and 2025.

5.2 Contributors and bus factor

auctify — bus factor 1, and it has been 1 since 2022.

foton <foton@centrum.cz> and Petr Mlčoch <foton@centrum.cz> are the same person (identical email; the git identity was renamed). Consolidated:

Counted on master by email, which is the only way to see it (git log --pretty='%ae'):

emaildisplay namescommitssharelines addedlines deleted
foton@centrum.czfoton (169) + Petr Mlčoch (11)18079 %14 8303 358
petr@mlcochovi.czPetr Mlčoch — same human10.4 %987
contact@petrmarek.euPetr Marek4018 %1 915811
dedekmm@gmail.comdedekm31 %52
dependabot[bot]—31 %33

Consolidating the two identities that are one person: 181 of 227 commits (80 %) and 14 928 of 16 851 lines added (89 %) come from a single individual. By year:

yearauthors
2021foton 148, Petr Marek 40, dependabot 3, dedekm 3, Mlčoch 3
2022foton 21, Mlčoch 2
2023Mlčoch 3 — one human
2024Mlčoch 4 — one human

Every commit since 2023-01-01 is by the same individual. If that person leaves Sinfin, auctify has no maintainer with working knowledge, and there is no CI and no documentation to substitute for them (§1.3, §4.1). This is the project-health finding that should drive the adopter's decision.

folio — bus factor nominally 1, effectively 2–3.

33 distinct author emails all-time (several people use two, so roughly 25 humans). Top of the list:

authorcommitsshare
Petr Marek5 19777 %
dedekm / Martin Dedek (3 identities)4617 %
Petr Mlčoch2283 %
Filip Ornstein1733 %
Vlada (Vladislav Trefil)1492 %
David Duben971 %
Lipan Martin731 %
dependabot[bot]711 %
Tomáš Cabák681 %
Jiří Motejl (2 identities)741 %

Last 12 months (since 2025-09-15), 672 commits from 19 distinct human identities: Petr Marek 426 (63 %), Petr Mlčoch 28, jirkamotejl 27, Filip Ornstein 26, Jiří Motejl 23, dependabot 22, Vlada 18, pj258 17, Tomáš Cabák 16, Martin Dedek 15, zaneta.gebka 12+, Nikolaj Kolesnik 10, Petr Panoška 7+, Daniel Halíř 6, Vláďa 4.

Petr Marek's 63 % share is high — if he leaves, folio slows badly — but eighteen other people touched the code this year and can read it. That is a materially different risk from auctify's one.

5.3 Do pull requests come from outside the company?

No, in either repo. PROVED.

folio: 248 merge commits; every PR merge names a branch in the same organisation:

git log --merges --pretty='%s' | grep -oE 'from [a-z-]+/' | sort | uniq -c
→ 71 from sinfin/

71 of 71. Zero fork PRs. The only non-Sinfin contributor is dependabot[bot].

auctify: same picture. The five human authors are all Sinfin people; the only other contributor is dependabot[bot].

Both are MIT-licensed public repositories with no external community. The "open source" label buys the adopter the right to read and fork the code. It does not buy a community that will review, patch or maintain it. For auctify specifically, the public-repo status is doing no work at all: no outside eyes have ever looked at the bidding algorithm.

5.4 Time between releases

auctifyfolio
tags3 (1.0.0, 1.1.0, v1.1.5)2 (v7.4.1, v7.6.2)
CHANGELOG releases836
first → last release2021-06-25 → 2023-09-052024-12-11 → 2026-07-23 (recent window)
median gap between releases~7 months~3 weeks
time since last release at review24 months7.5 weeks

folio's recent cadence from CHANGELOG.md: 7.0.0 (2026-01-05), 7.1.0 (01-07), 7.1.1 (01-08), 7.1.2 (01-12), 7.2.0 (02-05), 7.2.1 (02-12), 7.3.0 (02-24), 7.4.0 (03-04), 7.4.1 (03-11), 7.5.0/7.5.1 (03-19), 7.6.0 (04-09), 7.6.1 (04-16), 7.6.2 (04-20), 7.6.3 (04-22), 7.6.4 (04-28), 7.7.0 (06-02), 7.7.1 (06-16), 7.7.2 (07-23). A major version (6.x → 7.0.0) shipped in January 2026 with the migration written up in UPGRADING.md. That is a professionally run release train. It is also 36 releases against 2 tags, which is the discipline gap noted in §3.5.

5.5 Age of open issues

Issue trackers are on GitHub and cannot be read from a local clone. Two things are visible in the repository:

SUSPECTED that auctify's tracker is effectively abandoned. Confirm by opening github.com/sinfin/auctify/issues and sorting by oldest — one minute of work for someone with a browser.


6. The dormancy question

auctify's last seven commits, examined individually.

6.1 Were the 2023-09 and 2024-11 commits substantive or housekeeping?

Substantive. Four of seven changed real behaviour, with tests. PROVED.

commitdatefileswhat it didverdict
d992bc12023-09-0512 (+133/−40)Split "do not confirm bids for this auction" from "I agree with auction terms" into two independent flags. Adds migration …_add_confirmed_sales_pack_terms_to_bidder_registrations.rb, changes app/models/auctify/bidder_registration.rb and app/controllers/auctify/api/v1/auctions_controller.rb, adds 58 lines of controller tests.Substantive, legally relevant
782e4d12023-09-052"missing changed files" — Gemfile.lock + dummy schemaHousekeeping (fixup of the above)
2c908ac2023-09-051"and changelog, of course"Housekeeping
d3a3e0b2024-11-052bids_per_minute Yabeda metricSubstantive (observability)
c8f50ad2024-11-0515 (+129/−47)Adds Auctify.configuration.maximal_increase_of_price_proc — a per-bid ceiling on how far one bid may raise the price. Touches app/services/auctify/bids_appender.rb (check_price_maximum, :151-162), lib/auctify/configuration.rb, Czech locale, README, and 64 lines of new bid_appender_test.rb coverage. Closes issue #450.Substantive, money-protective
dbaf79c2024-11-151sale_counts_by_state metricSubstantive (observability)
a1b60f02024-11-159Yabeda rework: drops the yabeda-prometheus runtime dependency (auctify.gemspec), removes lib/auctify/railtie.rb wiring, adds dummy-app ActiveJob configSubstantive and breaking (§3.1)

The important one is c8f50ad. maximal_increase_of_price_proc is an anti-fat-finger guard: it caps how much a single bid can raise the price, which is precisely the control an auction house wants when a bidder can type 30 000 000 instead of 3 000 000. It shipped with tests. That is not the behaviour of an abandoned project.

The correct characterisation of auctify is not "dead". It is "maintenance-only, by one person, without CI." The work that gets done is competent. There is simply almost none of it, and only one person who can do it.

6.2 Is there a branch with real unmerged work?

Yes. One, and it is the feature an auction house needs most.

All 25 remote branches, with divergence from origin/master:

branchaheadbehindlast commitsubject
petr/manual-closing21202022-03-17add lock_bidding/unlock_bidding endpoints, authorize account
petr/folio91772021-04-23rm application_record ordered scope
leaving_isolate_namespace61842021-04-21added sale.number, removed sale.selling_price
foton/auction_callbacks21232021-05-05passing bid_appender to bid callbacks
md/mailers21232021-05-05add admin notifications to auction_mailer
monitoring_tool2132022-06-03added MonitoringRecordDownloader service
prometheus_monitoring2132022-06-07yabeda metrics verified and documented
petr/bids-appender-sql1362021-11-30reduce bids_appender sql query count by 1
petr/bids_appender_optim11002021-05-11use ordered_applied_bids to optimize SQL
petr/overbid-by-limit1482021-09-07pass overbid_by_limit option to form cell
petr/rails-6.111722021-04-26bump rails
update_folio_and_rails_611552021-08-04update folio, rails to 6.1
dependabot/bundler/rack-2.2.6.3172023-03-09Bump rack 2.2.3.1 → 2.2.6.3
dependabot/bundler/activesupport-6.1.7.2172023-01-31Bump activesupport 6.1.6 → 6.1.7.2
dependabot/bundler/actionpack-6.1.7.2172023-01-30Bump actionpack 6.1.6 → 6.1.7.2
dependabot/bundler/activerecord-6.1.7.2172023-01-30Bump activerecord 6.1.6 → 6.1.7.2
dependabot/bundler/globalid-1.0.1172023-01-19Bump globalid 1.0.0 → 1.0.1
dependabot/bundler/rails-html-sanitizer-1.4.4172022-12-14Bump rails-html-sanitizer 1.4.2 → 1.4.4
dependabot/bundler/loofah-2.19.1172022-12-13Bump loofah 2.18.0 → 2.19.1
dependabot/bundler/nokogiri-1.13.9172022-10-21Bump nokogiri 1.13.6 → 1.13.9
dependabot/bundler/omniauth-1.9.2182022-08-31Bump omniauth 1.9.1 → 1.9.2
manual-closing0202022-02-04folio bump (superseded)
shifting_of_sales_pack_time_frame0502021-08-11merged
master002024-11-15—

Two readings of this table.

(a) petr/manual-closing is a finished, tested, abandoned feature. PROVED, HIGH.

21 commits, 2022-03-04 to 2022-03-17, +616/−54 across 24 files, including:

Selected commits: 0efa80a "add close_manually! to sale/auction", 5672a5b "close_manually by Folio::Account only", b81c906 "fix bids_appender check_auction_state for manual auctions", aab2d4d "add price_check to manually_close", 7536802 "add bidding lock, require bidding lock to close manually".

This is live-room auctioneer control — the ability for a human to lock bidding and hammer a lot down manually rather than waiting for a timer. For a house that runs live auction nights with an auctioneer in the room, it is arguably the feature. It was built, tested, and never merged. It has sat 3.5 years, now 20 commits behind master, and merging it today means resolving conflicts against bids_appender.rb, auction.rb and base.rb — all files that master has since changed.

Consequence, and this is a pointed one: ACB does run live auctions. If Auctea has manual closing, this branch was re-implemented privately, which is direct evidence for the private-fork hypothesis in §3.4. If Auctea does not have manual closing, the house is running timed-only auctions and 616 lines of the feature it needs are sitting unmerged in a public repository. SUSPECTED either way; confirm by asking whether an Auctea operator can close a lot by hand from the console. One question to the auction house settles it.

(b) Nine unmerged dependabot branches are nine unapplied security updates. PROVED, HIGH.

nokogiri 1.13.6 → 1.13.9, rails-html-sanitizer 1.4.2 → 1.4.4, loofah 2.18.0 → 2.19.1, rack 2.2.3.1 → 2.2.6.3, globalid 1.0.0 → 1.0.1, omniauth 1.9.1 → 1.9.2, and actionpack/activerecord/activesupport 6.1.6 → 6.1.7.2. These are patch-level bumps in the exact version series dependabot raises for published CVEs. Every one was opened, never reviewed, never merged, and the oldest has been open since 2022-08-31 — three years.

The Gemfile.lock of this repository therefore still resolves rails (6.1.6) (Gemfile.lock, rails (6.1.6)), one patch series behind the 6.1.7.2 security release that dependabot offered in January 2023.

Severity of consequence depends entirely on whether Auctea's own Gemfile.lock inherits these versions. SUSPECTED that it does not — Auctea is on the folio-8 stack and must have its own lock. But this repository's published state is a Rails 6.1.6 application with nine ignored security bumps, and that is what any new adopter would clone.

(c) Everything else on that list is 2021-era rubble. Eleven branches more than 36 commits behind, several more than 170. Nobody prunes. That is cosmetic, but it is the same signal as the duplicated annotate block and the four-year-old NoMethodError: nothing in this repository is ever swept.


7. folio versus auctify, axis by axis

axisauctifyfoliowho wins
CInone, ever; proven by git log --all over every CI filenameJenkins, tests + Slack (Jenkinsfile:1-31) — though the test script lives outside the repofolio, decisively
Test volume180 engine tests, 885 assertions1 701 tests, 5 133 assertionsfolio
Test density0.35 assertions per production line0.11auctify
Test honesty16 of 180 are dead scaffold; one file raises NoMethodError on line 1 (test/system/auctify/sales_test.rb:12)skips are feature flags, not abandonmentfolio
Domain-level testinggenuinely strong — test/services/auctify/bid_appender_test.rb walks whole bidding wars with expected DB statebroad but mostly per-unitauctify
Concurrency testingzerozerotie, both fail — but only auctify handles money
Test parallelismnonetuned, test/test_helper_base.rb:75-96folio
Method sizeno method over 25 lines, 188 methods8 methods over 100 lines, worst 207auctify
Duplication6 blocks, all legitimate270 groups, 305 redundant occurrencesauctify
Largest class374 lines805 linesauctify
README245 lines; worked example raises an exception (README.md:210)7 089 bytes, accurate setupfolio
docs/two PNGs, no prose29 files, 5 991 linesfolio, by everything
UPGRADING.mdnoneper-version, with "Action required" and grepsfolio
CHANGELOG8 entries / 227 commits; last one has no version heading36 versions, Keep-a-Changelog, documents rejected work with benchmarksfolio, decisively
Tags vs releases3 tags, 8 CHANGELOG versions; 5 versions untaggable2 tags, 36 CHANGELOG versionsboth bad, folio worse on ratio, auctify worse on outcome
Release cadencelast release 24 months ago~3 weeks medianfolio
Commits, last 12 months0672folio
Bus factor1, and 1 since 20221 nominal / 2–3 effective, 19 humans active this yearfolio
Outside contributionsnonenone (71/71 PRs from sinfin/)tie, both closed
Ruby / Rails declared2.6.6 / ~> 6.1.4 — both end-of-life3.3.3 / ~> 8.0 — currentfolio
Installs todayno — ~> 6.1.4 cannot resolve against folio master's ~> 8.0yesfolio
Security updates applied9 dependabot PRs unmerged, oldest 2022-08-31dependabot merged, 71 commitsfolio
Unmerged real workpetr/manual-closing, 616 lines, abandoned 2022-03-17branches churn and mergefolio

Which one can an adopter rely on?

folio: yes, with a named condition. auctify: no, as published.

folio is a live product. It ships every three weeks, nineteen people worked on it this year, it documents its breaking changes before you hit them, and its CI runs on every commit. Its weaknesses are ordinary large-codebase weaknesses — eight very long methods, 270 duplicated blocks in the console layer, 2 git tags for 36 releases, and no outside contributors. The condition: pin to a SHA and read UPGRADING.md before each bump. Tracking branch: master — which is what auctify's own Gemfile:14 does — means running seven weeks of unreleased code, and the tags do not exist to pin to instead.

auctify cannot be relied on as published, and the reason is not code quality — the code is the better-written of the two per line. It is that nobody and nothing verifies it. No CI has ever run. Its declared Ruby has been unpatched for three and a half years. It cannot bundle install against the folio it names as its own dependency. It has one maintainer, who has committed seven times in three years, all seven alone. Its own scaffold tests have raised NoMethodError since 2021 with nobody noticing.

The engineering-quality conclusion is narrower and more uncomfortable than "auctify is bad": the good code in auctify — the short methods, the zero duplication, the bid-ladder test suite — was produced by one careful person, and every mechanism that would preserve that quality after that person is gone is absent. No CI. No runbook. No second reviewer. No external contributor. No release since 2023. The quality is biographical, not institutional.


8. Scores

0 = absent or actively misleading. 10 = what a well-run commercial project achieves.

dimensionauctifyfoliowhy
Test strength47auctify: 0.35 assertions/line and a genuinely excellent bid_appender_test.rb (:379 "ultimate bidding test") pull it up; 16 dead scaffold tests, one file that raises on line 1 (test/system/auctify/sales_test.rb:12), 9 skips, and zero concurrency tests on money-handling code with no bid-path lock pull it back down. folio: 1 701 tests, parallelised, VCR with secret scrubbing, 50–88 % file coverage by area; loses points for helpers at 19 %, lib at 29 %, and no concurrency tests.
Code health76auctify: no method over 25 lines in 188 methods, effectively zero duplication, clean four-case dispatch in bids_appender.rb:179-214. Marked down for the dual-source-of-truth current_winner (auction.rb:25 vs :202), the non-rolling-back fail! path (auction.rb:161 + service_base.rb:65), and the unvalidated Range ladder (minimal_bids_ladder_type.rb:52-54). folio: 8 methods over 100 lines, 805-line NodeBuilder, 270 duplicated blocks — normal for scale, not excellent.
Documentation39auctify: the README covers configuration and auction rules honestly, and doc/ has state diagrams — but there is no UPGRADING.md, no prose docs, no runbook, ## Features required mixes built with wished-for, and the one end-to-end example raises an exception at README.md:210. folio: 29 docs files / 5 991 lines, UPGRADING.md with "Action required", ROADMAP.md, AGENTS.md, 21 .skills/ directories.
Release discipline27auctify: 3 tags for 8 CHANGELOG versions, inconsistent v prefix, a dependency removal shipped on master under an unchanged VERSION, no release in 24 months. The two #### BREAKING entries that do exist are well written — that is the 2. folio: 36 releases in Keep-a-Changelog format, ~3-week cadence, breaking changes documented before you hit them, a rejected optimisation documented with benchmarks; held to 7 by 2 git tags for 36 releases and 7 weeks of unreleased master.
Project health18auctify: no CI in its entire history, 0 commits in 12 months, bus factor 1 since 2022, 9 unmerged security bumps (oldest 2022-08-31), 616 lines of tested live-auctioneer work abandoned on petr/manual-closing, EOL Ruby and Rails, and it does not install. The 1 is for the fact that the last real commits were competent and tested. folio: 672 commits and 19 humans in 12 months, Jenkins, dependabot merged; held to 8 by 77 % single-author concentration and zero outside contributors.

Verdict

folio is a mature project. auctify is not.

folio has every institutional marker maturity requires: continuous integration, a release train running at three-week cadence for years, breaking changes documented before the consumer meets them, 5 991 lines of documentation that would let a stranger become productive, and nineteen people who touched the code this year. It is not flawless — 77 % of commits come from one person, no outside contributor has ever sent a pull request, two git tags stand for thirty-six releases, and the code has the long methods and copy-paste you would expect in 45 000 lines of CMS. But those are the problems of a functioning engineering organisation, and the adopter can manage them by pinning to a SHA and reading UPGRADING.md.

auctify is a well-written 2 500-line domain engine that has been left alone. Judged purely on the code it is the better-crafted of the two: not one method exceeds 25 lines, there is effectively no duplication, and bid_appender_test.rb pins down the bidding rules that actually decide who pays millions of CZK with 625 lines of table-driven, state-verified tests. That is real quality and it deserves to be said plainly. But maturity is not a property of code; it is a property of the process around code, and around auctify there is none. No continuous integration has ever existed in this repository — a fact provable by git log --all over every CI filename, and demonstrated by a NoMethodError that has sat in test/system/auctify/sales_test.rb:12 since 2021. It declares a Ruby that lost security support in March 2022 and a Rails that lost it in October 2024. It cannot bundle install against the folio it names in its own Gemfile. Nine security updates have been open and ignored for up to three years. Its most valuable unmerged feature — manual closing for a live auctioneer, 616 lines with 291 lines of tests — has been abandoned since March 2022. It has had one contributor since 2023 and no commits at all in the last twelve months. For a system that decides who wins a lot at a house where hammer prices reach millions of CZK, the absence of any automated verification on the bid path, combined with the absence of any lock on that same path and any test for concurrent bidding, is the finding the board should hear: the correctness of the auction is guaranteed today by one person's memory, and by nothing else.

The practical reading for ACB: keep folio and treat it as a supported dependency. Treat auctify as inherited source code that you now own, not as a maintained library — and resolve first, before anything else, the question in §3.4: find out which auctify actually runs in Auctea production, because it provably is not this one.


Prepared from local read-only clones. auctify a1b60f0c2b5089b8e5356e93de5dfd559fdbf611 (2024-11-15), folio 71084c978cbe9c65e6ce36a9ae4dcf841bc7693e (2026-09-14). No Ruby interpreter was available, so RuboCop was not run; every figure above is reproducible with git, grep, find and wc.