Skip to content

Commit 2bf79b3

Browse files
Rinkiaclaude
andauthored
bench: add flask, the first target that can falsify a ranking change (#86)
Measuring ties in #83 exposed that every target here scores its labels inside a tied band. click's perfect 4/4 has a lower bound of 2/4; pluggy's 3/3 has a lower bound of 0/3. A benchmark of seven targets had, in effect, no regression guard — the two designated canaries could not falsify anything on their own. flask 3.1.2 was picked by measuring candidate distributions first, not by taste. Its ranking is graded rather than flat — 3 candidates at 1.00, 6 at 0.90, then exactly ONE at the rank-10 cutoff — so its recall@10 has no tie bounds at all: worst case equals best case. It is the only target of the eight whose number is pure evidence. flask 3.1.2 | 98 candidates | 6 labels | ranks 2, 3, 10, 38, 39, 98 | 3/6 Aggregate 18/34 -> 21/40, bounds 6..23 -> 9..26. All six new labels are tie-free, so the tie-decided share falls from 17/34 to 17/40. What it already says, recorded in benchmarks/README.md: - MethodView ranks 98 of 98, dead last. Arguably Flask's most-used class-based extension point, and every existing signal is blind to it. - DefaultJSONProvider ranks 1st while JSONProvider — the class the docstring explicitly says to subclass — ranks 10th. Role-suffix and base-role reward the concrete implementation over the contract it implements. - Dynamic imports take ranks 4-9 and push Flask and Blueprint below them: the pygments failure again, on a package with no plugin registry at all. Labels quote the docstrings shipped in the 3.1.2 wheel, per the rule about preferring docs pinned to the measured version. Two exclusions are recorded with their reasons: Flask itself (its shipped docstring never invites subclassing) and Blueprint (you construct one and register the instance — calling an API). Flask's largest extension surface, the before_request / errorhandler / template_filter decorators, cannot be labelled at all: they are methods on the app object and the parser only takes top-level defs as seams. That blind spot is now written down in the labels file instead of being an unwritten limitation. Labels only — no ranking code touched, as the gate requires. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f2f2819 commit 2bf79b3

3 files changed

Lines changed: 98 additions & 4 deletions

File tree

‎benchmarks/README.md‎

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,10 @@ broke in the ranking's favour, and if every tie broke against it.
506506
| junit-jupiter-api 5.11.3 | 4/7 | **0..7** | 18 tied at 1.00, 10 slots |
507507
| marshmallow 4.3.0 | 4/6 | 4..5 | 4 tied at 0.7, 1 slot |
508508
| pluggy 1.6.0 | 3/3 | **0..3** | 13 tied at 0.8, 10 slots |
509+
| flask 3.1.2 | 3/6 | **3..3** | 1 at the cutoff — no ambiguity |
509510
| pygments 2.20.0 | 0/5 | 0..0 | — |
510511
| sqlalchemy 2.0.51 | 1/7 | 0..2 | 50 tied at 1.00, 10 slots |
511-
| **Aggregate** | **18/34** | **6..23** | 17 of 34 labels are tie-decided |
512+
| **Aggregate** | **21/40** | **9..26** | 17 of 40 labels are tie-decided |
512513

513514
Three consequences, none of them comfortable:
514515

@@ -517,7 +518,9 @@ Three consequences, none of them comfortable:
517518
**lower bound**.
518519
2. **The regression canaries were never canaries.** click's perfect 4/4 has a
519520
lower bound of 2/4 and pluggy's 3/3 a lower bound of **0/3** — no tie order is
520-
guaranteed by the ranking. Neither can falsify a change on its own.
521+
guaranteed by the ranking. Neither can falsify a change on its own. Flask was
522+
added for exactly this reason; see "Flask: the first target that can falsify
523+
a change" below.
521524
3. **JUnit and SQLAlchemy are *not* the same problem**, despite looking alike.
522525

523526
### JUnit's band is not noise — the metric is wrong, not the ranking
@@ -549,6 +552,64 @@ alphabet, chosen arbitrarily, beats both.
549552

550553
Recorded so nobody re-derives it. The clamp is not hiding a usable signal.
551554

555+
## Flask: the first target that can falsify a change
556+
557+
Measuring ties exposed a problem the aggregate had been hiding: **every target
558+
here scores its labels inside a tied band**, so no single target could refute a
559+
heuristic on its own. click's perfect 4/4 has a lower bound of 2/4; pluggy's 3/3
560+
has a lower bound of 0/3. A benchmark of eight targets had, in effect, no
561+
regression guard.
562+
563+
Flask 3.1.2 was added to fix that, and it was chosen **by measurement, not by
564+
taste**. Its score distribution is graded rather than flat:
565+
566+
| Score | 1.00 | 0.90 | 0.85 | 0.80 | 0.60 | … |
567+
|---|---|---|---|---|---|---|
568+
| Candidates | 3 | 6 | **1** | 23 | 1 | |
569+
570+
Exactly one candidate holds the score at the rank-10 cutoff, so **flask's
571+
recall@10 has no tie bounds at all** — worst case equals best case. It is the
572+
only target of the eight whose number is pure evidence.
573+
574+
| Target | Candidates | Labels | Rank of each label | recall@10 | Median |
575+
|---|---|---|---|---|---|
576+
| flask 3.1.2 | 98 | 6 | 2, 3, 10, 38, 39, 98 | **3/6** | 24 |
577+
578+
Aggregate moves to **21/40, bounds 9..26** — the tie-decided share drops from
579+
17/34 to 17/40 because all six new labels are tie-free.
580+
581+
### What it already says about the ranking
582+
583+
- **`MethodView` ranks 98 of 98 — dead last.** It is arguably Flask's most-used
584+
class-based extension point, and it scores the bare public baseline: it is not
585+
an ABC, its name ends in no role suffix the catalog knows, its base
586+
(`View`) likewise, and it is not re-exported under a name the root `__init__`
587+
advertises as a seam. Every existing signal is blind to it.
588+
- **The implementation outranks the base it implements.**
589+
`DefaultJSONProvider` is 1st, `JSONProvider` — the class the docstring
590+
explicitly tells you to subclass — is 10th. The role-suffix and base-role
591+
signals both reward the concrete subclass more than the contract.
592+
- **Dynamic imports occupy ranks 4–9**, pushing `Flask` and `Blueprint` below
593+
them. This is the pygments failure again, on a package where the loader sites
594+
are ordinary `import_string` helpers rather than a plugin registry.
595+
596+
### What it deliberately cannot say
597+
598+
Flask's *largest* extension surface is the decorator methods on the app and
599+
blueprint objects — `before_request`, `errorhandler`, `template_filter`,
600+
`context_processor`. None of them can be labelled, because the parser only takes
601+
**top-level defs** as seams and these are methods. That blind spot is a reason to
602+
add this target, not a reason to avoid it: it is now visible in the labels file
603+
instead of being an unwritten limitation.
604+
605+
Two exclusions, both by the labelling rule:
606+
607+
- **`Flask` itself.** Its shipped docstring describes what the object does and
608+
never invites subclassing. The web guide is more encouraging, but labels come
609+
from the docs shipped *at the pinned version*.
610+
- **`Blueprint`.** You construct one and register the instance, which the rule
611+
counts as calling an API rather than implementing one.
612+
552613
## What this benchmark still cannot measure well
553614

554615
Function labels now exist, but only five of thirty-four — and three of those
@@ -659,7 +720,8 @@ If resolution ever genuinely binds, add **targets** — never labels.
659720
## Running it
660721

661722
```bash
662-
pip install pluggy==1.6.0 click==8.4.2 sqlalchemy==2.0.51
723+
pip install pluggy==1.6.0 click==8.4.2 sqlalchemy==2.0.51 \
724+
pygments==2.20.0 marshmallow==4.3.0 flask==3.1.2
663725
python benchmarks/score.py # all targets
664726
python benchmarks/score.py --target pluggy
665727
```

‎benchmarks/ground_truth.json‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,37 @@
188188
"justification": "Javadoc: 'InvocationInterceptor defines the API for Extensions that wish to intercept calls to test code.'"
189189
}
190190
]
191+
},
192+
"flask": {
193+
"version": "3.1.2",
194+
"docs": "https://flask.palletsprojects.com/ — labels quote the docstrings shipped in the 3.1.2 wheel.",
195+
"note": "Chosen because its ranking is GRADED rather than flat: at the top-10 cutoff exactly one candidate holds the cutoff score, so unlike every other target here recall@10 is not decided by the alphabetical tiebreak and this target can actually falsify a change. Partial list. Flask itself is excluded: its shipped docstring describes what the object does, never invites subclassing. Blueprint is excluded too: you construct one and register the instance, which the labelling rule counts as calling an API. Flask's largest extension surface - the before_request / errorhandler / template_filter decorators - cannot be labelled at all, because they are METHODS on the app object and the parser only takes top-level defs as seams. That blind spot is the point of adding this target, not a defect in it.",
196+
"extension_points": [
197+
{
198+
"id": "flask.json.provider:JSONProvider",
199+
"justification": "Class docstring: 'To implement a provider for a specific library, subclass this base class and implement at least dumps and loads.'"
200+
},
201+
{
202+
"id": "flask.sessions:SessionInterface",
203+
"justification": "Class docstring: 'The basic interface you have to implement in order to replace the default session interface'. Names the methods an implementer must provide."
204+
},
205+
{
206+
"id": "flask.views:View",
207+
"justification": "Class docstring: 'Subclass this class and override dispatch_request to create a generic class-based view.'"
208+
},
209+
{
210+
"id": "flask.views:MethodView",
211+
"justification": "Class docstring: dispatches request methods to instance methods - 'if you implement a get method, it will be used to handle GET requests'. Subclassed by the user to define the handlers."
212+
},
213+
{
214+
"id": "flask.wrappers:Request",
215+
"justification": "Class docstring: 'If you want to replace the request object used you can subclass this and set Flask.request_class to your subclass.'"
216+
},
217+
{
218+
"id": "flask.wrappers:Response",
219+
"justification": "Class docstring: 'If you want to replace the response object used you can subclass this and set Flask.response_class to your subclass.'"
220+
}
221+
]
191222
}
192223
}
193224
}

‎benchmarks/score.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
Answers one question before you open a pull request: did this heuristic change
1919
help? Runs offline and free — no API key, no paid call, no network.
2020
21-
pip install pluggy==1.6.0 click==8.4.2 sqlalchemy==2.0.51
21+
pip install pluggy==1.6.0 click==8.4.2 sqlalchemy==2.0.51 \
22+
pygments==2.20.0 marshmallow==4.3.0 flask==3.1.2
2223
python benchmarks/score.py
2324
2425
Reports, per target, where each labelled extension point ranks, plus recall@10,

0 commit comments

Comments
 (0)