The post-mortem loop — analysis → recommendations → applied

Proposed 2026-08-08. Not started. Written after the Washington DC post-mortem, which produced roughly twenty framework proposals and had nowhere to put them.

The dead end

The loop today runs three states and then stops:

    tripIsOver ─→ wantsPostmortem ─→ wantsPostmortemAnalysis ─→ hasPostmortemAnalysis
                  author writes       session writes             ● nothing consumes it
                  the account         the analysis

PostmortemAnalysisSheet says so in its own header comment: the analysis is "the culminating artifact of the whole post-mortem phase," carrying proposals — profile edits, brief questions, rules that should graduate. It lands as markdown that a reader reads. Then nothing.

The DC analysis found, among other things, that a required class field was missing from a live entry, that stop_groups can name a group that no longer exists and silently render zero stops, that skip is filled on 0 of 23 eat places, and that provenance and confidence — now the two richest authored fields in the corpus — are declared by no schema. Every one of those is a fix to the framework. All twenty-odd are currently a markdown file and a memory.

What this spec adds is the back half: a way to decide which proposals are real, a durable record of the ones rejected, and a gated step that applies the rest.

The whole loop

  ┌── author ──┐   ┌── author ──┐   ┌── session ──┐   ┌══ author ══┐   ┌── session ──┐   ┌══ author ══┐   ┌── session ──┐
  │ THE RECORD │   │ THE MEMORY │   │             │   ║ GATE ONE   ║   │             │   ║ GATE TWO   ║   │             │
  │ 1. Outcomes│ → │ 2. Account │ → │ 3. Analysis │→  ║ 4. Correct ║→  │ 5. Ledger   │→  ║ 6. Triage  ║→  │ 7. Apply    │
  │            │   │            │   │             │   ║            ║   │             │   ║            ║   │             │
  └────────────┘   └────────────┘   └─────────────┘   └════════════┘   └─────────────┘   └════════════┘   └─────────────┘
   outcome +        postmortem in    *-analysis.md    "is the          *-ledger.json     "should we      low-blast only:
   outcome_note     guide.json             ↑           evidence         every finding     do it?"         profiles, rules,
   per plan item                           │           right?"          becomes an item   per item        brief slots
                                           └───────────────┘                                                    │
                                        revised analysis                                                        │
                                        — the ledger derives from the revision                                  │
                                                                                                                │
                                        declines ───────────────────────────────────────────────────────────────┤
                                        persist to categories/<id>/decisions.json ──────────────────────────────┘
                                        and are read by every future analysis

Steps 1 and 2 have the UI but no gate. Step 3 exists. Steps 4–7 are new.

Step 1 was missing from the first draft of this spec, and it is the most consequential omission in it. DayOutcomesSheet exists and works — but it is reachable only from GuideDocumentView.swift:235, as a per-day affordance inside the open document. Nothing anywhere says "the trip is over, record what happened." There is no wantsOutcomes gate, no door, no count of unlogged items.

Asheville is what that costs. Fourteen plan items, zero outcomes, and nobody was ever asked — so its post-mortem analysis could answer one of the seven questions it was asked and had to decline the other six. DC has 53 of 56 logged only because the author happened to go looking for the sheet.

The outcomes step is the evidence half of the entire loop. Everything downstream — causes, conventions, traveler signals, whether a rule graduates — reads outcome and outcome_note. Without it, the analysis has nothing to hold the account against, and a post-mortem becomes a transcription of memory. It belongs first, and it belongs behind a door that appears on its own.

1. Model changes

1.1 New gates on Guide

Following the existing pattern in CorpusModels.swift — computed, derived from what is on disk, never a stored flag that can disagree with reality.

/// The trip is over and plan items are still unlogged. The evidence half of
/// the loop, and the one gate that has no UI entry point today — DayOutcomesSheet
/// is reachable only from inside the open document, so a guide can finish a trip
/// with zero outcomes and nothing ever says so. Asheville did exactly that.
///
/// Deliberately NOT keyed to "every item logged". An item you genuinely cannot
/// remember should be allowed to stay empty, so this reports a count and offers
/// a door rather than blocking the loop.
var unloggedPlanItems: Int
var wantsOutcomes: Bool { tripIsOver && unloggedPlanItems > 0 && !outcomesDismissed }

