Skip to content

Rust: remaining loose ends before the first crates.io publish #179

Description

@mario4tier

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 dev 7d2518e36:
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 dev bbe63f18e, 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) D8b — done (#332)
D9 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 dev ccacb4552, 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 dev c53acafa6. 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().

  • C8's second sentence is stale. The only non-panicking call, (1, 0), now returns
    OutOfRangeEndIndex, not OutOfRangeStartIndex — 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 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/22 because 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 FuncUnstId ordinal, 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.

  • C2 — Rust is the only backend with no MAType enum. Done, dev 493c76972.
    68 slots take MAType; TryFrom<i32> lives in the library. One decision that is
    load-bearing rather than cosmetic: try_from(i32::MIN) == Ok(DEFAULT), which is what
    keeps 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_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.

  • C14 (DONE 2026-08-18, 5a65f37a1) — the batch tier returns Result<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 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 keeps Success: 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.

    pub fn SMA_OpenAndFill(&self, inReal: &[f64], optInTimePeriod: i32,
                           outReal: &mut [f64]) -> Result<(SMA_Stream, OutRange), RetCode>

    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.

    As in C14, the composition seam did not migrate: _OpenAndFillInternal (the fused
    composed 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 OpenCore merge hand-roll theirs: MA carries the range out of the
    dispatch match itself — one arm runs, so the callee's OutRange is 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) #313 574b3fd72) — 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) #295 6d7851137) — 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) #312 cc08e64c7) — 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) #312 cc08e64c7) — 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) #305 e3fa82678: 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) #332 403f56b37, 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.

  • 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) #305 e3fa82678) — a category index on the
    crate front page, built from abstract_api::Group — the grouping the registry always had.
    Built in rust_doc::category_index rather than inline, so tests/backend_suite.rs can
    assert every function reaches the page.
    Original: D6 — no navigation. 1020 methods on one Core with no category index, prelude, or
    grouping. abstract_api::Group already encodes the grouping at runtime but is not surfaced
    as 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 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) #319 6056e927d); REAL_MIN/INTEGER_MIN no longer carry the copy-pasted "widest
    value" sentence; and REAL_DEFAULT/INTEGER_DEFAULT are both documented in
    types.rs and 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) #332 403f56b37) — 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) #292 c82450e25) — 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
    extracted target/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.

  • 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 by
    ci(gate): move the #298 MSRV floor check to the nightly #300 — cost x frequency, not overlap) — the msrv job compiles both publishable crates
    on the floor they declare.
    Original: E2 — MSRV 1.86 is compiled by nothing (rust-toolchain.toml pins 1.97.0). True
    today; 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) — 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) #290 5f8b3fab0) — 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) #306 ce91e24ec) — 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) #314 6ac02135c) — 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.

    • E8a — integer-output doctests asserted nothing about the computed values.
      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).
    • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions