All releases · How to verify this release
# CQELS 2.0.0-alpha.18
Released 2026-08-04. Published to this mirror with a signed `SHA256SUMS` covering every
artifact; see `VERIFY.md` in this directory for the verification procedure.
**Supersedes alpha.17.** alpha.17 set out to remove internal references from the
published metadata and, in one place, moved them instead. This finishes the job and adds the
checks that would have caught it.
### Fixed
- **Published SBOMs no longer name the source repository.** Every alpha.17 CycloneDX document
carried the deploy URL as a `distribution-intake` external reference — once per module and
fifteen times in the reactor-wide aggregate, in both JSON and XML. The cause: alpha.17
stripped `<distributionManagement>` from the published *pom* with flatten-maven-plugin, but
cyclonedx-maven-plugin reads the un-flattened project model, so the element still reached the
SBOM. The declaration is now deleted outright rather than hidden at one layer, which is the
only form of the fix that holds for every consumer of the project model. Verified against a
freshly generated aggregate BOM: same 146 components as alpha.17, zero occurrences.
- **Published jars no longer point consumers at an unreachable repository.** Two
`IllegalStateException` messages in `cqels-storage-spi` and one line of the bundled
`docs/CQELSQL_CEP_SYNTAX.md` in `cqels-mcp` named a repository that returns 404 to everyone,
so anyone hitting the error was sent nowhere. Reworded. Javadoc and comments are unaffected —
they never reach a jar.
- **Both gates that missed this now cover it.** The SBOM scanner read only component names and
descriptions, so `externalReferences` were never shown to a deny-list that already matched
the string; it now reads reference URLs and comments in both formats, for our own components
only. The staged-jar gate read entry *names* — which is why a correctly allowlisted bundled
document was never opened — so a second gate now scans decompressed jar contents. Four new
regression cases, including one asserting third-party components' references are still
ignored: their SCM URLs contain `git@`-style addresses and reading them would fail every
release.
- **A windowed hash join no longer discards rows to stay under its cross-product cap.**
When a per-arrival windowed join produced more intermediate bindings than the cap, the
compiler kept the prefix and dropped the rest — silently. The caller received a smaller
answer indistinguishable from a correct one, and any `COUNT`/`SUM`/`AVG` over the surviving
prefix was then wrong with no signal. The cap is now enforced by failing, naming the query,
the binding count and the cap. A join landing *exactly* on the cap is complete and is still
admitted. The count is measured **before** FILTER, so filtering cannot reduce it; where a
join is legitimately larger than the default 100 000, raise the bound with
`-Dcqels.join.crossProductCap=<n>`.
- **A `start()` that loses the concurrent-start race no longer drains pending CEP
registrations.** Both threads drained the queue; the loser drained it after the winner had
claimed it, so CEP patterns queued before startup were thrown away and never activated. A
failed start is terminal, so the entries are released by `stop()`/`close()`.
- **The bounded memory store measures its delta against committed state.** Each connection
inferred a statement's pre-transaction presence from its own listener notifications, which
is connection-local: two overlapping transactions adding the same absent statement each
claimed a slot, while RDF set semantics leave one resident statement. The counter drifted
above the true size, and under a tight cap rejected a duplicate write that would not have
grown the store at all; removals drifted below, which is the direction that lets the store
outgrow its bound. Each touched key's prior state is now read from a probe connection at
commit, and the commit path prepares before taking the store lock — preparing under it
inverted the lock order against MemoryStore's transaction lock and deadlocked.
- **SHACL numeric range constraints are evaluated.** `sh:minInclusive`, `sh:maxInclusive`,
`sh:minExclusive` and `sh:maxExclusive` were parsed and then ignored, so a shape declaring
a range validated everything. The parser's deny-list is now an allow-list: an unrecognised
constraint is reported rather than skipped, which is what let four constraints go missing
quietly. Comparison follows SPARQL numeric promotion rather than lexical forms — the
datatype gates the comparison, `xsd:float`/`xsd:double` compare in their own value space
because they round, and mixed datatypes move to a common type first.
- **A transitive closure that outgrows its per-delta cap fails instead of truncating.**
`deriveTransitiveStatements` returned early on reaching
`maxTransitiveDerivedStatementsPerDelta`, handing back a partial closure with no signal, so
a caller could not tell "these are all the reachable nodes" from "we stopped counting". It
now throws, naming the property, the overrun and the cap; a closure landing exactly on the
cap is complete and is returned.
- **CEP partial-match evictions are counted.** The state-explosion guard is correct, but its
losses were invisible — and because the least-advanced partials go first, the loss is biased
toward long patterns under load. The guard now reports how many partials it dropped into a
process-wide meter, with rate-limited warnings. The bound is unchanged; it is now
observable.
### Changed
- `CountingSailConnection`'s two-argument constructor
`(NotifyingSailConnection, BoundedReservation)` is replaced by a four-argument form carrying
the store-wide commit lock and a probe-connection factory. No known callers exist
outside `cqels-engine`, but the class is public, so an embedder constructing it directly
must adjust.
### Upgrade notes
- **Three fixes convert a silently wrong answer into a thrown exception** or a
reported violation. Unlike alpha.13's `MINUS` rejection, which fired at
*registration*, these fire at *runtime* on a data-dependent threshold — a query registers
clean and can fail later under load. **If you register continuous queries with a lambda,
supply a `QueryResultListener` with an `onError` before upgrading**: a tripped query
otherwise stops emitting permanently with only a log line to show for it.
- **SHACL shapes using constraints outside the supported fragment now fail to parse** rather
than being silently skipped. There is no opt-out; the remedy is to remove the unsupported
predicate from the shape.
- **Numeric bounds over non-numeric literals are reported as violations.** `push_stream_events`
builds `facts`-body literals as `xsd:string`, so a `watch_invariant` numeric bound over
data pushed that way will now report a violation on every observation. Use the `nquads`
body, which carries real datatypes, until the ingest handler is fixed.
### Note on alpha.17
alpha.17 reached the mirror but never became fetchable: the `maven.cqels.org` DNS record was
pointing away from its host throughout, so the release gate correctly refused to publish a
release page for it and no consumer could resolve it. Rather than leave it superseded, its
artifacts were withdrawn from the mirror — nobody can hold those bytes, so removing them
breaks no one's verification, and leaving them would publish that reference indefinitely
for a release no one should use.
## Using this release
Artifacts resolve anonymously — no token, no `settings.xml`:
```xml
<repositories>
<repository>
<id>cqels</id>
<name>CQELS Releases</name>
<url>https://maven.cqels.org/releases</url>
</repository>
</repositories>
<dependency>
<groupId>org.cqels</groupId>
<artifactId>cqels-engine</artifactId>
<version>2.0.0-alpha.18</version>
</dependency>
```
## Verifying this release
The signed manifest and its signature are served from the artifact repository;
the public key is served from a different origin, so verifying with it is not
circular. Take the key from the commit pinned below.
```bash
set -euo pipefail
BASE=https://maven.cqels.org/releases/supply-chain/2.0.0-alpha.18
curl -fsSLO https://raw.githubusercontent.com/cqels/CQELS4J/d7e9f09729823b66dcdbf98d0f9230dedcef91cf/cosign.pub
echo "36dd8daa9988f23eb40c4f3550fa7bdfa3796e5e58cce8d23b9cc6a99f47f30b cosign.pub" | sha256sum --check --strict - || exit 1
curl -fsSLO "$BASE/SHA256SUMS" -O "$BASE/SHA256SUMS.bundle"
cosign verify-blob --key cosign.pub --bundle SHA256SUMS.bundle \
--new-bundle-format=false --insecure-ignore-tlog SHA256SUMS
```
The URL is pinned to a commit rather than to `master`, so it keeps working
after a key rotation. `--insecure-ignore-tlog` is expected: these signatures
carry no transparency-log entry, which is precisely why the key's origin
matters. Full walkthrough: https://github.com/cqels/CQELS4J/blob/master/SUPPLY_CHAIN.md