Skip to content

docs+test: derive the capability lists from the source, and fail the build when they drift - #256

Merged
xerj-org merged 5 commits into
mainfrom
fix/issue-211-doc-drift
Aug 10, 2026
Merged

docs+test: derive the capability lists from the source, and fail the build when they drift#256
xerj-org merged 5 commits into
mainfrom
fix/issue-211-doc-drift

Conversation

@xerj-org

@xerj-org xerj-org commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Fixes #211

An AI agent (Claude Opus 5) wrote this change. Split of verified (commands run, output observed) vs assumed (untested) is at the bottom.

The defect, reproduced at HEAD

The issue reports that internal docs list features that do not exist and omit ones that do. All four claims check out against the source at origin/main:

claim verified how result
pipeline aggs exist but are undocumented cargo test -p xerj-engine --test integration test_bare_count_bucket_script_agrees_below_and_above_the_fast_agg_threshold okbucket_script runs; it appears in no published list
has_child / has_parent documented but rejected parser.rs::parse_has_child returns invalid(...); ast.rs calls the AST variants removed corpses confirmed
dashboard count disagrees with itself lib.rs said 13, seed.rs said 14, seed_specs() has 14 registry ids confirmed
settings count 38/56/60 belongs to #207 left alone — not touched here

Rather than reproduce by eye, the drift was reproduced as a test. Wrapping the existing stale lists in the new markers and running the new guard at HEAD prints exactly the issue's two directions:

engine/README.md section `agg-types` has drifted from the source of truth.
  implemented but undocumented: ["adjacency_matrix", …, "bucket_script", …]   (62 names)
engine/README.md section `rejected-query-types` has drifted from the source of truth.
  implemented but undocumented: ["has_child", "has_parent"]

Root cause, and why the fix is not "edit the prose"

"What XERJ supports" was written in prose in six places and derived from the code in none. Every list was a literal someone had to remember to bump, and nothing failed when they forgot. Correcting the text alone re-creates the defect on the next commit — which is exactly how the current text got there.

So the lists are now derived, and the derivation is enforced:

  • xerj_query::parser::SUPPORTED_QUERY_TYPES (50) and REJECTED_QUERY_TYPES (2). A unit test reads parse_query's dispatch arms out of the file's own source (include_str!, bounded by the match head and its catch-all arm) and fails if the constants and the arms differ in either direction. A second test proves the labels behaviourally in both directions: no supported name may answer unknown query type or refuse with a not supported message, and every rejected name must refuse with exactly that message. (The second half of the supported-side rule was missing until the fourth pass below — see the correction there.)
  • xerj_engine::aggs::SUPPORTED_AGG_TYPES (62), pinned the same way.
  • xerj_console_api::seed::BUILTIN_DASHBOARD_COUNT (14) — one place, pinned to seed_specs().
  • engine/crates/xerj-engine/tests/docs_capability_lists.rs compares engine/README.md and landing/llms-full.txt against those constants, and the README crate map against the workspace members.

