Introduced native circe-types cohort editor. - #165
Open
chrisknoll wants to merge 3 commits into
Open
Conversation
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.
This PR moves towards a circe-native object model for cohort editing (and other CriteriaGroup functionality used in IncidenceRates and StrataGroups in Characterization).
Cohort Editor Migration Notes
Overview
This document describes the migration from the legacy
cohort-buildercomponentlayer to the
cohort-editorcomponents that use native circe types. The goalwas to make
StrataEditor(Characterization subgroup analyses) andIncidenceRateStratifyRuleEditorfirst-class consumers of the same criteriaediting infrastructure already used by
CohortExpressionEditor— eliminatingduplicate implementations and aligning the data model with what the WebAPI
actually produces and consumes.
Background
The Legacy Layer
The original cohort-builder folder contained a set of components that grew
alongside the early Atlas 3 prototype. The primary ones involved in criteria
group editing were:
GroupCriteriaUI.vueCriteriaEventCard.vueAttributesEditor.vueDateAdjustmentEditor.vueCardinalityEditor.vueTemporalWindowEditor.vueEventConceptSetField.vueuseCriteriaGroupPicker.tsThese components used a hand-written TypeScript interface from
src/models/cohort.types.tswith camelCase property names (logicType,events,nestedGroups). That shape did not match the PascalCase JSONproduced by circe-be / WebAPI (
Type,CriteriaList,Groups), which meantevery consumer needed to translate between the two representations.
The Cohort Editor
The newer
cohort-editorfolder was built to serveCohortExpressionEditorand defined:
circe.types.ts— Zod schemas that mirror the circe-be Java modelexactly. TypeScript types are inferred from the schemas so the type
definition and runtime validation stay in sync.
CriteriaGroup.vue— A recursive criteria group editor that accepts aCriteriaGroupobject by reference and mutates it in-place using Vuereactivity. Handles
CriteriaList,DemographicCriteriaList, and nestedGroupsrecursively.CorelatedCriteria.vue/DemographicCriteria.vue— Individual criteriarow editors wired up through the group.
criteria-editor.types.ts— Supporting types for concept set selection(
ConceptSetOption,ConceptSetSelectionTarget).What Changed
1. Unified Type System
The legacy
CriteriaGroupfromcohort.types.tswas retired in favor of thecirce-native
CriteriaGroupfromcohort-editor/circe.types.ts.Before:
After:
The PascalCase names match the Jackson serialization output of circe-be
directly. No translation layer is needed anywhere on the round-trip path.
The model types in
characterization.types.tsandincidence-rate.types.tswere updated to reference
CriteriaGroupSchemafromcirce.types.ts, givingthem Zod-backed runtime validation as a side effect.
2. Component Replacement
GroupCriteriaUIand its full dependency tree were replaced by the singleCriteriaGroup.vuecomponent from the cohort editor.The key behavioral difference is the ownership model:
Legacy model (event-driven synchronization):
GroupCriteriaUIowned a copy of the criteria group and emittedupdateevents when it changed. Callers had to listen for those events and apply the
changes back to the document. This required a synchronization layer and
created extra mutation boundaries.
Circe model (direct document mutation):
CriteriaGroupreceives a reference to the actualCriteriaGroupobject inthe document and mutates its properties directly using Vue computed setters and
array operations. The reactive document is the single source of truth. No
intermediate event path is needed for field-level changes.
This aligns directly with principles 1, 2, and 17 from
Document_Editor_HOWTO.md:
3. Concept Set Selection Redesign
The legacy
useCriteriaGroupPickercomposable used a tree-traversal approach:when a concept set was chosen, it walked the criteria tree to find the target
field by a stored path.
The new
useCirceConceptSetPickercomposable uses a direct reference approach:When the user clicks a concept set picker inside any criteria card (at any
depth),
CriteriaGroupemits theselect-concept-setevent carrying aConceptSetSelectionTarget— a directRefthat points to theCodesetIdfield on that specific criteria object. When the user picks a concept set from
the dialog, the composable writes the selected ID into
targetRef.valuedirectly. No traversal, no path tracking.
One subtle implementation note:
activeTargetis stored as a plainletvariable rather than a
ref<ConceptSetSelectionTarget>. Wrapping it inrefwould cause Vue to auto-unwrap the inner
targetRef: Ref<number|undefined>,making the nested ref inaccessible. Keeping it as a plain mutable variable
avoids that interaction while still being correct for its lifecycle (it is only
meaningful during an active selection operation).
4. StrataEditor
StrataEditor.vue(Characterization subgroup analyses) was rewritten to useCriteriaGroupfor per-stratum criteria editing. Because the criteria groupeditor is too dense for the 280px rail panel, editing opens in a full-width
dialog. The dialog pattern creates a reactive deep clone of the stratum's
criteria, which
CriteriaGroupmutates in-place. On dialog close the updatedobject is emitted as part of the updated stratum.
The composable
useCirceConceptSetPickeris instantiated once inStrataEditor, withgetConceptSets/addConceptSetcallbacks targeting astrataConceptSetsarray owned at theCharacterizationDefinitionlevel.This keeps concept sets at the expression level, separate from individual
stratum criteria, which is the same pattern used in
CohortExpressionEditor.The
hasCriteria()helper was updated to checkCriteriaList.length + DemographicCriteriaList.lengthagainst the circe shape instead of the oldevents.length.5. IncidenceRateStratifyRuleEditor
IncidenceRateStratifyRuleEditor.vuefollows a simpler pattern: it maintains alocal
reactive<CriteriaGroup>copy of the rule'sexpressionfield andpasses it directly to
CriteriaGroup. Changes are detected by awatchEffectand emitted upward as a partial
StratifyRuleupdate. Concept sets flow downas a prop from
IncidenceRateWorkbenchthroughIncidenceRateStratifyInspectorand are managed by a
useCirceConceptSetPickerinstance in the rule editor.6. File Cleanup
The migration resulted in the following deletions once the replacement
components were stable and tests were passing:
Source files removed:
cohort-builder/GroupCriteriaUI.vuecohort-editor/criteria/CriteriaGroup.vuecohort-builder/CriteriaEventCard.vueCorelatedCriteria.vue/DemographicCriteria.vuecohort-builder/AttributesEditor.vuecohort-builder/DateAdjustmentEditor.vuecohort-builder/CardinalityEditor.vueCorelatedCriteria.vuecohort-builder/TemporalWindowEditor.vueCorelatedCriteria.vuecohort-builder/TemporalFilterChip.vuecohort-builder/EventConceptSetField.vueCorelatedCriteria.vuecohort-builder/CachePreviewSelector.vuecohort-builder/PatientCountBar.vuecomposables/useCriteriaGroupPicker.tsuseCirceConceptSetPicker.tsConfigurationWarningBanner.vue— the only remaining file incohort-builder/— was moved directly to
src/components/since it is an app-level concernunrelated to cohort building. The
cohort-builder/folder was then deleted.All corresponding test files for the deleted source files were also removed.
Structural Advantages
Single Type Authority
Before the migration, the same conceptual object ("a criteria group") had two
separate TypeScript representations in the codebase. Any component or store
that touched both the cohort editor and the characterization/incidence-rate
editors had to be aware of both shapes and manage the mapping.
After the migration,
CriteriaGroupfromcirce.types.tsis the onlydefinition. It is also Zod-backed, so the same schema is used for
deserialization and runtime validation — not just for TypeScript type checking.
Faithful Round-Trip
Because the circe types use the same PascalCase field names as the WebAPI JSON
payload, a
CriteriaGroupobject can be serialized directly without anytransformation. The stratify rule expression in an Incidence Rate analysis, or
the criteria object on a Characterization stratum, travels from WebAPI → Pinia
store → component → WebAPI without any renaming or remapping.
Recursive Editor Reuse
CriteriaGroup.vueis a self-contained recursive component. Any editor thatworks with a
CriteriaGroupgets full support for nested groups,DemographicCriteria, all OMOP domain criteria types, time-window constraints,occurrence counts, and concept set selection — for free. Adding a new OMOP
domain criteria type to
CriteriaGroupautomatically becomes available inevery feature that uses it.
Alignment with Document Editor Principles
The migration brought two additional feature editors into alignment with the
document editor principles established in
Document_Editor_HOWTO.md:changes (principles 2, 11).
rather than mirrored component state (principles 1, 9, 17).
document state (criteria, concept sets) lives in the expression (principle 3).
{ Type: 'ALL', CriteriaList: [] }and arrays are created lazily byCriteriaGrouponly when the user adds an item (principle 4).File Map (Post-Migration)
Key Composable:
useCirceConceptSetPickerCallers provide two callbacks — one to read the current concept set list, one to
append a new entry — and receive the four values they need to wire up
CriteriaGroupandConceptSetSelectionDialog.Lint Configuration
The cohort-editor's design patterns trigger several ESLint rules that are
intentional rather than erroneous. Rather than scattering inline disable
comments across every component, a single
overridesblock was added to.eslintrc.cjsscoped tosrc/components/cohort-editor/**:vue/no-side-effects-in-computed-properties— The document editor patterninitializes sparse model arrays lazily inside computed getters (e.g.
innerCriteriainCorelatedCriteriaensuresprops.criteria.Criteriaexists). This is the same rationale as the global
vue/no-mutating-props: 'off'already in the config.
@typescript-eslint/no-explicit-any— The generic plumbing layer(
bindings.ts,criteria-editor-helper.ts,criteria-editor.types.ts) usesRecord<string, any>to work across arbitrary Zod-inferred types. Eachdomain criteria Vue file also uses
Record<string, any>to extract its specificsub-object from the
Criteriadiscriminated union. The Zod schema at theWebAPI boundary is the real safety layer; converting these to
unknownwouldrequire 30+ type assertions with no meaningful safety benefit.
vue/multi-word-component-names—Death,Measurement,Observation,Window, andPeriodare OMOP/circe domain names that mirror their Java modelcounterparts exactly. Renaming them would misalign with the domain model.
vuejs-accessibility/no-autofocus—autofocuson a popover orinline-edit text field that the user just opened is correct UX and matches the
WCAG dialog interaction pattern. The rule targets page-load focus hijacking,
not triggered popovers.
One additional surgical fix was made outside the folder override:
concept-set-usage.tsuses awhile (true)loop to unwrap Zod schema layers.A single
// eslint-disable-next-line no-constant-conditioncomment was addedat that line rather than suppressing the rule folder-wide.
The
vue/no-restricted-html-elementswarnings throughout the cohort-editor(direct Vuetify component usage instead of Atlas wrappers) are already
configured as
warnseverity in the base config and do not block commits.Migrating to Atlas wrappers is a separate follow-up task.
Testing Notes
After the migration, test files for all deleted components were removed.
The surviving tests for the migrated components were updated to:
CriteriaGroupfromcohort-editor/circe.typesinstead ofmodels/cohort.types.CriteriaGroup: trueas a stub instead ofGroupCriteriaUI: true.(
{ Type: 'ALL', CriteriaList: [] }instead of{ logicType: 'ALL', events: [] }).conceptSetsprop where components gained it.Tests are run with
--no-isolatefor single-file or per-folder runs. Runningtoo many folders together with
--no-isolatecan produce state pollutionartifacts; scope the run to the folders under test.