Taipei Forcing Club

Computer science and contract bridge

Why AI companies pulp the books they buy

Anthropic bought millions of print books, cut off their bindings, scanned the pages, and threw the paper away. In Bartz v. Anthropic, a federal judge ruled that this was fair use. He rejected the same defense for the company’s library of pirated books, and those claims ended in a $1.5 billion settlement, approved in July 2026.

The money made the headlines. I keep coming back to the paper. Destroying it helped Anthropic’s case. Keeping the books for someone else to read would have meant defending a shakier one. Why should copyright give anyone a reason to prefer the bin?

I spent an evening arguing this with Claude. I came in thinking it was a money problem. I left thinking it was both, and that only Congress can fix the legal half.

The law does not say “destroy the book”

Buying a book gives you an object you can resell, lend, or give away under first sale. It does not let you scan it. Scanning is a reproduction, so you need permission or an exception such as fair use. There is no statutory right to turn a book you own into an ebook.

Fair use is a four-factor balancing test, decided after the fact by a judge. In Bartz, Judge William Alsup leaned on the fact that each digital copy replaced a destroyed print copy and never left the company. He did not order the books destroyed, and he did not rule that keeping them would have been illegal. But destruction was a fact he relied on again and again, and a company reading the ruling now has a reason to pulp its originals even when it could scan them intact.

The Internet Archive’s 2024 defeat is a different case. It lent full digital copies to the public without permission, in competition with licensed ebooks. It does not say that keeping a paper original next to a private scan is infringement. It does show how little patience courts have once copies start leaving the building.

The money half

This is where I had to concede. Loose pages go through a sheet feeder. An intact book needs its pages turned. At millions of books, that gap is the whole budget, and no exception in the statute makes a page-turning scanner cheaper. Some companies would cut the bindings regardless, and I can’t claim a better law would have saved Anthropic’s books.

What a better law can do is give whoever keeps the books intact a clean path to donating them. They would still need a willing library and someone to pay for sorting and shipping. Those costs are real. Legal doubt about keeping the scan needn’t be one more.

The argument I dropped

My first draft framed this as books lost to humanity. Claude talked me out of leading with that, and I think rightly. The books in a bulk purchase are mostly used copies of mass-printed titles. Pulping one leaves thousands of siblings, and a sharp reader says “so what, it’s a used copy of a bestseller” and closes the tab.

So the claim is narrower. Pulping a readable book does not erase the work, but it does leave one fewer copy for someone to read, and that is enough to ask what the destruction was for. Rare editions and annotated copies exist in any bulk lot, but the argument shouldn’t hinge on proving one went through the scanner. An ordinary readable book is worth passing on too.

The redundant waste

Now picture several companies following the same recipe. Each buys the same book, cuts it apart, and scans it. Several destroyed books, several files of the same text. The fair-use ruling gives none of them a right to copy another’s library, so each must build its own.

None of that is technically necessary. A rights holder can license one digital source to many customers, and most of these titles already exist as ebooks. Agreeing on rights, prices, and access rules is a bigger project than the exception I’m asking for below. It still beats feeding another copy of the same book through another scanner.

What I am asking

If you are American, ask Congress for a format-shifting exception: the owner of a lawfully acquired book may scan it for their own use, or for an organization’s internal library, and may then donate the original without deleting the scan. Nothing more. No right to distribute or lend the files, and no ruling on what further uses, AI training included, are allowed.

There is a serious objection. If each successive owner scans the book and passes it on, one physical copy could support many retained digital copies. Authors and publishers are right to worry about that, and a real bill would have to address repeated circulation, and whether commercial libraries need different conditions or compensation. Calling the change small doesn’t make those questions go away.

Congress already writes copying exceptions this specific. Section 108 lets libraries and archives make copies under detailed conditions. It is not a ready answer for private owners or AI companies, but it proves we can do better than “argue fair use and see who wins.”

I don’t have a finished bill. I have a test for one. When someone has paid for a book and scanned it, keeping that book available for another reader should never leave them worse off than throwing it away.

