Skip to content

The seven signals — what each wants, in what unit

Every need carries seven signals. Four are need-specific, asked or voted per need. Three are group-wide telemetry, written identically onto all 41 needs, so they say nothing about any one of them — which is why coverage counts the need-specific four only.

Source of truth (in hoen-assessment): apps/hoen-explorer/src/snapshots/normalisation.ts.

Need-specific — go in needs["<need-id>"]

Field Scale Direction Where it comes from
survey 0–10 higher = healthier Don't write this by hand. Supply questions instead and the rollup computes it. An inline value is only read when no question matched.
maturity 0–3 higher = healthier SME self-rating in the workshop. Whole or half steps are normal.
spendRank 1–10, or a longer stack inverted — 1 scores 0, the stack maximum scores ~100 Stack rank over current spend. Rank 1 = the most current spend is already going in, which the model reads as a live, funded gap. Getting this backwards inverts the signal. The default band is 10; a workshop stack that runs longer is fine and the group's own maximum is used instead.
spendAmount any number inverted by rank Optional. Raw dollars. Used when every need in the file carries one and spendRank stays inside the 1–10 band — then the canonical stack rank replaces the rank column. Partial coverage is ignored; a stack running past 10 keeps spendRank spacing.
workshopDotVote 0–1 inverted — a need that soaked up votes scores low Share of the workshop's dots that landed on this need. Dots mark where the experts think the problems are.

spendRank and workshopDotVote both invert, and that is the most common intake error. A need everyone voted for is a need in trouble, so it scores low — the same direction as every other signal, where 100 means healthy.

The spend stack: transcribe the park bucket too

This is the one place where writing down a rank you were told, rather than omitting it, is the correct move.

Facilitators stack all 41 needs and park the ones nobody discussed on a shared bottom rank, often with a leftover $1. The engine uses that shared bottom rank to detect what was never discussed and drop it from spend scoring altogether:

What the file carries What the engine does
Every need has a spendAmount, and 2+ needs share the bottom rank at the same leftover amount Needs above the leftover floor are scored; the parked tail is dropped
Ranks only, stack maximum ≤ 10 Every ranked need is scored
Ranks only, stack runs past 10 The shared bottom rank is treated as the park bucket and dropped

So transcribing only the funded top ten and omitting the rest is worse than useless: the other 31 needs default to rank 5.5, the stack maximum falls to 10, and 31 mid-stack allocations nobody ever made are scored as real. Before this rule existed, reading the parked tail as "well funded" made unspoken needs outscore each group's actual top priority.

Dot votes: share, not count

Given counts, divide by the total dots cast across all needs, so the values sum to roughly 1 for the group. A raw count — 7 — clamps to 1 and reads as "every dot landed here".

Group-wide telemetry — goes in rawMetrics

The rawMetrics block must be present or the file is rejected outright. Use {} when there is no telemetry — that is an honest empty block, not an error.

Field Unit Feeds Notes
leadTimeHours hours CTP (0.6) Commit to running in production. Curve is meaningful over ~1–60h.
deployPerDay per day CTP (0.4) Scored against a target of one deploy per two active contributors per day — so it needs totalActiveContributors to mean anything.
totalActiveContributors people CTP Defaults to 4 if absent, which will flatter a large group.
teamsCount teams CQA, QoS Incident counts are compared per team. Absent = treated as one team, which will make a multi-team group look far worse.
changeFailPct percent, 0–100 CQA (0.3) Not a 0–1 fraction. Clamped to 0–20 by the curve.
recoveryTimeHours hours CQA (0.3) and QoS (0.15) MTTR after a failed change. Note the model's measure table labels this in minutes; the snapshot field is hours.
incidentsLowPerMonth per month CQA (0.1) Low-severity, divided by teamsCount.
incidentsMedHighPerMonth per month QoS (0.4) Customer-affecting, divided by teamsCount.
waitTimePct percent, 0–100 CTP (0.2) Waiting duration ÷ total elapsed duration × 100, over a stated commitment-to-customer boundary. Steep between 60% and 85%. Absent on current intake — supply a measured or explicitly estimated reading as an override in the Assessment data cards.
releaseConfidence 0–10 CQA (0.3) and QoS (0.45) How confident the team is a release lands cleanly. Higher is better. Manual until the Likert items are wired; absent on current intake, and its weight redistributes when missing.

Two traps worth repeating. Both produce a plausible score rather than an error:

  • recoveryTimeHours is hours. A dashboard reporting MTTR in minutes needs dividing by 60. 45 minutes entered as 45 reads as nearly two days.
  • changeFailPct is 0–100. An SDO dashboard reporting 0.12 becomes 12.

Wait-time provenance

The percentage is not reproducible without its collection context. Record:

  • start and end boundary;
  • date range and number of work items;
  • included work types and exclusions;
  • workflow states classified as active or waiting;
  • waiting duration and total elapsed duration;
  • any correction factor, its basis and who agreed it.

Prefer a same-group trend over a cross-group comparison. Different work types, workflow conventions and status discipline can make two identical percentages mean different things. A benchmark or target is not a missing reading and must not be entered as a raw override.

What happens when something is missing

Nothing errors. ingest fills the gap:

Missing Filled with
survey (no matched question, no inline value) 5 / 10
maturity 1.5 / 3
spendRank 5.5
workshopDotVote 0.5
a telemetry field the signal is scored from whatever fields remain; a signal with no fields at all is absent

A defaulted need scores like a measured one. Hence omit, never guess — an omission shows on the coverage meter, an invented 1.5 does not.