TermExtractor
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, and it stays one. A CatalogPath holding [*] describes a set of locations and VariantPath describes one; writing asks "for each scalar under one of these paths, what is its signature", which this walk answers natively because it is the walk that produces [*] in the first place, and reading asks "which ordinals carry this signature", which the posting file answers. Neither ever enumerates the locations a wildcard stands for, and neither should: an ordinal per element would be a second ordinal space per segment, a .idx layout change and a BASE_VERSION bump.
A reader that does want them has CatalogPath.forEachNodeIn — phase 20, in rabosh-catalog, where the wildcard step lives. It is not this walk with a different sink and must not be confused for one. It runs when somebody asks about one document they already hold, not inside compaction, so it carries no IndexOptions.maxDepth or IndexOptions.maxChildren budget; the direction that makes that safe is that its nodes are a superset of the terms emitted here, never a subset, and NodeExpansionDifferentialTest is where the two are compared. The sentence above used to read "nothing in the engine converts between them, and nothing needs to". The first half was made false by that function; the second half was only ever true of the writer, and that is what it now says.
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.