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 Java library is a complete, buildable Maven project and the artifact builds: ta_codegen build --backend=java runs ./mvnw clean package in ta_codegen/output/java/library/, produces ta-lib-0.8.1{,-sources,-javadoc}.jar ta-lib-<version>{,-sources,-javadoc}.jar (the version follows VERSION) with coordinates io.github.ta-lib:ta-lib and all Central-required metadata, and
tests that jar — the same file ./mvnw -Prelease deploy would upload (A2). This
issue is the single checklist of what is still open before the first publish.
Status note, 2026-09-19
B1 closed. The key that item assumed existed was unusable, so it was replaced and
the old one revoked; details in B1. Still open in B: B2, B3, B4, B6.
The publish procedure left this issue.docs/release-runbooks/ holds every
release runbook now, with per-backend step prefixes (CP, JP, RP, PP) and its README.md
holding the order. The post-release version bump is its own post-publish-runbook.md
rather than the C runbook's tail, because VERSION is one number for every backend: a
bump before the bindings publish makes them carry the next version, which 79a013707
already had to undo by hand for ta-lib-python.
The Java release is cut from main, like every other. That mattered more than it
sounds: the naming fold (D8) was on dev only, and publishing from main beforehand
would have put RetCode.BadParam, TaLibFailure and metadata.FunctionInfo on Central
immutably, with 0.8.2 then renaming all of them. Merged with scripts/merge.py, both
branches at 034f9398f.
Status note, 2026-09-13
D8 LANDED on dev 2026-09-18 (36cd8ac1f). Batch and lookback method names, the
coined enums, the failure types and the metadata tier all follow docs/naming-spec.md.
Section B is otherwise unchanged from the 2026-09-10 note.
Status note — 2026-09-10
C4b and C5 closed on dev. Section B is now the whole issue, and every item left in
it is maintainer or account work: B1 (GPG key), B2 (Portal token), B3 (deploy), B4 (the
website banner, which must stay while v0.8.1 ships C only), B6 (announce).
Checked while here, and deliberately not done: B1's one repo-side half, adding --pinentry-mode loopback to maven-gpg-plugin. It is untestable without a key, and
how it should be spelled depends on how the passphrase is held (3.2.x prefers MAVEN_GPG_PASSPHRASE over settings.xml). Better decided at the moment B1 is done
than guessed now.
Also settled: nothing open here affects the v0.8.1 C ABI or behavior. The only item
that read as though it might was C4b's claim that those handles were covered by nothing
at all; that was never true for C, whose peek leg compares handles rather than values.
See C4b below.
Status note — 2026-08-31
Audit of the whole list against devbbe63f18e. Two items moved, one item split, two
added:
Everything in B is unchanged, and B is what actually gates the publish: B1, B2, B3,
B4, B6. B4 verified still open in the tree — all six website/src/api/{java,rust,csharp}{,/stream}/README.md still carry the
"Not yet released — Q1 2027" banner, and the top-level README.md (11 lines) mentions
no binding at all.
Status note — 2026-08-27
Nothing on this list moved, but one thing behind it did: docs/error-handling-spec.md
Appendix D now has no open items in any backend, Java included. C2 below (array-length
validation) was the last Java entry on it; #268 finished the streaming opener's argument
contract, so a Java caller's rejection is named, typed and ordered the same way the batch
tier's is. Nothing here is blocked on the error contract any more.
A. Blocking the first publish
A1 — no LICENSE/NOTICE in any of the three jars. The project's own BSD-3
clause 2 requires binary redistributions to reproduce the copyright notice and
disclaimer "in the documentation and/or other materials provided with the
distribution". The main jar has 222 entries, none of them the licence. Fix with a <resources> entry pointing at the repo LICENSE, or maven-remote-resources-plugin.
A2 — nothing, anywhere, has ever tested a .jar. Closed by e4289b4eb (branch fix/172-java-jar-gate), which resolves it by removing the second
builder rather than by proving two builders agree. B5 dissolves into this — there
is no second builder left to reconcile.
Maven is now the only builder.ta_codegen build --backend=java runs ./mvnw clean package (tests skipped) and then tests that artifact — the same file ./mvnw -Prelease deploy uploads. Nothing tests a class directory; there is no second
jar; byte-identity is a dead question, with nothing left to assert.
The design that was tried first, and dropped
A JDK-only jar built with -g to match maven-compiler-plugin's <debug>true</debug>.
It worked — every class byte-identical to Maven's, with the exception set being only META-INF/maven/** and two annotation-free package-info.class markers that
maven-compiler-plugin synthesizes itself (createMissingPackageInfoClass; -Xpkginfo:always does not reproduce them, differing in constant-pool order). It was
dropped because keeping it means keeping a standing proof that two builders agree, and
every "is the tested thing the shipped thing?" investigation on this issue is a cost of
having two. Recorded so nobody re-derives it.
Through a committed wrapper, not an installed mvn.mvnw, mvnw.cmd and .mvn/wrapper/maven-wrapper.properties now live beside the pom. ./mvnw is not a
different Maven: it downloads the exact Apache distribution pinned there, verifies it
against distributionSha256Sum, and execs the real mvn inside it — same binary, same
plugins, same ~/.m2. So the backend needs no Maven installed, and "which Maven
produced this release" has one answer on every machine. distributionType=only-script,
so no maven-wrapper.jar binary is committed. Pinned to Apache Maven 3.9.16 (4.x is
still RC). Checksum provenance is recorded next to it: the zip's SHA-512 matched the .sha512 published on both repo.maven.apache.org and downloads.apache.org, and the
SHA-256 was computed from that verified file, since Central publishes no .sha256.
unzip is a real prerequisite. Without it the wrapper quietly falls back to the .tar.gz distribution and then fails the pinned SHA-256 with "your Maven distribution
might be compromised" — a missing utility, not an attack. The gate probes for it first
and says so, and it is in LANG_PREREQS["java"].
Assertions on all three published archives
assertion
how
every ta_codegen/input/ function on the main jar's Core as exactly two public OutRange overloads
javap off the archive, matched by name set so a rename or a lost overload fails too; pinned against the input tree, never a literal
no test entry in main / sources / javadoc
jar --list
META-INF/LICENSE in main + sources
jar --list. The javadoc jar is exempt by design — the plugin consumes no <resources>, so the pom renders the notice via <bottom>; the gate extracts Core.html and asserts the rendered footer
module name io.github.talib
jar --describe-module (absent entry → ta.lib)
Artifact names come from target/maven-archiver/pom.properties, written by the build
itself — not a pom re-parse, not a glob.
The gate's own javac and javadoc runs are deleted: pom.xml already carried <doclint>all,-missing</doclint>, so the doclint teeth moved to maven-javadoc-plugin
intact (a bad @see now fails package). Net effect on gate wall-clock: ~33 s → ~30 s
warm, ~36 s cold including downloading and verifying Maven itself.
Sabotage-proved with clean controls either side: mvnw absent, unzip absent, a
function removed from input/, a float[] overload deleted, <Automatic-Module-Name>
renamed and mis-declared, the LICENSE <resources> mapping removed, the javadoc <bottom> removed, a valid *Test compiled into the shipped package, a
junit-importing suite added, a doclint break, and a corrupted distributionSha256Sum.
That the suites bind to the jar is decisive rather than structural: with the class
directory deleted they still pass; with the jar corrupted they fail.
Two fail-open holes closed while here
A junit-importing suite was dropped from the source list with an informational
line — compiled by neither Maven nor the gate, and green. That is the AllTests
vacuity discovery exists to prevent, and it became reachable in one line the moment
Maven could supply the dependency. Now a hard failure.
javadoc_notice_present extracted into a fixed /tmp path that it begins by
deleting, racing other worktrees. It uses bin/ like every other scratch path.
A3 — no package-info.java anywhere, so every package page in the published
javadoc has no description. That javadoc is the shop window for a first release.
A4 (added and closed 2026-08-31) — the pom's own <description> advertised a
stale indicator count. It said 174 while 176 shipped, and that string is published to
Central and immutable per version. A2's gate reads target/maven-archiver/pom.properties
for the artifact names and never looks at the description, so nothing owned it — the same
class of defect as Rust: remaining loose ends before the first crates.io publish #179 A2 (three hard-coded "161"s in the crate's shop window), in the
file that is the Maven search result. Found and fixed by fix(java): the published pom still advertises 174 indicators; 176 ship #317; generate now rewrites
it (sync_pom_indicator_count, main.rs) so regen-check is the standing gate (fix(ta_codegen): generate rewrites the Java pom's indicator count #320),
with scripts/pre-release-checks.py as the release-time backstop for a tree where generate was not re-run.
Superseded by 43ab73ae9: the description claims a fixed "200+ indicators" floor
instead of a count, and scripts/pre-release-checks.py fails the release if input/
drops below 200 functions or the pom stops saying it.
Sting worth recording: the first fix put the "generated — never edit" note in an XML
comment containing -- mid-body, which is malformed XML. Maven's POM parser rejects it,
so xlang: all langs vs pre-cutover C and synth-gate's Java leg went red on dev
(525a1408b). The pom is parsed, not just read — a comment in it is executable
surface.
B. Maintainer / account work
This section absorbed docs/java-deprecation-runbook.md, now deleted: its Phases 0, 1
and 4 shipped (website links, the buildable Maven project, the removal of the legacy java/ tree), its decisions are all locked and visible in the tree, and everything from
it still open is below. The two procedures it described that recur per release are
documented where they are used, not here: the release command, the wrapper rule and why autoPublish is false live in pom.xml's release profile; how to bump the pinned
Maven, and the provenance of its checksum, live in .mvn/wrapper/maven-wrapper.properties.
Namespace io.github.ta-lib verified (2026-08-05). Worth recording because the
step is non-obvious: io.github.<user> is auto-granted at signup but io.github.<org> is not — ta-lib is an org, so it took the manual path (the
Portal issues a verification key; create a public repo in the TA-Lib org named
exactly that key; verify; delete the repo). Sonatype's docs say org namespaces are
unavailable as automatically registered namespaces, which reads like a refusal and is
not. Verification does not expire, is not re-checked, and the proving repo is
disposable — but what it is anchored to is the org's identity, so never rename or
transfer the TA-Lib org. GitHub-anchored rather than org.ta-lib precisely so the
namespace does not depend on renewing ta-lib.org: a DNS-verified namespace becomes
claimable by whoever buys the lapsed domain.
B1 — GPG key (closed 2026-09-19). Signing key rsa4096/4F36E9F467C8547D, fingerprint B351CC3D856EE4AC3DBFD4C44F36E9F467C8547D, uid Mario Fortier <mario4tier@users.noreply.github.com>, never expires, no
passphrase. Live on keyserver.ubuntu.com with its uid, and on
keys.openpgp.org without one (that server publishes a uid only after mailing a
confirmation link, which a noreply address can never receive; Central validates
key material, not the uid).
The key named in the original item, 46F000E9CE598271 from 2026-08-06, was unusable: its passphrase was lost and was recorded nowhere. Nothing had ever
been signed with it, so it was replaced and then revoked on both keyservers
with its .rev certificate, which needs no passphrase.
No passphrase is deliberate. A signing key cannot publish anything; the
Portal token (B2) authorizes a release and already sits in plaintext beside it.
Adding one later is gpg --change-passphrase on the same key, with nothing to
republish.
Add --pinentry-mode loopback to the maven-gpg-plugin configis
obsolete advice and was not done. Verified in maven-gpg-plugin 3.2.8's
bytecode (GpgSigner.generateSignatureForFile): --batch --pinentry-mode loopback --passphrase-fd 0 sit entirely behind if (passphrase != null). The
plugin adds loopback itself when it has a passphrase; with none it passes no --batch and lets pinentry prompt, which is correct on a tty. Loopback without --passphrase-fd would only fail differently.
What was needed is <keyname> in the release profile (034f9398f): gpg signs
with the first secret key in the keyring when none is named, and with the
revoked key still present it kept picking that one and hanging. An absent pinned
key fails at verify with No secret key, before any upload. Pinning is not
lock-in: Central registers no fingerprint against a namespace and never checks
key consistency between releases, so rotating is that one line plus a keyserver
upload. Sonatype's own docs recommend the pom <configuration> for exactly this
multi-key case.
Back up the .rev certificate. The key has no passphrase, so a lost laptop
is a compromised signing key and that file is the only kill switch. The key
itself is worth backing up only to turn a rotation into an import.
B2 — Central Portal user token in ~/.m2/settings.xml under <id>central</id>
(the pom's publishingServerId). Shown once in a modal and never retrievable again,
only replaceable. This token, not the GPG key, is the credential that authorizes a
release — on a stolen laptop it is the one to rotate first, from any browser, since a
signing key alone cannot publish.
Proven 2026-09-19: the 0.8.1 upload authenticated with it (Using credentials from server id central in settings.xml). Nothing had ever exercised it before that.
B3 — publish (done 2026-09-19). The procedure is now a runbook, docs/release-runbooks/java-publish-runbook.md, reached from that directory's README.md; this item is just the checkbox.
0.8.1 is on Maven Central, published from main at d2bfc74d55bea3eea06127f3357f971d86176bbc, tagged java-v0.8.1, deployment 6f78a093-5bb6-4a5c-bf9c-20c0e9580cba. Verified by downloading the pom, jar, sources,
javadoc and signatures from repo1, and by resolving the coordinate into an EMPTY local
repository and calling SMA/RSI through it, so the copy install had put in ~/.m2
could not mask a failure. Central's own validation is also what first proved the signing
key is fetchable from a keyserver, which no local check could establish.
Do not re-investigate this: after publishing, the Portal status endpoint returns PUBLISHED alongside errors: {'common': ['Deployment components info not found']}.
That is the components record being cleared on completion, not a failure.
The old checkpoint in this item was not performable. It said to resolve io.github.ta-lib:ta-lib:<version> from a clean consumer project before
promoting. Sonatype's Portal has no staging repository, and a deployment that
has validated can only be published or dropped, never resolved. The runbook
therefore checks the artifact twice, and neither check is the old one: before
the upload against the locally installed artifacts (./mvnw -Prelease install
plus scripts/java_consumer_check, which depends on the coordinate the way a
user does), and after promotion against Central with -Dmaven.repo.local set to
a throwaway directory so the installed copy cannot satisfy it.
Tag after, never before. Nothing in the Maven build reads git (the version
comes from VERSION through sync.py), so a tag is a record, not an
ingredient. Dropping a pending deployment then leaves no tag to move, which is
what 0c11d8c22 had to forbid on the C side. The jar carries no commit sha
(BuildStamp holds only a gencode digest), so java-v<version> at the recorded
commit is the only later answer to which tree is on Central.
Rollback: drop the pending deployment in the Portal. Nothing is public until
promoted by hand, which is the whole reason autoPublish is off: Central is
immutable, so a released bad artifact cannot be withdrawn.
The release profile runs maven-clean-plugin at initialize (78a8c0fd0).
Without it a warm target/ made this step repackage the PREVIOUS javadoc, with
a new public class missing from the javadoc jar and a doclint break passing with
BUILD SUCCESS, because maven-javadoc-plugin decides whether to skip from its own
command line, never from the sources.
B4 — drop the "Not yet released — Q1 2027" banner (done 2026-09-19, e307a633e). Gone from both website/src/api/java/README.md and website/src/api/java/stream/README.md; the Rust and C# banners stay, being still true.
Section 2.0 also gained a link to the Central listing, where a reader finds versions
other than the one the snippet pins.
its <version> should be set to whatever B3 promotes — nothing sets it by hand
any more.sync.py rewrites that token from what Maven Central reports
(maven-metadata.xml's <release>), so the page follows the registry on its own
cadence rather than VERSION. The added prose deliberately carries no version of its
own, since a second copy would have nothing keeping it in step.
B5 — no CI job runs mvn at all. Dissolved with A2: the nightly's Java jobs
run regtest.py -> cargo run -- build, which now is./mvnw clean package plus
the three-jar assertions. No new CI job and no Maven install were needed — the wrapper
self-installs, and unzip is already present on the runners.
B6 — announce the coordinate (settled 2026-09-19). The announcement is the
website page, and nothing else. Both halves this item originally asked for were
declined by the maintainer on the day of the publish:
a user-facing CHANGELOG.md entry — the CHANGELOG is the C release's and stays
that way, so a binding publish never earns an entry, first release included. post-publish-runbook.md step PP2 is the only CHANGELOG step in a release round, and
it is the C one.
a root README.md note — written and then rejected; the root README stays about
TA-Lib Core and points at ta-lib.org.
Recorded so neither is re-proposed for the Rust or C# publishes.
No relocation POM (maintainer, 2026-08-05) — closed, recorded so it is not
re-litigated. com.tictactec:ta-lib:0.4.0is live on Central; it is immutable and
stays resolvable forever, so nothing we publish can break an existing pin, and nothing
we publish is a drop-in replacement for it either. A relocation would have to go out as
a new version under the old coordinate (com.tictactec:ta-lib:0.4.1), which needs
publish rights to a namespace verified against tictactec.com years ago and not worth
recovering for a signpost — and the two APIs have diverged far enough that a resolved
relocation would not have compiled unchanged anyway. Purely additive if the old
credentials ever turn up.
C. Test coverage gaps
C1 — FunctionInfo.newCall(Core) is never exercised.Dispatch reads the
given Core in two places, lookback and call; both are now pinned by newCallCarriesTheGivenCore in MetadataTest (the call half via test(java): the binder's call() path never read the Core it was handed (#172 C1) #296 --
measured beforehand to be the unexercised one: hardcoding Core.DEFAULT in Dispatch.call left all six Java suites green).
C2 — no array-length validation, and no test for what happens without it.
Measured against the shipped jar:
misuse
today
output array too small
ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
endIdx past the input end
AIOOBE: Index 200 out of bounds for length 200
mismatched input lengths
AIOOBE
null input array
JVM helpful-NPE from inside the algorithm
All thrown from deep inside the loop, after the output buffer is partially
written, with no OutRange telling the caller how far it got. C cannot check this
— it has no sizes. Java can: arrays carry their length. A fail-fast IllegalArgumentException("endIdx 500 exceeds input length 200") in the guarded
wrapper is a strict improvement and is testable. Memory-safe either way (the JVM
always bounds-checks), so this is contract quality, not a correctness bug.
Done. Both overloads of all 174 functions now carry the bound the Rust backend
already asserts: every input the body indexes must reach endIdx, every output must
hold the values actually produced (endIdx - max(startIdx, lookback) + 1 — the
produced count, not the width of the range). Emitted from backends/java.rs, so it
cannot drift per function. The core keeps its own diagnoses: a bad index still raises IndexOutOfBoundsException, an out-of-range parameter still names the parameter, and a
range shorter than the lookback is still an empty success with no output-length check.
Covered in BatchApiTest for each row of the table above, both sides of the boundary,
and that a rejected call leaves the buffer untouched.
Writing it turned up a second thing, now Java: more test coverage for unexpected I/O #235: nothing asserted that a call producing
no values also reads nothing. NoPhantomIoTest does, by passing zero-length arrays
where no I/O is expected, and its first run caught APO/PPO/PVO reading the input on a
range too short for them (fixed; value-neutral).
C3 — discover_java_tests warns where its own doc comment says it fails
(ta_codegen/generator/src/main.rs). A *Test.java without main() prints WARNING: ... not run and the build stays green; the comment claims it is
"reported and counted as a failure: silently skipping it would recreate exactly the
vacuity this replaces". The comment is the correct behaviour.
C4a — the streaming suite touched 8 of 176 handles. Closed by test(java): prove copy() independence and peek() non-commit on every stream handle (#172 C4) #281
(799f5c496). peekAndCopyHoldOnEveryHandle walks Functions.all(), opens every
registered handle by reflection on its own lookback, and asserts peek() non-commit and copy() independence on all 176 — a handle it cannot reach is a named failure, not a
smaller sweep. A second registry-driven pass pins every multi-output handle's Value as
a record with one component per registry output, the expectation derived from the
registry rather than a literal so the synth gate's injected fixtures do not turn a
correct tree red. Superseded by Java streaming: multi-output update() allocates a Value record every bar because it is cached for value() #310: that pass now requires each stream-capable
function's <N>Out sink to have one public field per registry output and to not be a
record. Non-vacuity is counted: swNonCommit/swCopy must equal the
registered count, and swMoved must exceed half — the corpus has to move most handles
off their open value or every property above is satisfied trivially.
C4b (closed 2026-09-10, dev 0e6a4cc93) — the handles a corruption could not
be named on.peek steps the handle rather than a scratch copy, and the sweep's
only observable was the value the handle reports, so on a candlestick whose pattern
this suite's series never fires every assertion read 0 == 0 whichever way the peek
behaved.
Closed by changing the observable, not the corpus.peekAndCopyHoldOnEveryHandle
now diffs the peeked handle against the never-peeked twin field by field, walking
arrays element by element and sub-handles recursively. No pattern has to fire for a
committed peek to be visible, so neither the MC/DC corpus (testing: MC/DC coverage for the remaining 55 candlesticks #219) nor FUZZ_CANDLE's
literal bars were needed, and Java does not acquire a third copy of that catalog to
keep in step with C and Rust.
That is what the C server's peek probe has always done (sv_steq_TA_<N>, generated
into ta_codegen_serve.c), which is also why the closing sentence this item used to
carry was wrong: the stream_verify blindness it cited is in the value legs, and
the peek leg does not compare values. C was never exposed here.
Sabotage-proved on the shipped jar, clean control either side:
sabotage
caught by
a candlestick peek committing the lag rotation its update does, placed after the decision so its own return is unchanged
this arm alone: 1 failure, every pre-existing check green
the same peek storing into its derived ring
this arm alone
a composed peek driving sub0.update instead of sub0.peek
this arm, naming the nested sub0.sub.ring_trailingIdx_inReal[0], plus two value checks
The third row is a class no Java gate covered: no_java_peek_copies_the_handle
classifies a line by what it writes, and a sub-stream commit writes nothing of its
own. C gates it (peek_suite::no_peek_entry_point_commits_a_sub_stream), Rust cannot
compile the bug at all (peek(&self) binds the sub-handle shared), and C# had
nothing until no_csharp_peek_copies_the_handle grew the same arm beside this work
(dev 69b205cb7; 22 sites named when the frame emits the committing verb).
Three non-vacuity floors, absolute: field values compared, handles carrying an array
field, handles holding a sub-stream.
C5 (added 2026-08-31, closed 2026-09-10, dev c4fdf617f) — the release branch
never built the jar it publishes.
A2's three-jar assertions — LICENSE entry, module name, the two-public-overload sweep
pinned against input/, no test class in any archive — live in ta_codegen build --backend=java, which is reached only through regtest.py in
dev-nightly's cross-language job. main-nightly-tests.yml runs test, fuzz-vs-064, xlang-hash, rust and regen-check; none of them invokes ./mvnw. xlang-hash looks like it should, and does not: it builds the Java server, which
embeds its own Core copy, not the Maven artifact.
Closed by a java job in main-nightly-tests.yml mirroring that rust job: ref: main, a JDK the only thing installed (the committed wrapper fetches and
SHA-256-verifies its own Maven, and unzip is already on the runners), then cargo run --release --manifest-path ta_codegen/generator/Cargo.toml -- build --backend=java. Verified from the repo root, ~60 s warm. Not gated on test, for
the reason the Rust job is not: no C build is involved, so a dist-pool failure must
not skip the release branch's only Java coverage.
Related and already tracked: #170 (the default-sentinel vector never reaches the
Java/C# float overloads). Note the servers do now carry use_float on all 168
functions (176 today) and test_codegen.c asserts the leg is non-vacuous, so float values are
verified cross-language; #170 is specifically about the sentinel parameter class. #170 closed 2026-08-07.
D. API polish — cheap now, breaking after the first release
D1 — Core and CoreBuilder are not final.Core's javadoc promises deep
immutability and JLS 17.5 safe publication "even when published racily"; a subclass
with one non-final field voids that, and CoreApiTest.coreFieldsAreFinal only
reflects over Core.class itself. There is no extension point to preserve.
D2 — 168 stale javadoc lines contradicting immutability. Every stream class
says "Do not mutate the owning Core's settings while streams opened from it are
live" (template at backends/java_stream.rs:584). Core has no mutators — this
survives from the pre-immutable era and ships in the published javadoc.
D3 — two different value-object idioms.OutRange is a record with
accessors (r.begIdx()); every XxxStream.Value is a hand-rolled class with public
final fields (v.macdSignal) plus hand-written equals/hashCode/toString. On a
JDK-17 library these should be records — identical doubleToLongBits semantics,
~40 fewer generated lines per function.
D4 — duplicate javadoc on the range constants.Core.java:197/199 both read
"Widest value a double optional parameter may take" for TA_REAL_MINand TA_REAL_MAX; :203/205 likewise for the int pair. Hand-written scaffolding.
D5 — fillRange() returns null after a plain open (documented, but a
nullable return in a new API). OutRange.EMPTY or Optional would be better.
D6 — Dispatch.java ships with broken indentation — import io.github.talib.OutRange; and the whole class body carry a ~9-space leak from the
emitter's raw string. It is in the sources jar, so every user who opens it in an IDE
sees it.
D7 — C-namespace residue in Java names:Core.TA_REAL_DEFAULT, TA_INTEGER_MIN, ... inside io.github.talib.Core; and getUnstablePeriod
(bean-style) against the builder's fluent unstablePeriod(...). Purely cosmetic, but
a first release is the only cheap moment.
D8: the Java names the naming spec changes. LANDED on dev 2026-09-18. core.rsi(..), core.rsiLookback(14), core.cdl3blackcrows(..); RetCode.BAD_PARAM, CandleSettingType.BODY_LONG, RangeType.REAL_BODY; the failure types take the TALib
prefix and the metadata tier takes C's words. MAType.SMA, FuncUnstId.HT_DCPERIOD, byName("RSI"), the javadoc and website prose and the parameter names are unchanged
(spec E1/E2). C# followed the same spec in the same commits; Rust: remaining loose ends before the first crates.io publish #179 C16 is the Rust half.
Verified on the landed tree: the PR gate's fourteen legs, build.py regtest (all four
languages 161/161) and regen-check. An adversarial review after that found a Java
ride-along casing bug (ridesma), eight assertions the rename had made unfalsifiable and
five shipped docs still naming *_Lookback; all are fixed in af88df569.
E. Informational — no action proposed
Core.class is 2.3 MB; the first indicator call in a fresh JVM costs ~46 ms of
class load + verification (measured; the second call is 0.004 ms). Irrelevant for a
server, visible in a CLI or lambda. Forced by Java having no partial class — the
escape, if it ever matters, is Core methods delegating to per-indicator
package-private classes so loading becomes lazy.
Constant pool is at 13,313 of the 65,535 limit, so there is no scaling cliff ahead.
fragments/ moved out of library/ (this branch), so library/ now contains exactly
the publishable Maven project.
The Java library is a complete, buildable Maven project and the artifact builds:
ta_codegen build --backend=javaruns./mvnw clean packageinta_codegen/output/java/library/, producesta-lib-0.8.1{,-sources,-javadoc}.jarta-lib-<version>{,-sources,-javadoc}.jar(the version followsVERSION) with coordinatesio.github.ta-lib:ta-liband all Central-required metadata, andtests that jar — the same file
./mvnw -Prelease deploywould upload (A2). Thisissue is the single checklist of what is still open before the first publish.
Status note, 2026-09-19
B1 closed. The key that item assumed existed was unusable, so it was replaced and
the old one revoked; details in B1. Still open in B: B2, B3, B4, B6.
The publish procedure left this issue.
docs/release-runbooks/holds everyrelease runbook now, with per-backend step prefixes (CP, JP, RP, PP) and its
README.mdholding the order. The post-release version bump is its own
post-publish-runbook.mdrather than the C runbook's tail, because
VERSIONis one number for every backend: abump before the bindings publish makes them carry the next version, which
79a013707already had to undo by hand for ta-lib-python.
The Java release is cut from
main, like every other. That mattered more than itsounds: the naming fold (D8) was on
devonly, and publishing frommainbeforehandwould have put
RetCode.BadParam,TaLibFailureandmetadata.FunctionInfoon Centralimmutably, with 0.8.2 then renaming all of them. Merged with
scripts/merge.py, bothbranches at
034f9398f.Status note, 2026-09-13
D8 LANDED on
dev2026-09-18 (36cd8ac1f). Batch and lookback method names, thecoined enums, the failure types and the metadata tier all follow
docs/naming-spec.md.Section B is otherwise unchanged from the 2026-09-10 note.
Status note — 2026-09-10
C4b and C5 closed on
dev. Section B is now the whole issue, and every item left init is maintainer or account work: B1 (GPG key), B2 (Portal token), B3 (deploy), B4 (the
website banner, which must stay while v0.8.1 ships C only), B6 (announce).
Checked while here, and deliberately not done: B1's one repo-side half, adding
--pinentry-mode loopbacktomaven-gpg-plugin. It is untestable without a key, andhow it should be spelled depends on how the passphrase is held (3.2.x prefers
MAVEN_GPG_PASSPHRASEoversettings.xml). Better decided at the moment B1 is donethan guessed now.
Also settled: nothing open here affects the v0.8.1 C ABI or behavior. The only item
that read as though it might was C4b's claim that those handles were covered by nothing
at all; that was never true for C, whose peek leg compares handles rather than values.
See C4b below.
Status note — 2026-08-31
Audit of the whole list against
devbbe63f18e. Two items moved, one item split, twoadded:
799f5c496) replaced the eight-handle spot-check with aregistry-driven sweep of all 176. Split into C4a (done) and C4b (the 14 handles that
sweep cannot observe). C1 and C3 were already ticked and re-verified in the tree.
and nothing owned it (fix(java): the published pom still advertises 174 indicators; 176 ship #317, fix(ta_codegen): generate rewrites the Java pom's indicator count #320).
Rust: remaining loose ends before the first crates.io publish #179 E5, which Kevin closed for Rust in ci(main-nightly): the release branch never compiled the crate it publishes (#179 E5) #306; the Java half was never mirrored.
B4, B6. B4 verified still open in the tree — all six
website/src/api/{java,rust,csharp}{,/stream}/README.mdstill carry the"Not yet released — Q1 2027" banner, and the top-level
README.md(11 lines) mentionsno binding at all.
Status note — 2026-08-27
Nothing on this list moved, but one thing behind it did:
docs/error-handling-spec.mdAppendix D now has no open items in any backend, Java included. C2 below (array-length
validation) was the last Java entry on it; #268 finished the streaming opener's argument
contract, so a Java caller's rejection is named, typed and ordered the same way the batch
tier's is. Nothing here is blocked on the error contract any more.
A. Blocking the first publish
A1 — no LICENSE/NOTICE in any of the three jars. The project's own BSD-3
clause 2 requires binary redistributions to reproduce the copyright notice and
disclaimer "in the documentation and/or other materials provided with the
distribution". The main jar has 222 entries, none of them the licence. Fix with a
<resources>entry pointing at the repoLICENSE, ormaven-remote-resources-plugin.A2 — nothing, anywhere, has ever tested a
.jar. Closed bye4289b4eb(branchfix/172-java-jar-gate), which resolves it by removing the secondbuilder rather than by proving two builders agree. B5 dissolves into this — there
is no second builder left to reconcile.
Maven is now the only builder.
ta_codegen build --backend=javaruns./mvnw clean package(tests skipped) and then tests that artifact — the same file./mvnw -Prelease deployuploads. Nothing tests a class directory; there is no secondjar; byte-identity is a dead question, with nothing left to assert.
The design that was tried first, and dropped
A JDK-only jar built with
-gto match maven-compiler-plugin's<debug>true</debug>.It worked — every class byte-identical to Maven's, with the exception set being only
META-INF/maven/**and two annotation-freepackage-info.classmarkers thatmaven-compiler-plugin synthesizes itself (
createMissingPackageInfoClass;-Xpkginfo:alwaysdoes not reproduce them, differing in constant-pool order). It wasdropped because keeping it means keeping a standing proof that two builders agree, and
every "is the tested thing the shipped thing?" investigation on this issue is a cost of
having two. Recorded so nobody re-derives it.
Through a committed wrapper, not an installed
mvn.mvnw,mvnw.cmdand.mvn/wrapper/maven-wrapper.propertiesnow live beside the pom../mvnwis not adifferent Maven: it downloads the exact Apache distribution pinned there, verifies it
against
distributionSha256Sum, and execs the realmvninside it — same binary, sameplugins, same
~/.m2. So the backend needs no Maven installed, and "which Mavenproduced this release" has one answer on every machine.
distributionType=only-script,so no
maven-wrapper.jarbinary is committed. Pinned to Apache Maven 3.9.16 (4.x isstill RC). Checksum provenance is recorded next to it: the zip's SHA-512 matched the
.sha512published on both repo.maven.apache.org and downloads.apache.org, and theSHA-256 was computed from that verified file, since Central publishes no
.sha256.unzipis a real prerequisite. Without it the wrapper quietly falls back to the.tar.gzdistribution and then fails the pinned SHA-256 with "your Maven distributionmight be compromised" — a missing utility, not an attack. The gate probes for it first
and says so, and it is in
LANG_PREREQS["java"].Assertions on all three published archives
ta_codegen/input/function on the main jar'sCoreas exactly two publicOutRangeoverloadsjavapoff the archive, matched by name set so a rename or a lost overload fails too; pinned against the input tree, never a literaljar --listMETA-INF/LICENSEin main + sourcesjar --list. The javadoc jar is exempt by design — the plugin consumes no<resources>, so the pom renders the notice via<bottom>; the gate extractsCore.htmland asserts the rendered footerio.github.talibjar --describe-module(absent entry →ta.lib)Artifact names come from
target/maven-archiver/pom.properties, written by the builditself — not a pom re-parse, not a glob.
The gate's own javac and javadoc runs are deleted: pom.xml already carried
<doclint>all,-missing</doclint>, so the doclint teeth moved to maven-javadoc-pluginintact (a bad
@seenow failspackage). Net effect on gate wall-clock: ~33 s → ~30 swarm, ~36 s cold including downloading and verifying Maven itself.
Sabotage-proved with clean controls either side:
mvnwabsent,unzipabsent, afunction removed from
input/, afloat[]overload deleted,<Automatic-Module-Name>renamed and mis-declared, the LICENSE
<resources>mapping removed, the javadoc<bottom>removed, a valid*Testcompiled into the shipped package, ajunit-importing suite added, a doclint break, and a corrupted
distributionSha256Sum.That the suites bind to the jar is decisive rather than structural: with the class
directory deleted they still pass; with the jar corrupted they fail.
Two fail-open holes closed while here
line — compiled by neither Maven nor the gate, and green. That is the
AllTestsvacuity discovery exists to prevent, and it became reachable in one line the moment
Maven could supply the dependency. Now a hard failure.
javadoc_notice_presentextracted into a fixed/tmppath that it begins bydeleting, racing other worktrees. It uses
bin/like every other scratch path.A3 — no
package-info.javaanywhere, so every package page in the publishedjavadoc has no description. That javadoc is the shop window for a first release.
A4 (added and closed 2026-08-31) — the pom's own
<description>advertised astale indicator count. It said 174 while 176 shipped, and that string is published to
Central and immutable per version. A2's gate reads
target/maven-archiver/pom.propertiesfor the artifact names and never looks at the description, so nothing owned it — the same
class of defect as Rust: remaining loose ends before the first crates.io publish #179 A2 (three hard-coded "161"s in the crate's shop window), in the
file that is the Maven search result. Found and fixed by fix(java): the published pom still advertises 174 indicators; 176 ship #317;
generatenow rewritesit (
sync_pom_indicator_count,main.rs) so regen-check is the standing gate (fix(ta_codegen): generate rewrites the Java pom's indicator count #320),with
scripts/pre-release-checks.pyas the release-time backstop for a tree wheregeneratewas not re-run.Superseded by
43ab73ae9: the description claims a fixed "200+ indicators" floorinstead of a count, and
scripts/pre-release-checks.pyfails the release ifinput/drops below 200 functions or the pom stops saying it.
Sting worth recording: the first fix put the "generated — never edit" note in an XML
comment containing
--mid-body, which is malformed XML. Maven's POM parser rejects it,so
xlang: all langs vs pre-cutover Cand synth-gate's Java leg went red on dev(
525a1408b). The pom is parsed, not just read — a comment in it is executablesurface.
B. Maintainer / account work
This section absorbed
docs/java-deprecation-runbook.md, now deleted: its Phases 0, 1and 4 shipped (website links, the buildable Maven project, the removal of the legacy
java/tree), its decisions are all locked and visible in the tree, and everything fromit still open is below. The two procedures it described that recur per release are
documented where they are used, not here: the release command, the wrapper rule and why
autoPublishis false live inpom.xml'sreleaseprofile; how to bump the pinnedMaven, and the provenance of its checksum, live in
.mvn/wrapper/maven-wrapper.properties.Namespace
io.github.ta-libverified (2026-08-05). Worth recording because thestep is non-obvious:
io.github.<user>is auto-granted at signup butio.github.<org>is not —ta-libis an org, so it took the manual path (thePortal issues a verification key; create a public repo in the
TA-Liborg namedexactly that key; verify; delete the repo). Sonatype's docs say org namespaces are
unavailable as automatically registered namespaces, which reads like a refusal and is
not. Verification does not expire, is not re-checked, and the proving repo is
disposable — but what it is anchored to is the org's identity, so never rename or
transfer the
TA-Liborg. GitHub-anchored rather thanorg.ta-libprecisely so thenamespace does not depend on renewing
ta-lib.org: a DNS-verified namespace becomesclaimable by whoever buys the lapsed domain.
B1 — GPG key (closed 2026-09-19). Signing key
rsa4096/4F36E9F467C8547D, fingerprintB351CC3D856EE4AC3DBFD4C44F36E9F467C8547D, uidMario Fortier <mario4tier@users.noreply.github.com>, never expires, nopassphrase. Live on keyserver.ubuntu.com with its uid, and on
keys.openpgp.org without one (that server publishes a uid only after mailing a
confirmation link, which a noreply address can never receive; Central validates
key material, not the uid).
The key named in the original item,
46F000E9CE598271from 2026-08-06, wasunusable: its passphrase was lost and was recorded nowhere. Nothing had ever
been signed with it, so it was replaced and then revoked on both keyservers
with its
.revcertificate, which needs no passphrase.No passphrase is deliberate. A signing key cannot publish anything; the
Portal token (B2) authorizes a release and already sits in plaintext beside it.
Adding one later is
gpg --change-passphraseon the same key, with nothing torepublish.
Addis--pinentry-mode loopbackto themaven-gpg-pluginconfigobsolete advice and was not done. Verified in maven-gpg-plugin 3.2.8's
bytecode (
GpgSigner.generateSignatureForFile):--batch --pinentry-mode loopback --passphrase-fd 0sit entirely behindif (passphrase != null). Theplugin adds loopback itself when it has a passphrase; with none it passes no
--batchand lets pinentry prompt, which is correct on a tty. Loopback without--passphrase-fdwould only fail differently.What was needed is
<keyname>in the release profile (034f9398f): gpg signswith the first secret key in the keyring when none is named, and with the
revoked key still present it kept picking that one and hanging. An absent pinned
key fails at
verifywithNo secret key, before any upload. Pinning is notlock-in: Central registers no fingerprint against a namespace and never checks
key consistency between releases, so rotating is that one line plus a keyserver
upload. Sonatype's own docs recommend the pom
<configuration>for exactly thismulti-key case.
Back up the
.revcertificate. The key has no passphrase, so a lost laptopis a compromised signing key and that file is the only kill switch. The key
itself is worth backing up only to turn a rotation into an import.
B2 — Central Portal user token in
~/.m2/settings.xmlunder<id>central</id>(the pom's
publishingServerId). Shown once in a modal and never retrievable again,only replaceable. This token, not the GPG key, is the credential that authorizes a
release — on a stolen laptop it is the one to rotate first, from any browser, since a
signing key alone cannot publish.
Proven 2026-09-19: the 0.8.1 upload authenticated with it (
Using credentials from server id central in settings.xml). Nothing had ever exercised it before that.B3 — publish (done 2026-09-19). The procedure is now a runbook,
docs/release-runbooks/java-publish-runbook.md, reached from that directory'sREADME.md; this item is just the checkbox.0.8.1 is on Maven Central, published from
mainatd2bfc74d55bea3eea06127f3357f971d86176bbc, taggedjava-v0.8.1, deployment6f78a093-5bb6-4a5c-bf9c-20c0e9580cba. Verified by downloading the pom, jar, sources,javadoc and signatures from repo1, and by resolving the coordinate into an EMPTY local
repository and calling SMA/RSI through it, so the copy
installhad put in~/.m2could not mask a failure. Central's own validation is also what first proved the signing
key is fetchable from a keyserver, which no local check could establish.
Do not re-investigate this: after publishing, the Portal status endpoint returns
PUBLISHEDalongsideerrors: {'common': ['Deployment components info not found']}.That is the components record being cleared on completion, not a failure.
The old checkpoint in this item was not performable. It said to resolve
io.github.ta-lib:ta-lib:<version>from a clean consumer project beforepromoting. Sonatype's Portal has no staging repository, and a deployment that
has validated can only be published or dropped, never resolved. The runbook
therefore checks the artifact twice, and neither check is the old one: before
the upload against the locally installed artifacts (
./mvnw -Prelease installplus
scripts/java_consumer_check, which depends on the coordinate the way auser does), and after promotion against Central with
-Dmaven.repo.localset toa throwaway directory so the installed copy cannot satisfy it.
Tag after, never before. Nothing in the Maven build reads git (the version
comes from
VERSIONthroughsync.py), so a tag is a record, not aningredient. Dropping a pending deployment then leaves no tag to move, which is
what
0c11d8c22had to forbid on the C side. The jar carries no commit sha(
BuildStampholds only a gencode digest), sojava-v<version>at the recordedcommit is the only later answer to which tree is on Central.
Rollback: drop the pending deployment in the Portal. Nothing is public until
promoted by hand, which is the whole reason
autoPublishis off: Central isimmutable, so a released bad artifact cannot be withdrawn.
The
releaseprofile runs maven-clean-plugin atinitialize(78a8c0fd0).Without it a warm
target/made this step repackage the PREVIOUS javadoc, witha new public class missing from the javadoc jar and a doclint break passing with
BUILD SUCCESS, because maven-javadoc-plugin decides whether to skip from its own
command line, never from the sources.
B4 — drop the "Not yet released — Q1 2027" banner (done 2026-09-19,
e307a633e). Gone from bothwebsite/src/api/java/README.mdandwebsite/src/api/java/stream/README.md; the Rust and C# banners stay, being still true.Section 2.0 also gained a link to the Central listing, where a reader finds versions
other than the one the snippet pins.
its— nothing sets it by hand<version>should be set to whatever B3 promotesany more.
sync.pyrewrites that token from what Maven Central reports(
maven-metadata.xml's<release>), so the page follows the registry on its owncadence rather than
VERSION. The added prose deliberately carries no version of itsown, since a second copy would have nothing keeping it in step.
B5 — no CI job runs
mvnat all. Dissolved with A2: the nightly's Java jobsrun
regtest.py->cargo run -- build, which now is./mvnw clean packageplusthe three-jar assertions. No new CI job and no Maven install were needed — the wrapper
self-installs, and
unzipis already present on the runners.B6 — announce the coordinate (settled 2026-09-19). The announcement is the
website page, and nothing else. Both halves this item originally asked for were
declined by the maintainer on the day of the publish:
a user-facing— the CHANGELOG is the C release's and staysCHANGELOG.mdentrythat way, so a binding publish never earns an entry, first release included.
post-publish-runbook.mdstep PP2 is the only CHANGELOG step in a release round, andit is the C one.
a root— written and then rejected; the root README stays aboutREADME.mdnoteTA-Lib Core and points at ta-lib.org.
Recorded so neither is re-proposed for the Rust or C# publishes.
No relocation POM (maintainer, 2026-08-05) — closed, recorded so it is not
re-litigated.
com.tictactec:ta-lib:0.4.0is live on Central; it is immutable andstays resolvable forever, so nothing we publish can break an existing pin, and nothing
we publish is a drop-in replacement for it either. A relocation would have to go out as
a new version under the old coordinate (
com.tictactec:ta-lib:0.4.1), which needspublish rights to a namespace verified against
tictactec.comyears ago and not worthrecovering for a signpost — and the two APIs have diverged far enough that a resolved
relocation would not have compiled unchanged anyway. Purely additive if the old
credentials ever turn up.
C. Test coverage gaps
C1 —
FunctionInfo.newCall(Core)is never exercised.Dispatchreads thegiven
Corein two places,lookbackandcall; both are now pinned bynewCallCarriesTheGivenCoreinMetadataTest(thecallhalf via test(java): the binder's call() path never read the Core it was handed (#172 C1) #296 --measured beforehand to be the unexercised one: hardcoding
Core.DEFAULTinDispatch.callleft all six Java suites green).C2 — no array-length validation, and no test for what happens without it.
Measured against the shipped jar:
ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3endIdxpast the input endAIOOBE: Index 200 out of bounds for length 200AIOOBEAll thrown from deep inside the loop, after the output buffer is partially
written, with no
OutRangetelling the caller how far it got. C cannot check this— it has no sizes. Java can: arrays carry their length. A fail-fast
IllegalArgumentException("endIdx 500 exceeds input length 200")in the guardedwrapper is a strict improvement and is testable. Memory-safe either way (the JVM
always bounds-checks), so this is contract quality, not a correctness bug.
Done. Both overloads of all 174 functions now carry the bound the Rust backend
already asserts: every input the body indexes must reach
endIdx, every output musthold the values actually produced (
endIdx - max(startIdx, lookback) + 1— theproduced count, not the width of the range). Emitted from
backends/java.rs, so itcannot drift per function. The core keeps its own diagnoses: a bad index still raises
IndexOutOfBoundsException, an out-of-range parameter still names the parameter, and arange shorter than the lookback is still an empty success with no output-length check.
Covered in
BatchApiTestfor each row of the table above, both sides of the boundary,and that a rejected call leaves the buffer untouched.
Writing it turned up a second thing, now Java: more test coverage for unexpected I/O #235: nothing asserted that a call producing
no values also reads nothing.
NoPhantomIoTestdoes, by passing zero-length arrayswhere no I/O is expected, and its first run caught APO/PPO/PVO reading the input on a
range too short for them (fixed; value-neutral).
C3 —
discover_java_testswarns where its own doc comment says it fails(
ta_codegen/generator/src/main.rs). A*Test.javawithoutmain()printsWARNING: ... not runand the build stays green; the comment claims it is"reported and counted as a failure: silently skipping it would recreate exactly the
vacuity this replaces". The comment is the correct behaviour.
C4a — the streaming suite touched 8 of 176 handles. Closed by test(java): prove copy() independence and peek() non-commit on every stream handle (#172 C4) #281
(
799f5c496).peekAndCopyHoldOnEveryHandlewalksFunctions.all(), opens everyregistered handle by reflection on its own lookback, and asserts
peek()non-commit andcopy()independence on all 176 — a handle it cannot reach is a named failure, not asmaller sweep.
A second registry-driven pass pins every multi-output handle'sSuperseded by Java streaming: multi-output update() allocates a Value record every bar because it is cached for value() #310: that pass now requires each stream-capableValueasa record with one component per registry output, the expectation derived from the
registry rather than a literal so the synth gate's injected fixtures do not turn a
correct tree red.
function's
<N>Outsink to have one public field per registry output and to not be arecord. Non-vacuity is counted:
swNonCommit/swCopymust equal theregistered count, and
swMovedmust exceed half — the corpus has to move most handlesoff their open value or every property above is satisfied trivially.
C4b (closed 2026-09-10, dev
0e6a4cc93) — the handles a corruption could notbe named on.
peeksteps the handle rather than a scratch copy, and the sweep'sonly observable was the value the handle reports, so on a candlestick whose pattern
this suite's series never fires every assertion read 0 == 0 whichever way the peek
behaved.
Closed by changing the observable, not the corpus.
peekAndCopyHoldOnEveryHandlenow diffs the peeked handle against the never-peeked twin field by field, walking
arrays element by element and sub-handles recursively. No pattern has to fire for a
committed peek to be visible, so neither the MC/DC corpus (testing: MC/DC coverage for the remaining 55 candlesticks #219) nor
FUZZ_CANDLE'sliteral bars were needed, and Java does not acquire a third copy of that catalog to
keep in step with C and Rust.
That is what the C server's peek probe has always done (
sv_steq_TA_<N>, generatedinto
ta_codegen_serve.c), which is also why the closing sentence this item used tocarry was wrong: the
stream_verifyblindness it cited is in the value legs, andthe peek leg does not compare values. C was never exposed here.
Sabotage-proved on the shipped jar, clean control either side:
sub0.updateinstead ofsub0.peeksub0.sub.ring_trailingIdx_inReal[0], plus two value checksThe third row is a class no Java gate covered:
no_java_peek_copies_the_handleclassifies a line by what it writes, and a sub-stream commit writes nothing of its
own. C gates it (
peek_suite::no_peek_entry_point_commits_a_sub_stream), Rust cannotcompile the bug at all (
peek(&self)binds the sub-handle shared), and C# hadnothing until
no_csharp_peek_copies_the_handlegrew the same arm beside this work(dev
69b205cb7; 22 sites named when the frame emits the committing verb).Three non-vacuity floors, absolute: field values compared, handles carrying an array
field, handles holding a sub-stream.
C5 (added 2026-08-31, closed 2026-09-10, dev
c4fdf617f) — the release branchnever built the jar it publishes.
A2's three-jar assertions — LICENSE entry, module name, the two-public-overload sweep
pinned against
input/, no test class in any archive — live inta_codegen build --backend=java, which is reached only throughregtest.pyindev-nightly's
cross-languagejob.main-nightly-tests.ymlrunstest,fuzz-vs-064,xlang-hash,rustandregen-check; none of them invokes./mvnw.xlang-hashlooks like it should, and does not: it builds the Java server, whichembeds its own
Corecopy, not the Maven artifact.So on the branch a release is cut from, nothing has ever built the jar. This is exactly
Rust: remaining loose ends before the first crates.io publish #179 E5, closed for Rust by ci(main-nightly): the release branch never compiled the crate it publishes (#179 E5) #306 — which added a
rustjob to main-nightly forprecisely this reason and left the Java half unmirrored. The uncovered window is the
same one ci(main-nightly): the release branch never compiled the crate it publishes (#179 E5) #306 names: the commits only
mainsees — the dist bot's (they rewritetracked files), hotfixes, and the release cut itself.
Closed by a
javajob inmain-nightly-tests.ymlmirroring thatrustjob:ref: main, a JDK the only thing installed (the committed wrapper fetches andSHA-256-verifies its own Maven, and
unzipis already on the runners), thencargo run --release --manifest-path ta_codegen/generator/Cargo.toml -- build --backend=java. Verified from the repo root, ~60 s warm. Not gated ontest, forthe reason the Rust job is not: no C build is involved, so a dist-pool failure must
not skip the release branch's only Java coverage.
Related and already tracked: #170 (the default-sentinel vector never reaches theJava/C# float overloads). Note the servers do now carry
use_floaton all 168functions (176 today) and
test_codegen.casserts the leg is non-vacuous, so float values areverified cross-language; #170 is specifically about the sentinel parameter class.
#170 closed 2026-08-07.
D. API polish — cheap now, breaking after the first release
D1 —
CoreandCoreBuilderare notfinal.Core's javadoc promises deepimmutability and JLS 17.5 safe publication "even when published racily"; a subclass
with one non-final field voids that, and
CoreApiTest.coreFieldsAreFinalonlyreflects over
Core.classitself. There is no extension point to preserve.D2 — 168 stale javadoc lines contradicting immutability. Every stream class
says "Do not mutate the owning
Core's settings while streams opened from it arelive" (template at
backends/java_stream.rs:584).Corehas no mutators — thissurvives from the pre-immutable era and ships in the published javadoc.
D3 — two different value-object idioms.
OutRangeis arecordwithaccessors (
r.begIdx()); everyXxxStream.Valueis a hand-rolled class with publicfinal fields (
v.macdSignal) plus hand-written equals/hashCode/toString. On aJDK-17 library these should be records — identical
doubleToLongBitssemantics,~40 fewer generated lines per function.
Superseded by Java streaming: multi-output update() allocates a Value record every bar because it is cached for value() #310: the per-bar
Valuerecord is gone. Multi-output stream valuesare
<N>Outsink classes with public fields (Core.BbandsOut), andStreamSmokeTestfails if a sink is a record.
OutRangestays a record.D4 — duplicate javadoc on the range constants.
Core.java:197/199both read"Widest value a
doubleoptional parameter may take" forTA_REAL_MINandTA_REAL_MAX;:203/205likewise for the int pair. Hand-written scaffolding.D5 —
fillRange()returnsnullafter a plainopen(documented, but anullable return in a new API).
OutRange.EMPTYorOptionalwould be better.D6 —
Dispatch.javaships with broken indentation —import io.github.talib.OutRange;and the whole class body carry a ~9-space leak from theemitter's raw string. It is in the sources jar, so every user who opens it in an IDE
sees it.
D7 — C-namespace residue in Java names:
Core.TA_REAL_DEFAULT,TA_INTEGER_MIN, ... insideio.github.talib.Core; andgetUnstablePeriod(bean-style) against the builder's fluent
unstablePeriod(...). Purely cosmetic, buta first release is the only cheap moment.
D8: the Java names the naming spec changes. LANDED on
dev2026-09-18.core.rsi(..),core.rsiLookback(14),core.cdl3blackcrows(..);RetCode.BAD_PARAM,CandleSettingType.BODY_LONG,RangeType.REAL_BODY; the failure types take theTALibprefix and the metadata tier takes C's words.
MAType.SMA,FuncUnstId.HT_DCPERIOD,byName("RSI"), the javadoc and website prose and the parameter names are unchanged(spec E1/E2). C# followed the same spec in the same commits; Rust: remaining loose ends before the first crates.io publish #179 C16 is the Rust half.
Verified on the landed tree: the PR gate's fourteen legs,
build.py regtest(all fourlanguages 161/161) and
regen-check. An adversarial review after that found a Javaride-along casing bug (
ridesma), eight assertions the rename had made unfalsifiable andfive shipped docs still naming
*_Lookback; all are fixed inaf88df569.E. Informational — no action proposed
Core.classis 2.3 MB; the first indicator call in a fresh JVM costs ~46 ms ofclass load + verification (measured; the second call is 0.004 ms). Irrelevant for a
server, visible in a CLI or lambda. Forced by Java having no
partial class— theescape, if it ever matters, is Core methods delegating to per-indicator
package-private classes so loading becomes lazy.
fragments/moved out oflibrary/(this branch), solibrary/now contains exactlythe publishable Maven project.