fix: rank the types a workspace already uses above the hub's own picks

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-09-07 11:57:07 +02:00
co-authored by Claude Opus 5
parent 58bd7746da
commit 2a85f4b7d9
2 changed files with 40 additions and 8 deletions
@@ -5,17 +5,34 @@ const order = (names: string[], hub: Record<string, number>, local: Record<strin
[...names].sort(byPopularity(hub, local))
describe('byPopularity', () => {
it('ranks hub picks above local usage', () => {
expect(order(['slack', 'stripe'], { slack: 1 }, { stripe: 40 })).toEqual(['slack', 'stripe'])
// The tier that stops a filling-up hub from squeezing the workspace's own stack out of
// the ordering: a global pick count grows without bound, a local one does not.
it('leads with what the workspace uses, whatever the hub says', () => {
expect(order(['slack', 'stripe'], { slack: 900 }, { stripe: 1 })).toEqual(['stripe', 'slack'])
})
it('breaks a hub tie on local usage', () => {
expect(order(['slack', 'stripe'], { slack: 5, stripe: 5 }, { stripe: 2 })).toEqual([
it('ranks the used types among themselves by hub picks', () => {
expect(order(['slack', 'stripe'], { slack: 900 }, { slack: 1, stripe: 1 })).toEqual([
'slack',
'stripe'
])
})
it('breaks a hub tie on how much the workspace uses it', () => {
expect(order(['slack', 'stripe'], { slack: 5, stripe: 5 }, { slack: 1, stripe: 2 })).toEqual([
'stripe',
'slack'
])
})
it('ranks the unused types by hub picks, below every used one', () => {
expect(order(['ably', 'github', 'stripe'], { ably: 900, github: 5 }, { stripe: 1 })).toEqual([
'stripe',
'ably',
'github'
])
})
it('falls back to alphabetical for everything neither signal ranks', () => {
expect(order(['stripe', 'ably', 'github'], { github: 3 })).toEqual(['github', 'ably', 'stripe'])
})
@@ -75,16 +75,31 @@ export function recordHubResourceTypePick(workspace: string, resourceType: strin
* Orders the lists that offer hub content: integrations in the flow step picker, resource
* types in the add-resource drawer.
*
* What the hub sees people pick ranks first, being drawn from every Windmill instance; what
* this workspace already uses breaks its ties. Alphabetical is the floor, and it is where a
* hub that ranks nothing leaves every entry it has no count for.
* Four tiers. **Whether this workspace already holds a resource of the type leads**, then the
* hub's pick count, then how many local resources there are, then the name.
*
* Used-here leads rather than merely breaking hub ties because the two counts are on
* incomparable scales: a hub pick count is global and grows without bound, a local count is
* usually single digits. Ranked the other way round, local usage only ever sorts the slice
* where hub counts are equal — which, since they are distinct integers, is just the tail
* that nobody has picked. That reads fine on a hub with few picks and silently stops
* mattering as one fills up, so the ordering would drift away from the workspace's own
* stack with no change to this code.
*
* Within each half the hub decides, so "yours" and "everyone's" are both honoured rather
* than blended with a weighting constant that would need tuning. Alphabetical is the floor,
* and it is where an entry neither signal knows about lands.
*/
export function byPopularity(
hub: PopularityCounts,
local: PopularityCounts
): (a: string, b: string) => number {
const usedHere = (name: string) => ((local[name] ?? 0) > 0 ? 1 : 0)
return (a, b) =>
(hub[b] ?? 0) - (hub[a] ?? 0) || (local[b] ?? 0) - (local[a] ?? 0) || a.localeCompare(b)
usedHere(b) - usedHere(a) ||
(hub[b] ?? 0) - (hub[a] ?? 0) ||
(local[b] ?? 0) - (local[a] ?? 0) ||
a.localeCompare(b)
}
/**