**Feature request: Emit password-change notifications for user management operations**

## Background
The Event Store exposes user management capabilities – create users, update profiles, change/reset passwords, disable/enable accounts, and delete users. Currently, there is no built‑in way for downstream consumers (auditing, monitoring, session invalidation, etc.) to observe that a user’s password has been altered. This change introduces a dedicated stream of such events.

## Acceptance criteria

1. **New notification stream**
   A well-known system stream exists, named `$users-password-notifications`, that carries events of type `$PasswordChanged`.

2. **When a password-change notification is written**
   A `$PasswordChanged` event MUST be appended to `$users-password-notifications` whenever any of the following user‑facing operations complete successfully:
   - A user **changes their own password** (`ChangePassword`).
   - An administrator **resets a password** (`ResetPassword`).
   - An administrator **disables** an account – because this revokes the validity of the current password.
   - An administrator **deletes** a user – because the password is removed.

3. **Event payload**
   Each `$PasswordChanged` event contains a JSON body with at least a `LoginName` field identifying the user whose password was affected. Other fields may be added later, but `LoginName` is the required minimum.

4. **Atomicity with the user operation**
   - The notification write must be attempted *before* the user’s own stream operation (update, disable, delete) is committed, and the overall command must not be reported as successful unless both steps succeed.
   - If the notification write fails (e.g., timeout, wrong expected version, access denied), the entire operation MUST fail without modifying the user’s stream, and an appropriate error (TryAgain, Unauthorized, Conflict, or Internal Error) MUST be returned.
   - Failed authentication attempts (e.g., wrong current password) must **not** produce a notification.

5. **Access to the notification stream**
   - The stream is written as the system account.
   - Access controls (stream ACL) must be set so that only authorized principals (by default, members of the `$admins` group) can read the stream.

6. **Idempotency**
   No special deduplication is required; consumers should treat the `LoginName` and wall‑clock time in the event metadata as sufficient to identify duplicate notifications caused by retries.

## Constraints and non‑goals
- Operations that do not affect a password (e.g., updating full name or groups) must **not** produce a `$PasswordChanged` event.
- The new stream is a user‑visible system stream; its existence and event schema become part of the public API for administrative consumers.

Added: writing $users-password-notification notifications when password changes


**Title:** Optimize PTable merge by using lightweight existence checks instead of full record reads

**Description:**
During index merges, when multiple PTables are combined, each candidate entry is validated to ensure it still references a committed record in the transaction log. Currently, this validation performs a full record read (reading the entire event record) for every entry, which is I/O‑intensive and slows merge operations—especially under heavy load. The merge process only needs to know *whether* the record exists (and, in the special case of an index version upgrade from 32‑bit to 64‑bit, the stream name). A cheaper existence check should be used where possible.

**Acceptance Criteria:**

1. When merging PTables (both through automatic merges triggered by `IndexMap.AddPTable` and explicit `PTable.MergeTo` calls), the system shall use a dedicated existence‑only check provided by the caller instead of always performing a full record read.
2. The existence‑only check shall be used for all entries **unless** the merged index is being upgraded from 32‑bit to 64‑bit **and** the entry’s source ptable is a 32‑bit table. In that case only, a full record read must be performed to obtain the stream name required to compute the upgraded 64‑bit hash.
3. The set of entries retained in the merged PTable must be identical to current behavior: any entry for which the existence check indicates the record is not present in the transaction log shall be omitted.
4. Merge performance shall observably improve, with reduced I/O and faster completion in environments with large index tables and high write throughput.
5. The implementation must not alter merge correctness for any ptable version combination (32‑bit only, 64‑bit only, mixed). In particular, upgrading hashes from 32‑bit to 64‑bit must still correctly compute and store the new hash.
6. The new existence check should be supplied by `TableIndex` using the transaction log reader’s `ExistsAt` method (or equivalent lightweight check), while the existing full‑read callback remains available for the upgrade scenario.

**Non‑functional constraints:**
- No regression in merge correctness; all existing merge behavior (including handling of missing entries and mixed versions) must be preserved.
- The change should have minimal impact on memory usage and introduce no additional synchronization overhead.


We need to support transaction demarcation in the LogV3 record format, but currently there is no way to write a transaction start or transaction end record. This prevents any client or internal process from delineating the boundaries of a transaction in the log, which breaks the ability to later reconstruct transactional state, enforce atomicity, or implement features like multipart transactions.

**Observed behavior**
- The `LogRecordType` enum (in `EventStore.LogCommon`) does not define entries for `TransactionStart` or `TransactionEnd`.
- The `Raw` structs in `EventStore.LogV3` provide sub-header definitions for other record types (`StreamWrite`, `Event`, etc.) but have no analogous definitions for transaction delimiters.
- `RecordCreator` (which provides a factory API for constructing records) offers no methods to create transaction start or end records.
- Any attempt to mark the start or end of a transaction cannot be persisted as a well-typed log record.

**Expected behavior**
- The log should support two new immutable record types: **TransactionStart** (opening a transaction) and **TransactionEnd** (closing a transaction).
- Each record must carry a fixed-size sub-header with all necessary metadata, defined as unmanaged structs with explicit layout.
  - **TransactionStart** sub-header must include:
    - `RecordCount` (uint32) – total number of records that will belong to this transaction, useful for rehydration.
    - `Status` (enum/byte) – the initial transaction status, with possible values: `Prepare`, `Committed`, `Failed`, `Removed`.
    - `Type` (enum/byte) – the transaction kind, at minimum distinguishing `StandardWrite` from `MultipartTransaction`.
  - **TransactionEnd** sub-header must include:
    - `RecordCount` (uint32) – the final number of records written, to verify completeness.
- The `LogRecordType` enum must be extended with `TransactionStart = 9` and `TransactionEnd = 10` to identify these records.
- `RecordCreator` must expose two new factory methods:
  - `CreateTransactionStartRecord` accepting `timeStamp`, `logPosition`, `transactionId` (mapped to the main header’s `RecordId`), `status`, `type`, and `recordCount`.
  - `CreateTransactionEndRecord` accepting `timeStamp`, `logPosition`, `transactionId`, and `recordCount`.
- Both records should have **zero payload length** (only the fixed header + sub-header) and use `LogRecordVersion.LogRecordV0` as the record version.
- The struct layouts must be verified to match their expected sizes: `TransactionStartHeader` = 6 bytes, `TransactionEndHeader` = 4 bytes (following the same explicit-layout/packed conventions used for existing sub-headers).

**Impact**
- Without these records, any user of LogV3 cannot represent transactions at all, which blocks higher-level features like client-initiated transactions, transactional write pipelines, and the ability to replay the log with transaction boundaries intact.
- The change should be entirely additive; existing record types and their layouts remain unchanged.
- Care must be taken that the `RecordId` field (the `Guid` in the main header) is populated with the transaction identifier, enabling correlation between start and end records during reads.


Add configuration toggles for anonymous access to certain HTTP endpoints and non-ACLed streams, with both defaults `true` to preserve existing behavior. Update the internal authorization provider factory to respect these flags, and adjust the authorization policy so that:
- `/ping`, `/info`, static UI content, and HTTP redirects always permit anonymous access.
- `/stats`, `/gossip`, and `HTTP OPTIONS` are governed by `AllowAnonymousEndpointAccess` (when `false`, require authentication).
- Stream read/write/delete/metadata-read/metadata-write operations are governed by `AllowAnonymousStreamAccess` (when `false`, require authentication even for streams without ACLs).
- Internal gossip reads (the cluster gossip endpoint) must always require a system identity; external client gossip reads remain subject to the endpoint toggle.

Expose both options via environment variable, YAML, and command line following existing naming conventions (`AllowAnonymousEndpointAccess`, `AllowAnonymousStreamAccess`). Wire the values through `ApplicationOptions` and into the `LegacyAuthorizationProviderFactory`. Update the `GossipController` registration to use the appropriate operation (`ClientRead`) so that internal gossip is correctly gated.

update authorization policy for EventStoreDB


**Title:** `MaxAge`-optimized read path mishandles hash collisions, duplicate event numbers, and ignores `SkipIndexScanOnRead`

**Background**
When a stream has `$maxAge` set, `IndexReader` takes an optimized code path that attempts to skip non‑viable events efficiently.  That path performs its own index lookups and prepare reads, but it does not share the deduplication/sorting logic used by the standard read path.

A number of edge cases have been observed that lead to incorrect results (wrong events returned, duplicates, wrong ordering, or `NextEventNumber`/`IsEndOfStream` miscalculations) **only** when the stream has `$maxAge`:

1. **Hash collisions** – Multiple stream names can hash to the same value.  The max‑age path does not consistently verify that the stream id of a read prepare actually matches the requested stream, allowing events from colliding streams to leak into the result.
2. **Duplicate event numbers** – A stream can legitimately have multiple index entries for the same event number (e.g., when 32‑bit and 64‑bit index tables coexist).  The standard path deduplicates these by preferring the entry with the lowest log position; the max‑age path does not, so duplicate events can appear in the output.
3. **`SkipIndexScanOnRead` flag ignored** – The setting that allows the reader to skip post‑processing (dedup + sort) when the index is known to have no duplicates and to already be in descending event‑number order is not honoured by the max‑age path.  This breaks the contract for clients that rely on that guarantee, and it causes the max‑age path to unnecessarily sort/dedup in scenarios that should be fast‑paths.

Additionally, the binary‑search logic inside the max‑age path can fail to terminate or produce wrong bounds when index entries are out of order (again possible with mixed bitness), and it may not correctly locate the first non‑expired event when a large range of events is expired.

**Expected behaviour**
- The max‑age path must **always** filter index entries by comparing the actual stream id of the resolved prepare with the requested stream id.
- It must deduplicate events with the same event number using the same rule as the standard path: keep the entry with the smallest log position.
- When `SkipIndexScanOnRead` is true, the max‑age path must trust the index ordering and absence of duplicates, just like the standard path.
- The binary‑search and early‑exit logic must remain correct (and terminate) in the presence of hash collisions and out‑of‑order entries, returning a valid `NextEventNumber` and `IsEndOfStream`.
- The `ReadStreamEventsForward` / `ReadStreamEventsBackward` results for max‑age streams must match the behaviour of non‑max‑age streams in all the above edge cases, including correct handling when metadata itself is invalid (the metadata should be ignored for event reads, which is already the case elsewhere but must remain so).

