Term Extractor
Walks a document and emits the values it contributes to a set of indexed paths.
The same walk SegmentSketchBuilder makes, with a filter on it. That is deliberate and it is not merely reuse: the paths being indexed were recommended by a walk of exactly this shape, so a document that contributed an observation to $.items[*].sku there must contribute a term to it here. A second, differently-shaped traversal would make the estimator and the index disagree about what a path even is — which array elements collapse, how deep a document is followed, what counts as a leaf.
This is a filtered walk, not a path expander. A CatalogPath holding [*] describes a set of locations and VariantPath describes one, and nothing in the engine converts between them. Nothing needs to: writing asks "for each scalar under one of these paths, what is its signature", which the walk answers natively because the walk is what produces [*] in the first place. Reading asks "which ordinals carry this signature", which the posting file answers. Neither ever enumerates the locations a wildcard stands for.
Candidates are narrowed on the way down, so an unindexed subtree is not walked at all. Each step keeps only the paths still matching, and an empty set prunes the whole subtree. A store with two indexes over shallow paths therefore pays for two field comparisons per document, not for a full traversal — which matters, because this runs inside compaction.
Public because the query layer's evaluation has to be provably the code that built the index. The rule is that a candidate is rechecked by the same walk that produced the term, and the only way to make that a fact rather than an intention is for there to be one class with every layer as a caller of it. rabosh-query builds exactly one of these over every path a predicate mentions, so a whole predicate costs one narrowing walk per document rather than one walk per leaf.