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 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.
┌── 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.
GuideFollowing 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).
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:
finding points back into the analysis by section. The ledger is a decision record, not a second copy of the argument — the reasoning stays in the markdown and the ledger cites it. This is the same discipline the analysis itself uses on entry ids.evidence is the list of entry ids the finding rests on, so the UI can make them clickable (§4.3). A recommendation you cannot trace to a record is the thing the DC analysis spent its whole preamble warning about.affected is computed at ledger-write time, not guessed. "This touches 169 entries across 2 guides" is the single most decision-changing number on the card and the session has to actually count it.status reuses NoteStatus — open, done, declined — plus accepted as an intermediate between "decided yes" and "actually written". Accepting and applying are different events and can fail independently.target.type — what a recommendation can point atNoteTarget 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:
| type | writes to | blast |
|---|---|---|
profile | profiles/<id>.json | traveler |
trip_rule | guides/<id>/guide.json → trip_rules | guide |
brief_slot | categories/<id>/brief.json | category |
schema | categories/<id>/schemas/entry.json | category |
validation | categories/<id>/validation-contract.json | category |
editorial | categories/<id>/default_editorial_notes | category |
entry_fix | one or more entries in this guide | guide |
app | none — a Press Raven change, filed not applied | app |
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.
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:
decisions.json first and is told: do not re-propose these; if you believe the evidence has changed, say so against the existing decision rather than filing a new one. That instruction belongs in the analysis session packet.reconsider_when is optional and is the good part. A decline with a stated trigger is not a dead end, it is a dormant recommendation. When trip five's evidence satisfies it, the item resurfaces marked as previously declined, with the original reason attached — so the decision is being revisited rather than re-litigated from scratch. This is the mechanism that makes the framework get smarter instead of naggier.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.
| stage | the question it answers | what it acts on | what it produces |
|---|---|---|---|
| 3 · on analysis | Is the evidence right? | findings, facts, framing | a corrected analysis |
| 5 · on ledger | Should we do it? | proposed changes | accept / 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.
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:
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.decisions.json records as declined. Argue against the existing decision in its thread instead.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.
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:
| gate | door label | tint |
|---|---|---|
wantsOutcomes | Log 14 outcomes | cmCoral |
wantsPostmortem | Record the trip | cmAmber |
wantsPostmortemAnalysis | Analyse it | cmButtonAI |
wantsRecommendations | Draw up the changes | cmButtonAI |
wantsLedgerTriage | 12 to decide | cmOcean |
wantsFrameworkUpdate | Apply 7 changes | cmFreshSage |
| — 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.
DayOutcomesSheet already renders the work well. What it lacks is any reason to open it. Three changes, none large:
D done · P partial · S skipped · N not taken · M moved · R replaced, then ⏎ to drop into the note field and ⇥ to the next item. Fifty-six items is a real sitting; at one keystroke each it is five minutes.outcome alone gives you a count; outcome_note gives you a cause, and §2 of both analyses is built entirely out of notes. So the field is always visible, never behind a disclosure, with a placeholder that models the register: "Closed; Andy's Pizza instead." — one line, why it went that way.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.
PostmortemAnalysisSheet grows a marginThe 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:
Annotation targeted at the analysis with the selected text as quoted context. This is the same gesture as every review tool and needs no explanation.cmOcean. Clicking scrolls and opens the thread. AnnotationsSheet already renders noteRow, statusControl and replyControl — reuse them verbatim rather than building a second commenting UI.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:
travelers roster, say so and offer to set one. Asheville has none, which is why its packet printed "No roster" and why no per-traveler box could be drawn at all. A silent absence is how that section stayed empty.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.
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:
TRAVELER / GUIDE / CATEGORY / APP — with the computed count beside it. You decide differently about one traveler's profile than about 169 entries, and the card should say which one it is before you read the title.d4-afternoon-sit opens it in the document behind the sheet, scrolled and highlighted. A recommendation you can check in two seconds is a recommendation you decide honestly. This is why evidence is on the ledger schema at all.schema and validation targets this is a small JSON fragment; for profile it is the field-level change. Nothing gets accepted sight-unseen.A accept, D decline, → defer, ⏎ reply, ← back. Twenty-two decisions at one keystroke each is a two-minute task; twenty-two mouse round-trips to a dropdown is a chore you abandon at item nine.decisions.json and stops the next trip re-proposing it. Offer an optional second field, "reconsider when," with a placeholder that shows a real example.open and moves on. The door keeps its count, so a half-triaged ledger is visibly half-triaged rather than silently lost.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.
Settled 2026-08-08: the session applies low-blast changes and never applies category-level ones.
| target | applied by | why |
|---|---|---|
profile, trip_rule, brief_slot | the session | one traveler or one guide; a mistake is obvious on sight and cheap to revert |
entry_fix | the session | scoped to this guide, and the diff shows exactly which entries |
schema, validation, editorial | the author | changes how every guide in the category is read, retroactively |
app | nobody | a 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.
provenance itself — the DC analysis found it was added on trip three and declared by no schema even then. A framework change with no record of why it exists is exactly the thing that gets re-litigated on trip six.feat(itinerary): apply 7 changes from the Washington DC post-mortem with the item titles as the body. Reversible by git revert, which is the only undo worth promising.accepted with an error on its thread; the rest still land. The door reappears with the remainder.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.
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.
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.