**Acceptance criteria**
- Existing max‑age tests continue to pass; additional tests covering hash collisions, duplicate event numbers, and `SkipIndexScanOnRead` with max‑age streams are provided.
- Reading a max‑age stream that shares a hash with another stream returns **only** events belonging to the requested stream.
- Duplicate event numbers are not returned more than once; the chosen event is the one with the smallest log position.
- When `SkipIndexScanOnRead` is enabled, max‑age reads avoid unnecessary sorting/dedup work.
- The binary‑search in the max‑age path handles expired events correctly when large ranges of events are aged out.

Make streams with MaxAge respect SkipIndexScanOnRead


## Request: Correct handling of `$causedBy` and `$correlationId` metadata on emitted events

### Background

Projections can emit new events into a stream using the `emit()` function. The runtime automatically captures the event that triggered the emission (the *causing event*). It is also possible to provide explicit metadata in the emit call. The emitted event should carry traceability metadata so that consumers can track causal relationships across streams.

### Expected behavior

1. **`$causedBy` is always present**
   Every event emitted by a projection must contain a metadata field `$causedBy` whose value is the `EventId` of the event that triggered the emission (i.e. the event that caused this emitted event to be produced).
   - If the projection code explicitly supplies a `$causedBy` in the emitted event’s metadata, that value **must be ignored** – the system-assigned causal ID always takes precedence.

2. **`$correlationId` propagates from the causing event**
   If the causing event’s own metadata contains a `$correlationId` field, that value should be automatically included in the emitted event’s metadata, unless the projection overrides it.

3. **Projection-provided `$correlationId` takes precedence**
   When the projection passes a `$correlationId` in the metadata of an `emit()` call, that value must appear in the final written event’s metadata. The propagated value from the causing event must **not** be added when the projection already provides its own.

4. **No duplicate or conflicting metadata keys**
   The emitted event’s metadata must not contain duplicate entries for `$causedBy` or `$correlationId`. Only one value for each should be present, following the precedence rules above.

5. **Non-`$`-prefixed metadata from the causing event is copied as before**
   Existing behavior for copying other source event metadata keys (those not starting with `$`) into emitted event metadata must remain unchanged.

### Constraints / edge cases

- If the causing event has no `$correlationId` and the projection does not provide one, `$correlationId` may be absent from the emitted event’s metadata (this is acceptable).
- If the causing event ID (`CausedBy`) is not available (e.g., synthetic projections), the `$causedBy` field may be omitted.
- The behavior must be consistent across all projections (system, user-defined, and prelude-based) and for events emitted both with and without explicit metadata.

### Observable effect

After this change, reading any event written by a projection should consistently expose the true causal event ID under `$causedBy` and the intended correlation id under `$correlationId`, with no overwrites or duplicates due to user-supplied metadata.

Added: basic causedBY & correlationId tests + fixes


**Title:** Re-authorize volatile subscriptions when the `$settings` stream is updated

**Background**
Our subscription service watches for writes to the per-stream metadata stream (`$$<stream>`) and re-checks access for the affected subscription. This ensures that changes like updating a stream ACL take effect immediately. However, changes to the **default** access control (the `$userStreamAcl` setting inside the `$settings` system stream) are ignored: existing subscriptions continue to deliver events even after read access is revoked globally.

**Request**
When an event is written to `$settings`, re-authorize **all** active volatile subscriptions. Specifically:

- Hook into the event-committed pipeline (`ProcessEventCommited`) in `SubscriptionsService` so that writes to `SystemStreams.SettingsStream` trigger a call to `Authorize` on every subscription across all topics.
- This should happen after the existing per-stream metadata check, and for both the historical and live event paths.
- `Authorize` should behave as it does today: if the subscription’s principal is no longer allowed to read the stream, the subscription must be dropped with `SubscriptionDropReason.AccessDenied`.

**Constraints and edge cases**
- The new trigger must not interfere with, or duplicate, the per-stream ACL logic (i.e., a single `$settings` write should not cause duplicate authorization checks for streams whose own metadata also changed).
- Subscription re-authorization must be applied to all stream types, including soft-deleted, truncated, and max-count streams, where the effective ACL is derived from the default.
- Ephemeral streams (e.g., `$mem-node-state`) are not expected to be affected by ACL changes, but re-authorizing them is harmless. No special guard is required; the existing authorization logic will handle them naturally.
- The change should only impact the volatile subscription service (`SubscriptionsService`); persistent subscriptions are out of scope.

**Expected behavior**
After an event with `$userStreamAcl` modifications is written to `$settings`, any volatile subscription whose access is now denied shall be terminated immediately with an `AccessDenied` reason, preventing further event leakage.

Re-authorize subscriptions when an event is written to the $settings stream


**Title:** Metrics label mappings: gracefully handle omitted Label or Regex in configuration

**Background**
The `MetricsConfiguration` contains `LabelMappingCase` entries (`Regex` / `Label`) for renaming queue names and message type labels in internal metrics. Currently, if a user supplies a `Regex` but omits the `Label` (e.g., leaves it `null` or empty), the system may throw a `NullReferenceException` without any clear explanation. Similarly, if the `Regex` is missing, the behavior is undefined and could cause unexpected matches.

**Requested change**
Make the label-mapping handling robust to these misconfigurations:

1. **Regex present, Label missing** (null, empty, or whitespace)
   - For message types: do not change the original label; log a warning indicating that a label was not specified for the matching message and pattern.
   - For queues: do not collect metrics for the matched queue; log a warning that the label is missing and that metrics will not be collected for this queue.
   In both cases, the mapping is treated as “configured” (the entry is consumed) but the label rewrite or tracker instantiation is skipped gracefully.

2. **Label present, Regex missing** (null, empty, or whitespace)
   - The mapping should not match any message type or queue name. No warning is required, but the entry must be harmless.
   - Concretely, the original label/queue name must remain unchanged, and no tracker should be created because of this entry.

3. **Property defaults**
   - Both `Regex` and `Label` should default to an empty string rather than null to prevent null-related errors in the matching logic itself.

**Affected components**
- `MessageLabelConfigurator` – the loop over `MessageTypes` configuration entries.
- `QueueTrackers` – the loop over `QueueLabels` configuration entries.
- The `LabelMappingCase` class in `MetricsConfiguration`.

**Acceptance criteria**
- When `Regex` is set but `Label` is blank, a warning is logged and no exception occurs.
- When `Label` is set but `Regex` is blank, the mapping is effectively ignored (no match).
- Existing valid mappings continue to work identically.
- No null-reference exceptions are possible from these configuration paths.

Handle the case where the user provides a regex but no label in the metricsconfig


Before the node shuts down for offline truncation (e.g., after a leader election), update the epoch checkpoint to point to the latest epoch record that is strictly before the truncation boundary.
This prevents a full sequential scan of the transaction log on the next startup, which currently occurs when the epoch checkpoint is invalid or ahead of the truncation point.

### Details

- Use the in‑memory epoch cache to locate an epoch record with `EpochPosition < truncationPosition`.
- If such a record exists, write its position to the epoch checkpoint and mark the checkpoint as truncated, preventing further reads/writes to it.
- If no suitable cached epoch exists, leave the checkpoint untouched (the existing scan‑on‑startup fallback remains).
- Perform the update before `RequestShutdown` is published, to ensure the checkpoint is correct when the node takes the database offline.
- The truncation of the checkpoint must be safe from concurrent access: once truncated, any attempt to read or write the checkpoint should throw.

Truncate the epoch checkpoint to the proper epoch position prior to offline truncation to avoid scanning the transaction log for the last epoch record


The `/streams/{stream}` endpoints should accept requests that include a trailing slash and permanently redirect the client to the canonical URL without the slash.

**Acceptance Criteria**

- When a client sends a GET, POST, or DELETE request to a URL that ends with `/streams/‹stream-name›/` (including any query string), the server SHALL respond with HTTP status `308 Permanent Redirect`.
- The response SHALL include a `Location` header whose value is the original request URL with exactly one change: the last character of the path component (the trailing slash) is removed. All other components—scheme, host, port, query string, and fragment—MUST remain unchanged.
- Requests that do not have a trailing slash SHALL NOT be affected and continue to be processed as before.
- The response body may be a short text description (e.g. “Moved Permanently”); it is acceptable to send an empty body.
- This behaviour applies to all HTTP methods currently recognised for a stream resource: GET, POST, and DELETE.
- No redirect loop must occur: the `Location` header MUST NOT itself contain a trailing slash.

Note: while the examples given involve the plain stream path, the behaviour is strictly scoped to `/streams/{stream}/`; other API resources (e.g. metadata or `$all`) are out of scope.

make /streams/foo/ 308 and return location /streams/foo


**Title:** Slave projection fails to report completion for streams with no result, causing the master projection to hang

**Description:**
When using parallel query projections (`SlaveQueryProcessingStrategy`), a slave projection is expected to send a `PartitionProcessingResult` message back to the master for every stream partition it has finished processing, regardless of whether that partition produced a result. Currently, if a stream is empty or the state handler yields a null result for the partition, the slave projection never sends the completion notification. This causes the master projection to wait indefinitely for the missing signal, effectively stalling the entire projection pipeline. The problem also manifests when processing multiple streams in sequence: only partitions with a non-null result trigger a completion message, so the master may never see completions for all expected partitions.

**Steps to reproduce:**
- Configure a projection that uses slave/parallel processing (e.g., a master projection that schedules partitions to slave readers).
- Point the projection at a stream that does not exist, or that contains no events matching the partition filter.
- Observe that no `PartitionProcessingResult` with a null `Result` is ever published for that partition, and the master does not receive a completion notification.
- Repeat with multiple streams; some completions are missing.

**Expected behavior:**
- For every partition for which the slave reader reaches the end of the stream, a `PartitionProcessingResult` message **must** be published back to the master. The `Result` property may be `null` when no projection result was produced; this is a valid signal that the partition is done but contributed no data.
- The standard `ResultWriter` (used in non-slave processing) should **not** attempt to emit a result event to the Event Store when the result body is `null`, avoiding unnecessary writes.

**Actual behavior:**
- Slave projections silently skip the completion message when the partition result is null, leaving the master hanging.
- This breaks any processing flow that depends on the master knowing about every partition’s completion, such as final aggregation or orderly shutdown of projection processing.

**Impact:**
Parallel projections become unreliable whenever a stream partition is empty or produces no result. The projection may hang indefinitely, block subsequent operations, and can lead to timeout errors or a perception that the system is non-responsive.

**Request:**
- Ensure that the slave projection’s EOF-path unconditionally publishes a `PartitionProcessingResult`, even when the result string is `null`.
- Guard the base `ResultWriter` so that a null result body does not cause a result event to be written. This keeps the storage clean while allowing the slave to signal completion.

Added: test son processing multiple streams by a slave projection


Add support for starting slave projections in the ProjectionManager:

- When `ProjectionManagementMessage.StartSlaveProjections` is received (carrying the master projection name and `SlaveProjectionDefinitions`), start the slave projections on the appropriate core queues according to the definitions and publish a `ProjectionManagementMessage.SlaveProjectionsStarted` response with the master projection name.

- Let `ParallelQueryProcessingStrategy.GetSlaveProjections()` return slave definitions instead of `null`. For now return one slave named "slave" requesting one instance per thread.

- Make the `SlaveProjectionDefinitions` constructor accept `params Definition[]` to simplify definition creation in callers.

- Fix the unimplemented `AssertLastEvent(string streamId, string data, string message = null)` helper in `TestFixtureWithExistingEvents` (currently throws `NotImplementedException`) so it can verify the last event data in a stream, aiding test setup for projection scenarios.

Added: failing test for projection manager start slave projections


**Feature Request: Reusable sliding-window max tracker**

Currently, `DurationMaxTracker` provides sliding-window maximum tracking for duration values (floating-point seconds) with bucketing logic based on an expected scrape interval. This logic is useful for other metrics that are not durations—for example, queue lengths, retry counts, or any numeric value that can spike. However, the implementation is coupled to `double` and to duration-specific semantics.

**Goal**
Extract the general sliding-window maximum tracking logic into a reusable, generic component. The existing `DurationMaxTracker` must retain identical observable behavior while delegating to the new component.

**Acceptance Criteria**

1. **Generic windowed max tracker exists** – a new type is available that tracks the maximum of any `T` (where `T` is comparable) over a recent, configurable sliding time window.
2. **Identical behavior for DurationMaxTracker** – all existing uses of `DurationMaxTracker` produce exactly the same measurements as before, including:
   - The recorded max value, the time window range, and the tags attached to the metric.
   - The handling of stale data: observations ignore records older than the window.
   - The handling of concurrent access: one thread may record while another observes, without lost updates or torn reads, but concurrent recorders are not supported.
3. **Configurable scrape interval** – the generic tracker accepts an expected scrape interval (seconds), using the same set of allowed values: 0, 1, 5, 10, or multiples of 15. Invalid intervals cause an `ArgumentException`.
4. **Bucketing rules preserved** – the window’s duration and sub‑bucket sizing follow the same rules as today:
   - The window is slightly longer than the expected scrape interval, so that spikes are unlikely to be missed even if scraping is late.
   - The observed window range (min–max seconds) is exposed and matches the existing calculation.
5. **Observable API** – the generic tracker exposes a method to record a value at a given instant and a method to observe the maximum currently in the window.
6. **Default values after reset** – buckets that have been reset contribute `default(T)` (e.g., `0.0` for durations, `0` for integers), ensuring a sensible lower bound when no records are present.

**Out of scope**
- Changing the set of allowed scrape intervals or the bucketing heuristics.
- Supporting multiple concurrent recording threads.


In the persistent subscription implementation, there is a race condition when reading the stream reaches the end and a live event is received before the read completion is handled.

When `HandleReadCompleted` processes a read that returns `isEndOfStream == true`, the subscription immediately calls `SetLive()` and transitions the stream buffer to live mode. If `NotifyLiveSubscriptionMessage` delivered a live event after the read began but before this completion callback executed, that event ends up in the stream buffer’s live buffer but is effectively skipped — the subscription never pulls it as a stream read, and subscribers joining later will never see it.

Adjust the logic so that after receiving an end-of-stream read result, the subscription checks whether any live events have already accumulated in the stream buffer. Only transition to live mode if the buffer is empty; otherwise, continue reading from the new position to fetch the missed events.

Implementation requirements:

- In `StreamBuffer`, change the existing `MoveToLive` method (or add a replacement) so it returns a boolean indicating whether the live buffer was empty at the time of the call. Set `Live = true` only when the live buffer is empty; if it contains any outstanding messages, leave `Live` unchanged and return `false`.

- In `PersistentSubscription.HandleReadCompleted`, after reading events and when `isEndOfStream` is true:
  - Call the new `TryMoveToLive` (or equivalent) on the stream buffer.
  - If it returns `true`, perform the existing `SetLive()` and return.
  - If it returns `false`, do **not** set live. Instead, update `_nextEventToPullFrom` to `newposition` and call `TryReadingNewBatch()` so the subscription reads the stream again, this time picking up the event(s) that arrived during the read window.

The existing on-disk checkpoint flow, client push logic, and retry/park behavior must remain unchanged.

Persistent subscription bug fix: when reaching end of stream and a live event comes in before the read is handled, the stream should be read again


As a user of the `VNodeBuilder` API, I want to configure the gRPC keep-alive interval and timeout for a cluster node so that these timeouts can be used when establishing or maintaining gRPC connections.

**Acceptance Criteria:**

1. The builder must expose two new configuration methods:
   - `WithKeepAliveInterval(TimeSpan keepAliveInterval)`
   - `WithKeepAliveTimeout(TimeSpan keepAliveTimeout)`

2. When a node is built, the `ClusterVNodeSettings` produced by the builder must contain the configured values as read‑only `TimeSpan` properties named `KeepAliveInterval` and `KeepAliveTimeout`.

3. Attempting to build a node with a keep-alive interval or timeout less than or equal to `TimeSpan.Zero` must throw an `ArgumentOutOfRangeException` (or equivalent) to prevent invalid configurations.

4. The default values for `KeepAliveInterval` and `KeepAliveTimeout` (when the client does not call the new methods) may be kept at `TimeSpan.Zero` initially; no implicit default is required beyond the zero‑value rejection, so any caller who relies on the settings must ensure they are explicitly set.

5. The existing builder methods and `ClusterVNodeSettings` constructor must be extended to accept and pass through these two values. No other external behavior, endpoints, or protocols should change.

This extension follows the same pattern as other time‑span settings already present in `VNodeBuilder` (e.g., `WithGossipInterval`, `WithExternalHeartbeatTimeout`).


**Bug Report: PTable midpoint calculation lacks invariant tests – potential for incorrect index file sizing**

**Observed behavior**
The `PTableConstruction` class computes required midpoint counts and file sizes when building index files, particularly for version 4 (IndexV4), but there are no tests that verify the outputs respect fundamental invariants. This leaves the behaviour unchecked, and any regression in the calculation could silently produce an invalid or suboptimal index file.

**Expected behavior**
The following invariants must hold for all valid `numIndexEntries` and `minDepth` parameters:

1. **File size monotonicity with midpoint insertion**
   After adding midpoints, the total file size must never be smaller than the file size up to the index entries alone. For versions that do not support midpoints (IndexV1–V3), the file sizes should be equal.

2. **Midpoint count monotonicity**
   For a fixed `minDepth`, as the number of index entries increases, the required midpoint count should never decrease.
   For a fixed number of index entries, as `minDepth` increases, the required midpoint count should never decrease.

3. **Midpoint count bounds**
   The required midpoint count must lie within a sensible range: at least `Math.Min(1 << minDepth, numIndexEntries)` (the depth cap), but no more than `Math.Max(2, numIndexEntries)`. For versions without midpoints the count must be zero.

**Impact**
Without these checks, a defect in the midpoint calculation could lead to:
- Incorrect file allocation, potentially wasting disk space or causing I/O errors.
- A midpoint count that does not guarantee the expected search depth, degrading index lookup performance.
- Hard-to-diagnose failures when indexes are built with large datasets or non-default depth configurations.

**Request**
Add comprehensive unit tests for the midpoint calculation logic that validate the invariants above across a wide spectrum of `numIndexEntries` (from zero up to very large values, using a non-linear stepping to cover boundary regions) and `minDepth` values (from 0 to a representative max depth like 35). Include coverage for all supported PTable versions, ensuring version-specific behaviour (midpoint-less vs IndexV4) is respected. The tested methods are `GetRequiredMidpointCountCached`, `GetFileSizeUpToIndexEntries`, and `GetFileSizeUpToMidpointEntries`.

Add tests for midpoint calculation assertions


We need TFChunk to support an advisory lock that prevents immediate file deletion while the chunk is still being read by an external component (for example, when a replica is still catching up from it). The lock should be reference‑counted so multiple readers can coordinate independently.

**Acceptance Criteria**

1. **Lock acquisition while the chunk is alive**
   - If the chunk has not yet entered its self‑destruction path, a caller can acquire a lock.
   - The lock is reference‑counted; each acquisition must be balanced by a release.

2. **Deletion is deferred while any lock is held**
   - When `MarkForDeletion` is called while a lock is held, the chunk must not delete the underlying file. Instead, the file survives until the last lock is released.
   - Once the last lock is released and deletion was already requested, the file and all internal resources should be cleaned up immediately (the same destruct path as if no lock were held).

3. **Post‑lock deletion proceeds without further action**
   - If all locks are released after `MarkForDeletion` was called, the chunk must attempt to delete the file automatically—no additional explicit call is required.

4. **Lock acquisition during self‑destruction must fail**
   - Once the chunk has moved into its self‑destruction routine (i.e., it has started disposing streams and is about to delete the file), any attempt to acquire a lock must throw an appropriate exception (`FileBeingDeletedException`) to signal that the chunk is no longer safe to use.

5. **Thread‑safety**
   - All lock operations (`AcquireLock` / `ReleaseLock`) must be safe for concurrent use, and the interaction with the existing destruct timer/dispose logic must remain correct under concurrent access.

add locking to tfchunk. This is probably not how we want it to work for


**Bug Report: Cluster-internal HTTP endpoints (gossip update, elections) are open to anonymous callers**

**Observed behavior**
Currently, the operations listed below are allowed for anonymous requests on the HTTP interface:
- `Operations.Node.Gossip.Update`
- `Operations.Node.Elections.Prepare`
- `Operations.Node.Elections.PrepareOk`
- `Operations.Node.Elections.ViewChange`
- `Operations.Node.Elections.ViewChangeProof`
- `Operations.Node.Elections.Proposal`
- `Operations.Node.Elections.Accept`
- `Operations.Node.Elections.LeaderIsResigning`
- `Operations.Node.Elections.LeaderIsResigningOk`

Any client that can reach the HTTP port can trigger gossip state updates or participate in elections without any authentication. This bypasses the intended cluster security model and could allow unauthorized interference with cluster membership and leader election.

**Expected behavior**
These operations should only be available to properly authenticated cluster nodes. The authentication should be based on the client certificate presented during the TLS handshake using the existing `TrustedRootCerts` configuration. If a request presents a valid certificate whose chain roots to one of the configured trusted certificates, the connection should be treated as the **system user**. Only requests authenticated as the system user should be authorized to perform the operations above. All other users (admins, ops, regular users, and anonymous) must be denied.