The underused 1♦ response

Recently, I have been working on 5-card major bidding systems:

These systems somehow work on the same problem. As Richard Granville puts it, “the 1♦ response to 1♣ is severely underused.” Each system hands 1♦ a new job, and the job decides how strong 1♣ can be.

Bar chart of the strength of 1♣: Muscat Walsh 11--20, Watermelon Dutch
Doubleton 11--23, Strawberry Polish Club 11 with no upper
limit

Transfer Walsh rearranges responses to a natural 1♣—1♦ for hearts, 1♥ for spades, and 1♠ for hands without a 4-card major. The 1♠ response needs some points (about 4+) so that an 18–20 opener can bid beyond 1NT and land on 2NT. The ceiling of 1♣ is roughly the same as in plain 2/1 because we still pass 3-counts, especially those without a 4-card major.

Polish Club takes the most radical way. The 1♣ opening is home to all strong hands, where 1♣ - 1♦ serves as a negative relay. The 2♣ opening is freed up for preemptive uses.

Dutch Doubleton is a lawyer’s Polish Club with a quasi-natural 1♣. Since 1♣ can hold 21–23, responder passes only a 0–5 hand with 4–5 clubs, a hand that cannot survive 1NT anyway. Every other weak hand goes through 1♣ - 1♦, a negative relay to park it. Otherwise a 3-count would respond 1♠ and push an 18–20 opener past 1NT.

The real lever is how often responder passes. The fewer hands pass 1♣, the more 1♣ can hold, and vice versa. Walsh still passes weak hands, so 1♣ keeps a natural-ish ceiling. Dutch Doubleton passes only a sliver of hands, with 1♦ as the parking spot for the rest, so 1♣ reaches 23. Polish Club never passes 1♣, so 1♣ has no ceiling at all and contains all strong hands.

How I ended up in CORE-MATH

CORE-MATH is the project that made correctly rounded math functions look practical instead of academic. It is also the reason metallic-rs exists: I wanted a correctly rounded math library in Rust, and I wanted it to be faster than the reference. Rust has no floating-point environment and only the default rounding mode, so I have one trade-off CORE-MATH does not: I skip the other three rounding modes and spend the savings on speed. CORE-MATH was the rival, and the oracle, and the source of worst cases. It was not supposed to become my job.

The gap

After f32 and f64 were done, I started on f128. That is where the plan broke. CORE-MATH has 41 binary64 functions and 15 binary128 ones. Square root, cube root, exponentials, one logarithm, and the inverse trigonometric functions were there. Sine and cosine were not: the binary128/sin directory had held a hard-to-round corpus since February, with no C file next to it. No tangent, no log2q, log10q, log1pq, or powq either.

Testing a correctly rounded function without an oracle is a different activity from testing one with an oracle. I ended up generating my own corpora with MPFR and a few Diophantine tricks, such as continued fractions of 2/π per binade for sine and cosine, and checking against them in CI. By the end of August every q function in metallic-rs that CORE-MATH also had was within a few percent of it, and the rest had nothing to be compared with except libquadmath, which is faithful rather than correctly rounded.

At that point the lazy conclusion was obvious. If the reference is missing the function, contribute the function to the reference.

The drama

On 3 September I mailed the CORE-MATH list a work-in-progress sinq and cosq, ported from metallic-rs to C and extended to all four rounding modes. The list bounced it: posting is for subscribers only, and I had never subscribed. I resent it the next day with the usual disclosure that I had used Claude Code while porting and testing.

Paul Zimmermann’s reply opened with the sentence I had been half expecting:

this is interesting. However we have not decided yet if we include AI-generated contributions.

followed by a question I could not dodge:

how much of the code was generated by you and not by AI?

Honestly, about all of it since April 2026. The methods are mine. I wrote them up in How to program mathematical functions back in 2021, turned that post into an agent skill this year, and have been doing what I can only call vibe optimization since. Everything else in the same reply was ordinary review: Taylor series where a minimax polynomial would do, a bound of π/512 < 2−7.35 that was wrong in the second decimal, and timings on an i7-8700 showing my sine at about 250 cycles while expq took 60.

