Skip to content

feat(antd-java): pointer API (REST + gRPC) - #331

Merged
Nic-dorman merged 1 commit into
mainfrom
nic/v2-1568-pointers-java
Oct 19, 2026
Merged

Nic-dorman merged 1 commit into
mainfrom
nic/v2-1568-pointers-java

Conversation

@Nic-dorman

@Nic-dorman Nic-dorman commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Part of the pointer API fan-out. Stacked on #326; review that first.

AntdClient, AsyncAntdClient and GrpcAntdClient gain the daemon's
pointer surface, with the same models and exceptions on all three:

  • pointerKeys, pointerCreate, pointerUpdate, pointerTransfer
  • pointerGet, pointerResolve, pointerFinality, pointerController
  • pointerPrepare (with a PointerPrepareOptions overload for signed
    quotes) and pointerFinalize, for an external signer

The models are records: PointerRecord (isFinal()), PointerTarget,
PointerFinality, PointerFinalState, PointerController,
PointerTransferResult, PointerWriteResult, PointerPrepareResult,
PointerFinalizeResult and SignedQuoteEntry. PointerOp is a sealed
interface of Create, Update and Transfer. The finality status is an
enum with an UNKNOWN value for statuses a newer daemon may add, and
rawStatus() keeps the daemon's string. Unset values are null on both
transports: REST's null and gRPC's empty string or absent message.
The gRPC stubs come from the existing protobuf Gradle build, which
already compiles every proto under antd/proto, so nothing generated
is committed.

Counters are BigInteger. The SDK's other 64-bit fields are signed
longs holding values far below 2^63, but a final pointer state sits
at 2^64-1 (PointerRecord.FINAL_COUNTER), which a long can only carry
as -1. A long read with Long.parseUnsignedLong would print and compare
wrong unless every caller used the unsigned helpers, while BigInteger
holds the value exactly and compares correctly. Parsing is strict:
ASCII digits only, at most 2^64-1. Anything else, including a sign, a
non-ASCII digit or a JSON number, is an AntdException, not a zero.

Errors map by the daemon's code where it diverges from the status.
POINTER_FINAL and CONFLICT become the new PointerFinalException and
ConflictException, and POINTER_FORKED becomes the existing
ForkException. None of the three reads as AlreadyExistsException.
CLOSE_GROUP_SHORTFALL becomes CloseGroupShortfallException, whose
isRetryable() says a pointer finalize kept the paid state. The three
new classes extend AntdException directly. The shortfall exception
is not a ServiceUnavailableException or NetworkException, so it reads
the same on both transports. REST reads the body's code. gRPC reads
the x-antd-error-code trailer, but only when it agrees with the status
code, so an untagged FAILED_PRECONDITION is still a PaymentException.
Over gRPC, retryable comes from the message's closing "(paid attempt
retained...)" hint. The trailer mapping applies to every RPC, since
the daemon reports a shortfall on any call. No exception type is
sealed, so the new subclasses do not break callers. A daemon before
the pointer API sent a shortfall as a 500, which read as an
InternalException.

No pointer write is retried. Each call to create, update, transfer or
finalize pays, and the javadoc and README say so.

Tests: 108 new unit tests. PointerRestTest covers both REST clients
against MockWebServer, GrpcPointerTest covers an in-process
PointerService, and Example08PointersTest drives the example against
a mock daemon. Together they cover every method's request and
response, counters from 0 to 2^64-1, rejected counters, null and
empty unset values, and the error mapping both ways, including tags
that disagree with the status. LivePointersTest is a round trip
gated on ANTD_TEST_URL, ANTD_TEST_GRPC and ANTD_TEST_POINTER_OWNERS.
It passed against a 10-node devnet:

  • create, update, get, resolve, with bytes fetched back through the
    pointer, and an async read
  • REST finality, and a gRPC create and get
  • a transfer to a final state at 2^64-1, then the controller
  • POINTER_FINAL over both transports, including a gRPC prepare
  • a prepare with signed quotes, then CONFLICT over both transports
    and a BAD_REQUEST finalize without payment

Example08Pointers uploads two pieces of public data, creates a
pointer at the first, updates it to the second, resolves and reads it
back, and prints its finality. It takes ANTD_POINTER_OWNER, plus an
optional ANTD_BASE_URL. Against the devnet it printed counter 1, the
second target's bytes, and finality "open" with 7/7 peers answering.

The README gains a pointer section and the new error rows, and no
longer says the SDK has immutable data types only.

Version bumps happen at release time; nothing here bumps a package version.

🤖 Generated with Claude Code