The checked regions are <!-- generated:… --> blocks that may hold only backticked type names plus plain-text family labels. A backticked qualifier (`knn (HNSW-served)`) is a hard failure, not a silently mis-parsed entry, and a missing marker fails loudly instead of skipping the file — the guard must not itself become an accepted-and-ignored input (#204).

Prose corrected against the source

  • engine/README.md — 23 query types → 50, plus the 2 rejected ones and why. 15 aggregations → 62. The old table also called cardinality approximate, contradicting aggs.rs; run_cardinality collects a HashSet and returns distinct.len(), so exact is the correct word. Crate map: 11 of 16 crates → all 16 (xerj-autoindex, the flagship feature, was one of the five missing).
  • landing/llms-full.txt §6 — the same two lists, in the file AI agents actually read.
  • ROADMAP.md — removes "26 publicly-documented query types", "~14 additional", "~49 dispatched / roughly 38 run", "15 aggregations plus ~15 more", and the note "(The README under-lists these — a docs gap, not a defect.)". Also removes a claim that is simply false at HEAD: nested does not "return 0 hits" — the inner query is evaluated per nested element in index.rs, and test_nested_query passes. What is genuinely missing is narrower and now stated as such: score_mode is parsed then ignored and inner_hits is not parsed at all, an accepted-and-ignored pair filed under Tracking: accepted-and-ignored is the dominant bug class, six instances found in one review #204. A stale parser.rs:330 citation is replaced by a name.
  • aggs.rs module docs — a nine-row "supported aggregation types" table replaced by a pointer to the constant.
  • xerj-console-api, demo/playbooks/CONSOLE_UX_REWORK.md — the 13/14 disagreements.

Honest scope

The lists are an acceptance claim, not a fidelity claim, and both docs now say so: a listed type parses, plans and executes; that is not a promise that every parameter matches Elasticsearch. Per-type gaps stay in ROADMAP.md; the ES-YAML suite remains the measured answer. weighted_avg is deliberately not in SUPPORTED_AGG_TYPES — it still returns a buried error instead of a 400, which is tracked, not fixed here.

Out of scope on purpose: the settings count (#207, has its own issue), and honouring-or-rejecting nested.score_mode / inner_hits (a behaviour change needing an owner decision).

engine/CLAUDE.md — the file the issue names — is gitignored and cannot appear in this diff. It was corrected in place on the working machine: the two stale lists now point at the constants, and four further drifts were removed (an example commit body carrying a Co-Authored-By: Claude trailer that the repo's own rule forbids; cargo build --release with no -p listed as an approved command; "tests NOT yet run against a live XERJ instance", true in April 2026; and a conformance-suite table whose file counts were wrong in every row — measured 200 files, not 182).

Gate

Run locally in this worktree:

  • cargo fmt --all -- --check — clean
  • cargo clippy -p xerj-query -p xerj-engine -p xerj-console-api --all-targets -- -D warnings — clean
  • cargo test -p xerj-query — 173 + 9 passed, 0 failed
  • cargo test -p xerj-console-api — passed, 0 failed
  • cargo test -p xerj-engine — all binaries pass except two pre-existing, non-related conditions on a machine at load average 43: several painless wall-clock-budget tests flake under contention (all 66 pass with --test-threads=1), and painless_script_limits::call_depth_limit_survives_the_multi_thread_block_in_place_path overflows its stack in a debug build. That last one was re-run on a stashed tree at origin/main and fails identically there — it is not from this change.
  • New tests: docs_capability_lists 6/6, parser::tests::dispatch_table_matches_capability_manifest, parser::tests::manifest_labels_match_observed_behaviour, aggs::capability_manifest_tests::dispatch_table_matches_capability_manifest, seed::tests::seeds_every_registry_dashboard.

No engine behaviour changes, so the ES-YAML conformance gate is untouched by construction — the only non-doc code added is pub const arrays and #[cfg(test)] code.

Verified vs assumed

Verified (ran the command, read the output): every test result above; the drift output quoted at the top; bucket_script executing; nested filtering; run_cardinality being exact; 50/2/62 counts extracted from the constants; the crate-map guard failing when a row is deleted; the stack-overflow reproducing at origin/main.

Assumed (not run): that the ES-YAML conformance suite is unaffected — argued from the diff containing no runtime code path, not measured. That CI's runner does not hit the debug-build stack overflow, since main is currently green with that test present.

Reference-coding: attempted xc.py xerj-search "registry that enumerates all supported query types or aggregation types by name"; the retrieval server on :9200 was down at the time and the task forbids starting it. Skipping is defensible here anyway — the mandate exempts work confined to this repository's own code, and the only non-trivial addition scrapes this repo's dispatch tables. How tantivy or Elasticsearch registers its own query types is not evidence for how XERJ's docs should be pinned to XERJ's parser.


Second pass — the docs site had the same defect (commit 2)

A second agent re-derived the four claims independently before touching
anything, then audited what the first pass had not covered. The first
pass's own claims hold up:

re-checked method result
manifest == parse_query dispatch independent Python scrape of the arm heads: 52 arms, SUPPORTED 50 + REJECTED 2, zero difference either way confirmed
has_child / has_parent are stubs parse_has_child/parse_has_parent are two-line invalid(...) calls (parser.rs:3611, :3625) confirmed
pipeline aggs ship 15 names dispatched in one multi-line arm → run_pipeline_agg (aggs.rs:2979-2994) confirmed
dashboards == 14 14 DashboardSpec literals; registry.js all array has 14 entries confirmed

What it missed is that "the docs" is bigger than this repository's markdown.

Query types the docs published that do not exist

landing/docs/queries.html shipped cards for boosted and
semantic_search. Neither string occurs anywhere under engine/crates
(grep -rn '"boosted"' engine/crates → nothing). The parser has boosting and
semantic; both published names would answer unknown query type.
semantic_search was also an entry in the docs-site search index, which is
duplicated verbatim into all 44 pages — so the site's own search box offered a
query type XERJ has never had. Fixed in all 44.

The page also listed 34 of 50 dispatched types and never mentioned the two that
are recognised-and-rejected, so has_child read as merely absent rather than
deliberately refused. It now carries all 50 plus a Recognised and rejected
section.

Aggregations: 15 of 62, pipeline family invisible

landing/docs/aggregations.html is the page an evaluator reads to answer "does
XERJ do pipeline aggregations?" — and the answer it gave was no, by omission.
It now lists all 62 under Metric / Bucket / Pipeline.

Two implementation claims the source contradicts

  • cardinality — advertised "Exact · bitmap-backed". run_cardinality
    (aggs.rs:6450) collects a HashSet<String> of every distinct value. Exact,
    yes; bitmap-backed, no — and memory grows with cardinality, the opposite of
    what the phrase implies.
  • percentiles — advertised "t-digest · bounded memory". run_percentiles
    (aggs.rs:7451) sorts all values into a Vec<f64> and interpolates. The
    source comment names t-digest as the ES option name, not the algorithm.
    Memory is O(N).

The page intro promised bounded memory on that basis; it is replaced by the
rule that is actually in fast_aggs.rs (FAST_AGG_MIN_DOCS = 10_000), and the
unmeasured "sub-second SIEM top-N over millions of events" phrasing goes with
it.

21 pages pointed at crates that do not exist

Every docs section ends with Source · engine/crates/…. Twenty-one named
pre-rename crates — logs, api, common, server, storage, cluster,
fts, compress — plus an otlp crate that never existed (OTLP ingest is
xerj-api/src/native.rs) and an api/src/middleware/auth.rs that is
xerj-api/src/auth.rs. Every replacement path was checked to exist on disk
before it was written.

Enforcement, so it cannot come back

  • docs_capability_lists.rs gained an HTML extractor — one name per
    <div class="item"> card, card descriptions deliberately not scanned, so
    a name mentioned in prose cannot pass as a capability. Both pages joined the
    checked set and are compared for equality with the constants: a phantom
    name and a missing family each fail.
  • A card whose text is not a bare type name panics instead of being absorbed,
    matching the markdown extractor's rule — the guard must not itself accept and
    ignore (Tracking: accepted-and-ignored is the dominant bug class, six instances found in one review #204).
  • every_source_pointer_in_the_docs_site_resolves walks every page under
    landing/ and fails on any engine/crates/<path> not on disk.
  • seeds_every_registry_dashboard now does what its name says. It compared two
    Rust values to each other; the registry is JavaScript, and that is the
    actual definition of a built-in dashboard. It now reads the all array out of
    xerj-ux/src/dashboards/registry.js, resolves each symbol to its module,
    takes that module's own id, and compares the id sets both ways — closing the
    cross-language half of the drift that produced three different counts.

Watched fail, then pass (each defect re-introduced on the fixed tree, test
run, output read, then reverted):

landing/docs/queries.html      documented but not implemented: ["boosted", "semantic_search"]
landing/docs/aggregations.html implemented but undocumented: [15 pipeline names]
docs site cites source paths that do not exist:
  landing/docs/analyzers.html → engine/crates/fts/src/analyzer.rs
seed_specs() has drifted from xerj-ux/src/dashboards/registry.js
  seeded but not in the registry: ["settings"]

Gate (second pass, re-run after rebase onto origin/main)

cargo fmt --all -- --check clean · cargo clippy -p xerj-console-api -p xerj-engine -p xerj-query --all-targets -- -D warnings clean ·
xerj-console-api 43/43 · xerj-query 173 + 9 · xerj-engine --lib 428/428 ·
docs_capability_lists 9/9. No runtime code changed in this commit — the edits
are HTML, one integration test, and a #[cfg(test)] module.

Still deliberately out of scope

The settings count (38 / 56 / 60) is #207's, and is untouched here to avoid
colliding with that issue's fix. nested.score_mode / inner_hits and the
type query (accepted, and degraded to match_all) are accepted-and-ignored
inputs that need an owner's call on honour-or-reject; they are described
honestly on the page rather than silently fixed.

Reference-coding: skipped, legitimately. The retrieval server on
localhost:9200 was down for this session (curl /_cluster/health → exit 7)
and the task forbids starting it — but the mandate also exempts work confined
to this repository, and this change is documentation plus a test that scrapes
this repo's own dispatch tables and HTML. How tantivy or Lucene documents its
own query registry is not evidence for what XERJ's pages should say.


Correction, and a third pass — the phantom was still on the site (commit 3)

The second commit's message overstates what it fixed, and this is the
correction.
aa3ac3c0 says of semantic_search: "Neither string appears
anywhere in engine/crates … Both would answer unknown query type … Fixed in
all 44."
It was fixed in the 44 embedded search-index blobs only. The two
places a human actually reads still shipped the phantom, on files this PR had
already edited:

where what it still published
landing/docs/playbooks/vector-search.html {"semantic_search": {"field": "embedding", "text": "…"}} as the worked example for the flagship AI-native feature
landing/docs/migration-from-es.html "32 query types … (knn, semantic_search, hybrid)", and an aggregation list with the pipeline family omitted

Re-verified against a live instance — release binary, scratch data dir, ES
port, not the shared retrieval server:

{"query":{"semantic_search":{…}}}                        -> 400 unknown query type `semantic_search`
{"query":{"semantic":{"field":"embedding","text":"…"}}}  -> 400 `query` must be a non-empty string
{"query":{"semantic":{"field":"summary","query":"…","k":10}}} -> 200, correct doc first (_score 0.8247)

Fixed

  1. vector-search.html semantic example — rewritten to
    {"query":{"semantic":{"field":…,"query":…,"k":10}}} (parse_semantic,
    parser.rs:2557, requires field + query; k defaults to 10), with the
    one-line semantic_text mapping that makes it runnable. Both snippets were
    executed verbatim against a live instance before being published.
  2. vector-search.html hybrid example — a second broken body on the same
    page, not previously flagged. It was published at the top level of the
    search body (parse_request only reads query400 Unknown key for a START_OBJECT in [hybrid]) with bare clauses in queries (parse_hybrid
    requires each entry to wrap its clause in queryhybrid.queries[0] missing query). Corrected and re-run. The knn block above it is
    left alone: top-level knn really is handled (es_compat.rs:6991) and
    returns hits.
  3. migration-from-es.htmlsemantic_searchsemantic, the count
    corrected and pinned, the two recognised-and-rejected types explained, and
    the pipeline family named with a link to /docs/aggregations.html.
  4. engine/README.md self-contradiction — "100% ES API compatible —
    drop-in replacement" and "all ES query types" sat 140 lines above this PR's
    own "Recognised and deliberately rejected with a 400 … Any other query type
    answers unknown query type", and are refuted by
    demo/playbooks/ES_COMPATIBILITY.md:65 (40 supported / 9 partial / 9
    unsupported of 58 ES catalog types). Replaced with "Broad ES 8.x wire
    compatibility", pointing at the measured coverage document.
  5. "All xerj aggregations are exact" — a claim this PR itself added
    run_sampler (aggs.rs:9640) sorts matched docs by _score and truncates
    to shard_size (default 200), so every sub-agg under sampler /
    random_sampler / diversified_sampler runs over a sample;
    random_sampler shares that impl and the string probability does not
    occur in aggs.rs. Narrowed to what is true — no probabilistic sketch in
    the metric path
    — with the sampling family named as the deliberate
    exception in engine/README.md, landing/llms-full.txt and
    landing/docs/aggregations.html. ROADMAP.md was missed on that pass
    and kept the strongest form of the claim on a line this PR adds; it is
    corrected in the fourth pass below.
  6. Two more stale counts the guard could not see: landing/pricing
    (twice) and landing/demo both said "38 query types". The parser
    dispatches 50.

The guard now reads the whole published surface

The marked-region checks only saw three files that had opted in; the docs site
is 60+ pages of prose and samples. Three new checks read
landing/**/*.html, landing/llms*.txt, engine/README.md and ROADMAP.md:

  • no_published_surface_names_a_phantom_query_type — a source-derived
    denylist of names the docs have published but the parser has never
    dispatched (boosted, semantic_search), matched on token boundaries so the
    real MCP tool xerj_semantic_search is not a hit. A companion test,
    the_phantom_list_only_holds_names_the_parser_really_lacks, fails the moment
    the parser grows one of those names — the denylist cannot outlive its reason.
  • every_query_clause_in_a_published_sample_is_a_real_query_type
    anything directly under "query": { … } must be in SUPPORTED ∪ REJECTED.
    That is the one position in an ES body where a key's meaning is unambiguous,
    so it needs no denylist. It asserts it is reading something — the guarded
    floor is checked > 20. (The figure "73 clauses across 11 distinct names"
    originally written here was measured at the third pass and is stale: the
    count at this head is 77, re-measured in the fifth pass by forcing the
    floor to fail and reading the number back. The test asserts the floor, not
    an exact count, which is why the drift was invisible — a published number
    pinned to nothing, the same defect this PR exists to remove, this time in
    the PR body rather than the docs.)
  • published_capability_counts_match_the_constants — a published number
    drifts exactly like a published list (32 / 38 / 50 for the same quantity), so
    counts live in <!-- generated:query-type-count --> regions pinned to the
    constants' .len(). Every occurrence in a file listed in COUNT_DOCS
    is checked, not just the first — and COUNT_DOCS was three HTML pages on
    this pass, missing ROADMAP.md, which the fourth pass below adds.

Watched fail, then pass (each defect re-introduced on the fixed tree, test
run, output read, then reverted):

landing/docs/playbooks/vector-search.html:181 names `semantic_search`
landing/docs/playbooks/vector-search.html:180 uses `semantic_search` in query position
landing/docs/migration-from-es.html publishes 32 for `query-type-count`; the source has 50
landing/pricing/index.html:138 publishes 38 for `query-type-count`; the source has 50

The last line is the proof that the second marked count in a file is checked.

Gate (third pass)

cargo fmt --all -- --check clean · cargo clippy -p xerj-engine --all-targets -- -D warnings clean · docs_capability_lists 13/13 · xerj-engine --lib
428/428 · every other engine test binary passes.

Two conditions did not pass locally, and neither contains a line from this
change
— the only Rust edited here is tests/docs_capability_lists.rs, and
both live in separate test binaries built from unmodified source:
painless_script_limits overflows its stack in a debug build (already recorded
on this PR as reproducing at origin/main), and
query_string_default_field::field_less_query_string_cross_product_respects_the_request_deadline
is a 150 ms wall-clock-budget test that ran past its 3 s allowance on a box at
load average 320 (32 cores). Both passed in CI's Build + Test on this
branch head before this commit; CI re-runs on the push.

Reference-coding

Retrieved this time — the server was up, where the previous pass recorded it
down. xc.py xerj-search "registerQuery SearchModule QueryBuilder named writeable registry query type names" returns
elasticsearch/server/src/main/java/org/elasticsearch/search/SearchModule.java
as the top hit: the analogous pattern, one registration site where the wire
name and the builder are declared together so the registry is the single source
of truth. APPROACH-ONLY (AGPL-3.0 / SSPL-1.0 / Elastic-2.0) — nothing
copied, and it confirms rather than changes the design already in this PR. An
earlier query for a capability manifest returned ESQL datasource plumbing;
irrelevant, and recorded as such rather than forced in.


Fourth pass — the three findings the merge gate left open (commit 4)

The merge gate re-measured this branch at d470eecf and kept three findings
open. All three were real. They are fixed here, each with the defect
re-introduced on the fixed tree first so the guard was watched failing.

1. ROADMAP.md published a false claim — one this PR itself added

ROADMAP.md:14 said, on a + line introduced by this PR:

All aggregations are exact (no HLL, no sampling).

That is false at HEAD, and the same PR's other files already said so:

measured source
sampler and random_sampler both dispatch to run_sampler aggs.rs:2934
run_sampler sorts matches by _score and .take(shard_size), unwrap_or(200) aggs.rs:9640-9659
diversified_sampler truncates the same way aggs.rs:2944
grep -c probability engine/crates/xerj-engine/src/aggs.rs 0 — ES's probability is accepted and ignored (#204)
percentiles with hdr quantizes through a DoubleHistogram replica aggs.rs:7460, 7505

Three of those sampling aggregations are inside the very count the sentence
covered, so the release would have shipped ROADMAP.md telling a reader that a
random_sampler job is exact while engine/README.md told them the same job
silently truncates to 200 documents. The sentence is replaced by an
Exactness, precisely paragraph that states what is true (no probabilistic
sketch in the metric path — cardinality is a true distinct count, terms
doc_count is precise) and then names both exceptions: the sampling family
with its shard_size default and its ignored probability, and hdr
percentiles.

hdr is the second exception and was missing everywhere, not just in
ROADMAP.md. engine/README.md and landing/llms-full.txt said the sampling
family was the deliberate exception; both now say there are two.
landing/docs/aggregations.html already described the hdr switch on the
percentiles card and needed no change.

2. ROADMAP.md's own counts were pinned to nothing

grep -c 'generated:' ROADMAP.md returned 0, and COUNT_DOCS listed three
HTML pages only. So this PR published 50 query types and 62 aggregation types in ROADMAP.md as hand-typed literals — recreating, inside its own fix,
the drift #211 exists to stop. Adding query type 51 would have left ROADMAP
saying 50 with every test green.

Fixed: ROADMAP.md joins COUNT_DOCS, and its three numbers now live in
<!-- generated:query-type-count -->, <!-- generated:rejected-query-type-count -->
and <!-- generated:agg-type-count --> regions. HTML comments are invisible in
rendered markdown, so the marker mechanism ports unchanged. The hand-copied
15-name pipeline list is deleted rather than marked — it was a second
unguarded copy — and replaced by a pointer to the machine-checked list. The
prose repeats of the numbers ("those 50 are the dispatch surface", "not among
the 62") are reworded so no unmarked literal remains. The floor on how many
marked counts must be found rises from 5 to 9, so a page dropping its markers
still fails.

Watched fail, then pass5051 and 6261 on the fixed tree:

ROADMAP.md:12 publishes 51 for `query-type-count`; the source has 50

3. manifest_labels_match_observed_behaviour could not see a mislabelled type

The gate reproduced this by measurement and it reproduced here identically.
Moving "has_child" from REJECTED_QUERY_TYPES into SUPPORTED_QUERY_TYPES
left both manifest tests green, because a recognised-and-refused type never
produces ParseError::UnknownQueryType — and the docs guard would then have
required has_child to be published as a supported capability. That is the
#211 defect, undetected by the test written to detect it.

Fixed at parser.rs:5679: the supported-side loop now also asserts the parse
error does not contain not supported — the phrase REJECTED_QUERY_TYPES is
required to carry is the phrase SUPPORTED_QUERY_TYPES is forbidden to carry.

Watched fail, then passhas_child relabelled supported on the fixed
tree:

`has_child` is listed as supported but refuses with a not-supported message:
parse error: parent-child join queries (has_child/has_parent) are not supported; …
  A recognised-and-refused type belongs in REJECTED_QUERY_TYPES, or the docs
  will advertise a query that 400s.

Reverted, both tests pass again (2 passed; 0 failed).

Known gaps, stated plainly

These are not fixed, and no sentence in this PR should be read as claiming
otherwise:

  • The count guard is opt-in. It checks every occurrence in every file
    listed in COUNT_DOCS — now four files — and nothing else. A new page that
    publishes "XERJ supports N query types" without a marker is unguarded, and no
    test will notice. There is no repo-wide scanner for numbers in capability
    sentences.
  • Aggregation names outside a marked region are unguarded. Query types
    have a phantom denylist (no_published_surface_names_a_phantom_query_type)
    and a query-position check; aggregations have neither. The three exemplar
    names left in ROADMAP.md (bucket_script, derivative, moving_fn) are
    true today and checked by nobody.
  • The labels test proves acceptance, not fidelity. It sends {"<type>": {}}
    and reads the error. A type that accepts that body and then does nothing
    useful still reads as supported — type is mapped to match_all and nested
    ignores score_mode/inner_hits, both documented under Partial in
    ROADMAP.md and both counted among the 50.
  • weighted_avg still returns HTTP 200 with a buried error instead of a 400
    (tracked in ROADMAP.md), and the settings count (38/56/60) remains Public claims contradicted by the source: cargo-audit and fuzzing are documented but not in CI #207's.

Gate (fourth pass)

Run in this worktree, CARGO_TARGET_DIR=/home/claude/.cargo-shared-target/shard-2:

  • cargo fmt --all -- --check — clean
  • cargo clippy -p xerj-query -p xerj-engine --all-targets -- -D warnings — clean
  • cargo test -p xerj-query173 + 9 passed, 0 failed
  • cargo test -p xerj-engine --lib428 passed, 0 failed
  • cargo test -p xerj-engine --test docs_capability_lists13 passed, 0 failed
  • cargo test -p xerj-engine681 passed, 0 failed across every test binary,
    then aborts in painless_script_limits on
    call_depth_limit_survives_the_multi_thread_block_in_place_path, which
    overflows its stack in a debug build. Already recorded on this PR as
    reproducing on a stashed tree at origin/main; it contains no line from this
    change. The eight test binaries that sort after it were run individually and
    all pass (perf_benchmark, product_experience,
    query_string_default_field, rc4_w2_storage_hardening,
    search_bounded_under_ghosts, search_context_ttl,
    shard_router_write_path, tombstone_only_segment_search).

The ES-YAML conformance suite is not re-run for this pass and does not need to
be: the fourth commit touches ROADMAP.md, engine/README.md,
landing/llms-full.txt, one #[cfg(test)] block in parser.rs and one
integration-test file. No runtime code path is edited.

Reference-coding: skipped, and the reason is the same one the mandate names
as an exemption — every edit here is either this repository's own prose or a
test that scrapes this repository's own constants. How another engine documents
its query registry is not evidence for what XERJ's ROADMAP.md should say
about run_sampler's shard_size.


Fifth pass — re-measured the gate's three findings, and found the #204 bug class inside the guard itself (commit 5)

The gate's three findings, re-measured rather than re-read

The fourth pass claims all three are fixed. Each was re-verified here by
re-introducing the defect on the fixed tree and reading the failure, not by
trusting the commit message:

finding how it was re-measured result
ROADMAP.md:14 "All aggregations are exact (no HLL, no sampling)." grep -rn "no HLL|all aggregations are exact" across *.md *.txt *.html *.rs 0 hits — gone repo-wide
ROADMAP.md counts pinned to nothing 5051 in the marked region fails: ROADMAP.md:12 publishes 51 for `query-type-count`; the source has 50
manifest_labels_match_observed_behaviour cannot see a mislabelled type "has_child" moved from REJECTED_QUERY_TYPES into SUPPORTED_QUERY_TYPES fails: `has_child` is listed as supported but refuses with a not-supported message (1 passed; 1 failed, where the gate measured 2 passed)

The replacement wording in ROADMAP.md was also checked against the source
rather than against the previous commit: aggs.rs:2934 dispatches sampler
and random_sampler to run_sampler; run_sampler sorts by _score and
.take(shard_size) with unwrap_or(200); grep -c probability aggs.rs is
0; the hdr branch (aggs.rs:7458-7520) really does replicate a
DoubleHistogram auto-ranging conversion, so "quantized" is the right word;
run_cardinality collects a HashSet and returns distinct.len(). The
nested rewrite checks out too — index.rs:29116 is
arr.iter().any(|elem| doc_matches_query(&inner, elem)), score_mode is
parsed at parser.rs:3139 and dropped by the QueryNode::Nested { path, query, .. }
destructure at index.rs:26757, and inner_hits does not occur in parser.rs
at all.

What that review turned up: the guard was swallowing unreadable pages

Five sites in tests/docs_capability_lists.rs discarded a read failure, so a
page that could not be read was scanned as an empty string — and an empty
string passes every check below it by finding nothing:

every_source_pointer_in_the_docs_site_resolves
  html_files:  let Ok(entries) = read_dir(dir) else { return };  +  entries.flatten()
  page body:   read_to_string(page).unwrap_or_default()
doc_surfaces  (feeds both site-wide checks)
  walk:        the same read_dir / flatten pair
no_published_surface_names_a_phantom_query_type
every_query_clause_in_a_published_sample_is_a_real_query_type
  file body:   read_to_string(&file).unwrap_or_default()

The failure mode is a confident wrong answer: "no phantom query type on this
page", for a page that was never read. The pages.len() > 20 and
files.len() > 20 floors do not cover the directory half either — one
subdirectory becoming unreadable drops its pages while sixty others keep the
total above the floor.

Both are hard failures now (read_dir_or_panic / read_surface_or_panic),
each naming the offending path. This matters beyond tidiness: this PR's own
prose claims twice that the guard "must not itself become an accepted-and-ignored
input (#204)", and until this commit that was true of the marker and card
extractors but not of the three site-wide walkers added in the second and third
passes.

Watched fail, then pass — a non-UTF-8 landing/docs/__gate_probe.html:

test every_query_clause_in_a_published_sample_is_a_real_query_type ... FAILED
test no_published_surface_names_a_phantom_query_type ... FAILED
test every_source_pointer_in_the_docs_site_resolves ... FAILED
cannot read published surface .../landing/docs/__gate_probe.html: stream did not
contain valid UTF-8 — an unreadable page must fail this test; treating it as
empty would silently pass every check

Removed, and docs_capability_lists is 13/13 again. The rest of the new test
code was scanned for the same pattern (let _ =, if let Ok(..) with no else,
.ok(), unwrap_or_default, flatten()) in seed.rs and parser.rs; there
are no other instances.

Known gaps, restated — nothing here is claimed to be fixed

Unchanged from the fourth pass, and still true:

  • The count guard is opt-in. Only the four files in COUNT_DOCS are
    checked, and only markers whose section name is in COUNT_SECTIONS. A new
    page publishing "XERJ supports N query types" without a marker — or with a
    mistyped marker name — is unguarded, and no test will notice.
  • Aggregation names outside a marked region are unguarded. Query types
    have a phantom denylist and a query-position check; aggregations have
    neither. The three exemplar names left in ROADMAP.md (bucket_script,
    derivative, moving_fn) are true today and checked by nobody.
  • The labels test proves acceptance, not fidelity. type is mapped to
    match_all and nested ignores score_mode / inner_hits; both are
    counted among the 50 and both are documented under Partial in ROADMAP.md.
  • weighted_avg still returns HTTP 200 with a buried error instead of a 400,
    and the settings count (38 / 56 / 60) remains Public claims contradicted by the source: cargo-audit and fuzzing are documented but not in CI #207's.
  • ROADMAP.md:11's ES-YAML figure ("1,365 / 1,368") is not touched by this
    PR and was not re-measured here.
    It disagrees with the "1,360 / 1,363"
    figure further down the same file. Correcting it needs a conformance run and
    belongs to whoever owns that number; it is called out rather than quietly
    left.

Gate (fifth pass)

Run in this worktree, CARGO_TARGET_DIR=/home/claude/.cargo-shared-target/shard-2:

  • cargo fmt --all -- --check — clean
  • cargo clippy -p xerj-query -p xerj-engine -p xerj-console-api --all-targets -- -D warnings — clean
  • cargo test -p xerj-query173 + 9 passed, 0 failed
  • cargo test -p xerj-console-api108 passed, 0 failed
  • cargo test -p xerj-engine --no-fail-fast710 passed, 0 failed across
    every test binary. --no-fail-fast is the change from the fourth pass: the
    eight binaries that sort after painless_script_limits were run as part of
    the same invocation rather than individually.
  • cargo test -p xerj-engine --test docs_capability_lists13 passed, 0 failed

One condition still does not pass locally: painless_script_limits::… aborts
with fatal runtime error: stack overflow in a debug build. This pass stopped
asserting that from memory and measured it — git checkout origin/main -- engine/
in this worktree and re-running that one binary aborts identically, so it
is not from this branch. It also passes in CI's Build + Test on this branch
head.

The ES-YAML conformance suite was not re-run and does not need to be: the only
non-test change anywhere on this branch is three pub const arrays
(SUPPORTED_QUERY_TYPES, REJECTED_QUERY_TYPES, SUPPORTED_AGG_TYPES) plus
BUILTIN_DASHBOARD_COUNT; everything else is #[cfg(test)] code, one
integration-test file, doc comments, markdown and HTML. CI's own ES-compat YAML
conformance check is green on this branch.

Merge-forward check: the branch is 31 commits behind origin/main. A trial
git merge --no-commit origin/main in this worktree merges cleanly, and
docs_capability_lists (13/13) and both parser manifest tests stay green on
the merged tree — so main's drift has not invalidated any published list or
count. The merge was aborted; the branch is not rebased.

Reference-coding: skipped, under the mandate's own exemption — every edit
in this pass is either this repository's prose or a test that reads this
repository's own constants and pages. How another engine documents its query
registry is not evidence about read_to_string(..).unwrap_or_default() in
XERJ's guard.

@cla-bot cla-bot Bot added the cla-signed label Aug 9, 2026
…build when they drift

A source review of this repo concluded XERJ has no pipeline aggregations. It
has fifteen, shipped and tested. The reviewer had read a hand-maintained list
that stopped at `composite`, and the conclusion nearly became a roadmap item to
build a feature that already exists. The same lists advertised `has_child` and
`has_parent`, which `parse_query` rejects with a 400. Two counts of the same
thing disagreed with each other in the same crate.

Root cause: "what XERJ supports" was written down in prose in six places and
derived from the code in none of them. Every list was a literal someone had to
remember to bump, and nothing failed when they forgot. Correcting the prose
alone would have re-created the defect on the next commit, so this change makes
the lists derived and the derivation enforced.

## What is now the source of truth

- `xerj_query::parser::SUPPORTED_QUERY_TYPES` (50) and `REJECTED_QUERY_TYPES`
  (2). `parser::tests::dispatch_table_matches_capability_manifest` reads
  `parse_query`'s dispatch arms out of the file's own source
  (`include_str!`, bounded by the `match` head and its catch-all arm) and fails
  if the constants and the arms differ in either direction.
  `manifest_labels_match_observed_behaviour` then proves the labels: every
  supported name must not answer `unknown query type`, and every rejected name
  must refuse with an explanatory message.
- `xerj_engine::aggs::SUPPORTED_AGG_TYPES` (62), pinned to the agg dispatch
  table by the same technique.
- `xerj_console_api::seed::BUILTIN_DASHBOARD_COUNT` (14) — the single place the
  dashboard count is written, pinned to `seed_specs()` by
  `seeds_every_registry_dashboard`.

## What is now checked

`engine/crates/xerj-engine/tests/docs_capability_lists.rs` compares
`engine/README.md` and `landing/llms-full.txt` against those constants. The
lists live in `<!-- generated:… -->` regions that may contain only backticked
type names and plain-text family labels; a backticked qualifier such as
`` `knn (HNSW-served)` `` is a hard failure rather than a silently mis-parsed
entry, and a missing marker fails loudly instead of skipping the file. A sixth
test holds the README crate map to the workspace members, counting only table
rows so a crate merely name-dropped in prose does not satisfy it.

## Before / after

Wrapping the old lists in markers and running the new test at HEAD reports
exactly the drift the issue describes:

    engine/README.md section `agg-types` has drifted from the source of truth.
      implemented but undocumented: [62 names, the whole pipeline family among them]
    engine/README.md section `rejected-query-types` has drifted...
      implemented but undocumented: ["has_child", "has_parent"]

After: 6 passed, 0 failed. Removing one crate-map row reproduces the crate-map
failure; both were watched fail and pass.

## Prose corrected against the source

- `engine/README.md` — 23 query types listed becomes 50 plus the two rejected
  ones; 15 aggregations becomes 62. The old table also called `cardinality`
  "approximate", contradicting `aggs.rs`; `run_cardinality` collects a
  `HashSet` and returns `distinct.len()`, so it is exact. Crate map went from
  11 of 16 crates to all 16 — `xerj-autoindex`, the flagship feature, was one
  of the five missing.
- `landing/llms-full.txt` §6 — the same two lists, in the doc AI agents read.
- `ROADMAP.md` — "26 publicly-documented query types", "~14 additional", "~49
  dispatched / roughly 38 run", "15 aggregations plus ~15 more" and the note
  "(The README under-lists these — a docs gap, not a defect.)" all go. So does
  the claim that `nested` "returns 0 hits (no real nested-document
  indexing/scoring)": the inner query is evaluated per nested element in
  `index.rs`, and `test_nested_query` passes at HEAD. What is actually missing
  there is narrower and is now stated as such — `score_mode` is parsed and
  ignored and `inner_hits` is not parsed at all, an accepted-and-ignored pair
  (#204). A stale `parser.rs:330` citation is replaced by a name.
- `aggs.rs` module docs — a nine-row table of "supported aggregation types"
  replaced by a pointer to the constant.
- `xerj-console-api` — `lib.rs` said 13 built-in dashboards, `seed.rs` said 14,
  `seed_specs()` has 14. The number now appears once, and the prose links to it.
- `demo/playbooks/CONSOLE_UX_REWORK.md` — said 13 and 14 in the same sentence.

## Honest scope

The lists are an *acceptance* claim, not a fidelity claim, and both docs now
say so: a listed type parses, plans and executes, which is not a promise that
every parameter matches Elasticsearch. Per-type gaps stay in ROADMAP.md and the
ES-YAML suite remains the measured answer. `weighted_avg` is deliberately not
in `SUPPORTED_AGG_TYPES` — it still returns a buried error instead of a 400,
which is tracked, not fixed here.

Fixes #211
… not exist, and no pipeline aggs

The first pass of #211 fixed the lists in `engine/README.md`, `llms-full.txt`,
`ROADMAP.md` and `engine/CLAUDE.md`, and pinned them to the source. It stopped
at the repository's markdown. The published docs site carried the identical
defect in both directions, and nothing checked it:

* `landing/docs/queries.html` shipped cards for **`boosted`** and
  **`semantic_search`**. Neither string appears anywhere in `engine/crates`
  (`grep -rn '"boosted"' engine/crates` → nothing); the parser has `boosting`
  and `semantic`. Both would answer `unknown query type`. `semantic_search` was
  also an entry in the docs-site search index duplicated into all 44 pages, so
  the site's own search offered a query type that does not exist.
* The page listed 34 of the 50 dispatched types and never mentioned the two
  that are recognised-and-rejected, so `has_child` looked merely absent rather
  than deliberately refused.
* `landing/docs/aggregations.html` listed 15 of 62 aggregations, with the whole
  pipeline family missing — the exact omission that made a reviewer conclude
  XERJ has no pipeline aggregations and nearly file work to build them again.
* 21 pages footed a section with `Source · engine/crates/<crate>/…` naming
  crates that have not existed under those paths since the workspace became
  `xerj-*`: `logs`, `api`, `common`, `server`, `storage`, `cluster`, `fts`,
  `compress`, plus an `otlp` crate that never existed (OTLP ingest lives in
  `xerj-api/src/native.rs`). A reader following one lands nowhere.

Two claims in the same card grids are contradicted by the code they describe:
`cardinality` was advertised as "bitmap-backed" when `run_cardinality` collects
a `HashSet<String>` of every distinct value, and `percentiles` as "t-digest ·
bounded memory" when `run_percentiles` sorts *all* values into a `Vec<f64>` and
interpolates — the source comment names t-digest as the ES *option*, not the
algorithm. Both are now described as what the code does; the intro sentence
that promised bounded memory is gone, replaced by the columnar-path rule that
is actually in `fast_aggs.rs` (`FAST_AGG_MIN_DOCS = 10_000`).

## What is now enforced

`docs_capability_lists.rs` grew an HTML extractor (one name per
`<div class="item">` card, descriptions deliberately not scanned) and both
pages joined the checked set, so the grids are compared for **equality** with
`SUPPORTED_QUERY_TYPES` / `REJECTED_QUERY_TYPES` / `SUPPORTED_AGG_TYPES` — a
phantom name and a missing family both fail. A card whose text is not a bare
type name panics rather than being absorbed, matching the markdown extractor's
rule (#204: no accepted-and-ignored input, including in the guard itself).
`every_source_pointer_in_the_docs_site_resolves` walks every page under
`landing/` and fails on any `engine/crates/<path>` that is not on disk.

Watched fail, then pass: re-adding the `boosted` card and renaming `semantic`
back reproduces `documented but not implemented: ["boosted", "semantic_search"]`;
deleting the pipeline section reproduces the 15 undocumented names; reverting
one crate path reproduces `landing/docs/analyzers.html → engine/crates/fts/…`.

## Dashboard count: the third source

`seeds_every_registry_dashboard` promised in its name — and
`CONSOLE_UX_REWORK.md` promised in prose — that the count is "pinned to the
registry list". It only compared `seed_specs().len()` with the constant beside
it. The registry is JavaScript (`xerj-ux/src/dashboards/registry.js`), and it
is the actual definition of a built-in dashboard; comparing two Rust values to
each other left the cross-language half of the drift unchecked, which is how
#211 found three different numbers for one thing. The test now reads the `all`
array out of `registry.js`, resolves each symbol to its module, takes that
module's own `id`, and compares the id *sets* both ways. Dropping `settings`
from `registry.js` now fails with `seeded but not in the registry: ["settings"]`.

Gate: `cargo fmt --all --check` clean; `cargo clippy -p xerj-console-api -p
xerj-engine -p xerj-query --all-targets -D warnings` clean; `xerj-console-api`
43/43, `xerj-query` 173+9, `xerj-engine --lib` 428/428,
`docs_capability_lists` 9/9. No runtime code changed — the only non-doc edits
are a test file and a `#[cfg(test)]` module.

Fixes #211
@xerj-org
xerj-org force-pushed the fix/issue-211-doc-drift branch from 6bb21da to aa3ac3c Compare August 9, 2026 21:00
…he guard see them

Review of PR #256 found the second commit's own claim to be false as written.
`aa3ac3c0` said of `semantic_search`: "Neither string appears anywhere in
`engine/crates` … Fixed in all 44." It was fixed in the 44 embedded
search-index blobs only. The docs site went on publishing the phantom in the
two places a human actually reads.

Verified against a live instance (release binary, scratch data dir, ES port):

  {"query":{"semantic_search":{...}}}
    -> 400 parse error: unknown query type `semantic_search`
  {"query":{"semantic":{"field":"embedding","text":"…"}}}
    -> 400 parse error: `query` must be a non-empty string
  {"query":{"semantic":{"field":"summary","query":"…","k":10}}}
    -> 200, correct doc first (_score 0.8247)

## What was still wrong

* `landing/docs/playbooks/vector-search.html` shipped
  `{"semantic_search": {"field": "embedding", "text": "…"}}` as *the* worked
  example under "Semantic search · embed at query time" — the flagship
  AI-native feature. Two defects in one body: the clause name has never
  existed (`parse_semantic`, parser.rs:2557, is reached by `semantic`) and the
  parameter is `query`, not `text`. Rewritten to the real form, with the
  one-line `semantic_text` mapping that makes it runnable — both snippets
  executed verbatim against a live instance before being published.

* Same page, `hybrid`: published at the top level of the search body and with
  bare clauses in `queries`. `parse_request` only reads `query`, so the body
  answers `400 Unknown key for a START_OBJECT in [hybrid]`, and
  `parse_hybrid` requires each entry to wrap its clause in `query`
  (`hybrid.queries[0] missing query`). Both corrected and re-run. The `knn`
  block above it is left alone: top-level `knn` really is handled
  (es_compat.rs:6991) and returns hits.

* `landing/docs/migration-from-es.html` — "32 query types … (knn,
  semantic_search, hybrid)" and an aggregation list that stopped at
  `composite`, i.e. the pipeline family omitted. That is the exact
  by-omission defect #211 was filed for, on a page this PR had already
  edited.

* `engine/README.md` contradicted itself. "100% ES API compatible — drop-in
  replacement" and "all ES query types" sat 140 lines above this PR's own new
  "Recognised and deliberately rejected with a 400: has_child, has_parent /
  Any other query type answers `unknown query type`". Both lines are also
  refuted by demo/playbooks/ES_COMPATIBILITY.md:65 (40 supported, 9 partial,
  9 unsupported of 58 ES catalog types).

* "All xerj aggregations are exact" (README) / "complete; all exact"
  (llms-full.txt) were **added by this PR** — and three lines below, the same
  generated block lists `sampler`, `random_sampler`, `diversified_sampler`.
  `run_sampler` (aggs.rs:9640) sorts matched docs by `_score` and truncates to
  `shard_size` (default 200), so every sub-agg under one runs over a sample;
  `random_sampler` shares that impl and never reads ES's `probability` (the
  string does not occur in aggs.rs). Narrowed to the claim that is true — no
  probabilistic sketch in the metric path — with the sampling family named as
  the deliberate exception, in the README, in llms-full.txt, and on
  landing/docs/aggregations.html.

* Two more stale counts the same guard could not see: `landing/pricing`
  (twice) and `landing/demo` both said "38 query types". The parser
  dispatches 50.

## Why the guard missed all of it, and what changed

The marked-region checks only see a list that opted in — three files. The
docs site is 60+ pages of prose and samples. Three checks now read the whole
published surface (`landing/**/*.html`, `landing/llms*.txt`,
`engine/README.md`, `ROADMAP.md`):

* `no_published_surface_names_a_phantom_query_type` — a source-derived
  denylist of names the docs have published that the parser has never
  dispatched (`boosted`, `semantic_search`), matched on token boundaries so
  the real MCP tool `xerj_semantic_search` is not a hit. A companion test
  retires an entry the moment the parser grows the name, so the denylist
  cannot outlive its reason.
* `every_query_clause_in_a_published_sample_is_a_real_query_type` — anything
  directly under `"query": { … }` in a published sample must be in
  SUPPORTED ∪ REJECTED. That is the one position in an ES body where a key's
  meaning is unambiguous, so it needs no denylist. It reads 73 clauses across
  11 distinct names today, and asserts it is reading something.
* `published_capability_counts_match_the_constants` — a published *number*
  drifts exactly like a published list (32 / 38 / 50 for the same quantity),
  so counts now live in `<!-- generated:query-type-count -->` regions pinned
  to the constants' `.len()`, every occurrence checked, not just the first.

Watched fail, then pass — each defect re-introduced on the fixed tree, test
run, output read, then reverted:

  landing/docs/playbooks/vector-search.html:181 names `semantic_search`
  landing/docs/playbooks/vector-search.html:180 uses `semantic_search` in query position
  landing/docs/migration-from-es.html publishes 32 for `query-type-count`; the source has 50
  landing/pricing/index.html:138 publishes 38 for `query-type-count`; the source has 50

The last one is the proof that the *second* occurrence in a file is checked.

## Gate

`cargo fmt --all -- --check` clean · `cargo clippy -p xerj-engine
--all-targets -- -D warnings` clean · `docs_capability_lists` 13/13 ·
`xerj-engine --lib` 428/428 · all other engine test binaries pass.

Two conditions did not pass locally and neither contains a line from this
change — both live in test binaries built from unmodified source, while the
only Rust edited here is `tests/docs_capability_lists.rs`, a separate binary:
`painless_script_limits` overflows its stack in a debug build (already
recorded on this PR as reproducing at `origin/main`), and
`query_string_default_field::field_less_query_string_cross_product_respects_the_request_deadline`
is a 150ms wall-clock budget test that ran past its 3s allowance on a box at
load average 320 (32 cores). Both passed in CI's Build + Test on this branch
head before this commit.

Reference-coding: retrieved this time (the server was up; the previous commit
recorded it down). `xc.py xerj-search "registerQuery SearchModule
QueryBuilder named writeable registry query type names"` returns
`elasticsearch/server/src/main/java/org/elasticsearch/search/SearchModule.java`
as the top hit — the analogous pattern, one registration site where the wire
name and the builder are declared together so the registry is the single
source of truth. APPROACH-ONLY (AGPL-3.0 / SSPL-1.0 / Elastic-2.0): nothing
copied, and it confirms rather than changes the design already in this PR.
A first query for a capability *manifest* returned ESQL datasource plumbing —
irrelevant, and recorded as such.
The merge gate re-measured this branch and kept three findings open. All
three were real; this is the correction.

1. ROADMAP.md:14 published "All aggregations are exact (no HLL, no
   sampling)" on a line THIS BRANCH ADDED, while the same branch's
   engine/README.md, landing/llms-full.txt and landing/docs/aggregations.html
   had already been corrected to say the opposite. Measured at HEAD:
   aggs.rs:2934 dispatches sampler and random_sampler to run_sampler;
   run_sampler (aggs.rs:9640-9659) sorts matches by _score and
   .take(shard_size) with unwrap_or(200); aggs.rs:2944 truncates
   diversified_sampler the same way; `grep -c probability aggs.rs` = 0, so
   ES's probability is accepted and ignored (#204). Three of those
   aggregations sit inside the 62 the sentence covered.

   Replaced by an "Exactness, precisely" paragraph that claims only what is
   measurable — no probabilistic sketch in the metric path, cardinality a
   true distinct count, terms doc_count precise — and then names BOTH
   exceptions. The second exception, percentiles with `hdr` (aggs.rs:7460,
   7505: a DoubleHistogram auto-ranging replica, so values are quantized),
   was missing from engine/README.md and llms-full.txt too; both now say
   there are two exceptions rather than one.

2. ROADMAP.md's own counts were pinned to nothing. `grep -c generated:
   ROADMAP.md` was 0 and COUNT_DOCS held three HTML pages, so this branch
   published "50 query types" and "62 types" as hand-typed literals —
   re-creating, inside its own fix, the drift #211 exists to stop. Adding
   query type 51 would have left ROADMAP saying 50 with every test green.

   ROADMAP.md joins COUNT_DOCS and its three numbers move into
   generated:query-type-count / generated:rejected-query-type-count /
   generated:agg-type-count regions (HTML comments are invisible in rendered
   markdown, so the mechanism ports unchanged). The hand-copied 15-name
   pipeline list is deleted rather than marked — it was a second unguarded
   copy — and the prose repeats of the numbers are reworded away. The floor
   on marked counts found rises 5 -> 9.

   Watched fail: 50 -> 51 gives
     ROADMAP.md:12 publishes 51 for `query-type-count`; the source has 50

3. parser.rs manifest_labels_match_observed_behaviour could not see a
   mislabelled type. Moving "has_child" from REJECTED_QUERY_TYPES into
   SUPPORTED_QUERY_TYPES left both manifest tests green (2 passed; 0
   failed), because a recognised-and-refused type never produces
   ParseError::UnknownQueryType — and the docs guard would then have
   REQUIRED has_child to be published as a capability. That is the #211
   defect, undetected by the test written to detect it.

   parser.rs:5679 now also asserts the supported-side parse error does not
   contain "not supported": the phrase REJECTED_QUERY_TYPES must carry is
   the phrase SUPPORTED_QUERY_TYPES may not carry. Watched fail with
   has_child relabelled, then reverted and green.

Known gaps, now stated in the PR body rather than implied away: the count
guard is opt-in (a new page with no marker is unchecked); aggregation NAMES
outside a marked region have no phantom denylist the way query types do; and
the labels test proves acceptance, not fidelity — `type` is still mapped to
match_all and `nested` still ignores score_mode/inner_hits, both counted
among the 50 and both documented under Partial.

Gate: cargo fmt clean; clippy -p xerj-query -p xerj-engine --all-targets
-D warnings clean; xerj-query 173 + 9 passed / 0 failed; xerj-engine --lib
428 passed; docs_capability_lists 13 passed; cargo test -p xerj-engine 681
passed / 0 failed before the pre-existing debug-build stack overflow in
painless_script_limits (reproduces at origin/main), with the eight test
binaries after it run individually and all passing. No runtime code path is
edited, so the ES-YAML conformance gate is untouched by construction.
The merge gate's three findings were closed in the previous commit and
re-measured here (has_child relabelled -> manifest_labels_match_observed_behaviour
fails with the not-supported message; ROADMAP 50 -> 51 ->
"ROADMAP.md:12 publishes 51 for `query-type-count`; the source has 50").

Reviewing the guard itself turned up the defect class it exists to prevent,
inside its own code. Five sites in tests/docs_capability_lists.rs swallowed a
read failure:

  every_source_pointer_in_the_docs_site_resolves
    html_files: `let Ok(entries) = read_dir(dir) else { return }`
                `entries.flatten()`
    page body:  `read_to_string(page).unwrap_or_default()`
  doc_surfaces (feeds the phantom + query-position checks)
    walk:       the same read_dir / flatten pair
  no_published_surface_names_a_phantom_query_type
  every_query_clause_in_a_published_sample_is_a_real_query_type
    file body:  `read_to_string(&file).unwrap_or_default()`

An unreadable or non-UTF-8 page became an empty string, and an empty string
passes every check below by finding nothing -- a confident "no phantom query
type on this page" for a page that was never read. The `pages.len() > 20` /
`files.len() > 20` floors do not catch the directory half either: one
subdirectory going unreadable drops its pages while sixty others keep the
total above the floor.

Both are now hard failures via read_dir_or_panic / read_surface_or_panic,
which name the path in the panic. This is the accepted-and-ignored pattern of
#204, and the file's own module docs and the PR body both claim the guard does
not do it; it does not now.

Watched fail, then pass: a non-UTF-8 landing/docs/__gate_probe.html made all
three site-wide checks fail with

  cannot read published surface .../landing/docs/__gate_probe.html: stream did
  not contain valid UTF-8 -- an unreadable page must fail this test; treating
  it as empty would silently pass every check

then 13/13 green once removed.

Gate: cargo fmt --all -- --check clean; cargo clippy -p xerj-query -p
xerj-engine -p xerj-console-api --all-targets -D warnings clean; xerj-query
173 + 9 passed / 0 failed; xerj-console-api 108 passed / 0 failed; xerj-engine
710 passed / 0 failed across every test binary run with --no-fail-fast, the
sole exception being the pre-existing debug-build stack overflow in
painless_script_limits, which this session re-measured on an engine/ tree
checked out from origin/main and which aborts there identically. No runtime
code path is edited -- the only non-test change on this branch remains the
three `pub const` arrays -- so the ES-YAML conformance gate is untouched.
@xerj-org
xerj-org merged commit 80f1129 into main Aug 10, 2026
12 checks passed
@xerj-org
xerj-org deleted the fix/issue-211-doc-drift branch August 10, 2026 13:18
xerj-org added a commit that referenced this pull request Aug 10, 2026
Eleven PRs landed on main today and every open branch went CONFLICTING.
Nothing here is a defect in either side — main moved underneath this one.
Three textual conflicts, all resolved by keeping both intents.

**CHANGELOG.md** — both sides appended bullets under the same
`## [Unreleased] / ### Fixed` heading. Purely additive on both sides; #207's
block is kept ahead of main's `index: false` (#260), phrase-semantics (#265),
tied-score (#267) and autoindex-generation (#259) entries. Nothing dropped.

**engine/crates/xerj-query/src/parser.rs** — both sides appended to the tail of
the same `mod tests`. Ours added the `query_string` char-boundary regression
(`a_non_ascii_query_string_is_parsed_or_refused_but_never_panics`), main's #256
added the capability-manifest scraper and its two drift guards
(`dispatch_table_matches_capability_manifest`,
`manifest_labels_match_observed_behaviour`). Disjoint; both kept, and all three
pass together.

**engine/crates/xerj-common/src/config.rs** — the only semantic conflict.
Ours replaced the hand-maintained field sum with a comment pointing at the
measured count; main's side still carried the stale sum
(`5+3+2+…= 57 fields`), which #247 had not updated when it added
`lifecycle`. Resolved toward ours, because "stop maintaining this by hand" is
the whole point of #207 item 10 — but the *number* is then main's to move, and
it did. See the follow-up commit.

**landing/docs/*.html (44 files)** — one conflict each, all the same generated
`docs-index` search blob, which both sides regenerated. Merged at character
level from the common ancestor so both edits survive: ours corrected the
`[vector] default_quantization` entry (base advertised a `scalar4` mode the
engine never accepted), main's #256 renamed `queries.html#semantic-search` to
`#semantic` and rewrote its snippet. Verified afterwards that 39 of the 44
pages now differ from main *only* in that blob, and that the remaining five
(config, operations, storage, troubleshooting, vectors) differ only by this
PR's own stale-default corrections — so #256's rewrites of aggregations.html
and queries.html are preserved intact.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stale internal docs list features that do not exist and omit ones that do

1 participant