Aletheus · methodology

How every answer
is made.

What this tool does, in the repository's own words. Every promise on this page is quoted or paraphrased from the code that keeps it, and beside each one is the name of the standing test that fails if the promise is ever edited away. Nothing here is a new claim.

§ 1

Where every answer comes from

Every answer is read from public filings — the SEC's EDGAR system, the SEC's monthly Form ADV adviser rosters, the SEC's enforcement release feeds, fund registration filings, and court records via CourtListener. The lookup module states the discipline in its own opening lines:

“Each answer carries the source that proves it, or says plainly that it doesn't know.” … “sourced or silent — no speculation, ever” lookup.py, module docstring

In the written report, an unsourced factual sentence is not merely discouraged — it cannot be rendered. The one function that admits a factual sentence into the findings describes itself as

“The only door a factual sentence has into the findings section” … “No pin, no sentence — the refusal is a raised error” onereport.py, cite()
held by test_report.py

When nothing is found, the answer says so — and says what that does and does not mean. A bare “no records found” is banned from every report; each empty result instead carries this fixed frame, followed by the source's own stated coverage window:

“No responsive record was found for this exact query, within this source’s own stated coverage — which is not a statement that no record exists anywhere.” nullcopy.py, the absence frame on every empty result

Which sources may be fetched by software and which may only be opened by hand is decided by each source's own terms, read and captured before use. The full source-by-source table — every database this tool knows, the posture its terms earned, and what the report shows for it — is written out on the Sources page, generated from the same inventory the search runs over. The catalog the code actually fetches from is a separate constant, and the counsel page's not-searched lane is injected from that catalog at serve time. Sources whose terms bar automated access are never fetched; they appear as labeled links a person opens themselves.

held by test_deeplinks.py — the fetch catalog is pinned row-for-row against the research record, so in the catalog the code obeys, no source is ever silently promoted from the hand-off lane to the automated lane

Since 2026-09-11 the research page carries one search box for all of it. One public shape — a firm or fund name, a CIK, a firm CRD, a ticker, a fund series or share-class id, a legal entity identifier, a surname, or an EDGAR filing URL — goes to every source the inventory knows (the count is read from the inventory itself at serve time, never typed into a page), and the answer holds exactly one row per source: searched (the publisher was asked; here is what came back, with the record's own date and its citation), linked (this program never asks that publisher — its terms, or the house rule — so here is the publisher's own page for a person to open), or not searched with one enumerated reason (offline, timed out, not the right kind of input, adapter not built yet). A row that says “no results” appears only after a completed bounded search; a source that is missing from the answer is a bug. The search module says so in its own opening lines:

“Nothing here is silent: a source that is missing from the response is a bug, and a row that says ‘no results’ without a completed bounded search is a bug … The vintage on every row is the publisher's own record date, never the moment of retrieval, never a file mtime, never the build night.” source_search.py, module docstring
held by test_source_search.py — every inventoried source yields one row in fixed order; addresses, links, query syntax and private identifiers are refused before any publisher is contacted; a slow publisher becomes a timeout row inside the ten-second budget; the ninth concurrent request is refused before any work; nothing about a query lands on disk. The per-source adapters were built and reviewed in a separate research lane against captured real publisher responses, with sockets disabled in every test.

For broker-dealers, the tool also reads the public portion of the firm's most recent X-17A-5 annual report on EDGAR (catalog row D-040, added 2026-09-02 in the open): the filing's structured cover verbatim, permalinks to its public documents, and whether a net-capital computation section is present in them. A figure is quoted only when it parses complete and verbatim from clean document text — never reconstructed from a PDF's glyph streams — and where only the cover is public, the answer says exactly that. The module states its own bound on every answer:

“this bears ONLY on the current-assets clause of what records cannot show, and ONLY for broker-dealers … a filed net-capital figure is what the firm reported as of the filing’s own period end — a dated statement from that filing, not what the firm owns today.” netcapital.py, the narrow claim carried on every answer
held by test_netcapital.py — the narrow claim rides every answer by identity, no number is ever machine-read out of a PDF, a truncated amount is refused rather than quoted, and the what-records-cannot-show sentence in lookup.py is asserted unchanged, verbatim
§ 2

Confirmed, possible, given