AntdClient, AsyncAntdClient and GrpcAntdClient gain the daemon's
pointer surface, with the same models and exceptions on all three:
- pointerKeys, pointerCreate, pointerUpdate, pointerTransfer
- pointerGet, pointerResolve, pointerFinality, pointerController
- pointerPrepare (with a PointerPrepareOptions overload for signed
  quotes) and pointerFinalize, for an external signer

The models are records: PointerRecord (isFinal()), PointerTarget,
PointerFinality, PointerFinalState, PointerController,
PointerTransferResult, PointerWriteResult, PointerPrepareResult,
PointerFinalizeResult and SignedQuoteEntry. PointerOp is a sealed
interface of Create, Update and Transfer. The finality status is an
enum with an UNKNOWN value for statuses a newer daemon may add, and
rawStatus() keeps the daemon's string. Unset values are null on both
transports: REST's null and gRPC's empty string or absent message.
The gRPC stubs come from the existing protobuf Gradle build, which
already compiles every proto under antd/proto, so nothing generated
is committed.

Counters are BigInteger. The SDK's other 64-bit fields are signed
longs holding values far below 2^63, but a final pointer state sits
at 2^64-1 (PointerRecord.FINAL_COUNTER), which a long can only carry
as -1. A long read with Long.parseUnsignedLong would print and compare
wrong unless every caller used the unsigned helpers, while BigInteger
holds the value exactly and compares correctly. Parsing is strict:
ASCII digits only, at most 2^64-1. Anything else, including a sign, a
non-ASCII digit or a JSON number, is an AntdException, not a zero.

Errors map by the daemon's code where it diverges from the status.
POINTER_FINAL and CONFLICT become the new PointerFinalException and
ConflictException, and POINTER_FORKED becomes the existing
ForkException. None of the three reads as AlreadyExistsException.
CLOSE_GROUP_SHORTFALL becomes CloseGroupShortfallException, whose
isRetryable() says a pointer finalize kept the paid state. The three
new classes extend AntdException directly. The shortfall exception
is not a ServiceUnavailableException or NetworkException, so it reads
the same on both transports. REST reads the body's code. gRPC reads
the x-antd-error-code trailer, but only when it agrees with the status
code, so an untagged FAILED_PRECONDITION is still a PaymentException.
Over gRPC, retryable comes from the message's closing "(paid attempt
retained...)" hint. The trailer mapping applies to every RPC, since
the daemon reports a shortfall on any call. No exception type is
sealed, so the new subclasses do not break callers. A daemon before
the pointer API sent a shortfall as a 500, which read as an
InternalException.

No pointer write is retried. Each call to create, update, transfer or
finalize pays, and the javadoc and README say so.

Tests: 108 new unit tests. PointerRestTest covers both REST clients
against MockWebServer, GrpcPointerTest covers an in-process
PointerService, and Example08PointersTest drives the example against
a mock daemon. Together they cover every method's request and
response, counters from 0 to 2^64-1, rejected counters, null and
empty unset values, and the error mapping both ways, including tags
that disagree with the status. LivePointersTest is a round trip
gated on ANTD_TEST_URL, ANTD_TEST_GRPC and ANTD_TEST_POINTER_OWNERS.
It passed against a 10-node devnet:
- create, update, get, resolve, with bytes fetched back through the
  pointer, and an async read
- REST finality, and a gRPC create and get
- a transfer to a final state at 2^64-1, then the controller
- POINTER_FINAL over both transports, including a gRPC prepare
- a prepare with signed quotes, then CONFLICT over both transports
  and a BAD_REQUEST finalize without payment

Example08Pointers uploads two pieces of public data, creates a
pointer at the first, updates it to the second, resolves and reads it
back, and prints its finality. It takes ANTD_POINTER_OWNER, plus an
optional ANTD_BASE_URL. Against the devnet it printed counter 1, the
second target's bytes, and finality "open" with 7/7 peers answering.

The README gains a pointer section and the new error rows, and no
longer says the SDK has immutable data types only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Nic-dorman
Nic-dorman marked this pull request as ready for review October 16, 2026 09:07
@Nic-dorman
Nic-dorman changed the base branch from nic/v2-1567-antd-pointer-api to main October 16, 2026 12:46

@dirvine dirvine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formal review: APPROVE

Repository: WithAutonomi/ant-sdk, PR #331.
Reviewed head: 5bade9b6565a4f758b48fe081567b06b7a4d752c. Original merge base: 8e67c611c8f54d032b036bab55bf1b7a2a00b64c.

No material blocker remains in the Java pointer implementation at this head. Complete-source inspection resolves all 11 non-APPROVE panel records. This is a source-based formal verdict with partial panel coverage, not a claim of a successful Java build.

