Why observation history is switched on per section of a form, inherited by its fields, and how the builder exposes that.
ADR-006: Section-level history — declared on a Group, inherited by its fields
Status
Accepted (2026-09-15). Extends ADR-005. All five workstreams implemented (2026-09-15).
Context
ADR-005 made previous values and flowsheets work in every host, switched on by omf.history on
each field. That is the right unit for rendering, but the wrong unit for the decision a designer
makes. A clinician thinks "the Observations section is charted every two hours", not "heart rate,
respiratory rate, systolic, diastolic, temperature and SpO2 each show history". Today that thought
has to be expressed six times, and the builder offers no control for it at all: the only ways to set
it are a refine-chat instruction the prompts have not been taught, or editing the exported JSON.
Three properties of the platform shape the answer:
- The definition already cascades behaviour from sections to fields: a
ruleon a Group hides its children;accentColor,iconand scoring subtotals live on the Group. Designers know the pattern. - The Dictionary panel already lists every field grouped by section and writes per-field clinical
metadata by a click (terminology bindings,
PATCH /forms/:id/coding). History belongs beside the binding that makes it durable, and the panel already draws the section headers a section control would hang off. - The renderers read a field's
omf.historyoff its own UI element. A JSON Forms control does not know its ancestor Group, so inheritance cannot be resolved inside the control; it has to be resolved once, from the definition, and handed down — the shapeHistoryScope(React) andHistoryScopeService(Angular) already have.
A form-level declaration ("this form is serial") was considered. It is the right hook for a future scheduling feature, but it over-reaches for history: a form often mixes a repeated observations section with one-off sections (admission details, consent), and the form-level switch would need a section-level off-switch anyway. The section is the natural grain; a form-level default can be added later as one more level of the same cascade without changing anything decided here.
Decision
1. omf.history is allowed on a Group and inherited
OmfHistoryOptions may sit on a Group (and on the Omf* section layouts that behave as groups).
Every Control beneath it inherits the setting unless something closer overrides it:
field ▸ nearest enclosing Group ▸ outer Groups … ▸ none
- A field with its own
omf.historywins, including{ show: 'none' }to opt a single field out of a section that has history on. - Inheritance applies to fields that can carry a scalar reading (number, integer, string, boolean,
enum, date). Layout-only elements and display controls (
scoreSummary,clinicalReferenceTable,Label) never show history. countandtrendinherit withshow.unitdoes not inherit — it is a property of the measurement, set per field (usually by conversion or in the Dictionary).recordTable:historyon the table's Group applies to the fields inside each record.
2. form-core resolves the cascade once
/** Effective history config per field key (index-free data path), after inheritance. */
export function resolveHistoryConfig(definition: FormDefinitionSchemas): Map<string, OmfHistoryOptions>;
collectHistoryFields gains history?: OmfHistoryOptions (the resolved value) and both renderers'
scopes carry the map. useFieldHistory / FieldHistoryComponent look their key up in the map
instead of reading the raw element, so a section-level setting reaches every control with no change
to the controls themselves. Host behaviour is unchanged: nothing shows unless the host supplies
history, and a field-level omf.history behaves exactly as in ADR-005.
3. The builder exposes it in the Dictionary panel
The Dictionary panel becomes the place where a designer decides history, next to the bindings:
- Per section header: a "Previous values" control — Inline · Popover · Off · (inherit) — writing
omf.historyon that Group. - Per field row: the same control showing the effective value, greyed when inherited, editable to override; plus a unit box for numeric fields.
- A warning on any field whose effective history is on and whose binding is not verified, because that field's history will break on the next revision (ADR-005 §"matching rule").
Sections need a stable identity to write to. Groups have no scope, so collectCodedItems gains
sectionPointer — the JSON pointer of the nearest Group in the layout (/elements/0), the same
addressing the refine chat's patches use.
The coding endpoint generalises into a per-target metadata write:
PATCH /api/forms/:id/field-meta
{ target: { scope } | { pointer }, history?: OmfHistoryOptions | null, unit?: string | null }
Same rules as PATCH /coding: DRAFT/REVIEW versions only, audited (form.field-meta.update),
tenant-scoped. PATCH /coding stays as is.
4. The AI learns the vocabulary
The conversion prompt and the refine prompt gain history (with the Group-first guidance: "set it on
the section, not on each field"), unit, and effectiveAt. Conversion sets history on a section
when the source is plainly a serial observation sheet (a time-column grid, "q2h", "each shift",
hourly rows). Refine handles "show previous values on the Observations section" as one patch.
Consequences
Easier
- One click, or one sentence, turns history on for a whole section; a field can still opt out.
- The decision is visible and auditable in the same panel as the binding it depends on.
- Hosts and the API change nothing; the definition still carries the answer.
Harder / accepted costs
- Inheritance is a second thing to explain: the Dictionary shows the effective value and where it came from, so a greyed "inherited from Observations" row answers the question before it is asked.
- Renderers must consume the resolved map rather than the raw element. Both already have the scope object to carry it; the source-guard tests in Angular pin the parity.
sectionPointerties the Dictionary's section write to layout position. Refine-chat edits that move a Group invalidate the pointer for the duration of the edit only; the panel re-reads the definition after every write.
Implementation plan
One PR per workstream, each targeting main, packages with changesets.
- Vocabulary + core —
OmfHistoryOptionsallowed on Group (types),resolveHistoryConfig,collectHistoryFields.history,collectCodedItems.sectionPointer; tests for the cascade (section on, field off, nested groups, recordTable). - Renderers — React
HistoryScopeand AngularHistoryScopeServicecarry the resolved map;useFieldHistory/FieldHistoryComponentread it. Mounted React test: history on the Group, chip on every numeric field, none on the opted-out one. Angular source guards. - Builder — Dictionary panel section and field controls with the inherited/override state and
the unverified-binding warning;
PATCH /forms/:id/field-metawith audit; API docs. - Prompts — conversion and refine vocabulary; the pinned prompt tests.
- Docs — Form Builder vocabulary (Group row), Clinical Terminology (the panel), the Observation History integration guide (a "which fields" section), this ADR to Accepted.