Filing thresholds are duplicated between bin/boundary-classify and bin/ia_store.py — tuning one silently desynchronizes the FR-4.5 stability gate #14

Closed
opened 2026-07-29 22:10:01 +03:00 by issuer-agent · 1 comment

Why: MIN_SHARE_OF_SPEND = 0.02 is defined independently in two places:

  • bin/boundary-classify:23 (with MIN_INVOCATIONS=3 at :28 and MIN_ATTRIBUTED_TURNS=30 at :29)
  • bin/ia_store.py:27 (with MIN_INVOCATIONS=3 at :28)

bin/boundary-classify uses its copy to decide verdict: file vs boundary-question. bin/stability-classify imports MIN_SHARE_OF_SPEND from ia_store and uses it to test whether two windows crossed the filing threshold (FR-4.5 stability gate).

The gate's notion of "the filing threshold" and the classifier's actual filing threshold are now two separate constants that merely happen to be equal. Tuning one — which TASKS 2.5/4.5 explicitly anticipate — would silently desynchronize the other. The gate would test a threshold that no longer matches the classifier, and mis-gate without erroring.

ia_store.py was created precisely to prevent divergences like this (dcec9ca: "so both scripts hash signatures identically. A divergence there would make the gate match nothing and fail open"). The threshold constants should have centralized too.

Acceptance:

  • MIN_SHARE_OF_SPEND has a single definition in ia_store.py
  • bin/boundary-classify imports it from ia_store
  • bin/stability-classify continues to use the same source

Related / proper fix: Per TASKS 2.3 design intent, all classification thresholds should live in references/signals-catalog.md with runtime loading — tuning would be a reviewable diff, not scattered Python constants.

Links: FR-4.5 commit dcec9ca, TASKS 2.3 / 2.5 / 4.5, found during spec-quality review of design/spec.md

**Why**: `MIN_SHARE_OF_SPEND = 0.02` is defined independently in two places: - `bin/boundary-classify:23` (with `MIN_INVOCATIONS=3` at :28 and `MIN_ATTRIBUTED_TURNS=30` at :29) - `bin/ia_store.py:27` (with `MIN_INVOCATIONS=3` at :28) `bin/boundary-classify` uses its copy to decide `verdict: file` vs `boundary-question`. `bin/stability-classify` imports `MIN_SHARE_OF_SPEND` from `ia_store` and uses it to test whether two windows crossed the filing threshold (FR-4.5 stability gate). The gate's notion of "the filing threshold" and the classifier's actual filing threshold are now two separate constants that merely happen to be equal. Tuning one — which TASKS 2.5/4.5 explicitly anticipate — would silently desynchronize the other. The gate would test a threshold that no longer matches the classifier, and mis-gate without erroring. `ia_store.py` was created precisely to prevent divergences like this (dcec9ca: "so both scripts hash signatures identically. A divergence there would make the gate match nothing and fail open"). The threshold constants should have centralized too. **Acceptance**: - [ ] `MIN_SHARE_OF_SPEND` has a single definition in `ia_store.py` - [ ] `bin/boundary-classify` imports it from `ia_store` - [ ] `bin/stability-classify` continues to use the same source **Related / proper fix**: Per TASKS 2.3 design intent, all classification thresholds should live in `references/signals-catalog.md` with runtime loading — tuning would be a reviewable diff, not scattered Python constants. **Links**: FR-4.5 commit dcec9ca, TASKS 2.3 / 2.5 / 4.5, found during spec-quality review of design/spec.md
oleks added the agent-wip label 2026-07-30 05:22:12 +03:00
oleks closed this issue 2026-07-30 05:30:57 +03:00
Owner

Confirmed real, fixed, pushed.

Root cause: bin/boundary-classify:23 and bin/ia_store.py:27 each independently defined MIN_SHARE_OF_SPEND = 0.02. bin/boundary-classify used its own copy (line 137/141) to decide file vs boundary-question. bin/stability-classify imports MIN_SHARE_OF_SPEND from ia_store (not from boundary-classify) to run the FR-4.5 stability gate. The two constants only agreed by coincidence — nothing enforced it, so tuning one (as TASKS 2.5/4.5 explicitly anticipate) would silently desync the gate from the classifier it's supposed to gate, exactly as the issue describes. Verified with:

$ grep -n "MIN_SHARE_OF_SPEND\|MIN_INVOCATIONS\|MIN_ATTRIBUTED_TURNS" bin/boundary-classify bin/ia_store.py bin/stability-classify
bin/ia_store.py:27:MIN_SHARE_OF_SPEND = 0.02
...
bin/stability-classify:66:from ia_store import (DEFAULT_STORE, MIN_SHARE_OF_SPEND, build_index,
bin/boundary-classify:23:MIN_SHARE_OF_SPEND = 0.02

Issue body is accurate as filed and was not touched by any of today's other fixes to this repo (checked git log --oneline -20 before starting — boundary-classify/ia_store.py last touched in dcec9ca, well before today's session).

Fix (commit a218a31, pushed to main): ia_store.py is now the single definition; boundary-classify does from ia_store import MIN_SHARE_OF_SPEND instead of keeping its own copy, matching how stability-classify already imports from ia_store. All three acceptance boxes are satisfied.

Scope note: MIN_INVOCATIONS/MIN_ATTRIBUTED_TURNS are still duplicated between the two files, but left alone — stability-classify never imports or reads them, so their duplication doesn't create the same "fails silently mid-tune" risk the issue is about. The "Related / proper fix" (all thresholds centralized in references/signals-catalog.md with runtime loading) is a separate, larger change and out of scope here.

Verification:

$ bash tests/classify.test.sh   # exit 0, all checks "ok"
$ bash tests/stability.test.sh  # exit 0, all checks "ok", including
                                 # "(f) S4 — the real anxious candidate is caught with no human in the loop"

Both pass on the current tree, run after the fix and after push.

Note: this repo is not worktree-isolated for this session's fan-out, so bin/plugin-inventory/tests/inventory.test.sh/.claude-plugin/plugin.json show unrelated concurrent edits from a sibling agent working kotkan/claude-plugin-inference-arbitrage#10 — untouched by this fix, left for that agent to commit.

Confirmed real, fixed, pushed. **Root cause**: `bin/boundary-classify:23` and `bin/ia_store.py:27` each independently defined `MIN_SHARE_OF_SPEND = 0.02`. `bin/boundary-classify` used its own copy (line 137/141) to decide `file` vs `boundary-question`. `bin/stability-classify` imports `MIN_SHARE_OF_SPEND` from `ia_store` (not from `boundary-classify`) to run the FR-4.5 stability gate. The two constants only agreed by coincidence — nothing enforced it, so tuning one (as TASKS 2.5/4.5 explicitly anticipate) would silently desync the gate from the classifier it's supposed to gate, exactly as the issue describes. Verified with: ``` $ grep -n "MIN_SHARE_OF_SPEND\|MIN_INVOCATIONS\|MIN_ATTRIBUTED_TURNS" bin/boundary-classify bin/ia_store.py bin/stability-classify bin/ia_store.py:27:MIN_SHARE_OF_SPEND = 0.02 ... bin/stability-classify:66:from ia_store import (DEFAULT_STORE, MIN_SHARE_OF_SPEND, build_index, bin/boundary-classify:23:MIN_SHARE_OF_SPEND = 0.02 ``` Issue body is accurate as filed and was not touched by any of today's other fixes to this repo (checked `git log --oneline -20` before starting — `boundary-classify`/`ia_store.py` last touched in dcec9ca, well before today's session). **Fix** (commit a218a31, pushed to `main`): `ia_store.py` is now the single definition; `boundary-classify` does `from ia_store import MIN_SHARE_OF_SPEND` instead of keeping its own copy, matching how `stability-classify` already imports from `ia_store`. All three acceptance boxes are satisfied. **Scope note**: `MIN_INVOCATIONS`/`MIN_ATTRIBUTED_TURNS` are still duplicated between the two files, but left alone — `stability-classify` never imports or reads them, so their duplication doesn't create the same "fails silently mid-tune" risk the issue is about. The "Related / proper fix" (all thresholds centralized in `references/signals-catalog.md` with runtime loading) is a separate, larger change and out of scope here. **Verification**: ``` $ bash tests/classify.test.sh # exit 0, all checks "ok" $ bash tests/stability.test.sh # exit 0, all checks "ok", including # "(f) S4 — the real anxious candidate is caught with no human in the loop" ``` Both pass on the current tree, run after the fix and after push. Note: this repo is not worktree-isolated for this session's fan-out, so `bin/plugin-inventory`/`tests/inventory.test.sh`/`.claude-plugin/plugin.json` show unrelated concurrent edits from a sibling agent working kotkan/claude-plugin-inference-arbitrage#10 — untouched by this fix, left for that agent to commit.
admin removed the agent-wip label 2026-07-30 05:52:01 +03:00
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kotkan/claude-plugin-inference-arbitrage#14