Skip to content

Split-View Record Editor

DASH-DATA-004 — comparison lanes, editable field catalogue, session semantics, and the validated save contract.

The split-view record editor (DASH-DATA-004) gives an authorized curator a comparison-oriented editing surface for publications: source value, current stored value, and the pending draft side by side, with the provenance viewer’s states as the warning layer. It composes field provenance — it does not define a second vocabulary.

First-release decisions are recorded in ADR-0021 (docs/adr/0021-split-view-editor-v1-live-session-publication-catalogue.md), which answers the source packet’s open questions before implementation.

  • Producer (read): the editor mounts the existing authorized provenance server function (getPublicationProvenanceworker-publication-query POST /v1/provenance) plus nothing else; the comparison is built from that payload alone.
  • Producer (write): dashboard server function savePublicationFieldEdits (apps/dashboard/src/server/functions/publications/field-edits.ts). It is the only write path for this surface and always translates to the existing worker-publication-write POST /v1/patch edit model — storage, edit counting, projections, and audit semantics are unchanged.
  • Consumer: SplitViewEditor (apps/dashboard/src/features/publications/components/), opened opt-in from the publication edit dialog (“Compare & edit”). The existing editor remains the default during rollout.
  • Authorization: workspace resolved from AuthContext (never client input); membership plus workspace_data:write enforced server-side before any service call. Deliberately not stricter than the existing edit dialog’s effective authorization, so split-view is an alternative surface, not a new privilege tier.

The editable set is exactly what the existing edit model projects stably (ADR-0021):

FieldTreatmentValue validation
titleeditablenon-empty string, ≤ 2000 chars
abstracteditablestring, ≤ 20000 chars (may be emptied)
authorseditableordered array of 1–500 named contributors (each 1–500 chars)
publication_yeareditableinteger, 1400–2200
publication_typecompare-only
cited_by_countcompare-only

Compare-only fields are provider-owned evidence: curators see source vs current but cannot hand-edit them here. Complex stored values (e.g. localized title objects) fall back to compare-only in the split view with a pointer to the standard editor — a degraded lane, never a lossy string edit (TKT-001 fallback rule).

  • Lanes per field: source (attributed provider raw value), current (merged stored value), new (pending draft).
  • Warnings reuse the provenance states (upstream / edited / conflict / unknown_legacy / unavailable) plus source_conflict — no new comparison vocabulary.
  • A field whose draft differs from the stored value is labelled changed by this session; unchanged drafts are dropped server-side without bumping edit_count.
  • Live session only. The draft is client state for the lifetime of one editor session; there are no server-side saved drafts in v1.
  • Stale-source guard. The session captures the provenance record revision it compared against (edit_count, last_edited_at, last_fetched_at). Save re-reads the current provenance and rejects with 409 stale_session (carrying the current revision) if the record moved — concurrent edits and re-ingestion are never silently overwritten. The editor shows an explicit banner, refetches, re-bases, and lets the curator retry; the draft is preserved.
  • Cancel discards. No persistence, no server round-trip.
  • One record per session, ≤ 8 edits, one edit per field. There is no bulk mode and no freeform JSON patch channel: the input schema is a discriminated union over the field enum, so arbitrary patch paths cannot enter through this surface.

Save outcomes are attributable and derived server-side (the client never asserts its own decision):

  • submitted value equals the attributed source value → accept_source;
  • any other submitted value → manual_edit;
  • fields left out of the batch are an implicit rejection of the source value.

One publication.edited activity event is written per successful save with the actor, committed paths, new edit_count, source: "split_view_editor", and the per-field decisions. Failed saves write no audit event.

  • Route metrics under publications.field_edits (duration, workspace, service_failure error tag) via the shared logRouteMetrics helper.
  • Comparison-load failures are captured through captureException and degrade to an explicit 502 comparison unavailable; GlitchTip/Cloudflare Observability ingest both.
  • Publications only; no researcher/person records (no provenance contract to compose yet).
  • No merge flows, no arbitrary JSON editing, no server-side draft persistence.
  • Field expansion requires both a provenance catalogue field and a stable write projection.
  • Rollout: opt-in variant launched from the edit dialog; the default editor is untouched, so the blast radius is the new dialog plus the new server function.
  • Rollback: remove the “Compare & edit” entry point; the validated save contract can remain dark with no schema migration and no change to the write service.