Every identification an answer makes is labeled with how it was made, and the label is part of the data — in the lookup module's words, “CONFIRMED vs POSSIBLE is structural, never a footnote” (lookup.py, module docstring).

  • CONFIRMED — the entities share an identifier the firm itself reported on its own filing: a CRD number, a CIK, an SEC file number. The counsel page states the rule and its reason: “An EDGAR link is marked CONFIRMED only when the firm itself reported that identifier on its own filing. … A wrong join is a false statement about a real business.”
  • POSSIBLE — a name-only match. It is shown as possible, with what would settle it, and is never silently merged: “Look-alike entities are never merged — a name-only match appears as possible, with what would settle it, never as the same entity” (onereport.py).
  • GIVEN — a fact supplied by the user's own records, such as the holdings in an uploaded statement. It is labeled as given rather than dressed up as a finding.

The relationship diagram applies the same rule to lines instead of names. Its docstring: “Edges the record does not support are simply absent — an unproven line on a chart is how a diagram starts lying” (lookup.py, chain()). Every edge drawn carries the filing behind it and one of the three labels above; a firm the rosters do not contain gets a refusal, not a partial chart.

held by test_chain.py — every edge carries a basis, every confidence label is one of the three, and an unknown firm refuses
§ 3

What this tool refuses to do

The refusals are not policy statements; each one is a standing test that fails the whole suite if the behavior appears.

  • It does not advise. Every tool answer carries, byte-exact, the same fixed sentence: answers “describe what was filed, not whether anyone did anything wrong, and they are not legal advice.” The text is pinned in a test, so it cannot be reworded or detached from the answers it governs. Phrases that would make this sound like a legal service are banned from every shipped page and module.
  • It does not accuse, and it does not conclude. Answers describe records in the record's own words. The report's executive summary is counts only, linted against an adjective blacklist (onereport.py); lookup answers are checked against a list of legal-conclusion words that must never appear.
  • It contacts no one. There is no code path that sends email or letters — no mail-sending module, no send command, anywhere in the shipped code. The one mail link on the whole site is a pre-filled draft on the client page that only a human can send.
  • It ranks no one. No scores, no grades, no ordered list of named firms or people is built by this code — and a test walks the whole tree and fails if a file or directory ever turns up under any of the names such artifacts are given: leaderboard, ranking, scoreboard, scorecard.
  • It loads nothing external. Every page is self-contained — no outside scripts, no outside fonts, no render-time request to any third party. External links are navigation a person clicks, never resources a browser fetches.
held by test_guards.py (the pinned disclaimer, the banned phrases, the no-send scan, the named-artifact walk) and test_copy.py (zero external resource loads on every served page)

What this site stores and transmits is a separate question, answered in plain English on Privacy & terms — this page describes method, not hosting.

§ 4

How current the data is — and how you'd know

Nothing in this tool runs on a clock. The freshness module says why:

“There is no scheduler. Nothing in this tool runs on a clock — no cron entry, no daemon, no background thread, no timer. Refreshing is a command a person runs on purpose… A file that is out of date is a reported fact — it says so on the page it is read from — not a silent one.” freshness.py, served with every /api/freshness answer

The program has no clock of its own; the host that runs the public site has one, and says so: a nightly job on that machine runs the same refresh commands a person would, an hourly job revalidates the SEC enforcement feeds against the publisher (one conditional request per feed), and a failed run raises an alert rather than a quiet stale file. Each bundled source declares who publishes it and how often that publisher actually republishes — the SEC's adviser rosters monthly (registered and exempt-reporting, kept apart), its fund and fee datasets quarterly, its enforcement feeds continuously (revalidated against the publisher on every nightly run, and again at query time after seven days). Page footers print each source's date beside its status, and any source past twice its publisher's cadence raises a banner naming the file, its age, and the exact command that refreshes it. A vintage that cannot be read reports as unknown — never as fresh. Where no lawful free source exists at all — daily closing prices — the table says “not refreshable” and why, rather than dressing the gap up as a schedule.

held by test_freshness.py — real ages, both boundary directions, and the banner in the bytes the server actually sends

Age alone is a countdown, not a check. Since 2026-09-11 the last step of every nightly run asks each publisher what it holds newest — the SEC's roster index, the next N-PORT quarter's archive, the fee dataset index — and writes the answer beside the file's own date. A source the publisher has since replaced reads as due whatever its age, the health check says so, and the footer's source table shows what the publisher holds, whether it is newer, and when that was last checked. The fee dataset's date is the newest quarter it covers, never the night it was rebuilt.

held by test_watermarks.py — a newer watermark makes a day-old file due; a build stamp cannot refresh a vintage; offline the probe refuses and leaves the file alone
§ 5

The standing admission

Every surface that shows an analysis admits the same thing, in the same words: automated analysis may be inaccurate or incomplete — verify anything that matters against the cited source documents before relying on it. A mislabeled column, an unusual export, a source that changed its format: any of these can make a computed figure wrong, which is why the admission travels with every answer instead of living on one page nobody reads.

