Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit61a33237e0

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.

QueryAssets readColdWarm
builtin, no assetType, no bound2,17228,558 ms, 6,051 reads19,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 engine500, cut8,328 ms, 1,283 reads159 ms, 0 reads
builtin, tool,toolbox440383 ms, 1,224 reads38 ms, 0 reads
builtin, guide,assettype125541 ms, 348 reads11 ms, 0 reads
builtin, searchGuides naming guide, after forget()76436 ms, 152 reads34 ms, 0 reads
builtin + 1,500 world guides, no assetType500, cut8,320 ms, 1,192 reads136 ms, 0 reads
builtin + 1,500 world guides, guide500 of 1,576, cut607 ms, 1,000 reads27 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.