HoEN data assist¶
ContributorsMyles Henaghan
When to use: On site with a client, turning what they actually have — survey exports, workshop walls, SDO (Software Delivery and Operational Performance Metrics) screenshots, and the docs and decks already written about all of it — into the two files the HoEN Explorer reads: group snapshot JSON, and the consultant narrative
.md.
Instructions¶
Two jobs, one working folder, done in this order:
- Readings → snapshot JSON. What was measured. One file per group.
- Existing material → narrative
.md. What it means. One file per scope.
The explorer scores (1), the consultant argues (2), and Export as static publishes both as a self-contained HTML file the client keeps.
flowchart TB
A[Survey export · workshop wall · SDO dashboard] --> B[snapshots/*.json]
C[Existing docs · decks · retro notes · workshop snips] --> D[narrative/*.md]
B --> E["/snapshots workbench"]
D --> E
E --> F[hoen-explorer-overrides.json]
F -.next session.-> E
E --> G[static export HTML → client]
First, say where the data is going¶
The explorer holds snapshots in memory for the session only — nothing is
written to localStorage, and an uploaded assessment leaves the browser only
when the consultant exports a file. That is the claim a client security team
cares about, and it is true of the tool.
This session is not the tool. Anything pasted, dropped or screenshotted into
this conversation goes to whatever model provider is running it. Say that once,
plainly, and let the consultant decide. If the engagement's data-handling terms
cover it, carry on and do not raise it again. If they do not, everything under
assets/ and references/ works by hand, without client data leaving the room.
The working folder¶
Build this as you go. The consultant drops the whole folder on /snapshots
once; JSON is routed by shape and anything else is ignored.
engagement/
snapshots/
grp-a-platform-2026-07-14.json # one per group, named so a human can tell them apart
grp-b-payments-2026-07-16.json
hoen-explorer-overrides.json # last session's calibration — weights, overrides, trade-offs
narrative/
top-opportunities-overall.md # the ranking a client reads first
top-opportunities-grp-b-payments.md # per-group rankings
curve-notes.md # optional, one heading per measure
hoen-explorer-overrides.json is written by Export override patch at the end
of a session and read back at the start of the next one, so a calibration
conversation is never retyped. Narrative .md is uploaded on the Rank &
narrate activity and written back in the dragged order — the file that comes
out of a session is the file that goes into the next one.
Part A — Readings into snapshot JSON¶
One JSON file per engineering group. Start from
snapshot-template.json — all 41 need ids in
model order, every value null.
snapshot-example.json shows a filled one.
Four rules¶
Omit, never guess. A need with no reading is filled with mid-scale defaults
and then scores like a measured one. An omission shows on the coverage count; an
invented 1.5 does not. Leave null or drop the key. Never interpolate, never
copy across from a similar need, never average two groups to fill a third.
Two signals invert. spendRank 1 means most current spend and scores 0.
workshopDotVote near 1 means most dots and scores 0. A need everyone voted
for is a need in trouble. Backwards, and the group's story inverts silently —
check the direction with the consultant when the source is ambiguous.
Never hand-write survey. Supply the questions array and let the rollup
weight it onto needs. An inline needs[...].survey is read only when nothing
matched, and writing one discards the question→need mapping.
Transcription is not inference. From a photo, transcribe what is legible.
Blurred, cropped or ambiguous — is that a 5 or an 8? — stays null and goes on
a list for the consultant. Show the transcription back before writing JSON. Once
it is JSON, a wrong reading looks exactly like a right one.
The four inputs¶
1. Survey export → questions[]¶
The strongest input. 161 catalogue questions, each weighted onto several needs, so one complete survey covers most of the model.
Match each exported row against
survey-questions.csv
(question_id, text, priority, feeds_needs). Resolution order mirrors the
rollup:
idagainstquestion_id(q0001–q0161), case-insensitive. Always prefer this.textagainst the catalogue prose — whitespace collapsed, case-insensitive, surrounding quotes stripped.
If the export carries an id column, map on it and pass text through as a
courtesy. If it does not, pass the prose verbatim — do not tidy, truncate or
re-word it, because a near-miss fails to match and the question is dropped.
- Score goes in
average, ormean—averagewins if both are present. Scale is 0–10. Rescale anything else and say so. nps,numberOfResponses,detractorsCount,passivesCount,promotersCountare carried for display, not scored.- A row with no
averageormeancontributes nothing. Leave it out.
When the export does not line up — a client's own instrument, or an older
template — do not force a mapping. Report what matched and what did not. The
feeds_needs column tells the consultant what each catalogue question drives,
which is what they need in order to decide.
2. Workshop wall → needs{}¶
Up to three readings per need. need-ids.csv maps all 41 ids to level and display name; match on the name, write the id.
maturity— SME self-rating, 0–3. Half steps are normal.spendRank— stack rank, 1 = most current spend (scores 0), inverted as above. The band is 1–10; a workshop stack that runs longer is fine and the group's own maximum is used.workshopDotVote— share of total dots, 0–1. Given counts, divide by the total dots cast across all needs; the values should sum to roughly 1.spendAmount— optional raw dollars, all-or-nothing. Used only when every need carries one andspendRankstays inside the 1–10 band.
Transcribe the whole spend stack, park bucket included. This is the one
place where writing down a rank you were told rather than omitting it is
correct. Facilitators stack every need 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.
Transcribe only the top ten and the other 31 needs default to rank 5.5, which
reads as a genuine mid-stack allocation nobody ever made.
Workshops rarely reach all 41 needs on maturity or dot votes. Expected — the
ones nobody rated stay null.
3. Delivery telemetry → rawMetrics{}¶
Ten fields, all optional, but the block itself must exist or the file is
rejected. Use {} when there is no telemetry.
Call this SDO — Software Delivery and Operational Performance Metrics — with the client: lead time, deployment frequency, change-failure rate, recovery time, and the incident counts. Do not score the group against a DORA maturity band or an "elite performer" table. Transcribe the local readings and leave the comparison to the narrative, where it has to earn its evidence.
Units are where this breaks. Read
signals-and-units.md before transcribing a
dashboard. Two traps produce plausible scores rather than obvious errors:
recoveryTimeHours is hours where most dashboards report MTTR in minutes,
and changeFailPct is 0–100 where most SDO dashboards report a 0–1 fraction.
teamsCount matters more than it looks. Incident counts are compared per team,
so a multi-team group without it is judged as one team.
waitTimePct and releaseConfidence are absent on current intake. The
Assessment data cards treat an empty reading as missing and let the
consultant type an override, which then rides the override patch.
Collecting % wait time¶
Wait time is the share of elapsed delivery time in which committed work is not actively progressing. Treat it as a health indicator, not a diagnosis.
- Set the boundary. Use commitment to available-to-customer. State a different start or end explicitly.
- Choose the sample. Use a small manual sample when tooling is weak, or at least three months of work-item history when it is reliable. Record the date range, item count, work types and exclusions.
- Classify the workflow with the team. Put every state into
activeorwaiting. Review, approval, blocked, queued and ready states are not automatically one or the other — use what happens locally. - Check status discipline. Ask whether people mark blocked and on-hold work consistently. Compare a few item histories with the people who did the work.
- Calculate it.
waiting duration / total elapsed duration × 100. Preserve the numerator and denominator beside the percentage. - Segment before averaging. Separate materially different work — incidents, small changes and large initiatives, for example — rather than hiding the difference in one mean.
If poor status history needs a correction factor, record the unadjusted reading, the factor, who agreed it and why. The result is an estimate, not telemetry. Never fill a missing wait-time reading from an industry range or another group.
High wait time does not say why work waits. Cross-check planning and sizing, work in progress, dependencies and hand-offs, interruptions, hidden work, capacity changes, approval controls and external constraints before writing the narrative.
4. Screenshots and photos¶
Same targets, more care. Transcribe into a table, show the consultant, then write JSON. Name every value you could not read instead of filling it. For a wall of sticky notes, count dots per need and convert to shares at the end — do not eyeball proportions.
Metadata worth chasing¶
| Field | Why |
|---|---|
asAt |
ISO-8601 datetime. Missing or unparseable rejects the file. |
code |
The group's name everywhere in the tool and the export. Absent, the filename stands in. |
id |
Stable id. Without one, a fresh id is generated per load, so re-loading duplicates the group — and narrative scope: has nothing to point at. |
participants.survey |
Drives sample-weighted aggregation. Absent, it falls back to averageTeamSize, then to 1. |
participants.workshop |
Shown on the card, so the reader can weigh the workshop signals. |
averageTeamSize |
The weighting fallback. |
Set id deliberately. It is what a narrative document's scope: names, so a
generated id means the per-group ranking cannot be filed against its group.
Validate before handing over¶
From a hoen-assessment checkout (or any cwd that can resolve the model — see
--model / HOEN_MODEL_PATH / HOEN_ASSESSMENT_ROOT):
# JSON by default, for the agent to read
node library/skills/hoen-data-assist/scripts/validate-snapshot.mjs snapshots/*.json
# console readout, for the consultant
node library/skills/hoen-data-assist/scripts/validate-snapshot.mjs --format text snapshots/*.json
Exit 0 when every file loads, 1 when any would be rejected, 2 on a usage error.
When the skill is transplanted under a consumer .agents/skills/, use that path
instead.
It reports the rejections and dropped questions, plus what the tool accepts
silently: readings outside their scale, telemetry that looks like a unit mix-up,
duplicate group codes, "N/A" where a number belongs.
Two failures reject a file outright: no rawMetrics block, and no valid asAt.
Everything else is a warning, and the warnings are usually the interesting part.
"31 questions matched no catalogue entry" means the export came from a different
instrument, not that the file is fine.
Then give the consultant three numbers per group: catalogue questions scored, needs carrying workshop readings, telemetry fields absent. The Assessment data activity shows them the same three. They should not be a surprise.
Keeping the lookup tables honest¶
assets/survey-questions.csv and assets/need-ids.csv are generated from
packages/hoen-model/dist/model.json in hoen-assessment. Checking is the
default; writing is opt-in, because a silent rewrite of the tables an intake
session trusts is the wrong default.
node library/skills/hoen-data-assist/scripts/refresh-lookups.mjs # check for drift, exit 1 if any
node library/skills/hoen-data-assist/scripts/refresh-lookups.mjs --write # rewrite after a model regeneration
Never hand-edit them. Never invent a question id or need id that is not in them — unrecognised ids are dropped on ingest, so the reading looks supplied and is not.
Part B — Existing material into the narrative¶
The client already has material: a discovery deck, a maturity self-assessment, retro notes, an architecture review, the facilitator's photos of the workshop wall. None of it is thrown away and none of it is quoted wholesale. It is reworked into one shape, repeated per opportunity:
## 1-tech-capability
**Skills silos across data ops, applications and domain knowledge**
### Symptoms
The same individuals always pick up the same work.
### Signals & measures
- SME self-rating 1.7/3; spend ranked #1 by the workshop
Heading is the need id. Document order is the rank. Full spec, heading vocabulary and round-trip rules: narrative-format.md.
The rework loop¶
Work one scope at a time — overall first, then per group.
- Inventory. List every source artefact with its date and author. Note which are superseded. A 2024 maturity assessment and a 2026 workshop disagreeing is a finding, not a merge conflict to resolve silently.
- Extract claims. Pull out each distinct assertion about the engineering system. One claim per opportunity — if a paragraph makes three, it becomes three entries or one entry with the other two demoted.
- Attach evidence. For every claim, name what backs it. A snapshot reading, a named document, a quoted observation from the workshop. A claim with nothing behind it is not a finding yet — see below.
- Place it. Map the claim onto a need id. If nothing fits, keep the consultant's own words as the heading and let it sit unresolved rather than forcing it onto the nearest need.
- Rank. Order the entries. The tool shows the model's own position beside each one, so a deliberate disagreement is on the record.
What each field is for¶
| Field | Holds | Length |
|---|---|---|
| Heading | The need id (or an unmapped phrase, or tbc) |
— |
| Thesis (the bold line) | The claim, in one sentence, in the client's language | One line |
### Symptoms |
What this looks like from inside the organisation — observable, not interpreted | 1–3 sentences |
### Signals & measures |
The evidence, as a bullet list. Readings, counts, named documents | 2–5 bullets |
Symptoms are what people would recognise if you read it back to them. Signals & measures are what you would show if they disagreed. Keeping them apart is what lets a client accept the symptom while arguing about the measure, which is a far more productive conversation than defending both at once.
Evidence and confidence¶
Cite the claim or soften the claim. Every thesis is backed in Signals &
measures by something a reader can go and check, or its language drops to match
what is actually known. This is Wires Uncrossed policy, not house style — the
full ladder is in
evidence-and-confidence.md.
The operative version:
| Backing | Write it as |
|---|---|
| A snapshot reading or a document you can name | Direct: "Lead time is 19 hours." |
| Several consistent observations, no measure | Attributed: "Three teams described the same review queue." |
| One observation, or an inference | Hedged and owned: "On what we have seen so far, this looks like a review bottleneck rather than a build one." |
| Nothing yet | It is not a finding. Say what would settle it, or park it under ## tbc. |
Two habits that go with it:
- State the assumption. Where a reading had to be interpreted — a rescaled survey, a rank read off a photo, a group without
teamsCount— say so inSignals & measuresas its own bullet: "Assumes the MTTR dashboard reports minutes; entered as 0.75h." - Leave room to be wrong. Write findings so new information revises them rather than embarrassing them. "This is our reading at the end of week one; the platform team's own telemetry would confirm or overturn it." Being seen to change position on evidence is the diagnostic working, not the diagnostic failing.
Do not launder confidence through structure. An entry with a firm thesis, three
vivid symptoms and one weak bullet under Signals & measures reads as
well-evidenced and is not. Weak evidence means a hedged thesis.
Rework recipes¶
Six worked before/after passes — workshop wall photo notes, a maturity self-assessment, retro and incident-review notes, an SDO readout, a prior discovery deck, and an existing markdown findings doc — are in rework-recipes.md. Read it before reworking an artefact type for the first time.
The pattern common to all six: the source's structure is never the narrative's structure. A deck organised by team becomes entries organised by need. A retro organised by sprint becomes entries organised by constraint.
Validate the narrative¶
Offline — this needs only the skill folder, so it runs on a client laptop with no checkout:
node library/skills/hoen-data-assist/scripts/validate-narrative.mjs narrative/*.md
# curve notes resolve against measure ids, not need ids
node library/skills/hoen-data-assist/scripts/validate-narrative.mjs --curves narrative/curve-notes.md
# console readout for the consultant
node library/skills/hoen-data-assist/scripts/validate-narrative.mjs --format text narrative/*.md
It reports headings that resolve to nothing, duplicate headings, entries missing
a thesis or Signals & measures, a scope: that no snapshot in the folder
claims, and — with --strict — theses whose language is more confident than
their evidence. Exit 0 clean, 1 on findings, 2 on a usage error.
Curve notes¶
An optional second .md, one ## <measure-id> heading per curve, rendering
under its own curve in the export. Curves you say nothing about are untouched.
The ids are not the dashboard's names — the real list is in
narrative-format.md.
Get one wrong and the note never reaches the export. Since 2026-08-20 the
panel marks each uploaded heading matched or unmatched and names the valid ids,
so check the chips after uploading; validate-narrative.mjs --curves catches
the same thing before you open the tool.
Objective trade-offs¶
The Rank & narrate activity also carries a Fixed ↔ Flexible axis and a
rationale, for overall and each group. It records what the organisation said
it was protecting when the assessment was taken, so the ranking beside it can be
read against a stated intent.
It is informational only — unlike the Trade-offs lens, nothing here re-scores or re-ranks. Two dimensions may not share a mark: equally tradeable is the one answer the exercise exists to refuse. Placements ride the override patch and appear as their own Trade-offs tab on the export once set.
Write the rationale as if the client will read it, because they will.
Part C — The session on site¶
/snapshots is three addressable activities. /snapshots/data and
/snapshots/narrate are deterministic return paths; bare /snapshots opens the
sheet.
| # | Activity | What happens |
|---|---|---|
| 1 | Assessment data | Drop the engagement folder. Check the three coverage numbers per group. Override a missing raw metric here, not upstream. |
| 2 | Needs × signals | The sheet, in table / list / heatmap. Scope tabs pick a group or the aggregate. Cell traces show raw → normalised → contribution. |
| 3 | Rank & narrate | Upload the narrative, map any unresolved heading, drag the order, write the three fields, place the trade-offs. |
Applying overrides¶
Use the smallest layer that tells the truth:
- Measured raw override — a real reading intake cannot currently carry,
such as sampled
waitTimePct. It may support a finding. - Estimated raw override — a reading adjusted for incomplete records. State the original value, method and uncertainty in the matching curve note.
- Signal what-if or weight override — a scenario or calibration choice. It shows what the model would do; it is not evidence and must not become a narrative finding.
Do not type a target, benchmark or another group's value into a raw metric slot.
Until override records can carry notes, use the matching curve note as the
decision record: source, date range, scope, sample size, calculation,
assumptions and who confirmed them. For a wait-time override, use
## wait-time.
Then Export as static writes one self-contained HTML file: Results · Scoring curves · Exercises · Group diff · Assessment data, plus Ranked constraints when a narrative exists and Trade-offs when placements are set. It is explore-only — no weight edits, no value overrides, no objective dials — and it carries no client name, because that comes from the narrative.
Finish by writing both files back: Export override patch and the narrative write-back. A session that ends without them has to be rebuilt from memory.
If coverage looks wrong, the problem is upstream in the snapshot file. Go back to Part A and the validator rather than nudging overrides — an override is marked orange and counted in a notice on the export, which is right, but it is still not a measurement.
Voice¶
Everything a client reads — thesis, symptoms, rationale, curve notes — is Wires Uncrossed content. Apply wue-messaging. The parts that bite most often in a narrative:
- Commonwealth spelling. Organise, prioritise, behaviour, recognised.
- Short sentences, verbs doing the work. No padding.
- Diagnostic, not prescriptive. Name what is happening and what to do. Do not lecture.
- Name the human outcome beside the operational one — calm, clarity, confidence, feeling heard. Put it in Symptoms, not as a slogan in the thesis.
- Avoid "agile transformation", "digital transformation", "best practice", "unlock", "holistic", "leverage", "seamless".
- Never a generic SDO claim with no specific context (no local reading, or a bare "elite performers" benchmark). In a narrative, that is exactly the unevidenced thesis above.
References¶
- narrative-format.md — the
.mdconvention, heading vocabulary, curve measure ids, round-trip rules - rework-recipes.md — six worked before/after reworks by source artefact
- signals-and-units.md — scales, inverted signals, telemetry units, what a missing reading becomes
- evidence-and-confidence.md — the citation and calibrated-language policy in full
- snapshot-template.json · snapshot-example.json
- narrative-overall-example.md · narrative-group-example.md · curve-notes-example.md
- rework-source-workshop.md — the messy input the recipes work from
- Companion: wue-messaging
Source of truth (in hoen-assessment), where this skill and the code disagree the code is right — fix the skill:
- Snapshot schema —
apps/hoen-explorer/src/snapshots/snapshot-types.ts,ingestinsnapshot-engine.ts - Narrative format —
apps/hoen-explorer/src/snapshots/narrative-md.ts - Export shape —
apps/hoen-explorer/src/snapshots/static-snapshot-*.ts - Plan and decisions —
docs/wip/snapshot-static-export.md,docs/core/kb/hoen-snapshot-reports.md
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
| Snapshot rejected on load | Missing rawMetrics or invalid asAt |
Ensure rawMetrics exists ({} is fine) and asAt is ISO-8601 |
| Coverage looks wrong | Invented / guessed need readings | Leave unmeasured needs null; re-run validate-snapshot.mjs |
| Many questions unmatched | Client instrument ≠ catalogue | Report the match rate; do not force-map; use feeds_needs to decide |
| Scores invert the workshop story | spendRank / workshopDotVote direction wrong |
Both invert. Confirm the direction with the consultant |
| Undiscussed needs outscore real priorities | Park bucket omitted from the spend stack | Transcribe the full stack including the shared bottom rank |
| Re-dropping the folder duplicates a group | Snapshot has no stable id |
Set id per group; it is also what narrative scope: names |
| A narrative heading sits in the unresolved banner | Heading is not a need id | Map to a need keeps the text — it is a rename, not a re-type |
| A per-group narrative tab never fills | scope: does not match any loaded group id |
Fix scope: to the snapshot id, not the display code |
| A curve note never appears in the export | Measure id wrong (change-fail-rate vs change-fail) |
Look for an unmatched chip on the curve-notes panel, or run validate-narrative.mjs --curves; ids are in narrative-format.md |
| Trade-offs tab missing from the export | No placement set, or every mark still at its default | Set positions and a rationale; an untouched scope exports nothing |
| Ranked constraints says unavailable | No narrative loaded | That notice is about the model's ranking. Load the narrative to replace it |
refresh-lookups cannot find the model |
Not run against hoen-assessment | Pass --model, set HOEN_MODEL_PATH, or HOEN_ASSESSMENT_ROOT |
| MTTR / change-fail look plausible but wrong | Unit mix-up | Hours not minutes; changeFailPct is 0–100 not 0–1 |