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:
recoveryTimeHoursis hours. A dashboard reporting MTTR in minutes needs dividing by 60. 45 minutes entered as45reads as nearly two days.changeFailPctis 0–100. An SDO dashboard reporting0.12becomes12.
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.