**Title:** `QuestionnaireForm`: Use `Questionnaire.url` for the response questionnaire when available

**Type:** bug / enhancement

**Description**
When the `Questionnaire` resource passed to `QuestionnaireForm` includes a `url` property (the canonical URL, per FHIR), the generated `QuestionnaireResponse` should use that URL as the `questionnaire` identifier rather than a resource-level reference like `Questionnaire/<id>`.

Currently the URL is ignored; the response always stores the resource reference. This deviates from FHIR's expectation that a canonical URL is the preferred stable identifier for a questionnaire, and it breaks interoperability when downstream consumers interpret the questionnaire field strictly as a canonical URL.

**Expected behaviour**
- If the questionnaire has a `url`, the response's `questionnaire` field must be set to that URL.
- If the questionnaire does **not** have a `url`, fall back to the reference string (e.g., `Questionnaire/<id>`)—the same behaviour as today.
- This must apply to both the initial “in-progress” response that is built when the form first renders **and** the “completed” response produced on submission.

**Notes**
- No change should be required for questionnaires that already lack a `url`; they should keep the current reference-string behaviour.
- The fix should respect the existing `subject`, `source`, and `authored` logic on the final response; only the questionnaire identifier selection logic is in scope.

Use Questionniare.url, fallback to Questionnaire reference (#6075)


Title: Reindex job searches not respecting statement_timeout setting

Description:
When running reindex jobs on large tables where most resources have already been reindexed, the job attempts to set a long `statement_timeout` (e.g., 1 hour) to allow expensive scans. However, the actual search queries that scan for resources still appear to hit a short default timeout (often 60 seconds), causing the reindex to fail with a query timeout error.

Expected behavior:
- The extended `statement_timeout` (1 hour) must apply to the search queries that scan the table for resources needing reindex.
- After the long-running search completes, the `statement_timeout` for the database session must be restored to the default value appropriate for the environment. The restored default must respect the existing database configuration: if connection configuration is disabled (`disableConnectionConfiguration`), use `'DEFAULT'`; otherwise, use the configured `queryTimeout`, falling back to 60 seconds if not set.
- The reindex worker version should be bumped to prevent any still-running old workers from processing jobs after the fix is deployed, since the change modifies how database interactions are handled during reindexing.

Acceptance criteria:
- Reindex jobs on large tables that have mostly been reindexed can complete without running into a query timeout.
- Following the long-running search, subsequent database operations within the same context use the standard timeout, not the extended one.
- The worker version is incremented so that only workers with the fix handle reindex jobs.

Correctly set statement_timeout in ReindexJob (#6413)


Add a server configuration option `rateLimitsEnabled` (boolean, default `true`) that, when set to `false`, completely disables all rate limiting:

- The HTTP request rate limiter (both standard and auth endpoints) must not enforce any limits.
- FHIR quota enforcement must be turned off—requests must not be rejected for exceeding FHIR quotas.
- Resource cap accounting and enforcement must be disabled.

The local development config (`medplum.config.json`) should set `rateLimitsEnabled` to `false` so that local servers are unthrottled by default. The environment variable `MEDPLUM_RATE_LIMITS_ENABLED` (parsed as a boolean) must also allow overriding this value.

When `rateLimitsEnabled` is `false`, it must take precedence over any other rate-limiting settings (e.g., `defaultRateLimit`, `defaultFhirQuota`, and project-level resource caps). When `true` (the default), all existing rate-limiting behavior must remain unchanged.

Server config setting to disable all rate limits (#9590)


**Title:** `PatientSummary` should fetch real appointment/encounter counts and replace manual consumer links; `PatientPage` must sync its active tab with navigation.

---

### Problem
Today `PatientSummary` renders two hard‑coded anchor links (href `#`) with the placeholder text `"No upcoming appointments"` and `"No documented visits"`.
Consumers that want real links must bypass this by passing a `topContent` prop and constructing their own navigation elements. In the provider example (`PatientPage`), the sidebar builds `View Appointments` / `View Documented Visits` links and passes them via `topContent`, but the active tab bar does not reflect the URL when the user clicks those links; it only synchronizes on initial load.

### Desired behavior

#### 1. `PatientSummary` becomes self‑contained
* **Data‑aware links** – For the given patient, `PatientSummary` should fetch:
  * **Upcoming appointments**: resources with status `proposed`, `pending`, or `booked` and `date >= today`.
  * **Documented visits (encounters)**: resources with status `finished` and `date <= today`.
* **Count display** – Show the count with correct pluralization:
  * `"No upcoming appointments"` / `"No documented visits"`
  * `"1 upcoming appointment"` / `"1 documented visit"`
  * `"X upcoming appointments"` / `"X documented visits"` (X > 1)
* **Configurable navigation** – Each link is rendered only when a corresponding URL prop is supplied:
  * A new `appointmentsUrl` prop controls the appointment link.
  * A new `encountersUrl` prop controls the encounter link.
  * If a prop is `undefined`, the link should not appear at all.
* **Remove `topContent`** – The old `topContent` prop is superseded by this built‑in link logic and should be removed from the component.

#### 2. `PatientPage` consumer update
* Pass the correct tab URLs to `PatientSummary` using the new `appointmentsUrl` and `encountersUrl` props (derived from the existing tab‑URL helpers).
* Remove the `topContent` memo and the manual `Anchor`/`Link` elements.
* **Tab synchronization** – The active tab indicator must track the current URL even when navigation happens outside the tab bar (e.g., clicking the links inside `PatientSummary`). After such navigation the appointments or encounters tab should become highlighted.

### Acceptance criteria
1. When no `appointmentsUrl` is provided, the appointment link does not appear. Same for `encountersUrl`.
2. When a URL is provided, the link appears and shows the real count based on data fetched from the server.
3. Counting and pluralization work correctly for 0, 1, and many items.
4. In the example `PatientPage`:
   * The sidebar summary shows the real upcoming appointment count linked to the appointments tab, and the real documented visit count linked to the encounter tab.
   * Clicking either link navigates to the correct tab and the tab bar highlights the corresponding tab.

Patient summary appointments and encounters links (#4524)


**Feature Request: Defer claim persistence to export/submit, and base claim-card visibility on CPT charge items**

## Summary
In the Medplum provider app, when a provider opens an encounter’s Billing tab, claim FHIR resources are being created and updated automatically as soon as a charge item is added or the billing practitioner is changed. This leads to premature, incomplete, or incorrect claim records on the server. Claims should only be persisted when the provider explicitly chooses to export or submit the claim. Additionally, the “Export Claim” card should appear whenever there are billable (CPT) charge items, not only after a claim has already been saved.

## Acceptance criteria

1. **No automatic claim creation/updates**
   - Adding a charge item, changing the billing practitioner, or loading conditions must not create or update any Claim resource on the server.
   - The existing debounced auto-save of encounter and practitioner data must still work (those updates should still be saved), but claim mutations must not be triggered by those state changes.

2. **Claim created/updated only on export or submit**
   - When the user clicks “Export Claim” and selects a format (e.g., CMS 1500), the system must:
      - Build a draft Claim from the current encounter state (patient, practitioner, charge items, selected coverage, conditions).
      - If no active/draft Claim exists for this encounter, create one; if one already exists, update it with the latest state.
      - Then perform the export operation using the persisted Claim.
   - The same behavior applies to “Submit Claim” (including Candid submit): claim persistence happens right before the submission request, not before.

3. **Export Claim card visibility**
   - The “Export Claim” card (with the export and submit controls) must be displayed whenever there is at least one charge item with a CPT coding, regardless of whether a Claim resource has been persisted.
   - If there are no CPT charge items, the card must not be rendered.

4. **Coverage handling**
   - Coverage selection and the creation of self-pay coverage (when no coverage exists) must continue to work during the export/submit flow just as they did before. Coverage should not be persisted as part of any automatic claim creation, but should be applied when the claim is built at export/submit time.

5. **Claim response card (Candid)**
   - After a claim is persisted (either for the first time via export/submit, or from a prior export), the UI should still be able to load and display any associated ClaimResponse (e.g., Candid status). The component should continue to fetch the latest Claim when appropriate so that the claim-response card works correctly.

6. **No duplicate claims**
   - If a claim for the encounter already exists at the time of export/submit, the system must update that existing claim rather than creating a duplicate. The export/submit operation must use the updated claim’s ID.

7. **Backward compatibility of export/submit flows**
   - The existing CMS-1500 export, EDI X12 placeholder, NUCC Crosswalk CSV placeholder, and Candid submit workflows must all function identically except for the timing of claim persistence.

## Additional context
The Billing tab currently receives claim and charge-item state from its parent, and the claim is saved reactively via a `syncClaim` mechanism. This causes the claim to be persisted as soon as the user adds a charge item or changes the practitioner. The goal is to remove that eager persistence and instead defer claim creation/update to the explicit export and submit actions, while keeping the UI responsive and stateless for display purposes.

fix(provider): Create/update claim during export or submit (#9366)


**Title:** Filter self‑pay coverage from encounter coverage eligibility check

**Background:**
When a provider opens the encounter coverage eligibility modal for a patient, the modal currently lists *all* active coverages, including those marked as self‑pay (type `SELFPAY`). Self‑pay coverages cannot be used for eligibility verification, so presenting them as options is misleading and can cause unnecessary UI noise or downstream errors if a user attempts to run an eligibility check against one.

**Expected behavior:**
1. **Filtering** – Active coverages whose type includes the `SELFPAY` code should be excluded from the coverage list shown in the eligibility modal.
2. **Empty state** – After removing self‑pay coverages, if no other active coverages remain, the modal must display the existing “No active coverage found for this patient.” empty state (instead of showing no options or a broken state).
3. **First‑coverage selection** – After filtering, the modal should automatically select the first remaining eligible coverage (if any), matching the original intent.

**Non‑obvious edge cases:**
- A patient who has only a self‑pay coverage (and no other active coverages) must trigger the empty state, not a leftover self‑pay entry or a blank selector.
- The filtering should not affect non‑self‑pay coverages or alter the logic for loading/displaying benefit details for the selected coverage.
- Do **not** change the network query to the server (the `Coverage` search call remains the same); filter client‑side after the results arrive.

fix(provider): Filter selfpay coverage from visit eligibility check (#9261)


```
Title: GraphQL mutations do not return proper GraphQL errors for invalid resourceType or mismatched IDs

When using the GraphQL endpoint (`/$graphql`) to perform FHIR mutations, if the `resourceType` in the `res` argument does not match the mutation name (e.g., `PatientCreate` with `resourceType: "ServiceRequest"`), or if the `id` inside `res` does not match the explicit `id` argument on a `*Update` mutation, the server response lacks a standard GraphQL `errors` entry.

Observed behavior:
- The `errors` array in the GraphQL response is empty or missing.
- The `data` for the affected mutation is `null`, but no explanation is given to the client.
- This makes it very difficult to debug invalid mutation inputs programmatically.

Expected behavior:
- The GraphQL response should include a proper `error` object in the `errors` array.
- The error should contain:
  - A clear `message` (e.g., "Invalid resourceType" for resource type mismatches, or "Invalid ID" when the provided IDs do not match in an update).
  - `locations` indicating the line/column in the GraphQL query where the mutation was called.
  - A `path` that identifies the mutation field (e.g., `["PatientCreate"]` or `["PatientUpdate"]`).
- The `data` field for that mutation should be set to `null`.

This should apply to all auto-generated create and update mutations (e.g., `PatientCreate`, `PatientUpdate`, `ObservationCreate`, etc.) and cover both the resource type mismatch and the ID mismatch scenarios.

The likely root cause is that the mutation resolvers currently handle these invalid cases by returning a value (likely an internal error outcome) rather than throwing an error. The GraphQL execution engine only populates the `errors` array when a resolver throws a recoverable error. The fix should make the resolvers throw an appropriate error for these validation failures so that the standard GraphQL error formatting activates.

Please also ensure the error messages are descriptive and consistent (e.g., "Invalid resourceType", "Invalid ID") and that the fix works for all FHIR resource types supported by the schema.
```

Fixes #3905 - better graphql mutation error reporting (#4308)


**Title:** Token search matching does not handle system-only and code-only search values for identifier and CodeableConcept parameters

**Background**
The FHIR token search specification (see “Token Search” in the HL7 FHIR search page) defines three forms for a token parameter value:

1. `[system]|[code]` – matches resources where both system and code/value are equal.
2. `[system]|` – matches **any** resource that contains an element whose system equals `[system]`, regardless of the code/value.
3. `|[code]` – matches **only** resources that have an element with no system and whose code/value equals `[code]`.

Currently the search matching logic for token-type search parameters in `@medplum/core` only handles the first form correctly. Searching with a system-only value (e.g. `http://loinc.org|`) or a code-only value (e.g. `|12345-6`) yields incorrect results (typically no matches when there should be matches, or matches when there should not be).

**Acceptance Criteria**

- For **identifier** search filters (applied against resources’ `identifier` list), a filter value that consists of only a system (e.g. `"http://example.com|"`) must match any resource that has at least one identifier with that system, regardless of the identifier’s value.
- For **identifier** filters, a value that consists of only a code/value (e.g. `"|12345"`) must match **only** resources that have an identifier with **no** system and whose value equals the provided code.
- For **CodeableConcept** search filters (e.g. the `code` parameter on Observation), a system-only value must match any resource that has at least one `coding` with that system.
- For **CodeableConcept** filters, a code-only value must match resources that have at least one `coding` with **no** system and whose code equals the provided code.
- The existing `system|code` behavior must remain unchanged (both parts must match).
- A value that is exactly `"|"` (empty system and empty code) should be treated as no match for both identifier and CodeableConcept filters.
- Matching must be case‑insensitive, consistent with the existing implementation.

**Relevant files**
The token matching logic for identifier and CodeableConcept filters lives in `packages/core/src/search/match.ts`. Look for the helper functions that evaluate a single resource value (an `Identifier` or a `CodeableConcept`) against a token filter string; those are the spots that need updating.

Improve matching on identifier and codeablceconcept token searches (#5148)


```
Feature Request: Example Data for Scheduling Demo App

The scheduling demo app (medplum-scheduling-demo) currently allows users to upload core ValueSets and deploy example bots via the app’s upload menu. We need a third upload option that populates the app with realistic, dynamic scheduling data so users can immediately explore the full appointment lifecycle without manual data entry.

Acceptance Criteria

1. Upload Option
- The app’s navigation menu (under "Upload") gains a third entry labelled “Upload Example Data”.
- Selecting this entry triggers a data loading process.

2. Data Generation
- The data is produced by a new bot that is automatically uploaded and deployed when the user clicks the existing “Upload Example Bots” button.
- The bot dynamically generates data relative to the current date, spanning the previous week, the current week, and the next week, so the dataset remains meaningful regardless of when it is executed.
- The bot must use the current practitioner’s schedule (already created in the app) as a reference.

3. Generated Resources
- Patients: Homer Simpson and Marge Simpson (two active patient resources with basic demographics).
- Slots:
  - Every Monday and Wednesday across the three weeks, free 1-hour slots from 9:00 to 17:00, excluding a lunch break at 12:00.
  - Every Friday across the three weeks, a single all-day (9:00–17:00) busy-unavailable slot.
  - Slots used by appointments (see below) are set to “busy”. If an appointment is cancelled, an extra free slot remains (since the slot was freed).
- Appointments (all booked with the same practitioner):
  - Past week, Monday 9:00–10:00: a cancelled routine appointment (with cancellation reason “Provider”). The slot becomes free.
  - Current week, Monday 10:00–11:00: a fulfilled routine appointment. An Encounter with status “finished” is auto-generated and linked to this appointment.
  - Current week, Wednesday 15:00–16:00: a booked emergency appointment.
  - Next week, Monday 11:00–12:00: a booked follow-up appointment with a comment “Followup appointment to assess the exam results”.
  - Next week, Wednesday 15:00–16:00: a booked routine checkup appointment with a comment “Routine checkup after the emergency room visit”.
- All appointments include appropriate coding for appointment type and service type (routine, follow-up, emergency; consultation, emergency room admission).

4. Upload Execution
- When “Upload Example Data” is clicked, the app executes the bot to get a batch bundle and submits that bundle in one operation.
- On success, a notification with a success message and icon is shown after a short delay.
- If any entry fails, an error is thrown and displayed appropriately.

5. Bot Deployment Integrity
- The existing logic that verifies whether all required bots have been deployed must be updated to include the new example-data bot. Thus, after clicking “Upload Example Bots”, the app should only consider the bots successfully deployed if this new bot is present alongside the four existing ones.

6. UI Enhancement (Appointment Details)
- In the appointment detail view, the list of related Encounters should support navigation: clicking on an Encounter row (left or middle/aux-click) navigates to that Encounter’s detail page (regular click: in-app navigation; aux-click: open in a new tab).

7. Documentation
- The demo app’s README should mention the new optional step (“Upload Example Data”) in the Getting Started section, and describe the purpose and structure of the example data, including an overview of the UI components and the roles of the `src` and `data` directories.
```

[Scheduling Sample] Example Data Bundle (#5156)


**Feature Request: Exclude "abstract" (notSelectable) concepts from ValueSet expansion**

Currently, the `ValueSet/$expand` endpoint returns all codes defined in the value set’s compose includes. Some code systems mark certain concepts as "abstract" — not intended for direct use in data entry — using the FHIR concept property `http://hl7.org/fhir/concept-properties#notSelectable`. For example, `_PersonalRelationshipRoleType` in the `v3-RoleCode` code system is abstract but still appears in expansions.

We need the expansion operation to support an optional boolean parameter `excludeNotForUI`. When `excludeNotForUI=true`:

- The expansion must omit any concept that has the `notSelectable` property set to `true` in the underlying code system.
- If the code system does not define that property at all, the parameter should have no effect (no filtering occurs).
- The filtering must work in combination with other supported parameters (`count`, `offset`, `filter`, and `is-a` filters in the value set definition).

Additionally, the operation currently reads some parameters directly from the query string, which does not align with the standard FHIR operation definition. Please ensure the endpoint uses the same operation parameter parsing and output handling as other operations in the codebase, so that the new `excludeNotForUI` boolean parameter is properly recognized and validated according to the `ValueSet` `$expand` operation definition. The existing error messages for invalid parameters (e.g., invalid `filter`) should remain meaningful.

The intended outcome is that a call like:
`GET /fhir/R4/ValueSet/$expand?url=<encoded-valuset>&count=200&excludeNotForUI=true`
returns an expansion where abstract codes (like `_PersonalRelationshipRoleType`) are absent, while an expansion without the flag still includes them.

Enable filtering out abstract codes from ValueSet expansion (#4032)


Add scroll-to-top behavior on every route change.

Currently, when navigating between pages in the Medplum web app, the scroll position from the previous page is preserved. A new page starting mid-way down the content is confusing. The viewport should scroll back to (0,0) after any internal route change.

Acceptance criteria:
- After clicking any link that changes the route (sidebar, in-content, etc.), window scroll is reset to top.
- Applies to all routes, not just the initial page load.
- Does not break routing or back/forward navigation.
- Any reusable scroll-to-top mechanism should be available in the component library so custom apps built with Medplum UI components can also adopt it easily.

Scroll to top of viewport on route change (#9358)


**Title:** Cache rate-limited users in-process to avoid Redis calls during repeated blocking

**Request:**
When a user exceeds the FHIR interaction rate limit (e.g., too many requests), every subsequent request from that user still queries Redis to check the limiter, even while the user remains blocked. This adds unnecessary load on Redis under sustained blocked traffic.

Add an in-process short-circuit inside the rate limiter:
- After a request is blocked (429 response), remember the user is blocked locally in the server process, including the rate-limit reset time.
- Before hitting Redis on the next request, check this local cache. If the cached block exists and the reset time has not yet passed, immediately reject the request with 429 *without* calling Redis.
- Once the reset time is reached, clear the cached entry and allow the next request through to Redis normally.
- Use a bounded in-memory cache (e.g., an LRU cache with a fixed capacity such as 1000 entries) to prevent unbounded memory growth from distinct blocked users.
- The cache should be shared across all requests in the same process (global to the rate limiter module), not per-request.

Edge cases / safety constraints:
- The rate limit state (the remaining quota) must still be correctly reflected on the response when serving from cache (same headers as a fresh Redis block).
- The cached block must expire at the correct `msBeforeNext` time provided by the Redis limiter result.
- Do not cache “allowed” states—only cache a block *after* Redis reports it, and only short-circuit when that cached block is still active.
- The check must be safe for both synchronous and the async (sleep-and-retry) flow: when `async` mode is enabled, blocked requests that are discovered in the cache should behave the same as if Redis reported the block (i.e., sleep then retry or throw 429 based on `async` setting).

The existing rate-limiter is backed by Redis (using `rate-limiter-flexible`). The `@medplum/core` package already exports an `LRUCache` utility.

Store heavily rate limited users in memory (#8374)


**Title**: Emails with display name in `from` are rejected even if the email part is approved

**Description**:

When sending an email through the Medplum server (`sendEmail` from the email service) with a `from` address that includes a display name (e.g. `"Display Name <no-reply@example.com>"`), the server logs a warning that the from address is not an approved sender and does not send the email. This happens even when the bare email address portion (`no-reply@example.com`) is present in the comma-separated `approvedSenderEmails` server configuration list.

**Expected behavior**:
- The approved sender check should extract and validate only the email address portion of the `from` string.
  - If the `from` contains a display name in angle brackets (e.g. `"Name <email>"`), the address inside `<>` should be compared against `approvedSenderEmails`.
  - If the `from` has no angle brackets, it is already a bare email address and should be compared as-is.
- The full `from` string (display name + angle-bracketed email) must still be forwarded to the email provider unchanged, so display names appear correctly in recipients' mail clients.
- Rejection and warning logging must continue for any `from` address whose extracted email is not included in the approved list.
- Existing behavior for bare email addresses (no display name) and for missing/invalid `from` fields must remain unchanged.

Handle display string format for approved sender (#6937)


The CMS 1500 PDF export currently renders box 24D (procedures, services, or supplies) and the modifier field using the display text from the CodeableConcept. These boxes must contain the actual billing code values (e.g., CPT/HCPCS codes) as defined in the coding array, not the human-readable display. When multiple codes are present, they should appear comma-separated.

Use CPT codes in claim CMS 1500 PDF (#6435)


**Fix profile loading so a cache miss falls through to the database instead of returning `undefined` immediately.**

Currently, when a `StructureDefinition` URL is not found in the per-project Redis cache, the profile loader returns `undefined` without querying the FHIR repository database. This causes profile-based validation to silently pass when it should reject resources, because the profile is never loaded, even if it exists in the database.

The expected behavior: if the cache lookup yields no match, the loader must proceed to query the database normally. The cache-hit case should still short-circuit and return the cached profile.

Don't return early on cache miss (#3948)


Implement the FHIRPath `join` string function from the STU additional string functions.

`join` takes a collection of strings and an optional separator. If no separator is given, concatenate the strings directly. If the input collection is empty, the result must be empty. If the separator is not a string, throw an error.

Add placeholder stubs for the other additional string functions (`encode`, `decode`, `escape`, `unescape`, `trim`, `split`) so the section is clearly defined.

Spec reference: https://build.fhir.org/ig/HL7/FHIRPath/#additional-string-functions

Implements FHIRPath string join (stu) (#4683)


**Feature: Respect project download settings during retries**
**Reported by**: internal team
**Severity**: medium – can cause unexpected outbound requests after admin disables downloads

### What happened
When auto‑download is turned off for a project (setting `autoDownloadEnabled` set to `false`) or a URL prefix is added to `autoDownloadIgnoredUrlPrefixes`, download jobs that are already in the queue or scheduled for retry still execute and perform the download request. The settings are only checked at the moment the job is created, not when it runs.

### Expected behavior
The download worker must re‑evaluate project settings **at execution time** for every job (including retries) and skip the download if:

- The project’s `autoDownloadEnabled` is explicitly `false`, **or**
- The download URL starts with one of the prefixes listed in `autoDownloadIgnoredUrlPrefixes`.

When a job is skipped for these reasons, the worker **must not** make an HTTP request to the target URL, and the job should be treated as completed so that no further retry occurs.

### Acceptance criteria

1. **Disabled auto‑download**
   - **Given** a project with auto‑download enabled and a queued download job
   - **When** an admin updates the project to set `autoDownloadEnabled` to `false`
   - **Then** the job, when executed (initial attempt or retry), **does not** initiate any network call, completes successfully, and is not re‑queued.

2. **Ignored URL prefix**
   - **Given** a project with auto‑download enabled and a queued download job
   - **When** an admin adds the URL’s prefix to the project’s `autoDownloadIgnoredUrlPrefixes` (as a comma‑separated list)
   - **Then** the job, when executed, **does not** initiate any network call, completes successfully, and is not re‑queued.

3. **Early bail‑out**
   - The settings check must happen before any attempt to fetch the URL, so that a paused/retrying job never generates outbound traffic after the admin disables downloads.

Stop auto download retries if disabled (#7601)


**Title:** Can't narrow broad Condition/Observation scopes during auth to category-specific sub-scopes

**Problem**
When an application requests a broad FHIR scope such as `patient/Condition.rs` or `patient/Observation.rs`, the scope selection screen (`ChooseScopeForm`) only shows a single checkbox for that exact scope value. There is no way for the user to narrow the grant to a specific resource category (e.g., encounter-diagnosis, laboratory, vital-signs) directly in the UI.

Additionally, even if the user tries to manually request a more granular scope like `patient/Condition.rs?category=problem-list-item`, the server-side scope validation (`POST /auth/scope`) always rejects it because the current logic requires an exact match with one of the originally requested scopes. This makes it impossible to limit access to a subset of a resource scope during the authorization flow.

**Expected Behavior**

1. **UI: `ChooseScopeForm` should offer category-specific scopes when a qualifying “open” scope is present.**
   - When the originally requested scopes include a scope for reading/searching a resource without any fixed category filter (e.g., `patient/Condition.rs` or `patient/Condition.*`), the form should also display checkboxes for well-known category-specific variants such as:
     - Condition: `?category=http://terminology.hl7.org/CodeSystem/condition-category|encounter-diagnosis`, `?category=http://terminology.hl7.org/CodeSystem/condition-category|problem-list-item`, `?category=http://hl7.org/fhir/us/core/CodeSystem/condition-category|health-concern`
     - Observation: `?category=http://hl7.org/fhir/us/core/CodeSystem/us-core-observation-category|clinical-test`, `?category=http://terminology.hl7.org/CodeSystem/observation-category|laboratory`, `?category=http://terminology.hl7.org/CodeSystem/observation-category|social-history`, `?category=http://hl7.org/fhir/us/core/CodeSystem/us-core-category|sdoh`, `?category=http://terminology.hl7.org/CodeSystem/observation-category|survey`, `?category=http://terminology.hl7.org/CodeSystem/observation-category|vital-signs`
   - These extra checkboxes should appear alongside the original scope checkbox and start unchecked by default (the original scope stays checked).
   - The feature should be robust: if a scope already includes a query parameter (e.g., `patient/Condition.rs?encounter=Encounter/1`), no additional sub-scopes should be offered from it, because it is already narrowed.

2. **Server: scope validation must accept valid subset scopes while still rejecting invalid ones.**
   - A submitted scope `candidate` should be accepted if it is exactly equal to an originally requested scope **or** if it is that original scope with a `?` query string appended directly (i.e., `candidate` starts with the original scope string and the very next character is `?`). This represents a valid narrowing of the original scope.
   - Reject any candidate that does not satisfy this rule, including:
     - A scope that doesn't start with an originally requested scope at all.
     - A scope that changes the base part of an original scope (e.g., a different resource type or permission suffix).
     - A scope that tries to add a query parameter to a scope that already has a query parameter; only flat “base + `?key=value`” extensions from a parameter-less original scope should be allowed.

**Impact**
- Users cannot scope their authorization to specific health data categories even when the application supports it, which forces overly broad grants or requires custom workarounds.
- The server’s strict exact-match policy blocks legitimate subset requests, weakening the principle of least privilege.
- This gap affects any Medplum deployment using the standard auth UI and scope selection flow for Condition and Observation resources.

Allow selection of more granular scopes (#5920)


**Request: Allow super admins to change or remove a User's project compartment**

### Problem
When a super admin updates a `User` resource and tries to change its `meta.project` (e.g., reassign the user to a different project or clear the project entirely), the change is silently ignored. The user remains stuck in the project they were originally created in. Ordinary (non-admin) users cannot change the project compartment, which is correct, but super admins should have the ability to move users between projects or unassign them.

### Desired behavior
1. **Super admin update**
   When a `User` resource is updated with super admin privileges (i.e., through the system repository / super admin context) and the update payload explicitly sets `meta.project` to a new project ID **or** to `undefined` (meaning no project), the resulting saved resource must reflect that change.
   - If `meta.project` is set to a valid project ID, the user should belong to that project.
   - If `meta.project` is set to `undefined` (or omitted), the user should have no project association (`meta.project` must be absent or `undefined`).

2. **Non‑admin update (no change)**
   When a non‑super‑admin user updates a `User`, any attempt to modify `meta.project` should still be **silently ignored**, and the user’s existing project compartment must be preserved.

3. **Scope**
   This relaxation must apply **only** to the `User` resource type. Other resource types whose project compartment is currently protected (e.g., resources in the `protectedResourceTypes` set) must not become mutable by super admins unless otherwise intended.

4. **Safety**
   The change must not inadvertently allow non-admins to reassign users or otherwise weaken compartment security. The existing guard that prevents normal users from moving themselves or other users out of their project must remain intact.

Allow super admins to remove User.meta.project (#5280)


**Bug: `SubscriptionManager` does not react to `MedplumClient` profile changes, leaving stale subscriptions**

### Steps to reproduce
1. Create a `MedplumClient` logged in as user A and start a `SubscriptionManager` with one or more criteria subscriptions.
2. Log out (so `getProfile()` returns `undefined`).
3. Observe that the WebSocket stays open and the old subscriptions are still active.
4. Log in as user B (different profile id). The manager does not close the old WebSocket, does not fetch new binding tokens, and does not re-bind the criteria for the new user.

### Expected behavior
- When the `MedplumClient` profile transitions from a defined profile to `undefined`, the `SubscriptionManager` should close the WebSocket.
- When the profile transitions from `undefined` to a defined profile, or from one profile id to a *different* id, the `SubscriptionManager` should reconnect the WebSocket, which must in turn re-fetch binding tokens and re-establish the registered criteria subscriptions.
- After a reconnect that is triggered by a profile change, the `SubscriptionManager`’s internal `wsClosed` flag must be reset to `false` so that future automatic reconnection (e.g. after a network interruption) is not suppressed.
- If the profile changes to the same id, no close or reconnect should occur.

### Additional context (implementation notes)
- `SubscriptionManager` already has a `reconnectWebSocket` method, but currently it does not reset `wsClosed`. This should be fixed as part of the profile-change handling so that any reconnect—including those from profile changes—leaves the manager in a properly recoverable state.
- The `MedplumClient` fires a `'change'` event whenever the session/profile changes. `SubscriptionManager` should use that event, together with `getProfile()`, to detect transitions and trigger the appropriate close/reconnect logic, while also keeping track of the previously seen profile id to avoid unnecessary actions.

fix(subscriptions): reconnect on profile change (#5143)


**Title:** Spaces Inbox should display tool calls and tool responses in the chat

**Summary**
The Spaces Inbox chat currently hides tool interactions entirely. Messages with `role: 'tool'` and assistant messages that contain `tool_calls` are filtered out of the visible message list. Users have no visibility into what FHIR requests the assistant makes or the data it receives, which hurts debugging and transparency.

**Expected behavior**
When a space bot calls a FHIR tool (e.g., `GET /Patient/123`) and receives a response, both the tool invocation and the response should appear inline in the chat conversation, interleaved with other messages.

- Assistant messages that contain `tool_calls` but no `content` should be shown with a summary of each call: at minimum the HTTP method (e.g. `GET`, `POST`) and the FHIR path (e.g. `Patient/123`). If the tool call cannot be parsed meaningfully, a fallback placeholder should be shown.
- Messages with `role: 'tool'` should display the returned data. The content is often a JSON FHIR resource; it should be presented in a readable, collapsible/expandable format (a simple toggle is sufficient).
- System-level messages (`role: 'system'`) should still be hidden.
- The redundant "AI Assistant" label and avatar that previously appeared above every assistant message should be removed. The sender context is already clear from message alignment and content.

**Current state**
In `SpacesInbox.tsx`, the `visibleMessages` filter excludes any message where `role === 'system'` or `role === 'tool'`, and any assistant message that has `tool_calls`. These messages are simply not rendered.

**Acceptance criteria**
1. Tool-call-only assistant messages appear as distinct entries in the chat, displaying the method and path from the parsed tool call arguments.
2. Tool response messages appear as collapsible entries in the chat, with the response payload shown in formatted JSON.
3. The chat thread no longer shows an explicit "AI Assistant" label or icon for any assistant message.
4. Existing behavior for regular user and assistant text messages (with content, component previews, and resource boxes) remains unchanged.

**Edge cases**
- Tool call arguments that are not valid JSON should not crash the UI; display a fallback message.
- Tool response content that is not valid JSON should be displayed as-is (not attempted to be prettified).
- Responses that contain a FHIR reference string like `Patient/patient-123` should still be findable in tests, but those strings may now appear inside a collapsible JSON block, not as free text.

feat(provider): Spaces show tools request/responses (#8897)


We need to add a missing defensive check to the OAuth refresh token endpoint.
When a client sends a `refresh_token` grant, the server currently only verifies that the associated login still has a stored `refreshSecret`. It does **not** ensure that the presented JWT actually contains a `refresh_secret` claim.

A token that arrives without this claim (e.g., one that was extracted and re‑signed without it) can slip past the initial guard and may cause confusing errors or undefined behaviour downstream.

We should harden the refresh handler so that it explicitly rejects tokens that lack the `refresh_secret` claim, returning a `400` with `invalid_request` and a descriptive error message such as *“Invalid refresh token”*.

This is a small, contained validation change in the refresh token flow (`handleRefreshToken` in `packages/server/src/oauth/token.ts`).

Defensive check for refresh_secret claim (#7295)


### Issue: `max: '*'` cardinality validation produces `NaN` in error messages and does not correctly treat the limit as unlimited

#### Summary
When an `OperationDefinition` parameter uses `max: '*'` to mean “unbounded”, the validation logic for operation input parameters:

1. Shows a malformed cardinality range in the error text (e.g. `Expected 1..NaN value(s) …` instead of `1..*`).
2. Does not treat `*` as unlimited when checking the actual array length (because `parseInt('*')` returns `NaN`, and comparisons against `NaN` are always false, so the upper bound check is effectively dead).

#### Steps to reproduce (example)
The `PlanDefinition/$apply` operation defines the `subject` parameter with `min: 1, max: '*'`. Calling the operation without a `subject` currently yields an error containing `Expected 1..NaN …`.

#### Expected behaviour
- The error message must display `*` (or an equivalent unlimited marker) instead of `NaN`.
- The upper cardinality check must allow an unlimited number of values when `max` is `'*'`.

#### Additional improvement: `db-configure-indexes` operation
The `db-configure-indexes` operation currently declares `tableName` with `min: 0, max: '*'` yet performs an extra manual check that throws `'tableName must be specified'` when no tables are provided. This works, but it would be cleaner and more consistent to:

- Set `min: 1` (since at least one table name is required).
- Remove the manual empty-array guard (the generic parameter validation will then reject a missing or empty array with a proper cardinality error).

No external behaviour change is intended for `db-configure-indexes` other than the error message aligning with the rest of the framework’s cardinality errors.

#### Acceptance criteria
- No operation produces error messages containing `NaN` for `max: '*'` parameters.
- Parameter validation correctly treats `'*'` as unlimited when checking array length.
- Operation definitions and handlers that duplicate cardinality enforcement are cleaned up so that the generic validation is the canonical authority.

Better validation error msg for max: '*' (#7754)


```
Title: Resources expunged or purged leave stale entries in lookup tables

Description:
When a resource is expunged (via $expunge for a single resource, a list of IDs, or a project compartment) or purged (via $purge with a timestamp threshold), the main resource row and its history rows are deleted, but the corresponding rows in database lookup tables (e.g., `HumanName`, `Address`, etc.) are left behind. This causes orphaned data that can lead to incorrect search results and violates the expectation that these operations fully remove a resource's traces.

The normal resource deletion path already cleans up lookup tables. The expunge and purge paths should be consistent with that behavior. All removal steps must execute within a single database transaction to avoid partial cleanup.

Acceptance Criteria:
- After expunging one or more resources, no rows remain in any lookup table that reference the expunged resource IDs.
- After purging resources older than a given timestamp, all matching lookup table rows are also removed.
- The lookup-table cleanup logic used elsewhere in the system is reused, ensuring the same semantics apply.
- All cleanup operations (main table, history, lookup tables) are performed atomically in a transaction.
```

Clean up lookup tables in Project expunge (#4035)


**Feature Request: Support scheduling from HealthcareService resources with shared scheduling parameters**

In the provider scheduling UI, the **FindPane** (the pane for selecting a service type and finding open slots) currently only considers service types defined on the `Schedule` resource itself. The scheduling backend now supports `HealthcareService` resources as shared containers for scheduling parameters via the extension `https://medplum.com/fhir/StructureDefinition/SchedulingParameters`. The provider app should also fetch and include service types from `HealthcareService` records that have this extension.

High-level requirements:

- **FindPane** should query for `HealthcareService` resources that have a service type (`service-type:missing=false`) and, among those, only consider ones that carry the `SchedulingParameters` extension.
- Service types from qualifying `HealthcareService` resources must be offered in the service-type list alongside those from the `Schedule` resource.
- **Deduplication**: if a service type appears in both a `HealthcareService` and the `Schedule`, it should only be shown once (e.g., by comparing `system|code` pairs of the codings).
- **Visibility**: if there are no schedulable service types at all (neither from the `Schedule` nor from any valid `HealthcareService`), the **FindPane** should render nothing (return `null`). The parent **SchedulePage** should no longer compute or inspect service types; it should just render `FindPane` whenever a `schedule` and `range` are available.
- The **FindPane** should accept a `className` prop so the parent can still apply layout styles (the existing wrapping `Stack` and conditional should be removed from `SchedulePage`).
- Existing behavior for service types derived solely from the `Schedule` must remain unchanged.
- `HealthcareService` resources without the `SchedulingParameters` extension must be ignored.

Provider: Schedule from shared scheduling parameters on HealthcareService resources (#8857)


Title: React test suite emits several console warnings

Description:
When running the test suite for the `@medplum/react` package, several warnings appear in the console. We want zero warnings in test output so that real regressions are immediately obvious and to keep component implementations free of anti-patterns. Please investigate and fix the root causes.

The warnings observed include:

- **Updating a component during render (QuestionnaireForm).** The `onChange` callback from the `useQuestionnaireForm` hook can fire during the rendering/initialization phase. If the parent (`QuestionnaireForm`) calls `setState` inside that callback, React warns because the update happens while a sibling is still rendering. The state changes must be deferred until after the commit phase so they don’t clash with React’s render lifecycle.

- **Unknown attributes passed to Mantine (Spotlight search input).** The `data-1p-ignore` and `data-lpignore` attributes meant to suppress password managers are being passed in a way that triggers Mantine prop-type warnings. They need to be applied correctly so the warning disappears while still reaching the underlying DOM element.

- **State update on unmounted component (ParticipantFilter).** A debounced search inside `ParticipantFilter` triggers state updates after the component has unmounted or out of sync with user input, producing a React warning. The cleanup on unmount must cancel pending debounced calls.

- **`act(...)` warnings in tests.** Several tests inside `ThreadInbox.test.tsx` and `ParticipantFilter.test.tsx` cause `act(...)` warnings because state updates occur outside React’s batching. The test code needs to properly wrap asynchronous operations in `act()` and await settled state before assertions, possibly with the help of fake timers where relevant.

Acceptance criteria:
- Running `jest` in the React package produces zero React-related warnings in the output.
- All existing tests continue to pass with the same coverage.
- No observable change in component behavior or public APIs.


**Title:** Photon Health: Add NDC fallback and store Photon treatment ID on MedicationKnowledge during formulary sync

**Context:**
Our Photon Health integration includes the `sync-formulary` bot that takes a FHIR `List` (a formulary of `MedicationKnowledge` references) and adds the corresponding medications to the Photon catalog. After a successful sync, the List entry is flagged so it won’t be re-processed.

**Problem:**
1. **NDC-only medications are missed** – The bot only attempts a Photon lookup using the medication’s RXCUI (`http://www.nlm.nih.gov/research/umls/rxnorm`). Many medications provide only an NDC (`http://hl7.org/fhir/sid/ndc`). These are silently skipped, and the formulary never syncs them.
2. **No Photon treatment ID is stored** – Once we confirm a medication exists in Photon, we lose the Photon treatment ID. Later workflows have no easy way to correlate the FHIR `MedicationKnowledge` with the Photon treatment.
3. **Fragile “already synced” detection** – The bot filters out already-synced List entries by checking if any flag coding has `code === 'synced'`, ignoring the system. This could accidentally match unrelated codes and mark entries as synced when they aren’t.

**Desired Behavior:**
- When extracting a medication identifier from `MedicationKnowledge.code.coding`, try the RXCUI system first, then fall back to the NDC system. Pass that code (if found) to the Photon medication lookup.
- After successfully resolving a Photon medication, **persist the Photon treatment ID back onto the `MedicationKnowledge` resource** so that it’s available for subsequent use. Since `MedicationKnowledge` has no `identifier` field, add the treatment ID to `code.coding` using system `https://neutron.health/treatments`.
- Tighten the synced-entry filtering: check the flag’s coding using the existing `NEUTRON_HEALTH` system (from `constants.ts`) in combination with the code, not just the code alone.
- If a `MedicationKnowledge` has no `code` at all, treat it as an error (rather than silently failing).
- If after filtering out synced entries the list of medications to process is empty, the bot should raise a clear “No medications to sync” error (instead of continuing with an empty array).

**Relevant files / areas:**
- `examples/medplum-photon-integration/src/bots/sync-formulary.ts` — main handler and helper logic.
- `examples/medplum-photon-integration/src/bots/constants.ts` — contains URL constants for the Neutron Health system.
- `examples/medplum-photon-integration/src/bots/utils.ts` — contains `handlePhotonAuth` and `photonGraphqlFetch` used by the bot.
- The Photon GraphQL `medications` query used inside `getPhotonMedication` already supports searching by various codes (including NDC), so no API change is needed there.

[Photon Health] Add NDC fallback and update MedicationKnowledge when syncing formulary (#5379)


**Title: `$export` operation fails for callers with read‑only access policies**

### Problem
The Bulk Data Export (`$export`) operation is a pure read operation, but it internally creates `AsyncJob` and `Binary` resources to track progress and store exported data. Currently these resources are written through the caller’s repository, which rejects the write when the caller has a read‑only access policy (e.g. `system/*.read` or a policy with `resource: [ { resourceType: '*', readonly: true } ]`). This makes the export fail with a `Forbidden` error.

Furthermore, if the access policy is compartment‑scoped (contains a `compartment` field), the caller cannot read back the bookkeeping resources because they don’t carry the expected compartment as an account reference. The job‑polling code and any attempt to download the exported `Binary` both read through the caller’s repository, which filters by the access‑policy compartment.

### Expected behavior
1. A caller with a read‑only access policy must be able to start and complete a `$export` successfully. The internal writes (`AsyncJob`, `Binary`) must not be rejected.
2. The caller must be able to read the `AsyncJob` (for polling) and the `Binary` (for downloading) even when the access policy includes a compartment.
   The bookkeeping resources should therefore:
   - be placed in the caller’s project (`meta.project`),
   - carry the access policy’s compartment (if any) as a `meta.accounts` entry.

3. When `close()` finalizes the export, the completed `AsyncJob` must retain the `meta.accounts` that were set when the job was created, so the caller can still read it after completion.

### References
- Operation: `$export` (Bulk Data Export)
- Resources involved: `AsyncJob`, `Binary`

Allow `$export` with read-only scopes (#9695)


**Title**: `$rate-limits` operation is slow for projects with many memberships and mishandles invalid membership IDs

**Severity**: Performance degradation / incorrect error response

**Environment**: All deployments

**Description**:

When calling the `Project/$rate-limits` operation for a project that has many active user memberships, the handler makes many sequential Redis roundtrips (separate `GET` and `PTTL` per membership plus separate calls for the project). This causes noticeable latency and adds unnecessary load to Redis.

Additionally, when the operation is invoked with explicit `membershipId` parameters, the handler currently reads each membership from the database using individual resource reads instead of a single batch read. This further increases database load and latency, especially when many ids are supplied.

Finally, if one of the explicitly supplied `membershipId`s does not exist (e.g., a deleted or mistyped id), the operation fails with an internal error (`500`) rather than returning a proper `400 Bad Request` response that identifies which membership id(s) were problematic.

**Expected behavior**:

1. **Performance**: The rate-limits handler should minimize Redis roundtrips by using a batched/pipelined approach to fetch the usage counters and time-to-live for all relevant keys (one batch for values, one batch for TTLs). Similarly, when the caller provides explicit membership IDs, the handler should batch the database reads for those memberships.

2. **Error handling**: When one or more explicit `membershipId` values cannot be resolved to existing `ProjectMembership` resources, the handler should respond with `400 Bad Request` and indicate which membership IDs were not found, rather than failing silently or with a `500`.

**Constraints / invariants**:

- The handler must still enforce that all supplied membership IDs belong to the project in context; any membership from a different project should result in `403 Forbidden`.
- When no `membershipId` parameters are provided, the handler should continue to derive the active memberships from the Redis sorted set as it does today.
- If the rate-limiter instance is unavailable (`ctx.fhirRateLimiter` is undefined), the handler should still return a valid response containing membership profiles and project id, without attempting Redis operations.
- The response shape for the `$rate-limits` operation must remain backward-compatible (membership list with quota status, project quota status).

**Steps to reproduce**:

1. Create a project with many active user memberships (e.g., hundreds).
2. Call the `$rate-limits` operation without `membershipId` parameters and measure latency.
3. Observe significantly increasing latency as the number of memberships grows, due to sequential Redis calls.
4. Call the operation with several `membershipId` parameters, including at least one nonexistent id.
5. Observe a `500` internal error response instead of a clear `400` error.

**Impact**:

- Degraded API performance and increased Redis load for operations that should be lightweight.
- Poor developer / integrator experience when diagnosing invalid membership IDs (500s are misleading).

perf(server/ratelimit): optimize calls to Redis keys (#9219)


The CCDA‑to‑FHIR conversion produces invalid FHIR `Address` objects when XML address elements carry attributes, such as the HL7 `partType` attribute on ADXP elements like `<city partType="CTY">Springfield</city>`. Instead of extracting just the text content, the conversion leaves the raw XML‑parser representation in the FHIR fields: `city`, `state`, `postalCode`, and `country` become `{ "#text": "Springfield", "@_partType": "CTY" }` objects, and `streetAddressLine` becomes an array of such objects.

**Observed behaviour**
- The FHIR `Patient.address[0].city` value is a plain object, not a string.
- FHIR `$validate` fails with messages like `Invalid additional property "#text"` and `Invalid additional property "@_partType"`.
- Downstream FHIR persistence crashes with `TypeError: city.trim is not a function`.

**Expected behaviour**
- After conversion, `city`, `state`, `postalCode`, and `country` must be plain strings containing only the XML element’s text.
- `line` must be an array of strings (or `undefined`).
- Address parts that do *not* carry attributes should continue to work as before (no regression).

**Steps to reproduce**
1. Parse a CCDA document that contains an address block like:
   ```xml
   <addr use="HP">
     <streetAddressLine partType="SAL">742 Evergreen Terrace</streetAddressLine>
     <city partType="CTY">Springfield</city>
     <state partType="STA">IL</state>
     <postalCode partType="ZIP">62704</postalCode>
     <country partType="CNT">US</country>
   </addr>
   ```
2. Run the standard CCDA‑to‑FHIR conversion (e.g. `convertCcdaToFhir(convertXmlToCcda(xml))`).
3. Inspect the resulting `Patient.address` array.

**Impact**
Any CCDA document that uses `partType` (or any other XML attribute) on address‑part elements cannot be reliably ingested into Medplum. This is a common pattern in HL7‑compliant CCDAs and blocks real‑world interoperability scenarios.

**Acceptance criteria**
- The conversion mapping for addresses strips XML‑parser artifacts and yields string values for all address components.
- Plain address elements without attributes remain functional.
- The fix should align with how name parts are already handled by the CCDA‑to‑FHIR name mapper (which already normalizes text nodes correctly).

fix(ccda): normalize address part text nodes in mapAddresses (#9534)


**Feature request: Support `author` search parameter for Subscription resources**

We need to allow access policies to restrict subscriptions by owner using a criterion like `Subscription?author=%profile`. Currently there is no `author` search parameter defined on `Subscription`, so any access policy rule containing that criterion will fail.

Add a SearchParameter named `author` for the `Subscription` resource that searches on `Subscription.meta.author`. The parameter should be of type `reference` and target `Patient`, `Practitioner`, `RelatedPerson`, `ClientApplication`, `Bot`.

To make this useful, also add a database column `author` on the `Subscription` table and an index on it (via a new schema migration). The column should store the author reference so the parameter can be used efficiently in queries and access‑policy enforcement.

feat(Subscription): add `author` as a `SearchParameter` (#4540)


### **Display resources alongside component previews in Spaces**

The component preview panel only shows the generated React code — users have no way to inspect the FHIR resources that were used to build that component. Add a **Resources** tab inside the component preview panel that lists each associated resource reference (e.g., `Patient/abc`, `Observation/xyz`). Each resource should be clickable to open the standard resource detail view. When navigating from a component-preview resource back to the detail panel, provide a back button that returns to the component preview.

Additionally, during streaming component generation, the panel currently shows nothing until the full code arrives. Instead, stream the raw component code into a read‑only code block inside the panel as chunks arrive, so users can see generation progress. Once streaming finishes, replace the streaming view with the normal interactive component preview (with Preview, Code, and Resources tabs).

### **Separate component streaming from regular content streaming**

Currently the same `onStreamChunk` callback is used for both text‑message streaming and component‑code streaming, which makes it impossible to display both simultaneously or handle them differently. Introduce a separate `onComponentStreamChunk` callback. When a component is being generated, use this callback so the UI can render the streaming component code in the preview panel without interfering with the main chat message stream.

### **Strip display‑only message fields before sending to the AI API**

Internal message objects contain extra fields (e.g., `componentCode`, `resources`, `sequenceNumber`) that are only meaningful for UI state and persistence — they should never reach the AI provider. Passing them causes API rejections and bloated payloads. Before sending messages to the bot (both REST and SSE paths), strip them down to only `role`, `content`, `tool_calls`, and `tool_call_id`.

### **Fix message persistence ordering to prevent corrupted state**

When an assistant message with `tool_calls` is saved before its corresponding tool responses, a crash or interrupt can leave `tool_calls` in the database with no matching responses, which breaks message loading and subsequent AI requests. Save tool‑response messages **before** the assistant message that contains `tool_calls`, so a failed save never orphans the `tool_calls`. Remove the `repairMessageHistory` workaround — it was masking this ordering bug instead of fixing the root cause.

### **Resource list display for many results**

When a message contains more than 2 resource references, show a single compact summary card (“N results”) instead of individual resource boxes. Clicking it opens a scrollable list panel where each resource can be selected to open the detail view. This prevents the chat from being overwhelmed by a long list of resource cards.

feat(spaces): Component preview, code and resources display (#8654)


**Title:** Separate dispatcher for main-thread (single-thread) execution

**Background**
The setting `RunOnMainThread` (via `FrameworkPackageSettings.RunOnMainThread`) causes the top-level work item to be executed directly on the calling thread instead of on a dedicated runner thread. Currently this is implemented by a boolean flag `RunsOnMainThread` on `WorkItem`, which the `SimpleWorkItemDispatcher` inspects to decide whether to dispatch inline or spawn a thread. This embeds main-thread semantics inside a dispatcher that is otherwise about simple single-thread-on-separate-thread execution, and it complicates the preprocessor conditional around platform capabilities. On `NETSTANDARD1_3`/`NETSTANDARD1_6` threading is unavailable, so the only viable execution is inline; yet today the dispatcher still relies on the same flag and conditional code.

**Requested changes**
1. Create a dedicated `IWorkItemDispatcher` implementation whose sole responsibility is to run work items directly on the calling thread (i.e., `Start` and `Dispatch` simply call `work.Execute()`).
   - This dispatcher should report `LevelOfParallelism` as 0.
   - `CancelRun` should throw `NotSupportedException` because cancellation from an external thread is not feasible when the runner operates synchronously on the caller’s thread.

2. Update `NUnitTestAssemblyRunner` so that the dispatcher is selected as follows:
   - On `NETSTANDARD1_3`/`NETSTANDARD1_6`: always use the new main-thread dispatcher (since threading is unavailable).
   - On other platforms:
     - If `PARALLEL` is defined and the level of parallelism is > 0, continue using `ParallelWorkItemDispatcher`.
     - Else if the settings contain `RunOnMainThread` with value `true`, use the new main-thread dispatcher.
     - Otherwise use `SimpleWorkItemDispatcher`.

3. Simplify `SimpleWorkItemDispatcher`:
   - Remove all conditional `NETSTANDARD…` blocks and the `RunsOnMainThread` branch inside `Start`.
   - Always spawn a runner thread in `Start` (as the “simple” dispatcher should always run on a background thread).
   - The thread-spawning and cancellation logic that was previously behind platform guards should become unconditional; platforms that cannot spawn threads will never use this dispatcher, so no special handling is needed here.

4. Remove the `RunsOnMainThread` property from `WorkItem`. The setting should influence only dispatcher selection in the assembly runner, not propagate into the work item.

**Behavioral invariants**
- When `RunOnMainThread` is set, no background thread is created; the top-level work item executes on the caller’s thread.
- The `SimpleWorkItemDispatcher` no longer has any knowledge of main-thread mode; it always spawns a thread.
- The `MainThreadWorkItemDispatcher` cannot cancel runs (throws `NotSupportedException`), which is acceptable because the caller owns the thread and can manage its own cancellation externally.
- Platform-specific preprocessor guards are only in the assembly runner dispatcher selection logic, not inside the dispatchers themselves.

Creating a separate MainThreadWorkItemDispatcher to accomodate cases when workitems need to be dispatched straight into the main thread


The following call using a `StringComparer` does not compile due to overload ambiguity:

```csharp
Assert.That("hello", Is.EqualTo("HELLO").Using(StringComparer.OrdinalIgnoreCase));
```

`StringComparer` implements `IComparer`, `IComparer<string>`, `IEqualityComparer`, and `IEqualityComparer<string>`, and the fluent `Using` methods on the string constraint’s return type have overloads accepting all of these. The compiler cannot pick one, resulting in a compilation error.

Users expect that when comparing strings they can pass a `StringComparer` (such as `OrdinalIgnoreCase`) directly to `.Using(...)` without casting, just as they can pass an `IEqualityComparer<string>` or a `Comparison<string>`. Providing a direct overload that accepts `StringComparer` on the string-specific equality constraint would resolve the ambiguity and improve usability.

Added StringComparer overload to EqualStringConstraint.


**Title:** Allow `Warn.If` and `Warn.Unless` inside `Assert.Multiple` blocks

**Area:** Framework / Assertions

**Description**
Currently, using `Warn.If` or `Warn.Unless` inside an `Assert.Multiple` block throws an exception (the message says these methods may not be used in a multiple assertion block). This contrasts with `Assert.Warn`, which is already allowed to run within `Assert.Multiple`. The restriction on `Warn.If` and `Warn.Unless` appears to be an oversight; users expect that warnings can be issued as part of a batch of assertions, just like failures.

**Desired behavior**
- `Warn.If` and `Warn.Unless` (all overloads) should work inside `Assert.Multiple` without throwing an exception.
- When a `Warn.If`/`Warn.Unless` condition triggers a warning, it should be collected and reported like other assertion results, and the block’s execution should continue.
- If the `Assert.Multiple` block produces **only** warnings (and no failures), the test result state should be `Warning`.
- If the block produces failures as well as warnings, the result state should be `Failure`, but the warnings should still be listed among the assertion results.
- Calls that do *not* trigger a warning (e.g., `Warn.If(false, ...)`) should still increment the assertion count. They should not affect the result state.

**Constraints / safety**
- This change must **not** affect the existing restriction that `Assert.Pass`, `Assert.Ignore`, `Assert.Inconclusive`, and `Assume.That` throw when used inside `Assert.Multiple`. Those remain forbidden.
- The warnings must not interrupt the execution flow of the multiple-assert block; all subsequent assertions must still be evaluated.
- The behavior of `Warn.If`/`Warn.Unless` outside of `Assert.Multiple` must remain unchanged.

**Likely implementation touchpoints**
The restriction is currently enforced by a private guard in the `Warn` class. Lifting the restriction should consist of removing that guard and its calls from the `If`/`Unless` overloads. Verify that the underlying assertion-reporting path already handles warnings correctly inside a multiple-assert block (as with `Assert.Warn`).

Allow Warn.If and Warn.Unless to run in a Multiple assert block


```
Title: NUnitLite does not parse `--params` / `-p` into a dictionary before passing to the framework

**Observed behavior:**
When running tests via NUnitLite with test parameters supplied on the command line (e.g., `--params:X=5` or `-p:X=5`), the test framework receives the raw, unparsed string (e.g., `"X=5"`) rather than the individual key-value pairs. As a result, tests that expect to retrieve parameters via `TestContext.Parameters["X"]` cannot access them without manually splitting the string.

**Expected behavior:**
NUnitLite should parse the `--params` and `-p` option values into separate key-value pairs and hand them over to the test framework in a form that allows direct keyed access. The framework already exposes a setting (`FrameworkPackageSettings.TestParameters` or similar) that can accept a dictionary; NUnitLite should populate that dictionary.

Multiple `--params` / `-p` options and semicolon-separated values should be merged appropriately (later occurrences with the same key should override earlier ones, or be combined — whichever the existing engine contract dictates). Invalid parameters (e.g., missing `=`, or nothing after `=`) should produce a clear error message during command-line parsing, similar to how other malformed options are reported.

**Impact:**
This makes NUnitLite the owner of parameter parsing, relieving the framework and adapters from having to re-parse the same raw string. It ensures consistent behavior across runners and gives test authors reliable access to individual test parameters at runtime.
```

NUnitLite taking responsibility for parsing test params


A test that explicitly catches an `AssertionException` (for example, calling `Assert.Fail(...)` inside a try/catch) should not record any assertion failure. The test should pass without leaving a failed assertion in the test result.

Before a recent update, such a test could pass but still exhibit a spurious failed assertion in the assertion results. This breaks tests that deliberately handle `AssertionException` for compatibility or cleanup, causing confusion because the test appears to both pass and report a failure.

The expected behavior:
- When `Assert.Fail` (or any assertion that would throw `AssertionException`) is called and the exception propagates uncaught, the test must fail as usual and the failure must be recorded in the test result's assertion list exactly once.
- When the exception is caught inside the test (so the test does not fail), no assertion entry should be recorded for that call. The test should pass with no side effects.
- Inside an `Assert.Multiple` block, failures must still be recorded as individual assertion results without throwing immediately, and the multiple block should still aggregate them at the end to fail the test. This behavior must be preserved unchanged.

In other words, the recording of a failure assertion should be tied to whether the exception actually causes a test failure, not to the moment `Assert.Fail` is invoked. Only unhandled assertion exceptions (or failures inside `Assert.Multiple`) should produce recorded assertion results.

Fix compatibility issue with AssertionException


**Title:** Improve performance of `EmptyDirectoryConstraint` on large directories and add test for subdirectory-only non‑empty directories

**Description**
When using `Is.Empty` (or `Is.Not.Empty`) on a `DirectoryInfo`, the constraint currently retrieves all files and all subdirectories eagerly via `GetFiles()` and `GetDirectories()`. For directories containing many thousands (or millions) of entries this causes noticeable slowdown, even when the first file system entry would suffice to determine that the directory is non‑empty.

**Expected behavior**
The constraint should determine emptiness by enumerating file‑system entries lazily and stopping as soon as any entry is found:

- On platforms where `EnumerateFileSystemInfos()` is available (most targets except .NET 3.5), use that lazy enumeration to avoid building a complete array.
- On .NET 3.5, at least consolidate the two separate calls into a single `GetFileSystemInfos()` so that the directory is not scanned twice.
- The result must treat any file system entry—file or directory—as making the directory non‑empty.

**Missing edge case / safety**
There is currently no test coverage for a directory that contains *only* a subdirectory (no files). The constraint’s previous implementation handled this by checking both file and directory counts, but any future change that accidentally ignores directory entries would break this contract. A test that creates a directory with only a child directory and asserts it is *not* empty needs to be added to prevent regressions.

**Impact**
- Performance: users who apply `Is.Empty` / `Is.Not.Empty` on large directories (e.g., build outputs, log folders, huge source trees) will see a measurable improvement.
- Correctness: the explicit test for a subdirectory‑only directory guarantees this case is protected going forward.

Add empty dir test. Only enumerate directory contents if needed


When multiple `[TestFixture(...)]` attributes with different arguments are placed on a single class, NUnit creates several parameterized fixture instances from that class. If the class also carries an `[Order]` attribute, the ordering should be applied to the generated suite that contains those fixture instances so that the whole group runs at the specified position relative to other test suites. Currently the `[Order]` attribute on such a class is silently ignored, and the parameterized fixtures run in discovery order instead of the declared order.

Desired behavior:
- A class with multiple `[TestFixture]` attributes and an `[Order]` attribute should have its combined suite (the container that holds all the generated fixture instances) honor the `Order` value.
- The ordering must work when the class is used alongside other test classes within the same namespace or assembly, with the numeric `Order` determining the relative execution position of the parameterized-fixture group.
- Nested type hierarchy should be respected: if the class is nested inside another class that also has `[Order]`, the declared ordering should propagate correctly (outer-to-inner, respecting inheritance of the attribute).
- The change must not affect ordering for ordinary single-instance test classes, where `[Order]` already works today.
- The implementation should remain consistent with how `[Order]` is applied to individual test methods and to non-parameterized test classes (i.e., the attribute sets a property on the test node, and the framework sorts based on that property during execution).

Apply Order Attribute to TestSuite for multiple Fixtures


We should remove the `OutputExceptionDataProperty` static property from `ExceptionHelper` and make including exception data the unconditional behavior. Currently `BuildMessage` checks this toggle before calling `AppendExceptionDataContents`. With the toggle gone, data is always appended. An important edge case: when `Exception.Data` is empty we must not emit a `Data:` header line (the helper already handles this by checking `ex.Data.Count > 0`, but we must not regress that). The existing tests in `ExceptionHelperOutputExceptionDataTests` only assert on data properties and should also verify that the exception message itself appears in the output. Please add coverage for the empty-data case.

remove static OutputExceptionDataProperty switch


```
Fix #2427: PropertyConstraint discards inner constraint's extra failure details

When a property constraint wraps another constraint (e.g.
Has.Property("Foo").EquivalentTo(...)), the failure message should include
any extra diagnostic lines provided by the inner constraint's result, such
as missing items in a collection equivalence check. Currently those lines
are omitted because PropertyConstraintResult does not forward the extra-
message call to the inner result.

Ensure PropertyConstraintResult delegates to the inner constraint's result
method for writing additional failure lines so the details appear in the
final message.

While fixing this, rename the method responsible for writing those extra
lines throughout the framework: `WriteExtraChainableMessage` →
`WriteAdditionalLinesTo`. Update the base method in `ConstraintResult`, all
overrides (`CollectionEquivalentConstraintResult`, `PropertyConstraintResult`),
and the private caller in `TextMessageWriter`. The rename should be purely
mechanical—no behavioral change beyond the delegation fix.
```

#2427 (PropertyConstraint throws away the more helpful message in the base constraint result) changes.


Implementation request:

1. **Theories without data points must fail instead of passing or being inconclusive.**
   When a theory method has parameters but no test cases are generated (e.g., no datapoints exist at all), the test should be reported as a failure with the message “No test cases were provided”.
   When a theory has test cases but every one of them is inconclusive (no successes), the theory should still be reported as a failure (current behavior), but the “no test cases” condition must be handled as a separate failure rather than silently succeeding or staying inconclusive.

2. **TestCaseSource on a parameterless method that yields zero test cases must be an error.**
   If `[TestCaseSource]` is placed on a method that has no parameters and the source returns zero test case rows, the test must be marked **not runnable** with a clear reason (e.g., “TestCaseSourceAttribute may not be used on a method without parameters”).
   When the method has parameters and the source yields zero items, the existing behavior—generating no test cases—should remain unchanged; do not flag that situation as an error.

Make Theories fail if no data at all is provided (#2037)


**Bug Report: `AnyOf` constraint – argument validation, failure messages, and comparer support**

**Summary**
The `AnyOf` constraint (`Is.AnyOf` and `ConstraintExpression.AnyOf`) has three problems that make it harder to use than other NUnit constraints:

1. **Missing argument validation for empty values**
   Calling `Is.AnyOf()` or `Is.AnyOf(/* nothing */)` with no expected values does not throw an informative exception. Instead, a confusing low‑level error occurs.
   *Expected behavior:* an `ArgumentException` should be thrown immediately with a message indicating that at least one expected value is required.

2. **Failure message / string representation does not display the expected list**
   When an `AnyOf` assertion fails, the message only shows something like `"any of"` without the actual expected values. This makes test failure output useless for debugging. Other constraints such as `EqualTo` include the expected values in angle brackets (`<...>`).
   *Expected behavior:* the `ToString()` representation of an `AnyOf` constraint should include the list of expected values, for example: `<anyof "a", "b", "c">`.

3. **`.Using(...)` with a custom comparison function is ignored**
   Applying `.Using(Func<T,T,bool>)` (or similar) to an `AnyOf` constraint does not use the supplied comparer. The default equality semantics are applied instead, making it impossible to customize matching for element presence.
   *Expected behavior:* when a comparison function is specified via `.Using`, the constraint should evaluate membership using that function.

**Additional details**
- The problem exists for both the static helper `Is.AnyOf` and the constraint builder `Is`/`ConstraintExpression.AnyOf`.
- The `ArgumentException` for an empty expected list should be thrown before any evaluation is attempted.
- The string representation must handle the expected collection in a readable way (similar to how other constraints format their arguments).

**Impact**
These issues prevent the `AnyOf` constraint from being integrated into tests that rely on clear failure messages or custom equality, and they can silently produce confusing results when no expected values are provided by mistake.


When a test uses `Is.EqualTo("some string")`, NUnit should support comparison against actual values that are not strings but can be treated as strings. Currently it only works for actual values that are `string` or `null`. Two cases should be supported:

1. **Implicit conversion to string:** If the actual type defines a `public static implicit operator string(…)` conversion, the constraint should apply the implicit conversion and then use normal string comparison, including support for `.IgnoreCase` and `.IgnoreWhiteSpace`.

2. **IEquatable<string>:** If the actual type implements `System.IEquatable<string>`, the constraint should delegate to `actual.Equals(expectedString)`. In this case, `.IgnoreCase` and `.IgnoreWhiteSpace` cannot be supported (the type's own `Equals` is responsible for any case/space logic), so those modifiers must cause an `InvalidOperationException` to be thrown with a clear message.

For types that are neither strings, nor implicitly convertible to strings, nor `IEquatable<string>`, the existing failure behavior should remain unchanged (the constraint should fail the assertion without throwing runtime exceptions).


```
Add a generic TryGet extension method on IPropertyBag to eliminate the repeated ContainsKey/Get pattern used when retrieving a single property value with a fallback default.

Many call sites do:

    if (properties.ContainsKey(key))
        value = (T)properties.Get(key);
    else
        value = someDefault;

or the ternary equivalent. Examples include retrieval of Timeout, ParallelScope, ApartmentState, LevelOfParallelism, and Order. This is verbose and easy to get wrong.

The new extension method should:

- Be named TryGet with two parameters: string key and T defaultValue.
- Return the value from Get(key) cast to T if the key is present, otherwise return defaultValue.
- Treat a null result from Get as “not found” (i.e., return defaultValue). This matches the existing defensive null handling in many callers.
- Be internal and placed in the existing file that already contains IPropertyBag extension methods (IPropertyBagDataExtensions).

After adding the method, update all current call sites that use the ContainsKey/Get pattern to use the new TryGet method instead.
```

Added TryGet extension method to IPropertyBag


```
Title: Restore backward compatibility for Is.EqualTo<T> returning EqualConstraint

Background:
The generic overloads `Is.EqualTo<T>(T? expected)` and `Is.EqualTo<T>(IEnumerable<T>? expected)`
are currently returning `EqualConstraint<T>`. This breaks existing user code that stores the
result in a variable typed as `EqualConstraint` or passes it to any API expecting the non-generic
`EqualConstraint` base type. The return type must be changed back to `EqualConstraint`.

Impact on UsingPropertiesComparer:
The fluent `UsingPropertiesComparer` configuration method (the one accepting a lambda like
`c => c.Excluding(x => x.SomeProperty)`) previously inferred the correct type from the generic
constraint. After the return type is reverted to `EqualConstraint`, callers will need to supply
the type explicitly, e.g. `.UsingPropertiesComparer<Person>(c => ...)`. The fix must ensure that
this explicit usage works correctly.

Safety requirement:
If the type argument passed to `UsingPropertiesComparer<T>` does not match the type parameter
of the underlying `EqualConstraint<T>`, an `ArgumentException` should be thrown with a
descriptive message. This prevents confusing errors when users accidentally specify the wrong type.

Acceptance criteria:
- `Is.EqualTo<Person>(somePerson)` returns `EqualConstraint` (not `EqualConstraint<Person>`).
- `Is.EqualTo(somePerson).UsingPropertiesComparer<Person>(c => c.Excluding(x => x.Name))`
  compiles and correctly compares properties.
- Calling `.UsingPropertiesComparer<WrongType>(...)` on a constraint created for a different
  type throws an `ArgumentException` at runtime.
```

Restore backward compatibility for Is.EqualTo<T>(T? expected) (#5013)


We currently have an `Is.ItemFrom(params object[])` constraint that checks whether an item is equal to any of the supplied values. The name `ItemFrom` is confusing — it sounds like it tests membership in a collection, but it actually tests equality against a set of arguments. We want to rename this public API to `Is.AnyOf(...)` so the intent is clearer.

Additionally, `Is.ItemFrom` has an overload that accepts an `IEnumerable` directly. That overload is unnecessary (you can already pass an array via `params`) and it forces the constraint to be collection-like. Remove that `IEnumerable` overload so the user can only pass in values as individual arguments.

Make sure that:
- The new `Is.AnyOf(params object[])` method works identically to the old `Is.ItemFrom(params object[])` in every other way: equality comparison, support for `IgnoreCase`, and all the `Using(...)` modifier overloads should still be available and behave as before.
- The description message for the constraint should also be updated to reflect the new name — something like "any of <values>" rather than "item from …".
- The same change should be applied to the helper on `ConstraintExpression` (the fluent syntax) so that `Is.AnyOf(...)` and `Is.Not.AnyOf(...)` both work.
- No other constraints or unrelated behavior should be affected.

Rename to AnyOf, remove IEnumerable override


Enable nullable reference types in the NUnit test result implementations and related helpers

The public API for test results (ITestResult, TestResult, etc.) exposes string-valued members that are often null in practice — for example, Message and StackTrace are null for passing tests or before a test finishes. Downstream projects that have nullable reference types enabled therefore see warnings when consuming these members because they are typed as non-nullable string.

This request covers the main test result implementations (TestResult, TestCaseResult, TestSuiteResult) and the ExceptionHelper utility used to collect and record exception information.

Desired changes:
- Annotate Message, StackTrace, and the corresponding backing fields as nullable (string?).
- Make overloads of SetResult and RecordAssertion that accept a string accept nullable strings, so callers can pass null without warnings.
- In ExceptionHelper, allow RecordException to return Exception?.
- In FlattenExceptionHierarchy, use the null-forgiving operator after pattern‑matching checks (such as `if (exception is ReflectionTypeLoadException)`) so the compiler trusts the variable is non‑null inside the branch.
- Initialize _resultState directly (without going through the ResultState property) in the TestResult constructor to avoid nullable‑analysis warnings when the property might expect a non‑null value but we assign the Inconclusive state.
- Ensure that after enabling nullable in these files, there are no remaining nullability warnings, and the internal state remains consistent even when Message, StackTrace, or inner‑exception collections are null.

This will make it easier for users who have nullable enabled to consume NUnit without workarounds, and will align the implementation with the fact that those properties can and do return null.

Enable nullability for ITestResult implementations


When using `Does.ContainKey` on a plain object that has a `ContainsKey` method (or a generic `Contains<TKey>` method) but does _not_ implement `IEnumerable`, the constraint throws an `ArgumentException`:

```
Not a collection supporting ContainsKey method.
```

This happens even though the object does support checking whether a key is contained by providing a method with the expected duck‑typing signature.

**Expected behaviour:**
The `Does.ContainKey` constraint should perform duck‑typing on any object, not only on objects that implement `IEnumerable`. If the object has an accessible `ContainsKey` method (or a generic `Contains(TKey)` method with a single parameter whose type matches the key type used by the constraint), the constraint should call that method to determine whether the key is present.

When no suitable method is found, the error message should clearly indicate that the value must have a `ContainsKey` or `Contains(TKey)` method, instead of the current misleading message about a “collection”.

**Impact:**
Users cannot use the `ContainKey` operator with custom classes that offer key‑containment but are not enumerable collections, which contradicts the constraint’s duck‑typed design and limits its usefulness.


Add a public read-only property `TestContext.WorkerId` that returns the unique name of the worker running the current test.  The property must be available during test execution and must return the correct worker ID for child tests inside composite (suite-like) test items, not just for top-level tests.

Internally, store the worker ID on the `WorkItem` instead of on `TestExecutionContext`.  Set it from `TestWorker` before the work item is executed, so the value is available for propagation to child work items and for rehydration into the execution context.  Remove the fallback-lookup logic in `TestExecutionContext.WorkerId`; after this change the value shall always be copied from the `WorkItem` into the context before the test runs, making the prior-context fallback unnecessary.


As a user of the NUnit Engine API, I need a way to programmatically discover which runtime frameworks are available on the current machine. At the moment, `IRuntimeFrameworkService` lets me select a target framework for a test package, but there is no public property or method to list all installed runtimes. This makes it impossible to, for example, populate a UI dropdown or log the available environments before a run.

Please add a public property to the runtime framework service that returns a collection of available runtime frameworks. The collection should contain objects that describe each framework, including at minimum:

- its runtime type (e.g., .NET Framework, Mono, Silverlight, etc.)
- the framework version
- the corresponding CLR version
- the profile (e.g., “Client” or “Full”) when applicable, represented as a string
- a human-readable display name that clearly includes profile information when the profile is not the default “Full”

The `RuntimeType` enum and an interface describing an individual runtime framework (such as `IRuntimeFramework`) should be defined in the public API assembly (`nunit.engine.api`), because consumers of the engine must be able to reference them without depending on engine internals. The existing internal `RuntimeFramework` class should satisfy that interface.

The service itself should implement the new collection-exposing interface (e.g., `IAvailableRuntimes`) and return the frameworks it already knows about. The returned list must be non-empty on any machine with at least one .NET or Mono installation. The display name must be accurate for .NET 4.x variants where the Client profile differs from the default Full profile—currently the display name for those frameworks may be missing the profile suffix.

Add new interfaces to api and expose IAvailableRuntimes from RuntimeFrameworkService. Fixes #1283.


**Title: Inconsistent DBNull handling in test case arguments on .NET Standard 1.6**

### Description
When writing parameterized tests (e.g., using `[TestCase]`, `[Values]`, `[Random]`, `[Range]`, or `[TestCaseSource]`) that pass `DBNull.Value` as an argument, NUnit normally converts `DBNull` to `null` before invoking the test method. This conversion works correctly on .NET Framework and most .NET Core targets, but it **does not occur when the test project targets `netstandard1.6`**. As a result, the parameter receives the `DBNull` object itself instead of `null`, causing type-mismatch failures, assertion failures, or unexpected behavior in tests that rely on the null substitution.

### Expected behavior
Arguments of type `DBNull` (i.e., `System.DBNull`) should be converted to `null` on all supported target frameworks, including `netstandard1.6`. The behavior should be identical to what users see on full .NET and other .NET Core targets: any `DBNull` value provided as test case data is transparently replaced with `null`.

### Steps to reproduce (conceptual)
1. Create a test project targeting `netstandard1.6` that references NUnit 3.x.
2. Write a test using `[TestCase(DBNull.Value)]` where the test parameter is of type `object` (or a nullable type) and the test asserts the argument is `null`.
3. Run the test – on `netstandard1.6` the conversion is skipped, and the test fails because the parameter is non-null `DBNull` instead of `null`. The same test passes on other targets.

The same issue affects other parameterized data sources that use the same internal conversion path.

### Additional context
- The conversion is expected to work even in environments where the `DBNull` type may not be directly referenced at compile time (common in some `netstandard` profiles). It is acceptable to perform the check by comparing the type’s full name (`"System.DBNull"`) at runtime rather than relying on a compile-time `is` check, as long as the substitution is applied consistently.


Title: `IStructuralEquatable` types should use NUnit equality (with tolerance) before collection comparison

When NUnit compares two objects that both implement `IEnumerable`, it normally treats them as collections and compares them element-by-element. However, some .NET types (e.g., `ImmutableArray<T>`, arrays, and other value‑type collections) also implement `IStructuralEquatable`. For these types, structural equality lets the type itself delegate item comparisons to an `IEqualityComparer`—which is the right way to inject NUnit’s own equality comparers (including tolerance, custom comparers, etc.) into the comparison of their items.

The current behavior ignores `IStructuralEquatable` and forces the collection path, which means assertions like:

```csharp
var a = ImmutableArray.Create<int>(1);
var b = ImmutableArray.Create<double>(1.1);
Assert.That(a, Is.EqualTo(b).Within(0.5));
```

fail because the items are compared with default equality (int vs double) without applying NUnit’s tolerance.

### Desired behavior

- When NUnit compares two objects that both implement `IStructuralEquatable`, it should use a dedicated comparison step that calls `IStructuralEquatable.Equals` with an `IEqualityComparer` that wraps NUnit’s full equality pipeline (including tolerance). This step must appear earlier in the internal comparer chain than the generic collection comparison, so that structural equality takes priority over “enumerate and compare items one by one.”

- **Tolerance propagation**: the wrapper must pass tolerance by reference so that any changes (e.g., the default floating‑point tolerance applied by `[DefaultFloatingPointTolerance]`) are correctly reflected back to the original tolerance object.

- **Cross‑type structural comparisons** must work. For example, `ImmutableArray<int>` and `int[]` should be comparable when both implement `IStructuralEquatable` in a compatible way—even though one type may be “aware” of the other and the other is not. The implementation should try `IStructuralEquatable.Equals` in both directions.

- **Respect `AsCollection`** at the top level. When a user explicitly forces collection comparison (e.g., `Is.EqualTo(...).AsCollection`), the structural‑equality step must not interfere and should fall through to the normal collection comparison. This keeps the existing opt‑out mechanism intact.

### Additional constraints

- The new behaviour must not apply on frameworks that lack `IStructuralEquatable` (for NUnit that means net35 and possibly net40—the exact condition can be determined during implementation).
- If only one operand implements `IStructuralEquatable`, the step should return “don’t know” so the comparer chain can continue.
- The previous special‑casing that skipped certain `IStructuralEquatable` value‑type collections and forced them into collection comparison should be removed; the new structural‑equality step replaces it cleanly.

IStructuralEquatable takes priority over IEnumerable using NUnit Equality.