So I did what one does. Version 2 swapped in a degree-four minimax from rminimax and fixed the bound to 2−7.34. Version 3 evaluated the accurate path in tiers of 128, 256, and 384 bits and halved its cost. Then came the least flattering exchange of the thread: “I don’t see the v3 in your core-math-c repo.” It was sitting unpushed on my development machine.

Paul also asked whether I would maintain the code if it were merged and problems turned up later. That question is the actual entrance exam. I said yes, and I added that I had a bigger plan: to close the gap between binary128 and binary64, one or two functions at a time. He opened an account for me on gitlab.inria.fr with the maintainer role, and sinq and cosq landed on master on 19 September. The announcement quotes 154.5 cycles per call for sinq against 872 for libquadmath, and 156 against 995.5 for cosq.

Where it stands

That was one function pair out of the two dozen or so still missing in binary128. I am now working on sincosq, tanq, the other logarithms, and whatever comes next, one or two at a time. Most of them already run in metallic-rs, so porting them is mostly a matter of adding three rounding modes and surviving review.

Taiwan's first Ace on IntoBridge

I used to boycott IntoBridge because Taiwan was not in its country selector. A country dropdown looks trivial until your country is not in it. So I closed the tab and played bridge elsewhere.

Recently I gave it another try, and there it was: Taiwan, flag and all. So I stayed. Two weeks and 549 ranked games later, I became Taiwan’s first Ace, ranked #1 in Taiwan and #338 in the world.

My IntoBridge dashboard, showing rank #338, the Taiwanese flag, and a
month-long graph: flat at 7♦ until August 10, then a climb to Ace in two
weeks

Naturally I wanted to know when Taiwan was added. I found no announcement, so I asked Codex to investigate. I even disclosed my ulterior motive up front:

Humans are not completely rational and would waste tokens on showing off.

The investigation went considerably further than the brag required. Codex compared archived copies of the country list in the site’s JavaScript. The old list had 195 options; the current one has 197. The only additions are Taiwan and Hong Kong. Their flag files (tw.svg, hk.svg) carry exactly the same server timestamp, 25 February 2026 15:34:12 UTC, while most other flags date back to 2022. That is not an official release date, but it strongly suggests both were added together that day.

The Taiwan leaderboard on IntoBridge, with jdh8 at
#1

Was it rational to spend tokens on this? No. But the flag is there, my name sits on top of it, and now the date is on the record. Tokens well spent. 🇹🇼♣️

polish.club

I just moved Strawberry Polish Club to polish.club. One site for one bidding system feels cleaner than a site for multiple systems. My other bidding systems are also on their own sites:

Strawberry adjunct

1m-(2M)-
3♠ = FG, 6+oM
3♥ = FG, 5=oM
3♥-3♠ = ask for stopper

I came up with this convention when I was writing system notes for my Polish Club.

Rebrand with a simple yet unique icon

A white club on a circle filled with red and blue check.  The colors symbolize my bidding systems, a Polish Club and a Blue Club.

IPv6 problem on Poker Chase

I encountered asset loading problem when playing Poker Chase in IPv6 networks. Asset loading fails from time to time. Sometimes I have to switch to mobile app to continue playing.

I found a solution on Firefox. Go to about:config and set network.http.fast-fallback-to-IPv4 to false. I am still not sure what causes the problem. I suspect it is related to the CDN used by the game.

Notifications when a Discord bot goes down

I want Natsuki to notify me when she goes down. This task is harder than I have thought. How can I count on a failing bot to send a message? I cannot trust the hosting provider, either. They may be the reason why the bot goes down. I have to rely on a third party.

Pair the bot with a web server

This sounds like a blasphemy against the single-responsibility principle. There are practical reasons to do this.

  1. Hosting providers may put idle bots to sleep. Some providers are so web-centric that they require serving a web page to keep the bot alive.
  2. HTTP(S) serves as a simple and reliable endpoint. Everyone online can ping it.

