DASH-DATA-004 — comparison lanes, editable field catalogue, session semantics, and the validated save contract.
Split-view record editor
Section titled “Split-view record editor”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.
Contract
Section titled “Contract”- Producer (read): the editor mounts the existing authorized provenance server function
(
getPublicationProvenance→worker-publication-queryPOST /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 existingworker-publication-writePOST /v1/patchedit 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 plusworkspace_data:writeenforced 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.
Field catalogue (v1)
Section titled “Field catalogue (v1)”The editable set is exactly what the existing edit model projects stably (ADR-0021):
| Field | Treatment | Value validation |
|---|---|---|
title | editable | non-empty string, ≤ 2000 chars |
abstract | editable | string, ≤ 20000 chars (may be emptied) |
authors | editable | ordered array of 1–500 named contributors (each 1–500 chars) |
publication_year | editable | integer, 1400–2200 |
publication_type | compare-only | — |
cited_by_count | compare-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).
Comparison semantics
Section titled “Comparison semantics”- 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) plussource_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.
Edit-session semantics (ADR-0021)
Section titled “Edit-session semantics (ADR-0021)”- 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 with409 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.
Attribution
Section titled “Attribution”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.
Observability
Section titled “Observability”- Route metrics under
publications.field_edits(duration, workspace,service_failureerror tag) via the sharedlogRouteMetricshelper. - Comparison-load failures are captured through
captureExceptionand degrade to an explicit502 comparison unavailable; GlitchTip/Cloudflare Observability ingest both.
Limits (v1)
Section titled “Limits (v1)”- 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 and rollback
Section titled “Rollout and rollback”- 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.