Dados B3 › Transparency
Transparency
Transparency — how you check the numbers yourself
Data version 2026-09-27-reapresentacao-agreg · page generated 2026-09-27 03:31 UTC. If these do not match /saude, you are reading a cached copy.
What is checked, before every publication — 466 automated tests, collected live
346 are DATA invariants (does the number tie out?) and 120 are product tests — access gate, navigation, language, AI connector. Calling all 466 'invariants' would be inflating it: the split is checkable in the list itself. Not a hand-picked highlight reel: every test below was found by introspecting the actual test files just now, the same way pytest --collect-only would. Grouped by what it protects against, not by which file it lives in.
Level 1 — Integrity: does the data tie out? (298)
I-X05— Every quote is positive and within a possible range.I-X06— No daily jump above 15%.I-X07— The US$ return must equal the R$ return minus the FX move — the identity the calculation itself asserts.I-X10— The `n` of each lag is EXACTLY the overlap between the flow and the shifted return — not one pair more.I-X11— No `n` may exceed the length of the shorter series.I-X12— The permutation preserves each group's size AND shuffles blocks that are contiguous IN TIME.I-X13— Attacks I-X12 with three defective implementations and requires it to fail two and pass one.I-X14— A Central Bank (SGS) window that fails is tried again before it becomes a gap, and each failure logs its REASON. In the 26/09/2026 build the 2008–2017 window came back 'unavailable' and the exchange-rate series had a nine-year hole; the same query answered minutes later. Without a retry a hiccup of the API is a hole until the next Sunday; without the reason, 'unavailable' could not tell a hiccup from a changed source.I-B34— `situacao` may only be one of the four declared values — an open domain would become free text, and free text in a field that exists to separate 'confirmed' from 'unknown' reintroduces the ambiguity it was built to remove.I-B35— A company marked `com_proventos` really has dividends, and one marked `sem_proventos` has none — the stamp is recomputable from the table it describes, so it cannot lie in the dangerous direction: claiming confirmed absence where the fetch failed.I-P16— Every stored unit composition respects the maximum error and minimum sample the inference itself declares — the difference between 'the function applies the rule' and 'what is in the database went through it'.I-P17— COUNTER-PROOF from B3's RAW file: recomputes `a×ON + b×PN` against the unit price without going through the function that inferred it. A wrong pair still minimises some error; what it cannot do is explain the price within 2% across thousands of sessions.I-P18— A company whose primary ticker is a unit WITH a known composition can no longer carry the not-adjusted flag, and its price-to-book must land in the same plausible band as everyone else — the outcome test that proves the basket was applied, not merely discovered.I-B57— The ÚLTIMO ordering is the ORIGINAL filing and PENÚLTIMO is the restatement, and the proof is structural rather than remembered. The oldest year in the base exists only as PENÚLTIMO, because it reaches us through the following year's file, the first one we download; and the most recent year exists only as ÚLTIMO, because next year's file has not been published. If that asymmetry ever disappears, the meaning of the two columns has changed and everything depending on it must be revisited.I-B58— On the restatements page, the first column serves the ORIGINAL filing and the second serves the restatement. This was published inverted: the largest-adjustments table showed one holding company's 2012 total assets rising from 39 to 364 billion when the movement was the opposite. Counts and the absolute difference never depended on the ordering; direction did, and a wrong direction on a page whose whole argument is rigour is worse than no page.I-E06— The ticker returned by the bulk dump is the same one `ticker WHERE principal=1` would give — never `empresa.ticker`, which is hand-curated enrichment covering only 20 companies and left 94% of the paid indicators dump with a blank ticker.I-E07— The `datas_suspeitas` dump dataset is EXACTLY what `eventos.datas_suspeitas()` computes internally — same source, two formats. If they diverged, the dump would be lying about what the pipeline itself treats as suspicious.I-E08— The counter-proof of the whole finding: BBDC3, ITSA3, FLRY3 and ALUP11 carry no suspicious date at all — a consumer that excludes a whole company instead of a single date would drop them for no reason. The problem was never too little confirmation; it was the wrong granularity in whoever consumed the data.I-B01— Total assets == total liabilities (account 2 already includes equity).I-B02— Gross profit == revenue + cost (cost arrives negative).I-B02b— Pre-tax income + tax == income from continuing operations (3.09).I-B03— NOPAT = EBIT × (1 − tax rate) with the rate in [0, 45%], and ROIC is only published when average capital is positive.I-B04— The database universe is EXACTLY what the published rule produces right now — recomputed by a route independent of what the ingest wrote.I-B04b— The 20 hand-resolved companies keep their ticker in the database — enrichment must not be lost in a bulk re-ingestion.I-B27— ROA = net income / average assets, and total assets are always positive (I-B05 already guarantees 0 <= assets < 1e14).I-B06— A hole in the MIDDLE of a company's series is a loading defect — data that should be there went missing.I-B09— Gross margin recomputed straight from the RAW filing (bypassing the standardised fact) matches the published indicator.I-B26— The Piotroski F-Score is a sum of 9 binaries: it can only be an integer from 0 to 9. Anything else means a criterion leaked a non-boolean.I-B28— Current ratio recomputed straight from the RAW filing (account 1.01 of the balance sheet over 2.01, bypassing the fact) matches the published indicator.I-B30— Every company with a fact has a macro sector, and the sector is one of the 10. A new registry sector that matches no rule leaves the company sector-less and fails this test — instead of landing silently in the wrong bucket.I-B21— A dividend is a cash inflow: value > 0, always.I-B22— Type within a closed domain and ex-date in ISO YYYY-MM-DD — what the endpoints and the dividend-yield calculation assume.I-E01— `mult_qtd` is recomputable from B3's RAW field by the published rule, and `mult_grupo` is the product of the events sharing the same day and ISIN.I-E02— Every event marked `conferido=1` genuinely passes the check against the COTAHIST price — the stamp cannot be more generous than the rule it claims to apply.I-E03— A verified event carries no rejection flag, and a rejected event is not marked verified.I-F01— PRODUCT INVARIANT: the book value per share reported in the filing equals equity ÷ shares from that SAME filing.I-F03— Counter-proof: P/B recomputed here (price ÷ book value per share), outside the fiis module.I-F04— The ISIN cross-match (COTAHIST ↔ filing) resolved a ticker for a reasonable mass of funds, and most of them have a P/B.I-X01— `net = inflow − outflow` in EVERY month.I-X04— The reference period is always 'YYYY-MM'.I-L01— When a fact's origin is composite ("DFC_MI:6.02.01+6.02.02"), the sum of the referenced lines equals the fact's value.I-L02— Every account cited in a fact's origin genuinely exists in the raw filing it points to.I-L05— End-to-end counter-proof: for a sample of known companies the lineage delivers indicator → input → account line → document with publication date → CVM file, and the line's value matches the fact.I-B19— The stored trailing-twelve-month profit matches the TTM recomputed HERE, straight from the annual and quarterly fact tables, bypassing the pricing code.I-B20— Every year from 2010 to the current one has prices in the database.I-B29— FCF yield = (CFO + capex) / market cap, recomputed straight from the fact (CFO 6.01 + capex 6.02), bypassing the pricing code.I-P01— No trading code is tied to more than one company.I-P03— In the window where the old and the new code coexist, the company has ONE price per day — the primary one's.I-B13— The same invariant as I-B01 (assets = liabilities), at quarterly granularity.I-B10— Numbers checked by hand, one by one, against the computed output.I-P21— A multiple whose price came two or more years after the fiscal year always carries a flag. The regulator's receipt date belongs to the LAST version of a filing, so when a company re-files, the date moves forward and our first-session-after-publication rule lands years past the year being measured. Anyone filtering on 'no flag' must not receive one year's profit matched against a price three years later.I-P22— Put the defect back and the stale-pair mark has to appear. Without this attack the rule could never fire and the invariant above would pass on an empty set, which is exactly how a checker is born decorative. It also checks that an existing flag is preserved rather than overwritten, and that the ordinary case — a filing published the following year — is left unmarked.I-P06— low ≤ open, close ≤ high, in EVERY row.I-P08— On a date where the company traded under TWO codes, all five prices come from the winning code — not one from each.I-P09— Reference prices, checked in ABSOLUTE value.I-P10— Financial volume ≈ quantity × average price — the anchor that CROSSES field slices.I-P11— Volume, quantity and trade count checked in ABSOLUTE value.I-P13— No primary ticker has far less traded value than a still-active sibling of a DIFFERENT share class — the property the liquidity correction exists to guarantee, asked of the database rather than trusted from the code that wrote it.I-B55— No company the regulator lists as non-active sits inside our universe. The page stating what this product does NOT fix publishes that intersection as zero, so if anyone ever loosens the ACTIVE-only filter on the company register, the published claim silently becomes a lie while nothing else breaks. This invariant is what turns a written promise into a checked one.I-B56— The measured size of our survivorship bias cannot quietly become zero. If ingestion of the company register fails, the page would publish 'no companies left the market since 2010', and a limitation that disappears is worse than a limitation admitted: the reader would conclude the base has no survivorship problem at all. The floor is deliberately far below the real count so only an ingestion failure trips it.I-B36— The published trail points at an account that EXISTS in that company's filing. Born from the public challenge, the first time it ran: provenance declared '3.11 (consolidated profit)' for every company, and 3.11 does NOT exist in Itau's statements — banks use a shifted chart and their profit sits in 3.09. Worse, the code varies between banks (Bradesco 3.11, Itau 3.09, Banco da Amazonia 3.13), so ingestion resolves those items by description and records the real account. The data was always right; the TRAIL was wrong — and the trail is what this product sells.I-B59— The most recent trading session in the base has to be recent. The battery had 266 checks on the CONTENT of the numbers and none on their AGE — nothing asked when the data was from. It cost 25 days in production: prices stopped on 10 August and the site kept serving them into September while the weekly rebuild reported ok every Sunday. Stale prices are not WRONG prices, which is what makes them slip through content checks; each row is still right for its own day, and it is the set that lies when it is presented as the series up to today. So this test reads no value at all, only the clock. The threshold is deliberately generous, because a failed build freezes everything, including what was fine.I-B60— The health endpoint publishes how far the series reaches, not just how many prices it holds. A count measures volume, not freshness, and it was the count alone that let the series freeze quietly: 869,232 prices looks healthy even when it has not moved in a month. This keeps the date published rather than internal, and checks that the limit shown is the limit actually enforced.I-B61— A balance-sheet account that drops about a thousandfold and comes back is flagged, and so is the return-on-equity built on it. The older detector finds the year where DOZENS of accounts are off scale, and everything downstream leans on one premise: if all of it is off by the same factor, the factor cancels in a ratio, so ROE and margins stay valid and get published. When a single account slips out alone that premise breaks — the profit is right and the denominator is not. One retailer filed equity of R$ 467 thousand between R$ 517 million and R$ 284 million, two accounts out of twenty-eight, and its ROE went out at -201.2% with no caveat when the truth was about -106%; because ROE uses average equity, the following year was contaminated too. A wrong number served as clean is the worst defect here, because the reader has no way to suspect it. An earlier version of this test only read the finished database and passed even with the flagging code deleted, so it now drives the rule itself over a synthetic series.I-B62— Equity on its way through zero is not mistaken for a scale error. A telecom holding shows equity of R$ 1.8 million between +R$ 116 million and -R$ 219 million: not a unit slip, a trajectory crossing zero on the way negative. A detector that accused that would be crying wolf, and a detector people learn to ignore costs more than none. Honest note: the sign guard was credited with protecting that case, and attacking it showed the case is actually excluded by the hundredfold minimum drop — across the whole base the sign guard currently prevents zero cases. It stays because the rule is right and data changes, but it is exercised against a synthetic trajectory so that removing it still fails this test.I-B63— The quarterly series publishes only what the quarterly filing publishes: Q1, Q2 and Q3. The regulator's interim filing never carries a standalone fourth quarter — it comes out of full year minus the nine months, an identity that closes by construction and that a direct competitor computes. We do not, because a figure we derived would enter the same list as the figures the company reported, carrying the error of two filings and erasing the line between what was filed and what we calculated, which is the line this whole database exists to hold. The rest of the test is the old rule: a ratio is only valid between accounts of the same vintage and the same consolidation perimeter, and a financial institution gets no gross or operating margin because that chart of accounts has none.I-B63b— Quarterly return on equity is twelve months of profit over the AVERAGE controllers' equity of the same twelve months, and it says so when there is no pair to average. A trailing profit against an end-of-period snapshot compares a year of flow with one photograph; the annual series already solved this with an average and declares the first year of the series. The window that matters is the SAME quarter of the previous year — not the immediately preceding quarter, which would average three months against twelve months of profit.I-B64— The decision to rebuild the database is read from disk, not held in the process's memory: a week already rebuilt successfully is not rebuilt again, and outside the early-morning window only an unfinished build from that same week is resumed. Every Sunday deploy used to rebuild the whole database, because the memory of “this week is done” was a local variable that resets with the process. Four deploys on one Sunday started four rebuilds, each killing the last, and on a single-CPU instance the visitor paid for it: a median of 1.9 seconds, a 90th percentile of 7.6 and peaks of 13 — including on the robots file, which is a constant and touches nothing. Nothing flagged it, because the panel read “running”, exactly what a healthy rebuild reads. The window itself was already promised in the loop's own docstring, which said Sunday small hours while the code fired at any hour of Sunday.I-B64b— Reading the build state never breaks the loop that it watches. A database from before this change simply has no record of the last successful build, and the behaviour then has to fall back to rebuilding — the safe side — rather than raising an error that would kill the maintenance thread. The same rule already applied to writing the state: the alarm must not be what burns the house down.I-B65— An indicator that a whole class of company never has is declared as not applicable, rather than rendered as a dash that reads as “we could not get it”. A bank's page showed sixteen years of blank return-on-invested-capital: the exclusion list named gross margin, operating margin, net-debt/EBITDA and current ratio, and forgot that one. The JSON route already said the right sentence; the page did not, and the page is what almost everyone reads. The test measures instead of trusting the list — an indicator with zero rows across all twenty-one financial companies must be declared, so the list cannot age in silence when a new indicator arrives.I-B66— The nightly price update looks for exactly the business days missing after the base's last session, up to today — and today only after the hour at which the exchange publishes its daily file; outside the night window it does not try. Prices used to enter only in the Sunday rebuild, from the exchange's ANNUAL file, which itself arrives days late: the base built on 9 September had 3 September as its last session, so the price was anywhere from zero to six days old and nothing said which. The exchange also publishes a daily file around 9 pm Brasília time; that is what now enters every night, and the price served by the API, the connector and the dump becomes yesterday's or today's close. The rule is pure on purpose, so it can be attacked without network: asking for today before 9 pm is a guaranteed 404, so is Saturday, and skipping the Monday after a Friday holiday is a hole forever.I-B66b— The nightly run ingests the daily file into a COPY of the base, checks it, and only then swaps the live base. A malformed file (minimum above close) is refused and the live base does not change by a byte; a night without a file (holiday, or not yet published) does not touch it. Never replacing good data has been the weekly build's rule from the start, and a routine that runs every night has more chances to go wrong than one that runs every Sunday. The ingestion functions commit midway, so writing into the live base would not be atomic; the copy restores atomicity, and the check is the daily gate: minimum ≤ close ≤ maximum on every new row (a shifted slice of the fixed-width layout produces a plausible number, not an error), last session advanced, nothing shrank, version stamp intact.I-B66c— The cache key of the screener, the today page and the ticker page changes when the base FILE changes, not only when the pipeline version changes. It used to be the version alone, and that does not change on Sunday: the weekly rebuild swaps the file and keeps the version, so the screener and the today page kept serving the previous week's list until the next deploy. With prices entering every night that would become a stale number every day, served from cache and looking fresh. The company page already keyed on (version, mtime) and did not suffer; the rule now lives in one place.I-B66d— Swapping the production base requires the write-ahead log of the target to be consolidated and empty; with pending frames beside it, the swap is refused. A plain file rename left the OLD log next to the NEW base. The base runs in WAL mode: if the log still holds unconsolidated frames (one open reader when the last writer closed is enough), SQLite applies them onto the new base at the next open — an old page on top of a new table. That is silent corruption no content invariant would see, and the routine that records build state in the live base is exactly such a writer. The swap now checkpoints and truncates the log first and refuses if it could not, for the nightly run and for Sunday alike.I-B67— The company profile (description, listing segment, headquarters, share registrar, auditor, control) comes from each company's MOST RECENT registration form; the listing segment is the one of the PRINCIPAL ticker; the address is the head office; the registrar is the one currently acting. An older version never overwrites a newer one, and the versioned derivative round-trips without losing a field. The first screen used to say the tax id and the regulator's code and nothing else. Comparing with a competitor made the gap obvious: what the company does, where it is, since when, in which segment it is listed, who keeps the share register, who audits, who controls. All of it was already in the files downloaded every week and simply not read. The description is the text the company ITSELF files, and the page says so; the rule choosing the version matters, because the form arrives with every version of the year and a reordering of rows must not swap the profile for an old one.I-B68— Besides the price, the nightly update asks the exchange for stock dividends and fund distributions, and the regulator for dividend filings (the future ex-date), on the same copy and before the yield is recomputed, and records how many came in. A source that is down does not bring the run down nor erase what already exists. The calendar was being born without a future: stock dividends only entered in the Sunday rebuild, so the newest announcement in the base was 18 days old and there was not one future ex-date for a stock, while funds had 138 scheduled payments. The build's rule applies here: an external source is best effort — if it fails, what was there stays, with the reason recorded.I-B69— Boot applies COLUMN migrations, not just CREATE TABLE IF NOT EXISTS — which never adds a column to a table that already exists. The migrations used to live only in the ingestion path, which runs on the weekly rebuild, so a new column could take a week to exist in production while the page depending on it kept saying 'not measured yet' with the data already in the repository. And reloading a versioned file cannot depend only on 'is the table empty?': that table was already full with the old columns, so the condition said no and the new portrait would never enter.I-B70— With the ETF table already filled, boot ADDS the rows the versioned file has and the database lacks, and leaves existing rows untouched. The old rule skipped the file entirely whenever the table had any row, so the history the backfill brought — the August 2025 anchor that enables the 12-month return — sat in the repository without ever reaching production, and the page kept showing zero funds. The house rule still holds: boot never overwrites what the build or the nightly run wrote. Completing is INSERT OR IGNORE, never REPLACE.I-B71— `dados_b3.leitura` is the stable internal reading interface: it only reads, takes the connection from the caller, imports no page or style module — and what it returns MATCHES what the company page shows, value by value and flag by flag. Sector medians are the same calculation; the company's own range uses only clean values; the universe distribution's 'fraction above X' equals a direct count. Without it, any new layer would be welded to the 1,133-line function that builds the page.I-B72— Rebuilding the database from scratch PRESERVES the snapshot of the companies that left the exchange (years of DFP filed, tickers, trading sessions), both in the table and in the re-exported versioned file. The build creates the table from the CVM registry (five columns) and rewrites the versioned file with what the table has; the snapshot comes from a manual survey that writes only to the file, and almost a third of the companies with a snapshot are not even in the registry of inactive companies. The first full rebuild after the survey rewrote the file with no snapshot, the build gate loaded that file and I-H11/I-B69 failed on every attempt — production stayed pinned to the previous data version for two days. The registry rules the five columns it knows; the survey rules the seven of the snapshot; a row only the survey knows enters whole.I-B73— The Selic target (Central Bank series 432) enters as continuous validity periods: a missing day breaks the period instead of stretching the previous rate over the gap; the fiscal-year average only comes out with the whole year covered; a source window that fails writes nothing; the versioned derived file is never rewritten with fewer days than it already covers; the boot loads it into an empty database; and the database covers every closed year with an indicator. A wrong average that looks right would decide the ROIC band of hundreds of companies.I-B74— The company page reads the indicator series, the multiples, the quarterly series, the restatements and the dividends through the data layer (first slice of the migration) — and what it receives EQUALS a direct database read, value and flag, for 20 companies of different kinds (bank, insurer, non-December fiscal year, withheld multiple). During the migration the HTML of those 20 pages, in Portuguese and English, came out byte-for-byte identical before and after, apart from the time stamp.I-B75— A registered restatement is the one of the aggregation the published figure uses that year (consolidated, or individual when the company does not consolidate), with the aggregation IN THE KEY — never again the individual statement overwriting the consolidated one. The real case, reproduced: Bradesco 2024, whose consolidated total assets matched across the two filings while the individual comparative came empty, and the old key recorded 'total assets from R$ 1.69 trillion to zero'. Measured on 26/09/2026: 9,473 → 8,660 rows, and 25 of the 356 traded companies move from a yellow to a green flag.I-F09— When ingestion receives a REIT monthly report with a higher version than the stored one, every published field that changed is recorded with before, after, both versions and both delivery dates — before the upsert overwrites it. Same version records nothing; a lower version neither overwrites nor records; a higher version with no published change records that nothing changed.I-F10— In a from-scratch build the “before” lives in the PREVIOUS database: every report whose version went up between the two is recorded field by field; with no previous database, or one without the report table, it returns zero and does not raise.I-F11— The record is a versioned derived file: the build loads it BEFORE ingesting, compares with the previous database and only then re-exports, never with fewer lines than the file has; the round trip keeps every field. The page reads on a read-only connection and accepts a database without the table.I-H09— Raw downloads never land inside `_build/`, the directory wiped at the start of every rebuild. This cost hours: the rebuild runs a subprocess with DADOS_DIR pointing at _build, and without ZIPS_DIR declared the naive default put ~3GB of raw files exactly there — every restart erased them and re-downloaded from scratch, so no attempt ever finished. The environment variable fixes it, but depending on one is fragile: deleting it was what created the loop. This test makes the DEFAULT safe, so configuration is a tweak and not a requirement.I-H10— The rebuild declares TMPDIR and SQLITE_TMPDIR outside /tmp. SQLite writes temporary files when creating indexes and sorting large tables, looking for a directory in this order: SQLITE_TMPDIR, TMPDIR, /tmp. Neither of the first two existed, so it fell through to /tmp — capped at 2GB on Render. The build died at the very end every time, right after ~550k REIT prices, during index creation: instance killed, restart, full ingestion again, never finishing. The first suspicion — the raw downloads — was wrong: moving those to disk changed nothing, because they were never the consumer at that stage.I-H11— A database missing the derived tables gains them at BOOT, not through a full rebuild. Covenants and the excluded-companies list depend on nothing the rebuild recomputes: they come from files versioned in the repository. Taking the official route of bumping the data version would cost around twenty-six minutes of reconstruction in a build that has already died of memory exhaustion and, on one occasion, took down an external audit in progress. The test loads into an empty database and checks it loaded, then runs again and checks it did NOT reload, because overwriting what a build placed would be the boot overruling the build's authority.I-H12— A failure loading a derived file becomes a log warning, never an exception at boot. One page missing is bad; the whole site being down is worse, and this product has already been unavailable precisely to the people trying to audit it. The attack forces ingestion to blow up and requires that the service still comes up, by checking that the caller wraps the load in a try/except rather than trusting it.I-P19— A B3 code is 4 positions plus a class digit, not 4 LETTERS plus a digit. The exchange itself trades as B3SA3, with a numeral inside the ticker root. The regex demanded [A-Z]{4} and the catalogue filtered on isalpha(), so B3 was discarded in both places: never ingested from COTAHIST, never present in the catalogue. It sat in the base under BVMF3, retired in 2017, with zero recent volume — anyone typing the real code found nothing, while B3SA3 shows 658 sessions and R$ 353bn traded.I-P20— A truncated B3 catalogue does NOT get to change which ticker represents a company. The endpoint is paginated and fails mid-way often — one measurement delivered 9 of 30 pages — and it used to do so in silence, returning half a catalogue as if it were whole. Deciding the representative ticker from half a source is worse than not deciding: the right code may simply not have arrived.I-P24— When the regulator’s file carries the whole consolidated block as zeros (TIM S.A., 2024 and 2025 annual filings) while the individual statements have figures, the standardised data uses the individual block, annotates the origin of every item and flags the indicator. Before, the zero was read as a value and TIM became a ‘company with no operations’. A single non-zero figure in the consolidated block keeps it; zeros on both sides have nowhere to fall.I-P25— For every company whose primary instrument is a unit with a known basket, the published market cap equals (price ÷ basket) × shares, recomputed here from the tables, in the annual and in the quarterly multiples, with or without the thousands-scale correction. The defect it guards against appeared on the first build with promoted units: the thousands-scale correction recomputed price × shares and dropped the divisor, so Santander came out at R$ 250bn (about R$ 125bn real) while flagged as adjusted, and the quarterly multiples had never divided any unit at all.I-P26— No PUBLISHED multiple has a share count 3x larger or smaller than the last published year before it without a corporate event to explain it. The plausibility band against equity let CSN Mineração through with a R$ 271 billion market cap in 2025 (33 billion in 2024, price falling): P/E 164 and P/B 40 published as clean. A share count does not change 10x without an event; the database holds the events; the cross-check was missing. The year after an IPO, counts already adjusted for events or scale, and any recorded event in the year, the previous or the next are accepted explanations.I-Q01— An ETF’s premium or discount is close ÷ NAV per share − 1 only on a day where both exist. A day with a price and no NAV, or the reverse, produces no row: nothing is interpolated, no ‘last known NAV’. Beyond ±10% the figure carries a flag and never disappears.I-Q02— The daily report filed on day D carries the month up to D. Reprocessing a month writes with the NEWEST document, and an older document never overwrites what a newer one wrote — the administrator’s latest version wins, as a restatement does for financial statements.I-Q03— Result accounts in the COFI chart accumulate over the fund’s fiscal year. The month’s fee expense is the balance of the month minus the previous month’s; a falling balance means a new fiscal year and the balance itself is the expense; without the previous month the figure is cumulative and flagged. The effective fee is expense ÷ average daily net assets × 12, flagged outside 0–5% a year.I-Q04— An ETF exists for the site only when the CVM registers it as an index-fund class, B3 lists it with an acronym and returns its CNPJ, and the ticker traded in the exchange file. A fund listed by B3 without the CVM class is out; one registered without a B3 acronym is out; one matched but without a price has no page. There is no hand-kept ticker list.I-Q05— The ETF page and the /etfs hub open as HTML with no key and no script, with the site frame, anchors only for sections that exist, the premium stated as same-day and the fee stated as EFFECTIVE, the index stated as inferred from the name; JSON through the same URL only with a key or an explicit Accept; an unknown ticker is 404; the count on the hub equals the table.I-Q06— Two sources, one figure: the NAV per share on the last day of the month (daily report) must equal net assets ÷ shares outstanding from the same month’s balance sheet within 1%, fund by fund, in the real base. If they disagree, either the parser read the wrong column or the administrator filed different things in the two documents — in both cases the figure cannot ship clean.I-Q17— When an ETF ranking has nobody in it, the page SAYS why, with the date the series starts — it does not vanish from the screen. NAV comes from FNET's daily filing, one document per fund-month, and the source throttles bursts, so the 12-month return and the effective fee start empty and fill in over time. A block that disappears makes the reader conclude the wrong thing — that this house does not compute it — when the truth is that the data is not there yet. The explanation names the missing data, not a missing method, and no substitute is invented: the headline fee stays out.I-Q22— Each backfill round skips FNET documents whose id is already in the database. Rounds have a time budget and each one re-lists the month; without this check a round re-downloaded what the previous one had stored — the second August 2026 round fetched 89 balance sheets and only 8 were new, so the month never finished while the log looked like progress.I-B37— The revenue-growth denominator is the RESTATED comparative, not the figure as originally filed. Found by reconstructing the published sample against the raw CVM archives, after an external reviewer could audit only a handful of cases because their tooling refuses zip files. The published formula said revenue(year) / revenue(year-1) - 1 without saying WHICH version of the prior year. For one retailer the two readings differ six-fold. The comparative is the right choice for the same reason that governs the rest of the product: whoever opens a year's statement does so the following year, and the comparative shown there is already restated. Mixing the old denominator with the new numerator compares two different accounting vintages. Cases with no comparative fall back to the original figure and are stamped with a flag, so a reader can always tell which base produced the number.I-B38— A financial year whose declared currency scale is contradicted by the following filing does not publish absolute values. Found by reconstructing the audit sample against the raw CVM archives: one company was showing revenue growth of over a hundred thousand percent, because its own filing declared units while reporting thousands. The trap was diagnosing this per financial year: a filing at the wrong scale corrupts BOTH columns it publishes, its own year and the prior-year comparative, so the neighbouring good year looks broken too. The rule therefore requires both columns of the same filing to agree before calling it. And we diagnose without ever rewriting a value: a corrected number would stop matching the accounts it cites, and a number that does not tie to its own source is worse than an absent one because it looks auditable and is not. Ratios still publish, since they divide two accounts from the same vintage and the scale cancels.I-B39— No fallback flag may cover an entire class of companies. Born from a mistake made the same day the rule was written: revenue growth started using the restated comparative, with a stamped fallback for filings that have none, but the lookup matched the item name used by ordinary companies while financial institutions store it under a different name. Every single bank series fell into the fallback and none used the new rule, while the commit claimed banks were covered. The defect is dangerous because it looks tidy: each number carries a flag, and a flag reads as an explanation. Only the proportion gives it away. An exception that applies to everyone in a class is not an exception, it is the rule failing for that class.I-B40— Closing equity equals opening equity plus transactions with owners plus comprehensive income plus internal movements. Born from an audit that could not finish: a reviewer built this bridge for one company and stopped halfway, because dividends and other comprehensive income were exposed nowhere in the API, so they could reach a suspicion but not a conclusion. Their finding turned out to be a false alarm, but the hole that prevented them from confirming it was real, and this bridge is precisely the check that catches an equity error. Run across the whole base, 5,347 of 5,361 financial years close. The fourteen that do not are defects in the source, one company filing every closing balance as zero, and those are suppressed rather than published: an internally inconsistent set is worse than an absent one, because whoever checks it concludes the error is ours.I-B41— An account the regulator's file reports twice with different values is not published. The archive repeats the same account, same financial year, same column, with contradictory amounts and nothing to tell them apart: same statement group, same dates, same description. One company files profit as both one real and zero on the same line. Until now this resolved by accident on both sides: our ingestion kept the last row read, and the reconstruction script we wrote to AUDIT ourselves kept the first. The two disagreed about the same company, and that is the only reason the conflict surfaced. Arbitrary resolution does not announce itself as arbitrary; it only shows when two of your own parts choose differently, and most systems do not have two parts reading the same source by independent paths.I-B42— The scale warning travels on the FACT, not only on the indicator. Found by an external audit: the scale-divergence rule suppressed absolute indicators and multiples, but the underlying facts still shipped clean, so a utility published equity of 1.6 million where the following year's comparative says 1.6 billion. Anyone reading the raw facts or the bulk dump got a number that could be a thousandfold wrong with nothing marking it. Marking rather than suppressing is deliberate here: ratios come from these same facts and remain correct, because the scale factor cancels between two accounts of the same vintage, so suppressing would kill good data to remove bad. The general rule it closes: a warning confined to one layer is not a warning, because whoever consumes the layer below never sees it.I-B43— Every published indicator recomputes from the facts that produced it. Found by adversarial mutation testing: defects were injected into the data to measure how many the suite caught, and doubling a net margin without touching any fact went completely undetected. The product's central promise, that the published formula is the applied formula, had no invariant at all — it was checked by hand whenever someone thought to look. The drift needs no bad faith: recompute the facts and forget the indicators, which happened in this very project.I-B44— A flag the rules require must actually be present. Two mutation survivors: erasing a flag from one indicator and a shell-company warning from another went unnoticed. The existing checks verified a flag was CORRECT when present, never that it was PRESENT when due. Flags carry nearly every judgement in this base, so a vanished one returns the number to the world looking clean, which is the worst way to be wrong because nobody reading it suspects.I-B45— Impossible values are never published: non-positive prices or market caps, or a market cap beyond any plausible order of magnitude. A mutation flipped a price sign and nothing complained. A negative price is not a wrong number but an impossible one, and an impossible value passing means nobody watches that layer. The worst incident on record here was a market capitalisation of 1.67 trillion reais, impossible before it was wrong.I-B46— Return on equity and on assets also recompute from the facts, not just the simple ratios. Round one of the mutation audit found that no indicator was recomputed at all and produced the first recomputation invariant; the detection rate then hit 100%, which almost always means the attack set is too easy rather than the system being safe. Round three swapped one company's return on equity for another's: perfect shape, plausible value, normal range, only the owner wrong, and it passed. These two had been left out because they average two financial years, which is more work to reconstruct. More work is not a reason, it is exactly where defects hide, because whoever writes the test also picks the easy path.I-B47— The version stamped in the database is the version of the code that built it. A mutation replaced the stamp with an invented label and nothing complained. That stamp governs the publication gate and every claim that a user knows which state they audited. A wrong stamp is worse than a missing one, because whoever cites the version ends up citing one that never existed.I-B48— A recorded restatement must actually show a divergence. The existing check asked the opposite question, whether every real divergence was recorded; nobody asked whether every record corresponded to a divergence. The asymmetry is easy to miss and the effect is bad in both directions, since the restatement count is a headline number on the home page and an empty row inflates a transparency argument with nothing.I-B49— There is no alternative route into the batch of published facts. This invariant fixes a pattern, not a case: in one week the same thing happened five times, where a rule was applied on one path through the code and forgotten on another, and every one of them was caught by an invariant rather than by review. The cause was structural, since more than one path led into the batch and whoever added an item by a new path had to remember to reapply each rule by hand. A rule applied on three of four paths is not worth 75%, it is worth zero, because the defect picks precisely the forgotten path. The fix is not remembering better, it is making forgetting impossible, and the test is structural because a behavioural one would only fail after the next incident.I-B50— The quarterly price-to-earnings ratio equals market cap divided by trailing twelve-month profit. A mutation shifted it by 40% and nothing complained: the annual indicators were recomputed by an earlier invariant, the quarterly series had no equivalent. Same pattern that has now appeared six times in this project, a rule applied to one layer and missing from the neighbouring one.I-B51— Interest on own capital does not vanish from shareholder payouts. A mutation deleted every such entry and no invariant complained, which would have halved the dividend yield of every company that pays it, silently. This is already a declared case in the public challenge, since that instrument is booked as a financial expense rather than a distribution, so anyone summing only the dividend line understates real remuneration. There was a challenge case and no invariant: the challenge proves we can defend that case, the invariant stops it breaking unnoticed.I-B52— The sum of the quarters cannot contradict the financial year. The mutation that prompted this was the least interesting part: while calibrating the threshold, the worst cases came in at 740 times and turned out to be financial years already flagged for a currency-scale problem. The comparison was detecting the same defect through an independent path, and that became a second scale detector which reaches what the first cannot — the last filing of a series has no following comparative, so it was undiagnosable by construction. The flagged population went from 27 to 45.I-B53— An income statement filed without its cost line does not publish a clean gross margin. The investigation started in the wrong place: mutation testing flagged company-years where the quarters summed to two or three times the full year, and it was recorded as an inflated quarterly series of unknown cause. The opposite was true. The quarterly filings were right and the annual one was malformed, reporting zero cost with gross profit equal to revenue, and the annual revenue figure matched the estimated annual GROSS PROFIT rather than revenue. A hundred and seven company-years publish a gross margin of exactly 100%, which does not exist in an operating company, and the number ships clean, so anyone sorting the market by gross margin gets them at the top.I-B54— When a company's quarterly and annual filings disagree, the data says so. Of the twenty-one contradictions found, only eight had the missing cost line; the rest have real costs and still show the quarters summing to twice the year, most plausibly because the consolidation perimeter changed mid-year, which is legitimate accounting and nobody's defect. The mark is therefore descriptive rather than accusatory: it states that the company's two publications disagree without asserting which is right. The opposite temptation nearly won, declaring the quarterly wrong because the annual is the number we publish.I-N100— When a re-filing was captured, the page shows month, field, before, after, difference and versions; when not, it says plainly that the CVM publishes only the version in force and that there is no “before” figure — never a silent empty table.I-N101— In /comparar/A/B each indicator comes from the comparison data in the house format (percent ×100 with one decimal, multiple with two and “x”, F-Score “/9”), FCF yield in %, the multiple year is one when it matches and “A × B” when not; the card carries name and sector; an account that does not exist for banks says whose it is; the JSON-LD lists both in positions 1 and 2.I-N102— /vigia/{T}: for a company or a fund, the ticker’s events — the SAME as the watcher’s, filtered by ticker — with a type filter, the follow form (companies only) and links to the ticker page, the general watcher and the JSON; a ticker with no event is noindex,follow; unknown is 404; not in the sitemap. The company “follow” block and the fund card link here.I-N103— On the real base, the company that publishes only the individual statements and has a restatement with one side zeroed in the CVM file: the restatements page says “individual — company does not consolidate” on each row and “no value in the file” where the side came zeroed, never R$ 0; the JSON at the same URL and /empresas/{T}/reapresentacoes carry the aggregation and the empty side, row for row as the table; the company page does not print 0.0 either. A company that consolidates stays con. PT and EN; no banned term.I-N20— Any page using the fact-page frame is registered in the panel's explicit list. The panel returns the thirty most-visited pages and its last row had twenty-eight visits, so a fact page born with five simply vanishes from it — and 'did not appear' reads as 'nobody visited' to whoever is looking. The fix was listing each one explicitly, which only works while the list stays complete; whoever adds the twelfth page and forgets to register it finds out here rather than a month later, staring at a zero that was never a zero.I-N21— Every path in the panel's fact-page list actually answers. The counterpart of the invariant above: without it the list can accumulate the path of a page that was removed, and the panel would publish an eternally zeroed row — a zero that does not mean 'nobody visited', it means 'this no longer exists'.I-N22— A company page answers 200 WITHOUT a key and is in the sitemap. That is the whole point of the page: the site was invisible three ways at once — the sitemap held 568 URLs of which 503 were audit samples and none was a company, a search for a well-known company's ROIC returned seven competitors and not us, and an AI asked for a bank's ROE hit a 401 and answered with another site. If anyone puts a gate here all three holes come back silently, because the page still exists and nobody can reach it.I-N23— The index on the stocks hub and the sitemap cover the same companies. Without it the index can list a code that does not open — a broken link on a page built to be indexed — or the sitemap can promise a URL that does not exist, and a crawler that hits a 404 arriving from the sitemap distrusts the whole site.I-N24— A withheld figure shows its reason on the company page instead of a blank. On a shop-window page the temptation to hide is greater, not smaller: a clean dash looks better than a dash with a caveat. The house rule holds here too, and this test pins it against real flagged indicators.I-N25— A fund page answers 200 in HTML WITHOUT a key, on the SAME URL that serves the JSON. The format comes from the Accept header: a browser reads, an agent consumes. Before this the hub linked each of its funds to a gated JSON, so a person who clicked a name got a 401 — explained, but shut — and to a search engine or an AI the fund did not exist. If anyone flips the order, gating the HTML or serving JSON to browsers, those links go back to being a closed door and the funds go invisible again.I-N26— The index on the funds hub and the sitemap cover the same funds. A page no other page links to is the last one a crawler visits, and a sitemap promising a URL that does not exist makes a crawler distrust the whole site, so the three lists — index, hub, sitemap — have to be one and the same.I-N27— A price-to-book with a caveat shows the caveat on the fund page instead of a clean number. A P/BV flagged out-of-range, shown bare, reads as a discount that is not there — and on a shop-window page the pull to tidy it away is strongest. The test picks a fund whose latest P/BV carries a flag and demands that flag in the HTML.I-N28— Without a key, a fund page returns the PAGE under any Accept header. The fourth time in this family, and this one was self-inflicted hours after the page was written: the route first negotiated on an explicit text/html, so Googlebot got the page while */* — the default of curl, GPTBot and ClaudeBot — got a 401. Search Console refused the indexing request for that URL and the reason was exactly this. The rule is the KEY, not the Accept: whoever sends a key wants the feed, and whoever does not has no access to the JSON anyway, so a 401 only hides public content.I-N29— The page and the JSON of an audit sample carry the same cases. Serving one sample in two formats risks them drifting apart, and then a seed would have two versions — which is exactly what TR24 prevents between the two URLs, while nothing prevented it between the two formats of one URL. An earlier draft of this invariant demanded the page be served to */* by analogy with the fund pages; the battery refused it, correctly, because there */* got a 401 with no content while here it gets the whole artefact, and that JSON is the citable proof the published protocol tells auditors to request.I-N30— No URL in the sitemap answers with a noindex tag. The two are opposite instructions, and for months 500 audit-sample URLs carried both: they were listed in the sitemap on the sound reasoning that a tool which only opens indexed URLs stalls otherwise, while the page itself said noindex — so it could never be indexed, the tool stalled anyway, and the entries took up 36% of a sitemap on a site where Google already reported forty pages detected-but-not-indexed. Each half was defensible alone and nothing looked at them together. The fix was to drop the samples from the sitemap rather than drop the noindex: the seed hub is indexable, sits in the sitemap and links all five hundred, so the tool reaches the hub and follows a link, while five hundred generated near-duplicates stay out of the index where they would compete with the real pages.I-N31— The screener table is rendered by the server, with no JavaScript. Building the table in the browser is the natural choice and would hand a crawler or an AI an empty page — and the 820 public pages opened in September are worth something precisely because the content is in the HTML. So sorting is a link and filtering is a GET form: the crawler walks exactly what a person sees, and every slice is an address.I-N32— Filters stack rather than replace, and each one can be removed on its own. A screener whose second filter silently replaces the first looks like it works — it returns rows and raises no error — and does not do the job; only counting catches it. Each active filter also carries a link that drops just that one and keeps the rest.I-N33— A row with no clean figure never leads the ranking. The classic sortable-table bug: a missing value becomes zero and the company we know nothing about shows up as the cheapest on the market. Flagged values are the same story — every negative P/E in the base carries a flag, so without this rule the top of 'cheapest' would be a queue of loss-makers. Those rows stay in the table with their figure visible and marked; they simply do not lead.I-N34— Every company page links to the other companies in its CVM sector, and every fund page to the other funds in its segment — all of them, none of itself. The 820 pages opened in September were born as islands, reachable only from the hub or the sitemap, and a page no other page cites is the last one a crawler visits; Search Console showed forty of them detected but not indexed. The grouping is the regulator's own classification, not ours.I-N35— Company and fund pages carry structured data (schema.org JSON-LD) that parses, names the right type — Corporation or InvestmentFund — and matches the page's ticker and CNPJ, with a breadcrumb trail. It is the format search engines and AI systems use to extract facts without reading prose. It is labelled JSON, not JavaScript: the screener stays script-free and this test does not touch it.I-N36— The about page names the person who builds the site, in both languages, with the credentials that speak to this product. It used to explain the operating company and say nothing about who makes it — and a site with no visible owner is the first thing a search engine or an AI discounts. A reference has a name.I-N37— The what-changed page opens under any Accept header, is in the sitemap and in the panel's explicit list, and declares both its window and how far each dataset reaches. It exists because after 820 open pages and a screener the site was an excellent dictionary with no reason to come back tomorrow; this is the recurring reason, built entirely from our own data. Each source lags differently, so the page shows the lag instead of hiding it.I-N38— Every figure on the what-changed page equals the database count for the same window, and an empty section says so in words. A page that summarises the last thirty days is the easiest place for a hand-written number to age silently; here the headline of each section is compared with the query behind it, and a zero is rendered as an explicit sentence rather than an absent block.I-N39— The what-changed page serves JSON only to a client that asks for application/json explicitly, and keeps serving the page to everything else. The format may follow the Accept header; access never may. That distinction cost this codebase four repetitions: on the fund pages the KEY decides, because there is a commercial gate there and answering 401 to */* hid public content from GPTBot, ClaudeBot and curl. Here there is no gate at all, so the header picks packaging and nothing else, and a missing or */* Accept still gets HTML. The totals in the JSON are compared against the database behind the page, so the two cannot drift apart.I-N40— The methodology index answers in JSON to whoever asks, carrying the title of each page. The MCP connector publishes a methodology() tool that called this index and parsed the result as JSON — and the result was always HTML, so any AI calling that tool got a parsing error instead of an answer. It survived because reading one page always worked; only the index was dead, and nothing on our side exercised that path. The lesson is not that JSON was missing: a published contract with no invariant exercising it is a promise, and the tool sat in the list, fully described, with no one on our side ever calling it.I-N41— A company page carries the quarterly series, every figure equal to the database, with no fourth quarter, in both languages. The annual series only exists once the fiscal year closes, so a company that filed its second quarter in July still showed its last full year, and all 455 pages stayed identical for twelve months. The quarterly data had always been in the database; it had never reached the person reading. A page that changes every quarter is one a search engine and an AI have reason to revisit; one that changes once a year is not. The fourth quarter stays out here for the same reason it stays out of the API: the interim filing does not publish it on its own, and deriving it would put a figure we computed in the same table as the figures the company reported.I-N42— The what-we-got-wrong page is generated from the invariants' own docstrings, not from a hand-kept list, and every dated defect in the code appears on it. Each invariant here was born from a real defect and keeps the story, with its date, in its docstring: sixty-five of them, written over months, none of which had ever left the code. What this test holds is not the page but the impossibility of curating it — a hand-written list would allow choosing what to show, and the temptation would be to omit the ugliest defect exactly when it is the most instructive. The page is a function of the docstrings: if a dated invariant exists in the code, it must be on the page.I-N43— An English page is either in English, or it says why part of it is not. Sweeping all seventy-five pages for words that exist only in Portuguese turned up eight, and seven were false positives of the most instructive kind: a company's legal name and the regulator's own account labels. Proper nouns and primary-source labels are not translated, because translating them would invent data the source does not have. The eighth was real: the audit-history page translated its frame and left seventy findings in Portuguese with no explanation. Translating those would be worse than leaving them — a rewritten audit record stops being a record, and the difference between what the auditor said and what we say they said is the whole point of keeping the table. So the page declares it, which is what this codebase does with every limitation.I-N44— Search finds an asset by ticker, by name with or without accents, and by tax ID; an exact ticker goes straight to the asset; an empty term returns nothing. The site had 824 asset pages and no search box at all: whoever arrived wanting one company had to open a list of 356 and hunt with their eyes, or guess the URL. That cost breaks no test and shows in no log — it shows on the first visit, which is the only one most people make. Accents cannot hide a result, because nobody searching knows how the regulator spelled the name, and requiring them to know turns search into a quiz. An empty term returns empty rather than the whole base, because 824 rows answering a question nobody asked look like a result and are not.I-N45— A comparison of two companies uses the most recent year in which BOTH have the figure and says which year that is, declares what does not exist for that kind of company, and never names a winner. Comparing one company's 2025 against another's 2023 renders cleanly and compares two different economies. Naming the better one would be a recommendation, which this site does not give, and a recommendation disguised as arithmetic, which is the worst kind. What this page has that others do not is trust side by side: each figure's caveat, and how many times each company restated its filings — a return on equity of 20% from a company that restated 113 rows is not the same figure as one that never restated.I-N46— The company page carries charts as SVG generated on the server — no script — one per indicator that applies; a year without a value is a gap in the line, a point with a caveat is drawn hollow, and the card’s last value is the database’s. The choice was deliberate: an AI reads the page as text and would receive a hole where an interactive chart sits; a phone opens it in half a second with no program to run; and a browser error is invisible from our side. What a chart must never do is invent continuity — joining two years across a year with no value draws a number that does not exist, the same defect as interpolation, only visual. And the caveat has to travel to the drawing: a point flagged in the table and clean in the chart is the table saying one thing and the figure another.I-N47— The company and fund pages show two prices with two owners, and say so. Ours is the closing price from the exchange's official daily file, entered every night, stored, served by the API, the connector and the dump, and reproducible by an audit. The other is a 15-minute delayed quote drawn by the TradingView widget: it arrives through their script, we never receive, store or serve it, and the page states that it never enters our API. This is the one exception to the no-script rule, decided by the owner after pricing the alternative (a delayed-data licence from the exchange plus a feed, display-only by contract). The widget is free only with attribution, and removing the credit gets a site banned, so the credit is our own text next to it and this invariant requires it. The page stays readable as text, with the closing price and its date, and no other script is allowed in.I-N48— The home page opens with the working shown for a real company: its return on invested capital recomputed from the raw filing lines, each line naming the regulator's account it came from, and the result is the SAME figure the base publishes. If the sum does not close, the section is not shown. The whole page carries a single script, the structured-data block; the live counts match; search works without JavaScript. The old home spoke to programmers, with a script playground and a headline about scraping, and 95% of visitors left without a click while two thousand people a month landed directly on company pages from AI assistants without ever seeing it. The new one is generated from the database, and instead of SAYING that every figure traces to the regulator's account it SHOWS it. A sum that failed to close there would be the opposite of what the site sells, so it is checked against the published indicator before it reaches the page.I-N49— The company page opens with what the company does and where it is (the registration-form profile, with its source declared), carries an anchor bar in which EVERY entry points to a section that exists, and a server-side SVG price chart whose last point is the last close in the base. The owner compared the site with a competitor and was right about the company page, which is where people land. What they do with 81 scripts and 747 KB can be done with HTML and SVG: the profile in cards, the sticky section bar and the price series. The chart is the close in the BASE, monthly, not split-adjusted and saying so, not a third-party quote: the same figure the API serves.I-N50— The fund page has what the company page has: a profile in cards with its source declared, an anchor bar in which every entry points to a section that exists, three server-side SVG charts (price, point-in-time P/BV, distribution per share) whose last point matches the base, and a trimmed P/BV series (24 reports) with the full one a link away. After the profile, the anchors and the chart entered the company page, the fund page looked like an earlier version of the product: no anchors, no chart, and a P/BV table with one report per row since 2010 on the main screen. First item of the post-review roadmap. What changed is the presentation; the figures are the same, and the test checks the last P/BV and the last distribution on the chart against the base.I-N51— The company page opens a “Size and valuation” block: market value (today's close times the latest public share count), enterprise value, earnings and book value per share, 12-month dividend yield, payout and share count, each card with its year, and the point-in-time multiples now carry the year in their label. The market value follows the SAME rules as the multiple (units, counts in thousands, plausibility band) and the yield is the one the dividends route serves. “P/E 33.1” did not say when, and what every competitor shows at the top was missing although all of it was already in the base. The risk was recomputing market value without the three rules the multiple learned the hard way: the largest bank came out at R$ 458 million (a count in thousands), a unit would come out N times inflated, and a garbage count from the registration form would become trillions. The test pins the three with real and synthetic cases.I-N52— Next to the close, the company and fund pages show the 1-month, 12-month and year-to-date return and the 52-week range, computed from the closing series in the base. A window that crosses a stock split or reverse split gets NO figure: a dash with the reason. A series shorter than 52 weeks says “since X” instead of “52 weeks”. The series is not split-adjusted (the exchange's file is not, and it is what the API serves): a 12-month return across a 10-for-1 split would read as −90% and look like a tragedy that never happened, so every window is checked before it becomes a number.I-N53— Next to each indicator and multiple, the company page shows the median of the OTHER companies in the same regulator sector, each in its latest year, with how many they are. Only figures without a caveat enter; the company itself stays out; below three peers there is no median. The page used to answer the number and not the question “is 32% return on equity a lot?”, while the fund page already said what the peers' median was. Median rather than mean because one company in judicial recovery with a return of minus two hundred percent drags the mean and not the median; clean figures only because a caveat (scale, capital without an average) must not become a reference for the others.I-N54— Below the ratios, the company page shows the history in REAIS — revenue, controlling net income, EBITDA, net debt, cash and investments, equity — year by year with the unit in the label. The values are the base's own (standardised lines and the methodology's derived figures), cash exists only in a year where the cash account was filed, and banks and insurers get “does not apply” where the account does not exist instead of a dash. A ratio alone hides size: forty billion in revenue and forty million have the same margin. Cash adds cash and financial investments because that is what the return-on-capital formula deducts; investments without a cash line do not become “cash and investments”, since a partial sum would read as a whole one.I-N55— The dividend calendar (on the today page, the home and the asset pages) shows only what has a date ahead — fund payments and ex-dates, stock ex-dates — never an impossible date (the exchange writes 9999-12-31 for “to be defined”), and SAYS how far the base can see (the date of the newest announcement it holds). When an asset has nothing scheduled the page says so with that date instead of staying silent. Fund distributions arrive with a payment date and the base held 138 future payments; stock dividends arrive with an ex-date only, and since the exchange source is read on Sunday the newest announcement was 18 days old with no future ex-date. Promising “the next 30 days” on Sunday's data would sell delay as freshness, so the page carries the cut-off date with it.I-N56— The future ex-date of a stock comes from the dividend report the company files with the regulator (the IPE category Relatorio Proventos): the form is read (approval date, last day with rights, ISIN, amount, payment date), enters without a type because the form does not separate dividends from interest on equity and the page says so, does not repeat what the exchange already published (same company, share class, ex-date and amount), keeps only the share class of the main ticker, and every item carries the link to the document. Free-format filings stay recorded as unparsed, with the link. HTML served in place of the PDF is not stored; a new version of a document replaces the old one; what was already read is not downloaded again. The exchange only publishes a stock dividend after its ex-date: a sweep of the whole universe (356 issuers) on 2026-09-11 found zero future ex-dates, while the regulator filing arrives a median of two days before, with a payment date the exchange does not carry.I-N57— The last pages that still ran JavaScript now come ready from the server: the sector race is an SVG drawn in Python with window and scale as links, the foreign-flow chart is an SVG drawn in Python, the transparency page filters by family through a link the server answers, and the methodology title is written into the page instead of rewritten in the browser. Apart from the JSON-LD block and the quote widget the owner allowed, no public page carries a script — an AI agent and a phone read the same page a browser does.I-N58— The company page lists every cash dividend of the main share class as a table: type, record date, ex-date, payment date and amount per share. The ex-date is the first trading session after the record date taken from our own price history, never a guessed business day. The payment date comes from the exchange company file, and when that file has none, from the filing to the regulator, marked as such. The year filter is a link the server answers, the default shows the latest 24 rows, and a summary sentence states the latest dividend, the trailing yield and the 12-month count and sum. The exchange file also fills payment dates on existing rows and adds dividends the list endpoint had not published; a date of 9999 is not a date.I-N59— The price chart on the company and fund pages takes its window from a link (one month, three, six, year to date, one year, five years, everything) and draws the closing price from the base at the granularity that fits: daily up to a year, weekly for five years, monthly for the whole history. The label states the return of the window, first close to last, annualized only when the window is clearly longer than a year. A split inside the window is not averaged away: the series starts after it and the label says so. No script is involved; every window is a page of its own.I-N60— The table in reais gains two columns from the quarterly filing: the latest quarter on its own and the trailing twelve months, built by the same identity as the TTM price-to-earnings (previous year minus its quarters to date plus this year to date); a missing piece leaves the cell empty, never interpolated. Balance lines (cash, equity) take the quarter-end figure; lines without a quarterly series (EBITDA, net debt) stay blank. Every multiple and valuation card carries a one-line definition, and the valuation block adds price to operating cash flow and price to free cash flow, outside banks and insurers, as with enterprise value.I-N61— After the coherence pass on the company page (the price chart sits with the quote, valuation shows its two moments in one section, the sparklines sit with the indicators they summarize, nine anchors instead of eleven), the page gains three things seen at a competitor and built only from what the base already held: indicators grouped by theme (valuation, debt and liquidity, efficiency, profitability, growth), each with a one-line definition and a mark when today market value is divided by an account of the fiscal year, with no multiple over a non-positive denominator and nothing that does not apply to banks and insurers; the three statements in one table (income, balance sheet, cash flow) with derived lines named for what they add up; and the corporate actions with their multiplier and whether they matched the observed price move.I-N62— From a second competitor, and again only from what the base already held: a return table by window (one month to ten years) with the price alone and with dividends reinvested at the close of the ex-date, the starting price adjusted by the corporate actions checked against the price and a jump they do not explain leaving the window blank; yield per year over the close of the year's last session and the five-year average; the months with a record date in the last five years, counted rather than projected; the average daily traded value over the last sixty-three sessions on the company card; and a checklist of nine objective facts with the threshold written in each line, banks and insurers exempt from the ones that do not apply, described as facts and not as advice.I-N63— The today block of the company page carries the twelve-month snapshot other sites call current: price to earnings over the trailing twelve months built by identity from the quarterly filings, price to book on the latest quarterly balance, return on equity and net margin over the same twelve months, each naming the quarter, with a note saying which cards use the last four quarterly filings and which use the annual one. A non-positive denominator yields no number. The ROIC card states the whole formula, and the methodology page explains why our ROIC differs from other sites (window, tax rate, invested capital on the average with goodwill), after an external audit found the largest gap there.I-N64— The company card shows the company logo, and the way it is done is the defensible one: the source is the website the company itself declares in its registration form, never an aggregator; every file carries its origin (URL, source, date) in a versioned manifest; the image is a square 64-pixel PNG of at most forty kilobytes served from our own domain, so no third party is called when a page is viewed; the alt and title say where it came from; without a file there is no image and no generic icon in its place; and a company listed in the withdrawal file disappears at the next build.I-N65— The return table gains a real line: the reinvested return divided by the inflation accumulated in the window, from the monthly IPCA the central bank publishes (series 433), compounded month by month with the edge months pro rata by days. A month missing inside the window leaves the cell blank, never interpolated; a missing latest month, which is only published around the tenth of the following month, stops the count there and the label says up to which month it went. The series is read in the build and every night, and a build that cannot reach the source inherits what the previous base had.I-N66— The company page gains shareholders and free float from the reference form the company files with the regulator, in its most recent version: only direct holdings, with the form's own Others line as the market and the capital distribution as the free float, and the page states the filing date and version; the chain of control is not reproduced and no taxpayer id is stored. Tag along comes neither from the form nor from the exchange, so what is published is the minimum guaranteed by the listing segment or by law, labelled as a minimum. Revenue by segment does not exist in structured public data and is therefore absent.I-N67— The price chart and the one-month, twelve-month and year-to-date line adjust the series by the corporate actions the base checked against the observed price drop, instead of cutting the series short or leaving the window blank. The point of each event carries a dashed mark with its factor, the label says how many events it adjusted and stops claiming the prices are unadjusted when they are. An event that was not checked adjusts nothing: a jump nobody explained still cuts the series, with the reason. The last close shown is always the one actually traded.I-N68— An old trading code redirects with a permanent redirect to the current one and the destination page says where the visitor came from, instead of silently answering a different question. A live share class of the same company never redirects. A mistyped code lands on a not-found page that suggests the closest codes, and a code that resembles nothing gets no invented suggestion.I-N69— The calendar shows the future payment date of a stock dividend, which the exchange company file carries and which until now appeared only in the company table, the way fund distributions already did. The filing to the regulator still covers only what the exchange has not published: the same dividend never appears twice under two sources, and a payment of a share class other than the main ticker's never appears at all.I-N70— Every public page wears the same frame and the same palette. Methodology, comparison, guides and the connector page were born with a header and footer of their own, without search or language, and the frame is now swapped at serving time for the site's. No page may declare a fixed colour outside the token block, because a hard-coded light background turns into unreadable text under the reader's dark system theme, and the owner decided against a theme button, so the system theme is the only one there is. A variable used without a definition, or pointing at itself, is a defect too. The categorical palette of the sector chart is the declared exception: it is colour of data, not colour of frame.I-N71— Three loose ends closed. Next to the sector median comes the rank in the sector, computed from the very values the median used, with the direction declared (high return on equity is good, a low price-to-earnings is good) and no rank invented for an indicator with no obvious direction. On the fund page the median of the segment peers leaves the loose paragraph and joins the price-to-book and yield cards, as it already did for companies. And each line of the table in reais gains the shape of its series as a tiny line, breaking where a year is missing and drawing the zero line when the series changes sign.I-N72— The home page opens with the market as the base sees it: the biggest gains and falls of the last session, the highest trailing twelve-month yields and the sectors by twelve-month return of our own total-return index. Three rules keep a data site from turning into a tip sheet: only stocks that actually traded that session, because below that a gain is the noise of a parked stock; any stock with a corporate action that day is left out, so a split never shows up as a fifty per cent fall; and the section states the date, the criterion and that it is descriptive, with no adjective anywhere.I-N73— What the company filed with the securities regulator outside its financial statements — material facts, notices to shareholders, minutes, market announcements — becomes a list on its page, newest filing first, each with the link to the document at the regulator. It is an index, not content: we do not download, summarise or classify the document, because our own summary of a material fact would be the first interpretation this base ever published. The subject line is the company’s own free text and goes as it came. The source is the same public filing index we already download every night for dividends, one extra reading of the same file, and if it does not answer the previous list stays standing.I-N74— Five things that only show up when you read the whole site at once, and each of them chips at the same impression — that one base sits behind the pages. The home announced seventeen years of filings while the Petrobras page, on the same visit, said sixteen; the extra year came from six companies whose financial year falls outside the calendar. Closed financial years are what count now. The English home showed the dividend type exactly as the source writes it, in Portuguese capitals, and charged in reais while the English subscription page charges in dollars — one plan with two prices in one language. The sector line above the company name stayed in Portuguese on the English page, with the translation two lines away. And the quarterly table printed figures with no unit at all. The version stamp now lives in the footer, once per page.I-N75— The /para-quem page is the summary an AI assistant can quote: who the service is for, who it is NOT for, what it costs and how to start, in Portuguese and English, with the FAQ as schema.org/FAQPage JSON-LD. What it states must be true NOW: the connector's tool count comes from the MCP server, the company/FII/ETF counts from the same indexes that feed the sitemap, the price from llms.txt. The 'when not to use' section names at least four limits, each also present on the limits pages — no limit invented to look honest. Every FAQ question in the JSON-LD appears as text on the page.I-N76— The FII and ETF hubs serve JSON at the same URL. The ETF ranking is open in both formats; the FII ranking follows the key rule of the internal MCP tool: with a key or an explicit Accept: application/json it returns the tool's dictionary, and without a valid key it returns a 401 in JSON with 'como_resolver', never the HTML page. The public connector (dados-b3-mcp) reads exactly these two routes, so this test also guards it against silently receiving HTML.I-N82— Earnings per share, book value per share and the share count come out in the API, and they are the SAME figures the company page shows — one computation, not two. A reader asked for it: he builds a Graham fair-value spreadsheet pulling book value and earnings per share 'from the base', but neither was in the JSON, and he could not derive them either, since the share count is absent from /fatos too. The block carries the page's three guards (units, thousands scale, a plausibility band against equity) and declares that it belongs to a different time than the annual series: today's snapshot, not point-in-time — that series stays what backtests use.I-N83— A testimonial meets the same demands a number does, adapted to what it is: somebody else's words. It has an owner and dates (who said it, since when they use the API, and when they gave permission — no anonymous 'a user says' here); it is verbatim, in the order it was written, with cuts marked; it is labelled as opinion rather than a figure of ours; and it sits OUTSIDE the proof strip, where every item links to the page that backs it. A translation declares itself a translation, because the original is in Portuguese.I-N84— The page about companies that left the exchange measures the hole without pretending to fix it, and publishes no inferred ticker. Every figure on it is counted from the database as it is served — none is typed into the prose. A trading code appears only where the company itself declared it in its registration form; matching by name was measured and errs (one in 28 checked), and a wrong ticker that looks right ruins a backtest silently. And the page states that recovering a fraction does not remove survivorship bias, pointing at the limits page instead of claiming a reconstruction.I-N85— The screener's `as_of` filters by what was PUBLIC on that date, not by the financial year. About 100 days pass between a year ending and its statement existing at the regulator, so asking for 'year 2018' returns an answer nobody had in 2018; `as_of` asks the question research needs — what could have been chosen on that day. Every row declares the date its statement became available, none uses a statement filed after the date asked, and the response states what `as_of` does NOT fix: the universe is still today's active companies, with the size of that hole measured on the page about the ones that left.I-N86— The portfolio-in-a-link page computes value, weight, return and dividends to receive, and hides nothing it could not compute. An unknown ticker is listed as left out instead of silently changing everyone else's weight; the database keeps one price series per company (the main share class), so a holder of the other class gets no borrowed price; every dividend shows its record date, since only holders on that date receive it. Nothing is stored — the address is the portfolio — and there is no recommendation.I-N87— `screener?...&recibo=1` freezes the answer at a permanent address; the same question on the same data version yields the SAME receipt; the published sha256 matches the stored result following the recipe in `como_conferir`; and the receipt stays the same after the database changes, while its page says the data moved on. A screener list is only valid until the next update — a receipt that changes with the data, whose hash does not match, or that duplicates on every request is a cache, not a receipt.I-N88— Every company data response (API and connector) carries a ready-to-copy `citacao` field: source page, data version and the date of the company's last filing received by the regulator. An AI that reads a number without it writes 'according to public data'; with it, it writes the source. There is also a stable address per indicator (/empresas/{ticker}/indicadores/{name}) with the series, the company's own range, the sector median, the universe distribution and the methodology link.I-N89— /empresas/{ticker}/veredito answers without a key: six numbers in three questions (does it make money? is it indebted? is it expensive relative to its sector and its own history?), each with a numeric band and comparisons to the sector median, the company's own range and the whole universe, plus the confidence flag about the SNAPSHOT itself (restatement, staleness, flagged values) with its reasons, a top sentence of at most 25 words, and the ready-to-copy citation. No judgement adjective, no recommendation; the same comes through the AI connector.I-N90— The company page opens with the verdict — the same top sentence, flag and six texts as the JSON — anchored, with a 'how we compute it' link on every number and no forbidden term. Page and JSON cannot diverge.I-N91— /small-caps lists ONLY the companies inside the configured liquidity window (floor ≤ average < ceiling), between 30 and 120 of them, with none inside the window left out; sorts by each of the six numbers via links, sending companies without the number to the end; filters by sector; and states on the page that the verdict exists for every company, with the path. Listed in the sitemap, llms.txt and the home page.I-N92— /acoes/{T}/reapresentacoes answers in its H1 'Has {name} ever restated its financial statements? In which years?', with one row per restated account (the same count as the data layer) and the CVM file of the year that revealed each one, in Portuguese and English; an unknown ticker is a 404; the page is in the sitemap and linked from the company page when there is a restatement.I-N93— Every company page carries a FAQPage with the five questions of the AI-citation script, and every NUMBER in every answer appears in the page's visible HTML (for the restatement question, in that company's restatements page). No FAQ text, Portuguese or English, carries a forbidden term; and two companies get different answers — it is data, not fixed text.I-N94— Every multiple card on the company page and the point-in-time note state 'price of DD/MM/YYYY', and the multiples JSON carries the same text in preco_de, matched to data_preco. A multiple without its date gets quoted as if it were today's.I-N95— The company pageis generated from the data — 'WEGE3: ROE 32.2%, P/E 33.1 (2025)' — with the ROE of the last year without a caveat and the P/E of the last filing, each in the page's own number format; the meta description carries the year's figures, not the corporate purpose. With no clean figure, the generic sentence. I-N96— Generated comparisons answer noindex,follow and are not in the sitemap; a company's restatement page is in the sitemap only when the company restated — otherwise it is noindex,follow. Company, REIT, ETF, fact and guide pages stay indexable, and the FAQPage stays on company pages only.I-N97— Every company page in the sitemap carries lastmod = the receipt date of its latest filing at the CVM; restatement pages the filing that revealed the latest one; REITs and ETFs their latest report. Text pages carry no lastmod, and no date is in the future or the build date.I-N98— /acoes lists every company with a page, by sector, each with ticker, name and the ROE of the last year without a caveat (the same figure as its page; a dash when there is none); the footer of every page links /acoes, /fiis and /etfs.I-N99— /fiis/{T}/reapresentacoes: the H1 is the question, the first line answers it, and the table lists every month with version above 1 — the same as the database — with the delivery date and the CVM file; links to FNET and to the fund page, which links back. Without any re-filing the page is noindex,follow; the sitemap has only funds that re-filed, with lastmod = the latest delivery of a version > 1.MCP10— The tool list published in llms.txt is generated from the connector's own catalogue, not kept as a parallel list by hand. The hand-written one was already wrong: it announced ten tools while the connector exposed fifteen, so a third of the product was invisible to any AI reading the file that exists precisely to introduce it. Two lists far apart age in silence; this one is derived, and a new tool that does not reach the front door fails the build.MCP11— The connector's `screener` tool accepts `as_of`, like the API. The parameter existed in the API and its menu, but an AI using the connector only sees what the tool declares: without `as_of` in the signature, the question 'what could I have picked on that day' was still answered with the statement of the financial year, which nobody had on that date. The connector answer carries the same guarantees: no row uses a statement received after the date, and the survivorship caveat is stated.MED13— The usage record cannot reconstruct who asked for what. Knowing that a given endpoint family was called forty times guides a decision; knowing WHO asked about WHICH company guides nothing worth the cost of storing it. The test attacks both ends — the raw key and the ticker.MED14— The panel states SINCE WHEN it has been measuring key usage. Without that date, a zero in the first weeks reads as 'nobody used it' when it means 'we were not yet measuring' — the same error nearly made with the fact pages, and which its own invariant exists to prevent from the other side.MED15— Every authenticated call goes through the usage counter, and the counter exists. The strategy became 'make the people already using it dependent on the product, then monetise' — which requires knowing whether anyone comes BACK, and that was exactly the question with no instrument: the counter lived in an in-memory dictionary, wiped on every restart. We knew sixteen keys had been created and nothing about what happened next. Third time the same lesson appears: the channel we bet most on was the only one without an instrument.MED16— Tasting — the denominator of the tasting-to-key conversion — is counted in the middleware only for a DATA route of an open ticker, without a key, that succeeded and was requested by a person; it stores path family and day, never the ticker. The aggregate ratio comes out per week and says it is not an individual funnel: without cookies, who tasted is never linked to who created a key.MED17— Retention comes out by cohort of the week of a key's first use: how many started and how many came back N days or more later. A cohort that has not had that long yet is marked and has no ratio — not a zero — and the result never carries a key hash.MED18— /admin/metricas does not exist without ADMIN_TOKEN (404) and, with it, brings the four Block 5 metrics — tasting conversion, retention, arrivals by channel and watcher events per month (the same counter as the /vigia page) — with no e-mail, IP or key hash.MED19— Two synthetic scenarios on the same kind of day: people reading 10 pages many times is not suspicious; the same volume spread once per page over 1,000 pages is flagged as a suspected crawler. The cut-offs come from analytics_config.json, the table has no IP or cookie column, and the panel says "browser", not "human".MED20— derivado/search_console.jsonl (one line per day, entered by hand by the owner) is read by analytics and appears in /admin/metricas as google_real next to the concentration; pulso prints the real Google clicks next to the google.com referer and the days suspected of crawling.TR34— Every public route WITH A PATH PARAMETER opens under any Accept header and is counted by the panel. TR16 audits public HTML pages but drops every parameterised route by construction and only looks at routes declaring an HTML response class, so the company pages, the fund pages and the 500 audit samples were all invisible to it. The samples served raw JSON to */* and were measured by nothing at all: the panel read zero for them since forever, and that zero meant not-measured, not nobody-came. This test also refuses to let the list of parameterised routes go stale, and proves the data-route label is honest rather than a hiding place.TR35— The bulk-export index states how many rows come without a traded code. Its note used to promise that every line carried the ticker and the CVM code, so the datasets would join against your own base — false for one row in six, 11,739 of 71,085 in the indicators set, across 96 companies. It is not a broken join: those are companies registered with the regulator that have no traded stock, and not one of them has a single trading session, so there is no code to carry. The data is right; the sentence promised more. The cost falls on whoever consumes it and is invisible — joining on ticker drops those rows with no error at all, and an outside audit that found the gap concluded it was a failing join, which is itself the symptom of an undeclared limit. Same defect as the price-session wording fixed a day earlier: a claim above what the data delivers, always in our favour, and the same fix — publish the size, computed from the base.TR36— `/empresas/{ticker}` answers instead of returning 404. Walking one level up a URL is the most natural move for anyone exploring an API, and it used to say not-found for a company that exists — an agent concludes coverage ended there. The root is a DATA route (JSON for everyone, any Accept, like the rest of /empresas/), it is OPEN because it carries no figure, and it says three things: who the company is, where its human-readable page lives, and which resources exist, each declaring whether it needs a key.TR37— The /saude alarms on a synthetic database, one at a time: a build started 3 times (the limit) without finishing lights up, 2 does not, a finished one does not; “running” for more than 6 h lights up, “failed” 7 h ago does not say “running”, “running” for 1 h does not; the last session 7 days ago lights the daily alarm, 6 does not; 3 daily-update failures in a row light up, 2 do not; a database with no price gives null coverage instead of breaking the route.TR38— _com_idioma adds the language switch exactly ONCE and is idempotent, pointing to the other language of the same route; public pages of each family carry one switch; the watcher JSON shows at most 500 events when no limit is given.TR39— _idioma: an explicit ?lang= wins in any case; otherwise the language that appears FIRST in Accept-Language; neither pt nor en gives PT. _com_idioma without a breadcrumb puts the switch right after “back”, once; with two breadcrumbs it marks only the first. _noindex_follow touches only the first . _data_por_extenso drops a time stamp and returns None without a date.TR40— The visit middleware counts, on GET only: each company page, its restatement page, the hubs, the comparisons and the FUND page when the answer is HTML; not an arbitrary /acoes sub-path, not POST, not the fund JSON on the same URL. Key usage counts only answers below 400 and never on the pages of _PAGINAS_ANALYTICS.TR41— On the page URL with a key: an unknown fund or ETF is 404; a database of another version answers 503 “building” (page and JSON); an old fund code redirects with 301 to the current one, and a current code equal to the requested one does NOT redirect.TR42— /vigia/eventos: `desde` includes the day itself; `limite` stays between 1 and 5,000; `tipos` carries the name of each type.TR43— A database with no build or daily-update keys: failures and starts are 0 and there is no warning; the disk is in GB with two decimals and the used share with one, of the same data folder; price coverage carries the FIRST and the last session.TR44— Tasting: /empresas/WEGE3 is an open data route; POST and error answers do not count. Daily limit: exactly the limit passes, the next one is 429, and the message names the plan (“free” or “Pro”). A public page that already declares Vary: Accept goes out with Accept, Accept-Language.TR45— The dadosb3 client (cliente/) runs against the app through the TestClient transport, with no network: verdict, watcher (company and funds), REIT restatements, screener with as_of and receipt, and reading the receipt. The recorded answers the package tests use offline have the SAME top-level keys the route answers today, and every key quoted in the package README exists.TR46— /api has one section per public and keyed route (company, indicators, multiples, dividends, screener with as_of and receipt, receipt, verdict, watcher, company and REIT restatements, REIT, ETF). In PT and EN, each section shows the curl, the five-line Python and the answer the route gives NOW, cut by the same cut function; the Python runs on the pip client and returns the same answer. The access written in each section is what the route does in commercial mode (open, key except for the open tickers, key always), and the screener example still comes out without opening the screener to anyone without a key. The key rule, the cache, the connector version and the MCP registry come from the app constants, and the cache header is what the page says. No banned term.TR47— The code only imports PUBLIC FastAPI and Starlette modules (no fastapi.middleware.* or starlette.middleware.* beyond the documented ones, no private segment starting with an underscore); requirements pins both within a range with an upper bound; and building the /api examples (the whole app through the public httpx ASGI transport) records no visit, key use or trial use, and brings the answer of every section.I-H01— The flow case of 21/08: the external source returned ZERO and the database under construction was left with no rows at all.I-H02— The FX case of 21/08 — the one a `if it came back empty: copy` would let through, because it came back half-full, not empty.I-H03— The rule in one sentence: after inheritance, no table may hold FEWER rows than the previous database.I-H04— First build ever (or a wiped disk): there is nothing to inherit from, and that must not raise.I-H05— The REAL numbers of 21/08/2026 have to be refused.I-H06— The gate must NOT be zero, and this is the test that prevents it.I-H07— The normal case: a bigger new database. It passes.I-H08— First build ever: there is nothing to compare against, so it releases.I-H13— A new rule may legitimately remove rows (the restatement key gained the aggregation level and 9,467 rows became 8,654), but only by DECLARING how much, per data version, and only at the version switch: the same declaration does not license the next build of the same version to shrink again, a declaration for one count does not release another, and the drop cannot exceed what was declared. Without this, the anti-shrink guard refused the first rebuild in production and FII/ETF pages stayed unavailable.PR05— The free-key button leads to a form, not to the payment provider. The free tier is a zero-value subscription, and the path to it used to be the same checkout as the paid plan — no card requested, but wearing the face of a payment form. The funnel measured the cost: 47 people opened it and 16 finished. Asking someone to cross a billing screen to collect something free is friction with nothing on the other side.PR06— The plans page states who the charge comes from. Forty-six checkouts were opened and none was paid, and at the last step the customer met a company name the site had never mentioned. The public name at the payment provider is now the product's, but the registered entity is still another one and it is the entity that appears on the invoice — so the page says so BEFORE, instead of leaving the discovery for the moment of the card.PR07— A malformed e-mail creates no customer, and neither does a crawler. Without the checkout in front, creating a key became an anonymous POST that creates a customer at the payment provider. The two remaining doors are e-mail validation and the crawler filter — the same one already blocking bots on the plans page, where 27 of 72 visits were automation.PR08— A created key records WHERE the person came from — the tag on the link we sent out and the optional 'how did you find us' answer — and none of it is stored with the e-mail. Without cookies, the entry visit and the key created later were rows with nothing linking them: the dashboard said 'N keys' and could not tell whether any came from the AI connector, from llms.txt or from an AI assistant. Tags come from a closed list; an invented tag is dropped, never stored as free text.I-Q09— The FNET daily report (latin-1 HTML) parses into the reference month, the CNPJ and one row per day — NAV per share, inflows, redemptions, net assets, total portfolio, shareholders — checked against the real BOVA11 document.I-Q10— The FNET balance sheet (COFI chart) parses into {account: (description, balance)} for ALL accounts, checked against the real document (118 accounts).I-Q11— From the CVM's Resolution 175 registry only the index-fund classes (FIIM) come out, with a 14-digit CNPJ; other types stay out.I-Q12— The portfolio file (CDA cda_fie) parses per position with the month's first day as reference, decimal-point numbers, filterable by CNPJ.I-Q21— The three quota accounts of an ETF balance sheet are stored exactly as the document declares them — no convention is chosen. The COFI chart does not give account 30915158 the same meaning at every administrator: for most it is already the month-end balance; for some (LVOL11 in August 2025 is one) it is the opening balance of the financial year, and the month-end figure is opening + issued − redeemed. An earlier version applied the second formula to everyone and broke most funds. Storing what is declared keeps the information for either convention, and the cross-check (I-Q06) accepts both.V01— The verdict text is number + numeric band + comparison; no judgement adjective enters, and the 'adjective' mode in the configuration file is OFF — it only turns on by an explicit decision, for the day a licensed analyst signs.V02— The top sentence has at most 25 words and is deterministic: the same input always yields the same sentence, with no language model in the request path.V03— Special cases have their own text and never an invented number: negative profit (P/E does not apply, P/B still does), negative EBITDA, net cash, banks (ROIC and debt/EBITDA do not exist), a young company without history, a value carried with a declared note, a flagged value that is not published, and a multiple flag that belongs to another field.V04— The confidence flag speaks about the SNAPSHOT, with reasons: green when nothing is off; yellow when a central number is withheld by a flag, when a material restatement (≥5% in a key account) happened in the last three years, or when the last annual statement is over 15 months old; red when the snapshot may mislead — the year's own statement was restated by more than 10%, or the regulator already holds a newer statement than the one computed here.V05— The configuration file is coherent with the codebase: every flag it names is produced in precos.py or indicadores.py, the not-applicable map equals the company page's, the forbidden-terms list catches what it must and ignores the standard disclaimer, and the translator never queries the database directly.V06— Run over every traded company: no forbidden term in any text, every top sentence within 25 words, every non-green flag carries a reason, the flag is not uniform (if 90%+ were one colour it would inform nothing), and the same ticker yields the same verdict twice.V07— The 'below the Selic rate' cut for ROIC is switched on and off by the configuration file: off, the fixed band applies; on and with the fiscal year's average Selic, the band says below/at-or-above with the year and the rate; on without that year's Selic, it falls back to the fixed band and never invents the rate. No new label carries a forbidden term.V08— Every cut in the configuration file falls on its declared side: a band's lower limit belongs to it; a zero ROIC with the Selic cut on is 'below', not 'negative'; the company's own history enters with 3 years, not 2; a restatement counts within the last 3 years, from exactly 5%, and only with a known difference; red needs MORE than 10% in the year itself and yellow-by-age MORE than 15 months. Born from the code mutation run of 26/09/2026: each of these comparisons could be flipped and the whole suite still passed.V09— The top sentence is only shortened when it EXCEEDS the word limit, and the cut removes exactly the 'against X for the sector' clause (then the reason for red) — nothing else; zero net debt reads as '0.0x', not net cash; a zero margin with a flag is not narrated as a loss; a FLAGGED number is never narrated as 'not applicable'.V10— The flag text states the filing date and how many central numbers were published, exactly — and the restatement reason quotes the cut as written in the file. The earlier test only checked the start of the sentence, and mutation showed that a date with one extra character or a doubled count went through.VG01— Each event type in the product instruction is detected — profit turned into loss (or the reverse), net margin and net debt/EBITDA changing band, restated statements, the confidence flag changing colour — each with the previous and new period, the number before and after, the date the revealing filing became public, the source and the data version. A restatement is dated by the NEXT annual filing, where the year comes back with a different number. A series with no change yields no event.VG02— No number lives in the watcher's code: the bands are the verdict's, the restatement cut is the same as the flag's, and the quarter used for comparison is chosen in the configuration file — changing the file changes the event.VG03— Only a number the verdict would publish becomes an event: a flagged value, a not-applicable value (negative EBITDA, zero revenue), an indicator that does not exist for banks, and an account that is zero on one side of a restatement (a missing line, not a republished number) are left out. A number the site withholds cannot change band in public.VG04— No watcher text — Portuguese, English or the e-mail — carries a forbidden term, even when the company described the account as 'sales revenue': the text uses our own label and the filed description travels only in the JSON. The e-mail is a skeleton: it builds subject and body for the followed tickers only, and nothing in the module sends, schedules or stores subscribers.VG05— The watcher reads only through the data layer and the verdict — no SQL in the module or its page — and imports neither the API nor the company page; the page only builds HTML. Deterministic: the same input yields the same events.VG06— Run over every traded company: every event has a known type, both periods, both numbers, a date, a source and the data version; no text carries a forbidden term; all five types occur; the monthly counter adds up to the list; a restatement is never dated in its own year; the same ticker yields the same events twice.VG07— /vigia and /vigia/eventos open without a key; the monthly counter on the page and in the JSON is counted from the list, never typed; each event on the page links to the company page and shows its source; filtering by ticker and type works; an unknown type is a 400; the page is in the sitemap and llms.txt.VG08— REIT watcher, pure function: P/B, 12-month yield and vacancy changing band and unitholders changing beyond the cut-off come with both numbers, both periods, both bands and the date of the revealing filing. Only the immediately previous period is compared (a gap is no event); a flagged number is no event; the cut-offs come from veredito_config.json; unitholders need the minimum in both reports.VG09— A report with version above 1 is an event dated by the delivery of the version in force; if the build captured the re-filing, the event carries the field that changed most (before and after) and how many others changed; without capture it says only what is known. Version 1 is no event.VG10— /vigia?classe=fiis and /vigia/eventos?classe=fiis show what the REIT watcher returns, filterable by type and fund, with a SEPARATE monthly counter; /vigia without a class is still the companies' page; an unknown class or type is 400; no forbidden term in any fund event; the e-mail is only a skeleton.VG11— The 12-month yield has no series at the source: each build snapshots fii_yield into fii_yield_historico (one row per fund and base date), with the versioned derived file in the symmetric step; leitura_fii reads on a read-only connection and accepts a database without the new tables; the REIT watcher has no SQL.VG12— The text formats (reais in thousands/millions/billions in both languages, date, quarter), the detection edges (zero margin is neither profit nor loss; a restatement exactly at the cut-off counts; a number missing on one side makes no band event), the event direction both ways and in English, the restatement source, the flag periods, the counter by type and without date, and the empty, sorted e-mail.VG13— REIT watcher: each band label in both languages; a value exactly at a cut-off belongs to the upper band; unitholders exactly at the minimum count; a fall is written as “fell” and “−”; unitholders with thousands separator and no decimals; on a re-filing, the field that changed most in RELATIVE terms and how many others; the counter by type and month; the filter by ticker and type; the e-mail empty, with inclusive `desde` and ordering.VG14— The restatement text: one account (“one account changed ...”) or several (“N accounts changed ...; the largest”), with the fiscal year and the following annual filing, in PT and EN; among accounts with the same relative change, the one with the largest absolute change is chosen. In the REIT watcher: the English unitholders text without decimals; a capture with only “no field changed” gives the text without numbers; month distance with two-digit months; an undated event sorts after dated ones without breaking.AC01— The whole follow-by-e-mail flow with the file provider: request on the company page creates an UNCONFIRMED row and queues the confirmation e-mail; the link confirms; the weekly notice is built from the watcher's events for that company, with the leave link; leaving cancels, and no notice follows. Asking again before confirming resends the SAME link; after confirming nothing is sent. Unknown ticker is a 404.AC02— No response — request page, confirmation, leave, invalid e-mail error, the admin metrics panel — contains the address, and no log line writes it. The panel counts requests, confirmations, cancellations and the most followed companies: numbers only.AC03— Cleanup deletes a never-confirmed request older than the configured deadline, and only that one: the confirmed stays, the recent stays. The deadline is a parameter from the configuration, not a constant.AC04— The follow block appears right after the verdict on EVERY company page — the three most viewed (ITUB4, PETR4, BBAS3) and the others, in Portuguese and English — with the form to /acompanhar and the right ticker, and no text of the block, the response pages or the e-mails carries a forbidden term.AC05— A crawler following the form creates no request and sends no e-mail, and above the configured hourly cap the answer is 429 — without storing IP addresses, like the free key.AC06— Without RESEND_API_KEY/CORREIO_REMETENTE the provider is 'file': it writes to the pending queue and returns False. With both, it sends a POST to the Resend API with sender, recipient, subject and text — and if the API refuses, the message falls into the queue instead of vanishing. The address never goes to the log.LE01— resolver finds the company by any of its codes and returns the MAIN ticker (or the registry one when there is no main); unknown ticker is None. por_ano, multiplos_por_cd and serie put every column in its own field, and the series only carries multiples for years with indicators — the full list carries all.LE02— retrato takes the last year with an indicator, counts its flags, summarises restatements (total, years, the largest per year) and measures staleness: last filing received, last quarter and how many years the regulator holds beyond what was computed.LE03— faixa_propria uses only clean values and states the exact position (0 = lowest, 1 = highest), None below 2 years; mediana_setor excludes the company itself, requires 3 peers and is None without a sector; distribuicao takes each company's last CLEAN year (or all in a given year, multiples included) and the right quantiles; the fraction helpers are strict.LE04— liquidez is the average volume over the last N sessions, with the window stated; without prices, None.LE05— recebimentos separates annual from quarterly filings and puts each quarter in the right slot; reapresentacoes orders accounts by the largest difference, names the item in the chart of accounts of the company's TYPE and dates each year by the FOLLOWING annual filing.LE06— retrato_em builds the snapshot of the day that year's annual filing became public: that year's indicators and multiple, only restatements of EARLIER years, the year's average Selic and 'today' = the filing date. A year without indicators, or an unknown ticker, is None.LE07— The quarterly series respects the company's type: a bank gets no gross margin (it does not exist in its chart of accounts); with no type, the general one applies.LE08— Dividends newest first with every column in place; trading dates in order; the payment date announced to the regulator keyed by class, record date and value to 6 decimals, and empty without the table; last close of each year; coverage or None.LE09— A company that does not consolidate (publishes only the individual statements) restates like any other: the reading gives the year’s aggregation (ind, nao_consolida) and which side came zeroed in the CVM file; the page says “individual — company does not consolidate” and “no value in the file”, never R$ 0 nor the 100% the empty side produces; the flag counts every row and measures the difference only where both sides have a value.I-P27— With round numbers, every field of a multiple comes out exact: market value = price × shares; P/E = value ÷ controlling-shareholder profit; P/B = value ÷ (total equity − minorities); EV/EBITDA = (value + net debt) ÷ EBITDA; FCF yield = (operating cash flow + capex) ÷ value — and no flag. The suite tested the BUILT database, not the code that computes it; code mutation showed that swapping any of these operations went through.I-P28— Each substitution comes with its own flag and a number that does not exist stays empty: without minorities, total equity; without controlling profit, total profit; negative profit gives a negative P/E flagged, zero profit an empty P/E; non-positive equity has no P/B; missing EBITDA, negative EBITDA or unknown debt silence EV/EBITDA without inventing it; without capex, no FCF yield.I-P29— The rulers fall on their declared side: a P/E of exactly 200 is not extreme, above is; market value BELOW 1% of equity is a share count in thousands (fixed ×1000 and flagged), exactly 1% is not fixed and falls outside the plausibility band; 0.02× and 50× equity are inside; above the market-value ceiling nothing is published.I-P30— A unit ticker divides the price by its basket (and says so), an unknown basket is flagged; a year with divergent scale loses the four multiples; without shares outstanding the row is empty with the reason; the share count used is the last PUBLIC snapshot up to the price date (never the future), flagged when it came from the reference form and when it is more than 200 days old.I-P31— A year's share count against the previous priced year: up to 3× either way passes; beyond, the year has no multiples — unless the previous year was the FIRST with a price (IPO), the ratio is the ~1000× of a thousands scale, a corporate action is recorded between the previous and the next year, or the count was already adjusted.I-P32— A synthetic COTAHIST line with a distinct value in every field records close, open, high, low, average (two decimals), volume, quantity divided by the quotation factor, trades, date and the source code. Other record types, odd-lot market, unknown codes, zero price and unreadable lines are not recorded.I-P33— In the week a company trades under the old and the new code, the day's record is the main ticker's — all eight fields of the same line and its code — whatever the order in the file; a day only the old code traded keeps the old one. Large batches are written in full.I-P34— The code map gives the main ticker priority 0 and history of the same class priority 1; history of another class is not stitched; a code claimed by two companies leaves both; a company without a main ticker is left out.I-P35— The main ticker comes only from the company's most recent registration file and only from an active, exchange-traded, valid code, common before preferred; the code history gathers every year without placeholders.I-P36— Trailing-twelve-month profit by the identity prior annual − prior Q1..Qk + current Q1..Qk; total net income enters flagged when controlling-shareholder profit is missing; a missing piece makes it null with the missing piece named.I-P37— The quarterly multiple uses the first session from the quarterly filing's receipt within 45 days inclusive; market cap = price × shares, P/E TTM = cap ÷ TTM profit, P/B = cap ÷ (equity − minorities). Each absence — no filing, no session in the window, no share count — is recorded by name.I-P38— The share count is the latest already public up to the date (inclusive), never a later one; on a tie DFP/ITR beat the FRE; FRE alone is flagged; zero counts are ignored; above 100 billion no number; staleness above 200 days is flagged, 200 is not.I-P39— The annual calculation writes one row per year with facts: no filing received gives sem_data_publicacao, no session in the window gives the receipt date and sem_preco_apos_publicacao, otherwise the first session from the receipt (never the one before) and the full computation; it returns how many rows it wrote.I-P40— One step above each cut-off changes the result: P/E 200.5 is extreme; market cap 50.5x equity leaves the band; equity of exactly zero has no P/B and does not divide; half a real of EBITDA still gives EV/EBITDA; half a real of profit is not negative; the market-cap ceiling and the implausible share count at the exact number and just above.I-P41— A split with ex-date on the share-count date is applied; on the price date it is not; the day before the price it is. A suspicious event without a factor inside the window silences the count; outside it, it does not.I-P42— Q2 and Q3 find the quarterly filings of 30 June and 30 September; quarterly P/E of exactly 200 is not extreme and 200.5 is; P/B 50 is not and 50.5 is; market cap at exactly 1% of equity is not rescaled; zero equity does not divide. In COTAHIST a zero quotation factor counts as 1 and a 50-cent price is a price.I-P43— A COTAHIST line with every byte filled — digits next to each field, every number using its full width — is read exactly, so a slice shifted by one byte changes the number. Share jumps of 950x and 1,050.5x fall outside the thousands band and are flagged; a prior year declared in thousands is multiplied by exactly 1,000. In the quarterly path, equity of 50 cents still gives P/B and TTM profit of 50 cents is not negative.
Level 2 — Semantics: does the account mean what the calculation assumes? (34)
I-B07— Every LATEST × PREVIOUS divergence above 0.5% in the key accounts must be recorded in `reapresentacao`.I-E05— The trading session used in each multiple falls INSIDE the window after the filing was published.I-F02— The same central invariant as the equity multiples, applied to REITs: the price behind P/B is never earlier than the filing's delivery date.I-F07— Vacancy is a fraction in [0, 1] (INVARIANT) and the source's ambiguous semantics is always flagged (HEURISTIC, in the same test).I-F08— No CLEAN P/B (flag NULL) pairs a book value from one reporting period with a trading price far later than the filing's publication.I-B24— Every bank and insurer that has a fact has both profit AND equity extracted in the most recent year.I-X02— Inflow and outflow are GROSS flows: negatives cannot exist.I-X03— The series skips no month between the first and the last.I-L03— An indicator published WITH A VALUE and WITHOUT A FLAG cannot have a required input with no backing in the database.I-L04— Every indicator the database publishes is declared in `INSUMOS`.I-B15— The price used in a multiple is never earlier than the filing's receipt date — the central INVARIANT of the whole multiples block: without it, the P/E 'knows' a balance sheet the market had not yet seen.I-B16— Counter-proof: market cap recomputed outside the calculation code (price × the most recent share count AS OF the price date) matches what was stored.I-B18— The same central invariant as I-B15, in the quarterly block: the multiple's price is never earlier than the quarterly filing's receipt date.I-P05— `codigos_negociados` works with ZERO COTAHIST files present: it returns an empty set instead of raising.I-P02— Every code feeding a company's price series belongs to the SAME share class as today's primary ticker.I-P04— Every market cap uses a share-count snapshot that WAS ALREADY PUBLIC on the price date — never a later one.I-P12— The quotation factor corrects the QUANTITY; the price passes through untouched.I-P14— The swap only ever happens between different share CLASSES — never between a code and the retired name of the same class. Without this, MOTV3 would lose to CCRO3 and EMBJ3 to EMBR3: the old code has more historical volume purely because it existed longer, and the site would start calling Embraer and Motiva by the codes they abandoned.I-P15— The liquidity swap elects a unit only when its basket is recorded in unit_composicao, and never dethrones one. A unit packs N shares into one instrument; without N, market cap comes out multiplied. The basket is now inferred from the unit’s own price and market cap already divides by it, so keeping units out left Energisa represented by ENGI3, which trades 2,669 times LESS than ENGI11 — Taesa, Iguatemi, Santander and Sanepar likewise, four of them vanishing from any liquidity-screened universe. A unit with no recorded basket still cannot win, however liquid.I-P23— A company that changed its ticker root (Marfrig to MBRF3 in January 2026) gets the new code even while the annual registration form still lists the old one, and the old code, with zero recent volume, yields without the 3x factor over the sixteen years of history it accumulated. Same root is never a rename; a root claimed by two companies is discarded; a living incumbent keeps the factor.I-B32— Every indicator the database emits has a provenance entry in the DICTIONARY — the formula, the CVM accounts and the profit BASE that /indicadores now ships ALONGSIDE the number.I-Q18— Asking FNET for an OLD month does not shrink the filing window. A fund's daily report for a month is filed in the first days of the next one, and FNET's search is by filing date. One parameter used to do two jobs — say which month is the last closed one, and stop the window from asking for the future — so pushing it back to fetch history collapsed the window to a single day and the source returned nothing. A seven-hour backfill stayed stuck on the newest month because of it, and the source took the blame it did not deserve.I-Q19— Asking only for balance sheets does not download daily reports. An ETF's effective fee comes from the difference between TWO balance sheets of consecutive months in the same financial year, because the chart-of-accounts figures accumulate across the year. The base held 153 balance sheets piled into two isolated months — 138 funds, only 4 with the pair — and a run aiming at the missing month spent its whole budget on that month's daily reports, which are useful but are not what blocks the calculation.I-Q20— The backfill's time budget applies INSIDE the document listing, not only between months. FNET's search pages 200 at a time, up to 30 pages, and each request waits up to 240 seconds when the source is throttling — two hours for a single listing. The budget used to be checked only between one month and the next, so a 30-minute round spent three hours without finishing its first one, and the easy reading would have been 'FNET is down'. It was not: the limit was ours, in the wrong place. Half a listing is an honest result — it saves what arrived and the next round continues.I-N77— The three pages that lacked JSON-LD in the 'as an AI' test (/hoje, /buscar, /comparar) now carry ONE valid schema.org block in the head: the screeners declare a SearchAction pointing at /procurar?q=, /hoje is dated by the DATA VERSION (not by today), and /comparar/A/B lists both companies with ticker, resolved name and the URL of each company page. Portuguese and English.I-N78— Every public read-only response without a key carries Cache-Control: public, max-age=300 and Vary: Accept-Language (the same URL serves PT or EN by browser header); no response carrying an API key, and no session, billing, protocol or keyed-data route, is ever publicly cacheable; 404 and 503 are not cached.I-N79— `pip install dadosb3` is the first move anyone who writes code makes, so it appears at the three doors that person arrives through: the /api hub, llms.txt (the door AI assistants read) and the /para-quem page, in both languages. The link points at PyPI — the source the reader can check — not at a page of ours.I-N81— When the filing to the regulator declares an instalment payment, the asset's calendar says so and links to the document, instead of going silent about the payment. The form carries the FORM of payment (lump sum or instalments); an instalment payment has no single date, it has several, inside a document we do not parse. Before, such a line produced no payment event and looked exactly like a dividend with no date at all — the reader concluded there was no scheduled payment, when what exists is a payment in instalments we have not opened.I-Q07— A Brazilian-formatted number ('13.919.737.980,15') becomes a float; empty or invalid becomes None, never an exception.I-Q08— A Brazilian date ('11/09/2026 20:27') becomes ISO; an invalid one becomes None.I-Q13— B3's ETF list is read even when the JSON arrives double-encoded.I-Q14— B3's fund detail yields ticker, CNPJ, manager, registrar and share count; an empty body yields None.I-Q15— The FNET document search yields id, type, reference date (a balance-sheet month becomes its first day) and filing date.I-Q16— The index is inferred from the fund NAME by declared rules, most specific first; a name with no clue yields None, not a guess.
Level 3 — Economic: is the result possible in the real world? (14)
I-X08— Every published correlation carries its own n, and n is never small enough to be noise.I-X09— The SAME-month correlation is the largest of them all.H-B05— Total assets never negative, never above R$ 100 trillion (catches a MIL currency scale applied twice).H-B08— |ROIC| > 200% without the `roic_extremo` flag is a defect until proven otherwise.H-B23— Same pattern as H-B08/H-B17: a payout above 5× the ex-date price is a historical price not adjusted for a split (common in older B3 data), not a parsing error.I-E04— No market capitalisation larger than Brazil's GDP.H-F05— Same pattern as H-B17: a P/B outside [0.2, 5.0] with no flag is a defect until proven otherwise (tiny or negative book value, corporate action).H-F06— A trailing-12m dividend yield above 30%/year is almost always an extraordinary distribution (property sale, capital amortisation booked as income at source), not a recurring yield.H-B25— A bank ROE outside [-100%, 150%] is almost always an extraction error (equity and profit swapped).H-B17— Same pattern as H-B08 (ROIC): |P/E| > 200 or |P/B| > 50 without a flag is a defect until proven otherwise.I-B31— The sector index carries the 10 macro sectors plus 'Market (B3)', each with a full series (same number of weeks), positive levels and base 100 at the start.I-B33— No week of any sector moves more than a stock market can move in a week.H-B14— Q1+Q2+Q3 standalone revenue <= annual revenue (2% tolerance) for the VAST majority of companies — the residual (implied Q4) must be non-negative.I-P07— The B3 average price falls OUTSIDE [low, high] in ~0.1% of rows — and this test exists to record that, not to fail on it.
Other product tests (120)
Access gate, navigation and the MCP connector — they test whether the product works, not whether a number is right, so they don't fit the 3 levels above by design. Still counted, still listed:
MED01— Every tool on the MCP connector goes through the counting point. The attack is to reintroduce a bare @mcp.tool() decorator on some tool: it would work perfectly and stay silent in the panel, which is the failure mode this project exists to avoid, because the number simply gets smaller and nothing warns you. The instrumentation is a single choke point rather than a line remembered in seventeen places, since this codebase has already paid three times for rules that depend on somebody remembering to apply them.MED02— Calling an MCP tool actually records the call. A decorator that exists but records nothing is worse than no decorator at all, because the panel then reports a confident zero, so this test calls a real tool and then looks for the row in the database. It records the tool name and nothing else: no arguments, no ticker, no API key, no IP address.MED03— A click in our own funnel is not counted as a visitor. Funnel events land in the same table with bot=0, so every checkout opened was inflating the human traffic count, and the error ran upward in a number used to decide where to invest. The first version of this test PASSED with the defect reintroduced, because the local database holds no event rows at all; an invariant that depends on the database happening to contain the case is an invariant that tests nothing on the day it does not. It now creates the rows it needs and measures the difference they make.PR01— This file's header may not lose the warning that passing the suite is not proof of truth.PR02— No indicator goes live without a methodology page.GC01— With Stripe unconfigured the product serves everything with no key — the development and UVI mode, which must not close by accident.GC02— With the gate on, WEGE3 stays open: the free taster that lets anyone check the data before deciding.GC03— Outside the taster, indicators, multiples and facts all require a key — and the refusal is a 401 on all three routes, not just the first.GC04— A non-existent key is refused. It looks obvious, and it is precisely the test that stops a gate from becoming decoration by accepting any string.GC05— The counter-proof to GC04: a valid key opens the gated ticker. Without both, the gate could be refusing everyone.GC06— The daily cap actually cuts: the call beyond quota returns 429, not a silent 200 with the counter ignored.GC07— Landing, methodology and /saude never sit behind the gate — the product's argument has to be verifiable by someone who is not a customer yet.GC08— Free plan: access to everything, but the daily cap is the free one (lower).GC09— /assinar?moeda=usd passes currency='usd' to checkout (international audience); the default stays BRL.GC10— ?lang=en serves the English landing page; an Accept-Language of en does too.GC11— The JSON dictionary states each indicator's profit base — the audit finding: net margin (total) != ROE (controlling shareholders).GC12— The screener filters by ranges and rejects invalid input (rather than ignoring it).GC13— Dividends follow the gate: WEGE3 (the free taster) opens with no key; PETR4 requires one; a valid key passes.GC14— Scores and restatements follow the gate: WEGE3 open; PETR4 requires a key.GC15— The 401 answers HTML to a browser and JSON to an agent.GC16— The 401 page comes out in English for whoever asks in English.GC17— The bulk dump is what separates the free plan from the paid one.GC18— The Pro 403 answers in the format of whoever asked.GC19— With no key, /dump must not offer the free key.GC20— A ticker that does not exist answers 404 BEFORE the gate asks for a key; HEAD answers like GET with no body; the CORS preflight on the connector endpoint accepts POST. Three findings from an agent testing production the way an AI would: a mistyped ticker got “key missing — subscribe” and the AI relayed the subscription as the fix; HEAD returned 405 on the home, robots and sitemap, which a link checker reads as a site that is down; and the connector preflight refused POST, so a browser-based MCP client could not talk to the server.I-N01— Crawls every page from the home: no internal link may answer an error, and every page must have a way back home.I-N02— Methodology aliases (URLs people GUESS, like /metodologia/pl) redirect to the canonical page — and a name that does not exist still 404s, so the alias does not become a hole that swallows everything.I-N03— Every HTML page carries a visible language switch.I-N04— The methodology — the product's central argument — exists in English.I-N07— Every configured vertical has a door, and the door speaks both languages.I-N05— Every public page declares a viewport and has a responsive rule.I-N08— The main menu, in both languages, has a door to every vertical.I-N06— A route that depends on a NEW table answers 503, never 500, while the table does not yet exist.I-N09— A table mixing two time bases must label each of them.I-N10— Every example the /screener menu offers must be accepted by /screener itself. Two of the three used to return 400 — the menu exists precisely for someone arriving without knowing how to use the endpoint, and handing them a broken example is worse than handing them nothing. The check runs against the same indicator set the filter consults at runtime, so renaming an indicator without updating the examples fails here.I-N11— The /screener menu answers callers who have NO key. The menu exists, in its own words, for 'whoever discovers the endpoint and opens it in a browser' — precisely the person who does not have a key yet, and for months it sat behind the 401. The test calls the endpoint with no key header and demands 200 with the indicator list, while also demanding that filtering stays closed — otherwise the fix would leak the paid product.I-N12— /saude publishes the version of the data being served, and it matches the database actually in use. Born from a sceptical external review: 'I want to say my backtest used Dados B3 version X and recover that dataset later'. The field existing is not enough — a label that drifts from the database is worse than none, because it grants false confidence. The test reads `meta` straight from the database, outside the route, and demands the two agree.I-N13— The restatements page is public while the per-company feed stays behind a key. This is the first fact page, and the rule behind it comes from measurement: over thirty days the AI assistants fetched the home page 209 times, the comparison page 15, the multiples methodology 14 — and the TWENTY conceptual guides added up to 7. An assistant already knows what a price-to-earnings ratio is; it fetches whoever answers what it does NOT know. Restatements are the one product dataset no competitor publishes, because the regulator overwrites the old version of a statement and we keep both. The split mirrors the rankings page: the AGGREGATE is public because it is acquisition, the per-company detail is paid because it is the product. Publishing the whole feed would give away what sustains the paid tier; hiding the aggregate would hide precisely what differentiates us.I-N14— The data-quality page lists what we do NOT publish, and why. Writing it fixed a defect before anything was published: while assembling the breakdown of the eight thousand withheld indicators, over a thousand turned up with a NULL flag, meaning they were withheld with no stated reason. On a page whose whole subject is why a figure is missing, thirteen percent of 'I do not know' is the page contradicting itself. The cause was zero revenue on the income statement, which happens in holdings whose result comes from equity income and in companies with no operations that year: the withholding was correct and the reason was simply not declared. Those became explicit flags and the silent cases fell to fifty-three. Withholding without saying why is half the house rule; the other half is saying it, and that is the half a public page enforces. The test also caps the silent cases, because without a ceiling the next rule that withholds without declaring would go unnoticed exactly as these did.I-N15— The bank chart-of-accounts page proves that an account code does not define an account. The fact it carries: one large state-owned bank reported profit under one code through 2019 and under a different one from 2020 onward, while the largest private bank stayed on the original code for the entire series. Anyone scraping the regulator with a fixed code gets the private bank right, gets the state-owned one wrong from 2020, and receives NO ERROR at all: they get an empty value, or another line's number. The stable case being the most famous bank is what makes this dangerous, because it is the one everybody tests a scraper against, and it passes. Seventeen of twenty-one institutions changed their equity account, most of them in the same year. This is not our theory: an external auditor found the mirror defect in our own output, where we declared one account and used another, and that correction became its own invariant. This page publishes the map that episode showed was missing, and the test attacks by checking the contrast survives.I-N16— The sustainability-reporting page states that the obligation was REPEALED, with the date. The idea arrived with an out-of-date premise, and that is what made it worth publishing: it came as companies being required to report from 2026 with the first reports in 2027, which was true until 29 May 2026, when a new resolution repealed the requirement outright rather than postponing it. Almost everything written in Portuguese on the subject predates that repeal and still says it is mandatory, so a page that corrects information an assistant would otherwise repeat is exactly the kind of page that gets cited. The page also declares what we do NOT have: we ingest annual and quarterly financial filings, not sustainability reports, so this is regulation rather than our own measurement. Without that admission the page would imply we measure environmental and governance data, which would be more useful to us and less true. The test attacks by removing either the word that carries the correction or the admission of the limit, and it also requires the sources to appear on the page, because a rules page without sources is an assertion.I-N17— The page listing the traps of the regulator's CSV counts them from the database rather than asserting them from memory. Every trap on that page bit us, and several were found by external auditors checking what we publish: the extra dimension in the statement of changes in equity came from one of them, who received six rows where our coordinate promised exactly one. The risk with a page like this is that it becomes a theoretical list of precautions for using public data, the kind anyone can write and nobody verifies. What distinguishes it is the COUNT, and the count has to come from the database: written by hand it goes stale in silence, which is precisely the defect another invariant exists to catch. The test attacks by hardcoding a figure into the text instead of querying for it, and it also checks the thousands separator matches the language, since a Brazilian separator reads as a decimal point in English.I-N18— The look-ahead page measures rather than asserts, and it separates a filing delay from a restatement. The theory of look-ahead bias is everywhere; the Brazilian NUMBER is not — how many days pass between a fiscal year ending and its statements existing at the regulator. It is about a hundred days on average, and a hundred and sixty-one filings took more than a year, which is the figure anyone calibrating a lag actually needs. The extreme case requires a caveat the page makes explicitly: the largest gap in the base is a bank showing an eight-year interval, and that is not a delay in the original filing, it is a restatement being republished years later. Selling that as a filing that took eight years to appear would be technically true and materially misleading, the same error nearly made when ranking restatements by percentage. The page also states what lagging the filing does NOT fix — survivorship, historical universe, delisting — because a correction that implies more coverage than it delivers is its own kind of look-ahead.I-N19— Someone arriving without a key gets the PAGE, whatever their Accept header says. This was a discovery defect found in production: the first version negotiated on an explicit text/html header, so a browser got the page while the wildcard header - the default for curl and for several automated fetchers - got a 401, as did a request with no Accept header at all. The page is listed in the sitemap and in the file we publish for AI agents, so anyone arriving through either got a closed door on the most differentiated page we have, and a 401 reads as nothing-here rather than wrong-format. This is the third time in the same family: the connector endpoint answering 406 in a browser, the audit route missing from the AI index, and now this one - the pattern is always a door that only opens for someone who already knows how to knock. The correct rule is not the Accept header but the API KEY: whoever sends a key wants the feed and existing integrations must not break, while whoever sends none has no access to the JSON anyway, so answering 401 merely hides public content.MCP01— The connector advertises EXACTLY the 14 expected tools — not one fewer (breaks clients) nor one more (drops out of the catalogue).MCP02— Through the connector the taster holds too: WEGE3 answers with ROIC and no key at all.MCP03— A gated ticker does not hand the AI a raw error: it returns an error WITH the way to resolve it, because the reader is an agent that needs to know what to tell the user.MCP04— A key passed as a tool argument opens the gated ticker — this is how an AI client authenticates, without an HTTP header.MCP05— The methodology is free through the connector too: the AI can explain HOW the number is computed without anyone paying for it.MCP06— MXRF11 is the REIT vertical's free taster: it opens with no key, just as WEGE3 does for equities.MCP07— The connector answers clients that send a third-party Origin. A real defect on 22/08/2026: the SDK's anti-DNS-rebinding protection validated Origin against a list holding only our own domains and returned 403 to chatgpt.com, chat.openai.com and claude.ai — the connector we advertise on Glama, mcp.so and X was turning away exactly the clients we invite. An external review hit it; our own probe missed it because curl sends no Origin — tested as a machine, not as a client. So this test sends the header explicitly. The other half matters just as much: opening the shop door is not opening the safe, so it also verifies that a paid ticker without a key is still refused.MCP08— Opening /mcp/ in a browser returns a useful page, not a 406. THREE external reviewers in a row failed to test the connector, and this was why: the MCP endpoint requires `Accept: text/event-stream`, so a plain GET received a 406 carrying a JSON-RPC error. To a human — and to the crawler of whoever is evaluating us — that reads as a BROKEN SERVICE. And it is precisely the URL we advertise on Glama, on mcp.so and in the README. An endpoint that only proves it works to someone who already knows how to use it proves nothing to someone evaluating it.MCP09— The connector's audit tool points at the regulator's archive, not at us. It exists because two of three external auditors had browsing disabled and could not reach a sample by URL at all; the connector is the door that does not need a browser. But it only counts if it sends the auditor to the primary source: an audit tool that points back at the product being audited is the number agreeing with itself. The test also enforces the two rules that keep a result from inflating on its own, that the auditor picks their own seed and that unchecked never becomes passed.I-TR01— Every test whose ID belongs to the data-invariant family (any "I-"/"H-" that is not navigation) must be classified in `_NIVEL_DO_TESTE`.I-TR02— The total collected by introspection is EXACTLY the number of tests `pytest --collect-only` sees in the same modules — not one more.I-TR03— No test disappears from the collection for lack of a docstring or ID — the explicit requirement is 'never vanish silently'.I-TR04— The featured-case lists may only cite IDs the real suite still has — a dangling case (a test renamed or removed) is dead history that should not keep being told.I-TR05— Every test in the suite has a description in Portuguese AND in English — the double gate exists because requiring only the translation would let an empty description come back, translated as empty.MED04— The build alarm lights up on the fact, not on the instant. It went silent through seven consecutive failures in production because it keyed on the state being 'failed', while the retry loop rewrites the state to 'running' at the start of every new attempt: the alarm switched off for the whole duration of each build, and a build retrying hourly is 'running' most of the time. The health endpoint was answering with seven failures, a timeout error and a null alert on the same line while production served two-day-old data. An alarm that blinks is not an alarm.MED05— If the database being served is not the version the code promises, the health endpoint says so. This is the ground truth that depends on no bookkeeping at all, and bookkeeping is exactly what failed: while the build was breaking, production served the previous version for two days with the correction already written, tested and committed, and nothing anywhere said so because nobody compared the stamp in the database against the version in the code.MED06— 'Running' has a deadline, so a hung build cannot pass for a healthy one. Running is the only state without an alarm, which makes it the worst place for a process to die: stalled and working look identical, and the silence reads as health. The ceiling is six hours against the twenty minutes a rebuild takes, deliberately generous so a slow instance never triggers it, because an alarm that cries wolf stops being read.MED07— A page that publishes a live count also says when that count is from. The rule was already written and applied to only half the pages: the stamp helper says it exists for any page carrying a live number, and two of four had it. An external reviewer then read a cached copy of the methodology page showing one test count while the live home page showed another, and concluded a number had been hardcoded in the HTML. Nothing was hardcoded, and both pages serve the same number today: the actual defect was that the methodology page could not prove how old it was, while the transparency page, which does carry a stamp, let the same reviewer spot his own cache and not report it as a data error. A rule declared and half-applied is the failure this project exists to hunt, so it is now structural: any file with a live-count placeholder is rejected until it carries the stamp.PG01— No module that tests DATA is left outside the publication gate. The gate was trimmed for a real reason and trimming is dangerous: four consecutive rebuilds died without recording any failure, the whole service dropping at around fifty-seven minutes, and the navigation module accounted for 185 of the battery's 220 seconds, crawling every page of the site through a test client while the freshly built 1.8 GB database sat open beside it. A broken link does not corrupt data, and it was blocking correct data from going live. But the next temptation is obvious, and this test exists to block it: to keep cutting until the gate is fast and empty. A gate that rejects little is not a cheap gate, it is an ornament. The check also verifies that every module the gate names actually exists on disk, because pytest given a missing target fails, and that would break every rebuild.TR05— Every `testes/test_*.py` file is declared on the transparency page — otherwise the page announces a smaller count than the real suite.TR06— No public page announces a company or test count with a number typed into the HTML.TR07— The global metrics are IDENTICAL in Portuguese and English.TR08— A number on an EN page comes out in the reader's format, with a comma.TR09— Two tests may not carry the same identifier.TR10— The 100+ test list is filterable, and the filter never hides anything from someone without JavaScript.TR11— No analytical page takes more than 2s to build.TR12— Sweeps ALL public pages for any number that coincides with a database count without being current.TR13— No page may claim exclusivity beyond the universe /comparativo declares it compared.TR14— A claim of SUPERIORITY, not merely of exclusivity, must say who it was compared against.TR15— The traffic panel has a count that does NOT rise when it is us browsing or when the suite runs.TR16— Every public HTML page is measured — the funnel pages above all.TR17— A paid click is identifiable, and nothing beyond the campaign is stored.TR18— A refusal by plan or by quota is counted, and the analytics database NEVER stores the key in clear text — counting usage per user without holding a credential is what keeps the published 'no cookies, no IP, no third parties' promise true inside the panel too.TR19— Every expected value in the public challenge is what the API serves right now. The challenge exists for a third party to contradict us — it publishes the hard cases with the number we claim and the recipe to recompute it from CVM filings. That only holds while the claimed number is true. A challenge whose expected value has aged does not look bad on the page; it becomes marketing that looks like an audit. So divergence here BLOCKS PUBLICATION rather than rendering nicely.TR20— Each of the challenge verifiers rejects the defect it watches. The challenge is only worth something if the verifiers bite: one that passes the defect is decorative, and decorative is worse than absent because it buys trust while giving nothing. desafio/atacar.py holds the attack table — one real defect per verification type — and this test runs the whole table.TR21— The transparency page stamps the data version and the moment it was generated. Born from an external review: the reviewer read a stale INDEXED copy of the page, carrying counts from weeks earlier, and reported it as an inconsistency against /saude, which was serving the current ones. The pages were right; the copy was old. But he had no way to know: without a stamp, a cached page is indistinguishable from a live one. In a product whose thesis is auditability that is serious — the reader must not have to guess whether what they are reading is current.TR22— The challenge publishes how many TRAP TYPES it covers, not just how many cases. An external reviewer pointed out that 20 cases package fewer than 20 distinct traps — three look-ahead cases in different sectors, two absent-indicator cases for banks, one declared control — and called the '20' marketing. He is right: the repetition is deliberate and explained in each case, but a reader seeing '20 cases' understands 20 traps. A challenge that inflates its own coverage has exactly the defect it exists to hunt.TR23— The same seed returns the same cases, and without a seed nothing is drawn. Three external reviewers raised the same objection: the challenge proves we can defend cases WE chose, and says nothing about the error rate of the base. One refused to use the challenge as an independent sample, calling it circular. The random-audit endpoint takes the choice out of our hands — but only if two properties hold: determinism, so the auditor can publish a result anyone reproduces; and the seed being theirs, because a server that picks the sample could retry until a convenient one appears.TR24— The path form of the random audit returns the same sample as the query-string form. A fourth external reviewer in a row could not test the API live: their browsing tool refuses non-indexed query-string URLs, so the sample was unreachable while the whole site was up. They rigorously reported 0 of 75 cases checked, inconclusive by access limitation, rather than claiming the base passed. When four reviewers trip on the same step, the step is ours. The second door only helps if it is the same door: diverging samples for one seed would be worse than having no second door at all.TR25— Every page that shows a live count stamps the data version and the generation time. The same external reviewer, two rounds running: first they read an old indexed copy of the transparency page and reported it as an inconsistency; then they saw the home page announcing one test count while the indexed Portuguese copy still showed a much older one. They were right about what was missing: to audit anything you must know which state of the system you are auditing. A live number with no generation date ages in silence, and the stale copy becomes indistinguishable from the live page.TR26— An auditor's sample survives an update to the base. Found against our own interest: running the external reviewer's three seeds on the rebuilt base to send them a case-by-case diff, there was no diff at all — none of the 75 cases recurred. The universe had gone from 63,406 rows to 63,375 and the draw was POSITIONAL, so removing 31 rows shifts every other one. That destroyed the only property this endpoint sells: the auditor publishes a result, the base updates, and nobody can re-check what they claimed. Worse, anyone trying would see different cases and conclude the sample had been hand-picked, which is the accusation the endpoint exists to make impossible. Hashing the key instead of the position gives each case a position of its own, so an update moves only the rows it actually touched. The test simulates the update rather than describing it.TR27— The audit sample is a citable artifact, not just a dynamic page. Suggested by the external reviewer and adopted in full: hashing fixed reproducibility, but three years from now the universe will be a different size, and someone who published an audit could not PROVE a given case belonged to that sample. With the sample key and its score the proof is arithmetic and does not depend on us — anyone recomputes the score from the key and the seed. The audit id ties the four parameters that define one run into a short citable label so two different audits cannot be confused. The test also enforces machine-readable quality: prose serves a human, but an API consumer has to decide without interpreting text, so 'this financial year has a scale mismatch, do not use its absolute value' must be a boolean, not a paragraph.TR28— The audit history separates outside auditors from ourselves and keeps the errors on record. The page exists against commercial instinct: the reflex is to publish no errors found and let the findings die in the chat of whoever audited. But trust us, the base is good is worth nothing coming from whoever sells the base, and nobody who hides errors publishes a table of errors. The test closes the two obvious ways this page could turn into marketing: adding internal and external checks into one number, and recording as checked the whole sample when only part of it was actually redone. One reviewer received seventy-five cases and could redo five; writing seventy-five would have been the comfortable lie, and it was the reviewer who refused to tell it.TR29— Wherever an agreement rate is published, the statistical caveat sits in the same block. Raised by the external reviewer, and he was right: this page can produce exactly the impression of certainty it exists to fight. Publishing eighty out of eighty reads as the base has no errors. What it means is only that no divergence was found in those eighty cases, which supports no claim about the population, especially when most of the checks were run by us. The test demands adjacency, not existence: a true caveat alone in a footer is a caveat nobody reads, and serves only to defend us after someone has already misread the number. It is enforced in both languages, since the reader least able to check the rest of the site is the one reading the translation.TR30— The published audit prompt sends the auditor to the primary source, not to us. The outside advice was to find a human third party. Half of that does not hold: an auditor need not be human, it needs to reach the primary source and have no stake in the outcome — the best external finding so far, a utility's EBITDA matching to the cent against the company's own release, came from an AI reading an independent source. The half that does hold is that we cannot sit in the middle: while we write the prompt and hand it over case by case, the framing is ours, and framing is half the result. So the prompt is published, it points at the regulator's archive rather than our endpoint, it tells the auditor to pick a seed nobody suggested, and it forbids turning could not check into passed.TR31— Every seed in the published range is listed, with no gaps, and the hub that lists them is indexable and in the sitemap. A contiguous, complete range is what makes cherry-picking impossible: publishing only 'some example seeds' would hand the choice of sample back to us, which is the very vice this endpoint exists to kill, and a gap in the middle is indistinguishable from carelessness — nobody counts five hundred links by hand. The sitemap half of this test was revised in September 2026: it used to demand all five hundred sample URLs be in the sitemap, which could never work because those pages are noindex; what delivers the original intent is the indexable hub that links them all.TR32— A failing build becomes visible state, not just a log line. Real incident: a change broke one path of the build, the exception was logged, the job retried hourly, the previous database kept serving, and the site looked perfectly normal for sixteen hours. The design protected users, since nothing half-built was ever published, but it protected too well and hid the problem from us as well. The fix is not email: it is publishing the state where someone already looks. The health endpoint is polled by the daily routine and open to anyone, so a silent failure now requires someone to ignore a field that says failed. The test also demands the error itself and a plain-language line explaining what the failure means, because a bare failure count only alarms whoever already knows how to read it.TR33— The comparison page states when it was checked and admits where we lose. It had been wrong in our own favour for three weeks: the table said a competitor had no MCP connector, only a partial methodology, and a quarterly price around forty reais. In fact it now ships a native MCP connector installable in one line, publishes a methodology that cites regulator account codes, and charges essentially our price with a free tier shaped like ours. That page is the SECOND most fetched by AI assistants, which makes it the worst possible place to be wrong in our own favour, and simply re-dating it without re-checking would have preserved the error - the word verified only means something if someone verified. The test attacks the shape a marketing document takes when it pretends to be a comparison: it requires the section listing where each competitor beats us to exist, requires a concrete admission (one of them has price history going back twenty-four years further than ours), and requires the competitor's connector to keep being acknowledged.MED08— A funnel step taken by a robot is not counted as human intent. The event recorder hardcoded the human flag regardless of user agent, and the arithmetic closed exactly: seventy-two hits on the subscribe route, forty-five human and twenty-seven robot, against seventy-two events, proving none were filtered. Counting crawlers turned noise into the metric that most resembles truth, which is measured purchase intent.MED09— A crawler following the subscribe link does not open a payment session. Every robot that passed through created a live checkout session at the payment provider as well as a funnel event: garbage on both sides, and of the worst kind, noise shaped like intent to buy. The test also checks that the agent check happens BEFORE the session is created, since a check that runs afterward has already opened the session it was meant to prevent.MED10— The free plan is visible in the served HTML, without executing any script. The readers who do not run JavaScript are the ones that matter most here: the AI crawlers and the search engine, three thousand hits a month and the only external channel that grows. An AI summarizing the site was quoting the monthly price with no way to mention the free key. The original design's honesty is preserved and tested: with the plan switched off the blocks stay hidden and the page does not promise what does not exist. The decision only moves from the client, where it depends on running code, to the server, where the fact is known.MED11— End-to-end counterproof for the rule above: the landing page as actually served carries the free plan. It is not enough for the function to work, it has to be wired into the path that serves the page — which was precisely the original defect, where the rule existed in the client script and the served HTML did not carry it.MED12— No page hand-writes a number the database already knows. This is the gap in the stamp rule, found while checking the AI connector page: the stamp rule only binds pages that USE a placeholder, and the page that teaches an AI how to connect escaped it by using none at all, carrying a typed 400-plus companies and sixteen years when the real figures were 456 and seventeen. The damage is specific and about as bad as it gets for that particular page: an AI reading it to answer questions about the product repeated numbers SMALLER than the truth, so we were understating ourselves on the one page whose audience is precisely the reader who will not check. Sixteen occurrences across seven files, English guides included, while the template helper already said in writing that a changing number must be a placeholder and never typed into the HTML — another rule written and never enforced. What the check deliberately does NOT flag is a third party's number: the comparison table cites a competitor's coverage, and that is a fact about them, while our own row in the same table uses the placeholder. A checker that rejected the competitor's figure would only teach people to route around it.PR03— Someone who has not chosen a plan sees the plans, not a credit card form. The subscribe route was a redirect straight into a live Stripe checkout, and the plan parameter defaulted to the paid tier, so eighteen links across the site, including the nav button, threw readers from a content page into a charge they had not asked for. The funnel showed exactly what you would expect: forty-one paid checkouts opened and zero card attempts, not one declined. Nobody balked at the price; people landed on a form they never requested.PR04— The plans page shows the free tier before the price. This is not an aesthetic preference: the free door is the only one with measured conversion, nine out of thirty-one against zero out of forty-one, so a page leading with the price pushes everyone toward the door that does not convert. The check reads the body only, because the meta description names both prices and is not visual order — the test itself once failed for that reason, and the defect was in the test.AUD01— The coordinates we hand an external auditor really do lead to the line. Three of the five external audit attempts died on the same rock: the reviewer's tooling cannot open a zip archive, so they read the published cases and could never compare against the source. Serving our own extracted CSV would have been worthless, since the auditor would then be checking our number against our own extraction, so we publish coordinates instead: the exact file inside the archive and the three-column filter that isolates the row. This test opens the archive, applies the published filter, and checks that exactly one row comes back and that its raw value times its declared scale equals what we serve. A wrong coordinate does not return nothing, it returns a real row belonging to a different account, and the auditor then reports a discrepancy against a correct database, which is the worst possible outcome of an audit.AUD02— The instructions do not promise a check that the source makes impossible. The first draft said to compare the archive checksum and that it must match. Running our own instruction before publishing showed it failing: the regulator REPUBLISHES these archives, and the one for 2024 was modified two days after we read it. Any auditor downloading today would get a different checksum and conclude we had tampered with the data. Publishing a check that fails by design is worse than publishing nothing, because it carries the appearance of rigor while manufacturing a false accusation against our own database. The checksum is still published, but as a statement of which snapshot we read, never as a byte-for-byte proof.AUD03— The route states its own limit instead of leaving it implicit. Two admissions have to survive any rewrite: the currency-scale conversion is OURS, and it is the one step in the chain the auditor cannot see; and anyone whose tooling can open the archive should read the original file rather than trust our coordinates. Without the second, a convenience for limited tooling quietly becomes an invitation to audit our own copy, which is the circularity this whole effort exists to avoid.AUD04— End-to-end check on the route external auditors actually use, which builds its coordinates by a different code path than the lineage does, so one passing does not imply the other. It found a real defect on its first run: the statement of changes in equity carries an extra dimension, so the three-column filter isolates one row in the balance sheet and income statement but returned SIX there, one per equity column. We would have published an ambiguous coordinate for every item sourced from it, the auditor would have picked one of six in the dark, found a number that genuinely exists, and reported a discrepancy against a correct database.AUD05— The methodology says what happens WHEN the reporting basis changes. I claimed the rule was undocumented and I was wrong: I searched the HTML shell and a content directory that does not exist, while the text lives in the sources-and-standardization page and has been live all along. Concluding absence from my own bad search is the same mistake an external reviewer made with a cached page on the same day. What was genuinely missing is the CONSEQUENCE at the boundary: that comparing two years there compares two different consolidation perimeters. This test now requires the rule, the flag name, and the concrete case to all appear in both languages, because an abstract rule with no example is exactly what let this boundary go unnoticed for months.AUD06— A comparison that straddles a change of reporting basis is marked as such. One company published a 268 percent revenue increase because its consolidated figure for one year was compared against its individual figure for the year before; on a consistent basis revenue had actually fallen by a quarter, and the number carried no flag at all. The instructive part is why the existing protection missed it: the routine that picks the restated comparative denominator deliberately matches on the aggregation level, but that only guarantees the DENOMINATOR is internally consistent and never compares the denominator's basis against the NUMERATOR's. The guard was correct and in the wrong place. We mark rather than recompute, because recomputing on the individual basis would discard the consolidated statements, which are the right basis for the year they exist in.AUD07— A browser gets a page; an agent that asks for JSON gets JSON. This is the fourth external audit attempt stopped by the same kind of door: the reviewer read the 47 KB home page and could not read the audit sample route at all. The route answered correctly in under two seconds — the problem was that it was 142 KB of raw JSON, a format browsing tools truncate or refuse. Half that weight was my own defect from the same day: the coordinate block repeated all twenty-odd accounts of the company for an indicator that consumes three, and a coordinate for an account the formula never touches does not help anyone audit, it just pushes what matters past the tool's reading limit. Trimming brought it to 60 KB; serving HTML closes it. Same remedy as the connector endpoint that used to answer 406 in a browser: someone arriving by browser is not speaking the protocol, they are trying to READ.AUD08— The audit sample route is named in llms.txt and the home page links to the audit protocol. The fifth external attempt produced the sharpest diagnosis yet: the reviewer picked its own seed, composed the URL, and its own reading tool REFUSED it, because that tool only opens URLs that already appeared in the conversation or in a search result, and no search engine had indexed the domain, so there was no path by link. Checking that turned up two defects of ours: the home page did not link to the audit protocol at all, and llms.txt — the file we publish precisely so that AI agents can find their way around — never mentioned the audit route, which lives only in the sitemap, a file reading tools do not consult. Publishing a door and hanging no sign on it is the same as having no door, for anyone who does not already know it is there. The file now also lists a concrete indexed URL, because a tool that cannot compose a URL cannot use a template.AUD09— Amortization of debt issuance cost is kept out of the depreciation figure that feeds EBITDA. This is the first data finding to come from an external auditor since the earlier successful one, and it started from a single case worth 0.34 percent: one company added a line labelled amortization of transaction cost to its depreciation and amortization, and the reviewer flagged it as a methodological caveat rather than declaring a discrepancy, because he could not open the source file to show the account. He was right, and the cause was systemic: the matcher accepted any label containing the word amortization. Measured across the base: 279 company-years affected, 226 inflated EBITDA figures, 107 of them by more than one percent and 35 by more than five, with the worst at nearly 39 percent. EBITDA is operating earnings plus depreciation and amortization OF ASSETS, and financing cost is not that, so this is a correction rather than a mark: the rule about marking instead of rewriting applies when the source is ambiguous, and a line that says cost of raising debt is not ambiguous. Half of this test guards the opposite error: amortization of the fair-value step-up from a business combination IS asset amortization and belongs in EBITDA, and the first exclusion pattern I wrote would have silently removed it from 89 lines.AUD10— The audit protocol declares the conflict of interest instead of pretending it away. The prompt used to open with a sentence saying the person asking is not the owner of the database and has no stake in the result — and the people who paste this prompt are overwhelmingly us. The fifth external auditor caught it: the audit opened with a claim of independence and the outcome arrived in the first person, ours, admitting fault and announcing the fix. His finding survives the conflict, because the account he checked sits in an audited financial statement rather than in his trust of whoever asked. But a protocol whose entire thesis is honesty cannot begin with a false sentence, and that sentence was mine, written to sound neutral. Declaring the tie is stronger than hiding it: an auditor who knows who asked calibrates his skepticism, while one who finds out afterwards discounts the whole result, and is right to.AUD11— The sample cache makes a retry instant and never serves a sample built from an older database. Six external attempts have now ended on transport rather than arithmetic: measured in production, the same route ranged from under two seconds to nearly fifty across six consecutive calls, with one exceeding a sixty-second ceiling, while locally it answers in 136 milliseconds. That is not the algorithm, it is contention on a half-CPU instance serving a 1.8 GB database from network-attached disk. Every audit tool retries the URL after a timeout, and the three that tried reported the same symptom under different names: a gateway error, a cache miss, an unavailable endpoint. Caching turns the second attempt into five milliseconds. The danger of the cache is precisely the defect it imitates: one auditor spent twelve reads looking at a frozen snapshot from the previous day and reported that the correction was not live. If WE served a stale sample after a rebuild we would be the cause of that, with the aggravation that the number would be wrong rather than merely old — so the database version is part of the cache key, and this test attacks by removing it.AUD12— For a bank, the generic list of source accounts is not presented as though it applied. The sixth external auditor found that one bank case declared one pair of account codes while the accounts actually used were a different pair entirely: declared did not match used. He classified it precisely as an audit-trail defect rather than a numerical one, and refused to turn a documentation inconsistency into a claim that the ratio was wrong — the accounts actually used are the correct banking structure. But 1,407 indicators, 2.2 percent of the base, were published that way, and the placement is the worst part: the banking chart of accounts is exactly the surface our own prompt tells auditors to attack. A field called declared that does not describe what was used looks auditable and is not. The chart varies even between banks, so swapping in another fixed list would only change whose statement is wrong; the honest output says the generic list does not apply here and points at the field that does.AUD13— An auditor can redo the ORDERING, not merely the individual scores. This is the finest objection we have received: having checked every published sample score and found them all correct, the auditor observed that this proves the key reproduces the scores shown, but not that those are the smallest scores in the whole population, because he never received the population to sort. He is right, and the distinction is subtle — checking each published score detects a fabricated key, but it cannot detect OMISSION. A case with a smaller score could have been left out and nothing in what he received would reveal it, so the claim that the sample is the smallest N rested on our word. A new route publishes the population keys, and this test does exactly what he would do: download them, score all of them, sort, and compare. Its first version had the same blind spot one level up — an attack that dropped one arbitrary line PASSED, because an arbitrary line is almost never in the top N — so the count of published keys is now checked against the table itself.AUD14— The published population declares an arithmetic that has to reconcile against a different route. The external auditor had just proved, independently, that the sample is the top of the ordering over the population file — but the SIZE of that population was still a bare assertion: the health endpoint published one indicator count and the population file delivered a smaller number of keys, with a gap of several thousand that appeared nowhere. He could verify that the sample was the top, and could not verify that the population was the population. Publishing the count of suppressed indicators closes the arithmetic across the two routes. This does not remove the dependency on us, since both numbers come from here, but it replaces an assertion with a SUM, and a sum that has to reconcile is attackable: dropping a key from the population now requires editing the health endpoint too. The attack truncates the population file without touching the other route.AUD15— The excluded depreciation lines are published, not only the ones that were summed. Without this the new rule was not falsifiable from outside: we published which accounts ENTERED the depreciation figure and nothing about which stayed out, so an auditor wanting to attack the exclusion of debt-issuance cost — a change that moved 232 EBITDA values — could see neither what was excluded nor why. The rule matches the NATURE of the label rather than the account code, since the same numeric code is transaction cost at one company and amortization of a sales stand at another, and a rule like that can only be defended by showing the label that triggered it, which means it can only be ATTACKED the same way. The auditor named three companies as targets; checking them showed the rule does not even fire at two of them, so the route exists for him to find the cases where it does fire on his own, rather than depending on the audited party to point at the battlefield.CV01— Every EBITDA definition a human read in a real indenture is one the lens still finds. The covenant lens went blind three separate times, and each time the only symptom was a smaller number: filtering debentures on a substring that the Portuguese accent broke returned zero, and requiring the word 'means' within forty characters of the term missed four real drafting styles, finding twenty-four definitions where thirty-one existed. This gate holds a set of excerpts checked by eye; if the lens stops seeing any of them the suite fails and no figure is published.CV02— Whether an EBITDA definition is discretionary or objective is what the human eye read, not what the lens claims. Each excerpt was labelled by hand before the test existed. On its first run it failed three cases, and in all three the lens was right and the hand-written label was wrong, which is exactly the service a gate is supposed to perform: catching the person who wrote it.CV03— Put the defect back and the discretionary flag has to change. Without this attack the flag could be permanently empty and the two tests above would pass by accident, which is precisely how an earlier invariant in this project was born blind, passing with the defect reintroduced because the local database happened not to contain the case.CV04— A clause saying '40% of consolidated EBITDA' is a use of the term, not a definition of it. That phrasing appears when contracts define which subsidiaries count as material. Counting it as a definition would inflate the whole base and make the published distribution describe documents that define nothing.CV05— A document that never mentions EBITDA does not produce a row claiming a definition. More than half the filings in this category are amendments that never touch the financial ratio. The computable field stays NULL rather than false: when the source does not allow a decision, no decision is made.CV06— A threshold written in a sentence is read; a threshold sitting in a table is not, and the page says so. In one indenture checked by hand, carrying five thresholds in a table, automated reading found none. This test pins the reason the page calls its threshold count a floor, and it fails on the day tables become readable, forcing the page to stop saying floor instead of repeating it out of habit.
Reconstruct it yourself — net margin across 5 companies, 4 sectors
We don't ask you to trust the numbers. Here's the net margin of five companies in four different sectors, each rebuilt straight from its annual report (DFP) at the CVM — net income ÷ revenue, matching the published figure exactly. For ANY indicator, of any company, the whole chain down to the line in CVM's file is at /linhagem:
| Company · sector | Net income (CVM acct) | Revenue (CVM acct) | Margin =÷ |
|---|---|---|---|
| WEGE3 · industrial | R$ 6.78 bn DRE:3.11 | R$ 40.80 bn DRE:3.01 | 16.6% |
| VALE3 · mining | R$ 11.81 bn DRE:3.11 | R$ 213.59 bn DRE:3.01 | 5.5% |
| PETR4 · oil & gas | R$ 110.61 bn DRE:3.11 | R$ 497.55 bn DRE:3.01 | 22.2% |
| ITUB4 · bank | R$ 45.85 bn DRE:3.09 | R$ 387.12 bn DRE:3.01 | 11.8% |
| BBAS3 · bank | R$ 16.78 bn DRE:3.11 | R$ 319.46 bn DRE:3.01 | 5.3% |
Download any of these DFPs from the CVM, take the income and revenue accounts, divide — you get the same number. Banks use interest income as revenue (what makes sense for a bank), so the reconciliation is sector-aware; the others use sales revenue. Figures are the latest fiscal year.
Why we may differ from another site (and it's not an error)
A difference between two sites usually isn't one being wrong — it's a method choice. We disclose ours:
- Average vs ending capital: ROE and ROIC use average equity/capital (this year + last ÷ 2), not the ending balance — so they're not a naive single-year division.
- Controlling vs consolidated: we state which one each item uses.
- TTM vs annual: quarterly multiples use trailing-twelve-months earnings.
- IFRS 16, goodwill, cash, exceptional tax: each handled explicitly and flagged when it distorts.
Do the tests bite? Yes — real cases the suite has caught
A test that never fails could mean perfect data — or a weak test. These were born from real errors that slipped through, and now fail — only shown for tests that actually exist in the collected suite above:
- I-B16 · banks' P/E = 0: the CVM reports share count sometimes in units, sometimes in thousands (varying by company and year); market cap came out 1000× too small and P/E was zero. I-B16 (price × shares recomputed) caught it — we fixed 661 annual and 2,027 quarterly multiples.
- I-B24 · the vanishing profit: the profit/equity account varies by bank (Itaú 3.09, BB 3.11); if the label search fails, the number vanishes silently and ROE is born wrong. I-B24 makes that hole fail.
- H-B23 · the 800% dividend yield: old B3 dividends carry a price not adjusted for splits; without H-B23 the yield would look absurdly real. The flag keeps the record and warns.
- I-B20 · the missing COTAHIST 2023: on the first deploy one year of prices failed to download silently; the API shipped with ~50k fewer prices and the suite passed, because nothing checked coverage. We added I-B20 — a year without prices now fails the build loudly.
- I-B33 · Oil & Gas at −89.6% in one week: COTAHIST isn't split-adjusted; a 10:1 split read as a −90% weekly return and stayed in the cumulative return forever, with nothing failing (a blind amplitude filter didn't fix it either — it just flipped which direction was wrong). I-B33 (no sector week beyond ±35%, the ruler is the exchange itself) stops the build.
- I-B31 · 364 of 865 sector-index weeks frozen: extending the series back to 2010, a sector with no company carrying a market cap that week turned into a factor of 1.0 — a flat line that the cumulative return read as 'market stood still', and the 'since 2010' return came out fictional (+527%). I-B31 fails on any week repeated to the cent.
- I-E05 · EMBRAER's 2010 P/E priced with 2025 data: the point-in-time price lookup fell forward with no ceiling whenever the current ticker had no price at balance-sheet time (ticker change, share-class migration); the multiple came out priced 15 years into the future, in 22% of annual and quarterly rows, silently. The 45-day window (and I-E05) blocks it: no session inside the deadline, no multiple computed.
- I-E04 · AZUL at R$ 1.67 quadrillion market cap: the balance sheet carried 54.7 trillion shares from the judicial recovery issuance, but the price used was already on the post-reverse-split (1:150,000) basis — two ends measured on different bases, market cap wrong by orders of magnitude. I-E04 (market cap never above Brazil's GDP) fails the build on that absurdity.
How far back each block of data goes
No asterisks here: this is coverage year by year, counted right now. Indicators (ROE, ROIC, margins, growth) come from the filings alone and cover the whole series. Multiples (P/E, P/BV, EV/EBITDA) need a price and a share count — and the share count comes from CVM's reference form, which the further back you go the fewer companies filed in a usable format. The gap between the two columns is a source limit, not an ingestion hole. If you look for an old P/E and do not find it, it is because nobody has it — not because we hid it. The same numbers as JSON: /cobertura.
| Year | Companies with indicators | Companies with multiples |
|---|---|---|
| 2025 | 438 | 290 |
| 2024 | 444 | 296 |
| 2023 | 442 | 298 |
| 2022 | 429 | 295 |
| 2021 | 423 | 296 |
| 2020 | 410 | 274 |
| 2019 | 371 | 164 |
| 2018 | 326 | 164 |
| 2017 | 317 | 162 |
| 2016 | 311 | 152 |
| 2015 | 303 | 143 |
| 2014 | 301 | 139 |
| 2013 | 292 | 130 |
| 2012 | 291 | 111 |
| 2011 | 284 | 97 |
| 2010 | 277 | 34 |
Live coverage
456 companies · 876,076 price points · 8,654 recorded restatements · last refresh 2026-09-27 00:20:28. Full live counts at /saude.
Sources: CVM (open data, ODbL) and B3 (COTAHIST). Not affiliated with B3 or the CVM. Not investment advice.
Numbers on this page are live. Data version 2026-09-27-reapresentacao-agreg · page generated 2026-09-27 03:31 UTC. If this does not match /saude, you are reading a cached copy.