/// The analysis has been read and commented on. Keyed to the author filing a
/// set rather than to a set existing, for the same reason `wantsPostmortem`
/// is keyed to submission: deciding you are done reading is a judgement.
var hasAnalysisReview: Bool

/// Read and commented, and no ledger has been written against those comments.
var wantsRecommendations: Bool {
    hasPostmortemAnalysis && hasAnalysisReview && !hasLedger
}

/// A ledger exists with at least one item still `open`.
var wantsLedgerTriage: Bool { hasLedger && ledgerHasOpenItems }

/// Every ledger item is decided and at least one `accepted` item is unapplied.
var wantsFrameworkUpdate: Bool { hasLedger && !ledgerHasOpenItems && ledgerHasUnapplied }

Only one of these is ever true at a time, which is what makes the guide bar's door unambiguous (§4.1).

1.2 The ledger

One file per pass, beside the analysis it came from:

guides/washington-dc/postmortem/
    changelog.md                    generated evidence          (exists)
    2026-08-08-analysis.md          the session's findings      (exists)
    2026-08-08-ledger.json          the decisions               (new)
{
  "version": 1,
  "guide": "washington-dc",
  "analysis": "2026-08-08-analysis.md",
  "created": "8 August 2026 · 17:40",
  "items": [
    {
      "id": "rec-class-required",
      "title": "Validate that every entry declares `class`",
      "finding": "§6.2",
      "rationale": "d4-afternoon-sit has no class and is invisible to everything that reads the corpus — absent from the item count, the changelog, and outcome logging.",
      "evidence": ["d4-afternoon-sit"],
      "target": { "type": "validation", "category": "itinerary" },
      "blast": "category",
      "affected": { "guides": 2, "entries": 169 },
      "change": "Add a required-field rule to categories/itinerary/validation-contract.json",
      "status": "open",
      "decided": null,
      "applied": null,
      "thread": []
    }
  ]
}

Field notes, where the choice is not obvious:

1.3 target.type — what a recommendation can point at

NoteTarget today addresses guide, brief, section, entry, miniGame — all of them inside one guide's material. Recommendations point outward, at the framework, which is why they need their own vocabulary rather than a sixth case on NoteTargetType:

typewrites toblast
profileprofiles/<id>.jsontraveler
trip_ruleguides/<id>/guide.json → trip_rulesguide
brief_slotcategories/<id>/brief.jsoncategory
schemacategories/<id>/schemas/entry.jsoncategory
validationcategories/<id>/validation-contract.jsoncategory
editorialcategories/<id>/default_editorial_notescategory
entry_fixone or more entries in this guideguide
appnone — a Press Raven change, filed not appliedapp

app matters: the DC analysis produced findings that are code changes, not corpus changes (the MarkdownReader renders pipe tables verbatim; created is not updated on rewrite). Those should be capturable by the ledger and explicitly not applicable by it — they leave as a note, not an edit.

2. Where declines live — the one call that decides whether this works

Declines must outlive the guide that produced them.

This is the difference between a loop that compounds and a treadmill. Trip five is a different guide. If a declined recommendation is stored in guides/washington-dc/postmortem/2026-08-08-ledger.json, the analysis for trip five will never read it, will re-derive the same finding from the same kind of evidence, and will re-propose it. The DC analysis already flagged this against itself: price-ceiling is a preference rather than a lesson, and absolutely will be re-proposed every trip unless the rejection is durable.

So the ledger is the per-pass record, and decisions are also projected to a category-level file:

categories/itinerary/decisions.json
{
  "version": 1,
  "decisions": [
    {
      "id": "rec-price-ceiling-graduate",
      "title": "Graduate the $200 clothing ceiling to default_editorial_notes",
      "status": "declined",
      "decided": "2026-08-08",
      "reason": "It is a preference, not something the road proved. Ask again if a trip records a purchase abandoned on price.",
      "from": { "guide": "washington-dc", "ledger": "2026-08-08-ledger.json" },
      "reconsider_when": "an outcome_note records a stop lost to price"
    }
  ]
}

Two consequences worth stating plainly:

3. Two feedback gates, doing two different jobs