Source and test coverage

  • REST/gRPC pointer clients, PointerWire, model records, typed errors, proto messages, retained-payment hints and test source checked against the frozen #346 daemon. The parent independently checked counter parsing, optional finality counters, tagged status handling and the daemon-generated retention suffix.
  • The parent verified that the Java tree's pointers.proto and common.proto are byte-identical to those in the frozen #346 daemon checkout, and verified each reference checkout's HEAD against its frozen inventory.
  • Tests were inspected, not run. No Java runtime is installed in this environment; no JDK, Maven or Gradle download was attempted.

Findings and adjudication

  • No reachable counter overflow/null failure with this daemon: PointerWire.java:36-66 requires ASCII decimal strings in the unsigned-64 range using BigInteger, with separate optionalCounter helpers. REST and gRPC finality use those optional helpers (:164-183,302-317); write/record/prepare/finalize counters are required daemon strings.
  • The kind_tag narrowing claim is not reachable under the pinned contract: daemon types.rs:706/720/729 uses u8. Unknown kinds preserve the raw tag and degrade to the unknown-kind model rather than failing a conforming response.
  • GrpcAntdClient.java:153-190 checks the x-antd-error-code tag against its matching status before assigning a pointer error. Its end-anchored retained-payment parser matches the exact suffix generated in daemon pointers.rs:714-718. No automatic new payment or paid-write retry is introduced.
  • Signed-quote conversion is not double base64 encoding: the daemon converts its REST base64 strings to proto bytes (grpc/service.rs:146-165); Java converts those bytes back to the shared base64 representation. Client-side AntdException(0) is an existing SDK convention, not a new retry state.
  • Residual compatibility risk, nonblocking: a future daemon that changes the retention-message suffix could suppress the parsed retryable hint. Keep a regression against the daemon format; this verdict is not compatibility assurance for arbitrary future servers.

Panel dissent and its disposition

  • Four CHANGES_REQUIRED records and seven UNCERTAIN records were resolved against complete files. The purported missing models/wire/client implementation was present; fragment-scoped absence was not production absence.
  • The strict-counter objection confused required counter fields with optional finality. The kind-tag objection used the proto's widened uint32 range instead of the daemon's actual u8 domain. Both blocking premises are refuted for this co-release contract.
  • The suffix-anchor and base64 objections are refuted by the daemon's emitted message and conversion code. This adjudication is not an extra independent panel vote; original seat verdicts and unavailable seats remain listed below.

Review team: actual participation

18/24 scope/role/model seats returned substantive, receipt-validated reviews; 3/3 distinct named models. Partial panel; provisional coverage limits apply to this formal verdict. A receipt is evidence of participation, not proof that its findings are correct. Counts below are per evidence scope, not additional independent models.

  • source-1: glm: correctness=APPROVE, adversarial=UNCERTAIN; qwen: correctness=UNCERTAIN, adversarial=CHANGES_REQUIRED; ds4: correctness=unavailable (Incomplete termination; unavailable seat), adversarial=unavailable (Incomplete termination; unavailable seat).
  • source-2: glm: correctness=APPROVE, adversarial=APPROVE; qwen: correctness=CHANGES_REQUIRED, adversarial=CHANGES_REQUIRED; ds4: correctness=unavailable (Incomplete termination; unavailable seat), adversarial=CHANGES_REQUIRED.
  • source-3: glm: correctness=APPROVE, adversarial=unavailable (Incomplete termination; unavailable seat); qwen: correctness=APPROVE, adversarial=UNCERTAIN; ds4: correctness=unavailable (Incomplete termination; unavailable seat), adversarial=unavailable (Incomplete termination; unavailable seat).
  • source-4: glm: correctness=UNCERTAIN, adversarial=APPROVE; qwen: correctness=UNCERTAIN, adversarial=UNCERTAIN; ds4: correctness=APPROVE, adversarial=UNCERTAIN.
  • Models: GLM 5.3 / OpenRouter z-ai/glm-5.3; local Qwen 3.8 / qwen38-27b; local DS4 Flash / deepseek-v4-flash. Missing seats received the configured same-model retry; no replacement model is counted as an independent vote.

Publication gates and limits

  • Live head rechecked against the reviewed head. Live base cea236950084f8738106cc37247c148f0b411d18: binary diff matches the reviewed diff, merge base is unchanged, no intersecting base-drift paths, and trial merge exited 0.
  • GitHub check snapshot: no checks reported; this is not a passing CI result.
  • No Java compilation, unit tests, live tests or paid integration tests were executed. Source review cannot establish runtime or dependency compatibility.
  • No paid or live-network tests. No merge, deployment, source change, or Linear state change. This verdict applies only to the exact head above; it is not a fleet-wide or cross-SDK integration sign-off.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants