Skip to content

Commit af02db1

Browse files
dfa1claude
andcommitted
core: Question.instructions() on the sealed interface; clearer local names
Every Question variant already had instructions(); declaring it on the interface removes three type switches that only read it (Sonar S1871 in ClefEngine). Also from Sonar: TokenCounter's private record(...) is count(...) (restricted identifier, S6213), QwenEngine's token-id arrays are letterIds/ digitIds instead of letters/digits next to LETTERS/DIGITS (S1845), and its empty Noul branch says why it's empty (S108). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 9180027 commit af02db1

8 files changed

Lines changed: 22 additions & 28 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
- **`core`: `Question.instructions()`** — the sealed interface declares the accessor every variant already has, so callers read a question's instructions without switching on its type.
1011
- **Breaking: `JsonCodec` is now `Codec`, and it and `HttpTransport` speak `byte[]`** — `writeValueAsBytes`/`readValue(byte[], ...)` (no pretty-printing variant: the CLI's `--verbose` prints compact JSON), `Builder.codec(...)` (was `jsonCodec(...)`), and a `byte[]` request/response body, so JSON goes to and from the wire as UTF-8 with no `String` copy and a binary codec can implement the same SPI; `Jackson2Codec` now writes emoji and other non-BMP characters unescaped, like `Jackson3Codec`. (#35)
1112
- **Breaking: modules split into model (`core`), serialization (`codec`), contract (`client`) and implementations (`client-*`), names aligned** — `core` keeps the model; `Codec` is the new `codec` module; `TypeSafeClient`, `TypeSafeException` and the decorators move to the new `client`, and `DefaultTypeSafeClient`/`ApiKey`/`HttpTransport` to the new `client-http`; `TypeSafeClient.builder()` becomes `DefaultTypeSafeClient.builder()`, and every module's artifact and package now match its directory (`client-http-jdk`, `client-http-okhttp`, `codec-jackson2`, `codec-jackson3`, `client-local`, `client-mapping`, `client-testkit`); see ADR 0003. (#35)
1213
- **docs: local-model setup as three steps** — dependency, Laya download, code, then a model comparison; the optional Clef 4-bit script runs from its GitHub URL with `uv run`, and load errors link to the how-to.

‎client-local/src/main/java/io/github/dfa1/typesafe/client/local/ClefEngine.java‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -221,11 +221,7 @@ private static Map<String, String> options(Question question) {
221221
}
222222

223223
private static Content instructions(Question question) {
224-
Content c = switch (question) {
225-
case Question.Noul q -> q.instructions();
226-
case Question.Choice q -> q.instructions();
227-
case Question.Score q -> q.instructions();
228-
};
224+
Content c = question.instructions();
229225
return c == null ? new Content.Text("") : c;
230226
}
231227

‎client-local/src/main/java/io/github/dfa1/typesafe/client/local/LayaEngine.java‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,7 @@ Sequence sequence(String state, Question question) {
128128
case Question.Score ignored -> "score";
129129
case Question.Noul ignored -> "noul";
130130
};
131-
Content instructions = switch (question) {
132-
case Question.Choice c -> c.instructions();
133-
case Question.Score s -> s.instructions();
134-
case Question.Noul n -> n.instructions();
135-
};
131+
Content instructions = question.instructions();
136132
long[] head = encode(type + " question: " + serialize(instructions).replace("[MASK]", " "));
137133
List<long[]> opts = new ArrayList<>();
138134
for (String option : options(question)) {

‎client-local/src/main/java/io/github/dfa1/typesafe/client/local/QwenEngine.java‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
/**
3030
* Answers from a chat LLM's next-token logits, never from generated text: one prefill per
3131
* question, ending where the answer token goes, then a softmax over just the candidate tokens
32-
* ({@code Yes}/{@code No}, option letters, level digits). Deterministic, nothing to parse.
32+
* ({@code Yes}/{@code No}, option letterIds, level digitIds). Deterministic, nothing to parse.
3333
* Prompted with Qwen2.5's ChatML template.
3434
*/
3535
final class QwenEngine implements Engine {
@@ -54,8 +54,8 @@ interface Forward {
5454
private final Model model;
5555
private final int yes;
5656
private final int no;
57-
private final int[] letters = new int[LETTERS.length()];
58-
private final int[] digits = new int[DIGITS.length()];
57+
private final int[] letterIds = new int[LETTERS.length()];
58+
private final int[] digitIds = new int[DIGITS.length()];
5959

6060
/** @param encode for the ChatML template, {@code encodeText} for the user turn (special tokens stay text) */
6161
QwenEngine(Function<String, long[]> encode, Function<String, long[]> encodeText, Forward forward, Runnable close,
@@ -67,11 +67,11 @@ interface Forward {
6767
this.model = model;
6868
this.yes = singleToken("Yes");
6969
this.no = singleToken("No");
70-
for (int i = 0; i < letters.length; i++) {
71-
letters[i] = singleToken(String.valueOf(LETTERS.charAt(i)));
70+
for (int i = 0; i < letterIds.length; i++) {
71+
letterIds[i] = singleToken(String.valueOf(LETTERS.charAt(i)));
7272
}
73-
for (int i = 0; i < digits.length; i++) {
74-
digits[i] = singleToken(String.valueOf(DIGITS.charAt(i)));
73+
for (int i = 0; i < digitIds.length; i++) {
74+
digitIds[i] = singleToken(String.valueOf(DIGITS.charAt(i)));
7575
}
7676
}
7777

@@ -162,9 +162,9 @@ Answer answer(Question question, float[] logits) {
162162
return switch (question) {
163163
case Question.Noul ignored -> new Answer.Noul(Probabilities.softmax(logits[yes], logits[no])[0]);
164164
case Question.Choice(Content ignored, Map<String, String> criteria) ->
165-
Probabilities.choice(criteria, Probabilities.softmax(pick(logits, letters, criteria.size())));
165+
Probabilities.choice(criteria, Probabilities.softmax(pick(logits, letterIds, criteria.size())));
166166
case Question.Score(Content ignored, List<String> criteria) ->
167-
Probabilities.score(criteria, Probabilities.softmax(pick(logits, digits, criteria.size())));
167+
Probabilities.score(criteria, Probabilities.softmax(pick(logits, digitIds, criteria.size())));
168168
};
169169
}
170170

@@ -180,6 +180,7 @@ private static float[] pick(float[] logits, int[] tokens, int n) {
180180
static void validate(String name, Question question) {
181181
switch (question) {
182182
case Question.Noul ignored -> {
183+
// Yes/No: nothing to check
183184
}
184185
case Question.Choice(Content ignored, Map<String, String> criteria) -> {
185186
if (criteria == null || criteria.isEmpty() || criteria.size() > LETTERS.length()) {

‎client-local/src/test/java/io/github/dfa1/typesafe/client/local/TokenizerInputs.java‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,7 @@ public static void main(String[] args) throws Exception {
4242
Content state = c.request().state();
4343
laya.add(LayaEngine.serialize(state));
4444
for (Question q : c.request().questions().values()) {
45-
Content instructions = switch (q) {
46-
case Question.Noul n -> n.instructions();
47-
case Question.Choice ch -> ch.instructions();
48-
case Question.Score s -> s.instructions();
49-
};
45+
Content instructions = q.instructions();
5046
String type = switch (q) {
5147
case Question.Noul ignored -> "noul";
5248
case Question.Choice ignored -> "choice";

‎client/src/main/java/io/github/dfa1/typesafe/client/TokenCounter.java‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public long totalTokens() {
5151
return inputTokens() + outputTokens();
5252
}
5353

54-
private EvaluateResponse record(EvaluateResponse response) {
54+
private EvaluateResponse count(EvaluateResponse response) {
5555
Usage usage = response.usage();
5656
if (usage != null) {
5757
inputTokens.add(usage.inputTokens());
@@ -69,12 +69,12 @@ private final class Counting implements TypeSafeClient {
6969

7070
@Override
7171
public EvaluateResponse evaluate(EvaluateRequest request) {
72-
return record(delegate.evaluate(request));
72+
return count(delegate.evaluate(request));
7373
}
7474

7575
@Override
7676
public CompletableFuture<EvaluateResponse> evaluateAsync(EvaluateRequest request) {
77-
return delegate.evaluateAsync(request).thenApply(TokenCounter.this::record);
77+
return delegate.evaluateAsync(request).thenApply(TokenCounter.this::count);
7878
}
7979

8080
@Override

‎core/src/main/java/io/github/dfa1/typesafe/core/Question.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
*/
1010
public sealed interface Question permits Question.Noul, Question.Choice, Question.Score {
1111

12+
/** What the question asks; every variant has one. */
13+
Content instructions();
14+
1215
static Noul noul(String instructions, Map<String, String> criteria) {
1316
return noul(Content.text(instructions), criteria);
1417
}

‎docs/reference.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ object holding the question in one field and data it refers to in others:
5151
| `Question.score(instructions, List<String> criteria)` | `instructions`, `criteria` | Rank against an ordered list of labels |
5252

5353
Each `Question` variant's `instructions` component is typed `Content`, not `String` — see
54-
[`Content`](#content-sealed-interface) below.
54+
[`Content`](#content-sealed-interface) below — and `Question.instructions()` reads it from any
55+
variant, without a switch.
5556

5657
### `Answer` (sealed interface)
5758

0 commit comments

Comments
 (0)