Settled 2026-08-08. The loop has two author-feedback stages and they are not the same review performed twice. Confusing them is the easiest way to build this wrong — my first draft of this spec did exactly that, by treating stage-one comments as a filter on which findings survive.

stagethe question it answerswhat it acts onwhat it produces
3 · on analysisIs the evidence right?findings, facts, framinga corrected analysis
5 · on ledgerShould we do it?proposed changesaccept / decline

Stage three is corrective, not selective. The author is checking the reasoning: what the record actually shows, what got misread, what context the session could not see from the corpus. The DC pass is the worked example — the author's note that Wednesday's entries had been rewritten after the fact reversed the analysis's headline finding. That is not filtering; that is repairing the evidence base before anything is derived from it. Had the analysis gone straight to a ledger, every downstream recommendation would have rested on "Wednesday completed," which was false.

So stage three produces a revised analysis, not just an annotated one, and the ledger derives from the revision. Concretely: 2026-08-08-analysis.md is rewritten in place (or superseded by a dated successor — see the final review), with the author's set attached and each note resolved done or declined in the usual way. A note that changed a finding gets a reply saying how.

Stage five is selective. Every surviving finding becomes a ledger item — nothing is dropped for lack of attention — and the author decides, item by item, what actually gets written.

3.1 The recommendations session

Step 4's packet is narrower than the analysis packet. It receives the revised analysis, the author's resolved annotation set, categories/<id>/decisions.json, and the current schema, brief, validation contract and profiles. It returns a ledger and nothing else. Four rules:

  1. Every finding in the revised analysis becomes a ledger item. The author's attention is not the filter — the triage is. A finding they did not comment on is a finding they did not dispute.
  2. Mark each item defect or judgement. A defect is a checkable fact about the corpus: class is missing, stop_groups names a group that does not exist, a declared field is unfilled. A judgement is anything about people, preferences, or what a rule should mean: "Beckett loves arcades," "graduate the price ceiling." The distinction is not decoration — it drives triage speed (§4.3) and it is the honest separation between what the record proves and what someone has to decide.
  3. Every item states its blast radius and counts what it touches. Not "this affects several guides."
  4. Never re-propose something decisions.json records as declined. Argue against the existing decision in its thread instead.

4. The interface

The material is the point here. Five screens, and the design goal for each is that the author never wonders what happens next or what a choice will cost.

4.1 The guide bar — one door, named for what it opens

GuideBar already owns this pattern and its header comment already argues for it: naming a control "costs one row and removes the guessing." The post-mortem door today has two labels across two states. It gains five more, and only ever shows one, because the gates in §1.1 are mutually exclusive:

gatedoor labeltint
wantsOutcomesLog 14 outcomescmCoral
wantsPostmortemRecord the tripcmAmber
wantsPostmortemAnalysisAnalyse itcmButtonAI
wantsRecommendationsDraw up the changescmButtonAI
wantsLedgerTriage12 to decidecmOcean
wantsFrameworkUpdateApply 7 changescmFreshSage
— none —(no door)—

Three of these carry a live count. A door that says "12 to decide" tells you the size of the task before you open it, which is the whole difference between a door you walk through and one you put off. "Log 14 outcomes" is the one that would have saved Asheville.

The loop rail. Beside the door, a seven-segment rail — filled, current, pending — with the current segment labelled. It is the only persistent answer to "where am I in this," and it costs about 40pt of the bar. Hovering a completed segment shows what it produced and when; clicking it reopens that artifact read-only. Nothing in the loop should be unreachable once passed.

4.1a Logging outcomes — the step that has no door

DayOutcomesSheet already renders the work well. What it lacks is any reason to open it. Three changes, none large:

The one thing to get right: never make a blank feel like a failure. An item you cannot remember should stay blank, because a guessed outcome is worse than no outcome — that is the whole lesson of the DC day-four contamination. The sheet should offer "I don't remember" as an explicit, dignified choice that closes the item without asserting anything, and the door's count should fall when you use it.

Timing. The door appears when the trip ends. It should also be reachable during the trip, because DC's best notes were written on the day and Asheville's were never written at all — but it must never nag mid-trip. Appearing quietly in the bar, with a count, is enough.

4.2 Reading and commenting — PostmortemAnalysisSheet grows a margin

