Every rule names its language; every finding names what encloses it
Two fields the rule card needs, and the second frontend will need more.
language is written down from the first frontend rather than assumed and
retrofitted from the second. It prefixes the rule's name everywhere it is
shown — [rust/discarded-check] — and names its page in the documentation.
Not patchable: a rule's language is a fact about the rule, not a setting.
item says which function, method or impl a finding sits inside, so a reader
can zoom out to the thing that is wrong rather than the line it happens to be
on. The span was already in the graph — a symbol is joined to whatever
contains it by a Contains edge, and that edge's evidence is the symbol's own
extent — so this asks the graph rather than making the extractor write
anything new down.
It is answered in one pass over every finding rather than at each of the twenty places one is made. A rule that forgot to ask would be a rule whose card quietly offers one zoom fewer than the others, which is the kind of gap nobody files.
The innermost enclosing item wins: a call inside a closure inside a method is most usefully shown as the method, not as the impl block that also contains it.
Scoring cqx with itself: 16 of 18 findings now carry an item, and the two that do not are about whole files, which is correct.
The wrapper is @samifouad/cqx
npm refused cqx-cli — not because it is taken, but as too similar to
cp-cli and cpy-cli, which is its typosquatting guard. That check applies
to unscoped names only, and the unscoped name we would actually want, cqx,
belongs to an unrelated package.
So the wrapper joins the four platform packages under the scope it already
depends on. npx @samifouad/cqx is a few characters longer and is written
once per workflow.
The packaging test builds into its own directory
build.mjs wipes its output directory, and the test called it with the default — so running the test left npm/dist holding five packages built from stub binaries, one real one and three that exit 3. Publishing after a test run would have put those on the registry.
It takes an output directory now and the test passes a scratch one.
Tell gh which repository, since publish never checks one out
The publish job downloads artefacts and nothing else, so there is no git
repository for gh to infer from and every gh release call failed with
"not a git repository". Naming it is cheaper than cloning a tree to answer
one question.
v0.1.15
Every command that reports a problem now exits like it — score and
query printed errors and exited zero, and score is the one CI gates on.
Built and published for four platforms rather than one, each from its own build cache, each run before it is kept.
And cqx can be built on Windows, which it never could: the pinned zega revision held a file whose name contains a colon.
A command that reports a problem has to exit like it
cqx score printed "No such file or directory" to stderr and exited zero.
It is the command a CI job gates on, so a mistyped --facts path did not fail
a build — it passed one, without having measured anything. Every error path
in score now sets the exit code, which is what the FAILED flag beside them
was already for.
cqx query had no exit code at all. cqx-store was missing from the list the
binary takes a maximum over, so nothing it reported could ever fail a
script.
And a build without the graph backend answered a query request by printing a node count and exiting zero. --zql and --named are advertised in --help, this build cannot honour them, and a script could not tell that from a query that ran and matched nothing. It says so now, and exits one. Counting still works, because --stats is a different question.
Found by packaging the CLI for npm and asking what a consumer sees. The tests are what the tool is for: extract, history, export and config were already right, and it was the two the CI story depends on that were not.
The environment follows the trigger
A dispatch was running the build jobs in release, which holds the token
that can write to cqx-releases, cqx-wasm and the product bucket. Those jobs
compile; compiling needs the build cache and nothing else. So they now take
public-ci unless the run came from a tag push, and public-ci cannot
reach anything but the caches — which r2-check proves rather than asserts.
Publishing says what it means: a push, to a tag beginning with v. It was previously reachable by dispatching with dry_run turned off, which made a release something one could do by hand with the right checkbox. The input is gone, so a dispatch can only ever build and smoke test.
ci push main, pull_request, dispatch → public-ci release dispatch → public-ci (build only) release push tag v* → release (build, publish)
Move the zega pin forward so Windows can check it out
cqx cannot be built on Windows, and never could. The pinned revision of the zega git dependency contains a file called `:memory:/wal.bin`, and a colon is not a legal character in a Windows filename — so cargo fails while checking the source out, before compiling anything:
cannot checkout to invalid path ':memory:/wal.bin'
It fails even though the dependency is optional and the feature is off, because cargo fetches a git source to resolve the graph regardless.
zega deleted that file some time ago; cqx was simply pinned behind it. The lockfile now points at a revision with no such path, and the zega feature still builds and the workspace still tests.
And the matrix stops hiding its own results: one failing runner cancelled the other three, so a dry run cost two attempts to learn one thing.
A dry run builds what is in front of us
Dispatching against v0.1.14 checked out a tree from before this workflow existed, so every job failed looking for composite actions that were not there yet. The tag was not the problem — building an old tag to test a new pipeline was.
A dry run now builds the commit it was dispatched from and takes the version from the manifest. A real release still builds the tag, and still refuses to start if the tag and the manifest disagree.
Build cqx for every platform it is distributed on
One tag, four binaries — linux-x64, darwin-x64, darwin-arm64, windows-x64 —
each built with its own sccache bucket, each run before anyone is asked to
download it. --version is the smoke test because it is the one answer that
proves the thing linked and started rather than merely existing, and it is
checked against the tag: a binary that reports a version nobody can find is
the one mistake that cannot be fixed afterwards.
The tag and Cargo.toml must agree before anything builds, for the same reason. npm would refuse the second publish at a number the first one already took.
Published three ways, because they answer different questions. Attached to
the GitHub release, where people look. Mirrored to cqx-releases under the
tag, because an installer wants a versioned URL on a CDN rather than a
redirect chain through a release page, plus a latest.json that does not
move. And the wasm to cqx-wasm, which is the bucket browsers read.
A dispatch defaults to a dry run: build and smoke test every platform, publish nothing. Four platforms cannot be proved any other way, and proving them should not require spending a version number.
The sccache checks are portable now. Asking the AWS CLI whether a bucket is reachable meant installing it, which is a different incantation on each of three runners; asking sccache what it thinks its own cache is costs nothing and reports the view that actually governs the build. A companion action checks afterwards that writing worked, because sccache treats an unreachable bucket as a cache miss — bad credentials compile everything, write nothing, and pass.