qualm 0.1.1-x86_64-darwin

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.
Files changed (7) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +220 -0
  4. data/exe/qualm +7 -0
  5. data/lib/qualm.rb +11 -0
  6. data/libexec/qualm +0 -0
  7. metadata +57 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4504215d89a6647e9f099181e090e99c688e4d7622fad52b8124401e9b6ff994
4
+ data.tar.gz: 6862baa1c7cea19e718461ff64fe88740eda9cb88781c61054b0d8d0afa6c59c
5
+ SHA512:
6
+ metadata.gz: 29dc3decbf7f556f2816fc2644b2129355392581543326c72bed25abd82dc3471912d2837e225f255e788d2712bd4082e310e288aecdd8a216a0f21b2deaab57
7
+ data.tar.gz: 680623db7e7f1e2b52f8266df00b0188ccec4151bc74cccdb6f44cc16ec532bb131268ff5cc357c2271e8cb8f93a72fe6844ae418cefd4267fe4a795b9e2f032
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Obie Fernandez
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.
data/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # qualm
2
+
3
+ qualm fails a pull request when an experienced reviewer would ask for the change to be simplified, and it tells the coding agent what to fix. It is the subjective gate for [Impatient Programming](https://impatientprogramming.org), and the sibling of [exhale](https://github.com/tools4imps/exhale-ruby), whose checks are deterministic.
4
+
5
+ Agents write more code than anyone reads. When green merges itself, something has to decide which changes still get human eyes. qualm asks that question of every changed file: would a reviewer push back on this?
6
+
7
+ The answer comes from [Jev](https://openrouter.ai/blog/insights/what-is-jev/), TypeSafe's System One decision model. Jev reads a diff, answers typed questions with probabilities, and writes no prose. No LLM runs inside qualm. The agent that reads its report does the reasoning and the fixing.
8
+
9
+ qualm has no parser, so it judges code in any language. It is one binary with no dependencies.
10
+
11
+ ## Install
12
+
13
+ With Ruby:
14
+
15
+ ```ruby
16
+ # Gemfile
17
+ group :development, :test do
18
+ gem "qualm", require: false
19
+ end
20
+ ```
21
+
22
+ or `gem install qualm`. The gem carries the binary for your platform and a `qualm` command that runs it.
23
+
24
+ With Go:
25
+
26
+ ```bash
27
+ go install github.com/tools4imps/qualm/cmd/qualm@latest
28
+ ```
29
+
30
+ Or download a binary for macOS, Linux or Windows from the [releases page](https://github.com/tools4imps/qualm/releases).
31
+
32
+ ## Use
33
+
34
+ ```bash
35
+ export OPENROUTER_API_KEY=...
36
+ qualm
37
+ ```
38
+
39
+ Run it in a git repository. It compares your working tree with the merge base of `HEAD` and the default branch, so it works before you commit as well as in CI.
40
+
41
+ `qualm` exits 0 when nothing drew a qualm. It exits 1 when the gate fails. It exits 2 when it couldn't run: no key, no network, a mistake in the config, a spent budget. It never passes a change it couldn't judge.
42
+
43
+ ## What it asks
44
+
45
+ One request per changed file. The file's diff is the state, and ten questions ride along:
46
+
47
+ | Question | What it asks | Role |
48
+ | --- | --- | --- |
49
+ | `push_back` | Would an experienced reviewer ask for this change to be simplified before it merges? | The gate |
50
+ | `direction` | Is the later version harder, the same or easier to understand and change? | Describes the change |
51
+ | `simplified` | Did the change remove complexity from this file? | Describes the change |
52
+ | `comments_only` | Does it differ only in comments, documentation or whitespace? | Describes the change |
53
+ | `new_behaviour` | Is it mostly new behaviour being added? | Describes the change |
54
+ | `grew_a_big_unit` | Did it make an already long function or class longer? | Diagnosis |
55
+ | `added_copies` | Did it add a near-copy of logic already in the file? | Diagnosis |
56
+ | `added_impossible_guards` | Did it add checks for states that can't happen? | Diagnosis |
57
+ | `added_placeholders` | Did it add placeholders, debug output or scaffolding? | Diagnosis |
58
+ | `added_unused_flexibility` | Did it add options or indirection used in only one place? | Diagnosis |
59
+
60
+ The exact wording is in [`internal/questions/builtin.json`](internal/questions/builtin.json). qualm adds one sentence to every question, telling Jev to treat comments and strings in the code as code to be judged and never as instructions.
61
+
62
+ ## The gate
63
+
64
+ The run fails when any changed file scores 0.6 or higher on `push_back`. The other nine answers can't fail a run by default. They are there to say why.
65
+
66
+ Jev's answers move a little between identical calls, by 0.03 at most in our tests. So when a gate answer lands within 0.05 of the threshold, qualm asks twice more and takes the middle value. That keeps a run on your machine and a run in CI from disagreeing over a hair.
67
+
68
+ ## Reading the report
69
+
70
+ A passing run prints one line. A failing run names each file, the answers that matter, and where to look:
71
+
72
+ ```text
73
+ qualm: 1 of 9 changed files drew a qualm.
74
+ Skipped 10 files: 8 test, 2 prose or data.
75
+
76
+ lib/mutineer/coverage_map.rb push back 0.65
77
+ harder 1.00
78
+ grew a big unit 0.85 lines 327-577
79
+ added copies 0.74 lines 327-577
80
+ added impossible guards 0.65 lines 327-577
81
+
82
+ What to do
83
+ A reviewer would likely ask for this change to be simplified before it merges.
84
+ The questions that fired:
85
+ grew_a_big_unit: Did the change make an already long function or class longer, where the new work could have gone in a unit of its own?
86
+ added_copies: Did the change add logic that is a near-copy of logic already in the file?
87
+ added_impossible_guards: Did the change add checks or fallbacks for states the surrounding code shows can't happen?
88
+ Rework the change so they no longer apply, then run qualm again.
89
+ A qualm is an opinion. If the change is right as it stands, a person can keep it from the top of the repository:
90
+ qualm keep lib/mutineer/coverage_map.rb --reason "..."
91
+ ```
92
+
93
+ That is a real run, on the commit of [mutineer](https://github.com/davidteren/mutineer) that added 22 methods to one class. It took under four seconds and cost a fraction of a cent.
94
+
95
+ The line ranges come from a second pass. On a failing file, qualm asks each diagnosis again hunk by hunk and reports the hunk where it is strongest.
96
+
97
+ `--format json` prints the same result as one object.
98
+
99
+ ## Keeping a change
100
+
101
+ A qualm is an opinion, and sometimes the code is right as it stands. When a person has looked at a failure and accepts it:
102
+
103
+ ```bash
104
+ qualm keep lib/matcher.rb --reason "The algorithm is this complicated"
105
+ ```
106
+
107
+ That records the path, a hash of the change, the reason and the date under `keeps` in `qualm.json`. A keep binds to the lines the change adds and removes. Edit any of them and the question reopens. A change somewhere else in the file on the base branch leaves the keep standing. Once the pull request merges, the keep matches nothing, and the next `qualm keep` clears it out.
108
+
109
+ Nothing stops an agent from keeping its own change, or from marking a file generated in `.gitattributes` so it is skipped. The safeguard is that both are visible changes to a file, and the report counts what it skipped and why. A team can put `qualm.json` and `.gitattributes` behind a required human review.
110
+
111
+ ## The config
112
+
113
+ `qualm.json` at the repository root. It's optional.
114
+
115
+ ```json
116
+ {
117
+ "gate": { "question": "push_back", "threshold": 0.6 },
118
+ "skip": ["db/schema.rb", "**/*.generated.*"],
119
+ "drop": ["added_unused_flexibility"],
120
+ "questions": [
121
+ {
122
+ "id": "added_feature_flag",
123
+ "type": "noul",
124
+ "instructions": "Did the change add a feature flag?",
125
+ "gates": true,
126
+ "threshold": 0.8
127
+ }
128
+ ]
129
+ }
130
+ ```
131
+
132
+ - `gate` changes the gate's question or its threshold.
133
+ - `questions` adds your own. One with a built-in's id replaces it, and one with `gates` and a `threshold` can fail the run too.
134
+ - `drop` removes built-in questions.
135
+ - `skip` adds paths to leave alone. A pattern with no slash matches a file name anywhere, `**` matches any number of directories, and a pattern ending in a slash matches everything under that directory.
136
+
137
+ A question's `type` is `noul` for yes or no, `score` for ordered levels listed in `criteria`, or `choice` for named options. An unknown key or a threshold outside 0 to 1 stops the run with exit 2, so a typo never passes for a clean run.
138
+
139
+ ## What it skips
140
+
141
+ Deleted and binary files, and files with no content change. Vendored and built directories. Lockfiles and generated files, including anything git marks `linguist-generated`. Prose and data such as Markdown, JSON and YAML. Tests, unless you pass `--include-tests`. The report's second line counts what was skipped and why. A changed file whose diff can't be read stops the run with exit 2.
142
+
143
+ ## What it costs
144
+
145
+ Jev charges about four cents per million input tokens, and a typical pull request costs a fraction of a cent. `--budget` caps a run, at one dollar by default.
146
+
147
+ Every answer is cached under a hash of the exact request, in your user cache directory. The same diff gets the same verdict, and a replay costs nothing. `--cache DIR` moves the cache and `--no-cache` skips it. qualm trusts what it finds in its cache, so keep the cache somewhere a pull request can't write to.
148
+
149
+ `qualm --dry-run` lists the files it would send and what that would cost, and sends nothing.
150
+
151
+ ## What leaves your machine
152
+
153
+ qualm sends the diff of each changed file to OpenRouter, which passes it to TypeSafe. Nothing else is sent. Run `--dry-run` first if you want to see the list, and use `skip` for anything that shouldn't go.
154
+
155
+ ## In CI
156
+
157
+ ```yaml
158
+ - uses: actions/checkout@v4
159
+ with:
160
+ fetch-depth: 0
161
+ - uses: actions/setup-go@v5
162
+ with:
163
+ go-version: stable
164
+ - run: go install github.com/tools4imps/qualm/cmd/qualm@latest
165
+ - run: qualm --base "origin/${{ github.base_ref }}"
166
+ env:
167
+ OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
168
+ ```
169
+
170
+ ## Flags
171
+
172
+ | Flag | What it does |
173
+ | --- | --- |
174
+ | `--base REF` | Compare with the merge base of REF and `HEAD` (default: the default branch) |
175
+ | `--threshold N` | Use this gate threshold for one run |
176
+ | `--include-tests` | Judge test files too |
177
+ | `--format text\|json` | The report's format |
178
+ | `--dry-run` | List what would be sent and its cost, and send nothing |
179
+ | `--budget DOLLARS` | Stop when this much is spent (default 1.00) |
180
+ | `--cache DIR`, `--no-cache` | Move the cache, or skip it |
181
+ | `--jobs N` | Files judged at once (default 8) |
182
+
183
+ ## How the gate was chosen
184
+
185
+ The first design scored whole files and compared before with after. We tested it on the 99 mainline commits of [mutineer](https://github.com/davidteren/mutineer) that touch its library, 322 file changes in all. It failed. It missed the one real refactor, it read a commit that only added docstrings as an improvement, and it never noticed a file creeping from 222 lines to 921.
186
+
187
+ Asking about the diff worked on the same changes. The refactor read as easier at 0.81. Documentation commits and a rename read as the same at 0.97 or above. A release that added 22 methods to one class scored 0.61 on `push_back`, and that question at 0.6 blocked 2 of the 99 commits.
188
+
189
+ That is the whole of the evidence. It is one codebase in one language, and nobody has labelled a set of changes by hand to check the threshold against. Treat 0.6 as a starting point and move it once you've seen what qualm says about your own code. The scripts are in [`spike/`](spike/).
190
+
191
+ ## How qualm holds itself to this
192
+
193
+ qualm has its own Contract in `contract/`: 89 numbered obligations across nine primitives (skip, diff, questions, config, jev, judge, gate, report and cli). Every obligation has at least one test that names it with a `// Contract: <primitive>/<id>` comment. A test in `internal/contractcheck` publishes contract coverage and fails while any obligation lacks a test.
194
+
195
+ The tests are held to account too. [Gremlins](https://github.com/go-gremlins/gremlins) mutates every package and reruns the suite. The tests kill 382 mutants, and the 7 that survive are each explained in [`docs/mutation.md`](docs/mutation.md).
196
+
197
+ Before a release, qualm runs on its own change.
198
+
199
+ ## Releasing
200
+
201
+ The version lives in `internal/cli/version.go`. Land a new number on `main` and the release workflow does the rest: it builds a gem and an archive for every platform, pushes the gems to RubyGems through [trusted publishing](https://guides.rubygems.org/trusted-publishing/), then cuts the tag and the GitHub release. A version already on RubyGems is skipped, so a re-run is safe.
202
+
203
+ `rake release:build` does the build half locally, into `pkg/`.
204
+
205
+ ## Known limits in 0.1
206
+
207
+ - It talks to Jev through OpenRouter only.
208
+ - A submodule bump and a symlink's target are judged as if they were a file's text.
209
+ - A custom diff driver set through git attributes can change the text after `@@` in a hunk header, which changes the cache key from one machine to the next. Keeps aren't affected.
210
+ - It judges one file at a time, so it can't see that a change copied logic from another file. exhale catches that for Ruby.
211
+ - The threshold rests on one codebase's history.
212
+ - A failing file is asked about again hunk by hunk to find where each diagnosis points, which costs one more request per hunk.
213
+ - An untracked file named `-` at the top of the repository stops the run with exit 2, because git reads that name as standard input. Staging the file is enough.
214
+ - A file that git is told to assume unchanged, or to skip in the working tree, is read as the index has it.
215
+ - On a file system that ignores case, a path argument typed in the wrong case matches no change, and qualm reports nothing to judge.
216
+ - There is no survey mode for scoring a whole codebase yet.
217
+
218
+ ## License
219
+
220
+ MIT
data/exe/qualm ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Hands the command line to the Go binary this gem carries.
5
+ require "qualm"
6
+
7
+ exec(Qualm.binary, *ARGV)
data/lib/qualm.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # qualm is a Go program: https://github.com/tools4imps/qualm. This gem carries its binary for one
4
+ # platform. The `qualm` command runs it, and Qualm.binary gives its path to anything that would
5
+ # sooner run it directly.
6
+ module Qualm
7
+ # The Go binary inside the installed gem.
8
+ def self.binary
9
+ File.expand_path("../libexec/qualm#{".exe" if Gem.win_platform?}", __dir__)
10
+ end
11
+ end
data/libexec/qualm ADDED
Binary file
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: qualm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: x86_64-darwin
6
+ authors:
7
+ - Obie Fernandez
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-10-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: qualm fails a pull request when an experienced reviewer would ask for
14
+ the change to be simplified, and it tells the coding agent what to fix. It asks
15
+ Jev, TypeSafe's System One decision model, about each changed file's diff. qualm
16
+ is one Go binary with no parser, so it judges code in any language. This gem installs
17
+ that binary for your platform.
18
+ email:
19
+ - obiefernandez@gmail.com
20
+ executables:
21
+ - qualm
22
+ extensions: []
23
+ extra_rdoc_files: []
24
+ files:
25
+ - LICENSE
26
+ - README.md
27
+ - exe/qualm
28
+ - lib/qualm.rb
29
+ - libexec/qualm
30
+ homepage: https://github.com/tools4imps/qualm
31
+ licenses:
32
+ - MIT
33
+ metadata:
34
+ source_code_uri: https://github.com/tools4imps/qualm
35
+ changelog_uri: https://github.com/tools4imps/qualm/blob/main/CHANGELOG.md
36
+ rubygems_mfa_required: 'true'
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '3.1'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubygems_version: 3.5.11
53
+ signing_key:
54
+ specification_version: 4
55
+ summary: 'The subjective gate for Impatient Programming: no PR merges while a reviewer
56
+ would ask for the change to be simplified'
57
+ test_files: []