A zero that means “not measured”.
Three testers went looking for what vexp gets wrong when it looks right: a 3,000-file Flutter and C# monorepo, a nine-repository Windows workspace rebuilt from a clean state, and a Windows machine running three IDEs at once. Between them they found vexp reporting a version it had not read, a call graph it had never built, and a daemon count it had just deleted — each one indistinguishable, from the outside, from a correct answer. That is the only kind worth a release: a wrong answer you can see is a bug, and a wrong answer you cannot is a belief.
Dart: a call written inside a method is a call. The Dart grammar splits a declaration into a signature and a separate body, and vexp compensated for that only where the body sits directly beside the signature — which is true of a top-level function and false of every method in a class. So a Dart method was stored with its own signature as its entire body: twenty-three bytes where the source has the call. No call written inside a Dart method could produce a call edge, which in a Flutter codebase is nearly all of the code, build() included. On the monorepo that reported it, a privacy gate with 27 call sites across 15 files answered “no callers”, while vexp search found all 27 — the class node keeps the full class text, so the references were always there and only the graph was empty. Its author's control was the giveaway: a second gate that worked, because the callers of that one lived in test files, and a Dart test calls from inside a top-level main(). The same extraction path was also dropping constructors, getters, setters and operator == entirely, and attaching mixin and extension members to the file rather than to their owner. All indexed now. The conformance matrix that certified Dart as full throughout had two top-level functions calling each other — the one shape that worked; a second matrix now puts the caller inside a class, across twelve languages.
The header says what this index cannot answer. A language that holds most of a codebase and resolves no calls makes impact answer nothing everywhere, and nothing about that answer says so. One line now does, in the header of every run_pipeline result: 96% of this index is dart and its call graph is EMPTY — a zero from impact means “not measured”, not “no callers”. It is computed at index time, so it costs nothing per query, appears only when the gap is big enough to change what an answer means, and stays silent otherwise; a language that resolves no calls by nature rather than by defect gets a different sentence, so EMPTY always means something is wrong. Size-skips are now read from every connected repository instead of the primary alone — oversized files in a member repo were invisible to the disclosure that exists to make them visible, while doctor listed them at the same moment. And because symbols and call edges are computed once, at index time, a parser fix lands only on re-index: an index built by a different engine than the one answering now says so, in the header and in index_status.
Two vexp installs now converge instead of fighting. Everywhere two installs contend for one shared thing, vexp asked “is this the same as mine?” and acted on any difference. With one install that reads as caution; with several at mixed versions it is a fight nobody wins. A supervisor left by an older install killed the newer MCP server on port 7821 and spawned its own, the newer CLI did the reverse, and a tester watched the server come back with a fresh pid every two minutes for days. The daemon adoption guard had the same shape, so with three IDEs at mixed extension versions each killed the other's daemon on activation — and the sidebar's own warning offered a Restart button whose only effect against a newer daemon would have been to downgrade it. All ordered now: older is replaced, newer is adopted as it is, equal is left alone. The version recorded for the MCP server was the version of the process that spawned it, read once at startup and held forever, so a supervisor running since before an upgrade stamped its own old version onto children running the new build — doctor reported v3.0.1 for a process two minutes old, on a machine where 3.0.1 was no longer installed anywhere. The record now carries the build being served and, separately, the process that started it, which is the one thing no takeover rule can fix from this side: doctor names that process and its pid, and ending it is the whole remediation.
A supervisor never deletes a daemon it does not own. vexp serve rebuilt the daemon registry from scratch on every health pass, keeping only the rows it had just resurrected and writing that as the whole file. Anything its plan skipped — a daemon started by someone else, a linked worktree — was deleted while it was alive and answering on its socket, and because the pass repeats every minute, the deletion renewed itself. On the machine that found it, three live daemons reported as one, and the two that vanished were precisely the two still serving a pre-upgrade build. The concurrency fence counts those rows, so this was not only a visibility bug: restarting a supervisor silently freed license slots. Liveness decides what stays in the registry now, never ownership. And because an upgrade restarts the daemon of the workspace it runs in and leaves every other one alone, vexp daemons flags any daemon older than the installed engine — it is the one place that sees them all.
One connection is enough to ask a daemon a question. On Windows the daemon serves its named pipe one client at a time: it creates an instance, waits, handles that connection, and only then creates the next. Two commands probed the endpoint for liveness and then opened a second connection to do the actual work, so the probe consumed the only instance and the real call arrived to nothing. vexp daemons printed v?, 0 nodes, up 0m for a daemon doctor reported healthy with 85,284 nodes, and vexp capsule --repos fell back to its in-process index against a perfectly reachable daemon. Unix has a listen backlog and never showed it. The call is the probe now, and the connection retries the two errors that mean the server is between accepts. Where stats cannot be read they are reported as unavailable rather than rendered as zeroes. The capsule fallback also says what it lost: the in-process engine opens one repository, so a --repos scope naming another was not narrowed but ignored, and the answer came from somewhere else entirely.
Setup writes the hook it registers, and never gives up in silence. Claude Code reported a missing vexp-hint.sh on every prompt: the extension deleted that script under its shipped defaults while registering the hook that points at it, re-running the configuration re-did the deletion, and since the extension reconfigures on every activation, reopening the window undid any manual repair. A script and its registration are removed together now, and a test asserts the rule rather than the one hook. The Antigravity MCP writer was the last config reader still intolerant of a UTF-8 BOM — PowerShell's default — so on a file written that way it registered nothing and said nothing, reported as “setup does not recreate the registration”; the Codex hooks file had the same latent bug. An existing entry that vexp cannot version, such as a hand-written launcher, is still left alone, but now says so, because a setup that changes nothing must not look like one that worked. The bootstrap that replaces a stale daemon was skipped silently outside a configured workspace. And vexp setup no longer blocks on its agent prompt when nothing is attached to the terminal: for a script or an agent following a doctor line, that prompt was not a question, it was a hang.
Smaller fixes that shipped along the way. A migration backup kept inside the project — a dated copy of an agent config directory — was indexed and returned as an architectural pivot: a backup holds a full copy of what it shadows, so ranking cannot help, because the copy really is as relevant as the original. Those are pruned now, narrowly. A workspace member whose directory was replaced wholesale lost the link back to its primary and silently indexed with standalone defaults, a 512 KB cap where the workspace said 2048; indexing from the primary repairs the link and prints what it repaired. The size-skip warning prints the absolute path of coverage.json, because on a monorepo people reasonably look for it beside the package they are working in. doctor reports the compressor as loading while the model is still starting, instead of naming the rule compressor for twenty seconds. setup-llm --status asks the running daemon what it is actually doing, so GPU capability and GPU in use are two lines rather than one. And the remediation for a stale MCP server names the command that does the job.