Skip to content

test(reverse-whois): record the 0.60/0.30 calibration basis and pin #122's Jaccard arms (ENG-5374) - #127

Open
santiago-praetorian wants to merge 10 commits into
mainfrom
santiagogimenezocano/eng-5374-pius-reverse-whois-calibrate-corroboration-thresholds-fix
Open

test(reverse-whois): record the 0.60/0.30 calibration basis and pin #122's Jaccard arms (ENG-5374)#127
santiago-praetorian wants to merge 10 commits into
mainfrom
santiagogimenezocano/eng-5374-pius-reverse-whois-calibrate-corroboration-thresholds-fix

Conversation

@santiago-praetorian

@santiago-praetorian santiago-praetorian commented Aug 5, 2026

Copy link
Copy Markdown

Fixes ENG-5374

⛔ STACKED ON #122 — merge #122 first

This PR now depends on #122 (ENG-5172) and merges its branch in. It must not merge before #122, and if #122's head moves during its review I will re-merge.

Reading the diff: until #122 lands, GitHub diffs this branch against main, so #122's own changes appear here toojaccardTokenSets, tokenSetContained, the verifier's swap onto Jaccard. Those are not up for review in this PR. To see only what this PR adds, diff against #122's head (33ae0b9): CLAUDE.md +1 line, reverse_whois_verify.go +52 lines all comment (zero added non-comment lines), reverse_whois_verify_test.go a retitle, and reverse_whois_calibration_test.go new. Once #122 merges, this collapses to exactly that.

ENG-5374's Item 1 (the over-corroboration fix) is delivered by #122, not by this PR. This PR carries no production predicate of its own any more — see the retraction below. What it does carry is the recorded calibration basis for simCorroborate/simMismatch, the enumeration that proves #122's metric closes the class rather than assuming it, the tests pinning two behavior changes #122 makes, and one corrected mislabeled test case.

Retraction: this PR's original production fix was redundant, and is deleted

The first three revisions of this PR added a shared-evidence floor to the corroborate arm — corroborationHasResolution / distinctSharedTokenCount / minCorroborateTokens = 2. All three are now deleted. #122 replaces the metric with Jaccard, and against Jaccard that floor is provably redundant:

Under Jaccard, sim = m/(kq+kc-m) for distinct token counts kq,kc and shared distinct count m. Asking which shapes can corroborate on a single shared token:

1/(kq+kc-1) >= 0.60   ⟹   kq+kc <= 2   ⟹   kq = kc = 1

That is an exact bound, not a search limit — and kq=kc=1 means both sides normalize to the same single token ("Praetorian" vs "Praetorian Inc"), which is the legitimate match the plugin exists to find. So Jaccard already forces ≥2 shared distinct tokens to corroborate. Under the old containment coefficient m / min(kq,kc) the same question admitted an unbounded family — every (1, kc) shape scored 1/1 = 1.0 on one shared token — which was exactly the ENG-5374 defect.

The floor was not merely redundant but a small regression on top of Jaccard: "Acme Acme" vs "Acme" has distinct token sets that are the same singleton (Jaccard 1.0, correctly corroborated), yet the two differ as joined strings, so the floor's equality exemption missed and it de-ranked a pair #122 scores correctly. It is deliberately not reinstated, and the reason is recorded in reverse_whois_verify.go so a future reader doesn't re-add it.

Premise-gate verdict on the ticket's Item 2: WRONG-LAYER — unchanged, and metric-independent

This part of the review stands exactly as before; #122 does not touch it.

The ticket asked for: a hermetic fixture corpus + measurement harness, run to confirm-or-reset simCorroborate (0.60) and simMismatch (0.30), on the premise that the mis-scoring is attributable to those values.

Why that premise is refuted — corpus-free, by enumeration. At kq=kc=1 the reachable similarity set is exactly {0.0, 1.0}; the ambiguous arm is unreachable. So there the cutoffs have zero discriminating power for every possible value of simCorroborate in (0,1] — re-setting 0.60 to 0.7/0.8/0.95 changes no verdict. This held for the containment coefficient and holds identically for Jaccard, which is why neither ticket is a re-tuning. The defect was the metric's resolution, never the threshold's value.

Why the fixture corpus was also refuted, independently — label provenance. "0.60 is the right cutoff" is a precision/recall claim at a decision boundary. It needs (i) candidates from the real ViewDNS/Whoxy return distribution, (ii) live-resolved registrant strings, (iii) per-candidate ownership labels of independent provenance. (ii) and (iii) are forbidden by this ticket's own hermetic AC, and fixtures authored here cannot substitute: their labels would be assigned by the same judgment that picked the threshold, so any precision/recall figure would be the prior re-derived wearing the authority of a measurement. That work needs production data and stays in ENG-5166, which already carries the AC verbatim.

Constants stay at 0.60 / 0.30, confirmed rather than silently dropped, with the basis written down in the CALIBRATION BASIS block in reverse_whois_verify.go and in CLAUDE.md.

What this PR contributes on top of #122

1. The enumeration, re-derived for Jaccard (reverse_whois_calibration_test.go, TestJaccardReachabilityByTokenCount). Corpus-free. Every cell's expected similarity and expected arm is hand-written, and production is checked against the table — so a regression has to be mirrored in both encodings to pass. Two tables: equal-count (kq=kc=k, k=1..6) and asymmetric (kq<kc), the second because the equal-count construction structurally cannot express the defect's shape.

2. Two of #122's behavior changes, pinned as deliberate. Neither was covered before:

shape old (containment) new (Jaccard) why it matters
kq=2, kc=4, m=2 1.0corroborated 0.5unverified containment now scores kq/kc, so it no longer corroborates regardless of how much longer the candidate is
kq=1, kc=5, m=1 1.0 → corroborated 0.2unverified, not mismatch tokenSetContained rescues it from the de-rank arm

The second row's contrast is the whole justification for the containment guard, so it is pinned against its non-contained sibling kq=2, kc=5, m=1 (0.1667mismatch), checked against production rather than against the table's own expectation. Both sides are first asserted to sit below simMismatch, so the contrast cannot silently degenerate into "one of them was simply above the threshold".

Worth stating precisely, because the obvious summary is wrong: #122 did not make containment stop corroborating. Under Jaccard a fully-contained pair scores exactly kq/kc, so it still corroborates whenever the candidate is at most ~1.67× longer (kq=3, kc=4, m=30.75). What #122 removed is containment's power to corroborate regardless of how much longer the candidate is — the old coefficient returned 1.0 for every kc. Both table rows above sit at kc = 2·kq, hence exactly 0.5. The test comment states the narrow claim rather than the tidy one.

3. A corrected mislabeled test case. A pre-existing TestDecideConfidence row was titled "corroborated partial (shorter fully contained)" while its inputs ("Acme" vs "Acme Corp") reduce after suffix-stripping to a single identical token — i.e. Jaccard 1/1 = 1.0, an equality, not containment. The old title claimed the table covered non-exact single-token containment, which it never did. That gap is precisely what let the over-corroboration defect pass review. Retitled to "corroborated exact equality after suffix stripping (acme == acme)"; inputs and expectation untouched. Attributed against merge base d760ec8 — both #122 and this PR retitled that same row, and this PR's title is the accurate one.

4. A recorded known limitation, now measurably smaller. Under containment, the similarity value could only fall below simMismatch with a shared token present once kq+kc >= 8, so in practice the mismatch arm meant "no shared tokens at all". Jaccard divides by the union, so that drops to kq+kc >= 5 (kq=1, kc=4, m=10.25). Note the distinction that makes the guard necessary: crossing the threshold is a statement about the value, not about the arm firing — that particular shape is pure containment, so tokenSetContained holds it at unverified anyway. The value-crossing region grows; the de-rank region does not follow it blindly. For equal-count shapes, where containment cannot apply, the first k at which mismatch is genuinely reachable with a shared token moves from k=4 to k=3 (0.20), and that figure is asserted. tokenSimilarity's own multiset/set asymmetry is untouched (github_org still scores against it and pins a floor assertion) and stays tracked as ENG-5890.

Scope discipline

github_org.go / github_org_test.go carry #122's content, auto-merged with no conflict — this PR contributed no change to either, before or after the merge. github_org.go is byte-identical to #122's version; github_org_test.go is #122's content plus origin/main's network-error test refactor (TestGitHubOrgPlugin_Run_GracefulOnHTTPError / …OnNetworkError), which this branch picked up in revision 4's origin/main merge and #122 predates — so that hunk is origin/main's, not this PR's, and #122 acquires it on its own merge. jaccardTokenSets and TestJaccardTokenSets in those files are #122's, and both are attributed to it in the diff. pkg/plugins/domains/ shows 5 files listed by gofmt -l; all 5 are byte-identical to origin/main (pre-existing drift, verified, and CI is green on main), so they are untouched here.

Verification

gofmt clean on every file this PR touches · go vet ./... clean · go build ./cmd/pius clean · go test -count=1 ./... green. All tests hermetic — no live ViewDNS/Whoxy/RDAP/WHOIS, no keys, no network. De-rank never drop: every outcome inside [0.35, 0.65), exactly one confidenceDecision per decideConfidence call.

Revision history

