Skip to content

Make summary prose correctable in place (sovereign user revisions) - #251

Merged
Ambisphaeric merged 3 commits into
mainfrom
feat/summary-correct
Jul 18, 2026
Merged

Ambisphaeric merged 3 commits into
mainfrom
feat/summary-correct

Conversation

@Ambisphaeric

Copy link
Copy Markdown
Owner

Closes #246

What

The summaries block why-line promised "a draft you can correct", but no edit affordance existed and the "user corrections sovereign" invariant was unreachable for summary prose. This makes summary prose correctable in place, stored as a sovereign, append-only user revision.

Sovereignty (the acceptance bar)

A user-corrected summary OUTRANKS any machine revision on read AND survives re-derivation: correct → re-run the producer (even one that appends a NEW machine revision for the same window) → the read still returns the user text. Resolution mirrors the resolveClaimHeads pair-key precedent — a stable window/level identity keyed so a freshly re-derived revision cannot defeat the correction. The machine chain is never mutated or deleted (retrievable with includeSuperseded).

Contract

  • Summary gains source: 'model' | 'user' (optional — absent ⇒ model, back-compat), a correction stamp, and a corrects link; proposal relaxed to boolean (a user correction is proposal:false).
  • New SummaryCorrection shape + CorrectSummaryRequest payload; POST /summaries/correct route. Schemas regenerated and committed (zero-drift).

Engine

  • store.correctSummary mints a source:'user' revision carrying the target's identity, confidence:1, a correction stamp, and only STRUCTURAL provenance — no model-invoke fields, so human prose is never attributed to a model. resolveSummaryHeads resolves supersession + sovereignty in one pass. The producer excludes user rows from the machine chain. A degraded target is refused. Purely local write — no new egress path.

Client

  • A pencil affordance on live rows (opt-in via a threaded correction context; degraded/empty rows get none) swaps the row to an inline editor; save posts the correction and the row re-renders with the corrected prose plus an honest "edited by you" marker (user-select:none). The SummaryEditSession controller dispatches the verbs (unioned into the interaction-honesty lint); a refused save surfaces a calm visible line with the raw reason on hover. Copy stays value-only (carries the corrected bare text).

Privacy

A correction is a local store write only. The summaries egress-seam tests are unchanged and still pass.

Tests

  • Engine: sovereignty proof (correct → re-run producer → user text wins, including across a new machine revision), served-route e2e, degraded-target refusal; egress seam unchanged.
  • Client: driven served e2e (click the pencil, type, save → corrected prose renders and persists on re-query; refused save shows a visible error line); block-render, selection-hygiene (new .corr marker), and interaction-lint coverage.

Full gate green: contracts 114, engine 1108, client 627; schema zero-drift.

…ser revisions)

Closes #246

The summaries why-line promised "a draft you can correct" but no edit
affordance existed. This makes summary prose correctable in place, stored as
a sovereign, append-only user revision that outranks any machine revision on
read and survives re-derivation.

Contract (shared/contracts):
- Summary gains source ('model'|'user', optional — absent ⇒ model for pre-#246
  rows), a correction stamp, and a corrects link; proposal relaxed to boolean
  (a user correction is proposal:false). New SummaryCorrection shape and
  CorrectSummaryRequest payload; POST /summaries/correct route. Schemas regen'd.

Engine:
- store.correctSummary mints a source:'user' revision carrying the target's
  identity (level/window/session/children/bound), confidence 1, a correction
  stamp, and only STRUCTURAL provenance (no model-invoke fields — human prose
  is never attributed to a model). resolveSummaryHeads resolves supersession +
  sovereignty in one pass, keyed by a stable window/level identity so a later
  re-derivation cannot defeat the correction. The producer excludes user rows
  from the machine chain. A degraded target is refused. Purely local write.

Client:
- A pencil affordance on live rows (opt-in via a threaded correction context;
  degraded/empty rows get none) swaps the row to an inline editor; save posts
  the correction, the row re-renders with the corrected prose and an honest
  "edited by you" marker (user-select:none). SummaryEditSession controller
  dispatches the verbs (unioned into the interaction lint); a refused save
  surfaces a calm visible line with the raw reason on hover.

Tests: engine sovereignty proof (correct → re-run producer → user text wins,
incl. a new machine revision), served route e2e, egress seam unchanged; client
driven served e2e (click/type/save renders + persists; refused save shows
error), block + selection-hygiene + interaction-lint coverage.
A collected user correction was appended at the tail of the resolved list;
sort the result by (windowStart, createdAt, id) so a correction reads in its
window's place, matching the append-only order the SQL query establishes.
Fresh-eyes repairs on the correction slice:

- Focus/caret theft (the #225 pattern): SummaryEditSession now snapshots the
  editor focus + caret before a destructive live re-render and restores them
  after (rerenderInto), scoped to .sum-edit-text, with IME-safe wipe deferral.
  dev-entry nests it around the input block rerenderInto so both fields survive
  one wipe. Driven test asserts draft + focus + caret survive a background
  re-render (the probe that returned focused=false is now green).

- NUL bytes: the summaryCorrectionKey and correctSummary id-hash separators
  used raw 0x00 bytes; replaced with the backslash-u-0000 escape (byte-identical
  runtime, matching resolveClaimHeads). Zero raw NULs remain in the diff.

- Pad correction (unblocked by #250): renderNotetaker threads the summaryEdit
  context through renderZonePanel, so the pad center summary gets the pencil.
  A correction targets a summary identity (not session current), so the write
  path works on any session, live and past-session views alike; a read-only
  past view still forbids capture but allows correcting derived prose. Driven
  pad e2e covers the live-view open/type/save/persist flow.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make summaries correctable in place: sovereign user-corrected revisions

1 participant