Additionally:
- `Operations.Node.Gossip.Read` and all other node operations that were already anonymous should remain anonymous.
- HTTP authentication should gain a new provider that checks the client certificate against the trusted root CAs and, on success, sets the user to `SystemAccounts.System`. If no certificate is presented, or the certificate fails validation, the provider must not authenticate the request (the request remains unauthenticated for the purpose of authorization).
- The authorization policy must be updated to require the system user claims for the election and gossip update operations, replacing their previous anonymous access.

**Impact**
Without this change, any network entity with access to the EventStore HTTP port can inject gossip updates or participate in elections, undermining cluster integrity and potentially causing split-brain scenarios or denial of service. The fix ensures only nodes that possess a certificate from the mutually trusted CA can execute these privileged cluster-internal operations, consistent with the trust model established for internal TCP communication.

Authenticate other nodes as system users


## Bug: No trailing‑slash redirect for POST to a metadata stream

Clients that inadvertently send a `POST` to a metadata stream URI with a trailing slash (e.g.
`/streams/myevents/metadata/`) receive an HTTP error such as `404 Not Found` or `405 Method Not Allowed`.
All other stream actions already implement a consistent *308 Permanent Redirect* from a trailing‑slash URL to the canonical form without the trailing slash. Metadata streams should behave in the same way.

### Steps to Reproduce
1. Send a `POST` request to `/streams/{stream}/metadata/` with a valid event body.
2. Observe the HTTP status code and response headers.

### Expected Behaviour
- The server **must** respond with status `308 Moved Permanently` (not `301`, not `404`).
- The `Location` header **must** point to the same path **without** the trailing slash:
  `/streams/{stream}/metadata`.
- The client should then re‑issue the `POST` to the `Location` URL to complete the write.

### Actual Behaviour
- The request is not recognised as a valid metadata endpoint; the server returns `404` or `405`.
- No redirect header is sent, so automated clients cannot recover transparently.

### Impact
- Inconsistent handling across the HTTP API – regular event streams, get/delete operations, and even `GET` on a metadata stream already perform the trailing‑slash redirect.
- Buggy or poorly‑formed clients that append a slash fail noisily instead of being automatically corrected.
- A simple mistake blocks writes to metadata, harming integrations and developer experience.

### Additional Context / Safety Constraints
- The redirect must be **308** (Permanent Redirect) so that the HTTP method is preserved (clients will re‑send the `POST` body).
- The existing handler for the canonical metadata POST (`/streams/{stream}/metadata`) must remain unchanged.
- No other verbs (e.g. `GET`, `DELETE`) need to change – they are already handled separately. This issue is specifically about the missing `POST` redirect for metadata streams.
- The implementation should reuse the same redirect logic used by the other stream endpoints (the internal `PermRedirect` handler).

make /streams/foo/metadata/ 308 to /streams/foo/metadata


### Bug Report: PositionTagger accepts mismatched checkpoint tag phases

**Observed behavior**
`PositionTagger` subclasses (`StreamPositionTagger`, `PreparePositionTagger`, `TransactionFilePositionTagger`, `MultiStreamPositionTagger`, `EventByTypeIndexPositionTagger`) do not validate that the `CheckpointTag` passed to them has the same `Phase` as the tagger itself. This allows operations like `MakeCheckpointTag` and `AdjustTag` to silently accept tags from a different projection phase, leading to corrupted internal state or incorrect checkpoint progression.

**Expected behavior**
- `MakeCheckpointTag(previous, ...)` and `AdjustTag(tag)` must **reject** any tag whose `Phase` does not match the tagger’s `Phase` by throwing an `ArgumentException` with a clear description of the phase mismatch.
- `IsMessageAfterCheckpointTag(previous, committedEvent)` should treat a `previous` tag with a **lower** phase as logically before the current phase: return `true` (the message is after the checkpoint) without inspecting the positional values. This allows correct progression when a projection transitions between phases and encounters a checkpoint tag from an earlier phase.

**Context**
`PositionTagger` instances are associated with a specific projection phase (`Phase` property, integer). `CheckpointTag` also carries a `Phase`. These tags are passed between components during projection processing. When phases are mismatched, important comparisons become invalid and can silently produce wrong checkpoint placements, often manifesting as stuck projections or missed events.

**Impact**
Without this validation, projection pipelines can write incorrect phase-contained tags, causing permanent corruption of checkpoint data. Debugging such issues is extremely difficult because the problem appears indirectly as missing or repeated event deliveries.

**Proposed fix**
Add phase‑consistency guards to every `PositionTagger` implementation:
- In `MakeCheckpointTag` and `AdjustTag`, throw `ArgumentException` if `previous.Phase != Phase` (or `tag.Phase != Phase`).
- In `IsMessageAfterCheckpointTag`, return `true` immediately when `previous.Phase < Phase`.
All tagger implementations need to be updated consistently.

Changed: positionTagger accepts only tags with the same Phase


The `AllSubscriptionFiltered` enumerator stops emitting `Checkpoint` responses after the subscription transitions to the live phase. Clients expect checkpoints to continue being sent at the configured `checkpointInterval` as long as the subscription is alive.

During catch-up, the checkpoint response is constructed using a stale position variable (the page start) rather than the actual position carried by the `ClientMessage.CheckpointReached` message. Once the subscription goes live this misbehaviour appears to prevent any further checkpoint from being written to the output channel. Additionally, a `CheckpointReached` with a null `Position` should cause the enumerator to terminate with an error instead of silently proceeding.

Ensure that:

- After `GoLive` is entered, checkpoints triggered by newly appended events are forwarded and output exactly as they were during catch-up.
- The `CommitPosition`/`PreparePosition` in the emitted `ReadResp.Checkpoint` always reflects the position from the `CheckpointReached` message, not a captured offset.
- A null position in `CheckpointReached` is treated as a fatal error that completes the channel with a descriptive exception.

Prevent checkpoint from no longer being updated when the subscription becomes live


**Title:** Stamp shared tool metadata onto Embedded UI projection definition writes

