fix(engine): surface discovery-index contract-version skew and actually send the version on the query wire - #9631
Conversation
…ly send the version on the query wire DISCOVERY_INDEX_CONTRACT_VERSION was declared on both wire shapes but read by nothing: both normalizers discarded whatever version the other side declared and relabelled it with the local constant, and queryDiscoveryIndex sent JSON.stringify(request.query), so the version never reached the wire on the query path at all -- while buildSoftClaimRequest already sends it. normalizeDiscoveryIndexRequest and normalizeDiscoveryIndexResponse now push a single templated warning (the module's existing clampLimit / JSONbored#6774 style) when the raw input declares a numeric contractVersion different from this build's; absent or non-number declarations stay silent by the tolerant-parser convention, nothing throws or is dropped, and both outputs still emit the local constant. The client now sends the whole normalized request flattened to one level ({ contractVersion, ...query }) because the server parses the body with normalizeDiscoveryIndexRequest, which reads the query fields and the declared version off the top level -- proven by a round-trip test that feeds the exact sent body back through the normalizer. No server changes. Regression tests land in BOTH graded suites: the engine package's own node:test suite (the `engine` Codecov flag's coverage run) and the root vitest unit suite (the `backend` flag), so every changed line is covered under each flag's own lcov. Closes JSONbored#9615
|
Warning ⏸️ LoopOver review result - manual review recommendedReview updated: 2026-07-29 02:00:56 UTC
Review summary Nits — 4 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9631 +/- ##
==========================================
+ Coverage 90.08% 90.10% +0.01%
==========================================
Files 889 889
Lines 112024 112042 +18
Branches 26587 26591 +4
==========================================
+ Hits 100921 100950 +29
+ Misses 9773 9762 -11
Partials 1330 1330
Flags with carried forward coverage won't be shown. Click here to find out more.
|
Closes #9615
Summary
fix(engine): surface discovery-index contract-version skew and actually send the version on the query wireDISCOVERY_INDEX_CONTRACT_VERSION = 1was declared on bothDiscoveryIndexRequestandDiscoveryIndexResponsebut read by nothing: both normalizers discarded whatever version the other side declared and relabelled it with the local constant (a v2 server answering a v1 client was silently processed as v1), andqueryDiscoveryIndexsentJSON.stringify(request.query)— so the version never reached the wire on the query path at all, even thoughbuildSoftClaimRequestalready puts it on its payload. The module is explicitly a tolerant parser over the "OPTIONAL, only-partially-trusted hosted index"; version skew is exactly what itswarningschannel exists to surface, and it reported nothing.Root cause
The version field was written into both normalizer outputs but never inspected on input, and the query client serialized only the inner
queryobject.Fix
normalizeDiscoveryIndexRequest: when the raw input carries a numericcontractVersiondifferent fromDISCOVERY_INDEX_CONTRACT_VERSION, it pushes exactly`DiscoveryIndexRequest declared contractVersion ${received}; this build speaks ${DISCOVERY_INDEX_CONTRACT_VERSION}.`— the module's existingclampLimit/ discovery-index-contract.ts caps every request-side list but leaves the response candidate list unbounded #6774 push-one-templated-string-and-continue style. Absent or non-number declarations stay silent (tolerant-parser convention).normalizeDiscoveryIndexResponse: same, withDiscoveryIndexResponsein the message. Neither function throws, drops candidates, or changes the emittedcontractVersion— the warning is the only behaviour change.queryDiscoveryIndexnow sends the whole normalized request — the contract version WITH the query — flattened to one level:JSON.stringify({ contractVersion: request.contractVersion, ...request.query }). Flattened because the server (packages/discovery-index/src/app.ts) parses the body withnormalizeDiscoveryIndexRequest, which reads the query fields (and now the declared version) off the top level; nesting the query under aquerykey would make the version travel but the query invisible to the server's parse. A round-trip test proves the server's parse is unaffected by the widened body.packages/discovery-index/src/app.tsorparseSoftClaimRequest; no new exported helper; no version negotiation; version constant unbumped.Diff:
packages/loopover-engine/src/discovery-index-contract.ts(+18),packages/loopover-miner/lib/discovery-index-client.ts(+6/−2), plus tests (new engine-suite file +50; vitest +57/−1).Tests (RED → GREEN)
Against the unfixed source:
(the two mismatch-warning tests and the two client wire-shape tests fail pre-fix). After the fix, across the contract, client, server (
discovery-index/app), soft-claim, discover-CLI, and repo-segment suites:normalizeDiscoveryIndexRequest({ contractVersion: 99, repos: ["a/b"] }).warningscontains the exact request-side message;.request.contractVersion === 1.normalizeDiscoveryIndexResponse({ contractVersion: 99, candidates: [] }).warningscontains the exact response-side message;.response.contractVersion === 1.contractVersion: "1"(non-number) each produce no version warning, on both sides;contractVersion: 1with a valid candidate produces no warning and keeps the candidate.fetchImplseam): the sent JSON body parses to{ contractVersion: 1, repos: ["a/b"], orgs: [], searchTerms: ["help wanted"], limit: 50, cursor: null }— top-level version and query fields — and feeding that exact body back throughnormalizeDiscoveryIndexRequestyields arequest.querydeep-equal to the one the client normalized, with zero warnings (server parse unaffected).test/unit/discovery-index/app.test.ts) passes unchanged.packages/loopover-engine/test/discovery-index-contract-version.test.tsmirrors mismatch/absent/non-number/matching (and the non-mapping response arm) inside the engine package's own node:test suite: 804 tests, 804 pass.Coverage
This diff spans two graded surfaces and is covered under each flag's own coverage run:
engineflag (CI's exact recipe:node --experimental-strip-types scripts/engine-coverage.ts, c8 over the engine package's own node:test suite), lcov cross-referenced against this PR's diff hunks:backendflag (root vitest v8; bothpackages/loopover-engine/src/**andpackages/loopover-miner/lib/**are insidecoverage.include):Both arms of each new conditional are tested independently on the request and response sides:
contractVersionabsent, present-and-equal, present-and-different, and present-but-not-a-number (plus the non-mapping/optional-chain arm on the response side).Validation commands (repo root)