# The consultant narrative — format reference The narrative is Markdown the consultant writes and the explorer reads, ranks and publishes. It round-trips: the tool parses it, the consultant drags the order, and **Write back** serialises the current order out again. The file that comes out of a session is the file that goes into the next one. Source of truth (in **hoen-assessment**): `apps/hoen-explorer/src/snapshots/narrative-md.ts` and `narrative-store.ts`. ## The whole format ```md --- scope: grp-b-payments title: Top opportunities, Payments --- ## 1-tech-capability **Skills silos across data ops, applications and domain knowledge** ### Symptoms The same individuals always pick up the same work — data ops, application changes, requirements and troubleshooting all route to named people rather than to whoever is free. ### Signals & measures - SME self-rating 1.7/3; spend ranked #1 by the workshop - Work allocated by person instead of from a priority queue - More than one incident a week needs a user in the room to diagnose ## tbc **Something the workshop flagged that we have not named yet** ``` Nothing else is structural. There is no schema to satisfy beyond this. ## Frontmatter | Key | Required | Meaning | |---|---|---| | `scope` | Effectively yes | `overall`, or a **snapshot `id`** — not the display `code`. Without it the document is filed against whichever tab was open. | | `title` | No | Section heading in the export. Defaults to nothing. | | anything else | No | Preserved verbatim on write-back. Use for engagement notes, author, review date. | Parsing is deliberately not YAML — it reads `key: value` lines and strips a trailing ` # comment`. Do not use nested keys, lists or block scalars; they will not parse and will not error either. **`scope` wins over the tab it was dropped on.** Uploading the group file while Overall is open files it correctly rather than merging it into Overall. **A document naming a group that is not loaded still gets a tab.** It is reachable and editable rather than stranded in the session — but check whether the `scope` is a typo before writing into it. ### Filenames The convention the tool writes back to: ```text top-opportunities-overall.md top-opportunities-.md ``` An uploaded file keeps its own name for write-back. Only a scope with no file behind it gets the default name. ## Headings — the ranking `## ` opens an entry. **Document order is the rank**, first is highest. There is no rank number to keep in sync, which is the point. Three kinds of heading, and the difference matters: | Heading | Kind | Behaviour | |---|---|---| | A need id — `## 2-alerting` | `target` | Ranked. The tool prints the model's own position beside it. | | `## need: 2-alerting` | `target` | Same thing. The `need:` prefix is stripped. | | `## tbc` | `provisional` | A reserved slot for something named but not yet placed. A real state, not an edge case. | | Anything else — `## Platform friction` | `unresolved` | **Held, not dropped.** The text survives, unranked, until it is mapped or renamed. | Need ids are the 41 in [need-ids.csv](../assets/need-ids.csv). Case-sensitive, matched exactly against the model. ### Unresolved headings are a feature Write the consultant's own words as the heading when nothing in the model fits yet. The tool shows a banner naming the unresolved headings with a *Map to a need* action, and **mapping is a rename, not a re-type** — the thesis, symptoms and signals written under it all survive the mapping. The alternative — forcing a claim onto the nearest need so the file parses cleanly — buries the fact that the model had no home for it. That fact is worth keeping. ## The body of an entry Everything between one `##` and the next belongs to that entry, split into four slots: | Slot | Read from | Written back as | |---|---|---| | **thesis** | The first non-blank line, `**bold**` stripped | `**thesis**` | | **symptoms** | Under `### Symptoms` | `### Symptoms` | | **signals** | Under `### Signals & measures` (or `### Signals and measures`) | `### Signals & measures` | | **extra** | Anything else — a second paragraph before a sub-heading, an unrecognised `###` with its own title | Appended at the end | Sub-heading matching is case-insensitive. `### signals and measures` is fine. Two consequences worth knowing: - **The thesis is one line.** A second paragraph before the first sub-heading is not folded into the claim — it lands in `extra` and is written back at the bottom of the entry. Put it under `### Symptoms` instead. - **An unrecognised sub-heading survives with its title.** `### Recommendation` is not lost; it becomes part of `extra`. Use this for material that has nowhere else to go, knowing it renders after the two known sections. ### Entry completeness The tool badges each entry by how much is written. Useful as a working checklist: | State | Means | |---|---| | `provisional` | It is the `## tbc` slot | | `empty` | No thesis and no symptoms | | `partial` | Thesis or symptoms, but no `Signals & measures` | | `written` | All three present | A ranking full of `partial` entries is a ranking of unevidenced claims. See [evidence-and-confidence.md](../../wue-messaging/references/evidence-and-confidence.md). ## Model divergence For every `target` entry the tool shows the model's own rank beside the chosen position, as `agrees`, `up N` or `down N`. This is not a warning — the ranking is the consultant's — but it is on the record, and the **Ranked constraints** tab of the export prints both. A deliberate disagreement is a strong artefact. An accidental one is a mistake about to be published. So when a chip says `up 7`, either the narrative says why or the order is wrong. There is no model position for a `provisional` slot or a need carrying no score, and the chip is blank rather than zero. ## Curve notes A separate document, same format, one heading per **measure id**. Each note renders under its own curve on the Scoring curves tab. Curves with no note are untouched. ```md --- scope: overall title: Notes on the scoring curves --- ## lead-time **Deploys wait on review, not on build.** ### Signals & measures - Median review wait is 14 hours of a 19-hour lead time - Three approvers required on every change, regardless of size ``` The whole entry is flattened into one note in the export — thesis, symptoms, signals and extra, joined in that order. The sub-headings are a convenience for the author, not a structure the curve tile preserves. ### Curve measure ids These are the ids, verbatim. They are **not** the dashboard's names and not the `rawMetrics` field names. A heading naming anything else resolves to a note the export never looks for. The upload resolver is deliberately permissive — it accepts any heading so the text is never lost — so the id is only checked when it is used. Since 2026-08-20 the panel marks each uploaded heading **matched** or **unmatched** and names the valid ids beneath the chips. Before that it gave no signal at all, which is how the bundled demo shipped a `change-fail-rate` note that never rendered. | Measure id | Signal | Reading behind it | |---|---|---| | `lead-time` | CTP | `leadTimeHours` — commit to production | | `deploy-freq` | CTP | `deployPerDay` — against active contributors | | `wait-time` | CTP | `waitTimePct` — share of lead time waiting | | `change-fail` | CQA | `changeFailPct` | | `recovery-time-cqa` | CQA | `recoveryTimeHours`, as change quality | | `low-incidents` | CQA | `incidentsLowPerMonth`, per team | | `release-conf` | CQA | `releaseConfidence` | | `medhigh-incidents` | QoS | `incidentsMedHighPerMonth`, per team | | `recovery-time` | QoS | `recoveryTimeHours`, as service quality | | `release-conf-2` | QoS | `releaseConfidence`, as service quality | Traps in that list: - `deploy-freq`, not `deploy-frequency`. - `change-fail`, not `change-fail-rate` or `cfr`. - **Recovery time and release confidence appear twice**, feeding two signals. A note on `recovery-time` shows under the QoS curve only; the CQA curve needs `recovery-time-cqa`. Write the same note to both ids if it belongs to both. Run `validate-narrative.mjs --curves` on the file to catch these before the tool is even open — it knows the same alias traps and suggests the right id. ## Objective trade-offs Trade-offs are **not** in the narrative `.md`. They are set in the tool and travel in `hoen-explorer-overrides.json`: ```json { "kind": "hoen-explorer-override-patch", "objectivePlacements": { "overall": { "positions": { "qualityOfService": 0.12, "valueGeneration": 0.34, "generativeCulture": 0.58, "governanceStd": 0.88 }, "rationale": "Quality of service is protected while the platform migration runs…" } } } ``` - Position is `0.05`–`0.95`. **Low is Fixed** (protected), high is Flexible (tradeable). `≤0.35` reads *fixed*, `≥0.65` reads *flexible*, between is *balanced*. - Marks closer than `0.02` count as a tie. The page names any tied pair and asks for a decision, because "equally tradeable" is the answer the exercise exists to refuse. - Scopes are `overall` and snapshot ids, matching the narrative convention. - A scope left at its evenly-spread default with an empty rationale exports nothing at all. Nothing here re-scores or re-ranks anything. It records stated intent so the ranking can be read against it. ## Write-back `serialiseNarrativeDoc` regenerates the file from the session: - Frontmatter is `scope`, then `title` if set, then any other keys in the order they were read. - Entries in the current (dragged) order. - Each entry emits only the parts that have content — an entry with no symptoms has no `### Symptoms` heading. - The thesis is re-bolded. So the round-trip is stable but not byte-identical: blank-line spacing is normalised, `### Signals and measures` becomes `### Signals & measures`, and `extra` moves to the bottom of its entry. Commit the file after a write-back rather than diffing it against your hand-written original and worrying.