**Context**
The KurrentDB Embedded UI (Blazor) currently creates and updates projections without tagging the writes with the standard projection tool‑metadata convention. Other Kurrent tools (gaffer, CI, Navigator) rely on this metadata to determine that a projection was authored via the UI. The gRPC front‑ends already stamp this metadata on `$ProjectionUpdated` events; the engine has existing support for an opaque `metadata` blob on `Post` and `UpdateQuery` commands (see #5638). No engine‑side changes are needed.

**Request**
Update the UI’s projection write paths to stamp the shared tool metadata onto every definition‑create and query‑update operation. Specifically:

- **Which operations** – Only `CreateAsync` and `UpdateQueryAsync` in `ProjectionsService`. All lifecycle/config writes (enable, disable, reset, delete, config update) must remain **unstamped**.
- **Wire format** – The metadata must be a protobuf `Struct` (the same shape used by the gRPC `ToMetadata` helper). Serialise it to a byte array and pass it as the existing `metadata` argument of the `Post` and `UpdateQuery` commands.
- **Metadata keys** (flat, top‑level, no `$`‑prefix – `$` is the server’s reserved namespace):
  - `"tool"` → constant string `"KurrentDB Embedded UI"`
  - `"tool_version"` → current product version (e.g. via `VersionInfo.Version`)
  - `"operation"` → `"create"` for create, `"update"` for update
  - `"actor"` → the acting identity from the `ClaimsPrincipal` (e.g. `principal.Identity.Name`)
- **Actor handling** – The `"actor"` key must be **omitted entirely** when the principal is unauthenticated/anonymous. Do not include it with an empty string.
- **Key ordering** – no specific requirement; tests will likely assert the **closed set** of keys (`"tool"`, `"tool_version"`, `"operation"`, and `"actor"` when present) but not insert gratuitous extras like `"revision"` (not applicable to UI edits).

**Acceptance Criteria**
- Creating a projection via the UI stamps the metadata on the outgoing command.
- Updating a query stamps the metadata with operation `"update"`.
- When a user is authenticated, the `"actor"` key is present with the correct name.
- When anonymous, the `"actor"` key is absent.
- Lifecycle/config commands remain unaffected.
- All keys are flat and do not start with `$`.

Stamp tool metadata on embedded UI projection writes (#5654)


**Title:** Record scavenge configuration on start and report accurate completion stats for interrupted scavenges.

**Description**

When a scavenge is initiated, the `ScavengeStarted` event written to the scavenge stream should record the scavenge parameters that were used for that run: `alwaysKeepScavenged`, `mergeChunks`, `startFromChunk`, and `threads`. This allows operators to reconstruct what settings were applied.

When a scavenge completes—whether normally or because the node was restarted—the `ScavengeCompleted` event must reflect the actual work performed. This includes:
- The total `spaceSaved` accumulated from all executed steps.
- A new `maxChunkScavenged` field indicating the highest chunk end number that was processed (useful for knowing how far the scavenge progressed).
- The total `timeTaken` from executed steps.

**Acceptance criteria**

1. **Start event enrichment**
   Every `ScavengeStarted` event in the `$scavenges-<scavengeId>` stream includes the fields `alwaysKeepScavenged`, `mergeChunks`, `startFromChunk`, and `threads` corresponding to the runtime configuration at start.

2. **Normal completion**
   When a scavenge finishes successfully, the `ScavengeCompleted` event contains:
   - `spaceSaved` equal to the total space saved over the entire run.
   - `maxChunkScavenged` equal to the largest chunk end number that was scavenged (or merged/indexed, whichever is latest in the processing order).

3. **Handling of interrupted scavenges after restart**
   After a node restart, any scavenge that was in progress (i.e., has a `ScavengeStarted` event but no `ScavengeCompleted` event) must be finalized with a `ScavengeCompleted` event that accurately aggregates:
   - `spaceSaved` – sum of `spaceSaved` from all `ScavengeChunksCompleted`, `ScavengeMergeCompleted`, and `ScavengeIndexCompleted` events that were already written.
   - `timeTaken` – sum of the `timeTaken` values from those same events.
   - `maxChunkScavenged` – the highest `chunkEndNumber` found in any `ScavengeChunksCompleted` event.

   The event’s `result` remains `Failed` with an error message indicating the node was restarted, but the stats must reflect partial progress rather than zeros/no `maxChunkScavenged`.

4. **Consistency across threads**
   The aggregations must be correct even when chunk processing is multi‑threaded and multiple `ChunksScavenged`/`ChunksNotScavenged` calls occur concurrently.

**Context**
The scavenge log events are written to per‑scavenge streams (`$scavenges-<id>`). Administrative tooling and the scavenge history UI rely on these events to display run details and outcomes. Without the changes, interrupted runs appear with zero progress and no indication of how far they got, making monitoring and debugging difficult.


Add synchronous and asynchronous `ReadEvent` methods to the ClientAPI. These methods accept a stream name, an event number (`-1` for the latest event in the stream), and a `resolveLinkTos` flag, and return a result object rather than throwing on non‑success statuses. The result object exposes the stream name, the requested event number, a status enum (`Success`, `NotFound`, `NoStream`, `StreamDeleted`), and the resolved event (only when status is `Success`). Implement the operation on both node and cluster connections, and include the new members in the `IEventStoreConnection` interface. Refactor the internal `GetStreamMetadataAsRawBytesAsync` to use the new read‑event operation instead of constructing a lower‑level operation directly.

Added: single event read operation to ClientAPI.


As a Ukrainian LanguageTool user, I expect the tokenizer to split apostrophes and dashes correctly, especially when they appear next to opening punctuation or quotation marks. The current tokenization often attaches them to adjacent words, which breaks downstream spelling and grammar checks.

Observed issues and desired behavior:

1. Apostrophe after an opening parenthesis or quotation mark should become a separate token rather than joining the next word.
   Example: `(‘дзеркало’)` should tokenize as: `[‘(’, ‘'’, ‘дзеркало’, ‘'’, ‘)’]`.

2. When a word ending with a quotation mark is followed by a hyphenated suffix, the quotation mark and the hyphen should all be separate tokens.
   Example: `«краб»-переросток` → `[‘«’, ‘краб’, ‘»’, ‘-’, ‘переросток’]`.

3. Double hyphens (`--`) that join two words should be split as a standalone punctuation token.
   Example: `вересні--жовтні` → `[‘вересні’, ‘--’, ‘жовтні’]`.

4. Mid-word apostrophes like in `Кан’-Ка` must remain a single token (the two parts still joined). Existing boundary cases—for example distinguishing `'дно` (as in `все 'дно піду`) from a true beginning apostrophe—must continue to work.

The fix should live in the Ukrainian tokenizer module and must not alter the tokenization of other languages or unrelated Ukrainian constructs.


**Title:** Spellchecker should suggest splitting camelCase words (e.g., `GranElefant` → `Gran Elefant`)

**Reported by:** User/Contributor
**Affects:** Catalan, Spanish, French spellcheckers (MorfologikSpellerRule‑based)

### Problem
When a word is accidentally typed in camelCase—where a lowercase part is followed by an uppercase letter—the spellchecker correctly flags it as misspelled, but offers **no suggestion to split the word** into two separate words.

Examples:
- Catalan: `GranElefant` → no split suggestion
- Spanish: `BretañaItinerante` → no split suggestion
- French: `BretagneItinéraire` → no split suggestion

### Expected Behavior
For any word that can be split into two valid dictionary‑recognized parts at an uppercase‑to‑lowercase boundary, the spellchecker should suggest the two words separated by a space.

### Context & Constraints
LanguageTool already provides comparable split‑suggestion logic for other patterns (e.g., words glued to numbers, apostrophe contractions, prefix‑word splits). This request extends that behavior to camelCase words.

The suggestion should only appear when **both resulting parts** are recognized as valid words (e.g., via POS‑tag validation). The fallback to other existing split rules should be preserved.

### Impact
Improves user experience for Catalan, Spanish, and French by catching a common typing/formatting mistake and providing a direct, high‑quality correction.


**Title:** German tagger: assign POS tags to adjective-derived adverbs ending in "-erweise" (e.g. "idealerweise")

**Description**
In the current German tagger, words like *idealerweise*, *lustigerweise*, and similar adverbs formed by appending *-erweise* to an adjective stem are left with **no POS tags**. This breaks downstream analysis because the tokens appear completely unknown. They should be tagged as adjectives, in line with the base adjective that provides the stem.

**Expected behaviour**
When the tagger encounters a word that:
- ends with `-erweise`, and
- the stem (the part before `-erweise`) is a **known adjective** in the dictionary,

it must assign a full set of adjective POS tags to the whole word. The exact set of adjective tags should be analogous to how a regular adjective is tagged in all relevant forms (nominative, accusative, singular/plural, definiteness, predicative, etc.).

**Safety / non-goals**
- Words that merely happen to end with `-weise` (or `-erweise`) but whose stem is **not** an adjective (e.g. *eimerweise*, *meterweise*, *literweise*, *blätterweise*) must **not** be tagged as adjectives.
- Verbal forms such as *überweise*, *verweise* must **not** be affected.
- *erweise* alone (if it ever appears) should not be tagged by this rule.
- The fix must not change the handling of words that are already successfully tagged through existing dictionary lookup or other decomposition logic.

**Context**
This bug is limited to the German language module (`languagetool-language-modules/de`). The existing `GermanTagger` already has fallback logic for unknown words; the requested behaviour should be integrated there, after the dictionary lookup fails but before other generic decomposition steps, so that it takes priority for this specific pattern.

[de] tag words like "idealerweise" which had no tags so far


**Title:** `[de-AT]` OldSpellingRule: false positives for „Geschoß“ and related forms

**Description:**
In Austrian German (language variant `de-AT`), the spelling *Geschoß* (with ß) is correct and widely used – it is **not** considered old spelling. This is due to a different pronunciation of the vowel (long instead of short), which means the `ß` after a long vowel is orthographically correct and the word was never changed to *Geschoss* in Austria.

Currently the `OldSpellingRule` (German module) treats *Geschoß* as an old-spelling form and suggests the new-spelling equivalent *Geschoss*, as it does for all other `ss`/`ß` replacements. This produces false-positive rule matches when checking Austrian texts.

**Expected behaviour:**
- For the language variant `de-AT`, the rule should **not** flag *Geschoß*, *Erdgeschoß*, inflected forms like *Geschoßes*/*Erdgeschoßes*, and any other word containing `geschoß` (case-insensitive).
- For other German variants (e.g. standard `de-DE`), the rule **should** continue to flag these words as old spelling and suggest the correct new spelling (e.g. *Geschoss* etc.).

**Notes:**
The rule currently does not distinguish language variants. The Austrian special case only applies to the stem *geschoß*; all other `ß`/`ss` replacements (e.g. *daß → dass*, *Schloß → Schloss*) must still produce matches in `de-AT`.


Improve Dutch handling of compound words formed from a directional prefix and a geographical name.

Currently, the Dutch proofing module incorrectly handles compounds that start with a geographical-direction prefix followed by a hyphen, such as “Noord-Afghanistan” (North Afghanistan), “Zuidoost-Turkije” (Southeast Turkey), or “West-Bergambacht”. These should be accepted as valid words and assigned the proper part-of-speech tag of a location, but they are either rejected as unknown or tagged as generic nouns.

There is a finite set of recognised directional prefixes in Dutch:

- Noord-
- Oost-
- Zuid-
- West-
- Noordoost-
- Noord-Oost-
- Zuidoost-
- Zuid-Oost-
- Noordwest-
- Noord-West-
- Zuidwest-
- Zuid-West-

A compound formed with one of these prefixes and a second element should be treated as geographically meaningful only when the second element itself is a known geographical location (country, city, region, etc.). For example, “Zuidwest-Gouda” is valid, but “Zuidwest-Frank” (where “Frank” is a personal name, not a location) should not be accepted as a compound word.

The fix must ensure:

1) The compound‑acceptance logic (Dutch module) recognises the full set of directional prefixes listed above and, for compounds where the first part ends with a hyphen and matches one of these prefixes, only accepts the compound if the second part is tagged as a location (the part-of-speech tag starts with “ENM:LOC”). Otherwise the compound is rejected as a spelling error.

2) The Dutch tagger must assign a compound like “Noord-Afghanistan” the proper proper‑noun location tag (e.g., the tag of the second part’s location reading) rather than falling back to a generic noun tag or leaving it untagged.

This change will reduce false spelling‑flagging for Dutch texts containing region or direction‑based compound geographical names while still catching invalid combinations.


**Title:** Dutch compound acceptor should accept words ending in common suffixes that always need an 's'

**Description:**

In Dutch, many compound nouns require an ‘s’ between the two parts (e.g., `bedrijfsvoering`, `dorpskerk`). Currently our `CompoundAcceptor` has a hard-coded set (`needsS`) of specific first parts that must end with ‘s’. This list is necessarily incomplete and causes many valid compounds to be rejected, especially those where the first part ends in a productive suffix like `-ing`, `-heid`, `-teit`, or `-schap`.

**Expected behavior:**

We want to generalize the acceptance logic so that if a compound’s first part ends with `s` and the base word (without the trailing `s`) ends with one of the following suffixes, the compound should be accepted, provided:

- the second part is a valid noun, and
- both the base word (without `s`) and the second part are spelled correctly.

The suffixes that *always* require an `s` in compounds:

- `heids`
- `ings`
- `teits`
- `schaps`

**Additional cleanup:**

Once the suffix-based rule is in place, remove entries from the `needsS` list that are now covered by these suffixes (e.g., words ending in `ings` or `heids`) to avoid duplication. Keep the remaining entries that are not covered by any suffix pattern (e.g., `bedrijfs`, `gevechts`, `jongens`). The existing behavior for those entries must continue to work exactly as before.

**Note:** The first part must already end with the letter `s` for this check to trigger, because the suffix-based rule only applies when the ‘s’ is present. The method should first check suffix membership, and only fall back to the `needsS` set for non-suffix cases.


```text
[nl] add vowel check to CompoundAcceptor

In Dutch compound checking, invalid compounds like "politieeenheid", "televisieeigenschappen", and "pyjamaaangelegenheid" are currently accepted. This happens because no rule prevents collisions between the final vowel of the first part and the initial vowel of the second part (e.g. "ie" + "e" → "iee", or "a" + "a" → "aa"). According to Dutch spelling rules, such vowel collisions are not allowed without a hyphen or diaeresis.

Please add a check to `CompoundAcceptor` that rejects a compound when the last character of `part1` and the first character of `part2` form one of the following vowel combinations:

aa, ae, ai, au, ee, ée, ei, éi, eu, éu, ie, ii, ij, oe, oi, oo, ou, ui, uu

While you are working on the `noS` word list used by the same class:

- Remove `"bestemmings"` from `noS` because it ends with "s", which is inconsistent with the intended meaning of the list (words that do not take an "s"-linking in compounds).
- Add `"pyjama"` to `noS` so that it can serve as a valid first part and be tested against the new vowel-collision check.

In the test class for `CompoundAcceptor`:

- Add assertions that `"politieeenheid"`, `"televisieeigenschappen"`, and `"pyjamaaangelegenheid"` are all rejected.
- Remove the existing assertions for `"kinderenHet"` and `"kindergaan"`; they are not related to the vowel check and are no longer needed.
```


**Title:** French speller falsely flags all‑caps hyphenated proper nouns and some multi‑word names as misspelled

**Description**
When checking French text, the speller incorrectly marks as errors:

- all‑caps hyphenated proper nouns such as `MÉTÉO-FRANCE`, `CLERMONT-FERRAND`, `MERCEDES-BENZ`
- multi‑word proper names listed in the global spelling exception file (`spelling_global.txt`), for example `Walt Disney Animation Studios`

These expressions are valid and should not produce any spelling rule matches.

The root cause for the hyphenated words appears to be twofold:
1. The fallback that tries to recognise unknown all‑caps words by looking up a capitalised variant fails for hyphenated forms. For `MÉTÉO-FRANCE` the current logic only capitalises the first character (`Météo-france`), which is not in the dictionary, whereas `Météo-France` is.
2. The French sentence analysis pipeline does not incorporate the global multi‑word exception list, so entries from `spelling_global.txt` are ignored.

**Acceptance criteria**
1. All‑caps hyphenated words whose title‑case form (each segment after a hyphen capitalised) appears in the French dictionary must not be flagged by the spelling rule.
   - Examples: `MÉTÉO-FRANCE`, `CLERMONT-FERRAND`, `MERCEDES-BENZ` → no spelling errors.
2. Multi‑word entries from `spelling_global.txt` must be recognised when checking French text and must not trigger spelling errors.
   - Example: `Walt Disney Animation Studios` → no spelling errors.
3. Existing handling of simple all‑caps words (e.g. `FRANCE`) must continue to work without regressions.
4. The fix must integrate naturally into the existing French disambiguation pipeline; no special‑casing outside the standard chunking/disambiguation flow.


Improve the German POS tagger to correctly analyze separable and non-separable prefix verbs and to reduce false-positive verb readings for nouns.

**Acceptance criteria / observable behavior**

- **Separable prefix verbs** (e.g. *nachgucken*, *herausfallen*, *herumgeben*, *rauslassen*)
  → The tagger must produce verb readings whose lemma includes the prefix.
  → **Finite forms** (1st/2nd/3rd person present, preterite, subjunctive) must receive a POS tag that distinguishes them from non-separable counterparts by appending **`:NEB`** (e.g. `VER:1:PLU:PRÄ:NON:NEB`).
  → **Infinitive, participles (PA1/PA2), and the extended infinitive with *zu* must not carry `:NEB`**.
  → **Imperative forms must not be emitted** for these verbs (German separable verbs do not form a prefixed imperative like *nachguck!*; they use *guck nach!*).

- **“zu”-infinitive for separable verbs** (e.g. *nachzugucken*)
  → Must be recognized as a single token and tagged as **`VER:EIZ`** (extended infinitive with *zu*), with the base verb lemma (e.g. *nachgucken*).
  → Must not be tagged as the infinitive of a non-existent verb *nachzugucken* or as a finite form.

- **Non-separable prefix verbs** (e.g. *vergären*, *empfinden*)
  → Must produce standard verb tags **without `:NEB`** and must not be mis-classified as modal/auxiliary unless the verb actually is one.

- **Nouns that start with a potential verb-prefix string** (e.g. *Geschichte*, *Bereich*, *Beispiel*, *Keksdose*, *Aktienarten*)
  → **Must not receive any verb readings**, even when morphological analysis would otherwise treat the substring as a prefixed verb.

- **Compound word handling**
  → When the first part of a compound looks like a separable prefix, verb readings derived from the compound analysis are allowed.
  → For other compounds, verb readings coming from that analysis must be suppressed to avoid false positives (e.g. *Aktienarten* being tagged as a verb).

- **Modal/auxiliary exclusion**
  → For prefixed verbs, readings that carry modal (`VER:MOD`) or auxiliary (`VER:AUX`) tags must be excluded.

- **Existing tests** for words like *herumgeben* and *herumgab* must be updated so they accept the `:NEB` suffix on finite forms while keeping the infinitive tag unchanged.


Enhance `Language.toAdvancedTypography` so that a non-breaking space is inserted after a solitary single-letter abbreviation (e.g., "v.", "s.") when it is immediately followed by a space and then a word. Currently only the case of two adjacent single-letter abbreviations is handled; single abbreviations can still break across lines.

Requirements:
- After a single-letter abbreviation with a trailing dot, replace the following space with a non-breaking space (U+00A0). Example: "v. haver" → "v.\u00a0haver".
- Ensure that a single quote appearing right after this new nbsp is correctly transformed into an opening single quote. Concretely, "v. 'haver'" should become "v.\u00a0‘haver’" (with the nbsp before the opening quote). The regex that detects opening single quotes must therefore accept U+00A0 and any thin space (U+202F) as valid preceding characters, alongside the existing set.

The behaviour must apply to all languages that enable advanced typography, not be limited to a single language override.

nbsp in advanced typography after abbreviation


When a paragraph contains a sentence that ends with a colon immediately followed by a URL (for example, “find it at: http://example.com”), the `PunctuationMarkAtParagraphEnd` rule reports incorrect character offsets (`fromPos`/`toPos`) for any matches that appear later in the same paragraph. This causes the error to be highlighted at the wrong position in the text. The offset error is proportional to the length of the colon‑URL sentence.

Acceptance criteria:
- When a paragraph includes a sentence matching the colon‑URL pattern, subsequent punctuation‑end‑of‑paragraph matches must have `fromPos` and `toPos` that correctly point to the intended text range, as if the skipped sentence were fully accounted for in the position tracking.
- The fix must not alter the rule’s decision to avoid flagging the colon‑URL sentence itself as an error.
- The position correction must apply even when multiple such sentences appear in the same paragraph, and regardless of whether the colon‑URL sentence is at the start, middle, or just before the final sentence.


The Crimean Tatar synthesizer currently creates a new instance per language object, while most other language synthesizers in the project follow a singleton pattern. This wastes memory and is inconsistent with the codebase conventions.

### Observed behavior
- `CrimeanTatar.createDefaultSynthesizer()` calls `new CrimeanTatarSynthesizer(this)`, which creates a new synthesizer each time.
- The `CrimeanTatarSynthesizer` constructor requires a `Language` argument, even though the synthesizer only needs the language code `"crh"` (the language is hard-coded and never varies).
- Tests construct the synthesizer with `new CrimeanTatarSynthesizer(new CrimeanTatar())`.

### Expected behavior
The Crimean Tatar synthesizer should:
- Expose a single shared instance (a `public static final` field, e.g., `INSTANCE`).
- Have a private no-arg constructor that passes the fixed resource file names and language code `"crh"` to the superclass, removing the `Language` parameter.
- `createDefaultSynthesizer()` should return that singleton instead of creating a new object.

### Impact
- Aligns with the pattern used by synthesizers in other language modules (e.g., Spanish, English, Polish), improving code consistency and thread‑safety.
- Eliminates unnecessary object allocation.
- The synthesizer must remain fully functional after the change (no regression in synthesis results).


**Title:** MorfologikSpellerRule should not flag tokens that contain no letters

**Description:**
Tokens without any letters (Unicode category `L`) are not spellable words, yet the Morfologik-based speller currently reports false spelling errors for them. This is observable across all language modules that use `MorfologikSpellerRule`.

**Acceptance criteria / expected behavior:**
- For any token that consists entirely of non-letter characters (e.g., pure emoji, symbol sequences, numbers with separating punctuation, superscript digits, etc.), the rule must produce **zero** spelling matches.
- Tokens that contain at least one letter should still be checked normally.
- The fix should be language‑agnostic and apply to all languages that rely on the shared Morfologik speller logic; there is no need to change individual language‑specific rules or dictionaries.

**Examples of tokens that should be silently ignored:**
- `🏽` (single emoji)
- `✈`
- `🧡🚴🏽♂`
- `5·10-³` (as part of a sentence like `"Fa 5·10-³ metres."`)
- Simple punctuation-only or number-only tokens that contain no letters.

**Why this matters:**
Users receiving false spelling alerts for emoji or technical notation erodes trust and adds noise, especially in texts that mix natural language with symbols. The speller’s scope should be limited to tokens that can meaningfully be considered words.

Morfologik: ignore tokens with no letters


We need to add a configurable port range for the server. Currently, setting `port=0` picks any available random port, but there's no way to restrict this to a contiguous range. That makes it hard to run LanguageTool behind a reverse proxy that needs a known range of ports, or in environments with strict firewall rules.

Add support for two new configuration properties in the server's properties file:
- `min_port` (integer, default 0)
- `max_port` (integer, default 0)

When `port` is set to `0`, the server should scan for a free port starting at `min_port` up to `max_port` (inclusive) and bind to the first available one. If no free port exists in the range, the server must fail to start with a clear error message. If `min_port` or `max_port` is non-positive or `min_port >= max_port`, the server should also fail to start with a meaningful message indicating an invalid port configuration. When `port` is non-zero the new properties are ignored.

The implementation should be entirely in the `languagetool-server` module (server startup and configuration parsing), consistent with the way other server options are handled.


**Title:** Memory optimization: reuse a single `EnglishTagger` instance

**Background**
The `EnglishTagger` loads a large dictionary file and is relatively heavy. In the current codebase, several components independently create their own `EnglishTagger` instances:

- `English.createDefaultTagger()` uses a `WeakReference`-based cache, but the instance can still be garbage-collected and re-created, defeating reliable sharing.
- `EnglishWordTokenizer` keeps a private `EnglishTagger` field initialized with `new EnglishTagger()`.
- `FindSuggestionsFilter` similarly creates its own instance.

Each of these loads the dictionary separately, leading to multiple copies in memory and unnecessary overhead, especially in server deployments where many `Language` objects may be active.

**Goal**
Ensure that all code that needs an `EnglishTagger` obtains the same shared instance, so the dictionary is loaded only once per JVM. The tagger is thread-safe and stateless per language, so sharing is safe.

**Acceptance criteria**
- Only one `EnglishTagger` instance exists in the runtime.
- All call sites (including `English`, `EnglishWordTokenizer`, `FindSuggestionsFilter`, and any other inadvertent direct instantiations) use that single instance.
- The `WeakReference`-based lazy initialization in `English.createDefaultTagger()` should be replaced with direct access to the shared instance.
- No regression in tagging behavior; all existing functionality must be preserved.

save memory by reusing EnglishTagger instance


**Feature Request: German rule to detect incorrect upper/lowercase using n-gram context**

Add a new German rule (ID `DE_UPPER_CASE_NGRAM`) that uses the n-gram language model to check whether certain words are wrongly capitalized and suggests the correct case. The rule should initially focus on a single test pair: `tage`/`Tage` and its inflected form `tagen`/`Tagen`.

The rule must produce the following observable behavior (when a language model is available and the rule is enabled):

*   **Correctly cased noun → no issue**:
    `"Nach 5 Tagen war es aus."`
    → the rule does **not** flag the token `Tagen`.

*   **Wrongly lowercased noun → flag and suggest uppercase**:
    `"Nach 5 tagen war es aus."`
    → when the n-gram model gives a much higher probability to the trigram `[5, Tagen, war]` compared to `[5, tagen, war]` (specifically, more than 50× higher), the rule flags `tagen` and suggests the spelling `Tagen`.

*   **Correctly lowercased verb → no issue**:
    `"Sie tagen im Hotel."`
    → the rule does **not** flag the token `tagen`.

*   **Wrongly uppercased verb → flag and suggest lowercase**:
    `"Sie Tagen im Hotel."`
    → when the n-gram model gives a much higher probability to `[Sie, tagen, im]` compared to `[Sie, Tagen, im]` (more than 50× higher), the rule flags `Tagen` and suggests the spelling `tagen`.

**Scope and constraints:**

*   Initially only the four surface forms `tage`, `tagen`, `Tage`, `Tagen` are checked; all other tokens must be left untouched.
*   Tokens that are entirely uppercase (e.g., `TAGEN`, `TAGE`) should never be flagged.
*   The rule uses the tri‑gram consisting of the **previous token, the current token, and the following token** to compute probabilities via `LanguageModel.getPseudoProbability()`.
*   The rule compares the probability of the original case against the probability with the case flipped (first character upper ↔ lower), and only fires if the less‑probable version’s probability is at least 50 times the more‑probable one’s.
*   The rule should be classified as a casing error and be **temporarily off by default** (so users must explicitly enable it).
*   It must be integrated into the list of rules that are activated when a language model is available for German.{
  "supervision_value": 3,
  "difficulty": 3,
  "reasoning": "The task requires adding a new rule class that hooks into the language model infrastructure, locates the German rules registration point, and implements a probability-driven casing check. This teaches a coherent mini-process (examine existing model-backed rules, understand Rule lifecycle, integrate a new rule, use LanguageModel, and apply casing logic). It's solid but bounded: limited to one word pair, one file modification, and one new file. The reasoning trail spans multiple repo areas but is not deep enough for a 4 or 5."
}

[de] initial version of ngram-power upper/lowercase rule, so far only for one pair (tage/Tage)


Title: [de] Avoid messed-up suggestions for case typos like "WUrzeln"

Description:
When German text contains a word that starts with two uppercase letters followed by lowercase letters (for example "WUrzeln" where the first two characters are uppercase and the rest lowercase), the rule that flags words that look incorrectly uppercased now incorrectly flags it and suggests a lowercased correction, which can produce a nonsense suggestion. These tokens are typing errors with mixed uppercase/lowercase and the spell checker already catches them. The rule should not intervene on such tokens.

The fix should skip any token that matches the pattern of a case typo: two initial uppercase characters (including Ä, Ö, Ü, ß) followed by lowercase letters, potentially with a hyphen later in the word. This avoids generating poor suggestions while still allowing the rule to flag genuine case errors (where the token is entirely uppercase or starts with a single uppercase letter).

[de] avoid messed-up suggestions - errors like "WUrzeln" are caught by spell checker (#3084)


**Implementation request**

In the Ukrainian `TokenAgreementVerbNounRule`, when a verb-noun agreement error is detected the rule produces suggestions. Currently those suggestions contain only the corrected noun form(s), but the error match span covers the verb token as well (the `RuleMatch` is anchored from the verb’s start position through the noun’s end position). Because the span includes the verb, the suggested replacements must also include the verb so that applying a suggestion replaces the whole marked phrase.

Adjust the suggestion construction so that every suggestion is prefixed with the verb token (e.g., for the phrase `перераховувати причин` the suggestions should be `перераховувати причинам`, `перераховувати причинами`, `перераховувати причини`, not just the noun forms alone). No other logic or detection changes are needed.


**Title:** Catalan tokenizer incorrectly merges 4-digit numbers followed by a space

**Description:**
When tokenizing Catalan text, a space that immediately follows a 4-digit number (such as a year, e.g. `2005 454`) is currently treated as a thousands separator and the adjacent number groups are merged into a single token. This breaks token boundaries for ordinary sequences like “el 2005 454 persones”.

**Expected behavior:**
- `2005 454` should tokenize as three tokens: `2005`, ` ` (space), `454`.
- The existing thousands‑separator grouping (e.g. `2005 57 114` tokenizing as `2005`, ` `, `57 114`) must remain unchanged.

**Scope/Constraints:**
- Only situations where a **4‑digit** number is immediately followed by a space should use a normal word boundary; the space must **not** cause digit‑group merging with anything that follows.
- No other number‑space patterns should be affected; in particular, separators that form valid groups of three (like `1 234 567`) must continue to be handled as before.


The Belarusian word tokenizer currently treats apostrophes (`'`, `’`, `ʼ`) as token separators, which incorrectly splits words containing those characters. In Belarusian orthography, apostrophes are part of the word and should never cause a token break. For example, `камп'ютар` should be tokenized as a single token, not multiple tokens.

Please fix the tokenization behavior for the Belarusian language module so that:
- Any apostrophe-like character used in Belarusian (`U+0027 APOSTROPHE`, `U+2019 RIGHT SINGLE QUOTATION MARK`, `U+02BC MODIFIER LETTER APOSTROPHE`) is treated as a word-internal character, not as a token separator.
- The change must only affect the Belarusian language; other languages must retain their existing tokenizer behavior.
- The fix should be implemented by providing a Belarusian-specific word tokenizer that removes those apostrophe characters from the tokenizing character set, and having the Belarusian `Language` class use that tokenizer as its default word tokenizer.


**Ticket: Offset correction for remote rule matches with supplementary Unicode characters**

**Background**
Remote rules in LanguageTool query external services (often Python-based) to get matches. These services typically report character offsets using Unicode code‑point indices (one‑per‑character), whereas Java’s `String` APIs and internal match positions count UTF‑16 code units. For text containing characters outside the Basic Multilingual Plane (e.g., emoji, rare CJK), a single `char`-based position can differ from the code‑point position by 1 or more, causing matches to highlight the wrong part of the text.

**Problem**
When a sentence like `"😁foo"` is sent to a Python‑backed rule server, the server may return a match with offsets (in Python’s indexing) `(1, 4)` for `"foo"`. In Java, those positions correspond to a substring that includes part of the emoji, leading to incorrect highlighting or reporting. This affects all `RemoteRule` implementations that rely on external offset semantics.

**Expected behaviour**
Before any downstream processing (such as match filtering), the offsets of every `RuleMatch` obtained from a remote result must be adjusted to align with Java’s internal UTF‑16 code‑unit offsets. The correction must:

- Account for every supplementary character (code point > U+FFFF) that precedes the match extent in the sentence.
- Be applied per sentence, for all returned matches of that sentence.
- Be controllable via a configuration option named `fixOffsets` in the remote rule’s options map, with a default value of `true` (i.e. correction enabled by default). When set to `false`, no offset adjustment should be performed, preserving the original server‑returned offsets.

**Non‑obvious edges**
- The offset correction must work correctly even when a match starts or ends inside a supplementary character (the adjustment should still reflect the number of surrogate pairs that have been consumed).
- The correction must be applied **before** the existing `filterMatches` step, because filtering may rely on correct span information.
- The dependency on the configuration option must be evaluated from `serviceConfiguration.getOptions()` in the same way the existing `filterMatches` option is handled.

**Affected component**
`RemoteRule` (and any subclasses that process remote matches). The fix should be located in the processing pipeline where remote results are converted into `RuleMatch` lists, after the remote call and before match filtering.

**Testing guidance**
Verification should include sentences containing emoji characters at various positions, and an assertion that the adjusted match offsets correspond to the correct character ranges in the original Java string when using `String.substring()`. The tests should be placed in a new test class within the `languagetool-core/src/test/java/org/languagetool/rules/` directory, and they can use a simple `FakeRule` to create synthetic matches with known problematic offsets.


**Bug report: Incorrect capitalization in English word repetition suggestions**

When the English "Word Repeat Beginning" rule fires on consecutive sentences that start with the same pronoun, it offers suggestions that include a lead-in phrase followed by the pronoun. The pronoun is always taken verbatim from the sentence start, so it appears in uppercase even though it is now in the middle of a sentence, which is grammatically incorrect for most pronouns.

**Steps to reproduce:**
1. Create a text with repeated sentence‑initial pronouns, e.g.:
   `He thinks so. He has seen that before.`
2. Run the English proofreading check.
3. Look at the suggestions for the repeated word.

**Current behaviour:**
Suggestions keep the pronoun in original uppercase:
- `Furthermore, He`
- `Likewise, He`
- `Not only that, but He`

**Expected behaviour:**
The pronoun should be lowercased in the inserted context, because it is no longer at the start of a sentence. The only exception is the first‑person singular pronoun `I`, which must remain uppercase.
Expected suggestions for the example above:
- `Furthermore, he`
- `Likewise, he`
- `Not only that, but he`

For input like `I think so. I have seen that.`, the suggestions must still be:
- `Furthermore, I`
- `Likewise, I`
- `Not only that, but I`

**Impact:**
Every time the rule triggers on a non‑“I” pronoun, the suggestions are grammatically wrong, making users distrust the tool or have to manually fix the suggestions.


```
The Ukrainian uppercase-sentence-start rule (`UppercaseSentenceStartRule`) incorrectly flags sentences that begin with a list marker of the form `<single Cyrillic letter>)` (e.g., `а)`, `б)`, `в)`). These are standard list item starts in Ukrainian and should be treated like already‑handled numeric list markers such as `1)` or `a)` – they should not trigger an uppercase warning. Adjust the Ukrainian‑module rule so that sentences like `а) водопі́й` are accepted without producing a match. The fix should be specific to Ukrainian and not alter the generic behaviour for other languages.
```


Portuguese spell checking currently misses real errors for words like `reune` and `autossabotar`: they are silently accepted instead of flagged, because the tagger treats `re-` and `auto-` as productive verb prefixes and invents verb readings for the whole word.  The only truly productive prefix that works this way is `soto-` (always with a hyphen), which must continue to be accepted even when the resulting hyphenated verb is not in the dictionary.

Acceptance criteria / expected behavior

1. `reune` is flagged as a spelling error and the suggestion `reúne` is offered.
2. `autossabotar` (and similar incorrectly prefixed forms that are not actual verbs) are no longer accepted as correctly spelled words.
3. Hyphenated `soto-` verb forms (`soto-pôr`, `soto-trepar`, etc.) remain accepted, both when the hyphenated form is in the dictionary and when it is not.
4. The tagger must not create a verb reading for a `re-` or `auto-` prefixed word, unless the resulting lemma genuinely exists in the dictionary.
5. The spell checker and tagger behavior for Portuguese should otherwise remain unchanged.

[pt] Remove 're' and 'auto' prefixes from tagger exceptions


The German old-spelling rule (`OldSpellingRule`) uses a mapping from old to new spellings loaded from `de/alt_neu.csv`. Currently, the rule only matches the exact case forms present in that mapping. As a result, words that are normally written entirely in lowercase (e.g., verbs, adjectives) are not detected when they appear capitalized at the beginning of a sentence.

For example, a sentence starting with "Naß" (old spelling of "nass") should be flagged just as the lowercase "naß" is, with the suggestion "Nass". Similarly, "Läßt" at the beginning of a sentence should be flagged and suggest "Lässt".

However, entries that are already capitalized (such as proper nouns or nouns like "Schluß" → "Schluss") should not be extended: the rule should not suddenly start flagging their lowercase counterparts (e.g., "schluß"), and the existing capitalized matches must continue to work unchanged.

Implement this coverage improvement while preserving all existing behavior:
- If both the old spelling and the new spelling forms in the mapping consist entirely of lowercase letters, then also accept the form with the first character uppercased as a valid old spelling, and suggest the new spelling with its first character uppercased.
- Do not alter the mapping for entries that already contain uppercase letters.
- The change must not introduce false positives for lowercase variants of entries that are inherently capitalized (like nouns that always start with a capital letter in German).

[de] increase coverage by also finding lowercase words with an uppercase first letter (beginning of sentence)


**Title:** Arabic diacritic words have wrong error positions in spell checking

**Description**
When Arabic text contains diacritics (tashkeel), the spell checker often marks the wrong character span. For example, a misspelled word like `الإقتصادي` (with a shadda) may show an error position that does not match the word’s actual boundaries, making it look like a different token is flagged.

The issue likely occurs because the Arabic tagger strips diacritics before generating `AnalyzedToken` readings, so offsets are computed against the diacritic-free string rather than the original input. This causes downstream rules (spelling, etc.) to report incorrect positions.

**Expected behavior**
- The text span for any error must correspond exactly to the original input.
- The tagging process may still use diacritic removal for stemming/analysis, but the final token readings must carry the original surface form and correct start/end offsets.

**Request**
Fix the offset handling in the Arabic tagger so that diacritic-bearing words produce the same spell-checker positions as their undiacritized equivalents.

[ar] Fix wrong offsets https://github.com/languagetool-org/languagetool/issues/3098


The Catalan weak pronoun duplication rule (`PRONOMS_FEBLES_DUPLICATS`) is currently not enabled by default because it was causing too many false positives and poor suggestions. The goal is to enable this rule after improving its detection logic and suggestion quality so that it reliably flags genuine pronoun duplication errors without incorrectly rejecting widespread legitimate constructions.

**Expected behavior**
1. **Accurate detection** – The rule should fire only when weak pronouns appear **both before *and* after the same verbal nucleus** in a way that is ungrammatical or redundant. For example, `S'ha de fer-se` is an error (suggest `S'ha de fer` or `Ha de fer-se`).
2. **Avoid false positives** – Many valid Catalan sentences contain weak pronouns attached to different verbs or appearing in separate clauses, and these must not be flagged. Examples that should remain without a warning include:
   - `Quan em va veure se'n va anar corrent.`
   - `se n'anà a veure'l`
   - `El pare el va fer anar a rentar-se la sang.`
   - `Li hauria agradat poder tenir-hi una conversa` (note the verb `poder` before an infinitive with a post-verbal pronoun)
   - `perquè els molts ulls que les volien veure poguessin saciar-se`
   - `Me n'aniria a queixar-me.` (different verb phrases)
   - `se n’aniran a viure-hi`
   - `se'n va a salvar-se`
   - and similar constructions where the pronouns belong to separate verbal groups or are separated by non‑verbal elements.

   The rule must correctly recognise verb‑group boundaries: a pronoun that follows a whitespace (not just an apostrophe) should be considered the start of a new group, not part of the preceding one.

3. **Improved suggestions** – When the error is correctly detected, the suggestions should be idiomatic and cover the normalised forms:
   - Keep either the pre‑verbal or the post‑verbal pronoun (as before).
   - **Special case**: when the detected duplicate consists of the pair `en`/`hi` with the verb `haver` or `havent` (e.g. `N'ha d'haver-hi`, `Hi ha d'haver-ne`, `En continuarà havent-hi`), the rule should also offer the perfectly correct combined form `n'hi` as an alternative (`N'hi ha d'haver`, `Ha d'haver-n'hi`, etc.).

4. **Enabled by default** – Once the false‑positive rate is low enough, the rule should be active without requiring manual activation.

**Impact**
Enabling this rule will catch a real and frequent grammatical mistake in Catalan while improved filtering will prevent users from being annoyed by incorrect flags. The rule will also teach a natural idiomatic form (`n'hi`) when appropriate.


```
Title: False alarm in German `AgreementRule` with "einiger" + adjective + noun phrases

The German agreement rule (`AgreementRule`) incorrectly flags correct adjective–noun agreement when the determiner is a word like "einiger".
For example, the sentence

    Die deutsche Kommasetzung bedarf einiger technischer Ausarbeitung.

is reported as an error, even though the genitive agreement is correct.
The same happens with two adjectives:

    … bedarf einiger guter technischer Ausarbeitung.

The rule must accept these grammatically valid phrases while still catching genuine mismatches (e.g., *einiger technisches Ausarbeitung* or *einiger guter technische Ausarbeitung*).

The issue seems to be in how agreement categories are determined for the first token (the determiner) in sequences that involve two or more following adjectives/nouns. Please investigate and fix the logic so that such correct constructions no longer trigger false positives.
```

[de] fix false alarm in e.g. "bedarf einiger technischer Ausarbeitung"


**Title:** Spanish tagger does not recognise prefixed adjectives like `superincómodas`

**Background**
The Spanish tagger (`SpanishTagger`) already has logic for handling certain prefixed adjective-like forms, but it currently skips prefixes listed in `NO_PREFIXES_FOR_ADJ` (anti, pre, ex, pro, afro, ultra, super, súper, …) without generating any additional readings. As a result, well-formed words such as _superincómodas_ receive no adjective (or past participle) analysis, even though the base _incómodas_ is correctly tagged.

**Required behaviour**
When `SpanishTagger.additionalTags` encounters a word beginning with the prefix **super** (also accented _súper_), and the remainder (the base) is a word with more than one syllable, it should attempt to look up the base in the dictionary. If the base yields any reading whose POS tag is an adjective tag (starts with `AQ`) or a past participle tag (starts with `V.P`), the tagger must produce an additional reading for the full word with:

- **token**: the original prefixed word (unchanged)
- **posTag**: the exact POS tag of the matched base reading (preserving gender/number agreement)
- **lemma**: the prefix `super` (lowercase) immediately followed by the lemma of the matched base reading (lowercase)

*Example:* For `superincómodas`, the base `incómodas` → adjective lemma `incómodo` with tag `AQ0FP0`; the full word should receive a reading `superincómodas` with tag `AQ0FP0` and lemma `superincómodo`.

**Edge cases / constraints**
- The rule must only fire when the remainder has at least two syllables (to avoid matching on short, non-adjective words). A minimal heuristic: the remainder contains at least two vowels separated by at least one character.
- If the base lookup returns multiple matching readings (adjective and participle), add one analysis per matching reading.
- The existing `NO_PREFIXES_FOR_ADJ` handling for `super`/`súper` should **not** be removed; the new rule is an additional check that runs only when the base is long enough and has the right POS, producing supplementary readings. Words that do not satisfy these conditions remain handled by the existing logic (or get no extra tags).
- The prefix `super` must be recognised case-insensitively and with or without the accent on the _u_ (`super`/`súper`).
- The base lookup must be performed using the standard dictionary (`dictLookup.lookup`) and the results converted via the existing `asAnalyzedTokenList` helper, consistent with other prefix-handling blocks already in `additionalTags`.

**Acceptance criteria**
- A new, non-intrusive block is added in `additionalTags` (before the existing `NO_PREFIXES_FOR_ADJ` matcher) that implements the described rule.
- Unit test(s) in `SpanishTaggerTest` verify that a word like `superincómodas` receives the expected lemma and POS tag, while shorter or non-adjective bases are not incorrectly tagged.


In the Catalan spell checker, when a word is misspelled, the suggestion list sometimes contains replacements that insert a space after a common prefix, for example suggesting `multi indisciplina` for `multiindisciplina`. Such prefix-split suggestions are linguistically incorrect and should not be offered to the user.

The request is:

- Filter out any suggestion that consists of a recognized Catalan prefix, a single space, and the rest of the word.
- The set of prefixes to filter includes: `avant`, `auto`, `ex`, `extra`, `macro`, `mega`, `meta`, `micro`, `multi`, `mono`, `mini`, `post`, `retro`, `semi`, `super`, `trans`.
- The filtering should be case-insensitive.
- Existing behavior that promotes certain initial particles (like `els`, `dels`, `la`, etc.) to the top of the suggestion list must be preserved – only the prefix-split suggestions should be removed entirely.
- A misspelling like `multiindisciplina` should still be flagged as an error, but no suggestion that splits at a prefix (e.g., `multi indisciplina`) should appear in the suggestion list.


[de] ProhibitedCompoundRule should not flag compound words that are part of personal names

### Problem
The rule that detects probable misspelled compounds currently fires on surname-like tokens when they appear after a given name, e.g. “Bianca Baalhorn” (the rule suggests “Ballhorn” for “Baalhorn”). This creates false alarms for multi-word names where the second element coincidentally looks like a spelling error.

### Expected behaviour
In a sequence of two or more consecutive tokens that are likely proper names, skip the prohibited-compound check for any token that is itself a proper name or an unknown word starting with an uppercase letter, if the immediately preceding token was a recognized proper name. The rule should still fire normally on identical tokens when they do not follow a proper name, and it should not skip non-name tokens in the sentence.

### Example
Input: “Das ist Petra Mitauto.”
Current: flags “Mitauto” → “Mietauto”
Expected: no match, because “Mitauto” follows the proper name “Petra”.

(Adjust the POS tags used for proper-name detection as appropriate for the German tagset.)