For the complete documentation index, see llms.txt. This page is also available as Markdown.

Scoring

How value equations turn contributions into scores

The Scoring domain in @holons/core/scoring is the engine that turns a holon's REA event stream into per-user scores. It is the working implementation of the value equation β€” the concept referenced across the rest of the docs.

Where REA decides what counts as a recorded event and Council / DNA decide what the holon stands for, scoring decides how much each event is worth. Every UI in the Harvest monorepo calls the same scoring functions, so "what's my score?" produces the same answer in the web app, the Telegram bot, the CLI, and via the MCP server.

The pipeline

        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚   REA events (rea_events)   β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
                       β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚   REAAggregator             β”‚
        β”‚   β†’ UserAggregates          β”‚
        β”‚   β†’ currencyBalances        β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
                       β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚   calculateUserScore()      β”‚
        β”‚   with ScoreEquation        β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                       β”‚
                       β–Ό
                    score

Three steps, three pieces of state:

  1. REA events β€” every action that should count produced an event (see REA Accounting).

  2. Aggregates β€” the aggregator reduces the event stream to per-user counters (UserAggregates) and per-currency balances.

  3. Score β€” a pure function multiplies aggregates by the equation's weights.

The value equation

DEFAULT_EQUATION ships with sensible starting values:

Field
Default
Meaning

initiated

1

Quests created

completed

2

Quests completed (deliberately worth 2Γ— initiating)

sent

1

Appreciations sent

received

1

Appreciations received

collaboration

1

Time-logging events

wants

1

Wants declared

offers

1

Offers declared

currencies.hour

1

Hours logged (canonical currency)

That "completed 2Γ— initiated" default isn't accidental β€” it encodes one of the holon's most basic principles: finishing things matters more than starting them.

Each holon can change every weight. The equation is loaded from the holon's settings lens, cached synchronously for instant UI access, and live-updated via subscribeToEquationChanges().

User aggregates

These are produced by REAAggregator from the event stream, or by toAggregates() from legacy data shapes. ZERO_USER_AGGREGATES is a useful default while REA queries are in flight.

The score formula

Plain weighted sum. No magic, no neural net, no decay (yet). Every weight is multiplied by its aggregate; everything is added. The result is a single number per user.

For per-currency balances (currencyBalances: Record<string, number>), the score adds one term per currency the user holds, weighted by equation.currencies[currency].

Operations

The public functions exported by @holons/core/scoring:

Function
Purpose

calculateUserScore(aggregates, equation?, balances?)

Pure: total score for one user

calculateScoreFromUserData(userData, equation?)

Convenience: aggregates + score in one call

calculateAllUserScores(holonId, equation?)

Score every user in a holon

getScoreBreakdown(aggregates, equation?, balances?)

Per-field contribution (for "where do my points come from?")

getActionScore(actionType, equation?)

What's one of these actions worth right now?

calculatePercentageShare(userScore, allScores)

One user's slice of the total

calculateTaskCompletionScores(...)

Specialized: per-task completion delta

loadEquation(holonId)

Read the holon's equation from settings

getCachedEquation(holonId)

Synchronous read of the cached equation

preloadEquation(holonId)

Warm the cache

subscribeToEquationChanges(holonId, cb)

Live updates

migrateEquation(raw)

Fold legacy / loose-shape data into canonical form

The split between pure scoring helpers (calculateUserScore, getScoreBreakdown, …) and impure loaders (loadEquation, subscribeToEquationChanges) means every UI can render a score live from cached state without round-tripping to the substrate.

Migration: legacy hours β†’ currencies.hour

Historically the equation had a top-level hours weight. Newer code reads currencies.hour. migrateEquation() folds the legacy shape into the new one on load, and also folds flat top-level currency keys (equation.euro = 0) β€” the shape telegram-ui historically wrote β€” into currencies.euro.

The migration is idempotent and safe to call on already-migrated data. The deprecated top-level hours field is preserved (set equal to currencies.hour) so unmigrated consumers keep computing the same score.

Subscriptions and caching

getCachedEquation(holonId) returns the cached equation synchronously, useful in render-paths that can't await. The cache is populated by preloadEquation() and kept fresh by subscribeToEquationChanges(), which listens to the underlying settings lens.

This pattern is why a slider in the web dashboard can instantly re-render every user's score when a holon adjusts a weight β€” the cache updates, the pure score function re-runs, every UI hears about it.

MCP tool surface

The MCP server exposes 9 tools in the scoring domain:

  • per-user score (calculate_user_score)

  • all-user scoring (calculate_all_scores)

  • score breakdown (get_score_breakdown)

  • per-action delta (get_action_score)

  • equation load/migration (load_equation, migrate_equation)

  • and the supporting variants

Agents connected via MCP can answer "what's my contribution?" and "what would happen if we changed the equation?" without writing any score-computation code of their own.

See also

Last updated

Was this helpful?