feat(contributions): implement deterministic contribution score normalisation engine - #393
Open
diegoveme wants to merge 1 commit into
Open
Conversation
…lisation engine Adds a standalone scoring primitive that converts heterogeneous weighted contribution metrics onto one bounded basis-point scale. - Normalises each metric with exact bigint arithmetic; no floating-point threshold calculation anywhere in the scoring path - Defines clamping for values below the minimum and above the target, and treats a zero-range metric as a step function rather than a division by zero - Combines weighted metrics into a single accumulator and floors once, so truncation does not grow with the number of metrics - Rejects invalid ranges, negative inputs, duplicate keys and weight totals other than 10000 bps via ValidationError with a stable code - Returns per-metric intermediate results and sorts the breakdown by key, so reordered inputs produce deeply equal results - Supports metrics far beyond Number.MAX_SAFE_INTEGER - Documents the scoring model in docs/contribution-normalisation.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds
@guildpass/contribution-normalisation, a standalone scoring primitive thatconverts heterogeneous weighted contribution metrics onto a single bounded
basis-point scale.
Raw contribution metrics operate on incompatible scales, so they cannot be
combined directly. This package defines how each metric is projected onto one
common scale and how those projections are combined, using exact
bigintarithmetic so the result is reproducible.
Each metric is normalised with a single exact division:
Weighted contributions are then accumulated exactly and divided once at the end:
Flooring once over the exact sum, rather than once per metric, keeps the
truncation error below one basis point in total instead of letting it grow with
the number of metrics. Because configured weights must total exactly 10000 bps,
the final score is bounded to 0–10000 by construction: each
normalisedBpsisat most 10000, so the accumulator is at most
10000 * Σ weightBps_i.Defined behaviours
normalisedBpsclampingminimum < value < targetin-rangevalue == minimum0in-rangevalue == target10000in-rangevalue < minimum0below-minimumvalue > target10000above-targetminimum == targetandvalue >= target10000zero-range-metminimum == targetandvalue < target0zero-range-belowA zero-range metric is a defined step function rather than a division by zero.
Exceeding the target earns no credit beyond it, so one outsized metric cannot
inflate a score past 10000 bps or compensate for a metric that was never met.
Weight totals other than exactly 10000 bps are rejected rather than rescaled,
because silently rescaling would make a configuration error look like a low
score.
Determinism
bigintarithmetic. Nothreshold is computed in floating point.
normalisedBps,weightBps,scoreBps) are returned asnumber, where conversion frombigintis exact. Unbounded quantities(
value,minimum,target) staybigintand are never converted.reordered inputs produce deeply equal results, not merely equal scores.
Linked Issue
Closes #387
Type of Change
Changes Made
packages/contribution-normalisation/src/index.ts— the engine:normaliseContributionScore, theContributionMetric/MetricBreakdown/ContributionScoreResultcontracts, theMetricClampingunion, theSCORE_SCALE_BPSconstant andValidationErrorwith stable codes.packages/contribution-normalisation/src/index.test.ts— 36 unit tests.packages/contribution-normalisation/package.json,packages/contribution-normalisation/tsconfig.json— mirror the existingprimitive packages exactly (no dependencies,
tscbuild,node --testoverdist).docs/contribution-normalisation.md— the documented scoring model, followingthe structure of
docs/webhook-verification.md.Nothing outside the new package and its doc is touched. No README change: the
README.mdtree documents the aspirational V2 architecture and lists none ofthe existing primitive packages, so adding this one there would be inconsistent
with its siblings.
pnpm-lock.yamlis intentionally not modified. The package declares nodependencies, and
pnpm install --frozen-lockfilewas verified to pass againstthis branch, so the CI install step is unaffected.
Test Evidence
pnpm --filter @guildpass/contribution-normalisation test:Full workspace, matching the Core CI sequence —
pnpm install --frozen-lockfile,pnpm typecheck,pnpm build,pnpm test— all pass with exit code 0.Coverage maps to the acceptance criteria:
normalisation(4)weighted combination(5)clamping(5)INVALID_RANGE,NEGATIVE_VALUE,NEGATIVE_RANGE_BOUND,INVALID_VALUEINVALID_WEIGHT×3,INVALID_TOTAL_WEIGHT×2large values(3), up to 10^40order independence(2), asserted withdeepStrictEqualexplainability(1)boundary values(3) plus the aboveGeneral Checklist
pnpm typecheckpassespnpm buildpassespnpm testpasses — all tests greenpnpm lint— nolintscript exists in this workspace, so this step wasnot run
.env.exampleis unchangeddocs/contribution-normalisation.mdAdditional Notes
The package satisfies the issue's independence requirement: it has no
dependencies, performs no persistence, and does not reference the reward engine,
contribution persistence or any other campaign work. It computes a score and
returns it.
One small heads-up while reading the docs, in case it is useful:
CONTRIBUTING.mdlooks like it has drifted behind the V2 rebuild. It documents an
npm-basedsetup (
npm install,npm run typecheck,npm run lint) and a workspace layoutof
policy-engine,sdk-liteandpackages/contracts, none of which are in thetree today. The prerequisites also list Node 18+ and npm 9+, while the root
package.jsonpinspnpm@11.16.0and Node >=24, andcore-ci.ymlruns on Node24. It is the first thing a new contributor reads, so it may be worth a refresh
when there is time.
For this PR I followed
.github/workflows/core-ci.ymland the rootpackage.jsonrather than thenpmcommands inCONTRIBUTING.md, so that whatI verified locally matches what CI actually runs.