vernier-scale 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vernier_scale-0.1.0/LICENSE +21 -0
- vernier_scale-0.1.0/PKG-INFO +333 -0
- vernier_scale-0.1.0/README.md +308 -0
- vernier_scale-0.1.0/pyproject.toml +102 -0
- vernier_scale-0.1.0/pyproject.toml.orig +60 -0
- vernier_scale-0.1.0/src/vernier_scale/__init__.py +106 -0
- vernier_scale-0.1.0/src/vernier_scale/cli.py +269 -0
- vernier_scale-0.1.0/src/vernier_scale/client.py +319 -0
- vernier_scale-0.1.0/src/vernier_scale/distance.py +130 -0
- vernier_scale-0.1.0/src/vernier_scale/errors.py +12 -0
- vernier_scale-0.1.0/src/vernier_scale/filler.py +118 -0
- vernier_scale-0.1.0/src/vernier_scale/noise.py +352 -0
- vernier_scale-0.1.0/src/vernier_scale/perturb.py +163 -0
- vernier_scale-0.1.0/src/vernier_scale/py.typed +0 -0
- vernier_scale-0.1.0/src/vernier_scale/questions.py +115 -0
- vernier_scale-0.1.0/src/vernier_scale/report.py +503 -0
- vernier_scale-0.1.0/src/vernier_scale/run.py +366 -0
- vernier_scale-0.1.0/src/vernier_scale/segment.py +189 -0
- vernier_scale-0.1.0/src/vernier_scale/types.py +130 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pablo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vernier-scale
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Measure meaning by perturbing text and watching a calibrated probability move.
|
|
5
|
+
Keywords: typesafe,jev,ablation,attribution,interpretability,nlp
|
|
6
|
+
Author: n0nuser
|
|
7
|
+
Author-email: n0nuser <n0nuser@users.noreply.github.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Project-URL: Homepage, https://github.com/n0nuser/vernier
|
|
22
|
+
Project-URL: Repository, https://github.com/n0nuser/vernier
|
|
23
|
+
Project-URL: Issues, https://github.com/n0nuser/vernier/issues
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# vernier
|
|
27
|
+
|
|
28
|
+
Measure meaning by perturbing text and watching a calibrated probability move.
|
|
29
|
+
|
|
30
|
+
`vernier` is built on [TypeSafe's Jev](https://docs.typesafe.ai), a System One
|
|
31
|
+
model that returns typed judgments with calibrated probability distributions
|
|
32
|
+
instead of generated text. Everyone uses Jev's answer and throws the
|
|
33
|
+
distribution away. The distribution is the measurement.
|
|
34
|
+
|
|
35
|
+
Remove one section of a document, ask the same question again, and see how far
|
|
36
|
+
the distribution moved. Repeat for every section. What comes back is an
|
|
37
|
+
attribution map: which parts of this document are actually carrying its verdict.
|
|
38
|
+
This is occlusion testing, borrowed from computer vision and pointed at meaning.
|
|
39
|
+
|
|
40
|
+
## The problem with doing that naively
|
|
41
|
+
|
|
42
|
+
Any tool can delete a paragraph and print a delta. The number will be wrong, for
|
|
43
|
+
four reasons, and `vernier` exists to handle all four.
|
|
44
|
+
|
|
45
|
+
**The model is not deterministic.** Ask Jev the same question five times and the
|
|
46
|
+
answer moves. On the example below the baseline wanders between 0.80 and 0.82.
|
|
47
|
+
Any delta smaller than that wander is not a finding, it is the instrument
|
|
48
|
+
breathing. So `vernier` measures the baseline *k* times before it perturbs
|
|
49
|
+
anything, and reports every result against that floor.
|
|
50
|
+
|
|
51
|
+
**A measured spread of zero is not a noise floor of zero.** Jev reports
|
|
52
|
+
probabilities rounded to two decimals. Five identical readings tell you the
|
|
53
|
+
jitter is below the grid, not that there is none — and if the baseline is
|
|
54
|
+
saturated at 1.00, every nonzero delta would "beat" a floor of zero and the tool
|
|
55
|
+
would confidently rank rounding error. The floor is therefore never below the
|
|
56
|
+
resolution limit, and a saturated baseline aborts the run instead of ranking it.
|
|
57
|
+
|
|
58
|
+
**Deletion is a confound.** Cutting text out makes the document shorter, can
|
|
59
|
+
orphan a heading and can break the grammar of what remains. All three move the
|
|
60
|
+
number for reasons that have nothing to do with meaning. So `vernier` also runs
|
|
61
|
+
a second mode that replaces the segment with length-matched neutral filler,
|
|
62
|
+
keeping the shape and swapping only the words. Where the two modes disagree, it
|
|
63
|
+
says so rather than picking the flattering one.
|
|
64
|
+
|
|
65
|
+
**The argmax is the wrong thing to watch.** A segment can reshape a whole
|
|
66
|
+
distribution without flipping which option wins. `vernier` compares
|
|
67
|
+
distributions — `|Δp|` for Noul, Jensen-Shannon for Choice and Score — and
|
|
68
|
+
reports entropy and Jev's own confidence alongside.
|
|
69
|
+
|
|
70
|
+
And one thing that is not a statistic: the tool ablates neutral control sections
|
|
71
|
+
it injected itself. They cannot change what the document says. If one of them
|
|
72
|
+
moves the number, `vernier` prints no ranking at all.
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
uv add vernier-scale # as a library
|
|
78
|
+
uv tool install vernier-scale # as a command, installed as `vernier`
|
|
79
|
+
export TYPESAFE_API_KEY=... # from console.typesafe.ai/keys
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The PyPI name is `vernier-scale` — a vernier scale being the instrument this is
|
|
83
|
+
named after — because `vernier` was already taken. It imports as
|
|
84
|
+
`vernier_scale` and its command is `vernier`.
|
|
85
|
+
|
|
86
|
+
## As a library
|
|
87
|
+
|
|
88
|
+
The CLI is a thin adapter over the same measurement. Everything it does is
|
|
89
|
+
importable, and nothing is read from the environment, logged or printed unless
|
|
90
|
+
you ask for it — the client is passed in, and rendering is a separate step.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import vernier_scale as vernier
|
|
94
|
+
|
|
95
|
+
report = vernier.ablate(
|
|
96
|
+
text=open("policy.md").read(),
|
|
97
|
+
question=vernier.noul("Does this policy permit a refund here?"),
|
|
98
|
+
client=vernier.HttpJevClient.from_environment(),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if not report.validity.ok:
|
|
102
|
+
raise SystemExit(report.validity.reasons[0]) # a control moved; do not trust a ranking
|
|
103
|
+
|
|
104
|
+
for row in report.ranked:
|
|
105
|
+
print(f"{row.strength:.4f} {row.verdict.value:14} {row.segment.label}")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`noul`, `choice` and `score` build the three question types, mirroring
|
|
109
|
+
TypeSafe's own SDKs. `Report` carries `ranked`, `inside_noise`, `indeterminate`
|
|
110
|
+
and `unmeasured`, plus the `floor` every one of them was judged against.
|
|
111
|
+
|
|
112
|
+
Swap the client to run the whole pipeline with no network and no key — this is
|
|
113
|
+
how vernier's own test suite runs:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
report = vernier.ablate(text, question, vernier.StubJevClient(base=0.8))
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Any object with an `evaluate(Call) -> Outcome` method satisfies `JevClient`, so
|
|
120
|
+
a recorded fixture, a cache or a different model drops straight in.
|
|
121
|
+
|
|
122
|
+
The statistics and the segmenters are usable on their own:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
vernier.segment(text, "section") # also paragraph, sentence, line, item
|
|
126
|
+
vernier.tvd(before, after) # and jsd, entropy, normalised_entropy
|
|
127
|
+
vernier.resolution_limit(3, "jsd") # the floor 2dp rounding alone imposes
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Errors all descend from `vernier.VernierError`, so one `except` covers the
|
|
131
|
+
library and nothing else. The package ships `py.typed`; a strict `mypy` run
|
|
132
|
+
against the public surface passes.
|
|
133
|
+
|
|
134
|
+
## A real measurement
|
|
135
|
+
|
|
136
|
+
The Contributor Covenant 2.1 is in `examples/`. It has a four-rung consequence
|
|
137
|
+
ladder, so it is a good document to ask a proportionality question of. (It is a
|
|
138
|
+
test fixture, not this project's own code of conduct; it is licensed
|
|
139
|
+
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), unlike the rest of
|
|
140
|
+
this repository, and the file records the one change made to it.)
|
|
141
|
+
|
|
142
|
+
> A contributor has, over several months and after a prior written warning,
|
|
143
|
+
> repeatedly made derogatory remarks about contributors of a particular
|
|
144
|
+
> nationality. Under this Code of Conduct, is a permanent ban the proportionate
|
|
145
|
+
> consequence?
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
vernier ablate examples/contributor-covenant-2.1.md \
|
|
149
|
+
--question-file examples/permanent-ban.json \
|
|
150
|
+
--baseline-calls 8 --trial-calls 6
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
BASELINE (unmodified document, one call each)
|
|
155
|
+
[0] p=0.82 [1] p=0.80 [2] p=0.82 [3] p=0.79
|
|
156
|
+
[4] p=0.81 [5] p=0.81 [6] p=0.81 [7] p=0.80
|
|
157
|
+
entropy 0.706 of maximum
|
|
158
|
+
|
|
159
|
+
NOISE FLOOR (spread across identical calls; nothing below it is a finding)
|
|
160
|
+
measured spread 0.0300 tvd over 28 pairs
|
|
161
|
+
resolution limit 0.0100 (probabilities arrive rounded to 2dp)
|
|
162
|
+
floor 0.0300 limited by run-to-run spread
|
|
163
|
+
threshold 0.0400 floor + one resolution step
|
|
164
|
+
entropy spread 0.0614 the floor haze is read against
|
|
165
|
+
|
|
166
|
+
CONTROL (neutral segments that must land inside the noise)
|
|
167
|
+
[pass] ## Building Access delete=0.0008 p=1.000 mask=0.0075 p=0.257
|
|
168
|
+
[pass] ## Facilities Housekeeping delete=0.0242 p=0.001 mask=0.0125 p=0.036
|
|
169
|
+
[pass] segmentation tiles the document byte-exactly (null ablation)
|
|
170
|
+
|
|
171
|
+
ATTRIBUTION (4 of 12 segments cleared 0.0400 at p≤0.05)
|
|
172
|
+
++ corroborated by both modes ~ mode-sensitive (deletion confound suspected)
|
|
173
|
+
size = distance between mean baseline and mean perturbed distribution
|
|
174
|
+
p = exact permutation test: could this run's jitter alone do it?
|
|
175
|
+
|
|
176
|
+
++ ### 4. Permanent Ban
|
|
177
|
+
delete 0.1892 p=0.000 ↓0.19 H+0.253 █████████████████·
|
|
178
|
+
mask 0.2008 p=0.000 ↓0.20 H+0.260 ██████████████████
|
|
179
|
+
++ ### 3. Temporary Ban
|
|
180
|
+
delete 0.0725 p=0.000 ↑0.07 H-0.177 ██████············
|
|
181
|
+
mask 0.0758 p=0.000 ↑0.08 H-0.187 ███████···········
|
|
182
|
+
~ ### 1. Correction
|
|
183
|
+
delete 0.0308 p=0.000 ↑0.03 H-0.068 ███···············
|
|
184
|
+
mask 0.0442 p=0.000 ↑0.04 H-0.101 ████··············
|
|
185
|
+
~ ### 2. Warning
|
|
186
|
+
delete 0.0608 p=0.000 ↑0.06 H-0.144 █████·············
|
|
187
|
+
mask 0.0208 p=0.013 ↑0.02 H-0.046 ██················
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`size` is how far the mean distribution moved, `p` is the permutation test,
|
|
191
|
+
the arrow is the signed shift on the leading outcome, and `H` is the change in
|
|
192
|
+
entropy. On a Choice or Score run each row also carries `conf`, the change in
|
|
193
|
+
Jev's own confidence.
|
|
194
|
+
|
|
195
|
+
Read the arrows. Removing **Permanent Ban** takes the verdict *down* by 0.20 —
|
|
196
|
+
that section is what the verdict rests on. Removing any of the three milder
|
|
197
|
+
rungs pushes it *up*. The lesser consequences are competing alternatives, and
|
|
198
|
+
deleting a competitor makes the harsh answer look more proportionate. The ladder
|
|
199
|
+
behaves like a ladder, and nobody had to tell the tool that.
|
|
200
|
+
|
|
201
|
+
Everything else in the document — the pledge, the scope, the standards, the
|
|
202
|
+
enforcement sections, the attribution — is inside the noise on this question.
|
|
203
|
+
`## Our Standards` establishes that the conduct is a violation, but it has
|
|
204
|
+
nothing to say about *which rung*, and the measurement shows that.
|
|
205
|
+
|
|
206
|
+
`### 2. Warning` is marked `~`: deleting it moves the number three times as far
|
|
207
|
+
as masking it does. That is the deletion confound made visible, and it is
|
|
208
|
+
reported rather than ranked as if it were solid.
|
|
209
|
+
|
|
210
|
+
The `H` column reads the same story a second way. Removing **Permanent Ban**
|
|
211
|
+
raises the entropy — take the top rung away and the question becomes harder to
|
|
212
|
+
settle. Removing any of the milder rungs *lowers* it, because there is one less
|
|
213
|
+
competing answer.
|
|
214
|
+
|
|
215
|
+
## The other two readings
|
|
216
|
+
|
|
217
|
+
`deadweight` is the same run inverted — the segments the verdict does not rest
|
|
218
|
+
on, with the ones that moved detectably but negligibly called out separately.
|
|
219
|
+
|
|
220
|
+
`haze` reads the shape of the distribution rather than its argmax. Asked whether
|
|
221
|
+
off-platform conduct falls within the Code of Conduct's scope, Jev answers
|
|
222
|
+
`out_of_scope` on all eight baseline calls — and the distribution is nearly
|
|
223
|
+
flat:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
BASELINE (unmodified document, one call each)
|
|
227
|
+
[0] out_of_scope [in_scope=0.28, out_of_scope=0.72] conf=0.44
|
|
228
|
+
[1] out_of_scope [in_scope=0.31, out_of_scope=0.69] conf=0.38
|
|
229
|
+
[2] out_of_scope [in_scope=0.22, out_of_scope=0.78] conf=0.55
|
|
230
|
+
... 10 calls, every one answering out_of_scope
|
|
231
|
+
entropy 0.860 of maximum
|
|
232
|
+
|
|
233
|
+
HAZE 0.860 ██████████████████████████····
|
|
234
|
+
|
|
235
|
+
The distribution is close to flat. With well-formed options that is a fact
|
|
236
|
+
about the input, not the model: this document does not decide the question.
|
|
237
|
+
Jev's own confidence: 0.44.
|
|
238
|
+
|
|
239
|
+
WHERE THE AMBIGUITY LIVES (change in entropy when a segment is removed)
|
|
240
|
+
entropy noise floor 0.144: the baseline's own entropy wanders this much
|
|
241
|
+
between identical calls, so smaller shifts are not reported.
|
|
242
|
+
|
|
243
|
+
No segment shifted the entropy further than the baseline shifts on
|
|
244
|
+
its own. The ambiguity is spread across this document rather than
|
|
245
|
+
located in any one section.
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The answer looks decisive and is not: ten calls, ten identical verdicts, and a
|
|
249
|
+
distribution that is 86% of the way to flat. Anything reading only
|
|
250
|
+
`answer.choice` would ship that as settled.
|
|
251
|
+
|
|
252
|
+
Note what the tool then declines to do. Its first instinct is to name the
|
|
253
|
+
sections responsible, but the baseline's own entropy wanders by 0.144 between
|
|
254
|
+
identical calls — normalised entropy is steep near p=0.3, so the ordinary
|
|
255
|
+
jitter in the probability is amplified in the entropy — and no segment shifted
|
|
256
|
+
it further than that. So it reports nothing, and says why. An earlier run at
|
|
257
|
+
fewer replicates did name two sections; they did not survive the floor at
|
|
258
|
+
higher power, which is exactly the outcome the floor exists to produce.
|
|
259
|
+
|
|
260
|
+
## Verdicts
|
|
261
|
+
|
|
262
|
+
| | |
|
|
263
|
+
|---|---|
|
|
264
|
+
| `++` corroborated | Above the floor under both modes, same direction. A finding. |
|
|
265
|
+
| `~` mode-sensitive | Above the floor under one mode only. The deletion confound, visible. |
|
|
266
|
+
| `?` indeterminate | Moved further than the floor without repeating consistently enough to assert. |
|
|
267
|
+
| *(none)* null | Effect below the floor in both modes, with the power to have seen more. |
|
|
268
|
+
| `XX` failed | A call did not come back. Never silently a zero. |
|
|
269
|
+
|
|
270
|
+
A finding has to clear two independent hurdles. **Effect size** must exceed the
|
|
271
|
+
noise floor — a movement smaller than the difference between two identical calls
|
|
272
|
+
is not worth reporting. And an exact **permutation test** must rule out this
|
|
273
|
+
run's jitter: pool the baseline and perturbed readings, and count how often a
|
|
274
|
+
random split separates the means as far as the real one did. Both statistics use
|
|
275
|
+
the mean distribution of each group, so both sharpen as replicates increase.
|
|
276
|
+
|
|
277
|
+
A big move measured once proves nothing. A perfectly repeatable move of half a
|
|
278
|
+
grid step is not worth knowing. Both hurdles, or it is not a finding.
|
|
279
|
+
|
|
280
|
+
## Usage
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
vernier ablate DOCUMENT rank segments by how far their removal moves the answer
|
|
284
|
+
vernier deadweight DOCUMENT the same run inverted: segments that move nothing
|
|
285
|
+
vernier haze DOCUMENT read the shape of the distribution, not its argmax
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Ask with `--noul QUESTION`, `--choice QUESTION --option a --option b`,
|
|
289
|
+
`--score QUESTION --level low --level high`, or `--question-file spec.json`.
|
|
290
|
+
|
|
291
|
+
| flag | |
|
|
292
|
+
|---|---|
|
|
293
|
+
| `--by` | `section` (default), `paragraph`, `sentence`, `line`, `item` |
|
|
294
|
+
| `--mode` | `delete`, `mask`; repeatable, both by default |
|
|
295
|
+
| `--baseline-calls K` | noise-floor replicates (default 5; 8 or more gives a steadier floor) |
|
|
296
|
+
| `--trial-calls K` | replicates per perturbed variant (default 3) |
|
|
297
|
+
| `--controls N` | placebo segments to inject (default 2) |
|
|
298
|
+
| `--no-control` | skip the control condition — the report will say the run is uncontrolled |
|
|
299
|
+
| `--json` | the whole measurement, including every replicate |
|
|
300
|
+
| `--stub` | run the entire pipeline offline against a synthetic model |
|
|
301
|
+
|
|
302
|
+
Exit codes: `0` measured, `1` invalid or aborted, `2` usage, `3` error.
|
|
303
|
+
|
|
304
|
+
A run is `segments × modes × trial-calls + baseline-calls` requests. Ablation
|
|
305
|
+
varies the state, so each variant must be its own request — there is nothing to
|
|
306
|
+
batch. Concurrency is bounded, retries follow the documented back-off, and a
|
|
307
|
+
failed call becomes a marked failure rather than a convenient zero.
|
|
308
|
+
|
|
309
|
+
## Layout
|
|
310
|
+
|
|
311
|
+
| module | |
|
|
312
|
+
|---|---|
|
|
313
|
+
| `__init__.py` | the public surface: a re-export facade, nothing else |
|
|
314
|
+
| `questions.py` | `noul` / `choice` / `score` constructors |
|
|
315
|
+
| `segment.py` | pluggable segmenters; every one tiles the document byte-exactly |
|
|
316
|
+
| `perturb.py` | delete and mask, plus placebo injection |
|
|
317
|
+
| `filler.py` | the neutral corpus both of those draw on |
|
|
318
|
+
| `client.py` | the System One client, and the stub that stands in for it |
|
|
319
|
+
| `distance.py` | TVD, Jensen-Shannon, entropy, and the resolution limit |
|
|
320
|
+
| `noise.py` | the noise floor, the permutation test, and the verdicts |
|
|
321
|
+
| `run.py` | orchestration: baseline, preflight, fan-out, assembly |
|
|
322
|
+
| `report.py` | rendering |
|
|
323
|
+
| `errors.py` | `VernierError`, the base every other error inherits |
|
|
324
|
+
| `cli.py` | argument parsing and exit codes; no measurement logic |
|
|
325
|
+
|
|
326
|
+
```sh
|
|
327
|
+
uv run pytest # no network, no key
|
|
328
|
+
uv run mypy
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The suite runs entirely against `StubJevClient`, which reproduces the three
|
|
332
|
+
quirks the statistics have to survive: answers on a two-decimal grid, identical
|
|
333
|
+
calls that differ slightly, and distributions that can saturate at 1.0.
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# vernier
|
|
2
|
+
|
|
3
|
+
Measure meaning by perturbing text and watching a calibrated probability move.
|
|
4
|
+
|
|
5
|
+
`vernier` is built on [TypeSafe's Jev](https://docs.typesafe.ai), a System One
|
|
6
|
+
model that returns typed judgments with calibrated probability distributions
|
|
7
|
+
instead of generated text. Everyone uses Jev's answer and throws the
|
|
8
|
+
distribution away. The distribution is the measurement.
|
|
9
|
+
|
|
10
|
+
Remove one section of a document, ask the same question again, and see how far
|
|
11
|
+
the distribution moved. Repeat for every section. What comes back is an
|
|
12
|
+
attribution map: which parts of this document are actually carrying its verdict.
|
|
13
|
+
This is occlusion testing, borrowed from computer vision and pointed at meaning.
|
|
14
|
+
|
|
15
|
+
## The problem with doing that naively
|
|
16
|
+
|
|
17
|
+
Any tool can delete a paragraph and print a delta. The number will be wrong, for
|
|
18
|
+
four reasons, and `vernier` exists to handle all four.
|
|
19
|
+
|
|
20
|
+
**The model is not deterministic.** Ask Jev the same question five times and the
|
|
21
|
+
answer moves. On the example below the baseline wanders between 0.80 and 0.82.
|
|
22
|
+
Any delta smaller than that wander is not a finding, it is the instrument
|
|
23
|
+
breathing. So `vernier` measures the baseline *k* times before it perturbs
|
|
24
|
+
anything, and reports every result against that floor.
|
|
25
|
+
|
|
26
|
+
**A measured spread of zero is not a noise floor of zero.** Jev reports
|
|
27
|
+
probabilities rounded to two decimals. Five identical readings tell you the
|
|
28
|
+
jitter is below the grid, not that there is none — and if the baseline is
|
|
29
|
+
saturated at 1.00, every nonzero delta would "beat" a floor of zero and the tool
|
|
30
|
+
would confidently rank rounding error. The floor is therefore never below the
|
|
31
|
+
resolution limit, and a saturated baseline aborts the run instead of ranking it.
|
|
32
|
+
|
|
33
|
+
**Deletion is a confound.** Cutting text out makes the document shorter, can
|
|
34
|
+
orphan a heading and can break the grammar of what remains. All three move the
|
|
35
|
+
number for reasons that have nothing to do with meaning. So `vernier` also runs
|
|
36
|
+
a second mode that replaces the segment with length-matched neutral filler,
|
|
37
|
+
keeping the shape and swapping only the words. Where the two modes disagree, it
|
|
38
|
+
says so rather than picking the flattering one.
|
|
39
|
+
|
|
40
|
+
**The argmax is the wrong thing to watch.** A segment can reshape a whole
|
|
41
|
+
distribution without flipping which option wins. `vernier` compares
|
|
42
|
+
distributions — `|Δp|` for Noul, Jensen-Shannon for Choice and Score — and
|
|
43
|
+
reports entropy and Jev's own confidence alongside.
|
|
44
|
+
|
|
45
|
+
And one thing that is not a statistic: the tool ablates neutral control sections
|
|
46
|
+
it injected itself. They cannot change what the document says. If one of them
|
|
47
|
+
moves the number, `vernier` prints no ranking at all.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
uv add vernier-scale # as a library
|
|
53
|
+
uv tool install vernier-scale # as a command, installed as `vernier`
|
|
54
|
+
export TYPESAFE_API_KEY=... # from console.typesafe.ai/keys
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The PyPI name is `vernier-scale` — a vernier scale being the instrument this is
|
|
58
|
+
named after — because `vernier` was already taken. It imports as
|
|
59
|
+
`vernier_scale` and its command is `vernier`.
|
|
60
|
+
|
|
61
|
+
## As a library
|
|
62
|
+
|
|
63
|
+
The CLI is a thin adapter over the same measurement. Everything it does is
|
|
64
|
+
importable, and nothing is read from the environment, logged or printed unless
|
|
65
|
+
you ask for it — the client is passed in, and rendering is a separate step.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import vernier_scale as vernier
|
|
69
|
+
|
|
70
|
+
report = vernier.ablate(
|
|
71
|
+
text=open("policy.md").read(),
|
|
72
|
+
question=vernier.noul("Does this policy permit a refund here?"),
|
|
73
|
+
client=vernier.HttpJevClient.from_environment(),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if not report.validity.ok:
|
|
77
|
+
raise SystemExit(report.validity.reasons[0]) # a control moved; do not trust a ranking
|
|
78
|
+
|
|
79
|
+
for row in report.ranked:
|
|
80
|
+
print(f"{row.strength:.4f} {row.verdict.value:14} {row.segment.label}")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`noul`, `choice` and `score` build the three question types, mirroring
|
|
84
|
+
TypeSafe's own SDKs. `Report` carries `ranked`, `inside_noise`, `indeterminate`
|
|
85
|
+
and `unmeasured`, plus the `floor` every one of them was judged against.
|
|
86
|
+
|
|
87
|
+
Swap the client to run the whole pipeline with no network and no key — this is
|
|
88
|
+
how vernier's own test suite runs:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
report = vernier.ablate(text, question, vernier.StubJevClient(base=0.8))
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Any object with an `evaluate(Call) -> Outcome` method satisfies `JevClient`, so
|
|
95
|
+
a recorded fixture, a cache or a different model drops straight in.
|
|
96
|
+
|
|
97
|
+
The statistics and the segmenters are usable on their own:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
vernier.segment(text, "section") # also paragraph, sentence, line, item
|
|
101
|
+
vernier.tvd(before, after) # and jsd, entropy, normalised_entropy
|
|
102
|
+
vernier.resolution_limit(3, "jsd") # the floor 2dp rounding alone imposes
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Errors all descend from `vernier.VernierError`, so one `except` covers the
|
|
106
|
+
library and nothing else. The package ships `py.typed`; a strict `mypy` run
|
|
107
|
+
against the public surface passes.
|
|
108
|
+
|
|
109
|
+
## A real measurement
|
|
110
|
+
|
|
111
|
+
The Contributor Covenant 2.1 is in `examples/`. It has a four-rung consequence
|
|
112
|
+
ladder, so it is a good document to ask a proportionality question of. (It is a
|
|
113
|
+
test fixture, not this project's own code of conduct; it is licensed
|
|
114
|
+
[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), unlike the rest of
|
|
115
|
+
this repository, and the file records the one change made to it.)
|
|
116
|
+
|
|
117
|
+
> A contributor has, over several months and after a prior written warning,
|
|
118
|
+
> repeatedly made derogatory remarks about contributors of a particular
|
|
119
|
+
> nationality. Under this Code of Conduct, is a permanent ban the proportionate
|
|
120
|
+
> consequence?
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
vernier ablate examples/contributor-covenant-2.1.md \
|
|
124
|
+
--question-file examples/permanent-ban.json \
|
|
125
|
+
--baseline-calls 8 --trial-calls 6
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
BASELINE (unmodified document, one call each)
|
|
130
|
+
[0] p=0.82 [1] p=0.80 [2] p=0.82 [3] p=0.79
|
|
131
|
+
[4] p=0.81 [5] p=0.81 [6] p=0.81 [7] p=0.80
|
|
132
|
+
entropy 0.706 of maximum
|
|
133
|
+
|
|
134
|
+
NOISE FLOOR (spread across identical calls; nothing below it is a finding)
|
|
135
|
+
measured spread 0.0300 tvd over 28 pairs
|
|
136
|
+
resolution limit 0.0100 (probabilities arrive rounded to 2dp)
|
|
137
|
+
floor 0.0300 limited by run-to-run spread
|
|
138
|
+
threshold 0.0400 floor + one resolution step
|
|
139
|
+
entropy spread 0.0614 the floor haze is read against
|
|
140
|
+
|
|
141
|
+
CONTROL (neutral segments that must land inside the noise)
|
|
142
|
+
[pass] ## Building Access delete=0.0008 p=1.000 mask=0.0075 p=0.257
|
|
143
|
+
[pass] ## Facilities Housekeeping delete=0.0242 p=0.001 mask=0.0125 p=0.036
|
|
144
|
+
[pass] segmentation tiles the document byte-exactly (null ablation)
|
|
145
|
+
|
|
146
|
+
ATTRIBUTION (4 of 12 segments cleared 0.0400 at p≤0.05)
|
|
147
|
+
++ corroborated by both modes ~ mode-sensitive (deletion confound suspected)
|
|
148
|
+
size = distance between mean baseline and mean perturbed distribution
|
|
149
|
+
p = exact permutation test: could this run's jitter alone do it?
|
|
150
|
+
|
|
151
|
+
++ ### 4. Permanent Ban
|
|
152
|
+
delete 0.1892 p=0.000 ↓0.19 H+0.253 █████████████████·
|
|
153
|
+
mask 0.2008 p=0.000 ↓0.20 H+0.260 ██████████████████
|
|
154
|
+
++ ### 3. Temporary Ban
|
|
155
|
+
delete 0.0725 p=0.000 ↑0.07 H-0.177 ██████············
|
|
156
|
+
mask 0.0758 p=0.000 ↑0.08 H-0.187 ███████···········
|
|
157
|
+
~ ### 1. Correction
|
|
158
|
+
delete 0.0308 p=0.000 ↑0.03 H-0.068 ███···············
|
|
159
|
+
mask 0.0442 p=0.000 ↑0.04 H-0.101 ████··············
|
|
160
|
+
~ ### 2. Warning
|
|
161
|
+
delete 0.0608 p=0.000 ↑0.06 H-0.144 █████·············
|
|
162
|
+
mask 0.0208 p=0.013 ↑0.02 H-0.046 ██················
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`size` is how far the mean distribution moved, `p` is the permutation test,
|
|
166
|
+
the arrow is the signed shift on the leading outcome, and `H` is the change in
|
|
167
|
+
entropy. On a Choice or Score run each row also carries `conf`, the change in
|
|
168
|
+
Jev's own confidence.
|
|
169
|
+
|
|
170
|
+
Read the arrows. Removing **Permanent Ban** takes the verdict *down* by 0.20 —
|
|
171
|
+
that section is what the verdict rests on. Removing any of the three milder
|
|
172
|
+
rungs pushes it *up*. The lesser consequences are competing alternatives, and
|
|
173
|
+
deleting a competitor makes the harsh answer look more proportionate. The ladder
|
|
174
|
+
behaves like a ladder, and nobody had to tell the tool that.
|
|
175
|
+
|
|
176
|
+
Everything else in the document — the pledge, the scope, the standards, the
|
|
177
|
+
enforcement sections, the attribution — is inside the noise on this question.
|
|
178
|
+
`## Our Standards` establishes that the conduct is a violation, but it has
|
|
179
|
+
nothing to say about *which rung*, and the measurement shows that.
|
|
180
|
+
|
|
181
|
+
`### 2. Warning` is marked `~`: deleting it moves the number three times as far
|
|
182
|
+
as masking it does. That is the deletion confound made visible, and it is
|
|
183
|
+
reported rather than ranked as if it were solid.
|
|
184
|
+
|
|
185
|
+
The `H` column reads the same story a second way. Removing **Permanent Ban**
|
|
186
|
+
raises the entropy — take the top rung away and the question becomes harder to
|
|
187
|
+
settle. Removing any of the milder rungs *lowers* it, because there is one less
|
|
188
|
+
competing answer.
|
|
189
|
+
|
|
190
|
+
## The other two readings
|
|
191
|
+
|
|
192
|
+
`deadweight` is the same run inverted — the segments the verdict does not rest
|
|
193
|
+
on, with the ones that moved detectably but negligibly called out separately.
|
|
194
|
+
|
|
195
|
+
`haze` reads the shape of the distribution rather than its argmax. Asked whether
|
|
196
|
+
off-platform conduct falls within the Code of Conduct's scope, Jev answers
|
|
197
|
+
`out_of_scope` on all eight baseline calls — and the distribution is nearly
|
|
198
|
+
flat:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
BASELINE (unmodified document, one call each)
|
|
202
|
+
[0] out_of_scope [in_scope=0.28, out_of_scope=0.72] conf=0.44
|
|
203
|
+
[1] out_of_scope [in_scope=0.31, out_of_scope=0.69] conf=0.38
|
|
204
|
+
[2] out_of_scope [in_scope=0.22, out_of_scope=0.78] conf=0.55
|
|
205
|
+
... 10 calls, every one answering out_of_scope
|
|
206
|
+
entropy 0.860 of maximum
|
|
207
|
+
|
|
208
|
+
HAZE 0.860 ██████████████████████████····
|
|
209
|
+
|
|
210
|
+
The distribution is close to flat. With well-formed options that is a fact
|
|
211
|
+
about the input, not the model: this document does not decide the question.
|
|
212
|
+
Jev's own confidence: 0.44.
|
|
213
|
+
|
|
214
|
+
WHERE THE AMBIGUITY LIVES (change in entropy when a segment is removed)
|
|
215
|
+
entropy noise floor 0.144: the baseline's own entropy wanders this much
|
|
216
|
+
between identical calls, so smaller shifts are not reported.
|
|
217
|
+
|
|
218
|
+
No segment shifted the entropy further than the baseline shifts on
|
|
219
|
+
its own. The ambiguity is spread across this document rather than
|
|
220
|
+
located in any one section.
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The answer looks decisive and is not: ten calls, ten identical verdicts, and a
|
|
224
|
+
distribution that is 86% of the way to flat. Anything reading only
|
|
225
|
+
`answer.choice` would ship that as settled.
|
|
226
|
+
|
|
227
|
+
Note what the tool then declines to do. Its first instinct is to name the
|
|
228
|
+
sections responsible, but the baseline's own entropy wanders by 0.144 between
|
|
229
|
+
identical calls — normalised entropy is steep near p=0.3, so the ordinary
|
|
230
|
+
jitter in the probability is amplified in the entropy — and no segment shifted
|
|
231
|
+
it further than that. So it reports nothing, and says why. An earlier run at
|
|
232
|
+
fewer replicates did name two sections; they did not survive the floor at
|
|
233
|
+
higher power, which is exactly the outcome the floor exists to produce.
|
|
234
|
+
|
|
235
|
+
## Verdicts
|
|
236
|
+
|
|
237
|
+
| | |
|
|
238
|
+
|---|---|
|
|
239
|
+
| `++` corroborated | Above the floor under both modes, same direction. A finding. |
|
|
240
|
+
| `~` mode-sensitive | Above the floor under one mode only. The deletion confound, visible. |
|
|
241
|
+
| `?` indeterminate | Moved further than the floor without repeating consistently enough to assert. |
|
|
242
|
+
| *(none)* null | Effect below the floor in both modes, with the power to have seen more. |
|
|
243
|
+
| `XX` failed | A call did not come back. Never silently a zero. |
|
|
244
|
+
|
|
245
|
+
A finding has to clear two independent hurdles. **Effect size** must exceed the
|
|
246
|
+
noise floor — a movement smaller than the difference between two identical calls
|
|
247
|
+
is not worth reporting. And an exact **permutation test** must rule out this
|
|
248
|
+
run's jitter: pool the baseline and perturbed readings, and count how often a
|
|
249
|
+
random split separates the means as far as the real one did. Both statistics use
|
|
250
|
+
the mean distribution of each group, so both sharpen as replicates increase.
|
|
251
|
+
|
|
252
|
+
A big move measured once proves nothing. A perfectly repeatable move of half a
|
|
253
|
+
grid step is not worth knowing. Both hurdles, or it is not a finding.
|
|
254
|
+
|
|
255
|
+
## Usage
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
vernier ablate DOCUMENT rank segments by how far their removal moves the answer
|
|
259
|
+
vernier deadweight DOCUMENT the same run inverted: segments that move nothing
|
|
260
|
+
vernier haze DOCUMENT read the shape of the distribution, not its argmax
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Ask with `--noul QUESTION`, `--choice QUESTION --option a --option b`,
|
|
264
|
+
`--score QUESTION --level low --level high`, or `--question-file spec.json`.
|
|
265
|
+
|
|
266
|
+
| flag | |
|
|
267
|
+
|---|---|
|
|
268
|
+
| `--by` | `section` (default), `paragraph`, `sentence`, `line`, `item` |
|
|
269
|
+
| `--mode` | `delete`, `mask`; repeatable, both by default |
|
|
270
|
+
| `--baseline-calls K` | noise-floor replicates (default 5; 8 or more gives a steadier floor) |
|
|
271
|
+
| `--trial-calls K` | replicates per perturbed variant (default 3) |
|
|
272
|
+
| `--controls N` | placebo segments to inject (default 2) |
|
|
273
|
+
| `--no-control` | skip the control condition — the report will say the run is uncontrolled |
|
|
274
|
+
| `--json` | the whole measurement, including every replicate |
|
|
275
|
+
| `--stub` | run the entire pipeline offline against a synthetic model |
|
|
276
|
+
|
|
277
|
+
Exit codes: `0` measured, `1` invalid or aborted, `2` usage, `3` error.
|
|
278
|
+
|
|
279
|
+
A run is `segments × modes × trial-calls + baseline-calls` requests. Ablation
|
|
280
|
+
varies the state, so each variant must be its own request — there is nothing to
|
|
281
|
+
batch. Concurrency is bounded, retries follow the documented back-off, and a
|
|
282
|
+
failed call becomes a marked failure rather than a convenient zero.
|
|
283
|
+
|
|
284
|
+
## Layout
|
|
285
|
+
|
|
286
|
+
| module | |
|
|
287
|
+
|---|---|
|
|
288
|
+
| `__init__.py` | the public surface: a re-export facade, nothing else |
|
|
289
|
+
| `questions.py` | `noul` / `choice` / `score` constructors |
|
|
290
|
+
| `segment.py` | pluggable segmenters; every one tiles the document byte-exactly |
|
|
291
|
+
| `perturb.py` | delete and mask, plus placebo injection |
|
|
292
|
+
| `filler.py` | the neutral corpus both of those draw on |
|
|
293
|
+
| `client.py` | the System One client, and the stub that stands in for it |
|
|
294
|
+
| `distance.py` | TVD, Jensen-Shannon, entropy, and the resolution limit |
|
|
295
|
+
| `noise.py` | the noise floor, the permutation test, and the verdicts |
|
|
296
|
+
| `run.py` | orchestration: baseline, preflight, fan-out, assembly |
|
|
297
|
+
| `report.py` | rendering |
|
|
298
|
+
| `errors.py` | `VernierError`, the base every other error inherits |
|
|
299
|
+
| `cli.py` | argument parsing and exit codes; no measurement logic |
|
|
300
|
+
|
|
301
|
+
```sh
|
|
302
|
+
uv run pytest # no network, no key
|
|
303
|
+
uv run mypy
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The suite runs entirely against `StubJevClient`, which reproduces the three
|
|
307
|
+
quirks the statistics have to survive: answers on a two-decimal grid, identical
|
|
308
|
+
calls that differ slightly, and distributions that can saturate at 1.0.
|