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.
| auctify | folio | |
|---|---|---|
| HEAD reviewed | a1b60f0 2024-11-15 | 71084c97 2026-09-14 |
| Ruby (app + lib) | 2 503 lines | 47 957 lines |
| Ruby (whole repo incl. test dummy) | 8 946 lines | 101 807 lines |
| Test files | 39 (31 engine, 8 dummy) | 466 |
| Test cases | 180 engine / 199 total | 1 701 |
| Assertion calls | 885 engine / 922 total | 5 133 |
| Commits | 227 | 6 749 |
| Contributors all-time | 3 humans + dependabot | 33 email identities (~25 humans) + dependabot |
| CI | none, ever | Jenkins |
| Last commit | 22 months before this review | same 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.
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.
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.
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.
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:
| Test | File:line |
|---|---|
| autobid / proxy-bid escalation | test/services/auctify/bid_appender_test.rb:77 |
| reserve price not met | test/services/auctify/bid_appender_test.rb:155 |
| bid-step ladder applied by price band | test/services/auctify/bid_appender_test.rb:196 |
| two identical limits — earlier bid wins | test/services/auctify/bid_appender_test.rb:361 |
| cannot overbid yourself | test/services/auctify/bid_appender_test.rb:285 |
| can only raise your own max | test/services/auctify/bid_appender_test.rb:302 |
| price never pushed above the limit | test/services/auctify/bid_appender_test.rb:463 |
| fat-finger ceiling per bid | test/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.
| Gap | Evidence | Severity | |||
|---|---|---|---|---|---|
| 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-derivation | cancel!/recalculate_bidding! appear on exactly one line of test: test/models/auctify/sale/auction_test.rb:145 | HIGH | |||
AutobidFillerJob | app/jobs/auctify/autobid_filler_job.rb has no test file at all | MEDIUM | |||
Auctify::Sale::AuctionCallbacks | app/concerns/auctify/sale/auction_callbacks.rb — no direct test; it is the documented extension point (README.md:141-142) | MEDIUM | |||
9 tests are skipped | test/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, :72 | MEDIUM |
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.
skip at test/concerns/auctify/item_test.rb:12 deserves quoting — SUSPECTED, HIGHskip "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.
466 test files, 1 701 test cases, 5 133 assertion calls. Coverage is broad and follows the code:
| app area | production files | test files | ratio |
|---|---|---|---|
| components | 123 | 108 | 88 % |
| cells | 91 | 65 | 71 % |
| controllers | 90 | 59 | 66 % |
| jobs | 37 | 23 | 62 % |
| mailers | 4 | 3 | 75 % |
| models | 124 | 62 | 50 % |
| services | 6 | 3 | 50 % |
| lib | 52 | 15 | 29 % |
| serializers | 7 | 2 | 29 % |
| helpers | 27 | 5 | 19 % |
Three things folio's test infrastructure does that auctify's does not:
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.
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.
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.
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:
Jenkinsfile:20 shells out to /var/lib/jenkins/test_rails.sh, a file on the Jenkins controller. Nothing in this repo states which suites run, whether system tests run, whether the build fails on RuboCop, or what database it uses. To an outside adopter the CI is a black box: you can see that something ran and that Slack went green. SUSPECTED that system tests are excluded (they need Chrome and Selenium); confirm by reading /var/lib/jenkins/test_rails.sh on Sinfin's Jenkins, which an adopter cannot do.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.
Measured by a brace-matching line counter over app/ and lib/.
auctify — this is the best part of the repository.
| bucket | methods |
|---|---|
| 1–10 lines | 158 |
| 11–25 lines | 26 |
| 26–50 lines | 0 |
| 51+ lines | 0 |
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:
| lines | file:line | class |
|---|---|---|
| 374 | app/models/auctify/sale/auction.rb:5 | Auctify::Sale::Auction |
| 249 | app/services/auctify/bids_appender.rb:4 | Auctify::BidsAppender |
| 159 | app/models/auctify/sale/base.rb:5 | Auctify::Sale::Base |
| 110 | app/models/auctify/sales_pack.rb:4 | Auctify::SalesPack |
| 84 | app/controllers/auctify/api/v1/auctions_controller.rb:6 | Api::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.
| bucket | methods |
|---|---|
| 1–10 lines | 2 325 |
| 11–25 lines | 752 |
| 26–50 lines | 193 |
| 51–100 lines | 58 |
| 100+ lines | 8 |
3 336 methods; 7.5 % exceed 25 lines. The eight worst:
| lines | file:line |
|---|---|
| 207 | app/lib/folio/tiptap/node_builder.rb:279 setup_structure_for_folio_attachment |
| 183 | app/jobs/folio/generate_thumbnail_job.rb:74 make_thumb |
| 159 | app/controllers/folio/console/api/autocompletes_controller.rb:257 react_select |
| 143 | app/helpers/folio/console/react_helper.rb:138 react_ordered_multiselect |
| 125 | app/components/folio/console/links/modal/list_component.rb:14 records_data |
| 108 | app/controllers/concerns/folio/console/api/file_controller_base.rb:250 update_thumbnails_crop |
| 107 | app/components/folio/console/ui/image_component.rb:54 set_data |
| 103 | app/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.
Measured as identical 8-line windows (comments and blank lines stripped), app/ only, test dummies excluded.
state :x, color: "y" declarations shared between app/models/auctify/sale/auction.rb:40-46 and app/models/auctify/sale/retail.rb:13-19 — legitimately parallel state machines. Effectively zero copy-paste. This is very good and unusual.app/. The single worst hotspot is app/controllers/folio/console/api/autocompletes_controller.rb (487 lines), where one 8-line block recurs five times — at :42, :161, :213, :300 and one more — and seven further blocks recur four times each at :44, :46, :47, :48, :49, :50, :51 and their echoes at :163-170, :302-309, :361-368. That is a copy-pasted autocomplete handler repeated four to five times. MEDIUM: it is console-only code, it does not touch bidding, and it is the kind of duplication that produces bugs fixed in three places out of four.These are engineering-quality findings. They are reported here because they are defects a maintainer inherits, not because this is the security report.
current_winner has two sources of truth — PROVED, HIGHapp/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:
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.
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.
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.
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.
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.
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.
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.
README.md:181, :190, :198 all read aucion.bid! (missing t). Nobody ever ran the documented example.
lib/auctify/version.rb:4 → VERSION = "1.1.5". Three git tags exist:
| tag | date | commit |
|---|---|---|
1.0.0 | 2021-06-25 | cc7d014 |
1.1.0 | 2022-01-21 | a96750e |
v1.1.5 | 2023-09-05 | 782e4d1 |
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.
The CHANGELOG does mark them, and clearly. Credit for this:
| version | breaking change | CHANGELOG line |
|---|---|---|
| 1.0.3 (2021-09-17) | auction_prolonging_limit renamed to auction_prolonging_limit_in_seconds | marked #### BREAKING |
| 1.1.0 (2022-01-21) | BiddingCloserJob no longer self-enqueues; the host app must now run EnsureAuctionsClosingJob periodically | marked #### 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.
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.
auctify cannot be installed today. PROVED, CRITICAL.
Three declarations, all in the repository, mutually unsatisfiable:
| file:line | declares |
|---|---|
auctify.gemspec:31 | spec.add_dependency "rails", "~> 6.1.4" |
auctify/.ruby-version:1 | 2.6.6 |
auctify/Gemfile:14 | gem "folio", github: "sinfin/folio", branch: "master" |
folio/folio.gemspec:68 | s.add_dependency "rails", "~> 8.0" |
folio/.ruby-version:1 | 3.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:
rests on "the tests pass" is unverifiable by an outside party today.
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.
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.
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.
| artifact | state |
|---|---|
README.md | 245 lines |
doc/ | two PNG state diagrams only — auction_state_diagram.png, retail_sale_state_diagram.png. No prose docs at all. |
UPGRADING.md | does not exist |
CHANGELOG.md | 8 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:
README.md:45-76 states the auction rules in business language: prolongation on a late bid (:47), the bid-step ladder with a real ACB-shaped example running to (2_000_000..) => 100_000 (:48-60), "first bid does not increase the price" (:61), no self-overbidding (:62), both bid types — proxy with a maximum and direct (:72-74).README.md:123-138 documents every configuration option with its default.README.md:221 honestly points the reader at the tests for the real behaviour: "Look into tests test/models/auctify/sale/auction_bidding_test.rb and test/services/auctify/bid_appender_test.rb for more info about bidding process."README.md:225-237 documents the Prometheus/Yabeda metrics and warns about the subtlety that diff_in_closing_time_seconds is written in the background-job process, not the web process.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:
README.md:198 — bidder2 bids price: 149, max_price: 155 against bidder1's limit of 150. solve_limits_fight (app/services/auctify/bids_appender.rb:188-196) raises bidder1 to 150 and sets bidder2 to 151.README.md:200 — auction.current_price # => 151.0. Correct.README.md:210 — auction.sold_in_auction!(buyer: bidder2, price: 149, sold_at: …).buyer_vs_bidding_consistence (app/models/auctify/sale/auction.rb:274-279) requires sold_price == bidding_result.won_price. 149 ≠ 151 → error :sold_price_is_not_from_bidding.if: :valid? (app/models/auctify/sale/auction.rb:90), so the bang method raises AASM::InvalidTransition.README.md:212 — auction.auctioned_successfully? # => true. False. The auction is stuck in bidding_ended.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 question | Documented? |
|---|---|
| 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.
| artifact | state |
|---|---|
docs/ | 29 files, 5 991 lines |
UPGRADING.md | 10 179 bytes, per-version, with "Action required" |
README.md | 7 089 bytes — setup, system requirements, image tooling, Yarn, quick install |
ROADMAP.md | 11 492 bytes |
AGENTS.md | 8 874 bytes |
.skills/ | 21 topic directories for AI-assisted development |
CHANGELOG.md | 67 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.
| year | auctify | folio |
|---|---|---|
| 2017 | — | 269 |
| 2018 | — | 671 |
| 2019 | — | 621 |
| 2020 | — | 561 |
| 2021 | 197 | 688 |
| 2022 | 23 | 599 |
| 2023 | 3 | 807 |
| 2024 | 4 | 1 107 |
| 2025 | 0 | 1 051 |
| 2026 (to 09-15) | 0 | 375 |
| total | 227 | 6 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.
auctify — bus factor 1, and it has been 1 since 2022.
foton <[email protected]> and Petr Mlčoch <[email protected]> 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'):
| display names | commits | share | lines added | lines deleted | |
|---|---|---|---|---|---|
[email protected] | foton (169) + Petr Mlčoch (11) | 180 | 79 % | 14 830 | 3 358 |
[email protected] | Petr Mlčoch — same human | 1 | 0.4 % | 98 | 7 |
[email protected] | Petr Marek | 40 | 18 % | 1 915 | 811 |
[email protected] | dedekm | 3 | 1 % | 5 | 2 |
| dependabot[bot] | — | 3 | 1 % | 3 | 3 |
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:
| year | authors |
|---|---|
| 2021 | foton 148, Petr Marek 40, dependabot 3, dedekm 3, Mlčoch 3 |
| 2022 | foton 21, Mlčoch 2 |
| 2023 | Mlčoch 3 — one human |
| 2024 | Mlč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:
| author | commits | share |
|---|---|---|
| Petr Marek | 5 197 | 77 % |
| dedekm / Martin Dedek (3 identities) | 461 | 7 % |
| Petr Mlčoch | 228 | 3 % |
| Filip Ornstein | 173 | 3 % |
| Vlada (Vladislav Trefil) | 149 | 2 % |
| David Duben | 97 | 1 % |
| Lipan Martin | 73 | 1 % |
| dependabot[bot] | 71 | 1 % |
| Tomáš Cabák | 68 | 1 % |
| Jiří Motejl (2 identities) | 74 | 1 % |
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.
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.
| auctify | folio | |
|---|---|---|
| tags | 3 (1.0.0, 1.1.0, v1.1.5) | 2 (v7.4.1, v7.6.2) |
| CHANGELOG releases | 8 | 36 |
| first → last release | 2021-06-25 → 2023-09-05 | 2024-12-11 → 2026-07-23 (recent window) |
| median gap between releases | ~7 months | ~3 weeks |
| time since last release at review | 24 months | 7.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.
Issue trackers are on GitHub and cannot be read from a local clone. Two things are visible in the repository:
c8f50ad (2024-11-05) says "close #450" — auctify's issue/PR counter had reached at least 450 against 227 commits. Dependabot inflates that number.master — i.e. opened, never looked at, left to rot. Full list in §6.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.
auctify's last seven commits, examined individually.
Substantive. Four of seven changed real behaviour, with tests. PROVED.
| commit | date | files | what it did | verdict |
|---|---|---|---|---|
d992bc1 | 2023-09-05 | 12 (+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 |
782e4d1 | 2023-09-05 | 2 | "missing changed files" — Gemfile.lock + dummy schema | Housekeeping (fixup of the above) |
2c908ac | 2023-09-05 | 1 | "and changelog, of course" | Housekeeping |
d3a3e0b | 2024-11-05 | 2 | bids_per_minute Yabeda metric | Substantive (observability) |
c8f50ad | 2024-11-05 | 15 (+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 |
dbaf79c | 2024-11-15 | 1 | sale_counts_by_state metric | Substantive (observability) |
a1b60f0 | 2024-11-15 | 9 | Yabeda rework: drops the yabeda-prometheus runtime dependency (auctify.gemspec), removes lib/auctify/railtie.rb wiring, adds dummy-app ActiveJob config | Substantive 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.
Yes. One, and it is the feature an auction house needs most.
All 25 remote branches, with divergence from origin/master:
| branch | ahead | behind | last commit | subject |
|---|---|---|---|---|
petr/manual-closing | 21 | 20 | 2022-03-17 | add lock_bidding/unlock_bidding endpoints, authorize account |
petr/folio | 9 | 177 | 2021-04-23 | rm application_record ordered scope |
leaving_isolate_namespace | 6 | 184 | 2021-04-21 | added sale.number, removed sale.selling_price |
foton/auction_callbacks | 2 | 123 | 2021-05-05 | passing bid_appender to bid callbacks |
md/mailers | 2 | 123 | 2021-05-05 | add admin notifications to auction_mailer |
monitoring_tool | 2 | 13 | 2022-06-03 | added MonitoringRecordDownloader service |
prometheus_monitoring | 2 | 13 | 2022-06-07 | yabeda metrics verified and documented |
petr/bids-appender-sql | 1 | 36 | 2021-11-30 | reduce bids_appender sql query count by 1 |
petr/bids_appender_optim | 1 | 100 | 2021-05-11 | use ordered_applied_bids to optimize SQL |
petr/overbid-by-limit | 1 | 48 | 2021-09-07 | pass overbid_by_limit option to form cell |
petr/rails-6.1 | 1 | 172 | 2021-04-26 | bump rails |
update_folio_and_rails_61 | 1 | 55 | 2021-08-04 | update folio, rails to 6.1 |
dependabot/bundler/rack-2.2.6.3 | 1 | 7 | 2023-03-09 | Bump rack 2.2.3.1 → 2.2.6.3 |
dependabot/bundler/activesupport-6.1.7.2 | 1 | 7 | 2023-01-31 | Bump activesupport 6.1.6 → 6.1.7.2 |
dependabot/bundler/actionpack-6.1.7.2 | 1 | 7 | 2023-01-30 | Bump actionpack 6.1.6 → 6.1.7.2 |
dependabot/bundler/activerecord-6.1.7.2 | 1 | 7 | 2023-01-30 | Bump activerecord 6.1.6 → 6.1.7.2 |
dependabot/bundler/globalid-1.0.1 | 1 | 7 | 2023-01-19 | Bump globalid 1.0.0 → 1.0.1 |
dependabot/bundler/rails-html-sanitizer-1.4.4 | 1 | 7 | 2022-12-14 | Bump rails-html-sanitizer 1.4.2 → 1.4.4 |
dependabot/bundler/loofah-2.19.1 | 1 | 7 | 2022-12-13 | Bump loofah 2.18.0 → 2.19.1 |
dependabot/bundler/nokogiri-1.13.9 | 1 | 7 | 2022-10-21 | Bump nokogiri 1.13.6 → 1.13.9 |
dependabot/bundler/omniauth-1.9.2 | 1 | 8 | 2022-08-31 | Bump omniauth 1.9.1 → 1.9.2 |
manual-closing | 0 | 20 | 2022-02-04 | folio bump (superseded) |
shifting_of_sales_pack_time_frame | 0 | 50 | 2021-08-11 | merged |
master | 0 | 0 | 2024-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:
20220304111030_add_manual_closing.rb, 20220308101021_set_manual_closing_per_sale.rb, 20220308111330_add_aasm_state_sales_index.rb, 20220317064149_add_biddings_locked_to_sales.rbapp/models/auctify/sale/auction.rb +88, app/models/auctify/sale/base.rb +66app/controllers/auctify/api/v1/auctions_controller.rb +38 — lock_bidding / unlock_bidding endpoints, authorised to Folio::Accountauction_test.rb +144, api/v1/auctions_controller_test.rb +127, base_test.rb +20, ensure_auctions_closing_job_test.rb +22Selected 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.
| axis | auctify | folio | who wins |
|---|---|---|---|
| CI | none, ever; proven by git log --all over every CI filename | Jenkins, tests + Slack (Jenkinsfile:1-31) — though the test script lives outside the repo | folio, decisively |
| Test volume | 180 engine tests, 885 assertions | 1 701 tests, 5 133 assertions | folio |
| Test density | 0.35 assertions per production line | 0.11 | auctify |
| Test honesty | 16 of 180 are dead scaffold; one file raises NoMethodError on line 1 (test/system/auctify/sales_test.rb:12) | skips are feature flags, not abandonment | folio |
| Domain-level testing | genuinely strong — test/services/auctify/bid_appender_test.rb walks whole bidding wars with expected DB state | broad but mostly per-unit | auctify |
| Concurrency testing | zero | zero | tie, both fail — but only auctify handles money |
| Test parallelism | none | tuned, test/test_helper_base.rb:75-96 | folio |
| Method size | no method over 25 lines, 188 methods | 8 methods over 100 lines, worst 207 | auctify |
| Duplication | 6 blocks, all legitimate | 270 groups, 305 redundant occurrences | auctify |
| Largest class | 374 lines | 805 lines | auctify |
| README | 245 lines; worked example raises an exception (README.md:210) | 7 089 bytes, accurate setup | folio |
| docs/ | two PNGs, no prose | 29 files, 5 991 lines | folio, by everything |
| UPGRADING.md | none | per-version, with "Action required" and greps | folio |
| CHANGELOG | 8 entries / 227 commits; last one has no version heading | 36 versions, Keep-a-Changelog, documents rejected work with benchmarks | folio, decisively |
| Tags vs releases | 3 tags, 8 CHANGELOG versions; 5 versions untaggable | 2 tags, 36 CHANGELOG versions | both bad, folio worse on ratio, auctify worse on outcome |
| Release cadence | last release 24 months ago | ~3 weeks median | folio |
| Commits, last 12 months | 0 | 672 | folio |
| Bus factor | 1, and 1 since 2022 | 1 nominal / 2–3 effective, 19 humans active this year | folio |
| Outside contributions | none | none (71/71 PRs from sinfin/) | tie, both closed |
| Ruby / Rails declared | 2.6.6 / ~> 6.1.4 — both end-of-life | 3.3.3 / ~> 8.0 — current | folio |
| Installs today | no — ~> 6.1.4 cannot resolve against folio master's ~> 8.0 | yes | folio |
| Security updates applied | 9 dependabot PRs unmerged, oldest 2022-08-31 | dependabot merged, 71 commits | folio |
| Unmerged real work | petr/manual-closing, 616 lines, abandoned 2022-03-17 | branches churn and merge | folio |
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.
0 = absent or actively misleading. 10 = what a well-run commercial project achieves.
| dimension | auctify | folio | why |
|---|---|---|---|
| Test strength | 4 | 7 | auctify: 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 health | 7 | 6 | auctify: 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. |
| Documentation | 3 | 9 | auctify: 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 discipline | 2 | 7 | auctify: 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 health | 1 | 8 | auctify: 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. |
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.