Skip to content

Java: remaining loose ends before the first Maven Central publish #172

Description

@mario4tier

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 dev bbe63f18e. Two items moved, one item split, two
added:

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 notta-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 config is
    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 promotesnothing 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 entrythe 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.0 is 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.

    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 rust job to main-nightly for
    precisely 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 main sees — the dist bot's (they rewrite
    tracked files), hotfixes, and the release cut itself.

    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.

    Superseded by Java streaming: multi-output update() allocates a Value record every bar because it is cached for value() #310: the per-bar Value record is gone. Multi-output stream values
    are <N>Out sink classes with public fields (Core.BbandsOut), and StreamSmokeTest
    fails if a sink is a record. OutRange stays a record.

  • D4 — duplicate javadoc on the range constants. Core.java:197/199 both read
    "Widest value a double optional parameter may take" for TA_REAL_MIN and
    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 indentationimport 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.

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