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.
@@ -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.