Skip to content

The .rejected File: When Human Decisions Beat Derived Status

Adityo Guni Waluyo

My research registry only had derived statuses, until a .rejected file gave human decisions a seat in the precedence chain.

TL;DR

A dismissed research topic kept reappearing in the registry scan, so the author added a .rejected marker file that humans write and tools only read, storing the reason in a rejected_reason field. The article compares this to precedence chains in rustup, pyenv, nvm, and git. Key lessons: markers are human decisions, and overrides must be visible, never silent.

This morning I re-ran the scan for my research topic registry, and a topic I had already dismissed kept showing up in the candidate list. My first guess was the classic one: the JSON generator must have misparsed something. I checked. Nothing was broken. The registry only had derived statuses (draft, and skilled from a .converted marker file) and no slot at all for "I rejected this topic". Machine derivation was the only source of truth, and my own decisions had no seat at the table.

The fix is one small file: a .rejected file inside the topic folder. Free-form content, written by a human, and the tool never writes it itself. When the scan finds it, the topic's status becomes rejected, the content lands in a rejected_reason field, and the show output displays it. I deliberately made it non-retroactive: older topics stay draft. Backfilling a dozen old folders with decisions whose context has already moved on is make-work.

Reading it is straightforward too. The show output now has one more status, sorted last, and the notes column for a rejected topic no longer shows old notes but the rejection reason I typed myself. The counter script I use for conversion signals also skips rejected topics automatically, so the signal numbers don't get polluted by topics that are already dead.

A precedence chain, written down

Turning human decisions into data that the tool respects is not a weird idea. Mature tooling has done this for years. rustup documents its toolchain precedence explicitly, five levels deep: the command-line shorthand, an environment variable, a directory override, the rust-toolchain.toml file, then the default toolchain. First match wins [1].

pyenv follows the same shape for picking a Python version: the PYENV_VERSION variable beats a .python-version file in the current directory, which beats one found in a parent directory, down to the global version file [2]. nvm is close behind; when no version is passed on the command line, it reads .nvmrc, and the lookup walks up through parent directories [3].

Even git plays. When deciding whether a path is ignored, git checks patterns from several sources with a documented precedence order, and within one level, the last matching pattern wins [4]. My registry's rule is the compressed version: .rejected beats .converted, and both beat an empty draft. The difference is that I have no env vars and no parent walk, so the chain is short.

A marker is a decision, not an output

There is a temptation to let the tool write marker files automatically. The nvm maintainer was asked almost exactly this: should nvm use also write .nvmrc by default. The answer was no, with a sharp reason: one developer using nvm doesn't mean the rest of the team should be locked to that version, and the repo shouldn't grow an .nvmrc unless everyone expects it [5]. A marker file is an expression of a decision. Once a machine writes it, it isn't a decision anymore, just a side effect.

In my registry the consequence is concrete. A .rejected file only ever appears because I typed it, with a date and a free-form reason. The format is nothing fancy: a date, a space, then whatever text I want. The old .converted convention stored a date plus a file count; I deliberately didn't copy the file count, because what matters here isn't data, it's the decision. The tool reads it, never writes it. The rejected_reason shows up in the output too, so when I forget weeks later why a topic was rejected, the reason sits where the decision was made.

An override that slips through silently is a time bomb

The second lesson came from rustup. Ratatui's CI installed a beta toolchain through an action, while the repo had a rust-toolchain.toml asking for something else. The file won, and it won quietly: the only trace was an info-level log note that is easy to scroll past [6]. They asked for the message to be raised to a warning, so the override is visible before it eats someone's afternoon.

That is where I locked the rules in: overrides are fine, silence is not. When show reports a rejected status, it doesn't just skip the topic, it prints the reason in the notes column. The display order is settled too: rejected shows up at the very bottom, after draft, skilled, and archived. A rare status reads best when its place is consistent, not wedged in the middle. If a .rejected file ever covers a .converted status that still matters, I want that conflict on the surface, not discovered years later.

Machines propose. Humans veto. And the veto has to be visible, not a whisper lost between lines of code.

Sources

[1] https://rust-lang.github.io/rustup/overrides.html

[2] https://github.com/pyenv/pyenv/blob/master/README.md

[3] https://github.com/nvm-sh/nvm/blob/master/README.md

[4] https://git-scm.com/docs/gitignore

[5] https://github.com/nvm-sh/nvm/issues/2849

[6] https://github.com/rust-lang/rustup/issues/4504

Related articles