Skip to content

feat: Add ability to use new display set split rules - #6137

Open
wayfarer3130 wants to merge 34 commits into
masterfrom
feat/customization-use-metadata-display-set
Open

wayfarer3130 wants to merge 34 commits into
masterfrom
feat/customization-use-metadata-display-set

Conversation

@wayfarer3130

@wayfarer3130 wayfarer3130 commented Jul 12, 2026 •

Copy link
Copy Markdown
Contributor

CS3D_REF: fix/display-set-split-key-stability

This PR depends on cornerstonejs/cornerstone3D#2861. CS3D #2861 must merge and release before this PR can merge. The CS3D_REF line above and the ohif-integration label tell CI to clone, build and link that branch. CI then does not install a published @cornerstonejs/* package, so you can test this PR now. The dependency goes one way only: CS3D #2861 validates against OHIF master, and not against this branch.

The University of Calgary (UCalgary) funded this work.

Context

SOP class handlers build the display sets in OHIF. The stack handler decides how a series becomes one or more display sets, and it makes that decision in hand-written code.

@cornerstonejs/metadata can now do the same work from rules that a deployment authors as data. Data rules make the same split available to two consumers: a server that builds a study index, and the viewer. Today each consumer implements the split separately, and the two implementations drift apart.

This PR adopts the metadata engine. A customization controls the engine, and that customization is off by default. The split rules are @cornerstonejs/metadata raw selector data, so a data-only customization, such as a JSONC file, can describe a rule.

Changes and results

1. useMetadataDisplaySet — the metadata engine splits the series, as an opt-in

When you enable the customization, DisplaySetService splits the instances of a series with the split-rule engine of @cornerstonejs/metadata. The service no longer uses the stack SOP class handler for these instances. Instances that no rule claims fall through to the registered handler loop without a change. The dicom-video, dicom-microscopy, cornerstone-dicom-seg, -sr, -rt, -pmap and dicom-pdf extensions therefore continue to work.

You can enable the customization in three ways:

  • per mode;
  • with the named module entry @ohif/extension-default.customizationModule.metadataDisplaySet;
  • from the URL, with ?customization=split/enableNewSplit.

extensions/default/src/displaySetSplitting/ holds three items:

  • the OHIF rule set;
  • the factory that converts an instance group into an OHIF ImageSet;
  • stackSopClassUids.ts.

stackSopClassUids.ts makes one list serve two purposes: the registration list of the stack handler, and the ownership test of the split rules. The two purposes cannot disagree about which instances belong to the stack path. getSopClassHandlerModule.js loses 227 lines to the shared factory.

Three rules diverge from the upstream defaults on purpose. Each file records the reason:

Rule Why this rule is not the upstream rule
singleImageModality This rule splits per instance. The upstream rule uses a coarse size bucket, and that bucket merges mammography views of the same resolution (RCC/LCC/RMLO/LMLO). The rule is off by default (priority null), see test case 4.
multiFrame This rule drops the SliceLocation requirement of the upstream rule. That requirement collapses ultrasound clips into one stack.
defaultImageRule The isImageInstance gate of the upstream rule is narrower than the SOP class list of the stack handler.

The rules are keyed by rule id, and each rule has a priority. splitRules is an object, and not an array. The key is the rule id. The engine tries the rules in ascending priority, and the first rule that matches wins. A priority of null turns a rule off.

The OHIF defaults use the priorities 1 to 5:

Priority Rule
1 singleImageModality
2 multiFrame
3 mixedDimensionalityBValue
4 volume3d
5 defaultImageRule

A rule with a priority below 0 runs before every default rule. A rule with a priority above 10000 runs after every default rule, and sees only the instances that no default rule claims.

An earlier revision used an array, and a customization added a rule with $unshift. An $unshift of a rule with an id that is already present gave two rules with one id, and the engine then threw for every series. A key cannot occur twice, so a customization now replaces, moves or turns off a rule by its id:

// Add a rule that runs before every default rule
useMetadataDisplaySet: { splitRules: { $merge: { myRule: { ...myRule, priority: -1 } } } }

// Move a rule
useMetadataDisplaySet: { splitRules: { volume3d: { priority: { $set: 0 } } } }

// Turn a rule off
useMetadataDisplaySet: { splitRules: { volume3d: { priority: { $set: null } } } }

An entry with a missing or non-numeric priority is a rule error. §3 describes what the system does with a rule error.

Existing display sets only grow. New instances of a series can arrive after the first split, and the rules can change during a session. In both cases, an instance that already has a display set stays in it. DisplaySetService never deletes a split-rule display set, never removes an instance from one, and keeps its displaySetInstanceUID, so the viewport state survives. The service places only the instances that are new to the series:

  1. into the existing display set that holds the other instances of their group, through the extendInstances hook of that display set;
  2. else into the existing display set that has the splitKey of their group;
  3. else into a new display set of their own.

The result can differ from a split of the complete series from the start, and that is intended. For example, an ultrasound series with a runBy rule arrives as img1 and img3 (single images) and clip4 (a clip). The first split gives [img1, img3] and [clip4]. Then clip2 arrives. A split from the start gives [img1] [clip2] [img3] [clip4]. The service gives [img1, img3] [clip2] [clip4]: the two existing display sets do not change, and clip2 gets a new display set.

A hanging protocol can find the display sets of a group of rules. Each display set records splitRuleId (the rule that made it) and splitGroupId. splitGroupId is the groupId of the rule, else the rule id, so it equals splitRuleId unless a deployment groups rules. Several rules can make one kind of display set. For example, mammography can arrive as breast tomosynthesis, as legacy mammography with all its views in one series, and as mammography that the modality already split. Each form needs its own rule, and all three rules can have "groupId": "mammo". A hanging protocol then matches splitGroupId equal to mammo. The group id does not change the split: groups and split keys stay per rule id. A rule's customAttributes cannot change splitRuleId, splitGroupId or splitKey.

2. The typed metadata cache holds the display sets

displaySetStore puts the display sets in the DISPLAY_SET module of @cornerstonejs/metadata. DisplaySetService.getDisplaySetCache() is deprecated, and it returns a read-only snapshot. The migration guide is platform/docs/docs/migration-guide/3p13-to-3p14/display-set-store.md.

3. Split rules are @cornerstonejs/metadata raw selector data

A JSONC URL module is data, and a JSON app config is data. A split rule must therefore be data too. OHIF has no rule language of its own. A split rule in the customization is a rule of the @cornerstonejs/metadata raw selector: the same safe-function vocabulary that the upstream default rules use. createDisplaySetSplitRules compiles the rules. A server that builds a study index compiles the same data, so the server and the viewer split a series in the same way.

