Fix live option guards, grounding operand evidence, source-bound benchmarks, trade artifacts, attribution, drawdowns, valuation tolerances, and SEC point-in-time factor inputs.
Add Brazil research data and market support, 13 fundamental and valuation factors, channel progress forwarding, and opt-in ZimaOS templates. Update seven README News sections with three visible dates.
Validation: 17,635 backend tests passed; 1,084 final targeted tests and 103 frontend tests passed. Frontend build, package build, documentation checks, and CI gates passed.
Preserve scientific-notation amounts and add regression coverage for the five review findings. Update README News after successful backend, frontend, and contract checks.
Signed-off-by: Haozhe Wu <haozhe_wu@connect.hku.hk>
Unreleased entries for #1572, #1634, #1636, #1578, #1602 and #1637.
#1572's own CHANGELOG hunk was written into the 0.1.16 section before
that release was cut, so it is dropped from the merge and rewritten
here under its own PR number (it cited #1569, the Email/WebSocket issue).
Rolls up 492 commits / 116 merged pull requests since 0.1.15, from 16
contributors.
The final-answer grounding gate stopped guessing what a number is from
the words around it: the model declares each figure's role (observed,
derived, proposed, cited or count), the gate checks the declaration
against the session's tool evidence, and a figure that fails is cut from
the answer instead of the whole answer being refused. A backtest's own
output now grounds the report written about it, run cards cite a metric
only where the saved CSV agrees, and the generated broker matrix states
each profile's permissions as declarations. Research runs keep what they
fetched: compaction follows the model's real context window instead of a
fixed 40K estimate. The missing-data sweep reached 59 more alphas.
Argentina (BYMA), weekly and monthly bars, Bahasa Indonesia, Gildata,
OpenCode, Email and WebSocket channels and four broker connectors (KIS,
Upbit, Toss Securities, Scalable Capital) land alongside.
Verified before release: the reporter's risk-parity prompt, which had
stopped with no_progress after 93 seconds on DeepSeek V4 Pro, completed
on 4a40a7f6's parent in 8m13s with both backtests run, 13 steps saved and
the report released with its core figures grounded.
Version declarations bumped in pyproject.toml, agent/SKILL.md,
Dockerfile, frontend/package.json, both package-lock.json entries, all
nine locales and the Layout test mock. CHANGELOG cites 116/116 merged
pull requests and credits 16/16 authors. README News carries the release
in seven languages, three entries visible.
The BYMA / TSX fix could not close Hong Kong. The hk_equity chain starts with
Tencent and Eastmoney, which declare no currency, so a guard on Yahoo's
declared currency only moved a counter to the next source. The venue gives a
better handle. HKEX's Stock Code Allocation Plan (updated 2026-03-12) assigns
80000-89999 to "Products traded in Renminbi", and 09000-09199, 09200-09399,
09400-09499, 09500-09599, 09700-09799, 09800-09849, 10900-10999 and
41500-41599 to products traded in USD. Every other .HK code trades in HKD, and
that includes 09600-09699 and 09850-09999 (9988.HK, 9618.HK). Checked against
the currency Yahoo declared for 24 codes on 2026-09-24: 6 CNY, 10 USD and
8 HKD, all matching.
Before: 80700.HK (Tencent's RMB counter, 375.40 CNY) or 9834.HK (a USD ETF)
ran in the HK engine's HKD pool next to 00700.HK with no check at all, because
Composite's mixed-currency guard only runs across markets. The grounding gate
also required the answer to call their prices HKD.
- _market_hooks.hk_counter_currency(): the one table. code_currency() uses it,
so Composite's guard sees CNY / USD counters.
- runner._create_market_engine refuses a Hong Kong code traded in another
currency, for single-market and cross-market runs alike, before any engine
prices it.
- grounding identity._infer_currency reads the same table (lazy import, as
context.py already imports from backtest), so 80700.HK is quoted in CNY
whichever source served it.
Tests cover the 24 live codes, both sides of every range boundary, the refusal
(mixed, pure RMB, USD, cross-market) and an HKD-only run that still builds.
Mutants each fail: no RMB range (12), a USD range one code too wide (1),
code_currency ignoring the counter (1), the runner guard off (4), grounding
ignoring the counter (1). Related suites: 2769 passed.
With imap_use_ssl off, the Email adapter and its connection probe logged in
over an unencrypted IMAP connection. That predates #1544, but #1544 put the
switch in the guided setup. SMTP already had smtp_use_tls, STARTTLS by
default when not on implicit SSL. IMAP had no equivalent.
- imap_use_tls (default true) upgrades a plain IMAP connection with STARTTLS
before LOGIN, in the adapter and the probe. A server that offers no
STARTTLS, or a failed handshake, is reported as network-class before any
LOGIN, so the password is never sent. Turning it off is the one explicit way
back to a plaintext login, and its help text says so in nine locales.
- verify_tls now governs every TLS path. Both STARTTLS calls used their own
always-verifying context, so a self-signed server had an opt-out on implicit
SSL but not on STARTTLS. email_tls_context serves all four paths.
The panel's label map in ChannelConfigPanel.tsx is a second hand-written list
beside FIELD_HINTS. A hint missing from it still renders, as its raw key with
no help, so the plaintext warning would have been dropped silently. A test now
requires every hand-written hint to have a panel entry and an en label and
help. It passes for all four existing tables and fails if imap_use_tls's
entry is removed.
Mutants each fail: no STARTTLS in the adapter (1) or the probe (5),
imap_use_tls ignored (1), probe or adapter SMTP STARTTLS ignoring verify_tls
(1 each). Channel sweep: 448 passed. Frontend i18n + settings: 64 passed.
Upgrade note in the CHANGELOG: an imap_use_ssl=false setup against a server
without STARTTLS stops connecting until imap_use_tls is set to false.
openai 3.19.0 still built request headers with the case-sensitive
_merge_mappings; 3.19.2 switched to _merge_headers. Main's pre-fix code with
langchain-openai 1.6.5 passes 9/9 on openai 3.19.0 and fails the one test on
3.19.2. The comments, test docstring, CHANGELOG and news said 3.x.
Two defense-in-depth fixes from an adversarial review of the Email/WebSocket
guided-config surface (#1544). Both harden pre-existing patterns that the new
email probe — the first whose target host is caller-controlled while reusing
stored credentials — made relevant.
Email TLS certificate verification (CERT_NONE -> verified):
- The implicit-SSL paths (IMAP4_SSL/SMTP_SSL) in the connection probe and the
polling/send adapter used Python's default unverified context (CERT_NONE, no
hostname check), so the mailbox password was sent to a server whose
certificate was never checked; an active MITM could harvest it. All four
call sites now build their context through a shared email_tls_context()
that verifies the certificate and hostname against the system CA bundle,
matching the STARTTLS path, which already did.
- New EmailConfig.verify_tls field (default True) is the documented opt-out
for self-signed/internal-CA servers, surfaced in the guided Email setup with
a localized label/help in all nine locales.
Settings-write CSRF guard:
- require_settings_write_auth was missing the cross-site browser guard its
siblings (require_auth, require_event_stream_auth) enforce on unsafe
methods. POST /channels/email/test merges stored credentials with a caller
patch, so a cross-site page could steer the stored mailbox password to an
attacker-chosen host. The guard now runs first on every settings-write
route; same-origin Web UI calls and non-browser clients (no Origin) are
unaffected.
Tests: 9 new email TLS regressions (helper + probe/adapter x verify_tls) and
5 new settings-write cross-site guard regressions; the email field-count
(29 -> 30) and i18n parity assertions were updated. Channel sweep 639 passed,
frontend 656 passed, test_readme_counts 108 passed; new/changed files are
black- and ruff-clean (email.py's 9 ruff errors are pre-existing import-block
debt, unchanged 9 -> 9).
Follow-up to #1519 (extends #1520, hardens #1544).
Signed-off-by: shadowinlife <shadowinlife@gmail.com>
Extend the contract-driven channel config surface from #1520 to the Email
and WebSocket adapters: both can now be configured, connection-tested and
hot-applied from Settings with no restart and no hand-edited JSON — the
"one hints dict entry" path #1519 designed for the long tail of adapters.
Backend
- Email: standalone IMAP + SMTP credential probe (email_probe.py) — login,
mailbox select, SMTP connect/(STARTTLS)/login — classified into the frozen
ok|invalid_credentials|network codes with secret-scrubbed, side-prefixed
details; the adapter's test_connection delegates to it.
- WebSocket: local-server probe (websocket_probe.py) — TLS cert/key material
validation plus a read-only bind of every resolved address (SO_REUSEADDR
gated to POSIX, matching asyncio's create_server default); an address held
by the already-running server reads as the expected state, not a failure.
The config routes now build the WebSocket adapter with the gateway services
it requires, so a WebSocket save or test no longer fails validation.
- config_meta: hand-written field hints for both channels. A hand-written
hint's secret flag is now authoritative for the keys it covers (the regex
fail-safe still masks every unhinted key), so token-shaped non-secrets such
as websocket_requires_token stay visible and editable instead of being
masked — a form save can no longer silently flip the stored token requirement.
- Email polling skips stale-config delete/move post-actions once a stop or hot
swap begins; the already-fetched batch is still delivered.
Frontend
- Localized labels/help and guided setup for both channels across all nine
locales; secrets are masked and never round-trip to the browser.
Adapter fix found during a live end-to-end smoke test against a real 163.com
mailbox: NetEase (163/126/yeah.net) accepts LOGIN but rejects the first SELECT
with "Unsafe Login" until the client sends an RFC 2971 IMAP ID, so the Email
channel could not read the most common Chinese mailboxes. The probe and the
polling loop now send a static, best-effort IMAP ID after login
(utils.send_imap_id) — it carries no user data or secrets and is ignored by
servers that do not support it. Verified end to end: an inbound mail from an
allow-listed sender passed SPF/DKIM, reached the agent, and an auto-reply was
delivered back over SMTP; on the same run the anti-spoofing check rejected the
mailbox's own SPF-failing service mail.
Follow-up to #1519 (extends #1520).
Signed-off-by: shadowinlife <shadowinlife@gmail.com>
The index-caliber comment said fqkline returns only "day" for all seven
indices measured; that holds for the six SSE/SZSE ones, and 899050.BJ
comes back empty.
Weekly and monthly bars (#1479), price indices stamped raw (#1541), the
compaction replay (#1488), field-ref tail-risk identity (#1444) and the
QVeris refusal (#1494). The 2026-09-20 entry, which announced that 1M was
rejected, moves under Earlier news.
The PR's CI failed on its own changes: the seven README loader tree lines
still said 27 sources without gildata, and two tests pinned a seven-source
A-share order that stops validating once gildata joins the chain.
README_id.md, which landed after the PR's base, also gets the source-table
row, the count and the chain. The loader docstring and CHANGELOG still said
the token rides the URL query string; since the review it is sent in the
Authorization header.
get_cn_macro_series (MacroIndustryData: China macro/regional/industry EDB
— FRED only covered US/global until now), get_cn_announcements
(AnnouncementData: A-share/HK/fund filings with highlighted excerpts —
get_sec_filings covers SEC only) and search_broker_reports
(FinancialResearchReport: the Juyuan report library, coexisting with the
eastmoney-backed get_research_reports). One thin shared client
(gildata_srv.py: JSON-RPC POST + format=json, own throttle bucket, dual
Accept header), routed api_names echoed so a mis-routed NL question is
visible to the agent, rows capped via limit, key-gated on GILDATA_TOKEN
with an optional GILDATA_SRV_TOOL_URL override. News/screening/FinQuery
deliberately left to the existing tools. MCP surface 75 -> 78.
Off-exchange funds publish daily NAV, not OHLCV, so this lands as an
agent tool instead of stretching the loader contract: NetFundUnitValueReport
behind the same ParamCandidateRecall + disk-cache machinery (new otc_fund
wiring). Serves unit/accumulated/adjusted NAV and daily growth, ascending,
most-recent limit, per-fund errors never abort the batch; exchange-listed
codes are rejected with a pointer to get_market_data. Key-gated on
GILDATA_TOKEN; exposed as MCP tool #75 (manifest + six-README lists).
HKStockDailyQuotes for 00700.HK-style symbols (raw bars, volume already in
shares, stamped raw as a per-market exception after measuring 00700.HK) and
IndexDailyQuote for exchange-specific CN index codes (000xxx.SH / 399xxx.SZ,
so 000001.SZ stays Ping An Bank). Both tools need a Juyuan internal code:
ParamCandidateRecall with an exact ref_code match, cached to
~/.vibe-trading/cache/gildata-codes.json so a symbol costs one round-trip
per machine. Joins the hk_equity chain tail. US equities excluded — this
token's US history is sparse before ~2022 (12 rows in 2020, 134 in 2021),
which would silently corrupt backtests.
OpenAI's gpt-5.6-* reject function tools on /v1/chat/completions unless
reasoning_effort is 'none', the model's own default effort included, so the
agent's default openai configuration failed on its first tool call. Setting
'none' would run the model without its reasoning, so the adapter now treats
that 400 like a rejected temperature (#1223) or stream_options (#1224): the
request is retried on /v1/responses, where langchain-openai carries the effort
as reasoning.effort, and the model is remembered for the process so later
calls go there first. The matcher reads the endpoint's own instruction
(reasoning_effort + not supported + /v1/responses); a gateway that rejects the
field as unknown is not rerouted, an explicit LANGCHAIN_USE_RESPONSES_API=true
is never second-guessed, and a refusal on /v1/responses itself surfaces once.
Every integer in a table cell was a measurement, so a ranked table's # column
(1, 2, 3) and label cells ("12m mean return", "Mean beta (9 reported)") were
flagged and cut; _sweep_same_values then removed every bare figure sharing a
cut figure's digits, single digits included, so "Names 1 to 3", "3 names" and
the "12m" headers went with them. Three shape rules, no word list:
- a column whose numbered cells read 1, 2, 3 ... in row order (two rows or
more, never an OHLC-bound column) is the table's index: exempt;
- a plain integer in a cell that also holds a word (two letters of any
script) is read as prose: bare, still subject to the integer-price check
for instruments quoted in the thousands. A cell holding only a number, any
decimal / percent / currency figure, and any cell under an OHLC column stay
measured;
- the sweep never keys on a single digit.
Reproduced on the two-bar ledger: 20 cuts before, the 11 invented figures
after.
gtja191_098 compared a NaN drift with 0.05, read the False as "not below"
and emitted -DELTA(CLOSE, 3) for every bar whose 30-bar mean still held the
gap. It now yields NaN there; on gap-free data every finite value is
bit-identical and only warmup rows (3..58, inside the declared 60) change.
The remaining 35 alphas the perturbation sweep flags are all accounted for:
28 recursive statistics (GTJA SMA(A, n, m) as ewm, one running product),
which skip a missing observation and continue from their last state — the
policy decided on #1463, now stated in the operator layer's header and
pinned by test_a_smoother_skips_the_gap (gap bar NaN, next bar not, later
values not the gap-free ones; a NaN-until-rewarm mutant fails it); 3 that
declare their own partial window; 4 rank(ts_cov(rank, rank, 5)) alphas whose
flagged cells are the oracle's rank-tie artifact (moves of exactly 1/48 or
1/24, 10 to 241 bars past a 5-bar window, covariances equal to twelve
decimals).
In position_adjustment="hold" the capital-fit search re-planned every open
on each bisection step, and every step whose size rounded to zero booked a
zero_size rejection for the same symbol on the same bar: one contract the
cash could not hold was reported as 26 lot-rounding failures. Trial plans
inside the search no longer reach _on_plan_rejected (_plan_open_order takes
observe=False); a sleeve that was a real order at full scale and left the
basket is reported once, after the search, as insufficient_capital. The
rebalance path's dropped sleeves use the same reason, and zero_size now means
the lot rule only. The #1274 test never scaled a sleeve away (its B target
was below one lot at full scale); it now does, and the lot-rule case is a
separate test.
One Robinhood login reaches several accounts, and every account-scoped tool
takes account_number. Nothing sent one, and every consumer read the reply one
level too shallow (data.positions where Robinhood nests data.data.positions),
so the portfolio could not use Robinhood and the live runner and pre-trade gate
failed closed on every read.
Shapes: one module, connectors/robinhood/mcp.py, reads every Robinhood reply,
built only from the shapes posted on #1428 (observed get_accounts and
get_portfolio, advertised get_equity_positions outputSchema). A non-empty
data.next, a null list or item, or an unmapped data key is an error, never a
shorter account.
Portfolio (#1428): robinhood-live-mcp-readonly shares the MCP server and
OAuth grant. A connection instance carries account_ref, chosen from
get_accounts through the API, the CLI (connector select-account, setup
--account) or the connection center, with nothing preselected. A source with
no account errors before any call. Positions stay unpriced, because the profile
declares no quotes.read. A non-zero or unreported options, crypto, futures,
event-contract, mutual-fund or fixed-income value fails the source.
Live (#1442): /mandate/commit requires an account for such a broker and checks
it against get_accounts (listed, not deactivated, agentic_allowed). The Web
confirm dialog and the CLI REPL ask the user to pick from GET /live/accounts,
and never take the account from the agent-written proposal. Runner reads and
submits, the ceiling fetch and the gate's reads resolve the mandate's account
on each call and unwrap through the same module. The gate refuses an unbound
mandate or an order naming another account, and prices held positions through
the order-notional quote path, failing closed per row.
Existing gate and runner tests encoded replies Robinhood never sends. They now
use tests/robinhood_mcp_helpers.py.
Both issues were reproduced on main before closing them and neither example
was fixed yet:
- `sharpe_sample_size = 1.8` grounded "Sharpe ratio 1.8" and
`drawdown_threshold = 0.12` grounded "Max drawdown was 12%", because a
metric word anywhere in a compound leaf named its kind. Only the head names
it now, after trailing numeric parameters and period/unit qualifiers
("cvar_95", "hit_rate_daily"); "strategy_max_drawdown" and
"portfolio_var_95" still resolve.
- "−5,132%" and "2,237" were read as −5132% and 2237 next to "1,57%". An
answer whose numbers include an unambiguous, marked decimal comma and no
thousands grouping reads every single-comma number as a decimal; an
unmarked "1,50" proves nothing, and "1,234,567" or "2,237.50" keeps grouping.
Replayed today's six real runs: identical verdicts. The #1418 VaR report grounds
once its 95% confidence level is declared as a count.
Untagged fences, integer prices of instruments quoted in the thousands, a price
declared count, and an unchecked number reaching the stream are no longer
described as unchecked.
Each item was reproduced on the branch before this commit before fixing; two reviewer residuals
did not reproduce (a plain 56.7 against a 0.567 low is already refused; a
bare-ticker call already grounds its canonical symbol) and are not changed.
- A table cell declared with a call's ref is compared with that call's values
for its own column: a close cell holding the high passed with `ref c1`.
- `var` and `es` count as tail-risk evidence only as the whole leaf name;
`var_explained` and `sales_es` were read as a VaR.
- A buffered run that hit the tool-call-syntax fallback streamed its message
twice (pre-existing).
- `thinking_done` no longer carries the figures block in an unbuffered run, and
a held-back last line in a tool-call turn is shown once the turn ends.
- The `_PrivateCompanySkillTool` test stub (named load_skill) takes a required
constructor argument, so registry discovery cannot instantiate it as the
real tool for later tests.
The [Unreleased] entries written with this branch's first commit described the word-catalogue
gate this branch deletes (level-word allowlists, subject words, derivation
phrasings). They now describe the shipped contract: shape-only inference, the
declared roles and what each is checked against, revision cap 2 with a cut
release, the status line, and the fixes found by review and real runs.
A ten-minute run for 159516.SZ ended in the three-sentence safe_fallback
refusal after the gate rejected four drafts. Three things were wrong: the
fallback discarded the whole draft, the gate rejected derivations its own
correction prompt asks for, and a missing provenance word cost a full model
round. This makes the rejected draft releasable with only its unverified
figures cut out, and closes the escapes an adversarial audit of that change
found.
Which evidence backs which claim
- _is_price_denominated_indicator decides on the LEAF. Under the old
any-token-anywhere rule indicators.ma_diff, .sma_cross, .ema_gap,
.support_strength and .supertrend_direction all classified as prices, and
any of them set to 1.30 let the answer print "现价 1.30 元". Pivot levels
r1/r2/r3/s1/s2/s3 were unreachable dead entries (the digit strip turned r1
into r) and now match; window/period/length/span/cross join the denylist.
- An indicator answers a price LEVEL claim and nothing else. The rule was
first written the other way round — admissible unless the clause names an
OHLC field — against a hand-written list of field phrases, and a
hand-written denylist is only as complete as the day it was typed. With
sma_20 at 1.150 and the observed close at 1.171, "收盘价为 1.150 元" was
caught while "closed at 1.150" / "the close was 1.150" / "highest price was
1.150" were released, and all ten spellings of a spot quote (现价 / 最新价 /
成交价 / 报价 / 股价 / current price / the quote is) leaked in both scripts,
because a spot quote names the latest print and names no OHLC field. The
allowlist is 支撑 / 阻力 / 均线 / band / pivot and their kin; everything else
is compared against OHLC evidence at the unchanged 0.5% band. A level word
the allowlist misses costs a correction round — a denylist gap released a
fabricated print.
- An indicator is symbol-bound. With evidence for two instruments and a
clause naming neither, 562500's sma_20 grounded a 600519 price claim.
- A metric figure is not grounded by happening to equal an observed price. A
run holds hundreds of observed values spanning the instrument's range, so
any percent-free metric written in that range collides with one:
"策略夏普比率 1.171" and "| 夏普比率 | 1.171 |" validated against an observed
close with no backtest in the session. The figure must also be WRITTEN as a
price, and a metric cell is exempt only when it restates a PAIR of observed
prints ("| 峰值→当前 | 1.180 → 1.110 |"), which is the shape the exemption
was added for.
Derivations and endpoint arithmetic
- Only a formula's own operands and result are exempt, at the position where
they sit. Value-scoped, "建议买入价 0.95 元(0.95 × 1.171 = 1.11245 参考)"
released an entry price 19% below every observed bar: arithmetic that
derives nothing, whose free operand inherited the exemption its own copy
inside the bracket earned. The occurrence inside the formula is exempt; the
one beside it is compared like any other claim.
- A formula's RESULT may not be a market print. "2026-06-23 收盘价 = 1.171 ×
0.80 = 0.937" claims a close the ledger holds as 1.137. When a print word
labels the equation itself, the operands stay exempt and the result is
compared; "基于收盘价 1.171 × 0.80 = 0.937 作为买入价" — the word on an
operand, the result proposed as an entry — still passes.
- The result separators are one shared list for both languages: "≈" passed
while "approximately" burned a revision round.
- _ARITHMETIC_EQUATION_RE is deleted. _evaluate_formula's two-operand rule
already subsumes it (mutation-verified: forcing the precondition false
changes no verdict), and it removes one entry from the regex catalogue.
- An English period ends a clause, exactly as 。 does. Two English sentences
sharing a line were one clause, so the header's "last close" labelled the
level claim after it and a derivation in the first sentence exempted a
fabricated price in the second — while the Chinese translation of the same
text got the opposite verdict.
- A drawdown may only be grounded by the falling direction. Measured on a
1.053/0.666 ledger (true fall −36.75%, inverse rise +58.11%):
"最大回撤约 58.1%" and "58%" both validated.
- The written-precision half unit is applied in the units the figure was
written in. A percent figure is compared only against the percentage-point
derivation; running it against the fraction gave an integer percent a
0.5 band in FRACTION units — 50 percentage points — so "区间收益率约 0%"
validated against a real +58% move.
- A STRATEGY-subject metric gets neither exemption. "回测显示该策略最大回撤
5.9%(从 1.180 跌至 1.110)" left a session with no backtest in it at all, on
the exemption written for a price drawdown against an observed high; the
subject word (策略 / 回测 / 组合 / 净值 / strategy / backtest / portfolio /
NAV) is what decides, and "股价较 5 月高点 1.053 元已回撤约 37%" keeps it.
- The endpoint exemption is applied figure by figure and the derivation
exemption is measured in the clause the claim was measured in. One correct
ratio used to carry every percentage in its clause out with it ("…down about
37% from the 1.053 May high with a max drawdown of 60%"), and any true
arithmetic anywhere on the line grounded a metric in the next clause.
Release path
- Cuts are located by the clause's character SPAN, recorded on the issue,
not by substring-searching the normalised claim text. Measured before:
a price written "1,450.50" was unlocatable and the release silently fell
back to the refusal; a metric table ("| 最大回撤 | 12% |") likewise, because
the claim is the synthetic "label: value"; and a bare cell claim "1.10"
matched inside "21.10 亿元", rewriting an untouched turnover figure into
"2(略※)". All three release correctly now, and the substring fallback is
deleted rather than left carrying a rationale the span had already settled.
- After cutting the flagged spans, the same figure is swept out of the rest
of the document. Before: "建议买入价 1.30 元" was cut while "| 第一档 | 1.30 |"
and "- 关键位:1.30 / 1.45" survived under a footnote asserting the figure
had been removed. Two kinds of occurrence are never swept — one the ledger
observed, and one sitting inside a valid formula. The formula protection is
scoped to its own character SPANS: scoped to the value, the rejected entry
price 0.95 survived in "| 第一档 | 0.95 |" as soon as the model also wrote
"基于收盘价 1.171 × 0.95 = 1.112" beside it, protected by the derivation. A
percent literal this same gate accepted in the same run is protected too —
a validated "区间收益率约 5.5%" was cut and footnoted as unmatched because an
unrelated "策略年化波动率 5.5%" was flagged in the next clause.
- A figure the run observed is no longer reported as an invented analysis
metric, in prose or in a table cell. "最新收盘 1.171 元且策略最大回撤 12%"
reported 1.171 (the first unmatched number), so the release cut the
correctly quoted close and footnoted it as unverifiable.
- The released document is validated WITH its footnote; the footnote prints
the symbol the way the answer spells it (562500.SS, not the canonical
562500.SH) and a large price in full (".10g", not "%g"'s 1.23457e+06).
- The redaction footnote is the only redaction statement in the document. A
draft writing its own "※ 略去 0 处……" shipped two contradictory footnotes;
both it and any marker pasted into the body are stripped before the cuts.
- The marker follows the script of the LINE it cuts, so a Chinese report
containing an English table no longer releases "| First |(略※) |", and the
column padding after a table pipe survives.
- 元 is only swallowed when it is a unit: "0.95 元宵节后关注" no longer
becomes "(略※)宵节后关注", and 元/股 is one compound unit — cutting half of
it left "建议买入价(略※)/股".
- The release path's own rechecks run through _validate(record=False).
One successful redacted_release raised validation_count 1 → 2 with no model
round, shrinking the revision budget and overstating the rejected-draft
count in the run reason and the artifact; the safe_fallback branch now
captures its count where the redacted branch does. The zero-round
provenance repair takes the same non-recording path, and the document that
actually shipped is recorded once under "released" so the artifact carries
evidence for the fail-closed promise instead of nothing at all.
- repair_provenance fills in the source and the currency only.
canonical_symbol_not_surfaced was repaired too: a draft reading
"贵州茅台(雅虎,CNY)最新收盘价 1.171 元" was released in zero model rounds
under a note naming 562500.SH. Neither is a draft carrying a price COLUMN no
validator reads ("| 档位 | 挂单价 |"): the note says where this run's prices
came from, and attaching it in zero rounds spends the round that was the
model's last chance to drop that figure.
Verification
- tests/test_grounding_release.py: 28 → 83 cases (144 with parametrisation),
every behaviour two-sided.
- Full suite green: 12903 passed, 138 skipped, 0 failed.
- 27 mutants, 26 killed, including one per finding above. The survivor is
equivalent by construction: with the analysis derivation exemption now
measured on the clause's own values, its `_DERIVATION_RE` keyword pre-filter
can be widened back to the line without changing any verdict.
- Real deepseek-v4-pro E2E on 159516.SZ (run 20260909_223146_43_233953,
10 calls / 551k tokens): status success, NOT degraded, one correction round,
and the released answer names an entry price (0.650, the observed
2026-09-04 intraday low) — the question the screenshot run refused.
The 0.1.15 entry said the guard "caught a fourteenth declaration site that
the eleven it was written for did not cover". That is wrong, and it credits
the guard with the opposite of what makes it good: the `Layout.test.tsx`
`app.version` mock has been in its enumeration since the guard shipped with
0.1.14 — `git show 7329cb09:agent/tests/test_release_version_consistency.py`
names it. It caught a stale value at a site it was explicitly written to
watch, which is the whole argument for enumerating declaration sites instead
of spot-checking remembered ones.
Rolls up 551 commits / 162 merged pull requests since 0.1.14, from 35
contributors.
Served price frames now stamp the adjustment caliber they carry, and a
source whose caliber was never measured against a live payload reports
`unknown` rather than guessing one. The Alpha Zoo's NaN contract moved out
of the individual alphas and into the registry: an alpha branching on an
`np.where` comparison never sees a NaN, so blanking every declared input on
one bar left 84 of 462 alphas returning a number anyway, each consumed as a
real signal because `dropna()` is what keeps a gap out of an IC. The same
sweep afterwards finds none. The futures loader chain was the last instance
of that class and closes in this release.
New markets and surfaces: UK equities (LSE .L/.IL, SDRT modelled as a
purchase-side-only duty), Zerodha Kite Connect as the fourteenth broker
(paper plus read-only, since Kite exposes no runtime live discriminator), a
read-only multi-broker portfolio across Web / REST / CLI / agent tool,
fifteen Quant Library additions, Brazilian Portuguese as the eighth locale,
read-only Nobitex and Wallex, and an offline evals harness that emits
NOT_EVALUABLE rather than passing when instrumentation is absent.
`test_release_version_consistency.py` caught a fourteenth declaration site
the eleven it was written for did not cover: the `app.version` mock in
`frontend/src/components/layout/__tests__/Layout.test.tsx`. The comment in
`test_cli_update.py` that named the current version is de-versioned here —
a version in a comment has no guard and goes stale at the next bump.
Reader-facing counts were re-measured rather than incremented: 74 MCP tools,
107 registry tools, 10 engines, 90 skills, 462 alphas, 27 data sources, 30
swarm presets, 14 broker connectors. None had drifted.