The sheet already has the right bones: a two-pane switch between "What it found" and "The evidence," a version picker for multiple analyses, and MarkdownReader below. Three additions:

4.2a Writing the account — the form is right; the prompting is not

PostmortemSheet is a four-field form and should stay one. The account is impression, not record, and a review-style interface would imply there is something to review — there isn't yet.

Two changes, both learned from what the two accounts actually produced:

One thing not to add: prompts that fish for structure. "How was the pacing?" would get answered even when the author had no view, and an invented answer in the account becomes a finding in the analysis. The existing hints — "what you would do again" — are the right level.

And fix the table rendering while here. MarkdownReader.swift:106 renders pipe tables as Text(rows.joined(separator: "\n")) — raw source, monospaced, no inline parsing. That was a deliberate call for the changelog, whose tables are pre-aligned in source. But it means any table that is not pre-padded reads as a code block, and bold inside a cell shows as literal asterisks. The DC analysis hit both. Either parse the pipe table into a real grid, or keep monospace and strip inline markers before display. Recommend the grid: these documents are increasingly tabular, and "preserve what the author intended" stops being the right instinct once the author is a session writing to a spec.

4.3 The ledger — a triage queue, not a form

This is the screen that decides whether the loop is used. It should feel like a review queue: one card, keyboard-first, evidence inline.

┌────────────────────────────────────────────────────────────────────────┐
│  Changes to decide                        7 accepted · 3 declined · 12 left │
│  ● ● ● ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○                          │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│   CATEGORY · itinerary          2 guides · 169 entries       §6.2      │
│                                                                        │
│   Validate that every entry declares `class`                           │
│                                                                        │
│   d4-afternoon-sit has no class and is invisible to everything         │
│   that reads the corpus — absent from the item count, from the         │
│   changelog, and from outcome logging.                                 │
│                                                                        │
│   Evidence   [ d4-afternoon-sit ↗ ]                                    │
│                                                                        │
│   Will write  categories/itinerary/validation-contract.json            │
│               ┌──────────────────────────────────────────┐             │
│               │ + { "rule": "required", "key": "class" } │             │
│               └──────────────────────────────────────────┘             │
│                                                                        │
│   ─────────────────────────────────────────────────────────────        │
│   [ A  Accept ]   [ D  Decline ]   [ ⌥  Defer ]      [ ⏎ Reply ]       │
└────────────────────────────────────────────────────────────────────────┘

The parts that are doing work:

Defects and judgements are triaged differently, and the queue should say so. A defect is a checkable fact — class is missing, stop_groups points at a group that no longer exists — and the only real question is whether to fix it now. A judgement is a claim someone has to stand behind. So:

That split is also the honest one. The session is entitled to assert defects — it can prove them — and is not entitled to assert judgements about people.

Grouping otherwise: default to blast radius ascending — travelers, then guide, then category, then app. It puts the cheap, obviously-correct decisions first, which builds momentum and gets the author calibrated before the expensive ones. A segmented control switches to analysis-section order for anyone who would rather work in the document's sequence.

4.4 Applying — split by blast radius

Settled 2026-08-08: the session applies low-blast changes and never applies category-level ones.

targetapplied bywhy
profile, trip_rule, brief_slotthe sessionone traveler or one guide; a mistake is obvious on sight and cheap to revert
entry_fixthe sessionscoped to this guide, and the diff shows exactly which entries
schema, validation, editorialthe authorchanges how every guide in the category is read, retroactively
appnobodya Press Raven change; it leaves as a note

The reasoning is about damage, not effort. A wrong profile edit says something untrue about Beckett and you see it next trip. A wrong schema or validation change alters how every existing guide validates and renders — including guides authored before the finding existed — and a plausible-but-wrong one may not surface for two trips. That is exactly the class of change that should cost a deliberate human act.

It also preserves the separation that makes the analysis worth reading. The session can report that the schema fails to declare provenance precisely because it is not the thing that rewrites the schema.

For author-applied items the pane shows the exact JSON to paste, per file, with a copy button — not prose describing the change. The friction that kills this loop is fiddly mechanical editing, and a correct fragment on the clipboard removes almost all of it while keeping the decision human.

For session-applied items: a single review pane with every accepted change as one combined diff, grouped by file, per-hunk toggles available. Nothing is written until "Apply" is pressed, and what is written is exactly what is shown.