{
  "requires": ["split/enableNewSplit"],
  "global": {
    "useMetadataDisplaySet": {
      "splitRules": {
        "$merge": { "ctScout": {
          "priority": -1,
          "viewportTypes": ["stack"],
          "series": [
            { "name": "hasScout", "scope": "mixed", "when": { "attribute": "ImageType", "contains": "LOCALIZER" } }
          ],
          "matches": {
            "all": [
              { "attribute": "Modality", "equals": "CT" },
              { "seriesFact": "hasScout" },
              { "attribute": "ImageType", "contains": "LOCALIZER" }
            ]
          },
          "groupBy": ["SeriesInstanceUID"],
          "customAttributes": {
            "set": { "label": "SCOUT" },
            "fromFirstInstance": { "SeriesDescription": { "expression": "`SCOUT ${SeriesDescription}`" } }
          }
        } }
      }
    }
  }
}

The rule first evaluates the series: hasScout is true when the series mixes localizer images and other images. The rule then matches each instance with a simple test: is this image a localizer? A series of localizers only, and a series without a localizer, stay whole.

A second example module, split/dwiByBValue.jsonc, holds the dwiByBValue rule of the specification: one display set for each b-value of a diffusion MR series. "How to test" gives the URLs for both modules.

Where the structural form is not sufficient, a condition or a value is an expression: { "expression": "Modality === 'CT' && Rows > 256" }. The expression language is compileExpression in @cornerstonejs/metadata. No eval and no new Function exist on the path from the data to the executed code.

Code supplies what data cannot express, as a named classifier. The OHIF default rules are raw data too. The only OHIF test that data cannot express is the SOP class list of the stack handler. @ohif/extension-default supplies that test as the stackImage classifier, in useMetadataDisplaySet.classifiers, and a rule references it as { "classifier": "stackImage" }. A mode that is written in TypeScript can also supply a function at any place in a rule. The compiler uses the function as it is.