rev what why
1 per-side distinct-token floor initial fix for Item 1
2 distinct shared token floor Codex round-1: a repeated token passed as two
3 floor moved to the distinct intersection Codex round-2: a 3-token side with a repeat still cleared a per-side floor
4 merge origin/main pick up test-timeout fixes
5 merge #122; delete the floor entirely; re-derive the enumeration for Jaccard the floor is provably redundant under Jaccard, and would regress duplicate-token equal-singleton inputs
6 replace the 34 of the 342 shape count with a closed form (comment-only) Codex round-4 P3: the count was real but rested on an undisclosed, arbitrary bound (kq,kc <= 12); the family is infinite, so it is now stated as kc > 10*kq/3

Four rounds of automated review have been adjudicated. Revisions 1–4: 11 findings (3 confirmed and fixed, 5 refuted with evidence, 1 no-result, 2 clean). Revision 5 emptied the production diff, so it was re-reviewed from scratch rather than inheriting those rounds — round 4 ran 5 findings (1 confirmed and fixed, 1 refuted as out of scope, 1 recorded as a known limit, 1 clean, 1 no-result). Verdicts and evidence are in the adjudication comment on this PR.

jnovak-praetorian and others added 3 commits August 5, 2026 18:16
… over-corroboration (ENG-5172)

decideConfidence compared the query org against a candidate's resolved
registrant with tokenSimilarity, which divides by the SHORTER token set
(containment). For a single-token query org, any registrant that merely
CONTAINS that token scored 1.0 and corroborated at 0.60 (top of the
needs_review band). e.g. registrant "Acme Enterprises LLC" (normalized
{acme, enterprises}) vs query {acme} -> 1/1 = 1.0.

Add tokenJaccard (|A n B| / |A u B|) and use it in the reverse-whois
verifier so both sides' distinguishing tokens count against the score. The
Acme-Enterprises case now scores 1/2 = 0.5, landing in the unverified band
(needs review) rather than spuriously corroborating. Genuine exact matches
after legal-suffix stripping ("Acme" vs "Acme Corp") still corroborate at
1.0. The simCorroborate (0.60) / simMismatch (0.30) thresholds are
unchanged, now Jaccard-interpreted.

github_org's name-similarity signal deliberately keeps tokenSimilarity
(containment): there a partial name overlap is a weak, 0.25-weighted hint,
not a corroboration gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…token test

- Remove the unreachable `if union == 0` guard in the Jaccard core (Gemini):
  after the empty-side early return both token sets hold >= 1 element, so the
  union is always >= 1. Extract the token-slice core as jaccardTokenSets.
- decideConfidence tokenizes each org once via a new normalizeOrgTokens and
  feeds the slices straight to jaccardTokenSets, dropping the redundant
  re-tokenize that tokenJaccard(normalizeOrg(...), ...) performed (Gemini).
  normalizeOrg is retained as a thin string wrapper for its other callers/tests.
- Add a duplicate-token case to TestTokenJaccard so the distinct-set semantics
  are pinned (CodeRabbit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te (ENG-5374)

tokenSimilarity is the containment coefficient m/min(kq,kc), so its
denominator discards the longer side's unmatched tokens and the value is
invariant under adding disconfirming evidence: query org "Apple" scores
exactly 1.0 against "Apple Tree Landscaping", against "Apple Tree
Landscaping Of Tampa Bay", and against every longer superstring. Feeding
that to a fixed threshold made an unrelated landscaping company
corroborate at the top of the needs_review band.

Gate the corroborate arm of decideConfidence on
corroborationHasResolution: exact equality after normalizeOrg, or a
minimum token count of 2. The exact-equality exemption is load-bearing --
normalizeOrg strips legal-form suffixes, so the legitimate "Praetorian"
vs "Praetorian Inc" reduces to one identical token, and a bare floor
would de-rank exactly the matches this plugin exists to find. The guard
sits after the empty-normalization check and is confined to the
corroborate arm, so a clean single-token mismatch ("Apple" vs "Walmart",
m=0) still de-ranks rather than being rescued.

The shared metric is untouched. Its other consumer, the github-org
plugin, takes similarity as a graded multiplier (0.25*sim) with no
decision boundary, and that plugin's test pins the shorter-denominator
behavior as desired via a GreaterOrEqual floor at 1.0. Changing the
denominator would fail that pin and halve the name contribution for every
single-token org -- a behavior change this ticket never scoped.

Thresholds confirmed, not re-tuned, with the basis recorded. Because sim
is always m/k, the reachable value set at k=1 is exactly {0.0, 1.0}: the
ambiguous arm is unreachable and the cutoffs have zero discriminating
power there for every possible simCorroborate in (0,1]. Re-setting 0.60
would not have changed this defect's verdict at all -- the problem was the
metric's resolution, not the threshold's value. Recorded known
limitation: below k=4, simMismatch is reachable only at m=0.

Calibration is therefore evidenced by enumeration rather than by a
fixture corpus. That 0.60/0.30 are precision-optimal is not establishable
hermetically -- it needs candidates from the real ViewDNS/Whoxy
distribution, live-resolved registrant strings, and ownership labels of
independent provenance, the latter two forbidden by this ticket's
hermetic requirement. Hand-authored fixtures cannot substitute, since
their labels would come from the same judgment that picked the threshold.

Full containment at k>=2 is deliberately left alone: crossing
simCorroborate there already forces at least 2 shared normalized tokens,
and the ambiguous arm is reachable, so the cutoffs retain real
discriminating power.

Also retitles a mislabeled existing case. "corroborated partial (shorter
fully contained)" used "Acme" vs "Acme Corp", but normalizeOrg strips
"corp" so both sides become "acme" -- an exact equality. No pinned case
ever exercised non-exact single-token containment, which is the hole that
let this defect survive.

De-rank-never-drop holds: the fix only ever moves 0.60 to 0.50, both
inside [0.35, 0.65), so every candidate is still emitted and still
flagged for review. One confidenceDecision per call, one AddConfidence
call, and the existing PII-free justification is reused, so no registrant
string enters the output.

Fixes ENG-5374

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Instruction-File Drift Detection

Code changes in this PR may have made documentation stale:

CLAUDE.md

Section Issue Evidence
"Reverse-WHOIS Verify-After-Retrieve" (lines 112-115) Documented corroboration condition is incomplete The condition "corroborated (similarity ≥ 0.60)" is no longer accurate. PR adds a new corroborationHasResolution() guard in decideConfidence (line 650 of reverse_whois_verify.go) that is now required alongside the similarity check. At k=1 (single token), corroboration now additionally requires exact string equality. This affects the documented behavior but CLAUDE.md does not mention this guard or the minCorroborateTokens = 2 constant.

Automated drift check — please review and update if needed.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Review

Critical Issues

  • pkg/plugins/domains/reverse_whois_verify.go:455 uses raw token count for the new resolution guard, but tokenSimilarity counts duplicate tokens on the shorter side against a set of longer-side tokens. A query like "Apple Apple" versus "Apple Tree Landscaping" is non-equal, has only one distinct shared token, but gets sim == 1.0 and min(len(tokens)) == 2, so decideConfidence at :390 still returns corroborated. That leaves the ENG-5374 false-positive class open whenever one side repeats the shared token. Consider basing the guard on distinct shared tokens or deduping tokens for this decision.

Security

No security concerns flagged.

Suggestions

No suggestions.


Reviewed by Codex (gpt-5.5)

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@santiago-praetorian, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 67a19aaa-3692-4e15-b694-f62924ea9de5

📥 Commits

Reviewing files that changed from the base of the PR and between dd5ceb1 and 19190ef.

📒 Files selected for processing (1)
  • pkg/plugins/domains/reverse_whois_verify.go

Walkthrough

