From 9966dc172826d99b6338f8f99ce2c824bb74d958 Mon Sep 17 00:00:00 2001 From: martbost Date: Tue, 18 Aug 2026 16:54:07 -0500 Subject: [PATCH] Fix off-by-one in catch-eligibility: catcher must be AT the bought level The pipeline widget, owner upgrade alerts, and the org routing/leak map all treated a position as eligible to catch a payment at the buyer's PRE-upgrade level (level >= depth). Verified against on-chain history: position 6, at exactly the pre-upgrade level (Ascensus) and qualified, was passed over on position 8's Fabrica buy - so the contract requires the catcher to be at the level being BOUGHT (depth+1), matching the "stay one level ahead" doctrine. - chain.js getOwnerUpgradeNeeds: eligible now pLevel >= depth+1; neededLevel reported as the bought level (was one low, so alerts would have stayed silent in exactly the situation they exist for) - chain.js getOrgRouting catcher(): same correction in the routing simulation - my.js pipeline: needLevel/eligible/summary-text ladder corrected; warnings now name the right level Co-Authored-By: Claude Fable 5 --- chain.js | 9 ++++++--- public/my.js | 10 +++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/chain.js b/chain.js index 2cbfe3e..80734ae 100644 --- a/chain.js +++ b/chain.js @@ -556,7 +556,10 @@ function getOwnerUpgradeNeeds(ids) { (function walk(n, depth) { if (!n) return; if (depth >= 1 && (n.level || 1) === depth) { - const eligible = pQual && pLevel >= depth; + // catcher must be AT the level being bought (depth+1), not the buyer's + // pre-upgrade level — verified on-chain: #6 was passed at exactly the + // pre-upgrade level on #8's Fabrica buy (2026-08-14) + const eligible = pQual && pLevel >= depth + 1; if (!eligible) items.push({ memberId: n.id, depth, amount: (costs.up[n.tier === 2 ? 2 : 1] || [])[depth - 1] || 0 }); } walk(n.left, depth + 1); walk(n.right, depth + 1); @@ -566,7 +569,7 @@ function getOwnerUpgradeNeeds(ids) { const atMin = items.filter(i => i.depth === minDepth); needs.push({ id, level: pLevel, levelName: levelName(pLevel), qualified: pQual, - neededLevel: minDepth, neededLevelName: levelName(minDepth), + neededLevel: minDepth + 1, neededLevelName: levelName(minDepth + 1), members: atMin.map(i => i.memberId), amountAtRisk: +atMin.reduce((s, i) => s + i.amount, 0).toFixed(2), reason: pQual ? 'upgrade' : 'qualify' @@ -592,7 +595,7 @@ function getOrgRouting(rootId, ownerIds) { if (!up || up === CROOT) return null; const u = M[up]; if (!u) return null; if (i < li) { up = u.uplineId; continue; } - if ((u.level || 1) > li && (u.directCount || 0) >= 2) return up; + if ((u.level || 1) > li + 1 && (u.directCount || 0) >= 2) return up; up = u.uplineId; } return null; diff --git a/public/my.js b/public/my.js index 0dabd59..3fcf024 100644 --- a/public/my.js +++ b/public/my.js @@ -199,7 +199,7 @@ if(depth>=1){ if(lvl===depth){ const amt=(up[tier]||[])[depth-1]; - if(amt)ready.push({id:n.id,depth,tier,amt,toLevel:LEVELS[depth]||('Level '+(depth+1)),needLevel:depth}); + if(amt)ready.push({id:n.id,depth,tier,amt,toLevel:LEVELS[depth]||('Level '+(depth+1)),needLevel:depth+1}); }else if(lvl=2&&myLevel>=g; + const buys=LV[g]||'the top'; const need=LV[g]||'the top'; + const have=d.directCount>=2&&myLevel>=g+1; const cnt=(gens[g]||[]).length; L.push(`• Layer ${g}${cnt?` (${cnt} ${cnt===1?'person':'people'})`:''}: their ${buys} upgrade pays you ${money(amt)} POL each — needs you at ${need}${have?' ✅ you already are':' ← get here to catch it'}`); } @@ -266,11 +266,11 @@ const nextName=LV[myLevel]||'max level'; // shallowest layer you can't yet catch let gap=0; - for(let g=1;g<=8;g++){ if((gens[g]||[]).length && !(d.directCount>=2&&myLevel>=g)){ gap=g; break; } } + for(let g=1;g<=8;g++){ if((gens[g]||[]).length && !(d.directCount>=2&&myLevel>=g+1)){ gap=g; break; } } // is anyone in the gap layer already close (at/near their pay-you level)? const gapClose=gap&&(gens[gap]||[]).some(n=>(n.level||1)>=gap-1); if(d.directCount<2) L.push(`YOUR NEXT MOVE: get your 2 directs. Until then none of this pipeline can pay you — spillover fills your team but only your own 2 qualify you.`); - else if(gap) L.push(`YOUR NEXT MOVE: your next uncovered layer is layer ${gap} — to catch their ${LV[gap]||'top'} upgrades you'll need to be at ${LV[gap-1]} (${myNext?money(myNext)+' POL':''}). They're still climbing toward it${gapClose?', and getting close — worth upgrading soon':", so no rush — just get there before they do"}. You're already eligible for everything shallower.`); + else if(gap) L.push(`YOUR NEXT MOVE: your next uncovered layer is layer ${gap} — to catch their ${LV[gap]||'top'} upgrades you'll need to be at ${LV[gap]||'the top'} yourself (next step: ${nextName}${myNext?', '+money(myNext)+' POL':''}). They're still climbing toward it${gapClose?', and getting close — worth upgrading soon':", so no rush — just get there before they do"}. You're already eligible for everything shallower.`); else if(myNext) L.push(`YOUR NEXT MOVE: you're currently eligible for every active layer. As your team goes deeper, upgrade to ${nextName} (${money(myNext)} POL) before that new layer climbs to its pay-you level.`); L.push(''); L.push(`See it live and get a red alert the moment you'd miss one: rmcircle.team/my/${d.id}`);