Below is how I pair Natsuki with an Axum server. The Axum server only serves 204 No Content at root.

#[shuttle_runtime::async_trait]
impl Service for Natsuki {
    async fn bind(mut self, addr: std::net::SocketAddr) -> Result<(), Error> {
        use axum::{response::NoContent, routing::get, Router};
        let router = Router::new().route("/", get(|| async { NoContent }));

        let (axum, serenity) = futures::join!(
            shuttle_axum::AxumService(router).bind(addr),
            self.0.start_autosharded(),
        );
        serenity.map_err(CustomError::new)?;
        axum
    }
}

Monitor the web server

The other part of the plan is a watchdog that periodically pings the web server. The watchdog informs me through a Discord channel whenever a ping fails. Since the web server is available worldwide, the watchdog can live anywhere such as GitHub Actions. Given the importance of GitHub, it is much more reliable than self-hosting.

My watchdog is open-source like Natsuki. It takes a Discord webhook to send messages. I make it an environment variable because the webhook contains sensitive data. If the webhook is leaked, other people can send arbitrary messages to the channel.

use dotenv::var;
use serde_json::json;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = reqwest::Client::new();
    let ping = || async {
        const ENDPOINT: &str = "https://natsuki-oehk.shuttle.app/";
        anyhow::ensure!(client.head(ENDPOINT).send().await?.status().is_success());
        Ok(())
    };
    if let Err(error) = ping().await {
        client
            .post(var("WEBHOOK")?)
            .json(&json!({ "content": error.to_string() }))
            .send()
            .await?;
    }
    Ok(())
}

Transfer O’ish Relay

An o’ish relay is a forcing bid that is natural or has extra strength. Famous examples are Birthright (Kokish Relay, 2♣-2♦-2♥) and Polish Club (1♣), after which I name this treatment. I advocate playing a transfer o’ish relay instead, the relay that either transfers to the next suit or has extra strength. This method renders the negative response nonforcing and allows more descriptive rebids.

Basic structure

  • TOR = transfer or extra
    • (R) = NF negative, accepts the transfer
  • TOR + 1 = transfer back to the suit of TOR

Examples

Birthright (Kokish Relay)

A classical problem with the strong 2♣ opening is whether the auction is game-forcing. One solution is the Kokish Relay, where opener bids 2♥ to show hearts or a unilaterally game-forcing hand. This treatment potentially wastes space because opener hardly passes 2♥-2♠.

The rebids after 2♣-2♦ I propose are:

2♥
5+ spades or FG
2♠
NF, 5+ hearts
2NT to 3♦
NAT NF

After 2♥, the default 2♠ fits any hand that responder would pass natural 2♠. Other responses are game-forcing.

After 2♠, responder passes with weak long spades. The 3♥ response is to play. Whether 2NT is game-forcing is up to partnership agreement.

Polish Club with swapped minor suits

1♣
Polish Club with diamonds instead of clubs:
  1. Balanced 12–14 HCP
  2. 11–17 HCP, 5+ diamonds or 4441
  3. 18+ HCP, any
1♦
11–17 HCP, 5+ clubs or (441)4

It took me almost one year to accept this idea until I realized that 1♣-1X-2♣ should retransfer to diamonds. The strength difference between natural diamonds (11+ HCP) and clubs (18+ HCP) is so large that opener should transfer twice. The odwrotka sequence of 1♣-1X-2♦ remains a power reverse.

Drawbacks

Responding to TOR + 1 is more complicated than TOR. You can no longer park at the negative relay (TOR + 1). Accepting the transfer (TOR + 5) becomes default and often meets a pass. Take 1♦ (showing clubs) for example:

  • Pass: weak long diamonds
  • 1NT: constructive natural, effectively 4+ diamonds
  • 2♣: almost a signoff with 3–4 clubs
  • 2♥♠: game try with 3+ clubs
  • 2NT: preemptive raise with 4+ clubs
  • 3♣: mixed raise with 4+ clubs