Scoring & Views — three input categories, rule-based groups, immutable order

The dashboard's two main decision-aids are scoring and views. Both are designed so users can rank and group hundreds of jobs without the system making opinionated cuts on their behalf.

Multi-dimensional scoring

Scoring takes three input categories and aggregates them under a single configurable curve.

1. Boolean criteria (knockouts)

Each boolean criterion carries a penalty factor in [0, 1]. 0 is a hard knockout — the job's score is forced to zero regardless of other inputs. Soft knockouts (penalty 0.5, 0.8) reduce the score multiplicatively without zeroing it. Examples: "Timelessness," "AI Safety," "Stable funding."

2. Numeric criteria (weighted 0–10)

Each numeric criterion is a 0–10 evaluation with a weight. Standard weighted average across this category. Examples: "Growth," "Ownership," "Team quality."

3. Hard facts (calibrated metrics)

Salary and commute (and any future calibrated metric) are standardized via a four-step pipeline so they can be combined with the other categories:

  1. Clamp the value to [min, max].
  2. Normalize to a [0, 1] goodness ratio. Direction-aware: a higher-is-better metric and a lower-is-better metric both reach 1.0 when "best."
  3. Shape with a scaling curve (linear, quadratic, exponential, log). This is where workspaces express "I really want >100k" versus "I tolerate any salary as long as it isn't insultingly low."
  4. Scale the result to 0–10 so it lines up with numeric criteria.

Aggregation modes

Mode Behaviour
Harmonic Default. Penalizes low outliers — a single 2 drags the whole down.
Geometric Less aggressive than harmonic; still punishes weak categories.
Arithmetic Permissive; one strong category can carry the score.

The harmonic default is intentional: a job with high salary but a 90-minute commute and a "stable funding" knockout penalty should NOT outrank a balanced offer. The user can switch modes per workspace if they want a more permissive aggregator.

Implementation: lib/domain/src/scoring-algo.ts.

Rule-based views

A view is a way to group jobs visually in the dashboard. Each view carries:

  • Groups — named, coloured buckets (e.g., "Due Now," "In Progress," "Resolved"). Colour is keyed by the TONE constant; see the design guide.
  • Rules — an ordered list of conditions that assign a job to a group. Supports simple conditions (field op value), compound conditions (AND / OR), and special operators (is_overdue, in_phase, is_terminal).

Rules evaluate in order. First match wins. A job that does not match any rule falls into a catch-all group.

Preset views: By Priority, By Location, By Value, By Score. Users can create custom views. View definitions live in the settings sub-doc.

Implementation: lib/domain/src/columns.ts and the related view helpers in lib/schemas/src/views.schema.ts.

Immutable OrderState

OrderState is the value object that captures all ordering across the workspace:

  • Dashboard column display order.
  • Per-view group order.
  • Per-view, per-group manual job order (sparse — only manually positioned jobs are stored, sort columns override the rest).

It lives in the order-state sub-doc (Y.Doc root key order_state).

Every mobility function in lib/domain/src/order.ts (moveItem, moveColumn, moveJob, …) returns a new OrderState. Old copies are not mutated. This buys two things:

  • Clean undo/redo. Reverting a reorder is just re-applying a previous OrderState snapshot.
  • No mutation bugs. Two consumers reading the same OrderState while one is mutating do not see partially-updated state.

When a sort column is active, manual order is ignored and the sort defines the displayed order. The OrderState still keeps the manual sparse entries intact, so toggling the sort off restores the user's prior arrangement.

Storage and edge cache

Both view definitions and OrderState are written via the op catalog (settings.update, order_state.update). They sit in their own sub-docs so the order-state sub-doc's high write rate does not blow the settings sub-doc's edge cache. When the Views modal saves changes that touch both sub-docs (a new view plus the order it should appear in), the write goes through client.applyBatch([...]) so peers never see a state where the new view is defined but the order has not caught up.

Why this design holds

  • Scoring stays explainable: every score is (boolean penalties) × (numeric weighted average + calibrated hard facts), aggregated by the chosen curve. Users can audit the score breakdown in the UI.
  • Views stay author-friendly: first-match-wins is the simplest rule semantic that still expresses everything realistic users want.
  • OrderState stays predictable: immutability means a reorder either took effect (new snapshot is broadcast) or did not, and there is no in-between.
Source: wiki/content/canon/scoring-and-views.md