The compiler is strict (CS3D #2861). The table splitRuleSchema in @cornerstonejs/metadata defines each field of a rule: the forms that the field accepts, and the arguments that the compiled function gets. An unknown key, or a form that the field does not accept, is a compile error that names the rule, the path and the allowed keys. Before, a typo such as matchs compiled, and the rule then claimed every instance. A comparator expression reads only a, b and context.

An earlier revision of this PR added a $function customization marker, a function-signature registry (registerFunctionSignatures), and a customizationFunctionPolicy.denyAttributes policy. Those three items made a second rule language in OHIF, beside the raw selector. This PR no longer contains them. CustomizationService is the same as on master.

An expression that names an attribute that the instance does not carry evaluates to undefined. This behaviour makes the sparse DICOM tags usable (DiffusionBValue != undefined). The same behaviour lets Modallity === 'CT' compile correctly and then match nothing. The compiler does not validate the identifiers against a list of known attributes, and that is a decision. A naturalized instance carries private tags, vendor additions, and per-frame data that the naturalizer folds in. No dictionary lists all of these attributes. collectIdentifiers in @cornerstonejs/metadata reports the attributes that an expression reads. Use collectIdentifiers to find a misspelled name.

A rule error stops the display, and names the rule. A rule can be critical for the clinician. A rule that the system drops gives a grouping that looks correct, but that is not the grouping that the deployment intended. So the system does not drop a rule, and it does not give the series to the SOP class handlers instead:

  • A rule that does not compile. compileSplitRules compiles each rule alone and returns { rules, errors }, so it can name every rule that fails. An unknown key, an unknown classifier, an invalid expression, or a missing or non-numeric priority makes a rule fail. While there is an error, the display set service creates no display sets for any study, not even SEG or SR display sets. This is the same as other errors that prevent the load of a study, because one rule set applies to every study.
  • A rule that fails at run time. A valid raw rule cannot fail at run time. A function that code supplies can, for example a classifier. compileSplitRules wraps each function of each rule, so the error is a SplitRuleRunError that names the rule and the field (matches, groupBy[1], runBy, customAttributes, …). The service then creates no further display sets for that study. The display sets that exist stay, and other studies continue.
  • In both cases, the service shows one error notification that stays on screen until the user closes it. The block clears when the splitRules value changes, and on mode exit.
  • The host sortInstances and compareInstances of the customization are not wrapped. An error in one of them also stops the study, but the error names no rule.

The specification records this behaviour as SP-SAFE-3, SP-SAFE-7, SP-PIPE-3, SP-PIPE-9, SP-PIPE-13 and SP-PIPE-14.

The limit of the current vocabulary. A series fact is true or false. A scanner that does not set LOCALIZER in ImageType gives the scout rule nothing to detect. A rule that finds such a scout needs a series fact that is a number, for example the lowest InstanceNumber. That fact needs a change in @cornerstonejs/metadata, and this PR does not contain it.

4. The display set keeps the instance order that the rule declares

The compareInstances of a rule had no effect on the display set. The split engine ordered the instances of each group with compareInstances. Then makeImageSetDisplaySet called imageSet.sort(customizationService). imageSet.sort ignores the order of the list that it receives, and sorts that list again from the start. The engine therefore computed the order of the rule, and OHIF discarded that order at once. Nothing wrote a warning. No default rule declares a comparator, so no test found the defect.

Two orders met at this point, and only one order could survive. The two orders now combine, and the engine defines the precedence:

  • the default order of OHIF is the base order;
  • the comparator of the rule overrides the base order at each pair where the comparator has an opinion;
  • a comparator that returns 0 declines to have an opinion. The comparator does not state that the two instances are equal. The base order therefore holds for each pair that the comparator does not decide.

The changes:

  • ImageSet.sortInstances(images, customizationService) is the body of sort(), and it applies to a list that the caller supplies. sort() now calls sortInstances. I extracted the body, and I did not write a second implementation on the split-rule side, so the default order of OHIF keeps one definition. The hook must sort a whole list, and a comparator cannot replace the hook. sortImagesByPatientPosition selects a reference instance — the middle instance, to avoid a scout — and projects the other instances onto the normal of that reference instance. No (a, b) function expresses that operation.
  • The split-rule factory orders the instances one time, with orderInstancesForRule(images, matchedRule, { sortInstances: <the OHIF sort>, compareInstances, series }). The factory does this for the first build and in extendInstances. The factory applies the order after it applies the image-list attributes, because the base order reads isReconstructable. For that reason the factory supplies the base sort, and the engine does not.
  • The factory also applies the comparator of the host (compareInstances of the customization). The engine ordered the group with that comparator, and a re-sort without it discarded that order.
  • The factory uses the series facts that the split computed, from InstanceGroup.series. A rule computes its facts from the whole series. Facts that the factory computes again from one display set can differ: a "this series mixes b-values" fact is true for the series, and false for each half after the split. extendInstances receives the facts of the new split when the new instances matched the rule of the display set. Otherwise the display set keeps its earlier facts.
  • makeImageSetDisplaySet accepts a skipSort option, and the split-rule path sets that option. The legacy SOP class handler path does not change. That path has no rule to read, so the default order of OHIF is the complete answer, and makeImageSetDisplaySet still applies it there.
  • The useMetadataDisplaySet customization accepts an optional sortInstances and an optional compareInstances, and DisplaySetService sends both to the engine. These two options change the order in which the engine walks the runs, and therefore change the display sets that a runBy rule produces. The two options do not change the final frame order. When you supply neither option, the engine uses acquisition order, as before.

No default rule declares a comparator, so no shipped behaviour changes.

Text composition from attributes is intended

A rule can build the label or the SeriesDescription of a display set from any attribute that the instance carries. That capability is the purpose of the templates. A rule that renames a split is a main reason to write a rule, for example SCOUT ${SeriesDescription}, or b=0 and b=1000.

The capability has a consequence, and you must know the consequence before you write rules. A rule decides the text in the study browser and in the viewport overlays, and a viewer template does not decide that text. The instance that the rule reads carries the patient identifiers beside the acquisition tags. Treat a split-rule set as content that a reviewer reads, at the same level as the overlay configuration.

The viewer does not load a customization from the URL until customizationUrlPrefixes names a prefix. That prefix must have the same write controls as any other deployed configuration.

displaySetSplitting.md records this section.

Dependency and release setup

@ohif/core, extensions/cornerstone and extensions/default depend on @cornerstonejs/metadata. Three defects stopped the release of that dependency:

  • extensions/default pinned metadata at 5.6.8. The rest of the repository used 5.8.2. The pin is an exact peer dependency, so the two pins conflict for a user who installs @ohif/extension-default beside @ohif/core. Both now use 5.8.2.
  • .scripts/cs3d-set-version.mjs did not list metadata. A CS3D version bump therefore moved the other eight packages, and left metadata at the old version. One install then holds two CS3D builds, and the mismatch appears as a missing export, and not as a version error. The script now lists metadata.
  • The same script found no workspace packages. The script read the workspaces field of the root package.json. That field left the repository when the repository moved to pnpm. workspaceGlobs was therefore [], and the script rewrote only the root package.json and reported success. The script now reads pnpm-workspace.yaml, and falls back to the workspaces field. The script exits with an error when it finds no globs. I verified the fix: the script found 31 package files, where it found 1 file before, and it updated 28 pins.

Work to do before merge: all three metadata pins must move to the CS3D release that contains CS3D #2861. The pins are now at 5.10.3, after a merge from master, and they stay there on purpose. A pin to an unpublished version breaks pnpm install for every user of this branch, and CI resolves the dependency through the CS3D_REF link path.

The published 5.10.3 does not contain what this PR needs. It does not export createDisplaySetSplitRules, orderInstancesForRule or resolveSplitRuleSet, and its split functions take an array of rules. Without the link to CS3D #2861, every split-rule display set fails.

The split is off by default, so the default configuration is not affected.

CI change in this PR: CircleCI and Netlify read CS3D_REF

This PR includes the commit 7ae88a5889, "chore(ci): apply the CS3D_REF of the pull request in CircleCI and Netlify". The commit is not about split rules. It is in this PR for these reasons:

  • Before the commit, only .github/workflows/playwright.yml read the CS3D_REF line. The CircleCI UNIT_TESTS job, the CircleCI Cypress job and the Netlify deploy preview always installed the pinned @cornerstonejs/* 5.10.3.
  • This PR is the first OHIF PR with unit tests that need an unreleased CS3D change. On the previous commit, 19 jest tests in 2 suites and 12 Cypress tests failed. The same code passed 214 Playwright tests against the linked CS3D branch.
  • A CI change of this type must run on a PR that shows the problem. A separate PR from master has no CS3D_REF dependency, so its CI shows nothing. That PR would need dummy code to test the change.

What the commit does:

  • .scripts/cs3d-read-ref.mjs reads the PR body with the rules of the gate job in playwright.yml. The gate keeps its own inline copy, because the gate runs before any code from the PR.
  • .scripts/cs3d-read-ref.test.mjs runs the real gate script from the workflow file against 32 PR bodies. The test fails when the two copies do not agree.
  • .scripts/ci/cs3d-apply-ref.sh runs after the ordinary install. With no line, the script changes nothing. With a version, the script changes the pins and installs again. With a branch, the script clones, builds and links the branch, as the Playwright job does.
  • .circleci/config.yml runs the script in UNIT_TESTS and in the Cypress job. netlify.toml runs the script before build:ci.
  • When the GitHub API does not answer, the job gives a warning and uses the pinned version. A read-only GITHUB_TOKEN in the CircleCI and Netlify settings prevents the unauthenticated rate limit.

A second CI commit, 845eef5300, adds libs/** to the ignores of eslint.config.mjs. The React Compiler lint budget lints ., so the CS3D clone in libs/@cornerstonejs counted against the OHIF budget: 99 errors and 106 warnings, against a budget of 96 errors and 94 warnings. A local CS3D checkout had the same effect.

The two commits change only CI files, eslint.config.mjs and cs3d-integration.md. The maintainers can move the two commits into a separate PR before the merge.

With the two commits, the CircleCI jest tests, the CircleCI Cypress tests and the Netlify deploy preview pass against the linked CS3D branch. Two checks still fail, and both failures are expected:

  • CS3D Branch Merge Guard. The CS3D_REF line names a branch, and the guard reports each branch ref on purpose.
  • BUILD_PACKAGES_QUICK, the security audit. The audit runs only when pnpm-lock.yaml changes. This PR changes the lock file, and master already has high and critical advisories, for example brace-expansion, react-router and protobufjs.

How to test

The ohif-integration label and the CS3D_REF line at the top of this description tell .github/workflows/playwright.yml to clone fix/display-set-split-key-stability. The workflow builds that branch with pnpm run build:esm, and links the branch into node_modules before the OHIF install.

Test on the deployed build

Two deploys build this PR against the linked CS3D branch. You can use either deploy:

  • The Netlify deploy preview: https://deploy-preview-6137--ohif-dev.netlify.app. The deploy/netlify check builds the deploy preview. The deploy preview links the CS3D branch because of the CI change in this PR (see the section above).
  • The CS3D preview deploy: https://cs3d-pr-6137--ohif-platform-docs.netlify.app. The Playwright workflow builds this deploy, and deploys it only after the Playwright tests pass. The workflow uses the alias cs3d-pr-6137, so the URL stays the same for each new commit.
  • Both deploys use config/netlify.js. That config sets customizationUrlPrefixes, so the ?customization= URLs below work.

1. The b-value series that the viewer rendered as 4D. The study 1.3.6.1.4.1.14519.5.2.1.4792.2001.921758700577562664959693695481 has the MR series DTI_high_iso SENSE (2380 instances). 2310 instances have DiffusionBValue = 800, and 70 instances have no DiffusionBValue. The 4D split of CS3D groups frames by DiffusionBValue, so the series becomes a 4D candidate.

2. One display set for each b-value, as a rule that a deployment writes in JSONC. The new module split/dwiByBValue.jsonc holds the dwiByBValue rule, which the specification traces. The rule has priority −1, so it runs before every default rule. The rule puts each DiffusionBValue of a diffusion MR series into its own stack display set, with the description <SeriesDescription> b=<value>. Frames without a b-value fall through to mixedDimensionalityBValue, and become one more display set.

3. The CT scout rule. ?customization=split/scoutSeries puts the localizer images of a CT series that also has other images into a separate display set with the label SCOUT. Add the parameter to the URL of any study with such a CT series.

4. One display set for each radiograph, as a rule. The new split turns off the default rule singleImageModality (priority null). With the new split on, a CR, DX or MG series is one display set. The stack SOP class handler always makes one display set for each image of these three modalities. A per-image split is a decision for each deployment, and a rule states that decision better than a fixed modality list. The rule stays in the defaults, so { singleImageModality: { priority: { $set: 1 } } } turns it on again.

The new module split/dxCrSingleImages.jsonc makes one display set for each image of a DX or CR series with fewer than 10 images. MG is not in the list, and a series with 10 images or more stays one display set. The series fact tenOrMoreImages has minInstances: 10, and matches requires { "not": { "seriesFact": "tenOrMoreImages" } }.

Test on your own machine

pnpm cs3d:checkout fix/display-set-split-key-stability
pnpm cs3d:install && pnpm cs3d:build && pnpm cs3d:link
pnpm test:unit

The unit tests in platform/core/src/services and extensions/default/src (38 suites, 374 tests) cover the compilation of the raw rules, the priorities, the rule errors and the notification, the display sets that only grow, the series facts, and the host comparator. extensions/default/src/displaySetSplitting/urlSplitModules.test.ts adds 1 suite and 3 tests. The test compiles each split/*.jsonc module over the OHIF default rules, because no build step checks those data files. The test also checks the split of dwiByBValue.

Then run pnpm dev, and add the same ?customization= parameters to a viewer URL. config/dev.js sets customizationUrlPrefixes. The default config does not set that property.

To see the instance order of a rule take effect, add a compareInstances to the scout rule: { "attribute": "InstanceNumber", "number": true, "descending": true }. The frames of the scout display set then come back in the reverse order. Before the order fix in this PR, the same rule changed nothing.

Not in this PR

Numeric series facts. A raw series fact is true or false, so a rule cannot yet compare an instance with a value that the whole series gives, such as the lowest InstanceNumber (§3). That fact needs a change in @cornerstonejs/metadata.

The customAttributes of a rule can still overwrite display set fields that are not reserved. RESERVED_ATTRIBUTES in makeDisplaySetFromInstanceGroup protects the identity and the content of the display set: images, instances, uid, displaySetInstanceUID, splitKey and the extendInstances hook. A rule therefore cannot redirect the pixel requests. The data source derives imageIds from images, and it does not store imageIds. RESERVED_ATTRIBUTES does not list StudyInstanceUID, SeriesInstanceUID or SOPClassHandlerId.

An earlier version of this description said that the gap matters because SeriesInstanceUID selects the series that receives a saved report. That statement was wrong. storeMeasurements builds the report from the measurement data, and from the explicit destination that the dialog supplies (predecessorImageId, SeriesNumber, SeriesDescription). No write path reads an attribute of a display set to select its target. The real consequences of an overwrite of one of these three fields are:

  • a display set match fails, for example a SEG does not find the series that it references;
  • an exported CSV holds wrong reference text.

A fix belongs in RESERVED_ATTRIBUTES, because the gap applies to a literal custom attribute and to a computed custom attribute equally. The gap does not leak data, and it does not misroute a request.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@netlify

netlify Bot commented Jul 12, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for ohif-dev ready!

Name Link
🔨 Latest commit 8a6f8c4
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/6abd99750eed690008fa0ce6
😎 Deploy Preview https://deploy-preview-6137--ohif-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds opt-in metadata-driven display-set splitting with safe $function expressions, ordered OHIF rules, incremental reconciliation, metadata-backed storage, legacy fallback handling, customization presets, tests, and documentation.

Changes

Metadata display-set splitting

Layer / File(s) Summary
Safe expression runtime
platform/core/src/services/CustomizationService/expression/*, platform/core/src/services/CustomizationService/CustomizationService.ts, platform/core/src/services/CustomizationService/CustomizationService.function.test.ts
Adds tokenization, parsing, CSP-safe compilation, aggregate/helper evaluation, $function resolution, memoization, error handling, and tests.
Declarative and default split rules
platform/core/src/services/DisplaySetService/normalizeSplitRules.ts, extensions/default/src/displaySetSplitting/ohifDefaultSplitRules.ts, extensions/default/src/displaySetSplitting/ohifDefaultSplitRules.test.ts
Normalizes declarative rules and adds ordered image, multiframe, diffusion, volume, and specialized-instance handling.
ImageSet factories and handler integration
extensions/default/src/displaySetSplitting/*, extensions/default/src/getSopClassHandlerModule.js, platform/core/src/types/DisplaySet.ts
Centralizes ImageSet construction and creates split display sets with rule attributes, dynamic-volume metadata, and incremental merging.
DisplaySetService orchestration and storage
platform/core/src/services/DisplaySetService/*, platform/core/src/types/DisplaySet.ts
Integrates grouping, reconciliation, stale-set removal, legacy fallback, and typed metadata-backed storage.
Customization entrypoints and presets
extensions/default/package.json, extensions/default/src/customizations/*, extensions/default/src/getCustomizationModule.tsx, platform/app/public/customizations/*
Registers the default customization, adds launch parameters, and provides metadata-splitting and CT SCOUT presets.
Display-set splitting documentation
platform/docs/docs/platform/services/customization-service/displaySetSplitting.md
Documents enablement, rule structure, expressions, precedence, examples, and overrides.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DisplaySetService
  participant CustomizationService
  participant SplitRulesEngine
  participant DisplaySetFactory
  participant displaySetStore
  DisplaySetService->>CustomizationService: read useMetadataDisplaySet
  DisplaySetService->>SplitRulesEngine: group instances by splitRules
  SplitRulesEngine-->>DisplaySetService: matched groups and unmatched instances
  DisplaySetService->>DisplaySetFactory: createDisplaySetFromGroup
  DisplaySetFactory->>displaySetStore: store split display set
  DisplaySetService->>DisplaySetService: route unmatched instances to SOP handlers
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the primary change: adding support for new display set split rules. It uses the expected feat prefix and is concise.
Description check ✅ Passed The description provides detailed context, implementation changes, results, dependency information, testing instructions, and documentation of known limitations. The repository checklist is not explic…
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/customization-use-metadata-display-set

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
extensions/default/src/getSopClassHandlerModule.js (1)

15-15: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Remove the unused second argument from makeDisplaySet calls

makeDisplaySet only forwards instances and appContext to makeImageSetDisplaySet, so instanceIndex / displaySets.length are dead arguments here. Remove them from the three call sites to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extensions/default/src/getSopClassHandlerModule.js` at line 15, Update the
three call sites of makeDisplaySet to pass only the required instances argument,
removing the unused instanceIndex and displaySets.length arguments while
preserving the existing makeDisplaySet implementation.
platform/core/src/types/DisplaySet.ts (1)

93-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a type annotation for the displaySetService parameter.

The displaySetService parameter on updateInstances has no type annotation, making it implicitly any. Importing DisplaySetService directly would create a circular dependency (the services layer imports from types/), so a minimal interface would preserve type safety without the architectural concern.

♻️ Suggested interface to avoid circular dependency
 export type DisplaySet = {
   displaySetInstanceUID: string;
   instances: InstanceMetadata[];
   isReconstructable?: boolean;
   StudyInstanceUID: string;
   SeriesInstanceUID?: string;
   SeriesNumber?: number;
   SeriesDescription?: string;
   numImages?: number;
   unsupported?: boolean;
   Modality?: string;
   imageIds?: string[];
   images?: unknown[];
   label?: string;
   /** Flag indicating if this is an overlay display set (e.g., SEG, RTSTRUCT) */
   isOverlayDisplaySet?: boolean;
   /** Flag indicating this is a derived dataset */
   isDerived?: boolean;
   /** flag indicating if it supports window level */
   supportsWindowLevel?: boolean;

   // Details about how to display:
   /**
    *  A URL that can be used to display the thumbnail.  Typically a data url
    * This can be set to null to avoid trying to display a thumbnail, eg for
    * display sets without a thumbnail.
    */
   thumbnailSrc?: string;
   /** A fetch method to get the thumbnail */
   getThumbnailSrc?(imageId?: string): Promise<string>;

   /** An opaque type of this viewport, used internally to specify which viewport to use */
   viewportType;

   /**
    * A fetch URL to display the content.  This is used for content such as
    * pdf display.
    */
   renderedUrl?: string;

   /**
    * The instance UID of the display set that this display set references.
    * This is used to determine if the display set is a referenced display set.
    * It usually is for SEG, RTSTRUCT, etc.
    */
   referencedDisplaySetInstanceUID?: string;

   /**
    * The FrameOfReferenceUID shared by every frame within this display set.
    * It will be undefined if the frames do not all share the same Frame of Reference.
    */
   FrameOfReferenceUID?: string;

   SeriesDate?: string;
   SeriesTime?: string;
   instance?: InstanceMetadata;

   /**
    * The predecessor image id refers to the SOP instance that is currently loaded
    * into this display set for SEG/SR/RTSTRUCT type values.  The name is chosen
    * for consistency when this value is used as the origin instance
    * for saving a new instance intended to replace this instance where the
    * new instance has a "predecessor sequence".
    */
   predecessorImageId?: string;

   /**
    * isLoaded is used for display sets containing a load operation that
    * is required before the display set can be shown.  This is separate from
    * isHydrated, which means it is loaded into view.
    */
   isLoaded?: boolean;
   isHydrated?: boolean;
   isRehydratable?: boolean;

   /**
    * The name of the comparison function (for sort) to use when comparing display
    * sets that are coming from same series instanceUID.
    */
   compareSameSeries?: string;

+  /**
+   * Minimal interface for the DisplaySetService methods that
+   * `updateInstances` needs, avoiding a circular import from
+   * `types/` into the services layer.
+   */
   /**
    * The deterministic, rule-namespaced group key assigned by the
    * `@cornerstonejs/metadata` split-rules engine when this display set was
    * created via the `useMetadataDisplaySet` customization.  Used to reconcile
    * re-splits of the same series with already-created display sets.
    */
   splitKey?: string;

   /** The id of the split rule that created this display set, when applicable. */
   splitRuleId?: string;

   /**
    * Incremental-merge hook for split-rule display sets.  Intentionally named
    * differently from `addInstances` (the SOP-class-handler merge hook) so the
    * legacy handler loop never feeds unmatched instances into split-rule
    * display sets.  Returns the updated display set, or undefined when the
    * display set cannot merge the instances.
    */
-  updateInstances?(instances: InstanceMetadata[], displaySetService): DisplaySet | undefined;
+  updateInstances?(
+    instances: InstanceMetadata[],
+    displaySetService: DisplaySetServiceLike
+  ): DisplaySet | undefined;
 };