Reverse-WHOIS confidence classification now uses Jaccard similarity over normalized distinct token sets. Symmetric containment handling keeps contained names unverified while sparse, non-contained overlaps can be de-ranked. The change adds token-set scoring and updates normalization and confidence decisions. New tests cover reachable similarity values, classification paths, review-band invariants, justifications, saturation, monotonicity, symmetry, and token-set containment.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch santiagogimenezocano/eng-5374-pius-reverse-whois-calibrate-corroboration-thresholds-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/plugins/domains/reverse_whois_calibration_test.go`:
- Around line 374-379: Correct the explanatory comment around
assert.Len(covered, 5) to state that covered is keyed by the table’s declared
returnPoint labels and the assertion only detects an extra label added to the
table without updating the expected count. Do not claim it detects new return
statements in decideConfidence that lack a matching table case.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 81851412-0761-4076-9308-5bbd997b7853

📥 Commits

Reviewing files that changed from the base of the PR and between d760ec8 and 4106b96.

📒 Files selected for processing (3)
  • pkg/plugins/domains/reverse_whois_calibration_test.go
  • pkg/plugins/domains/reverse_whois_verify.go
  • pkg/plugins/domains/reverse_whois_verify_test.go

Comment thread pkg/plugins/domains/reverse_whois_calibration_test.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4106b96c0c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if nq == nc {
return true
}
return min(len(tokenize(nq)), len(tokenize(nc))) >= minCorroborateTokens

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count distinct tokens before allowing corroboration

For org names that repeat the same token, this guard treats duplicate occurrences as independent evidence and lets the ENG-5374 false-positive class through. For example decideConfidence("Apple Apple", org("Apple Tree"), nil) still corroborates: tokenSimilarity counts both apple occurrences as matches and returns 1.0, while this min(len(tokenize(...))) check sees two tokens and allows the corroborate arm even though there is only one distinct shared token and unmatched remainder. If the resolution floor is meant to require at least two corroborating tokens, it needs to be based on distinct normalized tokens/shared evidence, not raw token slice length.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CONFIRMED — fixed in 20678b3.

Reproduced before changing anything. The mechanism is exactly as described, and it is worse than the "Apple Apple" example suggests because it is reachable from realistic WHOIS strings: normalizeOrg strips legal-form suffixes and rejoins whatever is left, so a registrant reading "Acme Corp Acme Inc" normalizes to "acme acme" — two raw tokens carrying one distinct token. Against a query org of "Acme Tree Services" that cleared min(3, 2) >= minCorroborateTokens, then scored m/k = 2/2 = 1.0, and corroborated at 0.60 on a single shared token. No duplicated word in the query is needed.

Root cause is the one you name: tokenSimilarity compares a multiset against a set — it walks the shorter side's token slice while testing membership in a map built from the longer side — so a repeat scores a match on every occurrence. A raw-length floor therefore counts one token of evidence as two.

Fix, per your suggestion: corroborationHasResolution now counts distinct normalized tokens on both sides via a new distinctTokenCount helper. Outcomes, all pinned as table cases:

query registrant normalized (raw/distinct) sim before after
Acme Tree Services Acme Corp Acme Inc 3/3 vs 2/1 1.00 0.60 corroborated 0.50 unverified
Apple Apple Apple Tree Landscaping 2/1 vs 3/3 1.00 0.60 corroborated 0.50 unverified
Praetorian Security Praetorian Security Group 2/2 vs 3/3 1.00 0.60 0.60 corroborated (unchanged)
Praetorian Praetorian Inc equal after normalization 1.00 0.60 0.60 corroborated (exemption)
Apple Walmart 1/1 vs 1/1 0.00 0.40 0.40 mismatch, not rescued

Both new cases fail under an -overlay reversion to the raw-length form, and nothing else in the package does — which also confirms the pre-existing suite had no coverage of this class.

One scope note recorded in the code rather than silently: the multiset/set asymmetry inside tokenSimilarity is not fixed, because that is the metric the github-org plugin scores against and its pinned floor assertion forbids changing it (the same constraint that leaves k >= 2 saturation out of scope here). sim still returns 1.0 for those first two rows — the guard, not a cutoff, is what withholds the corroboration. The metric-level defect is being filed separately, since it also lets github-org award its full 0.25 name-similarity credit on a duplicated token.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-2 re-review request. These three reviewers only trigger on pull_request: [opened, ready_for_review], not synchronize, so none of you saw commit 20678b3 — the fix for the P2 raw-token-count finding above. Requesting it explicitly per the mention gate.

What changed in 20678b3 (4 files, 140+/11-):

  • corroborationHasResolution now measures distinct normalized tokens via a new distinctTokenCount helper (slices.Sort + slices.Compact over the freshly allocated tokenize slice), so "acme acme" counts as k=1 and can no longer clear minCorroborateTokens.
  • Doc block above the predicate explains why the floor must be distinct-counted, and states explicitly that the metric-level asymmetry inside tokenSimilarity is not fixed here (now tracked as ENG-5890).
  • Four new TestDecideConfidence cases plus TestDistinctTokenCount pin the class in both directions (duplicate on the candidate side, duplicate on the query side, duplicate with zero overlap still de-ranking, and full containment at two genuinely distinct tokens still corroborating).
  • The overclaiming comment on assert.Len(t, covered, 5) in the calibration test was corrected; the assertion itself is byte-identical.

Specific things worth attacking:

  1. Is slices.Sort on tokenize's return genuinely safe — i.e. is that slice freshly allocated on every call with no other holder? The comment claims it is.
  2. Does the distinct-token floor change any pre-existing classification outcome? I believe not (an -overlay reversion check makes exactly the two new duplicate-token cases fail and nothing else), but a second opinion on reachability is welcome.
  3. github_org.go and github_org_test.go are byte-identical to origin/main — deliberate, because that plugin's test pins containment as desired. Confirm the diff really is empty.

@codex @gemini @claude

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Review

Critical issues

None. The fix is correct and narrowly scoped: gating the corroborate arm on corroborationHasResolution(nq, nc) closes the k=1 saturation defect without touching the shared tokenSimilarity (empty github_org.go diff confirms this). The predicate is placed after the empty-after-normalization guard so the unverifiable/mismatch distinction survives, and it is confined to the corroborate arm so a clean k=1/m=0 mismatch (Apple vs Walmart) still de-ranks to 0.40 rather than being rescued. The nq == nc exemption compares normalizeOrg output, so legitimate suffix-only diffs (Praetorian vs Praetorian Inc) still corroborate. The min builtin is fine on go 1.25. De-rank-never-drop holds (0.60 -> 0.50, both in-band).

Security

No security concerns flagged. No new network calls, no keys, and the change reuses the existing PII-free unverified justification with no interpolation of the resolved registrant string — it strictly reduces false corroboration of unrelated orgs.

Test coverage

Excellent. Enumeration/property tests cover the reachability ladder, argument-swap symmetry, monotonicity, and all five decideConfidence return points; the ENG-5374 case and exact-equality exemption are pinned in the pre-existing table, and the PR reports the guard was reversion-tested via -overlay (4 functions fail without the fix).

No critical issues — LGTM pending human review.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gemini Review

Critical Issues

None.

Security

No security concerns flagged.

Suggestions

  • Optimize token counting in corroborationHasResolution: Because nq and nc are already normalized by normalizeOrg, they are guaranteed to be single-space-delimited strings containing only [a-z0-9]. Calling tokenize() on them again redundantly allocates a strings.Builder and new slices for strings that are already sanitized. You can safely replace len(tokenize(nq)) with strings.Count(nq, " ") + 1 (and the same for nc) to compute the token counts with zero allocations while preserving the exact same behavior.

Reviewed by Gemini (gemini-3.1-pro-preview)

jnovak-praetorian and others added 3 commits August 5, 2026 20:44
…s (ENG-5172)

Switching decideConfidence to Jaccard fixed the single-token
over-corroboration this ticket was filed for, but pushed the same
containment shape off the other end of the band. Jaccard divides by the
union, so a query org fully contained in a longer registrant sinks on
length asymmetry alone: "Walmart" vs "Walmart Global Enterprises Holdings
LLC" is {walmart} vs {walmart, global, enterprises, holdings} = 1/4 =
0.25, under simMismatch (0.30). The bare threshold read that as a clear
MISMATCH and de-ranked a plausible subsidiary to 0.40 -- below a
candidate whose WHOIS never resolved at all. The PR's own doc comment
asserted such a name "stays unverified", which was false.

Sparse overlap is not the de-rank test; disagreement is. Add
tokenSetContained (distinct tokens, symmetric, false on an empty side)
and gate the de-rank arm on it, so reaching 0.40 now requires that each
side contribute a token the other lacks. A contained pair is
under-specification, not contradiction, and falls through to
confReverseWhoisUnverified (0.50).

Thresholds, justifications, tokenSimilarity, tokenJaccard and
jaccardTokenSets are unchanged, as is github_org.go's deliberate
retention of containment for its weak 0.25-weighted name hint. The
de-rank-never-drop invariant is preserved -- every outcome stays in
[0.35, 0.65).

Reported by Codex (P2) on PR #122. Verified red-before-green: with the
containment gate reverted, exactly the two new cases fail package-wide
(0.40 where 0.50 is required) and all 15 pre-existing TestDecideConfidence
subtests -- both disjoint-mismatch boundary cases included -- still pass,
so the new cases discriminate the fix rather than the file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round-2 review found tokenJaccard had no production caller — only its own
test. It was a one-line wrapper over jaccardTokenSets, which is what
decideConfidence actually calls.

The wrapper was also the double-tokenizing form, so the next caller reaching
for the string-shaped helper would have silently reintroduced a second
tokenize pass. jaccardTokenSets is now the single entry point.

tokenSimilarity is kept: unlike tokenJaccard it has a live production caller
at github_org.go:209, where containment is the deliberate choice for the weak
0.25-weighted name hint.

The Jaccard-vs-containment rationale that lived on the wrapper's doc comment
is merged into jaccardTokenSets rather than deleted. TestTokenJaccard becomes
TestJaccardTokenSets, tokenizing at the call site so it still covers
tokenize + Jaccard together; all 9 cases carry over unchanged, including the
duplicate-token case added earlier for CodeRabbit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…(ENG-5374)

Adjudication of automated review findings on PR #127.

CONFIRMED (codex-review, P2): the resolution floor added by the previous
commit counted raw token-slice length, so a repeated token passed as two
independent ones and the ENG-5374 false-positive class survived. The
shared similarity metric compares a multiset against a set -- it walks
the shorter side's token slice while testing membership in a map built
from the longer side -- so one shared token repeated on the shorter side
scores a match on every occurrence, and normalizeOrg does not dedupe.
A registrant reading "<Name> Corp <Name> Inc" normalizes to two raw
tokens carrying one distinct token, cleared k >= 2, then scored
m/k = 2/2 = 1.0 against a query org sharing only that token.

corroborationHasResolution now counts DISTINCT normalized tokens via a
new distinctTokenCount helper. Reproduced before fixing and pinned after:
"Acme Tree Services" vs "Acme Corp Acme Inc" and "Apple Apple" vs
"Apple Tree Landscaping" both drop 0.60 -> 0.50, and both fail under an
overlay reversion to the raw-length form. Controls pin the narrowing as
deliberate: two-distinct-token containment still corroborates, the
exact-equality exemption still corroborates, and a clean k=1/m=0
mismatch still de-ranks to 0.40 rather than being rescued.

The multiset/set asymmetry in the shared metric is documented as NOT
fixed -- it is the metric the github-org plugin scores against and its
pinned floor assertion forbids changing it. The guard only prevents that
duplicate-counting from producing a corroboration.

CONFIRMED (CodeRabbit): a comment overclaimed what Len(covered, 5)
detects. It is keyed on labels declared in the table, so a sixth return
statement in decideConfidence with no matching case still passes.
Comment corrected to state that scope; the assertion is unchanged.

CONFIRMED (drift-check): the repo instruction file's documented
corroboration condition was incomplete. Updated to record the
resolution requirement and why.

REFUTED (gemini-review): replacing the token count with
strings.Count(s, " ")+1 is not behavior-identical -- it returns 1 for
the empty string where tokenize returns 0 -- and it relies on an
upstream caller invariant for correctness. It is also moot now that the
floor needs distinct tokens.

Shared metric untouched: the github-org plugin and its test stay
byte-identical to origin/main.

Refs ENG-5374
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@santiago-praetorian

Copy link
Copy Markdown
Author

Automated-review adjudication — round 1

Every finding read against the cited code and either reproduced-then-fixed or refuted with evidence. Pushed as 20678b3.

# Reviewer Finding Verdict
1 codex-review (P2, also in review body) Resolution floor counts raw token length, so a repeated token passes as two — ENG-5374 class survives CONFIRMED → fixed
2 CodeRabbit Comment overclaims what assert.Len(covered, 5) detects CONFIRMED → fixed
3 drift-check CLAUDE.md corroboration condition now incomplete CONFIRMED → fixed
4 gemini-review Replace len(tokenize(...)) with strings.Count(s, " ")+1 for zero allocations REFUTED
5 claude-code-action No critical issues no action

#1 and #2 are answered in their own threads. #1 was the real one: it was reachable from realistic WHOIS strings, not just the contrived "Apple Apple" — a registrant "Acme Corp Acme Inc" normalizes to "acme acme" (suffix stripping, no dedupe), which is 2 raw / 1 distinct token, cleared k >= 2, then scored 2/2 = 1.0. corroborationHasResolution now counts distinct normalized tokens. Both new cases fail under an -overlay reversion to the raw-length form and nothing else in the package does, which also shows the pre-existing suite never covered the class.

#3 CONFIRMED. The bullet said corroborated (similarity ≥ 0.60), which is no longer the whole condition. It now records the resolution requirement — 2 distinct normalized tokens per side or exact normalized equality — and why a single-token query org otherwise saturates the containment metric at 1.0.

#4 REFUTED, on behavior rather than on taste. strings.Count(s, " ")+1 is not equivalent to len(tokenize(s)): for the empty string it returns 1 where tokenize returns 0. It is safe today only because decideConfidence returns early on nq == "" || nc == "" before the guard is reached — so the rewrite would make a local function silently depend on a caller-side invariant, and would break if the guard were ever moved or reused. The allocation it saves sits immediately after a per-candidate RDAP or WHOIS network lookup (10s timeout), so there is no measurable win to weigh against that. It is moot in any case now that the floor needs distinct tokens, which requires materializing the set the suggestion was avoiding.

Verification after the fix

gofmt -l (changed files)   empty     go vet ./...   exit 0
go build -o /dev/null      exit 0    go test -count=1 ./...   ALL ok (domains 119.104s)

git diff origin/main -- pkg/plugins/domains/github_org.go pkg/plugins/domains/github_org_test.go is still empty — the shared metric and its pinned test remain byte-identical to base, as the premise verdict requires.

Two things a human reviewer needs to decide

  1. This PR overlaps #122 (ENG-5172). That PR is open against the same base and edits the same two files, fixing the same single-token over-corroboration defect by a different mechanism — adding tokenJaccard and swapping the reverse-whois verifier onto it, while leaving tokenSimilarity for github-org. Both are reverse-whois-local and both are currently MERGEABLE against main, but they will conflict with each other textually and semantically. Jaccard is the broader change: it would also close the k >= 2 containment saturation and the simMismatch-does-nothing-below-k=4 limitation that this PR documents as known-and-not-fixed. It also re-interprets the unchanged 0.60/0.30 cutoffs against a new denominator, which is the calibration question ENG-5374's premise gate found is not answerable from hermetic data. These should not both merge as-is — worth a maintainer call on which mechanism wins before either lands.
  2. The metric-level defect is not fixed here. tokenSimilarity compares a multiset against a set, so it credits a repeated token once per occurrence. This PR only stops that from producing a corroboration in reverse-whois; the metric still returns 1.0, and github-org still awards its full 0.25 name-similarity contribution on a duplicated token. Being filed as its own ticket, since changing the shared metric is what the pinned github_org floor assertion forbids.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gemini Review

{
"session_id": "db12ba5e-aea3-464b-9dbd-964f4c702da3",
"response": "",
"stats": {
"models": {
"gemini-3.1-pro-preview": {
"api": {
"totalRequests": 2,
"totalErrors": 0,
"totalLatencyMs": 4386
},
"tokens": {
"input": 23240,
"prompt": 31385,
"candidates": 26,
"total": 31482,
"cached": 8145,
"thoughts": 71,
"tool": 0
},
"roles": {
"main": {
"totalRequests": 2,
"totalErrors": 0,
"totalLatencyMs": 4386,
"tokens": {
"input": 23240,
"prompt": 31385,
"candidates": 26,
"total": 31482,
"cached": 8145,
"thoughts": 71,
"tool": 0
}
}
}
}
},
"tools": {
"totalCalls": 1,
"totalSuccess": 1,
"totalFail": 0,
"totalDurationMs": 9,
"totalDecisions": {
"accept": 1,
"reject": 0,
"modify": 0,
"auto_accept": 0
},
"byName": {
"read_file": {
"count": 1,
"success": 1,
"fail": 0,
"durationMs": 9,
"decisions": {
"accept": 1,
"reject": 0,
"modify": 0,
"auto_accept": 0
}
}
}
},
"files": {
"totalLinesAdded": 0,
"totalLinesRemoved": 0
}
},
"error": {
"type": "INVALID_STREAM",
"message": "Invalid stream: The model returned an empty response or malformed tool call."
}
}EOF
gemini_errors<<EOF
Warning: 256-color support not detected. Using a terminal with at least 256-color support is recommended for a better visual experience.
Warning: tools.exclude in settings.json is deprecated and will be removed in 1.0. Migrate to Policy Engine: https://geminicli.com/docs/core/policy-engine/
YOLO mode is enabled. All tool calls will be automatically approved.
YOLO mode is enabled. All tool calls will be automatically approved.
Ripgrep is not available. Falling back to GrepTool.
[STARTUP] Phase 'cleanup_ops' was started but never ended. Skipping metrics.
[STARTUP] Cannot measure phase 'cleanup_ops': start mark 'startup:cleanup_ops:start' not found (likely cleared by reset).


Reviewed by Gemini (gemini-3.1-pro-preview)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Review

No critical issues — LGTM pending human review.

Note: focused tests could not be run because the sandbox is read-only and Go could not create module/cache directories under /home/runner/go or /tmp.


Reviewed by Codex (gpt-5.5)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 20678b306e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


// distinctTokenCount counts the distinct tokens in s — the resolution-bearing
// token count for corroborationHasResolution, where a repeated token is one
// piece of evidence rather than several. Sorting in place is safe because

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count shared distinct tokens before corroborating

Fresh evidence in this version is that the new guard only checks each side has two distinct tokens, not that two distinct tokens are shared. For example, queryOrg="Acme Acme Holdings" and registrant "Acme Landscaping Tampa" normalize to raw token counts of 3 and distinct counts of 2/3, so this line passes; tokenSimilarity then counts the repeated acme twice (2/3 >= 0.60) and the candidate still corroborates on one independent shared token. This leaves the duplicate-token false-positive class open whenever the duplicated side also carries one unrelated distinct token; the floor needs to be based on the distinct intersection/shared evidence, not just per-side distinct cardinality.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CONFIRMED — fixed in b4dd124 (branch now at c494241). Good catch; this was a real hole and your example is exactly right.

I verified it by executing the real package functions under go test -overlay rather than reasoning about it, because it turns on whether holdings survives normalization (it does — it is deliberately excluded from orgLegalSuffixes as a disambiguating token):

q="Acme Acme Holdings" c="Acme Landscaping Tampa"
  nq="acme acme holdings" nc="acme landscaping tampa"
  rawq=3 rawc=3 dq=2 dc=3 distinctShared=1 sim=0.6667 gate=true corroborates=true

So the per-side floor I added in 20678b3 measured the wrong thing. Two distinct tokens on a side says nothing about shared evidence; the property that matters is two distinct tokens in the intersection. The floor is now:

return distinctSharedTokenCount(nq, nc) >= minCorroborateTokens

The shared count is bounded by each side's distinct count, so this strictly subsumes the retired predicate — one floor, not an AND. distinctTokenCount is deleted. The candidate side is deduped before it is walked; skipping that would have reproduced inside the floor the very multiset/set asymmetry the floor exists to neutralize.

After the fix both defect pairs land at 0.50 unverified — de-ranked inside [0.35, 0.65), not dropped — while "Praetorian Security" vs "Praetorian Security Group" (distinctShared=2) still corroborates at 0.60, "Praetorian"/"Praetorian Inc" still corroborates through the exact-equality exemption, and "Apple"/"Walmart" still de-ranks to 0.40 rather than being rescued.


Your finding also refutes the Claude review on this PR, which is worth recording since the two reviews contradict each other and only one can be right. Claude wrote:

To pass the min(distinct) >= 2 gate while corroborating on one real shared token, the shorter side would need a repeat — which drops its distinct count below 2 and the gate refuses it. Robust.

That inference does not hold: a three-raw-token side can carry a repeat and still have two distinct tokens. "acme acme holdings" is precisely that shape, which is why it passed the gate. Claude's "Robust" conclusion on this specific point is REFUTED; the rest of its review (guard placement, PII, band invariant, no new network calls) is accurate and independently verified.

Gemini produced no round-2 review — its run succeeded but the model returned INVALID_STREAM: The model returned an empty response or malformed tool call, so there is nothing to adjudicate from it. Re-requesting below rather than counting silence as approval.


Also corrected the reasoning this finding falsified, in four comment sites plus the repo doc. The claim "crossing simCorroborate at k >= 2 already forces m >= 2 shared normalized tokens" was load-bearing for leaving k >= 2 out of scope, and it is falsem counts matching occurrences, not distinct tokens. The guard now enforces the shared-evidence property directly instead of assuming the metric implies it, and what remains out of scope is stated plainly: full containment on two genuinely distinct shared tokens still corroborates, which is real evidence rather than a duplicate artifact.

Reversion proof (mine, re-run independently — not relayed from the implementing agent): under an overlay restoring the per-side floor, exactly four subtests across three test functions fail, all of them the new round-2 cases, and nothing else in the package does. That is direct evidence the pre-existing suite could not have caught this class.

The metric itself is still unrepaired — github-org scores against the same tokenSimilarity and its pinned GreaterOrEqual floor assertion encodes containment as desired for that caller — now tracked as ENG-5890. github_org.go and github_org_test.go are byte-identical to origin/main.

This branch also merged origin/main (upstream unit-test timeout fixes); domains now runs in 2.6s instead of 116s.

Round-3 re-review please — the fix for a round-2 finding is itself unreviewed, and these workflows do not trigger on synchronize. @codex @gemini @claude

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: c4942418a4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Review

Critical issues

None. The fix is minimal and correct: gating the corroborate arm on corroborationHasResolution(nq, nc) closes the k=1 saturation class (sim = m/min(kq,kc) divides by the shorter side, so any single shared token scored 1.0). Verified statically:

  • slices is already imported, so distinctTokenCount builds; unverified is pre-constructed so the fall-through preserves de-rank-never-drop; the guard sits after the empty-normalization guard and is confined to the corroborate arm, so a clean m=0 mismatch still de-ranks rather than being rescued.
  • The distinct-token floor (vs raw slice length) genuinely closes the duplicate loophole: tokenSimilarity divides by raw len(shorter) and matches per-occurrence, so "acme acme" would otherwise pose as k=2. To pass the min(distinct) >= 2 gate while corroborating on one real shared token, the shorter side would need a repeat — which drops its distinct count below 2 and the gate refuses it. Robust.

Security

No security concerns flagged. No new network calls; the fix reuses the existing PII-free justifyReverseWhoisUnverified string with no interpolation, so no resolved registrant reaches output. The SSRF-safe dialer / byte-cap machinery is untouched.

Test coverage

Excellent and unusually rigorous — the asymmetric pair builder reproduces the exact defect shape (kq=1, kc>=2, m=1, strings never equal), reversion-testing confirmed four test functions fail without the guard, and the query-side/candidate-side duplicate cases plus benign controls pin the distinct-count floor from both directions.

No critical issues — LGTM pending human review.

… floor (ENG-5374)

Round-2 review finding, confirmed by execution. The previous commit made the
resolution floor count distinct tokens PER SIDE, which is not the property that
matters. A side with three raw tokens can carry a repeat and still hold two
distinct tokens, clearing a per-side floor while only one token is genuinely
shared:

  query "Acme Acme Holdings"      -> "acme acme holdings"      (3 raw, 2 distinct)
  registrant "Acme Landscaping Tampa" -> "acme landscaping tampa" (3 raw, 3 distinct)
  min(2, 3) >= 2 passes the floor, yet exactly ONE token is shared.

The shared metric then credits the repeated "acme" once per occurrence -- it
walks the shorter side's token slice against a membership set -- so 2/3 = 0.667
crosses simCorroborate and the candidate corroborated on a single shared token.
"holdings" is deliberately not a legal-form suffix, so the duplicate survives
normalization.

The floor now measures the distinct token INTERSECTION. That count is bounded by
each side's distinct count, so the new predicate strictly subsumes the old one --
one floor, not an AND. The candidate side is deduped before it is walked, since
skipping that would reproduce inside the floor the same multiset/set asymmetry
the floor exists to neutralize.

Also retracts the inference the finding falsified, in four comment sites and the
repo doc: "crossing simCorroborate at k >= 2 already forces m >= 2 shared
tokens" is false, because m counts matching OCCURRENCES rather than distinct
tokens. The guard now enforces the shared-evidence property directly instead of
assuming the metric implies it. What remains out of scope is stated plainly:
full containment on two genuinely distinct shared tokens still corroborates,
which is real evidence rather than a duplicate artifact.

The shared metric itself is still not repaired -- the github-org plugin scores
against it and its pinned floor assertion encodes containment as desired for
that caller. Tracked as ENG-5890.

Verified independently, not relayed: gofmt clean, go vet exit 0, build exit 0,
full suite green (domains 116.280s). Under an overlay reverting the floor to its
per-side form, exactly four subtests across three test functions fail -- all of
them the new round-2 cases -- and nothing else in the package does, which is
direct evidence the pre-existing suite could not have caught this class. The
github-org plugin and its test are byte-identical to the merge base.

Refs ENG-5374, ENG-5890
…eng-5374-pius-reverse-whois-calibrate-corroboration-thresholds-fix

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Review

No critical issues — LGTM pending human review.

Note: I could not run tests because the sandbox is read-only and Go could not create its module/cache directories.


Reviewed by Codex (gpt-5.5)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gemini Review

Critical Issues

None.

Security

No security concerns flagged.

Suggestions

  • Optimize distinctSharedTokenCount: Since both queryTokens and candidateTokens are freshly allocated by tokenize, you could sort and compact both slices in-place. This allows you to find the number of shared tokens using a simple two-pointer intersection walk, completely avoiding the inQuery map allocation.
  • Redundant Tokenization: In decideConfidence, normalizeOrg tokenizes and joins the strings, tokenSimilarity tokenizes them again, and distinctSharedTokenCount tokenizes them a third time. To save allocations, consider refactoring normalizeOrg to return a []string and updating the downstream scoring helpers to accept token slices directly (though this would also require updating the shared tokenSimilarity usage in github_org.go).

Reviewed by Gemini (gemini-3.1-pro-preview)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Review

Critical issues

None. The fix is narrow and correct: corroborationHasResolution gates only the corroborate arm, and because sim >= simCorroborate (0.60) can never also satisfy sim < simMismatch (0.30), an unresolved high-similarity pair falls through to the existing unverified decision rather than being mis-classified as a mismatch. distinctSharedTokenCount dedupes both sides (query via set, candidate via slices.Compact), so the multiset/set asymmetry it exists to neutralize cannot leak back in, and the nq==nc exemption runs after the empty-string guard so it cannot fire on all-suffix orgs. Symmetry, band-membership, and de-rank-never-drop invariants all hold.

Security

No security concerns flagged. No new network calls or secrets; the change is a pure scoring refinement. Registrant PII in justifications stays guarded: the fix reuses the existing PII-free unverified justification with no interpolation, and TestDecideConfidenceEveryReturnPointStaysInsideTheReviewBand asserts no justification reproduces the resolved registrant.

Test coverage

Thorough. New reverse_whois_calibration_test.go (enumeration + property tests) plus TestDistinctSharedTokenCount, the ENG-5374 round-2 duplicate-token cases, and the exact-equality exemption pin. One note: that suite Len(covered, 5) check keys off table labels, not production, so a sixth return added to decideConfidence would not trip it (already flagged as a manual step in the comment, which is acceptable).

Note: I could not execute the suite here (build/test commands are permission-gated in this environment), so the above is a static review. The PR reports go test ./... green with a guard-reversion check failing four functions, which is consistent with the diff.

@santiago-praetorian

Copy link
Copy Markdown
Author

Automated-review adjudication — ENG-5374 (final round)

frame check skipped — no non-trivial machinery: the shipped diff swaps one predicate inside an existing guard at the layer that already owned the decision, adding no new layer, abstraction, or subsystem (condition: size). Re-examined after round 2 grew the diff to 4 files / +321−105, and the condition still holds: 222 of those lines are tests, and the round-2 change is the narrowest form of the same fix at the same layer.

The ticket's second item — build a calibration harness to derive the thresholds empirically — did get a premise verdict, and it was WRONG-LAYER: any harness buildable under the hermetic-test AC would score against the same synthetic corpus that produced the current constants, making it a circular measurement rather than a calibration. That verdict is discharged by compliance — the harness was not built. What shipped instead is an enumeration + property table that pins what can be established hermetically, and the calibrate-against-production-data work stays in ENG-5166, which already carries that AC verbatim. Nothing undischarged remains.

reviewer coverage @ c4942418: ADJUDICATION-READY — every expected reviewer cleared (coverage-gate.mjs --repo praetorian-inc/pius --pr 127). Rounds 2 and 3 each required an explicit mention re-trigger: codex-code / gemini-code / claude-code fire on pull_request: [opened, ready_for_review] and pull_request_review_comment: [created]not synchronize — so a new commit is never re-reviewed on its own. Any run that stops at green CI skips these reviewers entirely.

Findings

# Round Reviewer Finding Verdict
1 1 codex The resolution floor counts raw tokens, so one repeated token passes as two CONFIRMED → fixed in 20678b3
2 1 coderabbit A comment overclaims what assert.Len(covered, 5) can catch CONFIRMED → comment corrected in 20678b3; assertion byte-identical. Re-confirmed addressed by CodeRabbit at 21:09:40Z
3 1 claude-md-drift CLAUDE.md states only the similarity threshold as the corroboration condition CONFIRMEDCLAUDE.md:113 updated in 20678b3
4 1 gemini Use strings.Count(s," ")+1 instead of tokenize for a zero-allocation count REFUTED — not behavior-identical (returns 1 for "" where tokenize returns 0), so it would make a local function depend on a caller-side early-return invariant; the saved allocation sits immediately after a 10s-timeout RDAP/WHOIS lookup; and it is moot once the floor must count distinct tokens, which a space count cannot do
5 2 codex The distinct-per-side floor still corroborates on one shared token: a three-raw-token side carrying a repeat keeps 2 distinct tokens, clears min(dq,dc) >= 2, and reaches sim 0.667 on a single shared token CONFIRMED → fixed in b4dd124 (details below)
6 2 claude "To pass the min(distinct) >= 2 gate while corroborating on one real shared token, the shorter side would need a repeat — which drops its distinct count below 2. Robust." REFUTED on that point — a three-raw-token side carries a repeat and still holds 2 distinct tokens, which is exactly finding 5. Two reviewers contradicted each other and codex was right; recorded because a bot LGTM is not evidence. The rest of that review is accurate and independently verified
7 2 gemini — no review produced — NO RESULT — the run concluded success but the model returned INVALID_STREAM: "The model returned an empty response or malformed tool call". Nothing to adjudicate; silence was not counted as approval, so round 3 re-requested it and it answered
8 3 gemini Replace the inQuery map with a two-pointer intersection walk over both sorted+compacted slices, "completely avoiding the inQuery map allocation" REFUTED on its stated premise — measured, not asserted (below)
9 3 gemini Refactor normalizeOrg to return []string and thread token slices through the scoring helpers REFUTED as out of scope — the suggestion itself notes it "would also require updating the shared tokenSimilarity usage in github_org.go", and this PR deliberately keeps github_org.go / github_org_test.go byte-identical to origin/main (verified: empty diff). That file pins {"Praetorian","Praetorian Security"} -> 1.0 with a GreaterOrEqual floor, i.e. containment is desired for that caller, so the shared metric cannot be changed from here. Also collides with #122. The metric-level work is tracked as ENG-5890
10 3 codex No major issues — LGTM pending human review no action
11 3 claude No critical issues; confirms the fix gates only the corroborate arm, that both sides are deduped, and that the nq==nc exemption runs after the empty-string guard no action — and this round its reasoning is correct, unlike finding 6

Finding 5 — the confirmation, and what it falsified

Verified by executing the real package functions under go test -overlay before accepting it, because reachability turns on whether holdings survives normalizeOrg (it does — deliberately excluded from orgLegalSuffixes as a disambiguating token):

q="Acme Acme Holdings" c="Acme Landscaping Tampa"
  nq="acme acme holdings" nc="acme landscaping tampa"
  dq=2 dc=3 distinctShared=1 sim=0.6667 gate=true corroborates=true   <-- DEFECT

The floor is now the distinct intersection, not per-side cardinality:

return distinctSharedTokenCount(nq, nc) >= minCorroborateTokens

The shared count is bounded by each side's distinct count, so this strictly subsumes the retired predicate — one floor, not an AND. distinctTokenCount is deleted. The candidate side is deduped before it is walked; skipping that would reproduce inside the floor the very multiset/set asymmetry the floor exists to neutralize.

It also falsified the inference that had justified leaving the k>=2 case out of scope — "crossing simCorroborate at k >= 2 already forces m >= 2 shared normalized tokens" is false, because m counts matching occurrences, not distinct tokens. Retracted at four comment sites plus CLAUDE.md, each with a note that it must not be reinstated, and where the same false inference had leaked into the calibration test's prose (assertions there byte-identical).

Reversion proof (run here, not relayed from the implementing agent — an overlay restoring the per-side floor, with exactly one verbatim match asserted before replacing so a silent no-op stub was impossible): exactly four subtests across three test functions fail, all of them the new round-2 cases, and nothing else in the package does. Direct evidence the pre-existing suite could not have caught this class.

Finding 8 — measured

The suggestion is behaviorally identical (169 ordered pairs over a 13-name corpus agree) and ~8% faster in CPU. But its stated benefit does not exist:

BenchmarkSharedMap-12           1513-1537 ns/op    1024 B/op    37 allocs/op
BenchmarkSharedTwoPointer-12    1404-1439 ns/op    1024 B/op    37 allocs/op

Identical bytes and identical allocation counts, because there is no map allocation to avoid — escape analysis puts it on the stack:

reverse_whois_verify.go:523:17: make(map[string]struct {}, len(queryTokens)) does not escape

The residual ~24ns/call is real but immaterial: at the maxReverseWhoisCandidates cap of 500, a whole pass saves ~12µs against a 90s pass budget, on a path entered immediately after a lookup bounded by a 10s network timeout. Declining, with the measurement on the record so this doesn't need re-litigating.

Verification

gofmt clean · go vet ./... exit 0 · go build ./cmd/pius exit 0 · go test -count=1 ./... all packages ok (domains 2.6s, down from 116s after merging origin/main's test-timeout fixes) · github_org.go and github_org_test.go byte-identical to origin/main · CI on c494241: all 14 checks plus titus and CodeRabbit SUCCESS.

Three rounds used, which is the full budget. Not merging — that stays a human decision, and #122 below needs one first.

⚠️ Still needs a maintainer decision: collision with #122

#122 (ENG-5172) adds tokenJaccard and swaps the reverse-whois verifier onto it while keeping tokenSimilarity for github_org. It fixes the same finding-1 defect by a different mechanism, edits the same two files, and is strictly broader. Both are MERGEABLE against main but conflict with each other textually and semantically. These should not both merge as-is.

Reviewer items collected through: 2026-08-05T21:58:10Z

#127 now stacks on #122 and must merge after it. ENG-5374's Item 1 — the
over-corroboration fix — is delivered by #122's Jaccard metric, not by this
branch, so this branch's own production predicate is deleted rather than
reconciled.

Deleted: corroborationHasResolution, distinctSharedTokenCount, and
minCorroborateTokens. Against Jaccard that floor is provably redundant, since
1/(kq+kc-1) >= 0.60 forces kq+kc <= 2, i.e. only kq=kc=1 — both sides the same
single token, which is the legitimate match the plugin exists to find. It was
also a small regression on top of Jaccard: "Acme Acme" vs "Acme" has distinct
token sets that are the same singleton (Jaccard 1.0, correctly corroborated) yet
differ as joined strings, so the floor's equality exemption missed and it
de-ranked a pair #122 scores correctly. The reason it must not be re-added is
recorded in reverse_whois_verify.go.

Conflicts resolved in #122's favor; what remains here is additive:

  reverse_whois_verify.go       #122's version, plus a comment-only CALIBRATION
                               BASIS block (zero added non-comment lines) — no
                               production predicate of its own.
  reverse_whois_calibration_test.go
                               re-derived for Jaccard.
                               TestSimilarityReachabilityByTokenCount ->
                               TestJaccardReachabilityByTokenCount; every cell's
                               expected similarity AND arm hand-written, so a
                               regression must be mirrored in both encodings to
                               pass. All 27 equal-count cells and 9 asymmetric
                               rows matched production on the first run.
  reverse_whois_verify_test.go  keeps this PR's retitle of the mislabeled
                               "corroborated partial (shorter fully contained)"
                               row; #122's competing title discarded, inputs and
                               expectation untouched.
  CLAUDE.md                    #122's two lines verbatim plus an additive
                               calibration-basis sub-bullet.

Also pinned as deliberate, neither previously covered: kq=2,kc=4,m=2 moves 1.0
corroborated -> 0.5 unverified, and kq=1,kc=5,m=1 moves 1.0 -> 0.2 UNVERIFIED
rather than mismatch, rescued from the de-rank arm by #122's tokenSetContained
and pinned against its non-contained sibling kq=2,kc=5,m=1 (0.1667, mismatch).
Stated narrowly: #122 did not make containment stop corroborating — a contained
pair scores exactly kq/kc, so kq=3,kc=4 is still 0.75 — it removed containment's
power to corroborate regardless of how much longer the candidate is.

github_org.go is byte-identical to #122; github_org_test.go is #122's content
plus origin/main's network-error test refactor picked up by this branch's earlier
merge. This branch contributes nothing to either.

Item 2 of the ticket (build a calibration harness) keeps its WRONG-LAYER premise
verdict, unchanged and metric-independent, and stays in ENG-5166.

gofmt clean · go vet ./... clean · go build ./cmd/pius clean · go test -count=1
./... green. Tests hermetic: no live ViewDNS/Whoxy/RDAP/WHOIS, no keys.

Refs ENG-5374, ENG-5172

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@santiago-praetorian santiago-praetorian changed the title fix(reverse-whois): require resolution before corroborating a candidate (ENG-5374) test(reverse-whois): record the 0.60/0.30 calibration basis and pin #122's Jaccard arms (ENG-5374) Aug 6, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/plugins/domains/github_org.go (1)

324-341: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider moving jaccardTokenSets next to its only consumer.

github_org.go deliberately keeps tokenSimilarity as its own metric, as the doc comment states. The only production caller of jaccardTokenSets is decideConfidence in reverse_whois_verify.go. A shared token-metric file, or reverse_whois_verify.go itself, would keep the dependency direction clearer. The set-building loop is also duplicated in tokenSetContained; a small distinctTokens([]string) map[string]bool helper would serve both. Behavior stays identical, so this is optional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/plugins/domains/github_org.go` around lines 324 - 341, Move
jaccardTokenSets from github_org.go next to its sole production consumer,
decideConfidence in reverse_whois_verify.go, or to an appropriate shared
token-metric file. Extract the duplicated token set construction into a
distinctTokens helper and reuse it from both jaccardTokenSets and
tokenSetContained, preserving all existing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/plugins/domains/github_org.go`:
- Around line 324-341: Move jaccardTokenSets from github_org.go next to its sole
production consumer, decideConfidence in reverse_whois_verify.go, or to an
appropriate shared token-metric file. Extract the duplicated token set
construction into a distinctTokens helper and reuse it from both
jaccardTokenSets and tokenSetContained, preserving all existing behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 465c92ac-aff3-464a-a339-46d68729dbde

📥 Commits

Reviewing files that changed from the base of the PR and between c494241 and dd5ceb1.

📒 Files selected for processing (6)
  • CLAUDE.md
  • pkg/plugins/domains/github_org.go
  • pkg/plugins/domains/github_org_test.go
  • pkg/plugins/domains/reverse_whois_calibration_test.go
  • pkg/plugins/domains/reverse_whois_verify.go
  • pkg/plugins/domains/reverse_whois_verify_test.go

// 0.95 would not have changed a single verdict. That is why neither ENG-5374 nor
// ENG-5172 was a threshold re-tuning: the defect was the metric's RESOLUTION, so
// both tickets changed what is measured and left the constants where they were.
func TestJaccardReachabilityByTokenCount(t *testing.T) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex @gemini @claude — please re-review from scratch. Revision 5 changed what this PR is, so the three review rounds already adjudicated on revisions 1–4 do not cover it; please do not rely on them.

What changed: this PR now stacks on #122 (ENG-5172) and merges its branch in. Its own production predicate — corroborationHasResolution / distinctSharedTokenCount / minCorroborateTokens — is deleted, because under #122's Jaccard metric it is provably redundant (1/(kq+kc-1) >= 0.60 forces kq+kc <= 2, i.e. only kq=kc=1) and was a small regression on duplicate-token equal-singleton inputs. reverse_whois_verify.go is now #122's version plus a comment-only CALIBRATION BASIS block — zero added non-comment lines. What remains here is tests and docs.

Reading the diff: until #122 merges, GitHub diffs this branch against main, so #122's changes appear here too (jaccardTokenSets, tokenSetContained, the verifier's swap onto Jaccard). Those are #122's and are not under review here. This PR's own contribution is the diff against 33ae0b9: CLAUDE.md +1 line, reverse_whois_verify.go +52 comment-only lines, a retitled TestDecideConfidence row, and this file.

Where I would most value scrutiny:

  1. The enumeration's correctness — this file hand-writes an expected similarity and an expected arm for all 27 equal-count cells and 9 asymmetric rows. If any cell is wrong, the table and production are wrong together and the test passes anyway. Please recompute a few independently rather than checking internal consistency.
  2. The redundancy argument for deleting the floor. If a shape corroborates on a single shared distinct token with kq+kc > 2 under Jaccard, the deletion is wrong and I want to know.
  3. The narrow claim about containment. I deliberately do not say "containment no longer corroborates" — a fully contained pair scores exactly kq/kc, so kq=3,kc=4,m=3 is 0.75 and still corroborates. The pinned rows sit at kc = 2*kq. Please check the comments state the narrow claim and not the tidy-but-false one.
  4. Whether the kq=1,kc=5,m=1 vs kq=2,kc=5,m=1 contrast actually isolates tokenSetContained as the cause.

On CI: ci / Test is red, and it is not this PR. The 8 failures are all in favicon_hash_test.go and reproduce on main's own tip (8de8a07) standalone — main is currently red for the same reason. Cause: favicon_hash.go:181 and :253 guard the CIDR observation with if err != nil on faviconHostCIDR's error, so a successfully parsed IP emits no CIDR finding. Introduced by #124 (commit a1a9b93), not by this branch, which does not touch that file. Reported separately; please review this PR's diff on its merits.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gemini Review

{
"session_id": "246967e0-4fdf-4b10-a35f-295ac4bc3019",
"response": "",
"stats": {
"models": {
"gemini-3.1-pro-preview": {
"api": {
"totalRequests": 2,
"totalErrors": 0,
"totalLatencyMs": 6614
},
"tokens": {
"input": 33887,
"prompt": 42047,
"candidates": 26,
"total": 42201,
"cached": 8160,
"thoughts": 128,
"tool": 0
},
"roles": {
"main": {
"totalRequests": 2,
"totalErrors": 0,
"totalLatencyMs": 6614,
"tokens": {
"input": 33887,
"prompt": 42047,
"candidates": 26,
"total": 42201,
"cached": 8160,
"thoughts": 128,
"tool": 0
}
}
}
}
},
"tools": {
"totalCalls": 1,
"totalSuccess": 1,
"totalFail": 0,
"totalDurationMs": 9,
"totalDecisions": {
"accept": 1,
"reject": 0,
"modify": 0,
"auto_accept": 0
},
"byName": {
"read_file": {
"count": 1,
"success": 1,
"fail": 0,
"durationMs": 9,
"decisions": {
"accept": 1,
"reject": 0,
"modify": 0,
"auto_accept": 0
}
}
}
},
"files": {
"totalLinesAdded": 0,
"totalLinesRemoved": 0
}
},
"error": {
"type": "INVALID_STREAM",
"message": "Invalid stream: The model returned an empty response or malformed tool call."
}
}EOF
gemini_errors<<EOF
Warning: 256-color support not detected. Using a terminal with at least 256-color support is recommended for a better visual experience.
Warning: tools.exclude in settings.json is deprecated and will be removed in 1.0. Migrate to Policy Engine: https://geminicli.com/docs/core/policy-engine/
YOLO mode is enabled. All tool calls will be automatically approved.
YOLO mode is enabled. All tool calls will be automatically approved.
Ripgrep is not available. Falling back to GrepTool.


Reviewed by Gemini (gemini-3.1-pro-preview)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Review

Critical Issues

None.

Security

No security concerns flagged.

Suggestions

No suggestions.

Static review only: go test ./pkg/plugins/domains could not run because the environment is read-only and Go could not create a module/build cache.


Reviewed by Codex (gpt-5.5)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Review

Critical issues

  • None. This PR's own diff (vs. its stacked base #122) is tests + comments only — the Jaccard metric swap, jaccardTokenSets, tokenSetContained, and the decideConfidence rewrite all belong to #122, which appears here only because GitHub diffs against main. There is no new production predicate of its own to break. Reviewers must not merge before #122 (author flagged this prominently).
  • Verified the calibration enumeration by hand across both tables (equal-count k=1..6 and the asymmetric shapes): every m/(kq+kc-m) value and arm assignment checks out, the boundary cell k=4,m=3=0.60 correctly relies on the inclusive >=, and the calibrationTokens pool covers the largest shape (kq+kc-m=11). Math is sound.

Security

No security concerns flagged. No new network calls, crypto, auth, or secrets — all added tests are hermetic (no live ViewDNS/Whoxy/RDAP/WHOIS), and the justification-never-logs-PII invariant is itself pinned by a test assertion.

Test coverage

Tests are present and unusually thorough (enumeration + boundary + symmetry + review-band invariants). One self-acknowledged gap worth keeping in view: TestDecideConfidenceEveryReturnPointStaysInsideTheReviewBand's Len(covered, 5) observes the table's own labels, not decideConfidence's actual return statements — a sixth return added in production with no matching case would pass silently. The author documents this as a manual step; fine, but it is a real coverage limit rather than a proof.

No critical issues — LGTM pending human review (and pending #122 landing first).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd5ceb1027

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// kq+kc >= 8, so the mismatch arm was in practice "no shared tokens".
// Jaccard divides by the union, so that bound drops to kq+kc >= 5
// ("Walmart" vs {walmart,global,enterprises,holdings} = 1/4 = 0.25) — and
// 34 of the 342 shapes below simMismatch are PURE CONTAINMENT. Making the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Qualify the finite shape count

This calibration claim gives an exact 34 of the 342 count without defining the finite universe being counted. With unrestricted org token counts, the below-simMismatch shared-token shapes are unbounded, and pure-containment examples continue indefinitely, e.g. (kq=1,kc>=4,m=1). If this is meant to summarize a bounded enumeration, the bound needs to be stated (or the count removed), otherwise the recorded calibration basis is misleading.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CONFIRMED — fixed in 19190ef.

You were right, and re-deriving it made the point sharper. The figures are reproducible, but only over a bound the comment never disclosed: exhaustive enumeration over 1 <= kq,kc <= 12, 1 <= m <= min(kq,kc), sim = m/(kq+kc-m) < 0.30 gives exactly 342 shapes, 34 of them pure containment. Nothing motivates 12, and neighbouring universes disagree:

universe total below simMismatch of which pure containment
kq,kc <= 10, m >= 1 194 22
kq,kc <= 11, m >= 1 261 28
kq,kc <= 12, m >= 1 342 34
kq,kc <= 13, m >= 1 437 40
kq+kc <= 13, m >= 1 145 34

Only that one universe reproduces both numbers, and the last row shows 34 alone doesn't pin it. The bound also silently dropped m = 0, which moves the total to 486 while leaving the containment count unchanged — so the count was an artifact twice over.

Rather than disclose an arbitrary cutoff, I took your first branch and removed the count, replacing it with the exact unbounded statement, since you're right that the family is infinite: under Jaccard a fully contained pair (m = kq) scores exactly kq/kc, so it sits below simMismatch for every kc > 10*kq/3(1,4), (1,5), ... , (3,11), ... without end. The claim the block exists to support is unchanged and now rests on a closed form instead of a bounded sample.

Comment-only: zero added non-comment lines (verified against the diff), gofmt clean, go build clean, package tests green.

…ation basis (ENG-5374)

Codex review finding (P3) on PR #127: the CALIBRATION BASIS block quoted
"34 of the 342 shapes below simMismatch are PURE CONTAINMENT" without
defining the universe being counted.

CONFIRMED. The pair is reproducible, but only over an arbitrary and
undisclosed bound -- exhaustive enumeration over 1 <= kq,kc <= 12 with
m >= 1 and sim < 0.30 yields exactly 342 shapes, 34 of them pure
containment. No other square universe reproduces both figures, and no
kq+kc <= K universe does either. The count was therefore an artifact of
a bound the comment never disclosed, and the reviewer is right that a
recorded calibration basis must not read that way.

The underlying set is infinite, so the repair is to state the exact
unbounded fact rather than to name the bound: under Jaccard a fully
contained pair (m = kq) scores exactly kq/kc, so it sits below
simMismatch for every kc > 10*kq/3 -- (1,4), (1,5), ... , (3,11), ...
without end. The qualitative claim the block exists to support is
unchanged and now rests on an exact statement: making the metric
specificity-monotone is what put pure-containment shapes in reach of the
de-rank arm, which is why tokenSetContained (#122) is needed.

Comment-only: zero added non-comment lines, verified against the diff.
gofmt clean, go build clean, package tests green.

Refs ENG-5374, ENG-5172

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@santiago-praetorian

Copy link
Copy Markdown
Author

Adjudication — round 4 (revision 5, the #122-stacked rewrite)

Revision 5 emptied this PR's production diff, so the three earlier rounds (revisions 1–4; 11 findings — 3 confirmed and fixed, 5 refuted with evidence, 1 no-result, 2 clean) do not carry over. I re-requested review and adjudicated everything that came back. 1 confirmed and fixed, 1 refuted, 1 recorded as a known limit, 1 clean, 1 no-result.


1. Codex, P3 — "Qualify the finite shape count" (reverse_whois_verify.go:96) — ✅ CONFIRMED, fixed

"This calibration claim gives an exact 34 of the 342 count without defining the finite universe being counted. With unrestricted org token counts, the below-simMismatch shared-token shapes are unbounded... If this is meant to summarize a bounded enumeration, the bound needs to be stated (or the count removed), otherwise the recorded calibration basis is misleading."

Correct, and I'll add the detail that makes it sharper than "unqualified". I re-derived the figures to find out what universe they came from. Exhaustive enumeration over 1 <= kq,kc <= 12, 1 <= m <= min(kq,kc), sim = m/(kq+kc-m) < 0.30 gives exactly 342 shapes, 34 of them pure containment — so the numbers are real, not invented. But that bound is nowhere in the comment, and nothing motivates 12:

universe total below simMismatch of which pure containment
kq,kc <= 10, m >= 1 194 22
kq,kc <= 11, m >= 1 261 28
kq,kc <= 12, m >= 1 342 34
kq,kc <= 13, m >= 1 437 40
kq+kc <= 13, m >= 1 145 34

Only that one universe reproduces both figures — note the last row, where the containment count matches but the total does not, so 34 alone does not pin it. The count was an artifact of an undisclosed cutoff on kc, exactly the reviewer's read; and it also silently excluded m = 0, which changes the total (486, not 342) while leaving the containment count alone.

Rather than disclose an arbitrary bound, I removed the count and stated the exact unbounded fact, since Codex is right that the family is infinite: under Jaccard a fully contained pair (m = kq) scores exactly kq/kc, so it sits below simMismatch for every kc > 10·kq/3(1,4), (1,5), …, (3,11), … without end. The claim the block exists to support is unchanged and now rests on an exact statement instead of a bounded sample.

Fixed in the follow-up commit; comment-only, zero added non-comment lines (verified against the diff), gofmt/go build/package tests green.


2. CodeRabbit, nitpick — relocate jaccardTokenSets, extract distinctTokens (github_org.go:324-341) — ❌ REFUTED (out of scope)

Both cited lines are #122's code, not this PR's. jaccardTokenSets and tokenSetContained are introduced by #122 (ENG-5172); this PR contributes no change to github_org.go — it is byte-identical to #122's version, which I verified rather than asserted.

Acting on this would recreate the exact collision revision 5 exists to resolve: I would be re-editing another engineer's in-flight lines in a PR that is stacked on theirs, and #122 would then conflict on merge. This is the same disposition as round-3 finding 9, for the same reason. The suggestion is reasonable on its merits and belongs on #122, where the author can take or leave it; it is not this PR's to make.


3. Claude — assert.Len(t, covered, 5) is a coverage limit, not a proof — 📌 ACCEPTED as a recorded known limit, no change

Right, and it's already documented as such in the test rather than dressed up as exhaustiveness. The assertion pins that the enumeration reached all five arms; it does not prove the table covers every reachable shape, and nothing in the PR claims it does. What carries the exhaustiveness weight is the closed-form derivation (1/(kq+kc-1) >= 0.60 ⟹ kq+kc <= 2), which is an exact bound rather than a search limit — and, after finding 1 above, the containment family is now stated in closed form too. Recording the observation here so it's on the thread; no code change, because the limitation is real and honestly labelled.

Claude also independently re-derived every m/(kq+kc-m) value and arm assignment across both tables by hand and confirmed them, including that the boundary cell k=4, m=3 = 0.60 correctly depends on the inclusive >=. That independent check is the useful result here.


4. Codex body review — ✅ CLEAN

"Critical Issues: None. Security: No security concerns flagged. Suggestions: No suggestions." Noted with its own caveat: static-review-only, because the environment was read-only.


5. Gemini — ⚠️ NO RESULT (INVALID_STREAM, second occurrence)

The run failed with INVALID_STREAM: "The model returned an empty response or malformed tool call." — it produced no review at all. This is the second time Gemini has failed this way on this PR (round 2's finding 7 was the same). This is not an approval and is not counted as one. Gemini's coverage of revision 5 is simply absent, and I'm recording that as a gap rather than papering over it.


CI

ci / Test is red on this branch for a reason that is not this PR's: main's tip (8de8a07) carries an inverted error check in favicon_hash.go (if err != nil where it must be err == nil, at both :179-181 and :251-253, from commit a1a9b93). GitHub Actions checks out refs/pull/127/merge — this branch merged with main's tip — so main's 8 failures land on every open PR. I reproduced all 8 standalone in a throwaway detached worktree at 8de8a07, and this branch does not contain that commit (merge-base 6f82e49). #132 already fixes it in flight and its ci / Test passes. I'll git merge origin/main (never rebase) and re-verify once #132 lands.


Status

Merge order is unchanged and binding: #122 first, then this PR. Merging is a human decision — I'm stopping at adjudicated + green-CI-pending-#132 and not merging. ENG-5374 stays In Progress.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants