Skip to content
stacks
Docs menu

How search ranks entries

How stacks search scores and tiers hits, what each JSON field means, and when the request leaves your machine.

View as MarkdownOpen in ClaudeOpen in ChatGPT

stacks search takes what you want to build in plain words and returns the stacks and fragments that fit, each with a tier and the words that matched. Ranking is lexical: the CLI runs the same scorer locally that the hosted API runs, and search never calls a model.

stacks search go cli --kind stack --limit 3 --local
Terminal
stack go-cli (strong)
A Go command-line tool with subcommands, configurable settings, actionable errors and human or JSON output.
Why: cli (platforms); go (languages, description, summary, requests); go cli (id, name, tags, body)
...

stack go-api (partial)
A Go HTTP API on the standard library with JSON resources, consistent errors, request logs and graceful shutdown.
Why: go (languages, id, name, description, tags, body, summary, requests)
Penalties: platform (cli; entry: server)
Caveat: Race-enabled tests need cgo and a C compiler on every machine and CI runner.
...

stack typescript-cli (partial)
A compiled command-line tool with subcommands, JSON results, lazy configuration and structured logs.
Why: cli (platforms, id, name, description, tags); go cli (body); go (requests)
Penalties: language (go cli (go); entry: typescript)
...

Next: stacks show go-cli, then stacks new go-cli <dir>.
Output

With --local the request never leaves your machine. go-api matches "go" well but is a server, so it takes a platform penalty and drops to partial. typescript-cli matches "cli" but not the language.

Flags

Flag What it does
<request...> What you want to build. The words are joined with spaces and trimmed, 1 to 500 characters
--kind stack|fragment Search only one list
--limit <n> Maximum hits per list, 1 to 20. Default 5
--local Rank on this machine without sending the request to the API
--offline No network requests, including the API call
--json Return one SearchResult object

Where the request goes

By default, search sends the request text to GET /v1/search on https://stacks.steamfield.io and ranks the local registry at the same time. API hits replace bundled and cached-index copies of the same entries. Your project and user entries keep their local ranking, and an entry the API didn't return stays available from the local ranking.

The API gets one attempt with an 800 ms deadline, including reading its response. A failure falls back to local ranking and backs off for ten minutes. Only the endpoint URL and expiry time are saved, in ~/.cache/stacks/search/backoff.json. No request or response is cached, and the request text is never written to logs.

These keep the request on your machine:

Switch Effect
--local This invocation only
--offline or STACKS_OFFLINE=1 Every network request
DO_NOT_TRACK set to anything except empty, 0 or false Search requests and telemetry

Search never refreshes the registry index or fetches stats. It uses whatever is cached.

How a hit is scored

Each entry is scored against the request's words with BM25, field by field, and the field scores are weighted and summed:

Field Source Weight Strong
Vocabulary facets (platforms, languages, product kinds, capabilities) Search card, closed vocabulary 3 Yes
requests (the example request closest to yours) Search card 2.5 Yes
id, name Frontmatter 2 Yes
description Frontmatter 1.5 Yes
summary, useCases, synonyms Search card 1.5 Yes
tags, category, goodFor Frontmatter, search card 1 No
Body The guidance prose 0.15 No

The search card is a small JSON file beside each entry; Write your own stack or fragment covers writing one.

Penalties then scale the sum down:

Penalty When Effect
platform You asked for a platform the stack doesn't target (for a fragment: none of the stacks it applies to) Score times 0.3
language You named a language the stack doesn't use Score times 0.3
productKind You asked for a kind of product the entry isn't Score times 0.3
notFor Your request matches something the entry says it's not for, names the entry as something to exclude, asks for a fragment that doesn't apply to the stacks you named, or asks to add to an existing repository and the entry is a stack scaffold Score times 0.5
untrusted The entry comes from an untrusted origin No score change, but blocks strong

When a hit has several penalties, the strongest applies in full and each further one applies at its square root, so three mismatches don't bury an entry that otherwise fits.

The adjusted sum is mapped to a score between 0 and 1. coverage is the share of your request's matched words that appear in an entry's strong fields.

Tiers and ordering

Tier Requires
strong score at least 0.5, coverage at least 0.5, no penalties, and a match on the id, the name or a vocabulary facet
partial score at least 0.25
weak Anything lower with a positive score

When your request names a stack, every other stack is at most partial.

Hits sort by tier, then trusted before untrusted, then score. Cached stats reorder hits only within a tier: the quality adjustment runs from -0.018 to +0.022, so evidence moves two hits at most 0.040 apart, and project counts add at most 0.01 of that. A failed last run adjusts the comparison by -0.002 and a passing one by +0.002. Stats from seeded development data still count toward ordering; statsPopulation says when that's the source.

Each list holds only hits with a positive score and at least one reason under why beyond a request constraint or the bare word add, so it can be shorter than --limit or empty. When a list has strong or partial hits, at most two weak hits follow them. A list with only weak hits keeps up to --limit of them.

The JSON result

stacks search --json returns a SearchResult. stacks schema search prints its JSON Schema.

Field Meaning
ranking api or local
request The request text as searched
understood Vocabulary terms recognized in the request, and under unmatched the content words no entry's strong fields matched. An unmatched word may still appear in entry bodies
noStrongMatch true when neither list has a strong hit
stacks, fragments The two hit lists
remote used, off or unavailable
remoteReason When remote is off: local_flag, offline, do_not_track, backoff or no_endpoint
method API results only: lexical or hybrid
statsPopulation When a shown hit carries stats, where they came from: stacks projects or seeded development data. If the API and the local cache both supplied shown stats and either is seeded, it's seeded. Absent when no shown hit has stats, or when a source doesn't say and none is seeded

Each hit has:

Field Meaning
kind, id, name, summary What the entry is
origin, trusted Where it came from; see Registries and trust
score 0 to 1, rounded to three decimals. Hits that round to zero are left out
coverage Share of matched request words found in strong fields, rounded to three decimals
tier strong, partial or weak
why Each matching field and the words that matched it
penalties Each penalty as { kind, request, entry }: the kind, the request phrase that triggered it, and the entry's conflicting facets or matched notFor text. An untrusted penalty has an empty request and the entry's origin
caveats Optional platform or operating limits from the search card
appliesTo Fragments only: the stacks or tags it fits
evidence Cached stats and last-run data, when available

Picking a stack as an agent

If you're an agent picking a stack for someone:

  1. Compare the strong hits using why and evidence, and read their caveats.
  2. When noStrongMatch is true, tell the user nothing fits well and confirm before using a weak match.
  3. Never pick an untrusted entry unless the user named it.
  4. Run stacks show <id> on the pick and explain why it fits before running stacks new.

Exit codes

Exit Meaning
0 The search ran, including empty results and no strong match
2 Invalid input (Usage, BadInput)
6 A registry couldn't be read (PermissionDenied)
130 Interrupted