Builtin Library
README.md
discovery
Content search in this engine. One question — discovery.search { q, assetType, match, mode, limit } — answered in one shape, in one of two modes. match
belongs to keyword mode.
Keyword reads the files each asset type's type.yaml names under
indexing.data for the slot match selects: identity is the identity role
(a README, usually); capability is the capability, interface and
implementation roles together (code, guide bodies); any is either and all
is both. It matches the query's words against those files verbatim and each hit
says which role and file matched. It runs on this engine's own files, so
unpushed content and a private world's own content are in it.
Semantic is ZeroMind's ranking over what ZeroMind may embed: the builtin
library, the origin worlds of installed libraries and imported assets, and this
world itself when it is public. Say what you need in q, in plain words. The
search decides which asset types the words are about, and a judge reads every
candidate against the query; the hits it keeps come back best first. match
does not apply to it. Leave assetType out: naming one replaces the search's
own choice of types, so only the named types are searched and a type guessed
wrong answers nothing even when the library holds what you asked for. Name a
type only when the task fixes it. A private world's own content is out of its
reach, and the answer's note says what it searched and that keyword mode
searches the world's own files.
The two modes never fall back to each other; the answer names the other one when it cannot answer.
The library corpus. library = true asks the semantic ranking over the
whole published library instead of the content this engine can reach
(discovery.libraryScope() names no world, so the request carries no world
filter). Each hit carries local_ when this world already holds it. The same
question is then asked of this world's own content by keyword mode
(keyword { own = true }, which reads only assets discovery.worldOwn holds to
be this world's: not the builtin library, not an installed library, not a
dependency, not anything world.installedAssets() lists), and those matches are
appended after the library's hits (discovery.withWorldMatches): a hit the
library already returned stays where the library put it, every other one is
listed after, marked from = "world". The library's hits keep the order the
search core gave them; no score is changed or combined. When ZeroMind cannot be
asked (resting, unavailable), the world's own matches still answer and the
note says the library was not searched and why. An engine bound to no world
answers as before. Keyword mode refuses library.
Where results may come from (from). A request names it with from:
all is everything published (the library corpus, what library = true
asks), bound the current world and its installed libraries (the worlds
discovery.scope() names: the builtin library, this world, and the origin
world of every installed library and imported asset), with no
published-library leg. from chooses where, never how: bound answers in
either mode, semantic ranking over those worlds or keyword matching this
engine's own files. A request naming no from keeps library, so
discovery.search stays bound unless asked, while search_tools,
guides, tools.search, the guides.search tool and zeromind.search ask
all by default. A name other than all or bound is refused, and so is
from = "all" with mode = "keyword". Every one of those surfaces takes
from and hands it here.
discovery.searchGuides asks the same question the way a guides { query }
call does: the documentation types DOC_TYPES names, guide, agentskill
and assettype. discovery.searchTools asks it the way a
search_tools { query } call does: the types TOOL_TYPES names, tool,
toolbox and workflow. Three surfaces mean three things (guides is the
docs, search_tools is what an agent runs, discovery.search is the content),
so a guides query answers out of the docs alone, and an answer holding no hit
carries NO_DOCS_NOTE, which says which kinds it matched against. Both MCP tools ask
with library = true whenever the engine is bound to a world.
Naming no assetType, keyword mode walks discovery.guideTypes() — the types
discovery.types() lists that DOC_TYPES names — and semantic mode asks
ZeroMind over them (discovery.semanticKinds, each type by the guid of its
.assetType) for SEMANTIC_FETCH_MAX hits (100, the most one call answers)
and keeps the first limit of those kinds (discovery.onlyKinds). Naming an assetType, it searches exactly those types; a
list naming tool, toolbox or workflow is refused with a sentence that points to
search_tools, and one naming any other type discovery.types() lists with a
sentence that points to discovery.search. searchTools refuses a documentation
type by pointing to guides, and a content type by pointing to
discovery.search. The hits stay in the search's own rank order.
The answer
{
mode: "keyword" | "semantic", -- the mode that ran; absent on a refusal
match: "identity" | "capability" | "any" | "all", -- keyword answers only
from: "bound" | "library", -- the scope that ran
status: "answered" | "private_world" | "resting" | "unavailable" | "refused",
results: { { q, hits } }, -- one entry per query, in the order asked
world: { guid, title, private }, -- the world this ran from, when one is bound
searched: { { guid, title, why } }, -- the worlds it covered
note: string, -- the reach this answer had, and where else to look
scanned: number, -- keyword: the most assets one query read
cut: boolean, -- keyword: whether a walk stopped at KEYWORD_SCAN_MAX
}
private is stated once the world's visibility has been read. Semantic mode
reads it — through discovery.visibility, which asks ZeroMind once per world
and holds the answer — because it decides what ZeroMind is asked over. A keyword
answer and a refusal ask ZeroMind nothing: they name the world from what the
engine holds (discovery.worldHere()), so an unreachable backend never delays
them. private_world is the status of an answered semantic search from a
private world; a resting or unavailable one keeps that status and its
retry_in_s or error.
ZeroMind ranks only what it has indexed, so a semantic search in either scope,
bound or library, also matches this world's own files by their words and
lists them after the ranked hits, each marked from = "world". A hit the
ranking already holds is not repeated. That is what finds a guide ZeroMind has
not indexed yet, an unpushed one, or one in a private world, and it holds for a
resting or unavailable answer too, which then lists those matches alone.
note is what an answered search says about its own reach.
PRIVATE_WORLD_NOTE is what a private world's semantic search ranked and
how that world's own files were matched; ALL_KINDS_NOTE is the semantic
search that named no kind, so ZeroMind ranked over every kind it holds and a
row can be of any of them; NO_DOCS_NOTE is the guides query the
documentation held nothing for, and says which kinds it matched against. They stand in the one note when more than one applies, the
private-world sentence first — status reads private_world there, so the
sentence that status names opens what the answer carries.
why is builtin, this world, library or import. A semantic answer's
searched is the scope ZeroMind was asked over; a keyword answer's is the
corpus it actually read — the builtin library when builtin assets were in it,
this world when the world's own were. A library or import entry's title
is read from the install record that brought its guid into this world; an
install whose record carries none names that origin by guid alone.
A hit carries guid, assetType (the server's wire field kind, renamed where
a hit is read), name, summary (the opening of the asset's README, as
discovery.brief takes it) and local_, the form of the
asset this engine holds: path, plus tool for a tool (the identity a call
names, from the tool registry through tools.callable), toolbox for a tool
and for a toolbox (the name a drill-in lists the toolbox by), or guide for a
guide or an assetType (the identity guides { path } reads), workflow for a
workflow (the identity workflow.start runs), skill for an agent skill (the
identity agent_skill opens), and readme, the VFS path of the asset's README
when it holds one. A tool or toolbox hit also
carries local_.registered, which says whether the registry holds what the hit
names: the function a call to the tool would run (tools.callable), or the
toolbox a drill-in would list (tools.registeredToolbox). A .tool folder
whose source registered none has tool = nil and registered = false; a
toolbox folder the registry holds nothing for keeps its toolbox name and has
registered = false. discovery.identityOf(hit) is the one name the surface
acting on a tool-family hit takes: a tool's <toolbox>.<tool>, a toolbox's
drill-in name. A hit from keyword mode carries
matched = { role, file, terms, words, together }: the role and file that
carried the query's words best, how many of those words the asset carried
anywhere in its text, how many it carried as words of their own, and the most
it carried side by side in the query's order. A hit from semantic mode carries
score, ZeroMind's own rank for it, and what the search core stated: p (the
judge's probability, which is the order of a library answer), type_key (a key
into the result's types), container_key and system_key (keys into the
result's containers), owning_world, and preview_description when the
server sends one. A request that asks for excerpts receives the full answer,
and its hits also state owner, world_title, license and updated;
open_issues counts the issues open against the asset and its world, when the
answer states them. A library result carries both tiers beside its hits. A
container entry is { key, guid, name, assetType, parent_key?, description? }, parent_key
naming the entry of the container above it and description the opening of the
container's README. discovery.brief takes that opening for every text a search
answer carries: the README's first paragraph (title, headings, HTML comments and
fenced code passed over) as one line, cut back to a whole word within
BRIEF_CHARS (300) and ending in an ellipsis when cut. A library result whose
texts held more than their openings states briefed = true, and the answer's
note then closes with BRIEFED_NOTE, which names zeromind.inspect: it reads
a README whole by the guid the hit or entry names. A keyword hit's summary is
the opening of the README its local_.readme names. container_key names the container
a hit ships in and system_key the highest one above it, so a tool in a
toolbox a package ships reads tool, toolbox (container_key), package
(system_key), with the toolbox's parent_key naming the package. A hit no
container holds may name the world it belongs to as its system, an entry with
assetType = "world". A hit matched in this world's own content by its words
carries none of the three keys.
A semantic request names its types through discovery.kindParam: every
registered type by the guid of its .assetType, builtin types included, which
is the identity ZeroMind files a type under. A name no registered type answers
to travels as written, so ZeroMind can say which name it does not know.
discovery.semanticKinds(names) spells the ones of names that resolve. A
search that names no kind where its surface documents some — none of them
resolving — answers with ALL_KINDS_NOTE, so a caller reads the ranking's
reach off the answer rather than off the rows.
discovery.shape(answer, "guides", query) lists a guide or assetType hit with
its assetType, the path guides { path } reads and its summary; a hit of
any other type with its assetType, name, a one-line purpose and
vfs_path, the README read_file opens. discovery.shape(answer, "tools")
lists each hit with its assetType (tool, toolbox or workflow), its identity
(discovery.identityOf), its toolbox, a one-line purpose and registered;
a tool row also carries tool, the tools.use identity while the registry
holds it and empty while it does not. Every row of both forms carries guid,
installed (whether this world holds it), skill on an agent skill and
workflow on a workflow this world holds, p when the search core judged it, and from = "world" on a match from this
world's own content. A row of either form, and a hit of
discovery.shape(answer, "answer"), carries the container_key and
system_key the search core stated; the list forms carry the containers
those keys point into once, beside the rows, and the answer form keeps them on
each query's result.
Ranking
Hits fall by matched.weight, what the query's words the asset carried as
words of their own weigh together: hand in "hand it over" and spawn in
"spawns" or "spawning" count, off inside "offline" does not (matched.words
is how many of them there were). A word weighs ln((read + 1) / (carriers + 0.5)), where read is how many assets the walk read and carriers how many of
them carry the word, so a word few assets carry outweighs several that nearly
every asset carries: in "roster who is here" the tool saying "roster" ranks above
the hundreds saying "who is here". Then by matched.together,
the most of the query's words the asset carried side by side in the query's
order (separated by spaces or hyphens), so "hand off" ranks above "hand" and
"off" apart. Then by matched.terms, how many of the words the asset carried
anywhere in its text, inside longer words included. Each word counts once
however many of the asset's files carry it. Then by the order the type declares
the role that matched in its indexing.data, then by name. match: "all" keeps only assets whose identity files and whose
capability files each carried at least one of the words.
A keyword answer's results[i].total is how many assets matched that query,
of which hits are the best limit.
The frames a search takes
A keyword walk reads the files of up to KEYWORD_SCAN_MAX assets, which on a
cold cache is thousands of reads. Called from code that can yield (a task, an
execute, the engine shell, search_tools), it gives the frame back every
SLICE_MS (8 ms) of reading, so the engine keeps stepping and drawing while it
searches. discovery.newSlice() is that rule as a function, for any other walk
to call between its pieces. Called from code that cannot yield, the walk reads
everything within the one call.
What it reads, what it holds, and what it reads twice
A keyword query reads the files the recipe names for every asset of every type
it covers. One copy of each file is kept — lowercased, which is what the match
reads, plus the one line a README answers for — against the checksum the asset
index reports for its asset (asset.meta(ref).checksum, the tree hash over the
asset's own files). So a second identical query reads nothing, and an asset
whose bytes changed is read again on the next query that reaches it. A recipe
can name files an asset does not hold (a tool's init.lua beside its
init.luau): the asset's folder is listed once per checksum, and a file it
does not list is held as absent without a read.
What is held is bounded by TEXT_BUDGET_BYTES, 16 MiB; past it the least
recently read asset's text is dropped, and a query that reaches it again reads
it again. discovery.stats() reports reads, the number of vfs.read calls
the module has made, and bytes, what it is holding right now.
discovery.forget() drops all of it.
Reading ahead of the first query
discovery.fill(types?) reads what a keyword walk over types reads, in the
walk's order and under its bound, into the same held text, so the first query
over those types reads only what changed since. With no types it reads the two
corpora a request naming no type walks: the tool family (toolTypes()) and the
documentation (guideTypes()), about 5 MiB together. An engine booted with the
editor profile runs it on a system task as it boots and again each time a world
finishes loading, giving the frame back every SLICE_MS like a query does; the
pass after a world load reads only the world's own assets, since the library's
are held. A runtime engine reads that text when a search first asks for it.
Fills read one at a time, and discovery.forget() ends every fill asked for
before it. discovery.stats() reports filling, whether one is waiting or
reading now, and fills, the latest FILLS_KEPT of them and any older one
still waiting or reading, each with the types it reads (named once it begins
reading), its state, how many assets it has walked and the frame it began
reading on.
Past the budget, the least recently read text is dropped until
TEXT_KEEP_FRACTION (three quarters) of it is held, so the ordering that picks
what goes is paid once per pass rather than once per read.
How much one query reads
A keyword query reads at most KEYWORD_SCAN_MAX assets, 1000. The walk
takes this world's own assets first and the library's after them, since no
other search answers for what the world holds; within each, it takes the types'
assets in turn — the first of each type, then the second of each — so a bound
walk samples every type instead of reading the first few types whole. A
tools.search drilled into a toolbox ({ toolbox = ... }) walks the tools
under that toolbox's folder and no others (within). The answer states
scanned, how many it read, and cut, whether the types held more. The bound holds whatever the query names: assetType
narrows which assets the 1000 are, so a type holding more than 1000 is cut too.
search_tools asks for tool,toolbox (about 510 builtin assets), which fits
inside the bound on the builtin library. A guides query that names no assetType
covers guideTypes() — guide and assettype, 125 builtin assets — so its
walk reads both types whole. A name no registered type answers to is refused,
naming it and the types that surface reads, and so is a name whose type
declares no indexing recipe, since no search reads its assets;
discovery.types() is the whole list a discovery.search can read.
Measured on a native headless offline engine, dev profile (optimized +
debuginfo), Windows, --dev over the checkout, 2026-09-14. "Cold" follows
discovery.forget(); "warm" repeats the same query. Every row but the
fresh-engine one ran on an engine whose earlier walks had already read the
same files once, so its cold figure is the module's cost without the first
read of each file from disk. The builtin library holds
2,172 assets across the 42 types that declare a recipe and more than 46 MB of
lowercased text; module (15.6 MB) and testSuite (past the 16 MiB budget on
its own) are the largest.
| Query | Assets read | Cold | Warm |
|---|---|---|---|
builtin, no assetType, no bound | 2,172 | 28,558 ms, 6,051 reads | 19,309 ms, 6,002 reads (the text passes the budget, so it is read again) |
builtin, no assetType, bound 500, first query of a fresh engine | 500, cut | 8,328 ms, 1,283 reads | 159 ms, 0 reads |
builtin, tool,toolbox | 440 | 383 ms, 1,224 reads | 38 ms, 0 reads |
builtin, guide,assettype | 125 | 541 ms, 348 reads | 11 ms, 0 reads |
builtin, searchGuides naming guide, after forget() | 76 | 436 ms, 152 reads | 34 ms, 0 reads |
builtin + 1,500 world guides, no assetType | 500, cut | 8,320 ms, 1,192 reads | 136 ms, 0 reads |
builtin + 1,500 world guides, guide | 500 of 1,576, cut | 607 ms, 1,000 reads | 27 ms, 0 reads |
The world guides were authored into the session's /source with
asset.create("guide", name), each carrying a two-line README.md and
guide.md. What a read costs depends on the file: a guide's two small files
read in 0.6 to 1.6 ms each, while an animation, font or texture asset's
recipe files cost 17 to 42 ms each, which is why the bounded unfiltered walk — which reaches
every type — costs more per asset than a family walk.