feat(cql): the CQL language conformance suite runs — 1622/1835 by our scheme, 1633 on the upstream rule (V7, ADR-060) - #398
Conversation
…irst number was 15x wrong in our favour (V7, ADR-060)
`cqframework/cql-tests` (1,835 cases / 16 files, pinned) through OUR translator
(@cqframework/cql 4.0.0-beta.1) and OUR engine (cql-execution 3.3.2):
pass 1622 | fail 155 | translation-error 12 | runtime-error 4
invalid-accepted 36 | invalid-refused 6 | skipped 0 (11s)
Why this suite: cql-execution 3.3.x has PUBLISHED results (1533/81/113/4), but that
run used the JAVA translator. We translate with the JS one and that delta has never
been published. Our 4 runtime errors are the same four cases as their 4 errors.
THE HARNESS WAS THE DEFECT, TWICE. The first full run reported 183 translation
errors; 171 were ours. 155 were `No default UCUM service available` — LibraryManager
takes the UCUM service as its FOURTH argument and defaults to one that throws, so
every quantity literal failed to translate. 16 more were our own grading line,
`define Passed: Actual ~ Expected`, refusing to type-check when the two sides have
different static types. Real figure: 12. Caught only because the plan required
clustering diagnostics before believing the total. Recorded in the evidence rather
than quietly fixed.
Real findings, all translator/engine, none in our measures:
- `Slice` unimplemented in the JS translator (10 of the 12)
- 36 of 42 `invalid` cases translate anyway (Ceiling(2147483648), Exp(1000), Ln(0))
— the Studio's CQL editor uses translator diagnostics as its authoring gate
- `Long` is silently wrong: 1L + 2L -> 12. String concatenation, no throw.
- decimal precision unapplied to aggregates; Ceiling not nulling at Integer bounds
Five files are perfect — logical, nullological, queries, aggregate, conditional —
the constructs our own measure CQL is built from.
Decisions (ADR-060):
1. translation-error is never folded into fail; the difference IS the deliverable.
2. cases are graded BY CQL (`Actual ~ Expected`), not by a JS literal parser that
would re-implement CQL semantics to test CQL semantics. Needed one genuine
package export: evaluateExpressions (data-free execution).
3. the SkipList is the capability set we CLAIM, and it is empty — skipping the weak
clusters would delete the finding (system.long is 33 cases).
4. CI gates on a per-file baseline, not a threshold: a translator upgrade can trade
30 passes for 30 different ones and keep every total identical.
5. ADR-048's node: CLI debt is REFRAMED, not paid. Its basis had expired
(live-cli.ts no longer imports devdb-cli.ts). The real hazard was that the
carve-out keyed on the FILENAME, so a request-path module named *-cli.ts would
pass. Now keyed on reachability, derived not listed. Mutation-checked both ways.
The runner REFUSES to report unless it parsed all 16 files and all 1,835 cases with
every case in exactly one bucket — a conformance harness that grades a subset
publishes a flattering number.
Production gap found, deliberately NOT fixed here (#397): the runtime translator has
no UCUM service either, so the ELM Explorer cannot compile any CQL with a quantity
literal. compileCql now takes an optional validateUnit; production passes none, so
its behaviour is byte-identical.
Suite 1863 -> 1876, 0 fail. Closes #296.
Evidence: docs/evidence/CQL_TESTS_2026-08-05.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d99b0e6f5d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| export function perFile(results: readonly CaseResult[]): Record<string, Counts> { | ||
| const out: Record<string, Counts> = {}; | ||
| for (const r of results) (out[r.file] ??= tally([]))[r.outcome]++; | ||
| return out; |
There was a problem hiding this comment.
Key the CQL baseline by individual test case
Because the baseline is reduced to per-file outcome counts here, pnpm cql-tests --check can miss the exact regression shape this gate is meant to catch: within a single XML file, one previously passing case can become fail while another failing case becomes pass, leaving pass/fail totals unchanged and producing no regression. This also lets translation errors move between cases when the global total is unchanged, so the committed evidence can drift while CI stays green; store and compare outcomes by file/group/name instead of only per-file tallies.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and fixed. You are right, and the framing is what makes it sting: the gate explicitly rejected a bare >=N passing threshold because a translator upgrade can trade 30 passes for 30 different ones — and then reintroduced the identical hole one level down by tallying per file.
regressions() now compares by file/group/name. Only the non-passing cases are stored — 213 entries rather than 1,835 — and that loses nothing rather than being a size compromise: a case absent from the map was passing, so "used to pass, now does not" stays decidable for all 1,835.
Three things fell out of doing it per case that the per-file version could not express:
- a change between non-passing outcomes (
fail→translation-error) is reported without failing — the evidence document enumerates those buckets, so silent drift between them would leave it stale; - a baseline case that did not run at all is a regression, not an improvement;
- a pre-feat(cql): the CQL language conformance suite runs — 1622/1835 by our scheme, 1633 on the upstream rule (V7, ADR-060) #398 baseline is refused rather than silently compared with the weaker rule.
The test now asserts the exact hole — a within-file swap whose tallies are proven identical by an assertion must still be caught. Mutation-checked end to end against the real corpus too: removing one entry from baseline.json yields CqlAggregateFunctionsTest.xml/Product/ProductLong: pass → fail.
ADR-060 decision 4 rewritten to record the hole rather than describe the design it replaced. Suite 1876, 0 fail.
…d the same hole one level down (Codex, #398) The gate rejected a bare ">=N passing" threshold because a translator upgrade can trade 30 passes for 30 different ones. The first cut fixed that with per-FILE tallies, which has the identical hole one level down: inside one XML file, a case can go pass->fail while another goes fail->pass, leaving every count identical and CI green. regressions() now compares by file/group/name. Only the NON-passing cases are stored (213 rather than 1,835) and that loses nothing — a case absent from the map was passing, so 'used to pass, now does not' stays decidable for all 1,835. Also: - a change BETWEEN non-passing outcomes is reported without failing; the evidence doc enumerates those buckets, so silent drift would leave it stale - a baseline case that did not run at all is a regression, not an improvement - a pre-#398 baseline is REFUSED rather than silently compared with the weaker rule - improvements are listed per case, not inferred from a total Test rewritten to assert the exact hole: a within-file swap whose tallies are proven identical must still be caught. Mutation-checked end to end against the real corpus — removing one entry from baseline.json produces 'CqlAggregateFunctionsTest.xml/Product/ProductLong: pass -> fail'. ADR-060 decision 4 updated. Suite 1876, 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… and 'fires on zero cases' was read off a field we never wrote (review, #398) Four review findings, two of them in text that was about to be published. CRITICAL — invalid cases are now EXECUTED when they translate. cql-tests-runner grades an invalid="true"|"semantic" case as a pass when the request fails for ANY reason, translation OR evaluation: if (invalid === 'true' || invalid === 'semantic') result.testStatus = response.status === 200 ? 'fail' : 'pass'; We graded purely on whether it translated. Measured: 5 of the 36 "accepted" throw at runtime and are upstream passes -> 11 refused (6 translation, 5 runtime) / 31 accepted. Worse, the finding's headline example, Ceiling(2147483648), is one of only 2 invalid="syntax" cases, which upstream does not route through that branch at all. The upstream-comparable total is 1,633, now stated beside our 1,622. IMPORTANT — a missing define was reported as an engine defect. Grading on defines["Passed"] === true means an ABSENT define reads as , labelled "engine computed the wrong value" — the exact misattribution ADR-060 d1 exists to prevent, one layer down, and it would fire en masse while still looking plausible. Now a named harness fault. IMPORTANT — the harness was never typechecked. scripts/ was outside tsconfig's , so the code producing a published number was only type-stripped by tsx. Adding it immediately caught two Baseline fixtures missing a required field. IMPORTANT — 'the fallback fires on zero cases' was FALSE. gradedInJs was never serialized by runnerJson, so both the author and a reviewer read 0 off a key that did not exist. It is 16. Now printed in the summary, serialized, recorded in the baseline, and an increase is a regression. Also: ucum.ts claimed the corpus uses 6 units (it uses 18+) — replaced with a test that WALKS the corpus so the claim maintains itself; single-pass entity decoding (numeric refs were decoded first, so &lt; double-decoded to <); single-quoted attributes read rather than silently dropped; skipDecision's map injectable so the test drives the real function instead of a copy of it; request-path floor raised 5->15 against an actual 21, and the re-key documented as the TRADE it is; baseline pin must match the corpus pin; the fetch script checks its own exit code and verifies HEAD before writing .pin. Numbers, evidence, ADR-060, conformance matrix, journal and Current Focus all updated. Per-file table now carries invalid-refused, so every row reconciles. Suite 1876, 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review findings addressed (4 fixed, all in this branch)A detailed review of this PR turned up one finding that would have shipped a wrong published claim, and three more worth having. CRITICAL — the
|
ucum.ts claimed the corpus uses 6 units |
it uses 18+ — replaced the assertion with a test that walks the corpus, so the claim maintains itself |
| entity decoding | numeric refs were decoded first, so &lt; double-decoded to <; now a single pass |
| single-quoted attributes | were silently dropped — an invalid='true' would have become a valid case |
skipDecision |
its test re-implemented the function and asserted against the copy; the map is now injectable so the test drives the real code |
| request-path floor | > 5 against an actual 21 → >= 15; and the re-key is now documented as the trade it is, not a pure tightening |
| baseline / corpus pin | must now agree, or the run refuses |
fetch-cql-tests.ps1 |
checks its own exit code and verifies HEAD before writing .pin — a failed fetch on a warm checkout could label results with a commit they were not produced at |
Every per-file row in the evidence table now reconciles (the invalid-refused column was missing). Suite 1876, 0 fail; --check green against the regenerated baseline.
# Conflicts: # docs/DECISIONS.md # docs/JOURNAL.md
Roadmap M-C / V7, a named member of the FHIR-column verification set (locked decision 2). Closes #296, open since 2026-07-15.
Why this suite specifically
cql-execution3.3.x — our exact runtime — already has published results atcql-tests-runner.quality.hl7.org: 1,533 / 81 / 113 / 4. That run used the Java translator. We translate with@cqframework/cql4.0.0-beta.1, and that delta has never been published. This measures it.The comparison is indicative, not like-for-like (their corpus was ~1,731; they skipped 113 we grade) and the PR says so everywhere it appears. What is directly comparable: our 4 runtime errors are the same four cases as their 4 errors.
The harness was the defect, twice, and nearly published
The first full run reported 183 translation errors. 171 of them were ours:
No default UCUM service available.LibraryManagertakes the UCUM service as its fourth argument and defaults to one that throws, so every expression containing1.0'cm'failed to translate.define Passed: Actual ~ Expectedwill not type-check when a case's expected value has a different static type from its actual.Real figure: 12. Publishing 183 as "the JS translator delta" would have been wrong by a factor of 15. It was caught only because the plan required clustering the diagnostics before believing the total — one command. Both defects are written up in the evidence rather than quietly fixed, because "the harness was wrong" is the outcome a conformance exercise most needs to be honest about.
Findings — all in the translator/engine, none in our measures
Sliceunimplemented in the JS translatorinvalidcases translate anywayCeiling(2147483648),Exp(1000),Ln(0),minimum Boolean. Matters because the Studio's CQL editor uses translator diagnostics as its authoring compile gateLongis silently wrong1L + 2L→12— string concatenation, no throw, no warning.Sum({6L,2L,3L,4L,5L})→62345. Independently confirms the connectathon researchPopulationStdDevat full IEEE-754 width vs the corpus's declared 28,8Ceilingdoes not null at the Integer boundaryFive files are perfect — logical, nullological, queries, aggregate, conditional. Those are the constructs our own measure CQL is built from, and no WorkWell measure uses
Long,Slice,timezone fromorToConcept(Code).Decisions (ADR-060)
translation-erroris never folded intofail. Merging them would attribute a translator gap tocql-execution, whose own posted results say otherwise. The difference between those columns is the deliverable.Actual ~ Expected, executed unfiltered. A TypeScript literal parser would re-implement CQL semantics in order to test CQL semantics, sharing defects with the thing under test. This needed one genuine package export:evaluateExpressions(data-free execution — the whole language suite is defined in that subset).system.longis 33 cases and holds the worst defect) and reported a better rate over a smaller denominator. The mechanism exists and is unit-tested against a fixture so it is not vacuous.node:CLI debt is reframed, not paid. Its stated basis had expired —live-cli.tsno longer importsdevdb-cli.ts, and every remaining importer of the four CLI files is a test or abin.tsshim. The real hazard was thatengine-boundary.test.tskeyed itsnode:carve-out on the filename, so a request-path module merely named*-cli.tswould pass. Now keyed on reachability, derived rather than listed. Mutation-checked both directions.Non-degeneracy
A conformance harness that parses nothing reports no failures and looks green — and here it would publish a number. The runner refuses to report at all unless it parsed all 16 files and all 1,835 cases, every file contributed, and every case landed in exactly one bucket summing to the total.
Production gap found, deliberately not fixed here — #397
The runtime translator has no UCUM service either, so the Studio's ELM Explorer cannot compile any CQL containing a quantity literal.
compileCqlnow accepts an optionalvalidateUnitand the harness passes one; production passes none, so its behaviour is byte-identical. Wiring it in is a real behaviour change and belongs in its own PR.Verification
pnpm test1863 → 1876, 0 fail (9 harness tests + 4 for the new package export).pnpm typecheckclean.pnpm cql-tests --check— no regressions vs the committed baseline.node:fsin a request-path module (measure-registry.ts) turnsengine-boundary.test.tsred; the same import indevdb-cli.tsstays legal.official-cases— pinned fetch, cached, held out ofpnpm testso an offline local run stays green.Evidence:
docs/evidence/CQL_TESTS_2026-08-05.md. Conformance claims added todocs/STANDARDS_CONFORMANCE.mdvia theconformanceskill — it is a measured pass rate over a public suite, not a conformance claim, and no certificate exists for it.🤖 Generated with Claude Code