You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The ta-lib crate (ta_codegen/output/rust/library/, v0.8.1) builds, packages and passes
every gate it has. This is the single checklist of what is still open before publishing —
the Rust analogue of #172.
What was verified first (so the list below is scoped correctly)
Does anything test the code we would publish? Yes — unlike Java. The Rust JSON-RPC
server links the shipped crate rather than embedding a copy of it
(tools/Cargo.toml:14 ta-lib = { path = "../library" }; tools/src/bin/ta_codegen_serve.rs:7 use ta_lib::{Core, ...}; zero inlined indicator bodies against 1514 core.* calls). So --codegen, --xlang-hash, server_verify and stream_verify all exercise the crate
source itself. Contrast project_java_server_embeds_core. The untested thing is
packaging, not code.
Can it be published today? Mechanically, yes. cargo package succeeds for both crates;
the ta-lib tarball is ~858 KB gzipped (limit 10 MiB). Both names are already owned
(mario4tier + team github:ta-lib:rust-crate-io-owners), and ta-lib-dispatch 0.1.1 is
already on crates.io (2026-08-03) byte-identical to dispatch/src/lib.rs, so the =0.1.1
pin resolves.
Is the declared MSRV honest? Yes, and it is not arbitrary: cargo +1.86 check -p ta-lib
passes, +1.85 is rejected by cargo's own gate, and 1.86 is exactly where safe #[target_feature] fns (RFC 2396) stabilized — which is what the 26 FMA clones are built
on. It is true but unguarded.
Is aarch64 covered? Yes — dev-nightly arm64-linux (dev-nightly-tests.yml:517) builds
and value-tests the Rust server on ubuntu-24.04-arm, exercising the #[cfg(not(target_arch = "x86_64"))] arm in all 26 dispatch files.
So the code is covered. What is not: packaging, the publish procedure, and ~12 public-API
shapes that freeze the moment 0.8.1 lands.
Status audit, 2026-09-24
0.8.1 shipped without the crate; the first publish is 0.8.2 (VERSION on dev). Read
"0.8.1" below as "the first published version". Pre-flight re-run on dev7d2518e36:
clippy, lib/integration tests, 628 doctests, rustdoc -D warnings, rust_package_check.py and cargo publish -p ta-lib --dry-run all pass, the last one on
its own now that dispatch 0.1.2 is live. The published dispatch 0.1.2 source is
byte-identical to dispatch/src (E9 holds today, still unguarded).
Ticked: D9b (website index and install page list Rust; top-level README f3e35305e), D10 (61d6df629), E5b (main-nightly now runs the debug-profile, arm64 and MSRV legs).
E8g is half done: SECURITY.md exists, issue templates do not. Runbook refreshed
(61d6df629, f3e35305e); E7 is its first-publish step RP7.
Still gating the publish: B2, B3, B4. A6 and C8b decided the same day (see the items).
Status audit, 2026-09-13
C16 LANDED on dev 2026-09-18 (36cd8ac1f): batch and lookback names follow docs/naming-spec.md, and the function-keyed enums keep their spelling. C8b is what is left
of section C. Nothing else re-audited.
Status audit — 2026-08-31
Re-checked every item against devbbe63f18e, after the two-day run of PRs #288–#319. Twelve items ticked, five split, one added. What is left is small and almost entirely
yours.
Ticked since the 08-27 audit: C8a (#313), C9 (#295), D1/D2 (#312), D4 (#305, by a different route than the item proposed — see below), D6 (#288 + #305), D7 (#279), E1 (#292), E2 (#298, moved to the nightly by #300), E3 (#309), E4 (#290), E6 (#314).
Split, because each was half-resolved:
was
done
left
C8
C8a — the panic, gate-pinned (tests/empty_range.rs)
C8b — the inclusive (startIdx, endIdx) signature still cannot spell "no bars"
D5
D5a — the drift mechanism: counts and the install requirement are interpolated, and the README is compiled (#297)
D5b — the three shared Rust samples are authored once (#332); the prose around them is still written per page
D8
D8a — paths, #[doc(alias)] on the batch + lookback tiers (#319), the REAL_MIN/INTEGER_MIN copy-paste, and the *_DEFAULT consts (now documented and cross-linked from lib.rs:37)
D9a — both crate READMEs carry crates.io + docs.rs badges
D9b — website/src/README.md (SEO description included), website/src/wrappers/README.md and the top-level GitHub README still say nothing about Rust
E5
E5a — clippy, rustdoc, generator tests, crate doctests, crate lib+integration tests, rust_package_check.py and regen-check now run on main (#306)
E5b — --rust-debug, rust_stream_debug.py and arm64 are still dev-only
E8
E8a–E8d, E8f — see the item; the three real gaps it named (no open_and_fill doctest, no bounds preamble on open_and_fill, the unchecked Cargo.lock) are all closed
E8e, E8g–E8h — the dispatch source pin, SECURITY.md/issue templates, and the 45 candlestick doctests that do not fire
D4 closed by removal, not by KaTeX.#305 trimmed Formula/Notes out of rustdoc
altogether and links ta-lib.org instead, so grep '\\frac\|begin{aligned}' src/ta_func/*.rs
is now 0 files. There is no math left on docs.rs to render, and no [package.metadata.docs.rs] is needed. The KaTeX half is moot rather than outstanding.
Added: E9 — nothing pins the dispatch/ tree to the published 0.1.2. Was buried in
E8's prose; promoted because it is now a live hazard rather than a future one: 0.1.2 is
published and immutable, the library pins =0.1.2, and rust_package_check.py packages
the local copy without ever comparing it to the registry's.
What actually gates the publish is unchanged and is all yours: A6 (no publish
credential — actions/secrets still reports total_count: 0), B2 (ta-lib 0.1.0/0.1.1/
0.1.2 all still unyanked, verified against the registry today), B3 (cargo publish the
crate), B4 (all six website/src/api/*/README.md still carry the "Not yet released —
Q1 2027" banner) and D10.
Status audit — 2026-08-27
Re-checked against devccacb4552, after #268 finished the streaming opener's
argument contract:
C7 is done. An opener no longer collapses four causes onto BadParam. A
history shorter than lookback + 1 is RetCode::InsufficientHistory, an empty
one is OutOfRangeStartIndex, one past MAX_INDEX + 1 is OutOfRangeEndIndex; what is left on BadParam is the argument contract —
parameters, buffer lengths, aliasing — which is what the batch tier answers for
the same faults. docs/error-handling-spec.md §2.3 states the whole tier, and
its Appendix D now has no open items in any backend. Ticked below.
The corpus is 176. SMI, VWAP and the rest landed after the 2026-08-18 audit.
The 174s below are measurements from when each item was written and still read
as "the whole corpus"; C9's literal is corrected in place, since it is part of a
public type.
Status audit — 2026-08-18
Re-checked every open item against devc53acafa6. Five corrections, no items added or
removed:
C3 is done — CandleSetting.range_type is the RangeType enum, shipped dev 7dfa4b961 (2026-08-15). Ticked below.
The corpus is 174, not 168. AC, AO, CMF, CMOU, EFI, HMA, MARKETFI, NVI, PVI, PVO,
QSTICK, VWMA and WAD landed since this page was written. Every "168" below means "the
whole corpus" and still reads correctly with 174 substituted; only C9's, where the number
is part of a public type, is corrected in place — and that it moved at all is C9's own
argument.
D3 was reworded but is still false.47bf967ec replaced the old sentence; the page now
says "RetCode implements std::error::Error, so results compose with ?"
(website/src/api/rust/README.md:182), directly under a table of the batch return
codes. impl Error is true, but the batch tier returns a bare RetCode, so core.SMA(..)? still does not compile. ? composes only on the builder and *_open
tiers, which return Result<_, RetCode> — which is exactly what the crate's own doc
comment on impl std::error::Error for RetCode says. Fix the website line, not the crate.
D8 has a second sub-defect fixed, beyond the path one already recorded: REAL_MIN and INTEGER_MIN no longer carry the copy-pasted "widest value" sentence. Still open there: #[doc(alias = "TA_SMA")] lands only on the stream type (sma.rs:233), not on Core::SMA; and the #![forbid(unsafe_code)] line still omits dispatch's one unsafe.
C8 is NOT resolved by C14, despite the shape change. C14 rewrote the OUTPUT
side; C8 is about the INPUT range. startIdx: usize, endIdx: usize is still an
inclusive pair, so the batch API still cannot express an empty input range. Both
halves of C8 remain open.
Correction to C8's own wording, measured 2026-08-18. C8 says the natural call (0, 0) panics. It does not: (0, 0) is a valid call asking for one output at
bar 0, and driving all 174 functions through the abstraction layer at (0, 0)
gives 30 with count == 1 (zero lookback) and 144 with count == 0 (the lookback
exceeds the range) — no errors and no panics. The panic needs BOTH a zero-lookback
function AND an empty input slice: ADD(0, 0, &[], &[], ..) traps, while SMA(0, 0, &[], 30, ..) returns Ok(count: 0) because its lookback of 29 makes
the assert's _assertStart > endIdx arm short-circuit. So the item is narrower
than written — it is about the empty slice, not about (0, 0).
(0, 0) was nevertheless untested in every backend, and now is not: b2161b156
pins every ordered pair from {0,1,2,3} into the --xlang-hash sweep for all 174
functions against all four servers. Sabotage-proved — a count == 0 fault is caught
on all 144 functions that can produce an empty result, where the pre-existing random
subranges caught it on only 119.
The three ports' OutRange are now the same (4ff662434): Java's endIdx(),
C#'s EndIdx and Rust's end_idx() all returned begIdx + count — an EXCLUSIVE
index — while every batch entry point takes an INCLUSIVE endIdx parameter. One
name, two conventions, and the failure mode is a silent off-by-one. All three removed;
each had a single caller, a test asserting the accessor equalled its own definition.
Member sets now match: begIdx/count, EMPTY/Empty, isEmpty()/IsEmpty/is_empty().
A1 (DONE 2026-08-11, dev d51371b) — the README install snippet says ta-lib = "0.6". That README is the
crates.io landing page, and it is the first copy-pasteable line on it. ^0.6 resolves to
nothing: published versions are 0.1.0/0.1.1/0.1.2 and the next is 0.8.1. library/README.md:18; hard-coded at ta_codegen/generator/src/main.rs:2135. The website
copy already has it right (website/src/api/rust/README.md:55 → "0.8"). One-line
generator fix.
A2 (DONE 2026-08-11, dev d51371b) — "161 indicators" in three shipped places; 168 ship. Cargo.toml:6 (the crates.io search-result tagline), lib.rs:3, README.md:6 — from main.rs:1967, :2032, :2123. Ground truth 168 two ways (ls ta_codegen/input minus helpers/types/enums.yaml; grep -c 'pub fn .*_lookback'). "61 candlestick patterns"
is correct. Interpolate funcs.len() rather than re-typing a literal — the same two
hard-coded copies are what let it drift (readme-vs-lib.rs duplication, below).
A3 (DONE 2026-08-11, dev d51371b) — no LICENSE file inside either .crate.tar tzf ta-lib-0.8.1.crate → 8
entries plus src/ta_func/, no LICENSE; same for the published ta-lib-dispatch-0.1.1.crate (checked from the registry cache, not a local build). A .crate is a source redistribution and BSD-3 clause 1 requires the notice to travel with
it. license = "BSD-3-Clause" is only an SPDX label; cargo injects nothing. Neither
manifest has include/exclude, so copying the root LICENSE into library/ and dispatch/ is enough. Same standard the project applied to the jars in Java: remaining loose ends before the first Maven Central publish #172 A1. Sting: dispatch 0.1.1 is immutable, so fixing it means a dispatch 0.1.2 published
before ta-lib is ever published, which also bumps the =0.1.1 literal at main.rs:1883
and main.rs:1985. Bundle A4 into that same 0.1.2 — it is the one free window.
A4 (DONE 2026-08-11, dev d51371b) — ta-lib-dispatch's manifest has no keywords and no categories
(dispatch/Cargo.toml, 15 lines) — unreachable by crates.io search, listed under no
category. Fold into the 0.1.2 from A3.
A5 (DONE 2026-08-11, dev d51371b — it was TWO defects: the C13 verbatim rename had also upper-cased the slug, so /functions/SMA 404s too; slug is now name.to_lowercase()) — 168 dead "Further reading" links in the shipped rustdoc. Every function page
ends in https://ta-lib.org/functions/<name>/ — trailing slash. Verified live: /functions/sma/ → 404, /functions/sma → 200. The site builds flat files
(dist/functions/sma.html), not sma/index.html. Rust-only: the same grep over output/java, output/csharp, src/ta_func, website/src returns 0. Drop the slash at backends/rust_doc.rs:143. One character, 168 pages.
A6 (DECIDED 2026-09-24, runbook b775a65d6): a maintainer's personal crates.io token, scoped publish-update to the two crates, with an expiry; manual cargo publish, like Java. No CI secret, no Trusted Publishing. The team owner is the bus-factor answer. Token created and cargo login done 2026-09-24 (ta-lib*, publish-update, expiring). Original: A6 — no publish credential exists.gh api .../actions/secrets → total_count: 0;
the only secret referenced anywhere is secrets.GITHUB_TOKEN; crates.io reports trustpub_only: false for both crates. Decide before writing any publish step: manual cargo publish from a maintainer machine with a personal token (works today, no repo
change), or crates.io Trusted Publishing (register the exact workflow filename per crate,
add permissions: id-token: write, no long-lived secret). Publish order is dispatch, then ta-lib — currently recorded only as a comment inside a generated file
(library/Cargo.toml:22-25).
B. Maintainer / account work (the #172 category-B analogue)
crates.io names ta-lib and ta-lib-dispatch owned by the project.
ta-lib-dispatch 0.1.1 published and byte-identical to the tree.
B1 — DECIDED 2026-08-12: keep lockstep.0.8.1 is hard-locked to the C VERSION in both directions (scripts/utilities/versions.py:309-320 rewrites
library+tools; :615 hard-fails pre-release on mismatch — dispatch is exempt and already
decoupled). Two consequences nobody has written down: (a) no Rust-only patch is possible
without cutting a full C release, which is why A1/A2/A3/A5 are pre-publish rather than
"fix later"; (b) the C cadence is patch-heavy (0.6.1→0.6.4, 0.7.1, 0.8.1) and ta-lib = "0.8" matches any 0.8.x, so a C-motivated 0.8.2 carrying any section-C fix would
auto-upgrade into every consumer's build as an API break in a patch. Decision (maintainer,
2026-08-12): keep lockstep. One number across C, Rust and Java — a release in practice
changes something every backend shares, so they are one generated library, not four that
happen to ship together, and publishing the crates becomes part of every release rather
than a one-off. The obligation: bump VERSION's MINOR whenever any backend breaks its
API, even when a C patch motivated the release. cargo-semver-checks (E7) is the
mechanical guard, once a published version exists to compare against. Recorded at get_version_string in scripts/utilities/versions.py and in docs/rust-publish-runbook.md.
B2 — decide whether to yank ta-lib 0.1.0–0.1.2. They are an unrelated third-party
FFI wrapper (virtualritz/ta-lib-rs), 5097 downloads, 0 reverse dependencies, unyanked. ta-lib = "0.1" will keep resolving to a different library forever, and docs.rs/ta-lib/0.1.2 stays live beside the new docs. Yanking breaks no published crate
and does not break existing lockfiles — it only blocks new^0.1 resolutions. Not
order-dependent; can be done release-day.
B3 — dispatch 0.1.2 PUBLISHED 2026-08-12, docs.rs green; ta-lib 0.8.1 still pending. Original: B3 — cargo publish dispatch 0.1.2, then ta-lib 0.8.1, then verify docs.rs built.
B4 — flip the website in the same release.website/src/api/rust/README.md and website/src/api/rust/stream/README.md still carry the "Not yet released — Q1 2027"
banner; website/src/install/README.md:16 says "none is released yet" and the Native table
lists only C/C++ (no install/rust/ page exists). The site deploys from main on push.
B5 (DONE 2026-08-12, dev 671d34a) — no Rust publish runbook. Java has docs/java-deprecation-runbook.md; Rust has
nothing. The two-crate publish order lives in a code comment.
C. Public-API decisions — cheap now, semver-breaking after 0.8.1 ships
These are the items with a real deadline. Everything else on this page can be fixed in a
later version.
C1 — #[non_exhaustive] on the public enums. DONE 2026-08-09, dev 1353c8737.
Shipped exactly as decided: the five below marked, the three abstract-layer types left bare,
and Compatibility untouched (it is pub(crate), where the attribute means nothing). The
one downstream cost landed too — the JSON-RPC server is a separate crate, so its RetCode
match needed a wildcard arm; all six variants stay mapped explicitly, so that arm is dead
today.
Cost is one _ => arm downstream; nothing else (construction, comparison and if ret != RetCode::Success are unaffected).
Mark #[non_exhaustive] — FuncId (168, rust_abstract.rs:36), FuncUnstId
(25, templates/rust/types.rs:51), RetCode (6, types.rs:3), Group
(10, rust_abstract.rs:961), CandleSettingType (12, types.rs:161). FuncId is the non-negotiable one — without it indicator ta_codegen: stream bodies emit duplicated NULL-check blocks (22 sites) #169 is a major bump forever. FuncUnstId already carries Unused1/12/15/22because the id space had to stay
stable, i.e. it has demonstrated it moves. RetCode is load-bearing for C7: without
it, splitting insufficient-history out of BadParam becomes "never". It is also where the
cost is lowest, since Success shares the enum with the errors so real code already writes if ret != RetCode::Success. Precedent: std::io::ErrorKind.
Leave exhaustive, with a comment saying it is deliberate — InputType (3), OutputType (2), OptInputType (4). These are the abstract layer's type
system, they mirror C enums unchanged since 2002, and matching them exhaustively is
the use case (dispatching a parameter binder, rendering a parameter widget). #[non_exhaustive] here forces an arm that can do nothing meaningful.
On OptInputType specifically: variant-level #[non_exhaustive] is also declined —
it would forbid struct-pattern destructuring without .., which is the primary way the
payload is read. The four payload shapes are frozen; adding a field to one is breaking
either way.
C13 — one verbatim name per function, in every language. DONE 2026-08-09, dev 9145c932a.
The name in ta_codegen/input/<dir>/<dir>.yaml is now the sole identity, spelled
verbatim everywhere. C alone prefixes TA_; suffixed variants take an underscore. So TA_SMA / TA_SMA_Lookback are SMA / SMA_Lookback in Rust, Java and C#, where the
crate had sma / sma_lookback. Enum members follow (MAType.SMA, FuncUnstId::EMA),
and the unstable-period wildcard is ALL in all three — Rust had spelled it FuncUnstAll, Java and C# All, C TA_FUNC_UNST_ALL.
Why it had to land before 0.8.1 and not after: it renames every public method and
every enum member in the crate. Post-publish it is a major-version migration; today it
costs nothing, because Rust, Java and C# are all unpublished.
The YAML camel_case field it removes was a hand-authored second identity on all 168
functions. It earned its keep on four, and had already frozen two typos into shipped
API (CdlHignWave, CdlSeperatingLines). Two mutually inconsistent PascalCase manglers
disagreed on eight FuncId variants, and one deliberately aliased STOCH_RSI and STOCHRSI onto a single identifier.
Also gone: camelCaseName from TA_FuncInfo and <CamelCaseName> from ta_func_api.xml — a public C struct layout change, so C consumers recompile rather
than relink; Java's FunctionInfo.javaMethodName(); C#'s FunctionInfo.MethodName; pascal_name and the authored c_name from enums.yaml (now c_prefix + {name, value}); five manglers; and Registry::java_base/csharp_base, collapsed into
one name_of. CHANGELOG entries added under 0.8.1 ### Changed.
Two gates were repaired rather than adjusted, both found by the pre-commit review: RustBackend::rendered_name now lower-cases a function name because the module path
still does (without it a function named LOOP passed the gate and emitted mod loop; —
the Java mangler used to catch that); and the unstable-period table now pins each
function's FuncUnstIdordinal, authored independently in enums.yaml, instead of its
variant name, which unst_row derives from the function's own name and so could never
disagree.
Verified: generator 678 tests, both clippy gates at -D warnings, crate lib + doc tests,
warning-free rustdoc, C library and all four JSON-RPC servers, the Java and C# suites, ta_regtest --codegen across all four languages, --xlang-hash bit-identical over
237,500 cases, idempotent regeneration, and synth_gate.py.
C12 — abstract-layer type naming. DONE 2026-08-08, dev 94a3f065a.
Shipped: abstract_api::OptDomain -> OptInputType, field OptInputInfo.domain -> .kind. One type, one field.
The original six-name proposal (*ParamInfo/*ParamType) was REJECTED — its premise
was false. Java already ships InputInfo/InputType/OutputInfo/OutputType/ OptInputInfo/OptInputType; C# ships InputInfo/OutputInfo/OptInputInfo/ OptInputDomain. Rust already matched Java on five of six names, so adding a Param
infix would have moved five agreeing names away and made Rust a fourth vocabulary for one
model — the exact problem it claimed to fix. (The stated readability gain was also
unreal: InputInfo and OptInputInfo differ by the prefix Opt, and so do InputParamInfo and OptInputParamInfo — Param is common to both.)
OptDomain was the single genuinely asymmetric name, and renaming it alone reaches the
Info/Type symmetry goal and converges Rust to Java. .kind matches the crate's own InputInfo.kind/OutputInfo.kind; Java spells all three type, which Rust cannot use
as a bare field.
Note for future readers, recorded in the type's doc comment: structurally this type is
C#'s OptInputDomain (fused tag + payload, exhaustively matchable). Java's OptInputType is a flat tag enum with the payload flattened onto the record. Rust takes
Java's name and C#'s shape; only one of the two could be matched.
The generator IR abstract_rows::OptDomain is unchanged — it is shared with the Java and
C# emitters and is not a public name in any binding.
Verified before commit: clippy -D warnings, 30 lib tests, 341 doctests, warning-free
rustdoc, the full generator suite, ta_regtest --codegen --language=c,rust 161/161, and
an RPC probe of TA_GetOptInputParameterInfo. Wire contract untouched.
C3 (DONE 2026-08-15, dev 7dfa4b961) — CandleSetting.range_type is the RangeType enum, TryFrom<i32> in the library rejecting anything else, matching Java and C#. Original: C3 — CandleSetting.range_type: i32 is a public field of a public struct with the
enum in a doc comment (templates/rust/types.rs:113-120). range_type: 7 is constructible
and silently means "none of the above" (types.rs:264 _ => 0.0). Java ships RangeType.
A public field's type cannot change after publish.
C4 (DONE 2026-08-11, dev 07f57da — REMOVED, not pub(crate): they have no callers at all, so pub(crate) leaves two dead_code items and fails the -D warnings gate) — two internal items are pub. (Was three. Core::ema_private — the
unvalidated variant whose own rustdoc says "its only callers are the guarded bodies" —
is pub(crate) as of Introduce TA_MAX_INDEX: bound the valid startIdx/endIdx range in all four backends #180, dev 4e9cf7f0b: it skips the validation prologue, so a pub
there was the one backend where a caller could reach an indicator with no TA_MAX_INDEX
bound. C's equivalent is file-static, Java/C#'s are package-private/internal.
Still open: Core::ta_candlerange and Core::ta_candleaverage (templates/rust/types.rs:259, :271 —
C macro names, ta_ prefix redundant inside ta_lib, raw rangeType: i32, non-snake-case avgPeriod, and zero callers anywhere in the crate). pub → pub(crate) is a removal
after publish. Zero #[doc(hidden)] exists anywhere in the crate.
C5 (DONE 2026-08-18, 5a65f37a1) — mod ta_func is private, the glob stays, and the eight crate-root constants moved to where Java already puts them: Core::{REAL_DEFAULT, INTEGER_DEFAULT, REAL_MIN, REAL_MAX, INTEGER_MIN, INTEGER_MAX, MAX_INDEX} and FuncUnstId::COUNT. Original: C5 — pub mod ta_func + pub use ta_func::* gives every public type two permanent
paths (lib.rs:80-82), one of them the C source-directory name — ta_lib::ta_func::Core
stutters. Make the module private and keep the glob. Also: the glob drops REAL_MIN, REAL_MAX, INTEGER_MIN, INTEGER_MAX, FUNC_UNST_COUNT at the crate root
(types.rs:90-109) — very generic names for "the widest value a TA-Lib optional parameter
may take". Associated consts or a params module; both moves are semver-major later.
C6 — batch tier returns the wrong range code. DONE 2026-08-08 via Introduce TA_MAX_INDEX: bound the valid startIdx/endIdx range in all four backends #180, dev 4e9cf7f0b.
All 168 batch entry points return OutOfRangeStartIndex for endIdx < startIdx
(sma.rs:159, add.rs:136); C returns TA_OUT_OF_RANGE_END_INDEX (ta_SMA.c:86) and the
crate's own abstract tier returns OutOfRangeEndIndex (abstract_api.rs:3005) — two public
tiers, two answers, and the one users call is the one that disagrees with C.
Nothing gates it: server_verify.c:512 skips every non-Success call ("parameter validation
is implementation-specific"), and ta_codegen_serve.rs:13415 reimplements C's guard inside
the server, so the crate's answer never reaches the driver. Flipping breaks no gate.
Shipped: flipped to OutOfRangeEndIndex as part of Introduce TA_MAX_INDEX: bound the valid startIdx/endIdx range in all four backends #180 — the same two lines of
prologue also gain the TA_MAX_INDEX bound, which is what gives OutOfRangeStartIndex a live
producer again (startIdx > TA_MAX_INDEX). Shipping them separately would be two behaviour
changes where one will do. The earlier "C6b" idea — returning a code instead of asserting on
out-of-slice indices — is not part of this; it needs slice lengths C does not have, and
stays in C8.
Not on the original list; it is the change C5 and D3 landed inside, and the
largest public-API decision taken before publish. pub fn SMA(startIdx, endIdx, inReal, period, outReal) -> Result<OutRange, RetCode> — the two &mut usize
out-params are gone and come back by value, unreachable on failure. OutRange
moved to the crate root; its second field is count, matching Java's and C#'s OutRange(begIdx, count).
Result<(), RetCode> was considered and rejected: RetCode contains Success, so Err(RetCode::Success) would be constructible, and Result<(), _>
discards the two values the call produces, leaving the out-params — the part that
actually reads as foreign. RetCode therefore keepsSuccess: the internal
tier must be able to report it, and Java and C# both ship a public RetCode
carrying it.
Cross-indicator calls deliberately did NOT migrate. The guarded body survives
as pub(crate) fn <N>_Internal(...) -> RetCode, exactly as Java routes them to SMA_Internal and C# to its RetCode overload. 19 of the 33 call sites pass the
callee their own &mut outBegIdx/&mut outNBElement and read them back, and four
fold "success with zero output" into the same conditional as the error, which ?
cannot express. So all 174 transcribed bodies are byte-identical and --xlang-hash
is evidence rather than a tautology. FMA dispatch stays on _Internal.
Also in the same commit: RetCode::as_c_int() moved into the library, closing a
fail-open — the server's mapping ended in _ => 5000, forced because RetCode is #[non_exhaustive] and the server is a downstream crate, so C7's future variant
would have reported as InternalError rather than failing to compile. And a new
gate, rust_public_entry_documents_exactly_its_parameters, pins each public
wrapper's # Arguments list to its signature; it caught the out-param bullets this
change left on all 174 rustdoc pages, which nothing else could see.
C15 (DONE 2026-08-19, 62de52a26) — *_OpenAndFill returns Result<(<N>_Stream, OutRange), RetCode>. Original: C15 — *_OpenAndFill still takes &mut outBegIdx / &mut outNBElement, so
the crate now ships two conventions for the same answer: batch returns an OutRange, OpenAndFill fills out-params. Java reports it as fillRange() on the handle and C#
as FillRange, so Rust is the only port with the split. Pre-existing rather than
introduced by C14 (verified: rust_stream.rs's whole diff there was one Self::MAX_INDEX hunk), but it is a public shape that freezes at publish.
That leaves the crate with no public &mut usize at all: every remaining outBegIdx parameter is on a pub(crate) fn (_Internal, _OpenCore, _OpenAndFillInternal).
Java and C# are deliberately not copied here. Both hang the range off the handle
and return it bare. Java has no tuple, so a per-function result record was the only
alternative; Rust's _Open already returns Result<(Stream, value), RetCode>, so (Stream, OutRange) composes two conventions the crate already has instead of adding
a third. A handle field would also go stale across Clone — Java pins exactly that
(StreamSmokeTest: a copy carries the original's fill range), and in Rust cloning a
stream is the documented way to fork it.
One signature, three bodies. The shared wrapper declares the pair as locals after its
guards and folds them in at the return, reading like the batch wrapper. The two tiers
exempt from the OpenCore merge hand-roll theirs: MA carries the range out of the
dispatch match itself — one arm runs, so the callee's OutRangeis the call's —
rather than round-tripping ten arms through locals, and MAVP builds it at the return.
Evidence. The OpenAndFill leg of stream_verify is the gate, and it compares the
reported range against the batch range: 174 functions, filled array == batch(0, n-1)
bitwise. Sabotage-proven — beg_idx + 1 in one function trips it at exit 87, and it
surfaces on TA_MA rather than TA_SMA, so the dispatch's range-threading is covered
end to end. Rust 161/0 and C 161/0, unchanged; --xlang-hash bit-identical.
Blast radius: excising the *_OpenAndFill function from both trees leaves the
remainder byte-identical in all 174 files, so no batch body, _Open, _OpenCore or update/peek moved. Two generator tests were pinned to the old shape and now assert
the negative (rust_dispatch_open_modes_differ_only_where_intended asserted
"OpenAndFill carries the out-meta pair"), and the hand-written Rust streaming page,
which no gate covers, was updated by hand.
C7 (DONE — TA_INSUFFICIENT_HISTORY + Streaming openers: answer the batch tier's codes, in the batch order (Appendix D items 6 and 13) #268) — *_open collapsed four causes onto BadParam, including "you passed 20 bars for a 30-period SMA" — the normal warm-up
condition every streaming user hits on their first run, indistinguishable from period = 0. Resolved the way the item asked, with a new RetCode variant rather than a
stream-tier error type: short history is InsufficientHistory (all four backends agree,
and it is the library's one recoverable condition), an empty history is OutOfRangeStartIndex and one past MAX_INDEX + 1 is OutOfRangeEndIndex — the implied
index pair of a call over [0, historyLen - 1]. BadParam now carries only the argument
contract, which is what the batch tier answers for the same faults.
C8a (DONE 2026-08-31, test(rust): the empty and sub-lookback input ranges were pinned nowhere, and three backends describe them wrong (#179 C8) #313574b3fd72) — the panic, and the sentence three
backends carried about it. The public tier never reaches the assertion preamble: rust_lang::gen_argument_checks runs its own bounds first and answers BadParam. So
the panic C8 describes lives in <N>_Impl, which is pub(crate) — reachable from the
JSON-RPC servers, not from a pub fn call. tests/empty_range.rs pins the empty series
and the sub-lookback range on the public tier, one function per input arity plus a
period-taking function at period = 1; every call returns rather than panics, and a
panic fails the test outright.
It also retired a false claim that had outlived the code in three backends: java::gen_argument_checks, Core.java's clampedStart and Core.cs's ClampedStart each said Rust applies its _assertStart > endIdx || escape to both
bounds, making the input bound "the one place Java/C# check more than C and Rust do" — Core.cs even named SMA(0, 5, &[], 30, &mut []) as Ok(count 0) in Rust. True of
the _Impl asserts; not true of the public tier, where the escape is taken on the
output bound only and Rust and Java/C# agree. Nothing pinned it, which is how the
sentence survived.
C8b (DECIDED 2026-09-24: keep the inclusive pair.) Rust works like every other backend. docs/error-handling-spec.md §2.2 already specifies it: B1/B2 make every valid range at least one bar, so an empty or too-short input is B5, BadParam where the language has a length (Rust, Java, C#) and undetectable in C. That differs by backend by design, not by defect. Original: C8b — the batch API still cannot express an empty input range. The breaking
half, and the one genuinely publish-gated item left in section C. startIdx: usize, endIdx: usize is an inclusive pair, so there is no value of it that means "no
bars": an empty series has no successful spelling at all, and the idiom every doc
teaches — data.len() - 1 — underflows on an empty Vec before the call is even made.
C answers this with a code because it has the same signature and the same problem;
Rust does not have to inherit it. Changing to a Range, or to endIdx: Option<usize>, is only possible before publish. 28 functions have an
unconditional zero lookback (acos ad add asin atan avgprice bop ceil cos cosh div exp floor ln log10 medprice mult nvi obv pvi sin sinh sqrt sub tan tanh typprice wclprice)
and every period-taking function joins them at period = 1, so this is the whole
corpus, not a corner.
C9 (DONE 2026-08-31, fix(rust): FUNCS is a slice, so the registry's size is not public API (#179 C9) #2956d7851137) — FUNCS is a slice, so the registry's
size is no longer part of a public signature. Original: C9 — pub static FUNCS: [FuncInfo; 176] (abstract_api.rs:398; it read [FuncInfo; 168] when this was filed) puts the count in a
public type. Mostly subsumed by C1 (adding ta_codegen: stream bodies emit duplicated NULL-check blocks (22 sites) #169 already needs a FuncId variant), but &[FuncInfo] or a funcs() accessor is free now. Note MAX_INPUTS/MAX_OPT_INPUTS/ MAX_OUTPUTS (:2605-2609) are corpus-derived values that will also move.
C10 (DONE 2026-08-11, dev 07f57da) — #[must_use]. Neither RetCode nor any of the 168 batch methods carries it
(168 *Stream structs and 168 peek do). core.sma(...) with a bad period returns BadParam, writes nothing, leaves the caller's zeros in place, and warns at no level. One
attribute on the enum in templates/rust/types.rs covers all 1020 methods. Technically
additive later — listed here because the first cohort of users learns the API either way.
C11 (DONE 2026-08-11, dev 07f57da) — derive gaps.ParamHolder (abstract_api.rs:2689) has no #[derive] at all,
so no Debug on the type users bind arguments into. RetCode/FuncUnstId/ CandleSettingType lack the Hash, PartialOrd, Ord that FuncId has — so HashMap<FuncUnstId, i32>, the obvious way to track per-function unstable periods, does not
compile. No PartialEq on Core/CoreBuilder/CandleSetting. All additive, all trivial.
C16: the Rust names the naming spec changes. LANDED on dev 2026-09-18. core.sma(..), core.sma_lookback(14), core.ht_trendline(..), and the numerics tier is sma_impl. MAType::SMA, FuncUnstId::HT_DCPERIOD, FuncId::CDL3BLACKCROWS, RetCode::BadParam, CandleSettingType::BodyLong, FuncInfo's "SMA", the case-
insensitive by-name lookup, #[doc(alias = "TA_SMA")] and the parameter names are all
unchanged (spec E1/E2). Java: remaining loose ends before the first Maven Central publish #172 D8 is the Java half; C# followed the same spec.
Verified on the landed tree: the PR gate's fourteen legs — including cargo doc under -D warnings and 613 doctests, which caught 1705 broken [Core::SMA] links and 201
doctests no other gate could see — plus build.py regtest (all four languages 161/161)
and regen-check.
That prediction held: eight assertions went vacuous rather than red, plus stream_ab.py's
anchor again. Each one now builds its needle from the same fold helper the emitter uses.
D. Docs and first impression (all fixable post-release; release day is when it matters)
D1 (DONE 2026-08-31, docs(rust): the crate front page never mentioned the streaming tier (#179 D1/D2) #312cc08e64c7) — the streaming tier is on the crate front
page.README.md and lib.rs both open on it now (grep -ci stream → 4 and 9, from 0
and 1), and the README's examples are compiled as doctests (test(rust): the crate README's examples are claims, and nothing compiled them (#179) #297), so the ergonomic path
is a claim a gate holds rather than prose. Original: D1 — the streaming tier is invisible.grep -i stream over README.md → 0 hits;
over lib.rs → one, an aside inside the FMA paragraph. Meanwhile 168 *Stream types are
flattened to the crate root and will dominate the docs.rs index with no explanation. The
crate's only example is a 7-argument call with two &mut usize out-params — while the
ergonomic answer (let (mut s, _) = core.rsi_open(&hist, 14)?; s.update(bar)) exists,
works, is Clone-able, has peek, and composes with ?. Undiscoverable unless you already
know to look for *_open. Add a "Live data" section to README + lib.rs.
D2 (DONE 2026-08-31, docs(rust): the crate front page never mentioned the streaming tier (#179 D1/D2) #312cc08e64c7) — the crate links the guide. lib.rs:114 and README.md:122 both point at ta-lib.org/api/rust/ and, separately, at
the streaming page. Original: D2 — the crate never links the Rust guide that already exists. website/src/api/rust/README.md (217 lines) answers calling convention, output sizing,
lookback, return codes, abstraction layer, unstable period, candle settings and threading —
better onboarding than anything in the crate. The crate links only /functions/ (per-
indicator formula pages) and docs.rs (the 1020-method wall).
D3 (DONE 2026-08-18, 218d69b83 + 5a65f37a1) — the claim is now TRUE rather than removed: the batch tier returns Result<OutRange, RetCode>, so core.SMA(..)? compiles. Original: D3 — the website claims ? composes with RetCode (website/src/api/rust/README.md:127).
It does not — the batch tier returns a bare RetCode. One line of Markdown, but it is the
first thing a Rust reader checks. (Whether to add a Result/Vec convenience layer is
additive and can wait; splitting Success out of the error enum cannot.)
D4 (DONE — fence half 07d56a53c; the KaTeX half closed by REMOVAL in docs(rust): trim Formula/Notes off rustdoc, link to ta-lib.org instead (#179 D6 follow-up) #305e3fa82678: Formula/Notes are no longer emitted into rustdoc at all, the pages link ta-lib.org instead, and grep 'begin{aligned}' src/ta_func/*.rs is now 0 files. No [package.metadata.docs.rs] is needed.) — LaTeX renders as raw backslash macros on docs.rs. 10 files carry math; RSI and
BBANDS — the two pages a new user opens first — show a wall of \begin{aligned}. In both,
the closing fence sits after the explanatory sentence, so the prose renders as code too
(rsi.rs:97-116, bbands.rs:138-152; emitter rust_doc.rs:668-675). Either [package.metadata.docs.rs] rustdoc-args = ["--html-in-header", "katex.html"], or plain-ify
the math in the Rust doc backend. Closing the fence at the $$ fixes the swallowed-prose
half on its own.
D5a (DONE) — the drift mechanism is closed. The counts and the install
requirement are no longer typed: n_funcs, n_candles and install_req are derived
from funcs and the VERSION file and interpolated into both literals (main.rs
~2338), and funcs is the whole corpus even under --func=, so they are whole-corpus
by construction. test(rust): the crate README's examples are claims, and nothing compiled them (#179) #297 added #[cfg(doctest)] #[doc = include_str!("../README.md")], so
the README's examples now compile under cargo test --doc — deliberately behind cfg(doctest) so the headings never render in the docs and its links stay ordinary
Markdown for crates.io.
D5b — the prose around the samples is still authored per page. The executable
half is closed (docs(rust): the front page's three samples are authored once, not twice (#179 D5b, D8b) #332403f56b37, doc-comment trim df0c6bebb): the three samples both
front pages carry — batch quick start, builder, streaming walk-through — are one FrontPageExample each, and each page's wrapper is added on the way out (//! plus the
hidden Ok for rustdoc, a pasteable fn main for the README), so the two can no longer
differ on code. They already had: the builder sample asserted the setting took on the
README and stopped at build()? in lib.rs, so that doctest proved only that the call
compiles. README.md regenerates byte-identical, which is what shows the renderers
reproduce the pages rather than merely compile.
What is left is the narrative, and it differs by more than markup: lib.rs states the
API shape as bullets carrying intra-doc links, the README as prose with a different
category list and an install section. Converging it means picking one rendering for text
that reads differently in the two places — a maintainer call, not a refactor. The sibling
crate's answer (dispatch/src/lib.rs:3 #![doc = include_str!("../README.md")]) stays
unavailable for the reason recorded at lib.rs:355 — the front page must keep ordinary
Markdown links to render on crates.io, and as crate docs they would be resolved as
intra-doc links.
Also unclosed by docs(rust): the front page's three samples are authored once, not twice (#179 D5b, D8b) #332: the same builder sample is authored twice more in templates/rust/types.rs (the Core and CoreBuilder item docs), both still stopping
at build()?, and once more in website/src/api/rust/README.md, which nothing compiles. FrontPageExample extends to the first two with a ///-prefixed renderer.
D7 (DONE 2026-08-29, docs(rust): document every public enum variant and struct field (#179 D7) #279cd0afd44a) — every public item, variant and struct
field carries its own doc, and #![warn(missing_docs)] is set (lib.rs:337) — warn
rather than deny so a future rustc widening the lint cannot break a downstream build;
the nightly's cargo clippy -- -D warnings is what makes it a gate. Original: D7 — 244 public enum variants / struct fields are undocumented and missing_docs is
not set. Item declarations are near-complete (1234 documented, 3 not); it is the members
that render bare — the ~35 that matter are the introspection payload structs
(FuncInfo/InputInfo/OutputInfo/OptInputInfo/OptDomain, CandleSettings), which is
exactly what a user reads to use that layer.
D8a (DONE) — four of D8's five sub-defects are closed: the ta-lib\src\ta_func
paths point at ta_codegen/input/<name>/ (07d56a53c); #[doc(alias = "TA_SMA")] and #[doc(alias = "TA_SMA_Lookback")] now land on the batch and lookback tiers, not only
on the stream type, so a C porter searching docs.rs for TA_SMA lands on Core::SMA
(docs(rust): the C symbol is a doc alias on the batch tier and the lookback too (#179 D8) #3196056e927d); REAL_MIN/INTEGER_MIN no longer carry the copy-pasted "widest
value" sentence; and REAL_DEFAULT/INTEGER_DEFAULT are both documented in types.rsand cross-linked from the crate docs (lib.rs:37) and from every
default-taking function's # Arguments, so the raw -4e37 is no longer what a reader
sees.
D8b (DONE 2026-09-02, docs(rust): the front page's three samples are authored once, not twice (#179 D5b, D8b) #332403f56b37) — the #![forbid(unsafe_code)] sentence
now says where the one unsafe in the shipped dependency graph is: ta-lib-dispatch,
inside the is_x86_feature_detected!("fma") test that proves it sound, and why forbid
here does not see it — it expands from another crate's macro.
Original: D8 — smaller doc defects: all 168 shipped files tell the reader to edit ta-lib\src\ta_func (a Windows path that is itself generated — source of truth is ta_codegen/input/<name>/); #[doc(alias = "TA_SMA")] lands on the stream types only, so
a C porter searching docs.rs for TA_SMA misses Core::sma; REAL_MIN/INTEGER_MIN carry
the copy-pasted "widest value" sentence that is only true for MAX; REAL_DEFAULT/ INTEGER_DEFAULT are used 20+ times in generated code but referenced from no doc comment
(the docs print the raw -4e37 instead); lib.rs:62 advertises #![forbid(unsafe_code)]
without noting the single unsafe lives in its own mandatory dependency.
D9a (DONE, dev a382d6e) — both crate READMEs carry crates.io + docs.rs badges.
D9b (DONE 2026-09-24) — the comms surfaces still omit Rust, verified 2026-08-31. website/src/README.md:3
(the SEO description) and :17 still say "a C/C++ core and wrappers for Python and R" with
zero mentions of Rust; website/src/wrappers/README.md has none either (and Rust is
first-party, not a wrapper); the top-level README.md — 11 lines, and the repository
link from crates.io — mentions no binding at all, Java included (Java: remaining loose ends before the first Maven Central publish #172 B6 is the same
line). Original: D9 — comms surfaces omit Rust.website/src/README.md — including its SEO
description — says "a C/C++ core and wrappers for Python and R", zero mentions of Rust; website/src/wrappers/README.md likewise (and Rust is first-party, not a wrapper); the
top-level GitHub README, which is the repository link from crates.io, says nothing about
the crate; neither crate README carries a crates.io/docs.rs badge.
D10 (DONE 2026-09-24, 61d6df629) — CHANGELOG 0.8.1 never mentions the crate. A first crates.io publish is
user-facing by the repo's own rule, and 0.8.1 is still unreleased and editable. Worth a
sentence about the 0.1.x discontinuity (see B2).
D11 (added 2026-09-24): no Polars recipe. A Rust Polars user calls the crate from
a closure, and the obvious spelling is silently wrong. Polars 0.55 marks Expr::map and map_many elementwise. By its source, a stateful indicator then runs across .over()
group boundaries, per streaming morsel, and on the filtered subset when a later filter/slice is pushed below it. Polars' own rustdoc points users at map. Add a short
recipe to the Rust API docs: apply/apply_many, rechunk() + cont_slice(), the output
placed at out[lookback..] behind a NaN prefix, and .over([col("symbol")]). Keep polars
out of the ta-lib dependencies, since its 0.x API breaks every few months. Compile-check the
recipe off the PR gate, because building polars is heavy.
E. Test / CI coverage gaps (none block the publish; all are regression protection)
E1 (DONE 2026-08-30, ci(nightly): package the two publishable crates and check what actually ships (#179 E1) #292c82450e25) — scripts/rust_package_check.py packages
BOTH crates (-p ta-lib-dispatch -p ta-lib, not the library alone — the =0.1.2 pin has
to resolve against the sibling), then checks what actually ships in the two .crate
files. It runs in dev-nightly and, since ci(main-nightly): the release branch never compiled the crate it publishes (#179 E5) #306, on main-nightly too — main is the branch a .crate publishes from. Original: E1 — nothing has ever run cargo package or cargo publish --dry-run. Zero hits
repo-wide. cargo publish performs packaging itself, so this cannot block the publish — but
it means a packaging break (a missing asset, a bad category, a manifest that fails to
normalise) is discovered with the version already burned. Add to the dev-nightly Rust job,
which already installs the toolchain (dev-nightly-tests.yml:440-467); then build + test the extractedtarget/package/ta-lib-0.8.1/ and assert the file list carries all 168 src/ta_func/*.rs plus ta_func_api.xml. This is the Java: remaining loose ends before the first Maven Central publish #172-A2 analogue, minus its sting —
because the Rust server links the real crate, only packaging is uncovered here.
E3 (DONE 2026-08-31, ci(nightly): compile the publishable crates off x86-64 (#179 E3) #309b04b74c1e) — the cross-target job compiles the
publishable crates off x86-64, so an emission bug in the cfg pair cannot ship silently
to Apple Silicon and every ARM/wasm user. Original: E3 — no non-x86-64 cargo check. Portability is correct today and aarch64 is
value-tested via the arm64 job, but a single emission bug in the cfg pair stops the crate
compiling on Apple Silicon and every ARM/wasm user. cargo check -p ta-lib --target aarch64-unknown-linux-gnu needs no linker.
E4 (DONE 2026-08-30, ci(gate): rustdoc on PRs — the one lint set no other gate step can see (#179 E4) #2905f8b3fab0) — RUSTDOCFLAGS="-D warnings" cargo doc --no-deps -p ta-lib runs on the PR gate (the one lint set no other gate step can see)
and on main-nightly. Original: E4 — cargo doc runs nowhere.CLAUDE.md instructs "verify with cargo doc --no-deps (warning-free)" as a manual step. docs.rs is the first place rustdoc executes on
a release, and a broken intra-doc link renders as literal text. (All 338 [Core::…]
targets resolve today — this is prophylaxis.) RUSTDOCFLAGS="-D warnings" cargo doc --no-deps -p ta-lib.
E5a (DONE 2026-08-30, ci(main-nightly): the release branch never compiled the crate it publishes (#179 E5) #306ce91e24ec) — the release branch compiles the crate it
publishes.main-nightly-tests.yml gained a rust job (both clippy gates at -D warnings, rustdoc, the generator's tests, the crate's doctests, the crate's unit +
integration tests, rust_package_check.py) and a regen-check job. Neither is gated on test, so a dist-pool failure cannot skip the release branch's only Rust coverage.
E5b (DONE, verified 2026-09-24: main-nightly runs all three) — three dev-nightly Rust legs are still not mirrored onto main: --rust-debug (cross-language-rust-debug), scripts/rust_stream_debug.py, and arm64-linux. MSRV stays deliberately dev-only. The uncovered window is unchanged and
small — the commits only main sees — but it includes the release cut itself. Either
mirror the three, or make a one-off manual run part of the runbook's pre-cut checklist
(docs/rust-publish-runbook.md), which is where a human-executed step belongs.
E6 (DONE 2026-08-31, ci(nightly): compile and test the crate on Windows and macOS (#179 E6) #3146ac02135c) — the rust-other-os job compiles and
tests the crate on Windows and macOS. As predicted, it needed no C build and no server
harness: there is no build.rs and zero cfg(windows)/cfg(unix)/cfg(target_os in library/src or dispatch/src. Original: E6 — no Windows/macOS Rust build. Smaller than it sounds: no build.rs, and grep 'cfg(windows)|cfg(unix)|cfg(target_os' over library/src dispatch/src → zero
hits. cargo check + cargo test --lib --doc on windows-2022/macos-latest needs no C
build and no server harness, so the POSIX-only argument that excludes the other jobs does not
apply here.
E7 — no cargo-semver-checks. Once 0.8.1 is on crates.io there is a baseline; add it
to the nightly so the B1 decision cannot be violated silently. Blocked on B3 by
construction — there is nothing to compare against until a version is published — so
this is the one E item that is correctly still open, and it should be part of the
post-publish checklist rather than the pre-publish one. scripts/utilities/versions.py:86
already names it as the mechanical guard.
E8 — smaller gaps. Split into seven 2026-08-31; four are closed, and the three
that were real defects rather than niceties (E8b, E8c, E8f) are among them.
E8b — every *_open_and_fill had no doctest at all, and it is the one public
entry point documented to panic. Closed by test(rust): every open_and_fill carries a doctest that is its own claim (#179 E8) #283 (6d23e8f9e): each carries a doctest
that is its own claim — the filled prefix compared bitwise against Core::<N>'s batch
output over the same series.
E8c — *_open_and_fill carried none of the bounds preamble the batch entry
points have, so an undersized output panicked mid-write naming internal codegen
variables. Now guarded like the batch tier: empty input → OutOfRangeStartIndex, past MAX_INDEX + 1 → OutOfRangeEndIndex, an output shorter
than len - lookback → BadParam, all before anything is written, and the rule is
stated in the # Errors section (rule S5).
E8d — 45 of the 61 candlestick doctests still only prove "in range". On the
shared example series only 16 of 61 patterns actually fire, so the other 45 assert a
domain claim that a function returning all-zeros would also satisfy. Documentation nicety, not a correctness gap:--xlang-hash already sweeps every
function across all 9 fuzz_gen shapes including FUZZ_CANDLE (built for Testing: extend non-vacuous synthetic pattern coverage (FUZZ_CANDLE + MC/DC) to all 61 candlesticks #109 so
every CDL* pattern fires instead of being vacuously zero), bit-for-bit against C,
nightly. Only the human-facing docs.rs example is uncovered. Reusing FUZZ_CANDLE's
literal bars (fuzz_cdl_catalog, src/tools/ta_regtest/fuzz_data.h) in the Rust
doctest generator is still the way to close it; C-only today, not yet ported.
E8e — 0 of 176 doctests assert a literal value. Every claim is relational or
a domain. Deliberate as far as it goes — a literal would duplicate ta_regtest's
hardcoded expectations in a place no oracle maintains — but it means the doctests
prove shape, never arithmetic. Listed so it is a decision rather than an oversight.
E8f — Cargo.lock was touched by no script and checked by no gate.
Closed by build(gate): check the committed Cargo.lock before any cargo call repairs it (#179 E8f) #331 (build.py check-cargo-lock, run first in regen-check, before any
cargo call can repair the lock silently) and 918a7b709 (sync_versions() refreshes
it by resolving, check_versions() compares it before tagging). The "only bites under cargo publish --locked" estimate above was wrong: the lock carries each workspace
member's own version, so every VERSION bump stranded it in the tag.
E8g — SECURITY.md now exists; still no issue templates. Original: no SECURITY.md and no issue templates (verified absent
2026-08-31: no SECURITY.md, no .github/ISSUE_TEMPLATE/). This is how RustSec finds
a maintainer, and a crates.io listing is the first thing that makes that matter.
E9 (added 2026-08-31, promoted out of E8's prose) — nothing pins the local dispatch/ source to the immutable published crate.ta-lib pins ta-lib-dispatch = "=0.1.2", and 0.1.2 is on crates.io and can never be changed. A
future edit to dispatch/src/lib.rs — a comment, a doc line, a real fix — would make
the tree and the registry disagree with nothing anywhere noticing: the workspace
resolves the path dependency, and rust_package_check.py packages the local copy
without ever fetching the published one to compare. The failure is silent and lands on
users, who get the registry's copy while every gate here ran the tree's. Two exits:
fetch and diff the published .crate in the nightly, or bump dispatch on any change
to it (which is the rule already, just unenforced). Was a "future-drift hazard" when
filed; it became live the moment 0.1.2 was published.
Verified already fine — do not re-litigate
Cargo metadata is complete and valid (license, homepage, repository, documentation, readme, 5
keywords ≤20 chars, 3 valid category slugs); no [package.metadata.docs.rs] is needed (no
features, no cfg-gated items) except optionally for D4's KaTeX. tools/ correctly carries publish = false. Send + Sync is proven, not assumed — types.rs:564 for Core/ CoreBuilder, plus a compile-time _assert_auto::<XStream>() in each of the 168 stream files. Core immutability + builder is done and tested (11 unit tests incl. an Arc-shared
concurrent batch). #172's D1 (final) has no Rust analogue. Compatibility is pub(crate)
and pinned. OptValue is sealed. The _impl/_fma variants are private and correctly
cfg-gated; dispatch's single unsafe carries a real // SAFETY: comment tied to the is_x86_feature_detected! guard. Default is implemented for Core and CoreBuilder. All
168 *Stream derive Debug, Clone. Supply chain is structurally near-zero: the shipped crate
has exactly one dependency and it is first-party; dispatch has none; serde_json is confined
to publish = false tools — cargo audit/deny would be vacuous. cargo test --lib = 30
real tests including every_function_binds_calls_and_agrees_with_its_lookback (drives all 168
in-process, asserts covered == FUNCS.len()); cargo test --doc covers 168 batch + 168 _open/peek in the debug profile. regen-check does gate output/rust — every fix above goes
in the generator, not the output tree. Zero todo!/unimplemented!/process::exit; every .unwrap()/.expect() in the 168 indicator files is inside a doctest. Per-function rustdoc is
genuinely strong (Arguments/Errors/Panics/See-also, #[doc(alias)], 336 runnable doctests, and
all 168 ta-lib.org slugs resolve — only the trailing slash in A5 is wrong). Crate size, name
ownership, publish order and the =0.1.1 resolution are all settled. unused_mut is in the
allow list — the old clippy-gate note is closed. No stale ta-lib-rust repo exists to retire;
the released C tarball ships no Rust, so there is no dist/vcpkg/PyPI interaction.
The
ta-libcrate (ta_codegen/output/rust/library/, v0.8.1) builds, packages and passesevery gate it has. This is the single checklist of what is still open before publishing —
the Rust analogue of #172.
What was verified first (so the list below is scoped correctly)
Does anything test the code we would publish? Yes — unlike Java. The Rust JSON-RPC
server links the shipped crate rather than embedding a copy of it
(
tools/Cargo.toml:14 ta-lib = { path = "../library" };tools/src/bin/ta_codegen_serve.rs:7 use ta_lib::{Core, ...}; zero inlined indicator bodies against 1514core.*calls). So--codegen,--xlang-hash,server_verifyandstream_verifyall exercise the cratesource itself. Contrast
project_java_server_embeds_core. The untested thing ispackaging, not code.
Can it be published today? Mechanically, yes.
cargo packagesucceeds for both crates;the
ta-libtarball is ~858 KB gzipped (limit 10 MiB). Both names are already owned(
mario4tier+ teamgithub:ta-lib:rust-crate-io-owners), andta-lib-dispatch0.1.1 isalready on crates.io (2026-08-03) byte-identical to
dispatch/src/lib.rs, so the=0.1.1pin resolves.
Is the declared MSRV honest? Yes, and it is not arbitrary:
cargo +1.86 check -p ta-libpasses,
+1.85is rejected by cargo's own gate, and 1.86 is exactly where safe#[target_feature]fns (RFC 2396) stabilized — which is what the 26 FMA clones are builton. It is true but unguarded.
Is aarch64 covered? Yes — dev-nightly
arm64-linux(dev-nightly-tests.yml:517) buildsand value-tests the Rust server on
ubuntu-24.04-arm, exercising the#[cfg(not(target_arch = "x86_64"))]arm in all 26 dispatch files.So the code is covered. What is not: packaging, the publish procedure, and ~12 public-API
shapes that freeze the moment 0.8.1 lands.
Status audit, 2026-09-24
0.8.1 shipped without the crate; the first publish is 0.8.2 (
VERSIONondev). Read"0.8.1" below as "the first published version". Pre-flight re-run on
dev7d2518e36:clippy, lib/integration tests, 628 doctests, rustdoc
-D warnings,rust_package_check.pyandcargo publish -p ta-lib --dry-runall pass, the last one onits own now that dispatch 0.1.2 is live. The published dispatch 0.1.2 source is
byte-identical to
dispatch/src(E9 holds today, still unguarded).Ticked: D9b (website index and install page list Rust; top-level README
f3e35305e),D10 (
61d6df629), E5b (main-nightly now runs the debug-profile, arm64 and MSRV legs).E8g is half done:
SECURITY.mdexists, issue templates do not. Runbook refreshed(
61d6df629,f3e35305e); E7 is its first-publish step RP7.Still gating the publish: B2, B3, B4. A6 and C8b decided the same day (see the items).
Status audit, 2026-09-13
C16 LANDED on
dev2026-09-18 (36cd8ac1f): batch and lookback names followdocs/naming-spec.md, and the function-keyed enums keep their spelling. C8b is what is leftof section C. Nothing else re-audited.
Status audit — 2026-08-31
Re-checked every item against
devbbe63f18e, after the two-day run of PRs #288–#319.Twelve items ticked, five split, one added. What is left is small and almost entirely
yours.
Ticked since the 08-27 audit: C8a (#313), C9 (#295), D1/D2 (#312),
D4 (#305, by a different route than the item proposed — see below), D6 (#288 +
#305), D7 (#279), E1 (#292), E2 (#298, moved to the nightly by #300),
E3 (#309), E4 (#290), E6 (#314).
Split, because each was half-resolved:
tests/empty_range.rs)(startIdx, endIdx)signature still cannot spell "no bars"#[doc(alias)]on the batch + lookback tiers (#319), theREAL_MIN/INTEGER_MINcopy-paste, and the*_DEFAULTconsts (now documented and cross-linked fromlib.rs:37)D8b— done (#332)website/src/README.md(SEO description included),website/src/wrappers/README.mdand the top-level GitHub README still say nothing about Rustrust_package_check.pyand regen-check now run onmain(#306)--rust-debug,rust_stream_debug.pyand arm64 are still dev-onlyopen_and_filldoctest, no bounds preamble onopen_and_fill, the uncheckedCargo.lock) are all closeddispatchsource pin,SECURITY.md/issue templates, and the 45 candlestick doctests that do not fireD4 closed by removal, not by KaTeX. #305 trimmed Formula/Notes out of rustdoc
altogether and links ta-lib.org instead, so
grep '\\frac\|begin{aligned}' src/ta_func/*.rsis now 0 files. There is no math left on docs.rs to render, and no
[package.metadata.docs.rs]is needed. The KaTeX half is moot rather than outstanding.Added: E9 — nothing pins the
dispatch/tree to the published0.1.2. Was buried inE8's prose; promoted because it is now a live hazard rather than a future one: 0.1.2 is
published and immutable, the library pins
=0.1.2, andrust_package_check.pypackagesthe local copy without ever comparing it to the registry's.
What actually gates the publish is unchanged and is all yours: A6 (no publish
credential —
actions/secretsstill reportstotal_count: 0), B2 (ta-lib0.1.0/0.1.1/0.1.2 all still unyanked, verified against the registry today), B3 (
cargo publishthecrate), B4 (all six
website/src/api/*/README.mdstill carry the "Not yet released —Q1 2027" banner) and D10.
Status audit — 2026-08-27
Re-checked against
devccacb4552, after #268 finished the streaming opener'sargument contract:
BadParam. Ahistory shorter than
lookback + 1isRetCode::InsufficientHistory, an emptyone is
OutOfRangeStartIndex, one pastMAX_INDEX + 1isOutOfRangeEndIndex; what is left onBadParamis the argument contract —parameters, buffer lengths, aliasing — which is what the batch tier answers for
the same faults.
docs/error-handling-spec.md§2.3 states the whole tier, andits Appendix D now has no open items in any backend. Ticked below.
The
174s below are measurements from when each item was written and still readas "the whole corpus"; C9's literal is corrected in place, since it is part of a
public type.
Status audit — 2026-08-18
Re-checked every open item against
devc53acafa6. Five corrections, no items added orremoved:
C3 is done —
CandleSetting.range_typeis theRangeTypeenum, shipped dev7dfa4b961(2026-08-15). Ticked below.The corpus is 174, not 168. AC, AO, CMF, CMOU, EFI, HMA, MARKETFI, NVI, PVI, PVO,
QSTICK, VWMA and WAD landed since this page was written. Every "168" below means "the
whole corpus" and still reads correctly with 174 substituted; only C9's, where the number
is part of a public type, is corrected in place — and that it moved at all is C9's own
argument.
D3 was reworded but is still false.
47bf967ecreplaced the old sentence; the page nowsays "
RetCodeimplementsstd::error::Error, so results compose with?"(
website/src/api/rust/README.md:182), directly under a table of the batch returncodes.
impl Erroris true, but the batch tier returns a bareRetCode, socore.SMA(..)?still does not compile.?composes only on the builder and*_opentiers, which return
Result<_, RetCode>— which is exactly what the crate's own doccomment on
impl std::error::Error for RetCodesays. Fix the website line, not the crate.D8 has a second sub-defect fixed, beyond the path one already recorded:
REAL_MINandINTEGER_MINno longer carry the copy-pasted "widest value" sentence. Still open there:#[doc(alias = "TA_SMA")]lands only on the stream type (sma.rs:233), not onCore::SMA; and the#![forbid(unsafe_code)]line still omitsdispatch's oneunsafe.C8 is NOT resolved by C14, despite the shape change. C14 rewrote the OUTPUT
side; C8 is about the INPUT range.
startIdx: usize, endIdx: usizeis still aninclusive pair, so the batch API still cannot express an empty input range. Both
halves of C8 remain open.
Correction to C8's own wording, measured 2026-08-18. C8 says the natural call
(0, 0)panics. It does not:(0, 0)is a valid call asking for one output atbar 0, and driving all 174 functions through the abstraction layer at
(0, 0)gives 30 with
count == 1(zero lookback) and 144 withcount == 0(the lookbackexceeds the range) — no errors and no panics. The panic needs BOTH a zero-lookback
function AND an empty input slice:
ADD(0, 0, &[], &[], ..)traps, whileSMA(0, 0, &[], 30, ..)returnsOk(count: 0)because its lookback of 29 makesthe assert's
_assertStart > endIdxarm short-circuit. So the item is narrowerthan written — it is about the empty slice, not about
(0, 0).(0, 0)was nevertheless untested in every backend, and now is not:b2161b156pins every ordered pair from
{0,1,2,3}into the--xlang-hashsweep for all 174functions against all four servers. Sabotage-proved — a
count == 0fault is caughton all 144 functions that can produce an empty result, where the pre-existing random
subranges caught it on only 119.
The three ports'
OutRangeare now the same (4ff662434): Java'sendIdx(),C#'s
EndIdxand Rust'send_idx()all returnedbegIdx + count— an EXCLUSIVEindex — while every batch entry point takes an INCLUSIVE
endIdxparameter. Onename, two conventions, and the failure mode is a silent off-by-one. All three removed;
each had a single caller, a test asserting the accessor equalled its own definition.
Member sets now match:
begIdx/count,EMPTY/Empty,isEmpty()/IsEmpty/is_empty().C8's second sentence is stale. The only non-panicking call,
(1, 0), now returnsOutOfRangeEndIndex, notOutOfRangeStartIndex— Introduce TA_MAX_INDEX: bound the valid startIdx/endIdx range in all four backends #180 flipped it as C6. The panic on(0, 0)over an empty slice is unchanged and still the item.A. Blocking the first publish
A1 (DONE 2026-08-11, dev d51371b) — the README install snippet says
ta-lib = "0.6". That README is thecrates.io landing page, and it is the first copy-pasteable line on it.
^0.6resolves tonothing: published versions are 0.1.0/0.1.1/0.1.2 and the next is 0.8.1.
library/README.md:18; hard-coded atta_codegen/generator/src/main.rs:2135. The websitecopy already has it right (
website/src/api/rust/README.md:55→"0.8"). One-linegenerator fix.
A2 (DONE 2026-08-11, dev d51371b) — "161 indicators" in three shipped places; 168 ship.
Cargo.toml:6(the crates.io search-result tagline),lib.rs:3,README.md:6— frommain.rs:1967,:2032,:2123. Ground truth 168 two ways (ls ta_codegen/inputminushelpers/types/enums.yaml;grep -c 'pub fn .*_lookback'). "61 candlestick patterns"is correct. Interpolate
funcs.len()rather than re-typing a literal — the same twohard-coded copies are what let it drift (
readme-vs-lib.rsduplication, below).A3 (DONE 2026-08-11, dev d51371b) — no LICENSE file inside either
.crate.tar tzf ta-lib-0.8.1.crate→ 8entries plus
src/ta_func/, no LICENSE; same for the publishedta-lib-dispatch-0.1.1.crate(checked from the registry cache, not a local build). A.crateis a source redistribution and BSD-3 clause 1 requires the notice to travel withit.
license = "BSD-3-Clause"is only an SPDX label; cargo injects nothing. Neithermanifest has
include/exclude, so copying the rootLICENSEintolibrary/anddispatch/is enough. Same standard the project applied to the jars in Java: remaining loose ends before the first Maven Central publish #172 A1.Sting: dispatch 0.1.1 is immutable, so fixing it means a dispatch 0.1.2 published
before ta-lib is ever published, which also bumps the
=0.1.1literal atmain.rs:1883and
main.rs:1985. Bundle A4 into that same 0.1.2 — it is the one free window.A4 (DONE 2026-08-11, dev d51371b) —
ta-lib-dispatch's manifest has nokeywordsand nocategories(
dispatch/Cargo.toml, 15 lines) — unreachable by crates.io search, listed under nocategory. Fold into the 0.1.2 from A3.
A5 (DONE 2026-08-11, dev d51371b — it was TWO defects: the C13 verbatim rename had also upper-cased the slug, so
/functions/SMA404s too; slug is nowname.to_lowercase()) — 168 dead "Further reading" links in the shipped rustdoc. Every function pageends in
https://ta-lib.org/functions/<name>/— trailing slash. Verified live:/functions/sma/→ 404,/functions/sma→ 200. The site builds flat files(
dist/functions/sma.html), notsma/index.html. Rust-only: the same grep overoutput/java,output/csharp,src/ta_func,website/srcreturns 0. Drop the slash atbackends/rust_doc.rs:143. One character, 168 pages.A6 (DECIDED 2026-09-24, runbook
b775a65d6): a maintainer's personal crates.io token, scopedpublish-updateto the two crates, with an expiry; manualcargo publish, like Java. No CI secret, no Trusted Publishing. The team owner is the bus-factor answer. Token created andcargo logindone 2026-09-24 (ta-lib*,publish-update, expiring). Original: A6 — no publish credential exists.gh api .../actions/secrets→total_count: 0;the only secret referenced anywhere is
secrets.GITHUB_TOKEN; crates.io reportstrustpub_only: falsefor both crates. Decide before writing any publish step: manualcargo publishfrom a maintainer machine with a personal token (works today, no repochange), or crates.io Trusted Publishing (register the exact workflow filename per crate,
add
permissions: id-token: write, no long-lived secret). Publish order isdispatch, then ta-lib — currently recorded only as a comment inside a generated file
(
library/Cargo.toml:22-25).B. Maintainer / account work (the #172 category-B analogue)
ta-libandta-lib-dispatchowned by the project.ta-lib-dispatch0.1.1 published and byte-identical to the tree.0.8.1is hard-locked to the CVERSIONin both directions (scripts/utilities/versions.py:309-320rewriteslibrary+tools;
:615hard-fails pre-release on mismatch —dispatchis exempt and alreadydecoupled). Two consequences nobody has written down: (a) no Rust-only patch is possible
without cutting a full C release, which is why A1/A2/A3/A5 are pre-publish rather than
"fix later"; (b) the C cadence is patch-heavy (0.6.1→0.6.4, 0.7.1, 0.8.1) and
ta-lib = "0.8"matches any 0.8.x, so a C-motivated 0.8.2 carrying any section-C fix wouldauto-upgrade into every consumer's build as an API break in a patch. Decision (maintainer,
2026-08-12): keep lockstep. One number across C, Rust and Java — a release in practice
changes something every backend shares, so they are one generated library, not four that
happen to ship together, and publishing the crates becomes part of every release rather
than a one-off. The obligation: bump VERSION's MINOR whenever any backend breaks its
API, even when a C patch motivated the release.
cargo-semver-checks(E7) is themechanical guard, once a published version exists to compare against. Recorded at
get_version_stringinscripts/utilities/versions.pyand indocs/rust-publish-runbook.md.ta-lib0.1.0–0.1.2. They are an unrelated third-partyFFI wrapper (virtualritz/ta-lib-rs), 5097 downloads, 0 reverse dependencies, unyanked.
ta-lib = "0.1"will keep resolving to a different library forever, anddocs.rs/ta-lib/0.1.2stays live beside the new docs. Yanking breaks no published crateand does not break existing lockfiles — it only blocks new
^0.1resolutions. Notorder-dependent; can be done release-day.
cargo publishdispatch 0.1.2, then ta-lib 0.8.1, then verify docs.rs built.website/src/api/rust/README.mdandwebsite/src/api/rust/stream/README.mdstill carry the "Not yet released — Q1 2027"banner;
website/src/install/README.md:16says "none is released yet" and the Native tablelists only C/C++ (no
install/rust/page exists). The site deploys frommainon push.docs/java-deprecation-runbook.md; Rust hasnothing. The two-crate publish order lives in a code comment.
C. Public-API decisions — cheap now, semver-breaking after 0.8.1 ships
These are the items with a real deadline. Everything else on this page can be fixed in a
later version.
C1 —
#[non_exhaustive]on the public enums. DONE 2026-08-09, dev1353c8737.Shipped exactly as decided: the five below marked, the three abstract-layer types left bare,
and
Compatibilityuntouched (it ispub(crate), where the attribute means nothing). Theone downstream cost landed too — the JSON-RPC server is a separate crate, so its
RetCodematch needed a wildcard arm; all six variants stay mapped explicitly, so that arm is dead
today.
Cost is one
_ =>arm downstream; nothing else (construction, comparison andif ret != RetCode::Successare unaffected).Mark
#[non_exhaustive]—FuncId(168,rust_abstract.rs:36),FuncUnstId(25,
templates/rust/types.rs:51),RetCode(6,types.rs:3),Group(10,
rust_abstract.rs:961),CandleSettingType(12,types.rs:161).FuncIdis the non-negotiable one — without it indicator ta_codegen: stream bodies emit duplicated NULL-check blocks (22 sites) #169 is a major bump forever.FuncUnstIdalready carriesUnused1/12/15/22because the id space had to staystable, i.e. it has demonstrated it moves.
RetCodeis load-bearing for C7: withoutit, splitting insufficient-history out of
BadParambecomes "never". It is also where thecost is lowest, since
Successshares the enum with the errors so real code already writesif ret != RetCode::Success. Precedent:std::io::ErrorKind.Leave exhaustive, with a comment saying it is deliberate —
InputType(3),OutputType(2),OptInputType(4). These are the abstract layer's typesystem, they mirror C enums unchanged since 2002, and matching them exhaustively is
the use case (dispatching a parameter binder, rendering a parameter widget).
#[non_exhaustive]here forces an arm that can do nothing meaningful.On
OptInputTypespecifically: variant-level#[non_exhaustive]is also declined —it would forbid struct-pattern destructuring without
.., which is the primary way thepayload is read. The four payload shapes are frozen; adding a field to one is breaking
either way.
C13 — one verbatim name per function, in every language. DONE 2026-08-09, dev
9145c932a.The name in
ta_codegen/input/<dir>/<dir>.yamlis now the sole identity, spelledverbatim everywhere. C alone prefixes
TA_; suffixed variants take an underscore. SoTA_SMA/TA_SMA_LookbackareSMA/SMA_Lookbackin Rust, Java and C#, where thecrate had
sma/sma_lookback. Enum members follow (MAType.SMA,FuncUnstId::EMA),and the unstable-period wildcard is
ALLin all three — Rust had spelled itFuncUnstAll, Java and C#All, CTA_FUNC_UNST_ALL.Why it had to land before 0.8.1 and not after: it renames every public method and
every enum member in the crate. Post-publish it is a major-version migration; today it
costs nothing, because Rust, Java and C# are all unpublished.
The YAML
camel_casefield it removes was a hand-authored second identity on all 168functions. It earned its keep on four, and had already frozen two typos into shipped
API (
CdlHignWave,CdlSeperatingLines). Two mutually inconsistent PascalCase manglersdisagreed on eight
FuncIdvariants, and one deliberately aliasedSTOCH_RSIandSTOCHRSIonto a single identifier.Also gone:
camelCaseNamefromTA_FuncInfoand<CamelCaseName>fromta_func_api.xml— a public C struct layout change, so C consumers recompile ratherthan relink; Java's
FunctionInfo.javaMethodName(); C#'sFunctionInfo.MethodName;pascal_nameand the authoredc_namefrom enums.yaml (nowc_prefix+{name, value}); five manglers; andRegistry::java_base/csharp_base, collapsed intoone
name_of. CHANGELOG entries added under 0.8.1### Changed.Two gates were repaired rather than adjusted, both found by the pre-commit review:
RustBackend::rendered_namenow lower-cases a function name because the module pathstill does (without it a function named
LOOPpassed the gate and emittedmod loop;—the Java mangler used to catch that); and the unstable-period table now pins each
function's
FuncUnstIdordinal, authored independently in enums.yaml, instead of itsvariant name, which
unst_rowderives from the function's own name and so could neverdisagree.
Verified: generator 678 tests, both clippy gates at
-D warnings, crate lib + doc tests,warning-free rustdoc, C library and all four JSON-RPC servers, the Java and C# suites,
ta_regtest --codegenacross all four languages,--xlang-hashbit-identical over237,500 cases, idempotent regeneration, and
synth_gate.py.C12 — abstract-layer type naming. DONE 2026-08-08, dev
94a3f065a.Shipped:
abstract_api::OptDomain->OptInputType, fieldOptInputInfo.domain->.kind. One type, one field.The original six-name proposal (
*ParamInfo/*ParamType) was REJECTED — its premisewas false. Java already ships
InputInfo/InputType/OutputInfo/OutputType/OptInputInfo/OptInputType; C# shipsInputInfo/OutputInfo/OptInputInfo/OptInputDomain. Rust already matched Java on five of six names, so adding aParaminfix would have moved five agreeing names away and made Rust a fourth vocabulary for one
model — the exact problem it claimed to fix. (The stated readability gain was also
unreal:
InputInfoandOptInputInfodiffer by the prefixOpt, and so doInputParamInfoandOptInputParamInfo—Paramis common to both.)OptDomainwas the single genuinely asymmetric name, and renaming it alone reaches theInfo/Type symmetry goal and converges Rust to Java.
.kindmatches the crate's ownInputInfo.kind/OutputInfo.kind; Java spells all threetype, which Rust cannot useas a bare field.
Note for future readers, recorded in the type's doc comment: structurally this type is
C#'s
OptInputDomain(fused tag + payload, exhaustively matchable). Java'sOptInputTypeis a flat tag enum with the payload flattened onto the record. Rust takesJava's name and C#'s shape; only one of the two could be matched.
The generator IR
abstract_rows::OptDomainis unchanged — it is shared with the Java andC# emitters and is not a public name in any binding.
Verified before commit: clippy
-D warnings, 30 lib tests, 341 doctests, warning-freerustdoc, the full generator suite,
ta_regtest --codegen --language=c,rust161/161, andan RPC probe of
TA_GetOptInputParameterInfo. Wire contract untouched.C2 — Rust is the only backend with no
MATypeenum. Done, dev493c76972.68 slots take
MAType;TryFrom<i32>lives in the library. One decision that isload-bearing rather than cosmetic:
try_from(i32::MIN) == Ok(DEFAULT), which is whatkeeps Rust inside Rust/Java/C#: the (MAType)int.MinValue sentinel is not mapped to the documented default, so enum opt params diverge from C #162's sentinel contract instead of joining Java's exemption.
C3 (DONE 2026-08-15, dev
7dfa4b961) —CandleSetting.range_typeis theRangeTypeenum,TryFrom<i32>in the library rejecting anything else, matching Java and C#. Original: C3 —CandleSetting.range_type: i32is a public field of a public struct with theenum in a doc comment (
templates/rust/types.rs:113-120).range_type: 7is constructibleand silently means "none of the above" (
types.rs:264 _ => 0.0). Java shipsRangeType.A public field's type cannot change after publish.
C4 (DONE 2026-08-11, dev 07f57da — REMOVED, not
pub(crate): they have no callers at all, sopub(crate)leaves twodead_codeitems and fails the-D warningsgate) — two internal items arepub. (Was three.— theCore::ema_privateunvalidated variant whose own rustdoc says "its only callers are the guarded bodies" —
is
pub(crate)as of Introduce TA_MAX_INDEX: bound the valid startIdx/endIdx range in all four backends #180, dev4e9cf7f0b: it skips the validation prologue, so apubthere was the one backend where a caller could reach an indicator with no
TA_MAX_INDEXbound. C's equivalent is file-
static, Java/C#'s are package-private/internal.Still open:
Core::ta_candlerangeandCore::ta_candleaverage(templates/rust/types.rs:259,:271—C macro names,
ta_prefix redundant insideta_lib, rawrangeType: i32, non-snake-caseavgPeriod, and zero callers anywhere in the crate).pub→pub(crate)is a removalafter publish. Zero
#[doc(hidden)]exists anywhere in the crate.C5 (DONE 2026-08-18,
5a65f37a1) —mod ta_funcis private, the glob stays, and the eight crate-root constants moved to where Java already puts them:Core::{REAL_DEFAULT, INTEGER_DEFAULT, REAL_MIN, REAL_MAX, INTEGER_MIN, INTEGER_MAX, MAX_INDEX}andFuncUnstId::COUNT. Original: C5 —pub mod ta_func+pub use ta_func::*gives every public type two permanentpaths (
lib.rs:80-82), one of them the C source-directory name —ta_lib::ta_func::Corestutters. Make the module private and keep the glob. Also: the glob drops
REAL_MIN,REAL_MAX,INTEGER_MIN,INTEGER_MAX,FUNC_UNST_COUNTat the crate root(
types.rs:90-109) — very generic names for "the widest value a TA-Lib optional parametermay take". Associated consts or a
paramsmodule; both moves are semver-major later.C6 — batch tier returns the wrong range code. DONE 2026-08-08 via Introduce TA_MAX_INDEX: bound the valid startIdx/endIdx range in all four backends #180, dev
4e9cf7f0b.All 168 batch entry points return
OutOfRangeStartIndexforendIdx < startIdx(
sma.rs:159,add.rs:136); C returnsTA_OUT_OF_RANGE_END_INDEX(ta_SMA.c:86) and thecrate's own abstract tier returns
OutOfRangeEndIndex(abstract_api.rs:3005) — two publictiers, two answers, and the one users call is the one that disagrees with C.
Nothing gates it:
server_verify.c:512skips every non-Success call ("parameter validationis implementation-specific"), and
ta_codegen_serve.rs:13415reimplements C's guard insidethe server, so the crate's answer never reaches the driver. Flipping breaks no gate.
Shipped: flipped to
OutOfRangeEndIndexas part of Introduce TA_MAX_INDEX: bound the valid startIdx/endIdx range in all four backends #180 — the same two lines ofprologue also gain the
TA_MAX_INDEXbound, which is what givesOutOfRangeStartIndexa liveproducer again (
startIdx > TA_MAX_INDEX). Shipping them separately would be two behaviourchanges where one will do. The earlier "C6b" idea — returning a code instead of asserting on
out-of-slice indices — is not part of this; it needs slice lengths C does not have, and
stays in C8.
C14 (DONE 2026-08-18,
5a65f37a1) — the batch tier returnsResult<OutRange, RetCode>.Not on the original list; it is the change C5 and D3 landed inside, and the
largest public-API decision taken before publish.
pub fn SMA(startIdx, endIdx, inReal, period, outReal) -> Result<OutRange, RetCode>— the two&mut usizeout-params are gone and come back by value, unreachable on failure.
OutRangemoved to the crate root; its second field is
count, matching Java's and C#'sOutRange(begIdx, count).Result<(), RetCode>was considered and rejected:RetCodecontainsSuccess, soErr(RetCode::Success)would be constructible, andResult<(), _>discards the two values the call produces, leaving the out-params — the part that
actually reads as foreign.
RetCodetherefore keepsSuccess: the internaltier must be able to report it, and Java and C# both ship a public
RetCodecarrying it.
Cross-indicator calls deliberately did NOT migrate. The guarded body survives
as
pub(crate) fn <N>_Internal(...) -> RetCode, exactly as Java routes them toSMA_Internaland C# to itsRetCodeoverload. 19 of the 33 call sites pass thecallee their own
&mut outBegIdx/&mut outNBElementand read them back, and fourfold "success with zero output" into the same conditional as the error, which
?cannot express. So all 174 transcribed bodies are byte-identical and
--xlang-hashis evidence rather than a tautology. FMA dispatch stays on
_Internal.Also in the same commit:
RetCode::as_c_int()moved into the library, closing afail-open — the server's mapping ended in
_ => 5000, forced becauseRetCodeis#[non_exhaustive]and the server is a downstream crate, so C7's future variantwould have reported as
InternalErrorrather than failing to compile. And a newgate,
rust_public_entry_documents_exactly_its_parameters, pins each publicwrapper's
# Argumentslist to its signature; it caught the out-param bullets thischange left on all 174 rustdoc pages, which nothing else could see.
C15 (DONE 2026-08-19,
62de52a26) —*_OpenAndFillreturnsResult<(<N>_Stream, OutRange), RetCode>. Original: C15 —*_OpenAndFillstill takes&mut outBegIdx/&mut outNBElement, sothe crate now ships two conventions for the same answer: batch returns an
OutRange,OpenAndFillfills out-params. Java reports it asfillRange()on the handle and C#as
FillRange, so Rust is the only port with the split. Pre-existing rather thanintroduced by C14 (verified:
rust_stream.rs's whole diff there was oneSelf::MAX_INDEXhunk), but it is a public shape that freezes at publish.That leaves the crate with no public
&mut usizeat all: every remainingoutBegIdxparameter is on apub(crate)fn (_Internal,_OpenCore,_OpenAndFillInternal).Java and C# are deliberately not copied here. Both hang the range off the handle
and return it bare. Java has no tuple, so a per-function result record was the only
alternative; Rust's
_Openalready returnsResult<(Stream, value), RetCode>, so(Stream, OutRange)composes two conventions the crate already has instead of addinga third. A handle field would also go stale across
Clone— Java pins exactly that(
StreamSmokeTest: a copy carries the original's fill range), and in Rust cloning astream is the documented way to fork it.
As in C14, the composition seam did not migrate:
_OpenAndFillInternal(the fusedcomposed open, Composed streaming Open recomputes every sub-call, and STDDEV's sqrt map cannot vectorize #192) keeps the pair, because its caller reads it back.
One signature, three bodies. The shared wrapper declares the pair as locals after its
guards and folds them in at the return, reading like the batch wrapper. The two tiers
exempt from the
OpenCoremerge hand-roll theirs:MAcarries the range out of thedispatch
matchitself — one arm runs, so the callee'sOutRangeis the call's —rather than round-tripping ten arms through locals, and
MAVPbuilds it at the return.Evidence. The
OpenAndFillleg ofstream_verifyis the gate, and it compares thereported range against the batch range: 174 functions, filled array ==
batch(0, n-1)bitwise. Sabotage-proven —
beg_idx + 1in one function trips it at exit 87, and itsurfaces on
TA_MArather thanTA_SMA, so the dispatch's range-threading is coveredend to end. Rust 161/0 and C 161/0, unchanged;
--xlang-hashbit-identical.Blast radius: excising the
*_OpenAndFillfunction from both trees leaves theremainder byte-identical in all 174 files, so no batch body,
_Open,_OpenCoreorupdate/peekmoved. Two generator tests were pinned to the old shape and now assertthe negative (
rust_dispatch_open_modes_differ_only_where_intendedasserted"OpenAndFill carries the out-meta pair"), and the hand-written Rust streaming page,
which no gate covers, was updated by hand.
C7 (DONE —
TA_INSUFFICIENT_HISTORY+ Streaming openers: answer the batch tier's codes, in the batch order (Appendix D items 6 and 13) #268) —*_opencollapsed four causes ontoBadParam, including "you passed 20 bars for a 30-period SMA" — the normal warm-upcondition every streaming user hits on their first run, indistinguishable from
period = 0. Resolved the way the item asked, with a newRetCodevariant rather than astream-tier error type: short history is
InsufficientHistory(all four backends agree,and it is the library's one recoverable condition), an empty history is
OutOfRangeStartIndexand one pastMAX_INDEX + 1isOutOfRangeEndIndex— the impliedindex pair of a call over
[0, historyLen - 1].BadParamnow carries only the argumentcontract, which is what the batch tier answers for the same faults.
C8a (DONE 2026-08-31, test(rust): the empty and sub-lookback input ranges were pinned nowhere, and three backends describe them wrong (#179 C8) #313
574b3fd72) — the panic, and the sentence threebackends carried about it. The public tier never reaches the assertion preamble:
rust_lang::gen_argument_checksruns its own bounds first and answersBadParam. Sothe panic C8 describes lives in
<N>_Impl, which ispub(crate)— reachable from theJSON-RPC servers, not from a
pub fncall.tests/empty_range.rspins the empty seriesand the sub-lookback range on the public tier, one function per input arity plus a
period-taking function at
period = 1; every call returns rather than panics, and apanic fails the test outright.
It also retired a false claim that had outlived the code in three backends:
java::gen_argument_checks,Core.java'sclampedStartandCore.cs'sClampedStarteach said Rust applies its_assertStart > endIdx ||escape to bothbounds, making the input bound "the one place Java/C# check more than C and Rust do" —
Core.cseven namedSMA(0, 5, &[], 30, &mut [])asOk(count 0)in Rust. True ofthe
_Implasserts; not true of the public tier, where the escape is taken on theoutput bound only and Rust and Java/C# agree. Nothing pinned it, which is how the
sentence survived.
C8b (DECIDED 2026-09-24: keep the inclusive pair.) Rust works like every other backend.
docs/error-handling-spec.md§2.2 already specifies it: B1/B2 make every valid range at least one bar, so an empty or too-short input is B5,BadParamwhere the language has a length (Rust, Java, C#) and undetectable in C. That differs by backend by design, not by defect. Original: C8b — the batch API still cannot express an empty input range. The breakinghalf, and the one genuinely publish-gated item left in section C.
startIdx: usize, endIdx: usizeis an inclusive pair, so there is no value of it that means "nobars": an empty series has no successful spelling at all, and the idiom every doc
teaches —
data.len() - 1— underflows on an emptyVecbefore the call is even made.C answers this with a code because it has the same signature and the same problem;
Rust does not have to inherit it. Changing to a
Range, or toendIdx: Option<usize>, is only possible before publish. 28 functions have anunconditional zero lookback (
acos ad add asin atan avgprice bop ceil cos cosh div exp floor ln log10 medprice mult nvi obv pvi sin sinh sqrt sub tan tanh typprice wclprice)and every period-taking function joins them at
period = 1, so this is the wholecorpus, not a corner.
C9 (DONE 2026-08-31, fix(rust): FUNCS is a slice, so the registry's size is not public API (#179 C9) #295
6d7851137) —FUNCSis a slice, so the registry'ssize is no longer part of a public signature. Original: C9 —
pub static FUNCS: [FuncInfo; 176](abstract_api.rs:398; it read[FuncInfo; 168]when this was filed) puts the count in apublic type. Mostly subsumed by C1 (adding ta_codegen: stream bodies emit duplicated NULL-check blocks (22 sites) #169 already needs a
FuncIdvariant), but&[FuncInfo]or afuncs()accessor is free now. NoteMAX_INPUTS/MAX_OPT_INPUTS/MAX_OUTPUTS(:2605-2609) are corpus-derived values that will also move.C10 (DONE 2026-08-11, dev 07f57da) —
#[must_use]. NeitherRetCodenor any of the 168 batch methods carries it(168
*Streamstructs and 168peekdo).core.sma(...)with a bad period returnsBadParam, writes nothing, leaves the caller's zeros in place, and warns at no level. Oneattribute on the enum in
templates/rust/types.rscovers all 1020 methods. Technicallyadditive later — listed here because the first cohort of users learns the API either way.
C11 (DONE 2026-08-11, dev 07f57da) — derive gaps.
ParamHolder(abstract_api.rs:2689) has no#[derive]at all,so no
Debugon the type users bind arguments into.RetCode/FuncUnstId/CandleSettingTypelack theHash, PartialOrd, OrdthatFuncIdhas — soHashMap<FuncUnstId, i32>, the obvious way to track per-function unstable periods, does notcompile. No
PartialEqonCore/CoreBuilder/CandleSetting. All additive, all trivial.C16: the Rust names the naming spec changes. LANDED on
dev2026-09-18.core.sma(..),core.sma_lookback(14),core.ht_trendline(..), and the numerics tier issma_impl.MAType::SMA,FuncUnstId::HT_DCPERIOD,FuncId::CDL3BLACKCROWS,RetCode::BadParam,CandleSettingType::BodyLong,FuncInfo's"SMA", the case-insensitive by-name lookup,
#[doc(alias = "TA_SMA")]and the parameter names are allunchanged (spec E1/E2). Java: remaining loose ends before the first Maven Central publish #172 D8 is the Java half; C# followed the same spec.
Verified on the landed tree: the PR gate's fourteen legs — including
cargo docunder-D warningsand 613 doctests, which caught 1705 broken[Core::SMA]links and 201doctests no other gate could see — plus
build.py regtest(all four languages 161/161)and
regen-check.That prediction held: eight assertions went vacuous rather than red, plus
stream_ab.py'sanchor again. Each one now builds its needle from the same fold helper the emitter uses.
D. Docs and first impression (all fixable post-release; release day is when it matters)
D1 (DONE 2026-08-31, docs(rust): the crate front page never mentioned the streaming tier (#179 D1/D2) #312
cc08e64c7) — the streaming tier is on the crate frontpage.
README.mdandlib.rsboth open on it now (grep -ci stream→ 4 and 9, from 0and 1), and the README's examples are compiled as doctests (test(rust): the crate README's examples are claims, and nothing compiled them (#179) #297), so the ergonomic path
is a claim a gate holds rather than prose. Original: D1 — the streaming tier is invisible.
grep -i streamoverREADME.md→ 0 hits;over
lib.rs→ one, an aside inside the FMA paragraph. Meanwhile 168*Streamtypes areflattened to the crate root and will dominate the docs.rs index with no explanation. The
crate's only example is a 7-argument call with two
&mut usizeout-params — while theergonomic answer (
let (mut s, _) = core.rsi_open(&hist, 14)?; s.update(bar)) exists,works, is
Clone-able, haspeek, and composes with?. Undiscoverable unless you alreadyknow to look for
*_open. Add a "Live data" section to README + lib.rs.D2 (DONE 2026-08-31, docs(rust): the crate front page never mentioned the streaming tier (#179 D1/D2) #312
cc08e64c7) — the crate links the guide.lib.rs:114andREADME.md:122both point atta-lib.org/api/rust/and, separately, atthe streaming page. Original: D2 — the crate never links the Rust guide that already exists.
website/src/api/rust/README.md(217 lines) answers calling convention, output sizing,lookback, return codes, abstraction layer, unstable period, candle settings and threading —
better onboarding than anything in the crate. The crate links only
/functions/(per-indicator formula pages) and docs.rs (the 1020-method wall).
D3 (DONE 2026-08-18,
218d69b83+5a65f37a1) — the claim is now TRUE rather than removed: the batch tier returnsResult<OutRange, RetCode>, socore.SMA(..)?compiles. Original: D3 — the website claims?composes withRetCode(website/src/api/rust/README.md:127).It does not — the batch tier returns a bare
RetCode. One line of Markdown, but it is thefirst thing a Rust reader checks. (Whether to add a
Result/Vecconvenience layer isadditive and can wait; splitting
Successout of the error enum cannot.)D4 (DONE — fence half
07d56a53c; the KaTeX half closed by REMOVAL in docs(rust): trim Formula/Notes off rustdoc, link to ta-lib.org instead (#179 D6 follow-up) #305e3fa82678: Formula/Notes are no longer emitted into rustdoc at all, the pages link ta-lib.org instead, andgrep 'begin{aligned}' src/ta_func/*.rsis now 0 files. No[package.metadata.docs.rs]is needed.) — LaTeX renders as raw backslash macros on docs.rs. 10 files carry math; RSI andBBANDS — the two pages a new user opens first — show a wall of
\begin{aligned}. In both,the closing fence sits after the explanatory sentence, so the prose renders as code too
(
rsi.rs:97-116,bbands.rs:138-152; emitterrust_doc.rs:668-675). Either[package.metadata.docs.rs] rustdoc-args = ["--html-in-header", "katex.html"], or plain-ifythe math in the Rust doc backend. Closing the fence at the
$$fixes the swallowed-prosehalf on its own.
D5a (DONE) — the drift mechanism is closed. The counts and the install
requirement are no longer typed:
n_funcs,n_candlesandinstall_reqare derivedfrom
funcsand theVERSIONfile and interpolated into both literals (main.rs~2338), and
funcsis the whole corpus even under--func=, so they are whole-corpusby construction. test(rust): the crate README's examples are claims, and nothing compiled them (#179) #297 added
#[cfg(doctest)] #[doc = include_str!("../README.md")], sothe README's examples now compile under
cargo test --doc— deliberately behindcfg(doctest)so the headings never render in the docs and its links stay ordinaryMarkdown for crates.io.
D5b — the prose around the samples is still authored per page. The executable
half is closed (docs(rust): the front page's three samples are authored once, not twice (#179 D5b, D8b) #332
403f56b37, doc-comment trimdf0c6bebb): the three samples bothfront pages carry — batch quick start, builder, streaming walk-through — are one
FrontPageExampleeach, and each page's wrapper is added on the way out (//!plus thehidden
Okfor rustdoc, a pasteablefn mainfor the README), so the two can no longerdiffer on code. They already had: the builder sample asserted the setting took on the
README and stopped at
build()?inlib.rs, so that doctest proved only that the callcompiles.
README.mdregenerates byte-identical, which is what shows the renderersreproduce the pages rather than merely compile.
What is left is the narrative, and it differs by more than markup:
lib.rsstates theAPI shape as bullets carrying intra-doc links, the README as prose with a different
category list and an install section. Converging it means picking one rendering for text
that reads differently in the two places — a maintainer call, not a refactor. The sibling
crate's answer (
dispatch/src/lib.rs:3 #![doc = include_str!("../README.md")]) staysunavailable for the reason recorded at
lib.rs:355— the front page must keep ordinaryMarkdown links to render on crates.io, and as crate docs they would be resolved as
intra-doc links.
Also unclosed by docs(rust): the front page's three samples are authored once, not twice (#179 D5b, D8b) #332: the same builder sample is authored twice more in
templates/rust/types.rs(theCoreandCoreBuilderitem docs), both still stoppingat
build()?, and once more inwebsite/src/api/rust/README.md, which nothing compiles.FrontPageExampleextends to the first two with a///-prefixed renderer.D6 (DONE 2026-08-30, docs(rust): a category index on the crate front page, from the grouping the registry already had (#179 D6) #288
a2f5b3067+ docs(rust): trim Formula/Notes off rustdoc, link to ta-lib.org instead (#179 D6 follow-up) #305e3fa82678) — a category index on thecrate front page, built from
abstract_api::Group— the grouping the registry always had.Built in
rust_doc::category_indexrather than inline, sotests/backend_suite.rscanassert every function reaches the page. Original: D6 — no navigation. 1020 methods on one
Corewith no category index, prelude, orgrouping.
abstract_api::Groupalready encodes the grouping at runtime but is not surfacedas docs. "How do I find the candlestick ones?" works only by alphabetical accident.
D7 (DONE 2026-08-29, docs(rust): document every public enum variant and struct field (#179 D7) #279
cd0afd44a) — every public item, variant and structfield carries its own doc, and
#![warn(missing_docs)]is set (lib.rs:337) —warnrather than
denyso a future rustc widening the lint cannot break a downstream build;the nightly's
cargo clippy -- -D warningsis what makes it a gate. Original: D7 — 244 public enum variants / struct fields are undocumented andmissing_docsisnot set. Item declarations are near-complete (1234 documented, 3 not); it is the members
that render bare — the ~35 that matter are the introspection payload structs
(
FuncInfo/InputInfo/OutputInfo/OptInputInfo/OptDomain,CandleSettings), which isexactly what a user reads to use that layer.
D8a (DONE) — four of D8's five sub-defects are closed: the
ta-lib\src\ta_funcpaths point at
ta_codegen/input/<name>/(07d56a53c);#[doc(alias = "TA_SMA")]and#[doc(alias = "TA_SMA_Lookback")]now land on the batch and lookback tiers, not onlyon the stream type, so a C porter searching docs.rs for
TA_SMAlands onCore::SMA(docs(rust): the C symbol is a doc alias on the batch tier and the lookback too (#179 D8) #319
6056e927d);REAL_MIN/INTEGER_MINno longer carry the copy-pasted "widestvalue" sentence; and
REAL_DEFAULT/INTEGER_DEFAULTare both documented intypes.rsand cross-linked from the crate docs (lib.rs:37) and from everydefault-taking function's
# Arguments, so the raw-4e37is no longer what a readersees.
D8b (DONE 2026-09-02, docs(rust): the front page's three samples are authored once, not twice (#179 D5b, D8b) #332
403f56b37) — the#![forbid(unsafe_code)]sentencenow says where the one
unsafein the shipped dependency graph is:ta-lib-dispatch,inside the
is_x86_feature_detected!("fma")test that proves it sound, and whyforbidhere does not see it — it expands from another crate's macro.
Original: D8 — smaller doc defects: all 168 shipped files tell the reader to edit
ta-lib\src\ta_func(a Windows path that is itself generated — source of truth ista_codegen/input/<name>/);#[doc(alias = "TA_SMA")]lands on the stream types only, soa C porter searching docs.rs for
TA_SMAmissesCore::sma;REAL_MIN/INTEGER_MINcarrythe copy-pasted "widest value" sentence that is only true for MAX;
REAL_DEFAULT/INTEGER_DEFAULTare used 20+ times in generated code but referenced from no doc comment(the docs print the raw
-4e37instead);lib.rs:62advertises#![forbid(unsafe_code)]without noting the single
unsafelives in its own mandatory dependency.D9a (DONE, dev a382d6e) — both crate READMEs carry crates.io + docs.rs badges.
D9b (DONE 2026-09-24) — the comms surfaces still omit Rust, verified 2026-08-31.
website/src/README.md:3(the SEO description) and
:17still say "a C/C++ core and wrappers for Python and R" withzero mentions of Rust;
website/src/wrappers/README.mdhas none either (and Rust isfirst-party, not a wrapper); the top-level
README.md— 11 lines, and therepositorylink from crates.io — mentions no binding at all, Java included (Java: remaining loose ends before the first Maven Central publish #172 B6 is the same
line). Original: D9 — comms surfaces omit Rust.
website/src/README.md— including its SEOdescription — says "a C/C++ core and wrappers for Python and R", zero mentions of Rust;
website/src/wrappers/README.mdlikewise (and Rust is first-party, not a wrapper); thetop-level GitHub README, which is the
repositorylink from crates.io, says nothing aboutthe crate; neither crate README carries a crates.io/docs.rs badge.
D10 (DONE 2026-09-24,
61d6df629) — CHANGELOG 0.8.1 never mentions the crate. A first crates.io publish isuser-facing by the repo's own rule, and 0.8.1 is still unreleased and editable. Worth a
sentence about the 0.1.x discontinuity (see B2).
D11 (added 2026-09-24): no Polars recipe. A Rust Polars user calls the crate from
a closure, and the obvious spelling is silently wrong. Polars 0.55 marks
Expr::mapandmap_manyelementwise. By its source, a stateful indicator then runs across.over()group boundaries, per streaming morsel, and on the filtered subset when a later
filter/sliceis pushed below it. Polars' own rustdoc points users atmap. Add a shortrecipe to the Rust API docs:
apply/apply_many,rechunk()+cont_slice(), the outputplaced at
out[lookback..]behind a NaN prefix, and.over([col("symbol")]). Keeppolarsout of the
ta-libdependencies, since its 0.x API breaks every few months. Compile-check therecipe off the PR gate, because building polars is heavy.
E. Test / CI coverage gaps (none block the publish; all are regression protection)
E1 (DONE 2026-08-30, ci(nightly): package the two publishable crates and check what actually ships (#179 E1) #292
c82450e25) —scripts/rust_package_check.pypackagesBOTH crates (
-p ta-lib-dispatch -p ta-lib, not the library alone — the=0.1.2pin hasto resolve against the sibling), then checks what actually ships in the two
.cratefiles. It runs in dev-nightly and, since ci(main-nightly): the release branch never compiled the crate it publishes (#179 E5) #306, on main-nightly too — main is the branch a
.cratepublishes from. Original: E1 — nothing has ever runcargo packageorcargo publish --dry-run. Zero hitsrepo-wide.
cargo publishperforms packaging itself, so this cannot block the publish — butit means a packaging break (a missing asset, a bad category, a manifest that fails to
normalise) is discovered with the version already burned. Add to the dev-nightly Rust job,
which already installs the toolchain (
dev-nightly-tests.yml:440-467); then build + test theextracted
target/package/ta-lib-0.8.1/and assert the file list carries all 168src/ta_func/*.rsplusta_func_api.xml. This is the Java: remaining loose ends before the first Maven Central publish #172-A2 analogue, minus its sting —because the Rust server links the real crate, only packaging is uncovered here.
E2 (DONE 2026-08-30, ci(gate): compile the publishable crates on the MSRV they declare (#179 E2) #298
2f3915a6b; moved from the PR gate to the nightly byci(gate): move the #298 MSRV floor check to the nightly #300 — cost x frequency, not overlap) — the
msrvjob compiles both publishable crateson the floor they declare. Original: E2 — MSRV 1.86 is compiled by nothing (
rust-toolchain.tomlpins 1.97.0). Truetoday; the generator emits new constructs constantly and the next one can raise the real
floor silently.
cargo +1.86 check -p ta-lib.E3 (DONE 2026-08-31, ci(nightly): compile the publishable crates off x86-64 (#179 E3) #309
b04b74c1e) — thecross-targetjob compiles thepublishable crates off x86-64, so an emission bug in the
cfgpair cannot ship silentlyto Apple Silicon and every ARM/wasm user. Original: E3 — no non-x86-64
cargo check. Portability is correct today and aarch64 isvalue-tested via the arm64 job, but a single emission bug in the
cfgpair stops the cratecompiling on Apple Silicon and every ARM/wasm user.
cargo check -p ta-lib --target aarch64-unknown-linux-gnuneeds no linker.E4 (DONE 2026-08-30, ci(gate): rustdoc on PRs — the one lint set no other gate step can see (#179 E4) #290
5f8b3fab0) —RUSTDOCFLAGS="-D warnings" cargo doc --no-deps -p ta-libruns on the PR gate (the one lint set no other gate step can see)and on main-nightly. Original: E4 —
cargo docruns nowhere.CLAUDE.mdinstructs "verify withcargo doc --no-deps(warning-free)" as a manual step. docs.rs is the first place rustdoc executes ona release, and a broken intra-doc link renders as literal text. (All 338
[Core::…]targets resolve today — this is prophylaxis.)
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps -p ta-lib.E5a (DONE 2026-08-30, ci(main-nightly): the release branch never compiled the crate it publishes (#179 E5) #306
ce91e24ec) — the release branch compiles the crate itpublishes.
main-nightly-tests.ymlgained arustjob (both clippy gates at-D warnings, rustdoc, the generator's tests, the crate's doctests, the crate's unit +integration tests,
rust_package_check.py) and aregen-checkjob. Neither is gated ontest, so a dist-pool failure cannot skip the release branch's only Rust coverage.E5b (DONE, verified 2026-09-24: main-nightly runs all three) — three dev-nightly Rust legs are still not mirrored onto main:
--rust-debug(cross-language-rust-debug),scripts/rust_stream_debug.py, andarm64-linux. MSRV stays deliberately dev-only. The uncovered window is unchanged andsmall — the commits only
mainsees — but it includes the release cut itself. Eithermirror the three, or make a one-off manual run part of the runbook's pre-cut checklist
(
docs/rust-publish-runbook.md), which is where a human-executed step belongs.E6 (DONE 2026-08-31, ci(nightly): compile and test the crate on Windows and macOS (#179 E6) #314
6ac02135c) — therust-other-osjob compiles andtests the crate on Windows and macOS. As predicted, it needed no C build and no server
harness: there is no
build.rsand zerocfg(windows)/cfg(unix)/cfg(target_osinlibrary/srcordispatch/src. Original: E6 — no Windows/macOS Rust build. Smaller than it sounds: nobuild.rs, andgrep 'cfg(windows)|cfg(unix)|cfg(target_os'overlibrary/src dispatch/src→ zerohits.
cargo check+cargo test --lib --doconwindows-2022/macos-latestneeds no Cbuild and no server harness, so the POSIX-only argument that excludes the other jobs does not
apply here.
E7 — no
cargo-semver-checks. Once 0.8.1 is on crates.io there is a baseline; add itto the nightly so the B1 decision cannot be violated silently. Blocked on B3 by
construction — there is nothing to compare against until a version is published — so
this is the one E item that is correctly still open, and it should be part of the
post-publish checklist rather than the pre-publish one.
scripts/utilities/versions.py:86already names it as the mechanical guard.
E8 — smaller gaps. Split into seven 2026-08-31; four are closed, and the three
that were real defects rather than niceties (E8b, E8c, E8f) are among them.
Closed by test(rust): every integer-output example states its output's domain (#179 E8) #293 (
b45c17dcf): every integer output now states a domain claim(write extent + value range; window extrema re-derived for the three index
functions).
*_open_and_fillhad no doctest at all, and it is the one publicentry point documented to panic. Closed by test(rust): every
open_and_fillcarries a doctest that is its own claim (#179 E8) #283 (6d23e8f9e): each carries a doctestthat is its own claim — the filled prefix compared bitwise against
Core::<N>'s batchoutput over the same series.
*_open_and_fillcarried none of the bounds preamble the batch entrypoints have, so an undersized output panicked mid-write naming internal codegen
variables. Now guarded like the batch tier: empty input →
OutOfRangeStartIndex, pastMAX_INDEX + 1→OutOfRangeEndIndex, an output shorterthan
len - lookback→BadParam, all before anything is written, and the rule isstated in the
# Errorssection (rule S5).shared example series only 16 of 61 patterns actually fire, so the other 45 assert a
domain claim that a function returning all-zeros would also satisfy.
Documentation nicety, not a correctness gap:
--xlang-hashalready sweeps everyfunction across all 9
fuzz_genshapes includingFUZZ_CANDLE(built for Testing: extend non-vacuous synthetic pattern coverage (FUZZ_CANDLE + MC/DC) to all 61 candlesticks #109 soevery CDL* pattern fires instead of being vacuously zero), bit-for-bit against C,
nightly. Only the human-facing docs.rs example is uncovered. Reusing
FUZZ_CANDLE'sliteral bars (
fuzz_cdl_catalog,src/tools/ta_regtest/fuzz_data.h) in the Rustdoctest generator is still the way to close it; C-only today, not yet ported.
a domain. Deliberate as far as it goes — a literal would duplicate
ta_regtest'shardcoded expectations in a place no oracle maintains — but it means the doctests
prove shape, never arithmetic. Listed so it is a decision rather than an oversight.
Cargo.lockwas touched by no script and checked by no gate.Closed by build(gate): check the committed Cargo.lock before any cargo call repairs it (#179 E8f) #331 (
build.py check-cargo-lock, run first inregen-check, before anycargo call can repair the lock silently) and
918a7b709(sync_versions()refreshesit by resolving,
check_versions()compares it before tagging). The "only bites undercargo publish --locked" estimate above was wrong: the lock carries each workspacemember's own version, so every VERSION bump stranded it in the tag.
SECURITY.mdnow exists; still no issue templates. Original: noSECURITY.mdand no issue templates (verified absent2026-08-31: no
SECURITY.md, no.github/ISSUE_TEMPLATE/). This is how RustSec findsa maintainer, and a crates.io listing is the first thing that makes that matter.
E9 (added 2026-08-31, promoted out of E8's prose) — nothing pins the local
dispatch/source to the immutable published crate.ta-libpinsta-lib-dispatch = "=0.1.2", and0.1.2is on crates.io and can never be changed. Afuture edit to
dispatch/src/lib.rs— a comment, a doc line, a real fix — would makethe tree and the registry disagree with nothing anywhere noticing: the workspace
resolves the path dependency, and
rust_package_check.pypackages the local copywithout ever fetching the published one to compare. The failure is silent and lands on
users, who get the registry's copy while every gate here ran the tree's. Two exits:
fetch and diff the published
.cratein the nightly, or bumpdispatchon any changeto it (which is the rule already, just unenforced). Was a "future-drift hazard" when
filed; it became live the moment 0.1.2 was published.
Verified already fine — do not re-litigate
Cargo metadata is complete and valid (license, homepage, repository, documentation, readme, 5
keywords ≤20 chars, 3 valid category slugs); no
[package.metadata.docs.rs]is needed (nofeatures, no cfg-gated items) except optionally for D4's KaTeX.
tools/correctly carriespublish = false.Send + Syncis proven, not assumed —types.rs:564forCore/CoreBuilder, plus a compile-time_assert_auto::<XStream>()in each of the 168 stream files.Coreimmutability + builder is done and tested (11 unit tests incl. anArc-sharedconcurrent batch). #172's D1 (
final) has no Rust analogue.Compatibilityispub(crate)and pinned.
OptValueis sealed. The_impl/_fmavariants are private and correctlycfg-gated;
dispatch's singleunsafecarries a real// SAFETY:comment tied to theis_x86_feature_detected!guard.Defaultis implemented forCoreandCoreBuilder. All168
*StreamderiveDebug, Clone. Supply chain is structurally near-zero: the shipped cratehas exactly one dependency and it is first-party;
dispatchhas none;serde_jsonis confinedto
publish = falsetools —cargo audit/denywould be vacuous.cargo test --lib= 30real tests including
every_function_binds_calls_and_agrees_with_its_lookback(drives all 168in-process, asserts
covered == FUNCS.len());cargo test --doccovers 168 batch + 168_open/peekin the debug profile. regen-check does gateoutput/rust— every fix above goesin the generator, not the output tree. Zero
todo!/unimplemented!/process::exit; every.unwrap()/.expect()in the 168 indicator files is inside a doctest. Per-function rustdoc isgenuinely strong (Arguments/Errors/Panics/See-also,
#[doc(alias)], 336 runnable doctests, andall 168 ta-lib.org slugs resolve — only the trailing slash in A5 is wrong). Crate size, name
ownership, publish order and the
=0.1.1resolution are all settled.unused_mutis in theallow list — the old clippy-gate note is closed. No stale
ta-lib-rustrepo exists to retire;the released C tarball ships no Rust, so there is no dist/vcpkg/PyPI interaction.