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¶
---
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:
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¶
## <heading> 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. 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
extraand is written back at the bottom of the entry. Put it under### Symptomsinstead. - An unrecognised sub-heading survives with its title.
### Recommendationis not lost; it becomes part ofextra. 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.
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.
---
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, notdeploy-frequency.change-fail, notchange-fail-rateorcfr.- Recovery time and release confidence appear twice, feeding two signals. A note on
recovery-timeshows under the QoS curve only; the CQA curve needsrecovery-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:
{
"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.35reads fixed,≥0.65reads flexible, between is balanced. - Marks closer than
0.02count 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
overalland 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, thentitleif 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
### Symptomsheading. - 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.