← nescio·mind

nescio-mind · shipped feature · issue #6

Representable Ambiguity

Teaching the reasoner to see and say when two answers are equally likely — the fix for F1 (ambiguity discarded before voting) and F3 (ambiguity never surfaced).

Baseline

10 / 11 MEETS

Result

voting scenario now MEETS

Locus

cataloger + neuron only

Sub-systems

unchanged

The problem · F1

The tie already exists. One line throws it away.

When two categories share an identical feature profile, BayesianReasoner computes exactly what you'd hope — a 50/50 posterior. But Cataloger._normalize_one keeps only each sub-system's single top answer, so the tie is gone before the cataloger ever votes. The system built to prefer "reporting competing candidates over picking one arbitrarily" cannot represent the most basic competition of all.

What Bayesian computes

leopard0.50
cheetah0.50

A genuine tie. Both live in the distribution.

What the cataloger keeps

leopard1.00
cheetah

cheetah discarded. Result: 1 candidate, share 1.00, ambiguity 0.00.

The fix · distributed mass voting

Carry the distribution into the vote — no sub-system changes.

The distributions already exist in the sub-system outputs; the cataloger just stops discarding them. Each sub-system casts votes across its pruned candidate set, weighted by its own distribution. Two genuinely-close categories then accumulate comparable weight, and the existing ambiguity = runner_share / top_share formula finally sees real data.

BEFORE SHIPPED Bayesian leopard 0.50 cheetah 0.50 _normalize_one keep argmax vote tally leopard 100% composite ambiguity 0.00 ambiguous ✕ cheetah ✕ dropped Bayesian leopard 0.50 cheetah 0.50 _normalize_one keep candidates distributed tally 0.50 / 0.50 composite ambiguity 1.00 ambiguous ✓ cheetah carried through
The one edge that changed: before, the second candidate was dropped at _normalize_one (top lane); the shipped fix carries it through the distributed tally (bottom, ochre), so a real tie produces two competing candidates and ambiguity 1.00.

Worked example

leopard vs cheetah, end to end

StageleopardcheetahNote
Bayesian posterior0.500.50identical profiles
Pattern similarity0.900.90both kept (≥ 0.5 × top)
Distributed tally1.401.40summed weights
Composite share0.500.50
ambiguity = runner ÷ top0.50 / 0.50 = 1.00≥ 0.70 → flag
resultambiguous: True · 2 candidatessurfaced on the return path

A clear winner behaves as before: its runner-up prunes away (below 0.5 × top), leaving one candidate and ambiguous: False. The change only fires on a genuine near-tie.

Surfacing it · F3

One truth, carried on every return path

Fixing the cataloger is invisible unless the neuron surfaces it. Before this fix, ambiguous was set only in the uncertain-path helper, so a confident near-tie returned at step 6 with no signal at all. Now ambiguous is a single composite-level field, carried out of every exit.

Return pathambiguouscandidates
Fast-path cache (step 1)False(a confident cache hit is unambiguous)
Confident (step 6)composite["ambiguous"]composite candidates
Routing delegation (step 7)external["ambiguous"]propagated from the expert
Honest uncertainty (step 8)composite["ambiguous"]composite candidates

Named constants

Three knobs, each under test

The thing this project is rightly wary of — unmotivated magic numbers. Each lives in one place as a named parameter, tested on both sides of its boundary.

candidate_keep_ratio

0.5

A candidate must be ≥ half the top's weight (per sub-system) to count. A clear winner prunes to one; a near-tie keeps both.

max_candidates_per_subsystem

3

Caps distribution-tail noise beyond the real contenders.

ambiguity_flag_threshold

0.7

Runner-up within 70% of the winner's share reads as genuinely competing. Makes explicit a value already implicit in the code.

Honest caveat. 0.7 is reasoned, not measured — naming it and testing both sides of the boundary is the mitigation, not a substitute for eventually tuning it against data.

How we know it works

Tests — and the guard that matters most

Intent scenarios

  • voting.close_candidates_flagged GAPMEET
  • voting.ensemble_disagreementnew: Bayesian favors A, Pattern favors B → a real near-tie via disagreement, not identical profiles
  • a confident-winner control → ambiguous: False

Cataloger unit tests

  • pruning: confident → 1 candidate; near-tie → 2; cap at 3
  • distributed tally & ambiguity computation
  • ambiguity_flag_threshold boundary, both sides
  • exclusions & hierarchy still respected
The load-bearing check. Unlike F2's isolated module, distributed voting perturbs every composite. The 10 MEETS must hold, each clear winner still reporting ambiguous: False. Any wobble is a signal to tune candidate_keep_rationever to weaken a scenario.

Decisions & boundaries

What's locked, what's out

Shipped

  • Full F1 + F3: compute ambiguity right and surface it everywhere.
  • Distributed mass voting; ambiguity is a property of the ensemble.
  • Change lives only in cataloger.py + neuron.py.

Out of scope

  • Sub-system internals; the needs_more_info flag.
  • Renormalising vote mass across sub-systems (a known, bounded imprecision).
  • Multi-hop routing ambiguity nuances beyond passing the flag through.