Dynamic and linked fields: skip logic, dependent options, derived values (ADR-0014) - #94
Merged
Conversation
ItemGroupRef gains the standard ODM CollectionExceptionConditionOID; CodeListItem gains the edc:CollectionExceptionConditionOID vendor extension (option excluded when the condition is true, matching ODM's excluded-when-true polarity). Both round-trip through XML/JSON and the 1.3 upconvert. Validation now resolves the new refs, warns when a referenced condition or method lacks an evaluable jsonata expression (CDISC example files with XPath expressions must still import), errors on circular derivation chains, warns on mandatory derived items, and extends the blinded-reference warnings to derivations and collection exceptions.
…ules evaluateFormState() runs the full dynamic pipeline over a form's rows, occurrence-aware like runChecksOverRows (which now shares its row bucketing): derivations in dependency order, then collection exceptions (item- and group-level, group skips cascading to descendants), then skipped values nulled in the evaluation context only, then option exclusions, then edit checks. Derivations see raw values; skip conditions see derived values — the one-way order rules out skip/derive circularity. Stored values persisting in skipped fields surface as residual findings under synthetic SKIP.<condition>.<item> check OIDs; skipped derived fields converge to null through the derivation write path instead. Cyclic derivations are dropped defensively (publish-time validation already errors on them). compileEditChecks now also excludes ConditionDefs referenced as group-level or option-level collection exceptions — previously a group-level skip condition would wrongly fire as an edit check.
New services/form-state.ts is the single gate for all intake: writes to derived items, to fields skipped under current responses (clearing to null stays allowed — that is how a site resolves a residual), and to currently excluded code list options are rejected as invalid. The route write, RTSM intake, and lab import all consult it; lab imports report rejections per row under a new skipped_not_collected outcome. After every accepted write, runPostWritePipeline recomputes derivations from stored values — appended via the audit path with the new "derivation" origin and an item_value.derived action so system-computed values stay permanently distinguishable — and then reconciles queries. evaluateFormChecks is now skip-aware: checks run with not-collected fields nulled, and a stored value persisting in a skipped field raises its own system query (SKIP.<condition>.<item>), auto-closed when the value is cleared or the controlling answer changes. Amendment migration runs the same pipeline so build changes recompute derivations.
FormEntryPage now runs evaluateFormState client-side on every edit — the same pipeline the server enforces — and renders from its result: skipped fields disappear (or, when they hold a saved value, render read-only with a not-collected badge and an explicit audited Clear), derived fields render read-only with a computed badge and a live local preview that the server's authoritative recompute reconciles on refetch, and excluded code list options drop out of selects (a saved but now-excluded value stays visible and flagged). Sections whose every field is skipped and empty hide entirely. Unsaved edits in a field that becomes skipped are dropped; saved values are never touched. Open SKIP.* queries render readable text via skipResidualMessages, a new rules export enumerating every residual check OID a build can produce. resolveGroup and resolveVariantForm now carry methodOid and collectionExceptionConditionOid on resolved refs (variants inherit them from the canonical build ItemRef), and FormPreview shows static computed/conditional badges from them.
ADR-0014 records the design: the shared evaluateFormState pipeline and its fixed evaluation order, ODM CollectionExceptionCondition honored at item and group level, the edc:CollectionExceptionConditionOID CodeListItem extension for dependent options, retain-and-flag semantics for values in skipped fields, server-written derivations, backtick-scan dependency extraction with publish-time cycle errors, and the blinding warnings. The demo study now exercises all three constructs: a pregnancy-test item skipped for male subjects, a conditional "Not performed" option, and a BMI item derived from height and weight.
The guide now covers ADR-0014 end to end: study-builds.qmd documents the three build constructs (skip logic, dependent options via the edc: CodeListItem extension, derived values) and how they differ from edit checks; data-capture.qmd documents the entry-side behavior (fields hiding, retained values with not-collected badges and residual queries, narrowed option lists, read-only computed fields) with a new dynamic-fields.png screenshot; the tour points at the demo study's pregnancy-skip and computed BMI; site-forms.qmd notes that skip logic and derivations follow items into variants. scripts/screenshots.mjs gains the dynamic-fields shot (staged on DEMO-002 demographics, idempotent) so the scripted refresh reproduces it instead of the hand-captured interim image going silently stale. README's capture bullet and the E6-08 traceability row (evidence: dynamic-fields.test.ts) are extended to match.
tgerke
added a commit
that referenced
this pull request
Jul 23, 2026
Dynamic and linked fields: skip logic, dependent options, derived values (ADR-0014)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes the runtime honor the dynamic-form constructs the ODM model has carried since ADR-0003, and adds one vendor extension. Design recorded in ADR-0014; six commits, one per implementation phase.
What this adds
CollectionExceptionConditionOIDon ItemRef and (newly modeled, standard ODM) ItemGroupRef is enforced at runtime: skipped fields hide during entry, group skips cascade to nested sections, conditions evaluate per occurrence in repeating groups, and every intake path (capture route, RTSM, lab import) rejects writes into skipped fields.edc:CollectionExceptionConditionOIDvendor extension on CodeListItem, same excluded-when-true polarity as ODM's construct. Plain-ODM consumers ignore the attribute and see the full option list (round-trip and strip tests cover this).MethodDefjsonata expressions compute item values server-side after every accepted write, stored through the audit path asitem_value.derivedwith a newderivationorigin. The client shows a live read-only preview it never submits. Derivation cycles fail build validation; the runtime drops them defensively.One shared pure function (
evaluateFormStatein packages/rules) runs identically in the browser and on the server, extending the ADR-0007 dual-evaluation pattern. Evaluation order is fixed: derivations, then skip conditions, then skipped values nulled in the evaluation context only, then option exclusions, then edit checks — so derivations read raw values, skips read derived values, and skip/derive circularity can't occur.Decisions worth reviewing
SKIP.<condition>.<item>) stays open until the site clears it (audited, reason required) or the controlling answer changes back. Clearing to null is the one write still allowed into a skipped field.dynamic-fields.test.tsevidence; system-behavior text only, no new regulatory citations.Docs and demo
Guide pages (study-builds, data-capture, site-forms), tour, README, CHANGELOG, and a new
dynamic-fields.pngscreenshot (also added to the scripted refresh so it can't go stale). The demo study now exercises all three constructs: pregnancy test skipped for male subjects, a conditional "Not performed" option, and computed BMI.Verification
446 tests pass (106 odm, 26 rules, 254 api including 8 new integration tests). Browser-verified end to end: skip/unskip live, option narrowing, residual query open → audited clear → auto-close, BMI preview plus server recompute with
item_value.derivedaudit rows. Quarto site renders clean.Known follow-ups (out of scope)