featkpr
Private demo

Blog / 30 Sep 2026

Proving that one feature depends on another

A link between two features is a guess from code until a run shows it. Paired runs proved 27 of BookStack's links; the last one found a bug.

The legs of a paired run, one lane per kind of relation enables: Create a book enables Create a bookshelf (from book). leg 1: a fresh copy (no book made yet), then b alone (convert a book nobody made); refused, as it must: without a, b cannot happen. leg 2: a: Create a book (POST /books), then b on that book (POST /books/{bookSlug}/ convert-to-shelf); passes, as it must: after a, b works. disables: Delete the api token disables Update the api token. leg 1: a: Delete the token (on a token the setup made), then b on that token (update what a deleted); refused, and nothing changed: after a, b is refused and b's step leaves no trace. enables a Create a book → b Create a bookshelf (from book) leg 1 a fresh copy no book made yet b alone convert a booknobody made refused,as it must leg 2 a: Create a book POST /books b on that book POST /books/{bookSlug}/convert-to-shelf passes,as it must disables a Delete the api token → b Update the api token leg 1 a: Delete the token on a token thesetup made b on that token update whata deleted refused, andnothing changed The legs of a paired run, one lane per kind of relation enables: Create a book enables Create a bookshelf (from book). leg 1: a fresh copy (no book made yet), then b alone (convert a book nobody made); refused, as it must: without a, b cannot happen. leg 2: a: Create a book (POST /books), then b on that book (POST /books/{bookSlug}/ convert-to-shelf); passes, as it must: after a, b works. disables: Delete the api token disables Update the api token. leg 1: a: Delete the token (on a token the setup made), then b on that token (update what a deleted); refused, and nothing changed: after a, b is refused and b's step leaves no trace. enables a Create a book b Create a bookshelf (from book) leg 1 a fresh copy no book made yet b alone convert a booknobody made refused, as it must leg 2 a: Create a book POST /books b on that book POST /books/{bookSlug}/convert-to-shelf passes, as it must disables a Delete the api token b Update the api token leg 1 a: Delete the token on a token thesetup made b on that token update whata deleted refused, and nothing changed
Two real pairs from the paired run on BookStack, both proven. A step is a filled node; the box is what the leg must end on. A pair counts as proven only when every leg ends as drawn.

27of 28 links between BookStack's goals proven by a run; 0 refutedBookStack at 1633b3ab · the paired run of 29 Sep 2026, run-scripts lane 36580253632, folded in production

A map says how features lead into each other #

featkpr maps a web app’s features, the flows people take through them and the goals they reach. Goals do not stand alone. In BookStack you cannot turn a book into a shelf until the book exists, and once an API token is deleted nobody can update it. On the map these are links between goals: creating a book enables turning it into a shelf; deleting a token disables updating it.

featkpr reads such links from the code, from which record a goal creates and which one another goal needs. A link read from the code is still a guess. The code can say a book is needed and still let the request through without one, or refuse for a reason that has nothing to do with the book.

A paired run takes the two goals of one link, call them a and b, and runs their tests on one fresh copy of BookStack. The figure at the top shows the legs, one lane per kind:

  • enables takes two legs. First b alone, on a fresh copy where nothing a makes exists: b must be refused. Then a, and b on the record a just made: b must pass. Only both together show that a is what b needs.
  • disables takes one leg. First a, then b on the record a acted on: b must be refused, and b’s step must change nothing.

A pair is proven when every leg ends as drawn, and refuted when a leg ends the other way, for example when b passes without a. If a leg ends on something neither rule expects, such as a server error, the pair stays unproven. A crash is not a refusal, so it proves nothing either way.

What happened on BookStack #

At commit 1633b3ab, featkpr’s map of BookStack held 229 links of the two kinds a pair can run. 28 of them had a runnable test at both ends. The run of 29 Sep 2026 sent 39 legs in 167 seconds, about 7 minutes of the homelab lane with its setup. 27 pairs were proven, 0 refuted and 1 left unproven.

BookStack at 1633b3ab · the paired run of 29 Sep 2026, run-scripts lane 36580253632, folded in production · one pin per link

The proven pairs are small, plain facts: creating a user enables updating that user, deleting a role disables opening it, creating a book enables turning it into a shelf. Each one now carries the runs that show it, where before it carried only a line of code.

The one left unproven is a BookStack bug #

The last pair was “Delete the revision” disables “Restore the page”. After a page revision is deleted, restoring it should be refused as not found, the way BookStack already answers for showing, comparing or deleting that revision. BookStack answered with a server error instead.

after a deletes revision id, b sends

PUT /books/{book}/page/{page}/revisions/{id}/restore

must answer

404 Not Found

as showing, comparing or deleting that revision already do

answered

500 Server Error

BookStack v26.09.1, reproduced by hand

Reported upstream as BookStack issue #6233 on 30 Sep 2026 · codeberg.org/bookstack/bookstack/issues/6233

We reproduced it by hand on BookStack v26.09.1 and reported it upstream as issue #6233. The link may well hold once the error is fixed, but until BookStack refuses the request cleanly the run cannot say, so the pair stays unproven rather than refuted.

What comes next #

201 links still wait. One of their goals has no runnable test yet, mostly deleting a book, a page or a chapter. Once those tests exist the same plan runs them; no model writes the missing tests for the pairs. After that come the other kinds of link, such as leads to and undoes. Both are on the roadmap.

In the app, a link a paired run proved is drawn solid where the goals are compared, and a link read from the code stays dashed. Production shows them now: after the re-seed of 30 Sep it holds 731 links between BookStack’s goals, and 22 goals carry a paired-run proof on the Ledger. The other links are still read from the code.

Compare, in featkpr's app · on a recorded world (goals-bookstack), not BookStack's production records

This post was drafted with AI assistance and checked by Tim. Every number in it is filled in from featkpr’s records when the site is built; how we write and check says how, and how to report an error.

What this shows #

  • 27 links between BookStack's goals, each shown by a run on a fresh copy, not only read from the code.
  • The legs each kind of link needs, and the rule that a pair stopped by something else stays unproven.
  • One BookStack bug found on the way, reproduced by hand and reported upstream.

What it doesn't #

  • The other 201 links: one of their goals has no runnable test yet.
  • Kinds of link beyond enables and disables, such as leads to or undoes.

See how BookStack's features lead into each other #

Half an hour, live: the map, the goals and a paired run, your questions first. Your own app comes after.

Ask for a private demo

↑ ↓ to move, Enter to open, Esc to close