held by test_disclaimers.py — the phrase is asserted on every reader-facing analysis surface, paired with the remedy, in generated output as well as source

This page describes how the tool works in general. It is not advice about anyone's situation, and nothing this tool produces establishes anything about any person or firm beyond what the cited records themselves say.

§ 6

Answering a stranger without learning who asked

There is a read-only surface for machine callers, and it has no account, no key and no password, because a key is a name. A list of keys would be a list of who is running adviser searches, which is the one record this tool exists not to keep — the same reason its search caches are kept out of version control, and the same reason it keeps no record of who asked anything. The honest cost is stated rather than worked around: with no idea who is calling, this surface cannot investigate a caller afterwards either.

Saying that in the answer is not the same as it being true, and for a while it was not. When this site is run in its hosted setting, the server opens a browsing session on the first request and hands back a cookie, so that one visitor's cached lookups stay out of another's. That session was being opened for every address on the server, including this one — so each answer that carried the no-cookie sentence also carried a cookie, a replayed one was accepted, and the server held one entry per caller in memory. Nothing was written to disk, but a per-caller identity is exactly what this surface says it does not have. This surface is now outside the session system altogether: on these addresses no cookie is issued, none is read, and no visitor is recorded, whichever setting the site is run in. The rest of the site keeps its sessions unchanged.

So what would otherwise be policed after the fact is made impossible to ask for instead. A name search needs at least three characters, because one letter used to match 32,712 of the archive's 34,213 firms — five vowels between them matched almost all of it, which is an export wearing a search box. An answer describes at most twenty-five firms, and the count is taken before the answer is built, so a query that would be expensive is declined rather than truncated: there is no paging, no cursor, and nothing behind the cap to walk. The two Form ADV disclosure columns the archive holds are served on no route at any size, so the surface cannot be assembled into a table of firms sorted by anything. Every answer carries its source, the dates of the roster files it came from, and the standing admission from § 5.

It is dark by default. It opens only where a dated owner decision exists on that host, in the same form and checked the same way as the sign-off that gates the Insights tab — delete the file and the surface closes again. On aletheus.org that decision was recorded on 2026-09-01, so the four questions answer there, with every cap and refusal described in § 11.

held by test_publicapi.py — every route, every refusal, the archive checksummed either side of a full sweep, and a parse of the module proving it makes no call that writes anything anywhere
§ 7

A plain-English question is a closed door, not an open one

There is a layer that takes a question in ordinary words — “tell me about firm 361”, “how many advisers were registered in November 2013”, “when did this firm leave the roster” — and answers it from the roster archive. What it is not is a translator: the question’s words never become part of a database query’s structure. The layer holds a fixed set of question shapes, each mapped by hand to one pre-written query whose only moving parts are bound values, and a question that fits no shape is refused by name, with the list of shapes that would have worked. It will not guess, because a best-effort reading that runs the wrong query is a wrong answer about a real firm.

The refusals are checked before the questions are. Ask it for the riskiest firms, the most of anything, a sort, a comparison, a disciplinary list, everyone in a state, or anything about a person, and the refusal names the rejected shape rather than approximating an answer to it. Lists it does return come in a fixed identifier order, time series come in date order, and no parameter anywhere chooses a sort key, a column, or a limit. Counting questions state two numbers separately — what the roster file listed and what the archive stores — and the November 2013 answer carries, in its own body, that the SEC’s file declared 13,447 advisers of which 2,542 rows had no identifier at all. A firm’s leaving is stated as a window between two named snapshot dates, never as a date and never as a reason. Every answer and every refusal carries the same pinned admission as § 5, the same withheld-columns rule as § 6, and the archive is only ever opened read-only.

held by test_questions.py — every intent and every refusal exercised, the six historical search defects re-asked through the plain-English door, every intent run against a read-only archive file checksummed either side, the live archive’s bytes checked unchanged, and a parse of the module proving no query is ever assembled from a question
§ 8

The same closed door, on the site

The question layer of § 7 is served to visitors at /ask. The page is a plain form round-trip — a question goes to the server, the answer or the refusal comes back as a rendered page, and no script is needed for any of it. The engine is used unchanged: the page adds no question shape, no parameter, and no way past any cap or refusal the layer already holds. A refusal is rendered as guidance, not as an error — the reason by name, the layer’s own explanation, and the list of what can be asked. Every string the page renders, the visitor’s own question included, is HTML-escaped before it reaches the page, and every answer carries the standing admission of § 5 in its own body.

held by test_ask_page.py — the route serves the page, an answer and a refusal both render through a real form round-trip, a hostile question comes back escaped, the barred shapes refuse on the page exactly as they do in the layer, and the disclaimer rides the served bytes
§ 9

