Human fact review¶
Fact review is the per-fact approval gate. It is the only point where new facts — and new entities — enter the wiki.
New entity creation during review¶
When a proposal targets an entity the planner marked new, nothing in
the wiki has been created yet. The entity stub is created atomically
with the first approval of a fact targeting it:
- On the first approval, the tool creates
<category>/<slug>.yamlwith canonical name, aliases confirmed so far,created_at, andcreated_by_ingestset to the current ingest ID. It then appends the approved fact. - Subsequent approvals for the same entity in the same review session just append to the now-existing file.
- Aliases confirmed during any approval for that entity are merged
into its
aliaseslist (on stub creation for the first approval; on update for later ones). - The in-memory entity index is refreshed after each approval, so a proposal reviewed later in the same session that references an entity created earlier in the session sees it as existing.
If every proposal for a proposed new entity is rejected, no stub is ever written. This falls out of the design rather than requiring cleanup logic.
MVP: terminal review¶
auto-lorebook review <ingest_id>
Walks through proposals one at a time. Each proposal must be approved,
edited, or rejected before the next is shown — there is no skip or
defer. If the user exits (Ctrl-C or closes the terminal), untouched
proposal files remain in pending/<ingest_id>/proposals/, and the
next invocation of review resumes with the first remaining file.
Claim-group bundling¶
Proposals sharing a claim_group_id are shown as one bundle
screen: the claim text appears once, followed by a numbered
checklist of routes — one row per target. A single
approve / edit / reject decision covers every checked route. The
[t]argets action toggles individual routes on or off and is also
the home for per-target section / speaker overrides. Singletons
(claim groups with only one target) render the same screen with one
checked row and no [t] clutter; bundles span across groups in
transcript order.
─── Bundle 1 of 8 · Claim group cg-001 (3 of 3 routes selected) ────────
Proposed text:
"Theron's grandfather founded Aldara in the Second Age."
Raw transcript:
"Fair-on's grandfather founded all-dara in the Second Age."
Corrections applied:
• "Fair-on" → "Theron" (global)
• "all-dara" → "Aldara" (reading)
Source: Worldbuilding Session 3
Locator: 0:04:32-0:04:41 → https://youtube.com/watch?v=abc123&t=272
Status: authoritative
Session date: 2026-01-15
Context:
Before: "So let's talk about the founding of Aldara."
After: "And that's why the Theron name matters so much now."
Routes:
1. [x] Aldara (existing) section=founding speaker=DM
Matched via: alias "the Aldaran Realm"
2. [x] Theron (existing) section=lineage speaker=DM
3. [x] Second Age (NEW — events, will be created on approval)
section=events-in-era speaker=DM
[a]pprove [e]dit [r]eject [p]lay [t]argets [u]ndo
>
Bundle-level edits carry only text, status, and status_reason
— those are claim-level facts about the world, so they propagate to
every checked route and should agree across siblings.
Per-target overrides carry only section and speaker and live
in the [t]argets sub-prompt: different routes point at different
entities, so section is inherently route-shaped, and speaker
attribution can vary route-by-route. The two field sets are disjoint
by design — a reviewer cannot set a bundle-wide section.
New-entity routes¶
A route targeting a proposed new entity that does not yet exist on disk renders inline in the checklist:
3. [x] War of the Dusk (NEW — events, will be created on approval)
Suggested aliases: "the Dusk War"
A route whose entity was created earlier in the same review session:
2. [x] War of the Dusk (events) — created earlier this session
This note is derived at display time by comparing the entity's
created_by_ingest to the current ingest ID — no extra state
required.
Alias confirmation¶
For checked routes whose planner matched via a mention that isn't yet an alias, alias confirmation fires as a per-route sub-prompt after the user picks approve / edit, before any writes:
Add "the Realm" as alias for Aldara? [y/n]
Alias confirmation only fires for routes that survived the bundle
selection — dropping a route via [t]argets skips its alias prompt.
Confirmed aliases are recorded with source: alias-confirmation and
added_by_ingest set to the current ingest — or merged into the
stub at creation time if the entity is new and this is its first
approval, in which case the source on the first-approval aliases is
stub-creation. Aliases added via alias confirmation are cleanly
removable by reject-ingest. On Ctrl-C resume, prompts already
answered earlier in the same ingest are not re-asked: the engine
seeds its dedup set from on-disk alias records whose
added_by_ingest matches the current source.
Actions¶
- Approve — every checked route becomes a fact, appended to its target entity's YAML (creating the YAML if this is the first approval for a new entity). Unchecked routes are dropped — their proposal files are deleted.
- Edit — bundle-level edits to
text,status, andstatus_reasonpropagate to every checked route. Per-targetsection/speakeroverrides are set in[t]argets. Tracks original text astext_sourceon each affected fact. - Reject — discards the whole bundle: every route's proposal file is removed, no entity is touched.
- Play — prints the URL; user clicks through to verify against audio. Play is not a decision.
- Targets — sub-prompt to toggle individual routes on / off and
to set per-target
section/speakeroverrides on kept rows. Returns to the main prompt; the bundle then re-renders. - Undo — resets the current bundle's accumulated state back to
defaults: clears bundle-level edits, drops every per-target
override, and re-checks every route (un-rejecting any routes
toggled off via
[t]argets). Scope is the bundle currently on screen — once a bundle has been approved or rejected and the next one is shown, undo cannot bring it back.
Crash recovery¶
Review writes happen in two steps: entity_yaml.write then
proposal_path.unlink. A Ctrl-C between them leaves a proposal file on
disk that references a fact already written. Two recovery invariants cover
this:
-
Idempotent skip. When
_approvefinds the proposal'sproposed_idalready present in the entity's facts list, it skips the append and unlinks the proposal file anyway. The approved and edited counters do not increment — the resume run sees the correct totals. -
Proposals as subset. At the start of
run(), the engine validates that every on-disk proposal corresponds to a(claim_group_id, target_entity)key in the plan. Missing keys are normal after a partial run (already-approved proposals were unlinked). Extra keys — orphans whose plan key doesn't exist — raiseReviewErrorand name each offending file. The recovery path isreplan, which rebuilds the plan and discards proposals no longer sanctioned by it.
See also ADR 0001 for the decision rationale.
No skip, no defer¶
A "come back to it later" action would accumulate stale state that
drifts out of sync with the entity index and planner output. If review
surfaces systematic problems, the correct move is
replan, not deferral.
Post-MVP: web review UI¶
Same actions, richer presentation. Side-by-side views, inline editing, one-click playback. See roadmap.
Next stage: Stage 4 summarizer.