+
+/**
+ * Minimal interface for the DisplaySetService methods that `updateInstances`
+ * callers need, avoiding a circular import from `types/` into the services layer.
+ */
+export interface DisplaySetServiceLike {
+  setDisplaySetMetadataInvalidated(displaySetInstanceUID: string): void;
+  getDisplaySetsForSeries(seriesInstanceUID: string): DisplaySet[];
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform/core/src/types/DisplaySet.ts` around lines 93 - 112, Update the
DisplaySet.updateInstances signature to replace the implicit-any
displaySetService parameter with a minimal local interface describing the
service members this hook uses. Define or reuse that interface within the types
layer rather than importing DisplaySetService, preserving type safety without
introducing a circular dependency.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@extensions/default/src/displaySetSplitting/makeImageSetDisplaySet.ts`:
- Around line 36-42: Update the volumeLoaderUtility lookup in
makeImageSetDisplaySet to check whether getModuleEntry returns undefined before
accessing exports. If the utility is unavailable, throw a clear descriptive
error; otherwise preserve the existing getDynamicVolumeInfo extraction and
invocation.

In
`@platform/core/src/services/CustomizationService/expression/expression.test.ts`:
- Around line 137-149: Rename the test around compileExpression to describe
graceful null property access in templates rather than runtime errors, warnings,
or an undefined result. Keep the existing `${a.b.c}` assertion and setup
unchanged.

---

Nitpick comments:
In `@extensions/default/src/getSopClassHandlerModule.js`:
- Line 15: Update the three call sites of makeDisplaySet to pass only the
required instances argument, removing the unused instanceIndex and
displaySets.length arguments while preserving the existing makeDisplaySet
implementation.

In `@platform/core/src/types/DisplaySet.ts`:
- Around line 93-112: Update the DisplaySet.updateInstances signature to replace
the implicit-any displaySetService parameter with a minimal local interface
describing the service members this hook uses. Define or reuse that interface
within the types layer rather than importing DisplaySetService, preserving type
safety without introducing a circular dependency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f0e9f3f4-e8d2-47ba-94e0-8102288831c9

📥 Commits

Reviewing files that changed from the base of the PR and between 110b293 and 14cfb7b.

📒 Files selected for processing (25)
  • extensions/default/package.json
  • extensions/default/src/customizations/metadataDisplaySetCustomization.ts
  • extensions/default/src/displaySetSplitting/makeDisplaySetFromInstanceGroup.ts
  • extensions/default/src/displaySetSplitting/makeImageSetDisplaySet.ts
  • extensions/default/src/displaySetSplitting/ohifDefaultSplitRules.test.ts
  • extensions/default/src/displaySetSplitting/ohifDefaultSplitRules.ts
  • extensions/default/src/getCustomizationModule.tsx
  • extensions/default/src/getSopClassHandlerModule.js
  • platform/app/public/customizations/index.html
  • platform/app/public/customizations/split/enableNewSplit.jsonc
  • platform/app/public/customizations/split/scoutSeries.jsonc
  • platform/core/src/services/CustomizationService/CustomizationService.function.test.ts
  • platform/core/src/services/CustomizationService/CustomizationService.ts
  • platform/core/src/services/CustomizationService/expression/compiler.ts
  • platform/core/src/services/CustomizationService/expression/expression.test.ts
  • platform/core/src/services/CustomizationService/expression/index.ts
  • platform/core/src/services/CustomizationService/expression/parser.ts
  • platform/core/src/services/CustomizationService/expression/tokenizer.ts
  • platform/core/src/services/DisplaySetService/DisplaySetService.test.ts
  • platform/core/src/services/DisplaySetService/DisplaySetService.ts
  • platform/core/src/services/DisplaySetService/displaySetStore.test.ts
  • platform/core/src/services/DisplaySetService/displaySetStore.ts
  • platform/core/src/services/DisplaySetService/normalizeSplitRules.ts
  • platform/core/src/types/DisplaySet.ts
  • platform/docs/docs/platform/services/customization-service/displaySetSplitting.md

Comment thread platform/core/src/services/CustomizationService/expression/expression.test.ts Outdated
@cypress

cypress Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Viewers    Run #6852

Run Properties:  status check passed Passed #6852  •  git commit 8a6f8c4b09: fix(display-sets): sort a grown display set with its current reconstructability
Project Viewers
Branch Review feat/customization-use-metadata-display-set
Run status status check passed Passed #6852
Run duration 02m 00s
Commit git commit 8a6f8c4b09: fix(display-sets): sort a grown display set with its current reconstructability
Committer Bill Wallace
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 28
View all changes introduced in this branch ↗︎

@wayfarer3130 wayfarer3130 changed the title feat: Add ability to use new display set split rules [WIP] feat: Add ability to use new display set split rules Sep 2, 2026
…use-metadata-display-set

# Conflicts:
#	extensions/default/src/getCustomizationModule.tsx
#	platform/docs/docs/migration-guide/3p13-to-3p14/index.md
wayfarer3130 and others added 4 commits September 8, 2026 13:15
…etadata

The safe function expression language was written on this branch and then ported
to `@cornerstonejs/metadata`, where it belongs: it exists to express display set
split rules, both sides of the wire have to compile the same rules, and a
viewer-only copy cannot serve a server building a study index. Until now both
copies existed, semantically identical bar formatting — two copies of one
sandbox, so a hardening fix would land in one and silently miss the other.

Deletes `CustomizationService/expression/` (~750 lines plus its 22-test suite,
which came across with the port) and imports `compileExpression` from the
package. Its only two non-test consumers were `$function` and one convenience
line in `normalizeSplitRules`; nothing in any extension or mode used it.

Also gates `$function` with a policy, read from
`appConfig.customizationFunctionPolicy` and — like `customizationUrlPrefixes` —
never from a customization, since a customization able to define the policy
could lift its own restrictions.

`denyAttributes` lists attribute paths where a marker is refused, as dotted
patterns (`*` = one segment, trailing `**` = any depth). Array indices are not
path segments, so a pattern describes the shape of a customization rather than a
position in a list and survives a rule list being reordered. Nothing is denied
by default; `['**']` disables `$function` entirely.

A deny list rather than an allow list: the set of attributes a rule may
legitimately compute is not knowable in advance — `customAttributes` keys are
chosen by the rule's author — so an allow list would refuse working
configurations by default, a worse failure than the one it prevents.

An earlier draft of this work justified an allow list by claiming a computed
`customAttributes.SeriesInstanceUID` could redirect where measurements are
saved. That is not so: `storeMeasurements` builds its report from the
measurement data plus the dialog's explicit destination, and no write path reads
a display set attribute for its target.

Documents two things that are deliberate rather than oversights: composing text
from any attribute is the point of template literals in a rule (and so a rule
set is content a reviewer reads, since it decides what the study browser says),
and an expression naming an attribute the instance lacks resolves to `undefined`
rather than being validated against a dictionary — a naturalized instance
carries private and vendor attributes no dictionary enumerates, so validating
would reject expressions that work.

Records in `ohifDefaultSplitRules` that OHIF's hand-written rules are
transitional and why they are not being converted to raw selector form yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three things stopped this branch's new dependency from being releasable.

`extensions/default` pinned `@cornerstonejs/metadata` at 5.6.8 while `@OHIF/core`
and `extensions/cornerstone` were at 5.8.2. As an exact-pinned peer dependency
that is a peer conflict for anyone installing `@ohif/extension-default` beside
`@OHIF/core`. Aligned at 5.8.2.

`cs3d-set-version.mjs` did not list `metadata`, so a bump would have moved the
other eight packages and left it behind — two CS3D builds in one install, which
surfaces as a missing export rather than a version error.

That script also updated nothing but the root `package.json`. It read the root
`workspaces` field, which went away when the repo moved to pnpm, so
`workspaceGlobs` was `[]` and it reported success having changed no pin. It now
reads `pnpm-workspace.yaml` (falling back to the `workspaces` field) and exits
non-zero rather than performing a silent no-op. Verified: 31 package files
discovered where it previously found 1, 28 pins updated.

Its closing advice still told the reader to run `bun install
--config=./bunfig.update-lockfile.toml`; replaced with the pnpm command the
"version" path in playwright.yml actually uses.

The three `metadata` pins still have to move to the CS3D release that carries
the safe functions. They are left at 5.8.2 deliberately — bumping to an
unpublished version would break `pnpm install` on this branch, and CI resolves
it through the CS3D_REF link path meanwhile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A rule's `compareInstances` had no effect on the resulting display set. The
split engine ordered each group by it, then `makeImageSetDisplaySet` called
`imageSet.sort(customizationService)`, which ignores the incoming order and
re-sorts from scratch — so the rule's order was computed and then thrown away.
Nothing warned, and because no default rule declares a comparator, no test
exercised it.

Two orderings met there and only one could win. Now they compose, with the
precedence the engine defines: OHIF's default order is the base, the rule's
comparator overrides it where it has an opinion, and a comparator returning 0
leaves the base alone.

- `ImageSet.sortInstances(images, customizationService)` is `sort()`'s body
  applied to a supplied list. Extracted rather than reimplemented on the
  split-rule side so OHIF's default order has one definition — and it has to be
  a whole-list sort, because `sortImagesByPatientPosition` picks a reference
  instance (the middle one, to avoid a scout) and projects onto its normal,
  which no pairwise comparator expresses. `sort()` delegates to it.
- The split-rule factory orders once, through
  `orderInstancesForRule(images, matchedRule, { sortInstances: <OHIF's> })`, on
  both the initial build and the incremental merge. It runs after the image-list
  attributes because the base order reads `isReconstructable`, which is why the
  base is supplied here rather than to the engine.
- `makeImageSetDisplaySet` takes `skipSort`, set by the split-rule path. The
  legacy SOP class handler path is unchanged: it has no rule to consult, so
  OHIF's default order is the whole answer and is still applied there.
- The `useMetadataDisplaySet` customization gains optional `sortInstances` /
  `compareInstances`, forwarded to the engine. These change the order the engine
  walks runs in — and so which display sets a `runBy` rule produces — rather
  than the final frame order; unset, the engine's acquisition order applies as
  before.

No default rule declares a comparator, so no shipped behaviour changes.

Requires the ordering hooks added in cornerstonejs/cornerstone3D#2861.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`$function` compiled with the params the *data* declared. So a marker written
`params: ['a', 'b']` at a site the consumer invokes as `(instance, context)`
compiled cleanly and then computed nonsense, with nothing anywhere to warn
about it — the data author is the wrong party to state a calling convention it
cannot see.

`customizationService.registerFunctionSignatures({ '<path pattern>': params })`
moves that declaration to the code that calls the closure. Paths use the same
dotted patterns as `denyAttributes` (`*` for one segment, trailing `**` for any
depth) and the most specific match wins, so a `series.*` convention can be
overridden for one named fact. A marker whose own `params` disagree with the
registered signature is compiled with the registered one and warns; a marker
that spells the same signature out is accepted silently. With nothing registered
the previous behaviour stands: the default `['instance', 'context']`, and a
marker's own `params` honoured.

Deliberately a method rather than a customization or an app-config value.
Signatures are a property of the code doing the calling, and a customization
able to declare them could hand itself a different convention — the same reason
`denyAttributes` is app-config-only. Registering also clears the transformed
cache, since a signature changes how an already-resolved marker compiles.

`@ohif/extension-default` registers the split-rule signatures, which is what
makes an instance-ordering comparator declarable as data:

    { "compareInstances": { "$function": "a.SliceLocation - b.SliceLocation" } }

Both instances are in scope because the caller said they would be, not because
the rule guessed. Returning 0 declines to have an opinion, so OHIF's default
order carries whatever the comparator does not decide.

Note the safety of an expression was already settled before this: it is parsed
against a closed vocabulary with a fixed helper whitelist, once, at
customization-read time. What was missing was never safety but agreement about
the calling convention, which is what this adds.

Documents the one footgun it does not fix: bare identifiers still resolve
against the first argument, so in a comparator `SliceLocation` silently means
`a.SliceLocation`. Suppressing the implicit scope for comparator-shaped
signatures needs a compiler option upstream.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add the display set splitting specification (prefix SP), with user
requirements (SP-FIX, SP-DESC, SP-READ, SP-DET, SP-SAFE, SP-REUSE) and
implementation requirements (SP-FORM, SP-PIPE, SP-EXPR, SP-DEPLOY, and the
proposed SP-GEN and SP-SRV). It includes mermaid diagrams of the rule
pipeline, the assistant route, and reuse by a server.

Move the $function expression language out of the display set splitting
page into its own functionExpressions page. The splitting page keeps only
what is specific to split rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ctions for split rules

OHIF had a second rule language on top of the metadata compiler: the
$function customization marker, its function-signature registry, the
customizationFunctionPolicy deny list, and the object-form series and
customAttributes maps of normalizeSplitRules. This removes all of it.

- Split rules in the useMetadataDisplaySet customization are
  @cornerstonejs/metadata raw selector data, compiled by
  createDisplaySetSplitRules. compileSplitRules compiles each entry on its
  own and drops a rejected entry with a warning. A rule that code has already
  compiled passes through.
- The OHIF default rules are raw data. The only code is the stackImage
  classifier, which the customization supplies as `classifiers`.
- CustomizationService and its index are back to the master versions;
  functionPolicy.ts and the $function tests are deleted.
- The SCOUT example first evaluates the series (does it mix localizer and
  other images?), and then matches each localizer instance.
- The docs describe split rules as raw selector data, and
  functionExpressions.md describes { expression } in split rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rewrite the display set splitting specification for the
@cornerstonejs/metadata raw selector form, which is now the one rule
format. Remove the descriptions of the OHIF $function marker, the
signature registry and denyAttributes, which commit 1f2911c removed.

Record the decisions on the open questions:
- A rule with an error, or a rule that fails at run time, stops display
  set creation and shows an error that names the rule (SP-SAFE-3,
  SP-SAFE-7, SP-PIPE-9, SP-PIPE-13). The code still drops or falls back;
  the specification records each gap.
- One display set from several series (SP-FIX-7) and a split of the
  frames of one multiframe instance (SP-FIX-8) are deferred.
- The agent skill (SP-GEN-1) is a separate change; SP-SAFE-6 is a
  future item; the register entry waits for the register branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and name the rule

Implements SP-SAFE-3, SP-SAFE-7, SP-PIPE-3, SP-PIPE-9 and SP-PIPE-13 of the
display set splitting specification.

- compileSplitRules compiles each rule alone and returns { rules, errors }.
  It no longer drops a rule that does not compile.
- While the rule set has an error, makeDisplaySetForInstances creates no
  display sets for any study, SEG and SR included, as other errors that
  prevent a study load do. The block clears when the splitRules value
  changes, and on mode exit.
- compileSplitRules wraps every function of every rule, so an error at run
  time is a SplitRuleRunError that names the rule and the field (matches,
  groupBy[1], runBy, series, compareInstances, customAttributes).
- An error from the engine, createDisplaySetFromGroup or extendInstances
  stops further display sets for that study. The display sets that exist
  stay, and other studies continue.
- Each failure shows one error notification through uiNotificationService
  that stays until the user closes it.
- The spec records the global scope of a rule set error, the strict rule
  schema (SP-FORM-7, SP-FORM-8, SP-EXPR-5, SP-EXPR-6), the wrap of rule
  functions (SP-PIPE-14), and the current state of SP-PIPE-9 and SP-PIPE-13.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The display set factory records splitGroupId: the groupId of the matched
  rule, else the rule id, so it equals splitRuleId unless a deployment groups
  rules. A hanging protocol can match splitGroupId to find the display sets of
  several related rules, for example breast tomosynthesis, legacy mammography,
  and mammography already split, all with groupId "mammo".
- customAttributes can no longer overwrite splitRuleId or splitGroupId. The
  reconciliation compares splitRuleId with the matched rule, so a changed
  splitRuleId changed the sort of a display set that grows (SP-PIPE-11).
- The spec adds the user requirement SP-READ-6 and SP-PIPE-15, and closes the
  SP-PIPE-11 gap. The docs describe groups of rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e the split URL modules in a test

split/dwiByBValue.jsonc is the rule that the display set splitting
specification traces: one stack display set for each DiffusionBValue of a
diffusion MR series. Frames without a b-value fall through to the default
mixedDimensionalityBValue rule.

The split/*.jsonc modules are data that no build step checked, so a module
with an unknown key failed only when a user loaded it. The new test compiles
each module over the OHIF default rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lify

Before, only the Playwright workflow read the CS3D_REF line. The CircleCI
unit test and Cypress jobs, and the Netlify deploy preview, always used the
pinned @cornerstonejs/* versions, so a PR that needs an unreleased CS3D
change failed there even when Playwright passed against the branch.

- .scripts/cs3d-read-ref.mjs reads the PR body with the rules of the
  Playwright gate job. The gate keeps its copy inline, because it runs
  before any PR code.
- .scripts/cs3d-read-ref.test.mjs runs the real gate script from the
  workflow file against the same bodies, and fails when the two disagree.
- .scripts/ci/cs3d-apply-ref.sh applies the ref after the ordinary install:
  nothing, a version rewrite, or a clone, build and link of the branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wayfarer3130 and others added 2 commits September 30, 2026 11:26
The lint budget lints `.`, and eslint.config.mjs did not ignore libs/. When
a CS3D_REF line names a branch, cs3d-apply-ref.sh clones and builds CS3D into
libs/@cornerstonejs, so the CS3D files counted against the OHIF budget
(99 errors and 106 warnings, against 96 and 94). A local CS3D checkout had the
same effect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aySetRules example

The module embeds the unchanged mammoViewSplit rule of the cornerstone3D
displaySetRules example. The rule splits a mammogram that holds all four
views in one series into one display set for each laterality and view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3D displaySetRules example"

This reverts commit 2deea76.
@wayfarer3130 wayfarer3130 changed the title [WIP] feat: Add ability to use new display set split rules feat: Add ability to use new display set split rules Sep 30, 2026
…gleImages split module

With the new split on, the default rule singleImageModality is off (priority
null), so a CR, DX or MG series is one display set. A per-image split is a
decision for each deployment, and a rule states it better than a fixed
modality list. The rule stays in the defaults, so one $set turns it on again.

split/dxCrSingleImages.jsonc shows the split as a rule: one display set for
each image of a DX or CR series of fewer than 10 images. A series fact with
minInstances: 10, under `not`, gives the size test. MG is not in the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…use-metadata-display-set

# Conflicts:
#	pnpm-lock.yaml
wayfarer3130 and others added 3 commits September 30, 2026 15:26
`compileSplitRules` did not compile a rule when any behaviour field was a
function, so a JSON field next to a function stayed uncompiled. A function
`matches` with a `groupBy` entry `{ attribute: 'DiffusionBValue', number:
true }` put b=0 and b=800 in one display set, with no compile error, and a
JSON `runBy` next to a function failed at run time.

`createDisplaySetSplitRules` keeps a function at a function place as is, so
every rule now goes through that compiler. A `__proto__` rule id is now a
compile error: `rules['__proto__'] = rule` replaced the prototype of the
rule set, and the rule was lost with no error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uctability

The growth hook added the instances and sorted them before it computed
`isReconstructable` again. OHIF's default sort reads `isReconstructable` to
choose patient-position or instance-number order, so a display set that
grew from one slice sorted by instance number, and a fresh load sorted by
patient position.

The initial build and the growth hook now compute the image-list
attributes, sort, and then compute the attributes that depend on the order
(`instance`, `messages`, the thumbnail) again. The spec adds SP-PIPE-16.

`RESERVED_ATTRIBUTES` also holds `__proto__`, because `setAttributes`
assigns each key and a `__proto__` key replaced the prototype of the
ImageSet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch had an error being deployed

1 failed and 1 inactive (outdated) deployments
unrestricted — 8a6f8c4b Deployed Sep 30, 2026 by wayfarer3130 via playwright-tests (24.15.0) #5143
fork-pr-approval — efa1f5ab Deployed Sep 8, 2026 by wayfarer3130 via playwright-tests (24.15.0) #4982
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ohif-integration Causes a CS3D/OHIF integraiton build to be run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant