Skip to content

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:

top-opportunities-overall.md
top-opportunities-<snapshot-id>.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

## <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 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.

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, 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:

{
  "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.050.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.