A typo gets alternatives, never a guess

When a name search matches nothing, the refusal may carry up to five similar names drawn from the archive’s own name index — trigram-filtered, then verified within a small, length-scaled edit distance (at most two edits), in alphabetical order. They are alternatives for the asker to re-ask, never a ranking and never a substitution: no search is ever run with a corrected string on anyone’s behalf, a suggested name establishes nothing about any firm, and zero suggestions is a complete answer. The index behind this is built in the serving process’s memory only; nothing derived from anyone’s query is ever written anywhere.

held by test_namesuggest.py — real misspellings surface the right names, garbage stays quiet, the order is alphabetical and deterministic, the barred shapes still refuse unchanged, a static parse proves the module holds no write statement, and the archive’s bytes are checksummed unchanged across a full suggestion sweep
§ 10

An issuer’s own funds — and the list this tool will not build

Given one issuer (fund family) name or one fund ticker, the tool lists the defined-outcome funds its local, filing-derived fund index ties to that issuer — in fixed ticker order, each row carrying the filing-derived source the index holds for it, every answer carrying the index’s stated coverage and the retrieval date. That is the whole feature, and its module says why in its own opening lines: the reverse index people actually ask for — name a failed product, get back the firms that sold it — is not built here. The adviser roster archive carries no issuer, product, security or CUSIP field of any kind; its private-fund and wrap-program columns are counts a firm filed, not names of anything; and broker-dealers are not on the SEC investment-adviser roster at all. No data source held here ties an issuer to the firms that sold or advised on its product, and this tool does not guess across that gap.

The refusal is structural, not editorial. The module that answers the issuer question never imports the database layer — it cannot reach the adviser roster or produce a CRD, so it cannot name a firm even by accident. Every answer, found or refused, states the unanswerable half in so many words and names the per-firm questions that can be asked instead when a firm is already known. Funds are never ranked, counted into an ordering, or sorted by anything but their tickers; the banned scoring vocabulary this site already enforces on the migration tool is asserted key-for-key on every answer shape this module can produce.

held by test_issuer_funds.py — fixed ticker order, the verbatim banned-vocabulary scan on every output key, no firm identifier anywhere in any answer, the import scan proving the module cannot reach the roster, and the selling-firms limit riding every answer with what can be asked instead
§ 11

The four questions a machine may ask, and the wall around them

The stranger-facing surface of § 6 answers on exactly four routes, under the path /public/v1/, and it will name them itself when asked. A capabilities route lists what follows and what it withholds. A search route finds a firm by name, CRD, CIK or SEC file number, answering up to twenty-five firms and refusing rather than truncating beyond that. A firm route returns one firm’s roster record, addressed by exact CRD and never by name — its registration identity, where it filed from, the sizes it reported, its own affiliation answers, and every snapshot date it appears in. A snapshots route lists every roster file behind every other answer, with its date, firm count and source filename. There is no fifth route, and no route that writes anything anywhere.

Every answer and every refusal arrives in one fixed envelope: the source it was read from, the roster dates it covers, the caps it was held to, the sentence naming what a registration roster cannot show, and the standing admission of § 5. A refusal draws its reason from a fixed vocabulary — a name query shorter than three characters, a name that matches more firms than one answer may describe, a CRD that is malformed or absent from the archive, a route that does not exist, a surface too busy to answer, or a surface not open on this install — and each reason carries, in the body of the answer, the thing to do instead. A refusal is the same answer to the question that was asked, honestly incomplete; it is never a page of a longer list, because there is no paging, no cursor, and nothing behind the cap to walk.

Two columns the archive holds leave through no route at any answer size: the two Form ADV disclosure columns. The surface states its own reason in place of a habit. Served in bulk they are a firm-level table of which firms carry a disclosure and of what kind, and sorted that is the shape ROADMAP.md rejects as “a public accusation engine” — one no cap on a single answer’s size would keep a caller from assembling one firm at a time. Withholding the column is what makes the assembly not worth beginning. A firm’s own switchboard number is withheld for a smaller reason: this tool contacts no one, and no question a roster archive answers needs it.

This is the surface of § 6, and it keeps § 6’s posture exactly: dark until a dated owner decision sits on the host and an operator turns it on, answering every route with a stated refusal until both are true (both are true on aletheus.org since 2026-09-01). Nothing here is a developer portal for a feature that is live by default; it is a description of the shape the surface takes on the day an owner opens it.

held by test_publicapi.py — every route and every refusal exercised over a real socket, the archive checksummed either side of a full sweep, and a parse of the module proving it makes no call that writes anything anywhere