{"document_id":"2e7def73979005b88be1b0981949b0336f83844c8bae293fe86654d52950414e","content_sha256":"7774e51a4af1e9b4eae96b11fdbdde3060e4e44eb22a0596618f1a923e863bea","hashed":{"address":"zetlyn://zetlyn/docs/README.md","title":"Zetlyn documentation","url":null,"language":"eng","published":"2026-09-12T08:16:15Z","keywords":"","text_chars":1511,"word_count":267,"text_hash":"9b7cfefbfa85aa93d6f1925e6c89dc4d5eae0f4003eba404e94c91634eb70625","catalog_version":"folder-2","embed_model":"multilingual-e5-small","extra":{"bytes":1723,"extension":"md"}},"embed_method":"chunked-256w-mean-v1","text":"Zetlyn documentation\n\nZetlyn searches a body of documents and plans the order to read them in. It runs on your own\ninfrastructure, over your own documents, and can search them together with corpora other people\npublish.\n\nWhat comes back is a list of results pointing at the documents where they live. Zetlyn does not hand\nover the documents and does not answer the question.\n\nWhat are you here to do?\n\n| | |\n|---|---|\n| Try it | Quick start — search over a published corpus, in three commands |\n| Understand it | Concepts — corpus, adapter, scope, and what each piece does |\n| Run it | Handbook, then deploy when one process is not enough |\n| Evaluate it | Evaluation — where documents are, the permission model, what it does not do |\n| Build against it | API and reference |\n| Write an adapter | Adapters |\n\nWhat is not here\n\nThe format. A corpus is defined by the\nOpen Zetlyn Corpus Specification, which is openly licensed\nand which anyone may implement without this product and without permission.\n\nThis repository documents a proprietary product, and is public so the documentation can be read before\na purchase. The documentation comes first: an implementation follows it, and where the two disagree the\nimplementation is wrong.\n\nLicence\n\nText is CC BY 4.0: share it, translate it, quote it, commercially or not, with attribution.\nCode samples are part of the text and carry the same licence.\n\n\"Zetlyn\" is a trademark. A modified copy of this documentation may exist; it may not present itself as\nZetlyn's.","simhash":16966735867240078898,"bands":[1029803706889563,64820742715362722,116536773193648029,176441077372717612,238961863863767521,511789791280402645,512722674150986988,541986334348529527,543388016113339976,565915732138545117,599773411098663471,680682880337991415,727885076444748655,748960634954557063,749663276257512617,777676326629448112,825296020235861096,952909764241260760,1049876466205176118,1054497729396445387,1111787830233256776,1247255617179127341,1264285539472414380,1442700636004570293,1558530368974717101,1577185655211465190,1690157678811510892,1800543316262389573,1940697074309606139,1967120239657175829,1985617661343303022,2006372028621663742,2071016743473066306,2183963055822278202,2523011944414673871,2555904663088524177,2583273996386553450,2613269987375498329,2667769848782464088,2709169969391799745,2729779562702295926,2797227614052836221,2802218245126437147,2916894428131935736,3062250288629454217,3200190346198058211,3223220226077220954,3286440632871093546,3350634959891489119,3631897371237747124,3664380847883725962,3690685381354125878,3723377494795593346,3758921040243089114,3897922977055446628,3932094640301690627,3966789733557194895,4093055992395650194,4219492445342128224,4233777364975831750,4241815664963816570,4255836990852747530,4305821041001311090,4356748750965732953],"entities":["zetlyn","quick","understand","concepts","run","handbook","evaluate","evaluation","build","write","adapters what","open zetlyn corpus specification","licence text"]} {"document_id":"00b85b7fcb8306275f53e254754cc0097c743c2c912fa3d734a8f7b2154ad052","content_sha256":"1accb0df010f0b5477f1e5924cfd35d7978af5d4fe89318ef2d09699fd1753f7","hashed":{"address":"zetlyn://zetlyn/docs/adapters.md","title":"Zetlyn adapters","url":null,"language":"eng","published":"2026-09-14T13:38:05Z","keywords":"","text_chars":17176,"word_count":2934,"text_hash":"d37018dd4df01ea73e79211f4af099eed4304c44eb421d4a39de7e0b2a4a8963","catalog_version":"folder-2","embed_model":"multilingual-e5-small","extra":{"bytes":17911,"extension":"md"}},"embed_method":"chunked-256w-mean-v1","text":"Zetlyn adapters\n\nAn adapter reads one kind of source and writes documents into a corpus. A corpus has exactly one.\n\nEvery adapter is a separate program, including the ones Zetlyn ships. They speak the\nadapter protocol,\nwhich is published, so an adapter you write is not a second-class one.\n\nThe adapter declares five things when it starts: how documents are addressed, what they are findable\nby, which extraction produced the text, whether the source ends, and whether several workers can read\nit at once. Those are properties of the\nadapter, not settings you turn.\n\nWhat it does not decide is whether the corpus evicts and what a reader may be shown. Those are\nyours, per corpus, and the same adapter serves one corpus that evicts and another that does not.\n\n| adapter | source | addresses | findable by | ends | divisible |\n|---|---|---|---|---|---|\n| zetlyn/folder | a directory of files | corpus-local | whole text | yes | no |\n| zetlyn/sql | a query against a database | corpus-local | whole text | yes | no |\n| zetlyn/wiki | Confluence, MediaWiki | corpus-local | whole text | yes | no |\n| zetlyn/feed | RSS and Atom | global | title and keywords | no | no |\n| zetlyn/web | public pages | global | title and keywords | no | yes |\n\nCorpus-local addresses mean the same address in two corpora is two documents: two companies both\nhave /docs/readme.md. Global means it is one document.\n\nEnds decides what zetlyn corpus run does. A source that ends is read once and the command\nreturns; one that does not keeps going until you stop it.\n\nDivisible decides whether several workers can fill the corpus at once. Only a crawl divides\nusefully — one process walks a directory faster than three coordinate over it. See\ndeploy.\n\nInstalling one\n\nzetlyn adapter install zetlyn/folder\nzetlyn adapter list\nzetlyn adapter remove zetlyn/folder\n\nAn adapter name carries its owner. The five below are zetlyn/…; one you write is\nsuspero/jira. A bare folder is refused rather than resolved, so no two people can publish an\nadapter under the same name.\n\ninstall fetches the program from a hub, checks its bytes against the version it was served under,\nand writes its path into your config. It lands beside your stores rather than on the system path, so\ntwo deployments on one machine hold their own. Nothing is searched for at run time:\n\n[adapter.\"zetlyn/folder\"]\ncommand = \"/usr/local/lib/zetlyn/adapter-folder\"\n\n[corpus.\"suspero/handbook\"]\nadapter = \"zetlyn/folder\"\npath = \"/srv/handbook\"\n\nThe path is the adapter's row, written once however many corpora use it. A corpus names the adapter\nand nothing else about it; everything else in a corpus block is handed over unchanged, so Zetlyn needs\nto know nothing about any adapter's settings — including one you wrote this morning.\n\nA corpus whose adapter is not installed is a valid deployment and an incomplete one. corpus list\nsays so, and corpus run refuses with the command that fixes it.\n\nFour keys in a corpus block belong to Zetlyn rather than the adapter: adapter, which names the row\nabove; target, because eviction happens in the corpus and not in the source; subscribe, because a\nsubscribed corpus has no adapter at all; and readgate, because what may be shown is the operator's\ndecision about one corpus and not a fact about the source. The adapter never sees them, and it never\nsees where its own program lives either.\n\nTwo more names are reserved: corpus and cursor. Those are what Zetlyn writes into the file it\nhands the adapter, so a setting of your own under either name would be overwritten by the run that\nresumes and missing on the first. A corpus block using one is refused, naming it.\n\n---\n\nzetlyn/folder\n\n[corpus.\"suspero/handbook\"]\nadapter = \"zetlyn/folder\"\npath = \"/srv/handbook\"\ninclude = [\"**/*.md\", \"**/*.pdf\"] # default: every readable type\nexclude = [\"**/drafts/**\"]\n\nReads Markdown (.md, .markdown), plain text (.txt, .text, no extension), HTML (.html,\n.htm), PDF, Word (.docx, .docm), PowerPoint (.pptx, .pptm), Excel (.xlsx, .xlsm) and\nOpenDocument (.odt, .odp, .ods). Anything it cannot extract text from is reported as skipped,\nwith the reason, and zetlyn corpus run prints the count when it finishes. A scanned PDF with no text\nlayer is the usual case.\n\nA PDF and an Office document lay their text out in a page. What is stored is the text and not the\nlayout: runs of spaces collapse and a page break does not become four empty lines. Markdown and plain\ntext are left exactly as written, because there the indentation is the document.\n\nA skipped file is not a deleted one: if it was read successfully before, the older document stands.\n\nTwo things are passed over without a word. Entries beginning with a dot are not visited — a\nrepository's .git is not a set of documents, and reporting sixty thousand of them as skipped buries\nthe files that really could not be read. Symbolic links are not followed, because one pointing out of\nthe directory is somebody else's documents and one pointing back into it is a walk that never ends.\n\n| | |\n|---|---|\n| address | the path below path — docs/readme.md |\n| title | the first heading, or the file name when there is none |\n| text | the extracted text. Markdown keeps its words and loses its markup — a link becomes its label, and the target is dropped as navigation |\n| published | the file's modification time, unless the document carries a date |\n\nA file gone from disk is removed from the corpus — but only when the run completes. An adapter that\ndies halfway reports nothing complete, and nothing is removed. That is why pointing path at a\nhalf-mounted share does not empty the corpus.\n\nzetlyn corpus run --watch keeps the corpus current by walking again on an interval. It does not\nwatch for filesystem events, and the reason is the deletion rule rather than effort: an adapter told\nto keep running never sends done, and done claiming completeness is the only thing that licenses\nremoving a file that is gone. A walk that ends each pass keeps that, so a file deleted while the\nwatch runs leaves the corpus at the next pass.\n\n---\n\nzetlyn/sql\n\n[corpus.\"suspero/orders\"]\nadapter = \"zetlyn/sql\"\ndsn = \"env:ORDERS_DSN\"\nquery = \"\"\"\n select id::text as address,\n subject as title,\n body as text,\n created as published,\n updated_at\n from orders\n\"\"\"\ncursor_column = \"updated_at\"\n\nThe query supplies the columns and they are read by name, case ignored. address and text are\nrequired; title, published, language, url, extra and deleted are optional. extra is one\ncolumn holding a JSON object. Every other column the query returns is ignored, and named once on\nstandard error so you can see it was — updatedat above is selected only so the cursor has\nsomething to order by, and a column like that inside extra would sit inside the content hash and\nchange every document whenever a row was touched.\n\nSecrets come from the environment rather than the file: env:NAME. A connection string with a\npassword written into zetlyn.toml is refused, because that file is read by whoever can read the\ndeployment and is the first thing anyone pastes into a ticket.\n\ncursorcolumn makes a run incremental. The result is ordered by that column, the adapter reports\nthe highest value it has seen as it goes, and the next run asks only for rows at or after it —\nso a row sharing the highest value is read again rather than lost, and a run interrupted after an\nhour resumes after that hour. That is what makes a nightly run over a large table cheap.\n\nA cursor cannot see a deletion. A row that disappears is never returned by the incremental query,\nso its document stays. The adapter therefore does not report the run complete, and nothing is removed\nfor being absent. Two ways to handle it:\n\nzetlyn corpus run suspero/orders --full ignores the cursor, reads the whole result set, and reports completeness —\nso deletions are picked up. Run it periodically;\nreturn deleted rows with a deleted column set, and they are removed by name as they go — which\nan incomplete run is allowed to do, because being named is not the same as being absent.\n\nGive Zetlyn a read-only database user. The query is run as written, inside a read-only transaction,\nand it is never parsed: what a cursor adds is a derived table, a bound and an order around it.\n\nPostgreSQL negotiates TLS by itself — sslmode in the connection string decides. MySQL has no such\nthing, so tls = true asks for it. tlsca = \"/path/ca.pem\" replaces the public roots, and is\nusually needed: a managed database presents a certificate from its provider's own authority and an\ninternal one from the company's.\n\n---\n\nzetlyn/wiki\n\n[corpus.\"suspero/internal\"]\nadapter = \"zetlyn/wiki\"\nkind = \"confluence\" # or \"mediawiki\"\nurl = \"https://wiki.internal\"\nspace = \"ENG\" # namespace, for MediaWiki\ntoken = \"env:WIKI_TOKEN\"\nuser_agent = \"Suspero corpus (ops@suspero.com)\" # optional; the wiki is yours\n\n| | |\n|---|---|\n| address | the page id, not the title, so a rename is an edit rather than a new document |\n| title | the page title |\n| text | the rendered page, macros expanded, navigation removed |\n| published | the page's last edit, in UTC |\n\nRendered rather than sourced: a page whose body is three macros is three macros' worth of nothing\nuntil they are expanded. What comes back still carries what the wiki weaves around the text — a table\nof contents, an edit beside every heading, a category box — and each of those is dropped by the\nclass it is labelled with.\n\nThe token comes from the environment, like zetlyn/sql's connection string, and one written into\nzetlyn.toml is refused. A token with a colon in it is user:secret and is sent as Basic; anything\nelse is a bearer token — which covers a Confluence Cloud API token, a Data Center personal access\ntoken and a MediaWiki owner-only OAuth token without a setting naming which.\n\nThe first run reads the whole space and reports completeness. After that the wiki's own change feed\nsays what has been edited, created, moved or deleted, and only those are read. A move keeps the page\nid, so it is an edit.\n\nTwo things the feeds cannot do, and each is handled rather than hoped about:\n\nMediaWiki's list of recent changes is not an archive. Past $wgRCMaxAge — ninety days out of\nthe box — it does not hold what happened, so an incremental run against an older cursor reads an\nempty feed and reports success. The adapter asks how far back the feed goes and reads every page\ninstead when the answer is later than its cursor, saying so.\nConfluence cannot report a deletion incrementally. A search bounded by lastModified never\nreturns a page that is gone, so that run never claims completeness and a deleted page leaves the\ncorpus on the next --full. MediaWiki keeps the page id in its deletion log, so there it is\nremoved by name as it happens.\n\nAttachments are not indexed. Put them in a folder corpus and search both in one scope.\n\n---\n\nzetlyn/feed\n\n[corpus.\"suspero/news\"]\nadapter = \"zetlyn/feed\"\nurls = [\"https://example.org/feed.xml\"]\nuser_agent = \"SusperoBot/1.0 (+https://suspero.com/bot)\"\npoll = \"5m\" # omit for a single pass\ntarget = 50000\n\n| | |\n|---|---|\n| address | the item's link, which is global |\n| title | the item's title |\n| text | the linked page, fetched. When the feed carries the full item, that is used and nothing is fetched |\n| published | the item's date, in UTC |\n\nA feed carrying 1200 characters or more of an item is carrying the article, and that is what is\nstored. Less than that is a teaser, and the page it points at is fetched instead — unless the page\nhas less to say than the teaser did, which is what a paywall looks like from here.\n\nA feed does not end, so run keeps going and never reports completeness: documents are never removed\nfor being absent. target is what bounds the corpus — the best documents for that budget are kept and\nthe rest evicted. Without poll the adapter makes one pass and stops, which is the shape to use when\nsomething else already schedules the run. 30s is the shortest poll it will ask a publisher for.\n\nAn item is read once. It is read again when its date or its title changes, because a correction is\nthe reason to poll a news feed at all.\n\nuseragent is yours and is required. You are the one calling: a publisher who wants this stopped\nwrites to the address in the header and blocks the name in it, and both have to reach you rather than\nwhoever wrote the program. It is also what robots.txt is matched against, so User-agent:\nSusperoBot addresses your crawl and nobody else's. One with no contact in it — no http address, no\nemail — is refused, because every other rule here is about a publisher being able to say no.\n\nA feed listing a page is not permission to hold it. robots.txt is read before anything else on a\nhost, and a page it asks not to be read is not stored at all — not even from the text the feed itself\ncarried. One request per host at a time, at least a second between them, and a Crawl-delay is\nhonoured; none of that is configurable, for the mirror image of the reason above. The publisher's\nterms are a separate question and still yours to check.\n\n---\n\nzetlyn/web\n\n[corpus.\"suspero/research\"]\nadapter = \"zetlyn/web\"\nseeds = [\"https://example.org/start\"]\nuser_agent = \"SusperoBot/1.0 (+https://suspero.com/bot)\"\nallow = [\"*.example.org\"] # default: follow anywhere\ndeny = [\"*.ads.example\"]\ntarget = 100000\n\n| | |\n|---|---|\n| address | the URL in the normal form of specification section 3, which is global |\n| title | the page's |\n| text | the main text of the page, navigation and boilerplate removed |\n\nNormalised rather than taken as the link was written: the address is global, so the same page reached\nfrom two sites — or from a feed corpus beside this one — has to be one document. A page with fewer\nthan 200 characters of text is skipped rather than stored, because a redirect notice, a login wall\nand an index all arrive as one paragraph.\n\nuseragent is yours and is required, for the same reasons as in zetlyn/feed, and it is the name\nrobots.txt is matched against.\n\nrun continues until you stop it, and stopping it is how it ends: what has been read is on disk and\nthe corpus builds from it. When the frontier empties, pages read longer ago than a week come round\nagain — a crawl that never ends has to return, or the corpus is a snapshot of whenever each page\nhappened to be seen first.\n\nIt honours robots.txt, keeps one connection per host, and waits five times however long the last\nrequest to that host took — so a server that starts slowing down is asked less often without anybody\nhaving to notice. These are not configurable: a crawler that can be told to ignore them will be.\n\nOne queue per host, and the host whose pause runs out soonest goes next. A site that links every page\nto every other may have ten thousand URLs waiting and no more; it is not more interesting than the\nrest of the web put together, and without the bound it decides the crawl.\n\ntarget is the size you want, not a limit you hope not to hit. Once reached, a new page competes with\nwhat is already there and one of them is evicted, so a corpus at target keeps improving without\ngrowing.\n\n---\n\nWriting your own\n\nAn adapter is a program. It prints which protocol versions it supports, then prints documents as JSON\nlines — and skip for anything it reached and could not read:\n\n$ my-adapter --zetlyn-interface\n1\n\n$ my-adapter --zetlyn-interface 1 --config corpus.toml\n{\"t\":\"hello\",\"interface\":1,\"adapter\":\"suspero/jira\",\"ends\":true,\n \"address_space\":\"per-corpus\",\"indexing\":\"full_text\",\"catalog_version\":\"v1\"}\n{\"t\":\"doc\",\"address\":\"PROJ-41\",\"title\":\"Refund not received\",\"text\":\"…\"}\n{\"t\":\"done\",\"complete\":true}\n\nPoint a corpus at it:\n\n[corpus.\"suspero/tickets\"]\nadapter = \"suspero/jira\"\ncommand = \"/opt/bin/jira-adapter\"\nproject = \"PROJ\"\n\nEverything below command is your adapter's configuration, handed over as written.\n\nYou can also skip the program. corpus add reads the same records from a file or a pipe, which is\nthe fastest way to try a shape before writing an adapter around it:\n\nyour-exporter | zetlyn corpus add suspero/tickets --from -\n\nadd never removes anything. An adapter reporting a complete run is what lets documents be removed for\nbeing absent, and a pipe cannot report one.\n\nThe one rule that is not obvious: done with complete: true is a statement that you emitted every\ndocument your source holds, and it is the only thing that lets Zetlyn remove documents it did not\nsee. If you are not sure you read everything, do not send it. Nothing is lost by leaving stale\ndocuments in place for one run.\n\nThe adapter protocol\nhas the rest: the record types, the cursor, and what each exit code means.\n\nWhat an adapter does not decide\n\nHow documents rank inside the corpus, and what a reading route does with them. Those are the same\nwhichever adapter filled it.\n\nTwo corpora built by different adapters can sit in one scope and be searched together. Which relations\ncan be derived between them depends on their declared parameters rather than on their adapters:\nzetlyn scope check says which.","simhash":7853546883189729945,"bands":[2973497428484976,4589216313337377,5416806647903882,6686466549985392,15921369585790343,17191856257380796,23801843895090358,25669132664694474,64096857445608800,70117371227254382,74640544419158097,86951145383567913,97102289086106467,101775739030036831,103975522679468757,105290911078269155,107266749502098957,115884935498918426,116073735332214481,121661339976326820,123753541347443319,125518318904905323,131260599982773450,140857095689463221,148127245328310947,150588432552567799,157320770709067846,167712011185678397,173709272199807637,178068556422830140,178232102595938198,179762309650244311,181299727746093644,189206887897762718,201452565297156431,203807404668518239,206171082277271842,207959281033174845,210200684165150766,210977141105750492,211112122703994237,214233232557243320,218954323939192566,219774362428591482,228117884942331810,233414270986552128,233723179048988333,236380976398257310,250173792192604860,259688358120857083,262682687555581127,276772664276613303,287898523085783984,288815906523556240,302664929341659583,306968540092237074,307369513101698528,309353603770182643,315524345129880481,325811131100965361,336288337360025540,354228742998215710,358266608586438435,377437622495512785],"entities":["zetlyn","confluence","mediawiki","rss","atom","corpus-local","nothing","reads markdown","html","pdf word","powerpoint","excel","opendocument","office","markdown","env:orders_dsn","json","env:name","give zetlyn","postgresql","tls","mysql","eng","env:wiki_token","suspero","utc rendered","basic","confluence cloud","data center","oauth","lastmodified","susperobot/1.0","utc a","user-agent","susperobot","crawl-delay","normalised","urls","t\":\"doc\",\"address\":\"proj-41\",\"title\":\"refund","point","proj everything"]} {"document_id":"6da56f81d40eae0fede12f06c6ac6cfaed0674223dc5e2d14bc338856763c13c","content_sha256":"57ee2fdda896371ac520389d9eed84fa92e16a0696fb76c932e476f5ce6a1845","hashed":{"address":"zetlyn://zetlyn/docs/api.md","title":"API","url":null,"language":"eng","published":"2026-09-14T15:06:35Z","keywords":"","text_chars":6859,"word_count":1071,"text_hash":"ab50adbbc0116c88db7f2e7a6f63755219bfe337f71cb3f90bdbcba38485baec","catalog_version":"folder-2","embed_model":"multilingual-e5-small","extra":{"bytes":7117,"extension":"md"}},"embed_method":"chunked-256w-mean-v1","text":"API\n\nWhat zetlyn serve answers. JSON over HTTP, one scope per request.\n\nFour endpoints and a health check. /results answers, /route orders, /record shows one document\nand /why explains one relation between two. There is no fifth: everything a prompt needs arrives in\none of those responses with nothing to resolve afterwards, which is the whole reason there is no MCP\nserver either.\n\nCommon parameters\n\n| | |\n|---|---|\n| q | the question |\n| scope | which scope to answer over. Required |\n| k | how many results or steps |\n| lang | restrict to a language |\n\nscope names a scope the operator declared. There is no parameter that takes a list of corpus names:\na caller that could assemble its own set would be building combinations nothing was declared about.\n\nA scope that does not exist is refused. The refusal says so and does not list what does.\n\nGET /results\n\nA finished answer. One entry per source, not per document: a publisher's documents in one corpus\ncollapse into one entry that says how many it stands for and what put them together.\n\n{\n \"query\": \"refund deadline\",\n \"scope\": \"support\",\n \"count\": 4,\n \"results\": [\n {\n \"url\": \"https://…/refunds\",\n \"title\": \"Refunds\",\n \"corpus\": \"suspero/handbook\",\n \"summary\": \"…\",\n \"stands_for\": 3,\n \"collapsed_by\": \"near_duplicate/simhash\",\n \"also\": [\n {\"url\": \"https://…/refunds-old\", \"title\": \"Refunds (2024)\"}\n ],\n \"subjects\": [\"refund window\", \"SEPA\"]\n }\n ]\n}\n\nEvery reference is resolved to a URL and a title. There is nothing to join and no ids to look up.\n\ncollapsedby and also appear only where something was collapsed. collapsedby names the most\nspecific rule that holds between the entry and what folded into it — duplicateof/texthash,\nnearduplicate/simhash, quotes/minhashshingle — or the plain word publisher, which is not a\nrule from the registry and does not pretend to be one: it says these came from the same place, and\nthat is the whole of the claim.\n\nTwo things collapse. A rule about the text always does — two documents that are duplicates, near\nduplicates or one quoting the other are one answer. The same publisher does too, but only in a\ncorpus whose addresses are global: there a publisher is one party among many and folding their\nfifth page about the same question into their first is what makes ten results readable. In a corpus\nof one company's wiki, one file share or one database the publisher is the corpus, and collapsing\nby it would return the whole corpus as a single row.\n\nCollapsing happens within one corpus either way. An entry names one corpus, and merging two of\nthem would make that field a lie. Across a boundary the two documents are two sources by construction\n— that is what a corpus boundary is — and what stands between them is reported by /route as a link.\n\nGET /route\n\nThe reading order.\n\n| | |\n|---|---|\n| have | URLs already read, comma-separated. The route spends no budget on them, nor on documents that only restate them |\n| pool | how many candidates to plan over |\n| budget | coverage or tokens |\n\n{\n \"query\": \"rust ownership\",\n \"scope\": \"default\",\n \"steps\": [\n {\n \"step\": 1,\n \"url\": \"https://…/ownership\",\n \"title\": \"Understanding ownership\",\n \"corpus\": \"zetlyn/web\",\n \"publisher\": \"doc.rust-lang.org\",\n \"rule\": \"seed\",\n \"was_rank\": 1\n },\n {\n \"step\": 2,\n \"url\": \"https://…/borrowing\",\n \"title\": \"References and borrowing\",\n \"corpus\": \"zetlyn/web\",\n \"publisher\": \"blog.example\",\n \"rule\": \"new_voice\",\n \"was_rank\": 23,\n \"link\": {\"to_step\": 1, \"kind\": \"cites\", \"rule\": \"cites/outlink\", \"version\": 1}\n }\n ]\n}\n\nrule is the term that chose the step: seed, newvoice, newground, related or relevance.\nwasrank is where the document sat in the plain relevance ordering — a step from rank 23 is one a\nten-result ranking could not have shown.\n\nlink appears when the step stands in a relation to an earlier one, whether or not that relation\ndecided it. acrosscorpora is true where the two endpoints are in different corpora: neither corpus\nstated that relation, and it was derived from what both of them declare. A route is planned over what\nretrieval returned, so a document with no overlap with the question at all is not in the pool and no\nrelation reaches it.\n\nbudget is an argument on the request and not a setting on the server, because only the caller knows\nwhether this is a research task or a retrieval loop.\n\nGET /record\n\nOne document as this deployment holds it. Takes url or id, and scope.\n\n{\n \"url\": \"https://…/refunds\",\n \"address\": \"https://…/refunds\",\n \"title\": \"Refunds\",\n \"corpus\": \"suspero/handbook\",\n \"publisher\": \"example.org\",\n \"language\": \"eng\",\n \"subjects\": [\"refund window\", \"SEPA\"],\n \"countables\": {\n \"text_hash\": \"…\",\n \"simhash\": 10540996613548315209,\n \"text_chars\": 4820,\n \"word_count\": 763\n },\n \"relations\": [\n {\"kind\": \"near_duplicate/simhash\", \"to\": \"https://…/refunds-old\",\n \"title\": \"Refunds (2024)\", \"version\": 1, \"countable\": {\"distance\": 2}}\n ]\n}\n\npublisher appears only where the corpus's addresses are global. In a per-corpus corpus a\ndocument has no publisher, and answering \"who published this\" with the document's own address is the\nkind of vacuous field this endpoint exists to be the opposite of.\n\nThe countables are the point. Everything else here a caller could have assembled from /results;\nthose four are what let it check that the document it is being shown is the document that was\nindexed, without asking this server a second time.\n\nrelations are the ones stored in this document's own corpus. One whose endpoints are in two\ncorpora is derived per pair rather than listed, because listing them is a scan of the scope — ask\n/why for one of those. The read gate applies here as everywhere: a source its corpus does not allow\nto be shown is refused with 403, not returned in fuller form.\n\nGET /why\n\nWhy one relation exists. Takes from, to, kind, scope.\n\n{\n \"from\": \"https://…/a\",\n \"to\": \"https://…/b\",\n \"kind\": \"quotes\",\n \"rule\": \"quotes/minhash_shingle\",\n \"version\": 1,\n \"countable\": {\"shared_bands\": 4, \"of\": 64},\n \"recompute\": \"12-token shingles, FNV-1a 64-bit, keep the 64 smallest, count values in both\"\n}\n\nNo confidence score. A rule fired or it did not, and recompute is enough to check it yourself.\n\nWhat a title may be\n\nA document is shown under the rules of the corpus it came from, not the corpus the query started in.\nWhere a publisher has not cleared its headline the field is empty rather than filled with a substitute.\nAttribution, where a source requires it, travels in the same object as the text.\n\nErrors\n\n{\"error\": \"scope \\\"finance\\\" does not exist\"}\n\nThe service refuses rather than substituting. It does not answer over a different scope, return fewer\ncorpora than asked for, or drop a corpus it could not read.","simhash":3918362480613225127,"bands":[21501036795330639,26447863485519795,36923282627322110,79532416195255416,113885460671478567,114559156283480173,120688027325127632,188237911025319915,197535668427635109,206298649295790239,249271961234724475,252104166929748360,257542169179824012,264331869575822733,268839607575222522,294936367038890285,303046620978705687,315498542887579631,357786118795457550,386438835836283079,386552447001927843,406805148478758138,425152724488762135,430130987311388323,446569381123286424,511576594439742599,515667854709110492,519374184642801517,555614259439796117,557274520594617392,561968964863661963,570045427965629113,574172940738987041,584320512966540896,592485184649409294,620928506431832742,636196035107201002,658582902750160050,672070137231620582,694743053220957454,732252016319503284,754457296520780617,759079765317978859,770684390940267765,775926536710274280,784374614813523184,812140095108917095,828360723034299697,831935699429431378,915400712199843331,924150887811526374,929882074777298255,954723509380607869,957650561315765271,975150971431433541,1004079169393476869,1008023703717690022,1015661047660315403,1021856907106878220,1037955551322138586,1071124464825139334,1082325549942455894,1091455108230298730,1094945033782615608],"entities":["api what","json","mcp","get","refunds","sepa every","urls","understanding","references","sepa","fnv-1a"]} {"document_id":"8fca77488a90e1c26994bfe1469850bb2d400ed3ae5f49d0ed438110ed69de44","content_sha256":"366e5756c8485620725ee8c93da50484a09053a2d0f2ba080b21c7435a1ef1e7","hashed":{"address":"zetlyn://zetlyn/docs/concepts.md","title":"Concepts","url":null,"language":"eng","published":"2026-09-14T11:56:22Z","keywords":"","text_chars":4231,"word_count":720,"text_hash":"4763c62cc4b5d7bd4550df2724761ec92eb24710334e985655fce4dc9dff0dbe","catalog_version":"folder-2","embed_model":"multilingual-e5-small","extra":{"bytes":4591,"extension":"md"}},"embed_method":"chunked-256w-mean-v1","text":"Concepts\n\nZetlyn searches a body of documents and plans the order to read them in. It runs on your own\ninfrastructure, over your own documents, and it can search your documents together with corpora\npublished by other people.\n\nWhat comes back is a list of results pointing at the documents where they live. Zetlyn does not\nhand you the documents and does not answer the question. It tells you which ones to open, in what\norder, and why each one was chosen.\n\nThis handbook takes you from nothing to a running deployment. Everything here is a command you can\npaste.\n\nThe pieces\n\nBuilding a corpus:\n\n a source an adapter a corpus a version a hub\n ──────── ────────── ──────── ───────── ─────\n your files ──► zetlyn/folder ──► suspero/handbook ──► bytes ──► others fetch it\n your database ──► zetlyn/sql ──► suspero/orders\n the open web ──► zetlyn/web ──► suspero/research\n\nSearching:\n\n corpora a scope a server\n ─────── ─────── ────────\n suspero/handbook yours ┐\n suspero/orders yours ├───────► support ───► results, pointing at the source\n eu/eu-law from a hub┘\n\n| | What it is |\n|---|---|\n| adapter | pulls documents out of one kind of source and fixes the rules for them |\n| corpus | one configured adapter with a store of its own, named owner/name |\n| version | a corpus frozen into bytes: content-addressed, verifiable, handable to someone else |\n| scope | a set of corpora searched as one, chosen per query |\n| server | takes a query over a scope and returns results |\n| hub | where published corpora are fetched from |\n\nNames carry their owner. A corpus is suspero/handbook, an adapter is zetlyn/folder. The owner\nis a namespace, so two publishers can both offer a handbook, and an unqualified name is refused\nrather than resolved against a default — a bare handbook would mean something different on every\ndeployment that accepted it.\n\nOne corpus has exactly one adapter. That is the question everyone asks first. You do not attach\nthree adapters to a corpus; you make three corpora and put them in one scope. An adapter carries rules\n— how documents are addressed, what they are findable by, what may be shown — and a corpus with two\nadapters would be a corpus with two sets of rules and no way to say which applies to a given document.\n\nThe adapters\n\n| adapter | source | addresses | findable by | ends |\n|---|---|---|---|---|\n| zetlyn/folder | a directory of files | corpus-local | the whole text | yes |\n| zetlyn/sql | a query against Postgres or MySQL | corpus-local | the whole text | yes |\n| zetlyn/wiki | Confluence, MediaWiki | corpus-local | the whole text | yes |\n| zetlyn/feed | RSS and Atom | global | title and keywords | no |\n| zetlyn/web | public pages | global | title and keywords | no |\n\nOn addresses: global means an address identifies a document in the world, so the same URL in two\ncorpora is the same document. corpus-local means it does not: two companies both have\n/docs/readme.md, and those are two different documents.\n\nOn what a document is findable by: a web-shaped corpus indexes the title and the most frequent terms,\nbecause there are billions of candidates and precision is the problem. A private corpus indexes the whole text, because\nyou know the word is in section 7.3 and not finding it is not a worse ranking.\n\nMost frequent, minus the function words — English and German are covered by a fixed list, named in\nevery manifest, and a document in another language keeps every term it has. Without it a corpus\nfindable by fifteen terms spends nine of them on the and in is a to of at.\n\nEvery adapter is a separate program, including these five. They speak a published protocol, so an\nadapter you write for a source nobody has covered is not a second-class one — and if you would rather\nnot write one, zetlyn corpus add takes the same records by hand.\n\nEach adapter's settings, and how to write one, are in the adapter reference.\n\nWhere to go from here\n\nQuick start serves a corpus somebody else published, in three commands. The\nhandbook covers building your own, and how it works is the\nexplanation underneath both.","simhash":10890858597025736050,"bands":[11156958036510847,18128527137535086,23004631191451213,37018673148728222,55575443595054805,100776675033869983,206325071056016231,219774362428591482,238101133663276074,328925204399027389,336528266903390372,368277734437761384,387707763074087791,409296026695688179,410681153859324371,414527722237235344,436810321877368728,437735207092986525,469411854347507964,471646351996030034,512722674150986988,625322986672195687,627575545665262255,711696687932111090,735387564193525022,805596053488097102,820006132827793022,843201536502950562,848926301985447187,851931095026665458,859870610796433093,914536784044201177,933531013037052813,940733819911714180,956703990956769071,974218399238998954,975257142393964584,1024499624504927826,1063116859530963513,1085640762767307374,1088830997139699922,1168614789667248463,1200462423982181377,1205389908772261652,1283949155138099554,1294010668772208442,1396430673655302206,1434687493882269065,1473927635433392508,1481153662432020471,1485136116945538504,1510467586001022092,1557958816120189800,1623861662120260477,1646366315111520062,1662835978019004043,1679329587348208573,1682339132069281507,1727631689569042273,1751888154581903255,1756566776138145994,1887280455549425538,1893197449686316894,1927247165706030859],"entities":["concepts zetlyn","building","searching","names","postgres","mysql","confluence","mediawiki","rss","atom","english","german","quick"]} {"document_id":"9d41f8b4691418e6e561135ab7371d1228361d25d40842b8390c03255eb2604c","content_sha256":"7cdf5b09dc50637093790fe19303eec6ec37deef69f811aa9e3fc8d70455288c","hashed":{"address":"zetlyn://zetlyn/docs/deploy.md","title":"Deploy","url":null,"language":"eng","published":"2026-09-12T17:58:51Z","keywords":"","text_chars":3564,"word_count":623,"text_hash":"834b31be424eeec709a85d9bb076a96a57f4d5725eec548884657cc2dea17919","catalog_version":"folder-2","embed_model":"multilingual-e5-small","extra":{"bytes":3789,"extension":"md"}},"embed_method":"chunked-256w-mean-v1","text":"Deploy\n\nFour rungs. Each one says what it buys and what it costs, so you can stop at the lowest that holds.\n\nMost deployments stay on rung 1 or 2. Nobody needs rung 4 because a diagram showed it.\n\n| | | when |\n|---|---|---|\n| 1 | one process, one host | until it stops holding |\n| 2 | filling and serving are separate processes | filling must not be visible in answer times |\n| 3 | several servers reading one published version | more readers than one process carries, or failover |\n| 4 | several workers filling one corpus | one source is bigger than one process |\n\nOne process\n\nWhat the quick start builds.\n\nzetlyn corpus run suspero/handbook\nzetlyn corpus build suspero/handbook\nzetlyn serve\n\nserve reads the store directly. A run in progress competes with it for the same disk, which on a\nfolder of documents nobody notices.\n\nCosts nothing. Fails when a crawl is loud enough to be felt in answer times, or when the host is the\nonly host.\n\nSeparate reading from writing\n\nThe same machinery you use to hand a corpus to somebody else, turned inward: filling writes a version,\nserving reads one.\n\nzetlyn corpus run suspero/research # fills the store\nzetlyn corpus build suspero/research\nzetlyn corpus publish suspero/research --to /srv/versions\n\n[serve]\nlisten = \"127.0.0.1:8080\"\nreads = \"/srv/versions\"\n\nserve now reads a finished version and never touches the store being written. A version is a set of\nbytes that does not change, so a reader never sees a half-written corpus, and rolling back is pointing\nat the previous one.\n\nBuys: a crawl that cannot be felt in answer times, and a rollback. Costs: answers are as old as the\nlast publish. On a corpus you rebuild nightly that is what you wanted; on a news feed it is not, and\nyou stay on rung 1.\n\nSeveral servers\n\nThe same as rung 2, with the versions somewhere several hosts can read.\n\n[serve]\nreads = \"s3://versions.internal/zetlyn\"\n\nEach server fetches a version once, keeps it locally, and switches when a new one appears. They share\nnothing at request time and do not talk to each other, so one falling over removes capacity and\nnothing else.\n\nBuys: readers scale by adding hosts, and one host is no longer the deployment. Costs: object storage,\nand every server holds a full copy of the corpus.\n\nSeveral workers\n\nOnly for a source that one process cannot read fast enough — in practice a crawl, sometimes a large\ndatabase.\n\nzetlyn coordinate --listen 0.0.0.0:4300 # hands out work, tracks what is done\nzetlyn corpus run suspero/research --coordinator host:4300 # on each worker\n\nThe coordinator holds the work that is outstanding and hands out leases. A worker that dies loses its\nlease and the work returns; a worker that is slow gets less. Workers do not talk to each other.\n\nAn adapter has to be able to be split for this to apply. zetlyn/web can: the frontier divides.\nzetlyn/folder cannot usefully — one process walks a directory faster than three coordinate over it.\n\nAn adapter reports divisible when it starts, and corpus run --coordinator against one that says\nfalse is refused rather than quietly run as a single worker that looks slow.\n\nBuys: filling scales with hosts. Costs: a coordinator to run and watch, and a failure mode that did not\nexist before — work that is leased and never returned.\n\nWhich rung you are on\n\nzetlyn status\n\nPrints what is running, which version each server is reading, how far behind the store it is, and — on\nrung 4 — how many leases are outstanding and how old the oldest is. That last number is the one that\ntells you a worker died.","simhash":17140627813893781409,"bands":[13207596474195044,42741694264451370,63879141536187946,80682356234291587,84898060004681764,107015367335859189,210219718340085105,227454585775042053,245188227986298388,338227180695288309,349289848729138334,371052080996573431,398143834584102843,418432642548565673,433100618635248036,466461710141828266,512040238246779755,596321595424224514,667851122110736882,677146161706525819,801470314814273867,808780318049492132,816843228614285794,832383255334298672,844114596616471577,854757074409622154,866267420238861883,936130363483246797,951532097674619018,960055072764470493,979426780775636446,986279466028852391,993879958727351332,1004020866646169743,1017651144245092416,1036138698996896995,1066486342388115391,1147654550968020833,1157768935976997796,1185427280722535669,1214779288125141220,1245405003305898686,1271513484839452991,1288211726631335812,1292338304348650048,1335201267593569407,1418934775197478103,1463875262780374068,1483159384560151225,1521870248744617662,1523707690788691957,1533538793974545671,1557419373633090280,1558387144006807279,1582565036532745052,1586680455798452530,1632704375011940626,1632962205341917003,1640063407319025422,1672178267262307632,1675812801745159025,1684099137112306233,1795137954855241474,1797623943143963341],"entities":["deploy four","only","prints"]} {"document_id":"d672c41f09dc3def069f5348cc0e0f8cbbb039d34b374b0dbaa2d58646a1fcc2","content_sha256":"f6a358f728e21c6be9d715b13e097853001d938787a6872558787616229b331c","hashed":{"address":"zetlyn://zetlyn/docs/evaluate.md","title":"Evaluation","url":null,"language":"eng","published":"2026-09-12T17:58:51Z","keywords":"","text_chars":4168,"word_count":715,"text_hash":"fa9ae3dfac6b7f42ab3a8aace6e848210c2206cdd206b87ca99bf5c90f3873c8","catalog_version":"folder-2","embed_model":"multilingual-e5-small","extra":{"bytes":4261,"extension":"md"}},"embed_method":"chunked-256w-mean-v1","text":"Evaluation\n\nFor the people who have to sign it off rather than run it.\n\nWhere the documents are\n\nOn your infrastructure. Documents reach a corpus through an adapter you configure; nothing is sent\nout, no model is called out, and there is no training on your content.\n\nA corpus you subscribe to is fetched once and read locally. Nothing about a query leaves the building\neither — subscribing pulls bytes, it does not send questions anywhere.\n\nThe permission model\n\nThree layers, each refusing rather than substituting:\n\n| | |\n|---|---|\n| corpus | one configured adapter with a store of its own |\n| scope | a named set of corpora, declared by the operator. Every query names one |\n| read gate | per corpus, per source: whether a headline, a summary or the text may be shown |\n\nA caller cannot assemble a set of corpora. There is no parameter that takes corpus names, so the\ncombinations that can be queried are exactly the ones an operator wrote down. Nothing can be declared\nabout a set that a caller invents at request time.\n\nA document is shown under the rules of the corpus it came from, not the corpus the query started\nin. A service that applied one corpus's posture to another's documents would return a well-formed\nanswer that violated a publisher's terms, and report nothing.\n\nWhat can be audited\n\nEvery relation names the rule that produced it, that rule's version, and the countable that fired it.\n/why returns all three plus how to recompute the value yourself.\n\nA corpus declares the content hash of the rule set it was built with and of the display rules in force,\nso an answer given months ago can be matched against the rules that governed it.\n\nThere are no confidence scores anywhere.\n\nWhat it does not do\n\nEach of these is a decision rather than a gap.\n\nIt does not say whether two sources agree. That judgement was the one thing a reader could not\ncheck, and it was removed rather than improved. What you get instead is structure you can count: six\nindependent publishers cite this, two reuse its wording, three of these apparent sources are one\npublisher.\n\nIt does not say whether a document is worth reading. A route says a document adds something the\nearlier ones did not, and which rule said so.\n\nIt does not generate an answer. Nothing in the query path writes prose, and nothing hands you the\ndocuments. You get results pointing at them.\n\nSubscribing to somebody else's corpus\n\nA subscribed corpus brings a licence covering its documents and answers derived from them, attribution\nthat must appear wherever its material does, and whatever retention its terms impose. Those obligations\ntravel with the answers, including internally.\n\nzetlyn corpus show <ref> prints the licence and the conformance level before you subscribe.\n\nA hub is a directory served over HTTPS and holds no privilege: it serves files, answers no queries, and\nnever sees a question. You can run your own, and a corpus is verified against its own manifest whatever\nserved it. Pin a version hash, or pin the publisher's signing key, and a hub cannot substitute\nanything.\n\nPublishing one\n\nA corpus can be offered to others. Three conformance levels are defined by the\ncorpus specification; a distributed corpus reaches the\nthird, which adds per-document provenance, licence, attribution and content-addressed versions.\nzetlyn corpus verify reports the level reached.\n\nA corpus that stays inside the building stops at the second on purpose.\n\nThere is no recall. A subscriber who has fetched a version holds the bytes. An obligation to delete\nbelongs in your licence rather than in the format, which cannot enforce it and does not pretend to.\n\nDeployment and continuity\n\nOne process is a working deployment. Deploy has the four rungs and what each costs.\n\nRunning it needs no outbound access. A published version is a set of bytes you can archive, and a\ncorpus can be rebuilt from its sources at any time — nothing in a store is unrecoverable.\n\nLicensing of the product\n\nThe runtime is proprietary. The format it reads and writes is openly licensed and separately\nspecified, so a corpus you build is not locked to this product: anyone may write a reader for it\nwithout permission.","simhash":3418629296449584357,"bands":[17155094686196854,77893490951141650,80701531528304410,186738813558714322,192581151676310997,223380437342006578,238318485870580974,364346759299551307,372400903231230090,517880827633280903,537501001137455966,559191465801379744,574743877962180278,651460879171906405,744902307810992179,802372864368775931,808120345743730406,841064273205855557,880683835150501214,919442653066726687,965734867389409430,1025199125529000020,1028303866761735899,1095817733662221168,1098913153792218194,1101261842990277900,1147533722144854601,1154996527573919596,1186656618507296876,1209173378662487841,1218676843541624733,1238153128070514499,1243924244841140471,1300498870567287911,1357749100720192817,1408495004377521755,1424873897047201057,1426983481306436736,1503472056615911944,1522833236844718963,1528440853549635476,1530503545538910160,1536159513844342890,1558455164360047435,1590872956812239510,1605366683581543842,1605734243349338702,1625665519301162163,1631850923352439481,1637082739105079142,1637231019249053078,1669557763397429905,1694445917933114286,1699697811962229477,1706180091338807817,1724425853657681115,1838199784161171258,1943395241846445614,2072911197486746051,2174237903544142518,2213717161832370819,2257395278431737808,2327405775452632544,2353451002107096375],"entities":["evaluation for","nothing"]} {"document_id":"4f7c5c49deabf69b43e51caed364a242305dd70f2a53a3ac1c916b38f363a4ba","content_sha256":"fd6a3933bb66ea4ff60d263410dfa572d4e617fa8d7c57417d03a1fb6aea8183","hashed":{"address":"zetlyn://zetlyn/docs/handbook.md","title":"Handbook","url":null,"language":"eng","published":"2026-09-13T15:46:30Z","keywords":"","text_chars":9949,"word_count":1515,"text_hash":"c99ee85ffbb6c7492fef5676a53c475e65e3d6d3c339891dea8f7cc2b3a2ce9a","catalog_version":"folder-2","embed_model":"multilingual-e5-small","extra":{"bytes":10504,"extension":"md"}},"embed_method":"chunked-256w-mean-v1","text":"Handbook\n\nEverything after the quick start: building corpora of your own, crawling, searching\nseveral at once, and publishing.\n\nInstalling an adapter\n\nFilling a corpus of your own needs an adapter, which is a separate program:\n\nzetlyn adapter install zetlyn/folder\n\ninstall fetches the program and writes its path into your config, in the adapter's own row, so\nnothing is searched for when a corpus is filled. You only need the adapters you use — serving a corpus somebody else published needs\nnone at all.\n\nAn adapter name carries its owner. The five Zetlyn ships are zetlyn/…; one somebody else wrote is\nsuspero/jira. A bare folder is refused rather than resolved.\n\nBuild a corpus from your own documents\n\nYou have a directory of files. Markdown, HTML, PDF, plain text, Office documents.\n\nzetlyn init\nzetlyn corpus new suspero/handbook --adapter zetlyn/folder\nzetlyn corpus run suspero/handbook\nzetlyn corpus build suspero/handbook\nzetlyn serve\n\nWhat each does:\n\n| | |\n|---|---|\n| new | declares the corpus and its adapter in zetlyn.toml. Creates the store |\n| run | reads the source: new documents, changed ones, and ones that are gone |\n| build | computes the fingerprints, the entities, the relations and the index, and writes a manifest |\n| serve | takes queries and returns results |\n\nWhere the directory is goes in zetlyn.toml rather than on the command line, because zetlyn does not\nknow what settings a folder adapter takes. It hands the whole block over:\n\n[corpus.\"suspero/handbook\"]\nadapter = \"zetlyn/folder\"\npath = \"/srv/handbook\"\n\nrun and build are separate because reading a source is slow and network-bound and building is not.\nA re-build after a settings change does not re-read your files.\n\nWatch a directory instead of running it by hand:\n\nzetlyn corpus run suspero/handbook --watch\n\nA run that does not finish removes nothing. An adapter that dies halfway has not seen the rest of\nthe directory, and Zetlyn does not read that as \"those files were deleted\".\n\nCrawl the web yourself\n\nzetlyn adapter install zetlyn/web\nzetlyn init\nzetlyn corpus new suspero/research --adapter zetlyn/web --target 100000\nzetlyn corpus run suspero/research\n\n[corpus.\"suspero/research\"]\nadapter = \"zetlyn/web\"\nseeds = [\"https://example.org/start\"]\ntarget = 100000\n\nThe same command as Path 2, behaving differently because the adapter does. A folder ends, so the\nrun returns. The web does not, so it keeps going until you stop it. You do not tell Zetlyn which it is;\nthe adapter says so when it starts.\n\nzetlyn corpus status suspero/research # what it is doing right now\nzetlyn corpus stop suspero/research\n\ntarget is the size you want. Zetlyn keeps the best documents for that budget and evicts the rest; it\ndoes not grow without limit. The crawler is polite and not configurably so: it honours robots.txt,\nkeeps one connection per host, and backs off when a host slows down.\n\nA run builds as it goes here. There is no separate build step: a document is searchable, with its\nrelations, the moment it is fetched.\n\nBecause the source never ends, a run never reports itself complete, so a document is never removed for\nbeing absent. target is what bounds this corpus.\n\n---\n\nCorpora other people publish\n\nA reference is host/owner/name@tag:\n\nzetlyn corpus subscribe suspero/handbook # default hub, tag `latest`\nzetlyn corpus subscribe suspero/handbook@v3\nzetlyn corpus subscribe hub.internal.example/ops/runbooks@v3 # another hub\n\nThe first segment is a host when it contains a dot. Without one the default hub is used, which is\nwhy an owner may not contain a dot and a corpus name may.\n\nTwo hubs may both serve suspero/handbook, and a deployment cannot hold two corpora with one name.\nRename one when you take it:\n\nzetlyn corpus subscribe hub.internal.example/suspero/handbook --as suspero/handbook-internal\n\nThe name inside the artifact stays the publisher's. The name in your deployment is yours.\n\nA tag is mutable — the publisher moves it when they release. A version hash is not:\n\nzetlyn corpus subscribe suspero/handbook --pin sha256:4f2b…\n\nPinning is also the answer to a hub you do not control: a tag is the one thing a compromised hub can\nmove. If you follow tags instead, pin the publisher's signing key and a moved tag stops verifying:\n\n[hub.keys]\nsuspero = \"ed25519:MCowBQYDK2VwAyEA…\"\n\nFind one:\n\nzetlyn corpus search law\n\nInspect it before you commit to it. show reads the published corpus and holds it against what you\nhave:\n\nzetlyn corpus show eu/eu-law\n\n eu/eu-law\n owner EU Publications Office\n licence CC-BY-4.0\n documents 412,006\n conformance L3\n cadence weekly, versions kept for 12 months\n\n published 2026-09-08\n yours 2026-09-01 pinned\n\nSee what you have:\n\nzetlyn corpus list\n\n NAME ADAPTER SOURCE DOCUMENTS VERSION\n suspero/handbook zetlyn/folder /srv/handbook 1,204 built here\n suspero/orders zetlyn/sql postgres://db/… 58,913 built here\n eu/eu-law — the hub 412,006 2026-09-01 pinned\n zetlyn/web — the hub 2,145,880 2026-09-08 update available\n\nA corpus you built and one you subscribed to are used the same way. The difference shows up in this\nlisting and nowhere else: a subscription names where it came from and which version it is on.\n\nupdate available means the tag you follow now points at a different version. Take it, or stop\nfollowing:\n\nzetlyn corpus update zetlyn/web\nzetlyn corpus unsubscribe zetlyn/web\n\nA pinned subscription never says update available, because a pin does not follow a tag.\n\nSearch several corpora at once\n\nA scope is a named set of corpora.\n\nzetlyn scope new support --corpora suspero/handbook,suspero/orders,eu/eu-law\nzetlyn serve\n\ncurl \"localhost:8080/route?q=refund+deadline&scope=support&k=5\"\n\nEvery query is answered over a scope, and a caller cannot assemble one. There is no parameter that\ntakes a list of corpus names. The operator declares which combinations exist; a caller that could name\ncorpora directly would be building its own, and nothing could be declared about it in advance.\n\nA deployment starts with a scope named default. zetlyn corpus new and zetlyn corpus subscribe add the corpus to\nit unless you say otherwise, which is why the queries in the three paths above needed no scope=.\n\nResults from different corpora are combined by rank, never by raw score. A corpus that has nothing to\nsay about the question does not fill the result list just because it is in the scope.\n\nRelations across corpora — a document in one corpus quoting a document in another — are derived when\nboth corpora agree on what the relation rests on. Most of that is fixed by the specification; what\ndiffers in practice is the extraction. zetlyn scope check tells you where a scope can\nand cannot do that:\n\nzetlyn scope check support\n\n suspero/handbook suspero/orders eu/eu-law\n ──────────────── ────────────── ─────────\n shared subjects: all three\n duplicates, near-duplicates, quotations: suspero/handbook, suspero/orders\n (eu/eu-law: built on a different extraction)\n citations: eu/eu-law only\n (the others are not globally addressed)\n\nPublish a corpus\n\nTurn a corpus into bytes somebody else can fetch.\n\nzetlyn corpus publish suspero/handbook \\\n --owner \"Suspero GmbH <docs@suspero.com>\" \\\n --licence CC-BY-4.0 \\\n --attribution \"© Suspero GmbH\" \\\n --cadence \"weekly, versions kept 12 months\"\n\nAll four are required to reach L3, which is the level a corpus needs before anyone else can use it.\nzetlyn corpus verify says so if one is missing.\n\nThis writes a version: the documents, the index artifacts, and a manifest, all content-addressed. Two\nbuilds from the same documents produce the same version.\n\nGive it a destination to make it available:\n\nzetlyn corpus publish suspero/handbook --to hub.zetlyn.com --tag v3\nzetlyn corpus publish suspero/handbook --to /srv/pub --tag v3 # a directory you host yourself\n\nThe corpus already carries its owner and name, so --to names only where it goes. Without it the\nversion stays in your own store, which is what you want while you are still checking it.\n\nPublishing under an owner on hub.zetlyn.com needs that owner registered to you. Another hub is a\ndirectory served over HTTPS and is written to by whatever writes to that host.\n\nLater versions are deltas when that is cheaper. publish knows the version it produced last and works\nout what changed, so there is no flag for it.\n\nA delta is applied only onto the base it names, and someone subscribing later cannot replay an\nunbounded chain — so a full version is written periodically regardless. --full forces one now.\n\nThere is no recall. Once someone has fetched a version, they have the bytes. If a document must be\nwithdrawn, stop publishing it in later versions and put the deletion obligation in your licence.\n\nCheck a corpus\n\nzetlyn corpus verify suspero/handbook\nzetlyn corpus verify ./downloaded-corpus\n\nverify recomputes what the corpus claims: document ids from addresses, content hashes from fields,\nfingerprints from text, entity frequencies from postings, the version from the payloads. It reports the\nconformance level reached:\n\n| | |\n|---|---|\n| L1 searchable | can be searched, and ranked together with another corpus |\n| L2 related | relations can be derived against another corpus |\n| L3 publishable | can be handed to somebody else under stated terms |\n\nA corpus that stays inside your building is done at L2. L3 adds provenance, licence and attribution,\nwhich cost you something and buy nothing unless you distribute.\n\n---\n\nWhere to go next\n\nReference for the commands and zetlyn.toml, adapters for the five\nsources and how to write a sixth, API for what serve answers, and deploy for\nmore than one process.","simhash":17022685039698581666,"bands":[5841409144335750,26774384024867919,38223550957610540,56195843072611164,70238207232125709,78582245423840942,78936402168496168,97677963024867266,100005957716673022,130885642795142127,133399709903941493,134805772038491773,141191031359717624,142202530296031705,154319743203202801,165155099445988835,170459080529491126,173171090753737947,179367983783761782,195687541483651892,206637041734275478,224218392027906134,232034197046527349,248762675640848846,256701674515729238,269402959137200221,274285640839751463,276285416763216666,278566710721274873,284548079838949103,331409210293804619,335079644085345102,362395561451097683,362419185918406732,366750047730873958,376323805031767265,397403417263401850,422678767948892344,446110357389246607,459216784309938361,489657857289346472,496951269559034769,504722628858383715,504934586847395698,505011165862221745,518257234838414324,525759254171166656,531700191094669219,534942195336763439,551164407535209646,557253788724958274,568898694256410115,576340317259517827,608291073714005642,610721981467399271,660973298642848100,662271023616660810,662927858968931441,674576238726433945,683015699257389431,685708897750337371,686395594115126615,689133924553571332,690631210597003679],"entities":["handbook everything","filling","zetlyn","html","office","path","pinning","ed25519:mcowbqydk2vwayea","find","inspect","eu publications office","cc-by-4.0","see","name","adapter","source","documents","version","publish","turn","suspero","gmbh","all","reference"]} {"document_id":"d226724bd504048c5b509ac93aa3b1c74d28b8824ea71e74119ce1f731e26454","content_sha256":"f22b848e0c2a5d7a40c3ef3a9ffef293ce743af2b7aec1f85bd2bee12bd48a65","hashed":{"address":"zetlyn://zetlyn/docs/how-it-works.md","title":"How Zetlyn works","url":null,"language":"eng","published":"2026-09-12T17:59:18Z","keywords":"","text_chars":7991,"word_count":1418,"text_hash":"d0ca24d13c7c4397bf13f8f3c0b040f083c7429ed6ad2c117793394da77e179b","catalog_version":"folder-2","embed_model":"multilingual-e5-small","extra":{"bytes":8154,"extension":"md"}},"embed_method":"chunked-256w-mean-v1","text":"How Zetlyn works\n\nWhat happens to a document on its way in, and to a question on its way out. This is the explanation,\nnot the instructions — the handbook is where the commands are.\n\nThe parts\n\n a source ──► an adapter ──► a store ──► build ──► serve ──► results\n (a program) (a corpus)\n\n| | |\n|---|---|\n| adapter | a separate program that reads one kind of source and writes documents. It does the extraction, and it declares how documents are addressed and what they are findable by |\n| store | one corpus: its documents, their derived values, its relations and its indexes |\n| build | computes everything derived from the text and writes the manifest |\n| serve | takes a question over a scope and returns results |\n| coordinator | only when one source needs several workers. Deploy has the four rungs |\n\nA corpus has exactly one adapter. Several corpora are searched together through a scope, which is a\nset of them named by the operator.\n\nWhat happens when a document arrives\n\nThe adapter hands over an address and plain text. Everything after that is computed once, from the\ntext, and never guessed at again.\n\nIdentity. The address is hashed. Under a global address space the address is the native one — a\nURL identifies a page on the internet, so the same URL reached from two corpora is one document. Under\na corpus-local space the address is scoped to its corpus first, because two companies both have a\n/docs/readme.md and those are two documents.\n\nA content hash over the facts about the document — the address, the title, the language, the\ncounts, the hash of the text, the extraction version, the embedding model, and a block of whatever\nonly this kind of source has: a web page's outbound links, a row's status. A reader holding the\ndocument can recompute it and get the same value, which is what makes a corpus checkable rather than\nmerely fetchable.\n\nThe countables, each derived from the text and each with its method fixed so that another party's\nvalue means the same thing:\n\n| | |\n|---|---|\n| text hash | two documents with the same hash are the same text |\n| simhash | 64 bits over word trigrams. Close fingerprints are near-duplicates |\n| shingle bands | the 64 smallest hashes of its 12-token runs. A shared band is a shared verbatim span |\n| embedding | one vector, from a declared model applied in a declared way |\n| entities | the distinctive names the document mentions, canonicalised |\n\nNone of these is a score or a judgement. Each is a measurement that a second implementation would\nreproduce exactly from the same text.\n\nWhat is indexed\n\nLexical. Either the title and the most frequent terms, or the whole text. The adapter decides,\nand it is not a tuning choice: a web-shaped corpus has billions of candidates and precision is the\nproblem, while a private corpus is small and the caller knows the word is in section 7.3 — not finding\nit is not a worse ranking, it is a broken product.\n\nVectors. One per document, in a column in document order. The nearest-neighbour structure is built\nfrom that column by whoever serves the corpus, so a consumer that chunks differently can build a\ndifferent one over the same vectors.\n\nEntities. Which documents mention which name, and how many documents mention each name. The\nfrequency table is published because distinctiveness cannot be derived from one side alone.\n\nThere is no batch. A document is searchable, with its relations, the moment it is written. That is\npossible because every corpus-wide quantity an insert would need is either maintained incrementally or\napplied later, at read time — which is the next section.\n\nHow relations are found\n\nA relation is a connection between two documents, produced by a named rule at a named version, and it\ncarries the countable that fired it. Nine rules: identical text, near-duplicate fingerprints, shared\nverbatim runs, an outlink that resolves, the same rolled up to a domain, a mention, a shared\ndistinctive entity, and two that a publisher's own index declares.\n\nEvery rule compares hashes, counts, string identity or a Hamming distance. None reads a distance out of\nembedding space. A relation is a measurement of form, never of content: quotes says a verbatim run\nappears on both pages, not that one endorses the other.\n\nSome relations are stored, one is not. Whether an entity is distinctive depends on how many\ndocuments mention it, which changes as the corpus grows. A predicate frozen at insert would be right\non the day it was written and quietly wrong a month later, so the fact is stored — this document\nmentions this entity — and the corpus-relative gate is applied when the relation is read.\n\nThat principle is why inserting stays cheap while the corpus-relative meaning stays correct.\n\nWhat happens when a question arrives\n\nThe scope decides which corpora. Every query names one; a caller cannot assemble its own set, so\nwhat a query can reach is exactly what an operator wrote down.\n\nEach corpus answers for itself. Lexical retrieval and vector retrieval run against that corpus and\nare combined into one ordering of its own candidates.\n\nAcross corpora, positions are combined, never scores. A retrieval score depends on the corpus it\nwas computed in — a term that is rare in a web crawl is ordinary in a standards corpus — so two scores\nare two scales. Ranks carry no such units.\n\nWhether a corpus actually answers is a separate question, and a rank cannot settle it. Rank one is\nrank one whether the document answers the question or merely exists, so a corpus holding nothing relevant would still\ncontribute its best and fill the answer with it. What is needed is a magnitude on a scale that means\nthe same thing everywhere — one end a genuine match, the other a document no better than one picked at\nrandom — and a corpus whose best is far below what the others offer is dropped rather than scaled down.\n\nThe pool becomes a route. The surviving candidates — a few hundred, not the ten a result page\nshows — are planned over, which is the next section.\n\nEach step is checked against the rules of its own corpus before it is shown. A document subscribed\nfrom somebody else is displayed under their terms, not under the terms of the corpus the question\nstarted in.\n\nWhat comes back is an ordered list: each step with its document, the corpus it came from, the rule that\nchose it, where it sat in the plain relevance ordering, and any stored relation to an earlier step.\n\nWhy a route and not a ranking\n\nA ranking answers which documents match. Nothing in it asks whether the fourth result says anything\nthe first one did not, which is why five results can be two things: a guide, two copies of it, a\nsibling domain under the same owner, and one independent explanation.\n\nA route is built one step at a time. At each step the planner takes the candidate that adds the most\ngiven everything already chosen, and adds is the whole design. A candidate is worth more when it\ncomes from a publisher not yet heard from, when it covers ground the earlier steps missed, or when it\nstands in a relation to something already picked. It is worth less when it repeats one of them.\n\nThe step reports which of those decided it, so a reader can disagree with the plan rather than only\nwith the result.\n\nhave is the same idea across calls. The caller names what it has already read, and those\ndocuments count as chosen before the planning starts — so the second call does not spend its budget on\nthem, nor on the documents that only restate them.\n\nThe weights are not published. Which term counts how much, and how a candidate's relevance trades\nagainst what it adds, is the part of Zetlyn that is not specified anywhere and is not meant to be. The\nshape above is what you need to read a route; the numbers behind it are what makes one route better\nthan another.\n\nWhere the parts run\n\nOne process is a working deployment. Splitting filling from serving, adding servers, and adding\nworkers are three further rungs, each with a cost — deploy has them in order.","simhash":13919459152273646833,"bands":[11340141490527237,13353905536738186,19764852201498210,26849969834670901,30287769780848473,39908065394700990,48401778825210111,49973589056157929,76036740366116611,89774918658465359,103600389967116218,105042206182042336,109518845582568978,133377575334410342,151568372182373297,151998569371540036,183296780903475480,188002590913092685,188549291393596484,199069575833218114,199266153548196035,203743100039796803,238923215049204912,241269225346567688,242837782717261325,250154120608878179,270681008895192151,277139114906556975,280223104223138091,284146788687907920,287040429636572385,291396399212084045,291495831865898204,300576611238332208,311102119633213435,311604522635570159,316649261411734620,326326375688314189,346404212060869054,353861190424021348,371156501925782751,372597315256915196,382787554739934049,386829113848161371,399640503863505269,406470594730864551,445066204571267168,483203881755795880,491674138838087352,522246119294301701,527147500848239601,537279695943809587,544660172984074704,544700085277963533,547998525787659205,568482690185763599,579687904929848336,591921183789467972,599321000804117182,611490735495884662,613284804385889151,618908203377930784,628874818802553703,654326290761243774],"entities":["zetlyn","under","none","lexical","hamming"]} {"document_id":"6b0a50d437a6c3722b48deaffbf67c100f1bcb083432ae6867bad3b295a8535a","content_sha256":"ff87f23c90c48886644a2df970e3f00ee77e9be4c8f31c56297b55b455fa4361","hashed":{"address":"zetlyn://zetlyn/docs/quickstart.md","title":"Quick start","url":null,"language":"eng","published":"2026-09-13T15:46:30Z","keywords":"","text_chars":1564,"word_count":241,"text_hash":"41d0a49e6d0fb5f61a17ea183cd66a53136c384ed868b284efa0c392210d59d2","catalog_version":"folder-2","embed_model":"multilingual-e5-small","extra":{"bytes":1866,"extension":"md"}},"embed_method":"chunked-256w-mean-v1","text":"Quick start\n\nSearch over a corpus somebody else published, in three commands. No adapter to install, nothing to\ncrawl, nothing to build.\n\nIf you would rather know what a corpus and a scope are first, read concepts.\n\nInstall\n\ncurl -fsSL https://get.zetlyn.com | sh # Linux, macOS\nbrew install zetlynhq/tap/zetlyn # macOS\ndocker pull ghcr.io/zetlynhq/zetlyn # anywhere\n\nOne binary. Adapters are separate programs, and you need none of them to serve a corpus that already\nexists.\n\n---\n\nSubscribe and serve\n\nzetlyn init # set up here\nzetlyn corpus subscribe zetlyn/web # use a published corpus\nzetlyn serve\n\ncurl \"localhost:8080/route?q=rust+ownership&scope=default&k=5\"\n\nsubscribe fetches from a hub, verifies the bytes against the manifest, and writes the corpus into\nyour config. A tag works like a git tag: mutable, moved by the publisher when they release.\n\nzetlyn corpus subscribe zetlyn/web@2026-09 # a tag\nzetlyn corpus subscribe zetlyn/web --pin sha256:4f2b… # these bytes, forever\n\nFinding other corpora, and seeing what you already hold, is in\nthe handbook.\n\nWhy scope=default\n\nEvery query names a scope: a set of corpora the operator declared. A caller cannot assemble its own,\nwhich is what makes it possible to say in advance what a query may reach.\n\nsubscribe puts the corpus into the scope named default, so there is one to name from the start.\nSearch several corpora at once has the rest.\n\nWhat next\n\nBuild a corpus from your own documents,\ncrawl the web, or\npublish a corpus of your own.","simhash":9144826600707150360,"bands":[35063475610582697,91579453833673038,160646139393541393,212405604348957050,239350829582784297,252190725542732545,416044006553426417,493211424094063271,553830013284961421,588766011551715644,612493207209968871,647577505487151056,1049603449191609650,1753543258095809714,1896474000358575826,1949490053547946569,2012358282789355831,2132116425889160882,2319667410359196056,2540088569763862159,2574092705311174579,2638333638637445634,2751414164892404475,2965411565069130024,2998901307261026269,3015989146202878151,3035641294947948446,3251942116071882521,3346241399924498654,3356508709724137037,3356923287013647079,3580063123579544616,3650208431009330387,3739228457311172933,3770484896082244270,3885491887369943025,3923778806589939748,3931653478358141607,3957461033893505736,4121100327386440782,4204007421956920054,4218346197358079474,4246973828283027900,4247397912258656198,4483860784440148372,4565477113963518792,4637877282001031583,4666601305262044366,4800112128870651014,4923734143235091612,5057810649084739644,5112345946566094576,5128671474061550242,5178903694784678643,5222058025423613124,5318952422977140387,5587595258868447510,5651468693216124388,5683666816621219668,5714804621576908875,5719328063668341136,5889917354037646963,5993872561236478529,6011452224533365841],"entities":["search","fssl","linux","macos","finding","build"]} {"document_id":"72265fc6495b638f169a79d380575196396b8fd7565c45f3995918f272bce823","content_sha256":"9e46685eddc041c58b36a4f9bec428563730a0c8708fb62e013a0209a5c21c38","hashed":{"address":"zetlyn://zetlyn/docs/reference.md","title":"Reference","url":null,"language":"eng","published":"2026-09-14T10:55:23Z","keywords":"","text_chars":7024,"word_count":1113,"text_hash":"f073f633d07b14643f8a4560d16333a499540c8fb74291cf6703c15c4b84f487","catalog_version":"folder-2","embed_model":"multilingual-e5-small","extra":{"bytes":7391,"extension":"md"}},"embed_method":"chunked-256w-mean-v1","text":"Reference\n\nzetlyn.toml\n\nEverything the commands above write, you can also write yourself.\n\n[serve]\nlisten = \"127.0.0.1:8080\"\ndata = \"./.zetlyn\"\n\n[embed]\nmodel = \"multilingual-e5-small\"\nmethod = \"chunked-256w-mean-v1\"\ndimension = 384\nnormalized = true\n\n[adapter.\"zetlyn/folder\"]\ncommand = \"/usr/local/lib/zetlyn/adapter-folder\"\n\n[adapter.\"zetlyn/sql\"]\ncommand = \"/usr/local/lib/zetlyn/zetlyn-sql\"\n\n[hub]\ndefault = \"hub.zetlyn.com\"\n\n[hub.keys]\nsuspero = \"ed25519:MCowBQYDK2VwAyEA…\" # a moved tag from this owner stops verifying\n\n[corpus.\"suspero/handbook\"]\nadapter = \"zetlyn/folder\"\npath = \"/srv/handbook\"\ninclude = [\"**/*.md\", \"**/*.pdf\"]\n\n[corpus.\"suspero/orders\"]\nadapter = \"zetlyn/sql\"\ndsn = \"env:ORDERS_DSN\"\nquery = \"\"\"\n select id::text as address, title, body as text, updated_at\n from orders\n\"\"\"\n\n[corpus.\"suspero/research\"]\nadapter = \"zetlyn/web\"\nseeds = [\"https://example.org/start\"]\ntarget = 100000\nread_gate = \"closed\"\n\n[corpus.\"eu/eu-law\"]\nsubscribe = \"eu/eu-law\"\npin = \"sha256:4f2b…\"\nhub = \"hub.example.com/corpora\" # where it came from, when not the default\n\n[scope.default]\ncorpora = [\"suspero/handbook\", \"suspero/orders\", \"eu/eu-law\", \"zetlyn/web\"]\n\n[scope.support]\ncorpora = [\"suspero/handbook\", \"suspero/orders\", \"eu/eu-law\"]\n\ndefault always exists. corpus new and corpus subscribe add to it unless told otherwise, so a\ndeployment has a scope to name from its first corpus onward. Remove a corpus from it by editing the\nlist; the corpus stays, and queries naming default stop reaching it.\n\ndata is where stores live. Everything under it is rebuildable from the sources.\n\nA hub carries corpora and adapters, in two trees: hub.zetlyn.com/corpora/… and\nhub.zetlyn.com/adapters/…. Both are fetched the same way and checked the same way — the files a\nmanifest lists, each against what it says, and then the set of them against the version they were\nserved under. A fetched adapter lands beside the stores rather than on the system path, because a\ndeployment is its directory.\n\nAn owner may not hold a corpus and an adapter under one name. zetlyn/folder names one thing, and a\nname that meant a corpus in one command and a program in another would be a name nobody could read.\n\nTwo models are pinned and are not settings: zetlyn-entities-1, the entity\nrecognizer, and the embedding below. zetlyn model install refuses anything whose hash is not the one\nthe name names — a corpus built with other bytes is not conforming under that name, whatever its\nmanifest says. Both come from download.zetlyn.com.\n\nembed is a declaration, not a tuning knob. The model is one of the twelve fields a document's\ncontent hash covers, so changing it after a corpus is built makes every document in it a different\ndocument. The values shown are the defaults and are the specification's reference values; a\ndeployment that means to use others says so before it builds anything.\n\nAn adapter's path is its own row, written once. In a corpus.x block Zetlyn owns its own keys —\nadapter, target, readgate, subscribe, pin and hub — and hands everything else over\nunchanged, which is why a new adapter needs no change to Zetlyn.\n\npin fixes a version. A subscriber who pins gets the same bytes forever and is unaffected by anything\nthe hub does afterwards; one who follows a tag is trusting the hub not to move it, which is the only\nthing a compromised hub can do to them. hub is where the bytes came from, written down at\nsubscription so update looks in the same place without being told again — a directory, or a host\nwith the prefix the tree begins at.\n\nreadgate is open or closed, and open unless you say otherwise: what may be shown for a source\nthe corpus has no rule for. Open is right for your own documents and wrong for a crawl of other\npeople's pages, which is why it is the operator's key and not the adapter's.\n\nCommands\n\n| Command | Context | |\n|---|---|---|\n| zetlyn init | deployment | writes zetlyn.toml in this directory |\n| zetlyn version | deployment | |\n| zetlyn serve | deployment | takes queries, returns results. --listen |\n| zetlyn coordinate | deployment | hands work to run workers |\n| zetlyn status | deployment | what is running, which version each server reads, outstanding leases |\n| | | |\n| zetlyn adapter install <owner/adaptername>@tag | adapter | fetches the program, checks it, writes its path into the config. --command <path>, --from <path\\|host> |\n| zetlyn adapter list | adapter | which adapters are installed |\n| zetlyn adapter remove <owner/adaptername> | adapter | |\n| | | |\n| zetlyn corpus new <owner/corpusname> --adapter <owner/adaptername> | corpus | declares a corpus, creates the store. --target, --read-gate |\n| zetlyn corpus list | corpus | what this deployment holds, built and subscribed |\n| zetlyn corpus rm <owner/corpusname> | corpus | removes a corpus and its store |\n| zetlyn corpus run <owner/corpusname> | corpus | reads the source. --watch, --every, --full, --coordinator |\n| zetlyn corpus add <owner/corpusname> --from <file\\|-> | corpus | puts documents in directly: the adapter protocol's records, by hand |\n| zetlyn corpus build <owner/corpusname> | corpus | computes everything derived. --built-at |\n| zetlyn adapter publish <owner/adaptername> --binary <path> --to <path> | deployment | puts an adapter on a hub. --target, --interface, --tag |\n| zetlyn model install --from <file> | deployment | installs a pinned model and checks it is the one its name names. --embedding takes a directory |\n| zetlyn model list | deployment | which pinned models are installed |\n| zetlyn corpus status <owner/corpusname> | corpus | what a running run is doing |\n| zetlyn corpus stop <owner/corpusname> | corpus | stops it |\n| zetlyn corpus verify <owner/corpusname\\|path> | corpus | recomputes, reports the conformance level. A path checks a corpus you have not subscribed to yet |\n| zetlyn corpus search <term> | corpus | finds published corpora |\n| zetlyn corpus show <owner/corpusname> | corpus | one published corpus, against the version you hold |\n| zetlyn corpus subscribe host/<owner/corpusname>@tag | corpus | uses a corpus somebody else published. --pin <hash>, --as <owner/corpusname>, --from <path\\|host> |\n| zetlyn corpus update <owner/corpusname> | corpus | fetches a newer version of a subscription |\n| zetlyn corpus unsubscribe <owner/corpusname> | corpus | stops using it |\n| zetlyn corpus publish <owner/corpusname> | corpus | freezes a version. --to <path>, --tag, --owner, --licence, --attribution, --cadence |\n| | | |\n| zetlyn scope new <name> --corpora <a,b,c> | scope | declares a scope |\n| zetlyn scope check <name> | scope | what can be derived across it |\n\nReferences\n\n[host/]owner/name[@tag]\n\nThe first segment is a host when it contains a dot; without one, hub.default is used. No tag means\nlatest, which is an ordinary tag the publisher sets rather than anything automatic.\nHub layout has the rest.\n\nSee also\n\nAdapters — the five sources, their settings, and how to write your own.\nAPI — what zetlyn serve answers.","simhash":3038468972729038997,"bands":[4448587717097823,6692487669101236,24316178553092671,34108998424448410,39937310047383250,40522875262364136,43362666520680947,88622873209209655,91714428507239474,120524576867885771,133725391733200473,142635609853259377,143600461571017717,153248239117757326,154666141257888337,167181702001041302,200901678027483462,227555587437950662,227980887950701810,290122386358204778,298826499742059501,301624441089233818,302220819495352828,321674676086506673,336309697253390930,344325629296907661,351765729310071636,353407751478251612,392129843185549524,420914385883389075,432131484015445630,445334830441919146,465427011965803935,489880435285297910,492601631044642746,552009316156887488,557931300512972500,561742702367008957,562953194801761173,570944436041960659,575274516970869786,601976412636835998,654699738226574664,701393573352688961,708444915789489676,752300795240404002,770095625167022852,782812874810816253,819220834984192084,839210647702861551,845527182799046408,885512633486006383,932604085207546518,946840030497090231,961016554535541694,962238085128161130,965494498659212234,979101636424811605,1003052244689147558,1008577160544077851,1043801628699106832,1107852318801920655,1146879070863401612,1148211909646842199],"entities":["ed25519:mcowbqydk2vwayea","env:orders_dsn","zetlyn","commands command context","references","adapters"]}