Skip to content

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:

  1. Readings → snapshot JSON. What was measured. One file per group.
  2. 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:

  1. id against question_id (q0001q0161), case-insensitive. Always prefer this.
  2. text against 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.

{ "id": "q0001", "text": "We trust our alerting…", "average": 6.4, "numberOfResponses": 14 }
  • Score goes in average, or meanaverage wins if both are present. Scale is 0–10. Rescale anything else and say so.
  • nps, numberOfResponses, detractorsCount, passivesCount, promotersCount are carried for display, not scored.
  • A row with no average or mean contributes 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.

"2-alerting": { "maturity": 1.0, "spendRank": 1, "workshopDotVote": 0.31 }
  • 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.
  • workshopDotVoteshare 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 and spendRank stays 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.

  1. Set the boundary. Use commitment to available-to-customer. State a different start or end explicitly.
  2. 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.
  3. Classify the workflow with the team. Put every state into active or waiting. Review, approval, blocked, queued and ready states are not automatically one or the other — use what happens locally.
  4. 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.
  5. Calculate it. waiting duration / total elapsed duration × 100. Preserve the numerator and denominator beside the percentage.
  6. 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 in Signals & measures as 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

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, ingest in snapshot-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