--- name: hoen-data-assist description: "On-site assistant for a HoEN assessment: turn raw client material — survey exports, workshop walls, SDO screenshots — into group snapshot JSON, and rework existing docs, markdown and workshop notes into the consultant narrative the explorer ranks and the static export publishes. Use when a consultant is with a client holding assessment data or prior write-ups in any shape the tool does not read yet, or asks to build, fix, validate, rank or narrate an assessment. Trigger phrases: snapshot JSON, group snapshot, HoEN intake, survey export, workshop data, dot vote, maturity ratings, spend ranking, rawMetrics, SDO, software delivery and operational performance, narrative markdown, top opportunities, rank and narrate, curve notes, trade-offs, export as static, \"turn these notes into the report format\"." license: Proprietary — Wires Uncrossed compatibility: Requires Node.js. The narrative validator is fully offline. Snapshot validation and lookup refresh need a hoen-assessment checkout (or HOEN_MODEL_PATH / --model pointing at packages/hoen-model/dist/model.json). phase: [diagnose] hoen: [2-info-mgmt, 3-service-metrics, 2-standards] maturity: trial metadata: user-invocable: true site-files: true version: "2.0" provenance: original_creator: team: WU Engineering team library_added_by: name: Myles Henaghan date_added: 2026-08-10 source: origin_project: hoen-assessment hoen_library_tag: not_set source_commit: not_set stewarding_team: wires-uncrossed --- # HoEN data assist > **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. ```mermaid 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. ```text 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](assets/snapshot-template.json) — all 41 need ids in model order, every value `null`. [snapshot-example.json](assets/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](assets/survey-questions.csv) (`question_id, text, priority, feeds_needs`). Resolution order mirrors the rollup: 1. **`id` against `question_id`** (`q0001`–`q0161`), 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. ```json { "id": "q0001", "text": "We trust our alerting…", "average": 6.4, "numberOfResponses": 14 } ``` - Score goes in `average`, or `mean` — `average` 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](assets/need-ids.csv) maps all 41 ids to level and display name; match on the name, write the id. ```json "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. - `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 *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](references/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`): ```bash # 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. ```bash 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: ```md ## 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](references/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](../wue-messaging/references/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](references/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: ```bash 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 `## ` 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](references/narrative-format.md#curve-measure-ids). 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](../wue-messaging/SKILL.md). 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](references/narrative-format.md) — the `.md` convention, heading vocabulary, curve measure ids, round-trip rules - [rework-recipes.md](references/rework-recipes.md) — six worked before/after reworks by source artefact - [signals-and-units.md](references/signals-and-units.md) — scales, inverted signals, telemetry units, what a missing reading becomes - [evidence-and-confidence.md](../wue-messaging/references/evidence-and-confidence.md) — the citation and calibrated-language policy in full - [snapshot-template.json](assets/snapshot-template.json) · [snapshot-example.json](assets/snapshot-example.json) - [narrative-overall-example.md](assets/narrative-overall-example.md) · [narrative-group-example.md](assets/narrative-group-example.md) · [curve-notes-example.md](assets/curve-notes-example.md) - [rework-source-workshop.md](assets/rework-source-workshop.md) — the messy input the recipes work from - Companion: [wue-messaging](../wue-messaging/SKILL.md) 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](references/narrative-format.md#curve-measure-ids) | | 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 |