4.5 The decisions ledger, at category level

A category needs a way to see its own accumulated judgement. Proposed: a Decisions pane on the category, listing every declined and applied recommendation with its guide of origin, date, and reason — searchable, and each row expandable to the original thread.

Two affordances that matter: a Reconsider button on any declined row, which reopens it into the next ledger; and a filter for "declines whose reconsider_when has been met," which is the queue this whole mechanism exists to produce.

5. What to build first

The loop is only useful end to end, but it can land in three shippable stages:

Stage zero — the outcomes door. wantsOutcomes, the trip-level entry point, keyboard triage, and the "I don't remember" escape. Small, and it is the only stage that improves the next trip rather than the last one — everything else in this spec processes evidence, and this is the stage that creates it. Asheville is the argument: no amount of downstream tooling recovers a trip nobody logged.

Stage one — capture. Ledger schema, decisions.json, the recommendations session, and a plain list UI with accept/decline. No diffs, no apply — accepted items are a checklist the author works by hand. This alone fixes the dead end, and it is where the durable-decline mechanism starts paying.

Stage two — the queue. The card UI, keyboard triage, evidence chips, blast badges, the loop rail, and the analysis-sheet commenting margin. This is the stage that makes it pleasant enough to actually use every trip.

Stage three — apply. Diff generation, the combined review pane, the commit, per-item failure handling, and the category Decisions pane with Reconsider.

Fix MarkdownReader's table rendering in stage one regardless — it is small and every document in this loop is tabular.

Final review

What this gets right. It closes the loop without weakening the thing that makes the analysis trustworthy: the session still only proposes, and every framework change passes through an explicit human decision. It reuses the annotation model almost entirely — fix/ask, open/done/declined, threaded replies — rather than inventing a parallel commenting system. And the category-level decisions file is the piece that makes trip six cheaper than trip five, which is the actual goal.

What it costs. Six states is a lot of ceremony for a trip that produced two findings. A short trip should be able to run the whole loop in one sitting, and if steps 3–5 feel heavy for a three-item ledger the author will skip straight to editing files by hand — which is the current behaviour and the thing this is meant to replace. Mitigation: when a ledger has three or fewer items, collapse the queue to a single list with inline accept/decline and skip the card UI entirely.

Where it is inadequate. The affected counts are only as good as the session's ability to compute them, and a wrong count on a CATEGORY card is worse than no count — it manufactures false confidence in exactly the decision that deserves the most care. These should be computed by the app from the corpus after the session writes the ledger, not trusted from the session. I would move that to stage one rather than leaving it as a session responsibility.

Second — and this is now the main path rather than an edge case. Gate one produces a revised analysis, so revision is normal, not exceptional. The mechanics need to be decided rather than assumed: does the session rewrite 2026-08-08-analysis.md in place, or write 2026-08-08-analysis-v2.md beside it? Recommend rewriting in place, with git as the history — the DC analysis is already the only readable account of the trip, and two files where one has been superseded is the same trap §1.1 of that analysis describes. But note the irony and take it seriously: the finding was that in-place rewriting destroys the evidence that reality differed. The difference here is that the analysis is authored prose under version control, not a record whose whole purpose is to be compared against an outcome. Worth a second opinion.

Third: nothing here handles a recommendation that is right but too expensive to apply now. defer is per-session, not durable. That probably wants a fourth status — deferred, with its own resurface trigger — but I would wait until a real ledger produces one rather than designing for it now.

Confidence: 84/100, up from 78 on the first draft. The two decisions settled 2026-08-08 — corrective-then-selective gates (§3), and applying split by blast radius (§4.4) — removed the two places the spec was guessing at how the author works, and both replaced an assumption with a stated rule. The defect / judgement split that fell out of them is the part I would now defend hardest: it is the honest line between what the session can prove and what a person has to decide, and it makes the triage queue fast where it should be fast.

What is still unproven is unchanged and unfixable by more thinking: no real ledger exists, so the card queue is designed against an imagined data shape. Asheville carries 0 outcomes and 0 provenance across 61 entries, so there is no second trip to test rule graduation or the reconsider_when mechanism against — those need the next trip, not a better spec. Building the DC ledger for real is the one action that would move this further.