@@ -17,16 +17,22 @@ are expensive to rediscover, and that the code alone does not explain.
1717 ` *BufferFunction ` , ` Context* ` , ` FieldIndex* ` ) that lets consumers parse without
1818 allocating per field.
1919- ` jmh/ ` — standalone Gradle build (` includeBuild("..") ` , so it always benches local
20- sources). ` IocBench ` and ` MigrationBench ` over two real documents. Its ` README.md `
21- holds the FieldMatcher migration decision table and the measurement methodology —
22- that table is the justification for the current deprecations, so read it before
23- touching the dispatch APIs.
20+ sources). ` IocBench ` , ` MigrationBench ` , and ` SourceBench ` over two real documents.
21+ Its ` README.md ` holds the FieldMatcher migration decision table and the measurement
22+ methodology — that table is the justification for the current deprecations, so read
23+ it before touching the dispatch APIs.
2424- Releases are automated: conventional commits → release-please (` always-bump-patch ` ,
2525 no ` v ` prefix in tags) → publish. Don't hand-edit versions or ` CHANGELOG.md ` .
2626
2727Build and test with ` ./gradlew check ` . Dependencies resolve from GitHub Packages, so a
2828` savaGithubPackagesUsername ` / ` savaGithubPackagesPassword ` pair is needed in
29- ` ~/.gradle/gradle.properties ` (see ` README.md ` ).
29+ ` ~/.gradle/gradle.properties ` (see ` README.md ` ). Beyond ` check ` , the hardening tasks
30+ (sava-build's ` hardening ` extension in ` json-iterator/build.gradle.kts ` ): PIT mutation
31+ suites ` pitestIterator ` / ` pitestNumbers ` / ` pitestUtil ` (reports under
32+ ` build/reports/pitest/<suite>/ ` ) and the Jazzer fuzz targets described under
33+ correctness landmines. A standing task: the iterator suite's surviving/uncovered
34+ mutants are to be revisited only after the ` readObject ` /` readObjField ` removal (see
35+ settled decisions) — most of them live in that deprecated plumbing.
3036
3137## Correctness landmines
3238
@@ -42,8 +48,24 @@ on the chars path. A scan-path change that passes a smoke test can still be badl
4248 offset in a window rather than testing one happy path. If you touch string scanning,
4349 escape decoding, base64, or ` skipUntil ` , these are the tests that will catch you —
4450 and if you add a new scan path, it needs a sweep of its own.
51+ - ** The heavier police are the differential fuzz targets** — ` fuzzJson ` (byte vs char
52+ sourced iterators must produce identical event streams or both reject), ` fuzzDouble `
53+ and ` fuzzNumber ` (JDK bit-equality oracles), ` fuzzInstant ` . Run the relevant one for
54+ a few minutes after any scan or parse change:
55+ ` ./gradlew :json-iterator:fuzzJson -PmaxFuzzTime=120 ` . The harness contract is
56+ strict — ` JsonException ` (plus ` DateTimeException ` for instants) is the only
57+ accepted rejection on any source, so crash-class regressions (bounds faults, stale
58+ reads past ` tail ` ) surface as findings, not noise. When a run produces a crash
59+ input, fix the bug, then promote the input into the seed corpus
60+ (` src/test/resources/fuzz/<target>/regression-* ` ) so it replays forever.
4561- Multibyte lead bytes are ** negative** as signed ` byte ` s, and ` 0x80 ` appears
4662 mid-character in ordinary text — neither is a safe sentinel.
63+ - ** Never index a lookup table with a raw source value.** A signed byte is negative on
64+ multibyte content and a char exceeds any byte-sized table, so ` TABLE[c] ` faults with
65+ the wrong exception class instead of rejecting. One fuzzing session found this in
66+ three separate tables (digit resolution, ` VALUE_TYPES ` , ` JHex ` ); the surviving
67+ shapes are range-checked arithmetic (` BaseJsonIterator.intDigit ` ) or a guarded
68+ accessor (` ValueType.of ` ) — use those, don't add a fourth table.
4769- ` \/ ` is legal JSON, and ` / ` is in the base64 alphabet. Escape handling and base64
4870 decoding interact.
4971- Surrogate pairs arrive as two separate ` \u ` escapes and must be validated as a pair.
@@ -68,6 +90,16 @@ outright removal or a silent behavior change; and workload assumptions measured
6890local consumers (all-` byte[] ` input, field-name lengths, dispatch widths) are
6991defaults to optimize for, not invariants to depend on for correctness.
7092
93+ Removing deprecated surface follows the same doctrine, as a procedure: a
94+ ` @Deprecated(forRemoval = true) ` marker must ride at least one published release,
95+ and removal is cleared per member by re-running the consumer sweep — grep every
96+ jsoniter-importing ` .java ` file under ` ~/src ` (skip ` forks/ ` , build output, and dead
97+ code like idl-clients-drift), then weight each hit by its repo's last commit date.
98+ Zero hits or dormant-only hits (repos untouched for a year or more) clear removal;
99+ any actively-committed consumer blocks it until those call sites migrate. Do not
100+ trust a stale survey — the sweep is cheap, repo activity changes, and the hit list
101+ rots faster than this file is edited.
102+
71103** ` FieldMatcher ` fields sit directly above the predicate that consumes them** , not at
72104the top of the class with the other static fields. The matcher's declaration order
73105defines the ` case ` indices of the switch that dispatches on it — that coupling is
@@ -80,10 +112,22 @@ full suite. The headline: `FieldMatcher` wins big on large unions (~40% at 37–
80112and on kind/discriminator dispatch (~ 10% and zero allocation), but the char
81113` fieldEquals ` ** chain is deliberately not deprecated** — it is the fastest option below
82114roughly 8–10 names. Don't "modernize" small enums onto the matcher for performance;
83- there isn't any. The deprecations (` readObject ` /` readObjField ` , the
84- ` testObjField ` /` applyObjField* ` family, the masked-buffer ` testObject ` overload) rest on
85- API consistency, not on speed — ` readObjField ` 's String-per-field loop actually measures
86- * faster* than the char IOC walk under ZGC.
115+ there isn't any. The deprecations rest on API consistency, not on speed —
116+ ` readObjField ` 's String-per-field loop actually measures * faster* than the char IOC
117+ walk under ZGC.
118+
119+ ** One ` forRemoval ` door is still open: ` readObject ` /` readObjField ` ** (plus
120+ ` JsonIterParser ` 's bufSize shim, which goes in the same pass). The rest of the
121+ deprecated surface was removed in 2026-07 under the sweep procedure above; these two
122+ stayed because the sweep found five consumers with 2025–2026 commits (glam,
123+ liquid-stake-serivce, oracle_research, solscripts, rebalance-service — re-sweep
124+ before acting rather than trusting this list). Closing the door is a sequenced task,
125+ not a delete: migrate those call sites (they are single-field/config probes; the
126+ non-deprecated shapes are ` applyObject ` , ` testObject ` , or a matcher dispatch), let
127+ the deprecation ride a published release, remove the members, then re-score
128+ ` pitestIterator ` — the suite's uncovered-mutant mass is concentrated in exactly this
129+ plumbing, so the standing "revisit surviving mutants" task is gated behind this
130+ removal and is meaningless before it.
87131
88132** Data source: feed ` byte[] ` .** Measured in ` SourceBench ` ; the table is in
89133` jmh/README.md ` . The rule matters more than it looks, because the cost of feeding a
@@ -183,6 +227,16 @@ on a full scoreboard, but rows can be of mixed vintage. Delete an archive file t
183227bad run's rows. Every benchmark cross-checks its variants' checksums at ` @Setup ` ; with
184228` -foe true ` a disagreement is a hard failure, not noise.
185229
230+ ** Scope the run to the change; don't default to the full suite.** A full-suite
231+ ` -PjmhFork=3 ` run costs hours; a change-scoped ` -PjmhIncludes ` subset gives
232+ decision-grade rows in minutes (the 2026-07 scan-path A/B ran
233+ ` SourceBench.(bytes_reset|chars_reset),IocBench.blockParse ` — every touched hot path —
234+ in under five minutes per side). Pick rows by which paths the change touches:
235+ ` bytes_reset ` /` chars_reset ` isolate the two source walks, ` blockParse ` adds real
236+ ` readLong ` volume, the dispatch rows only matter for ` FieldMatcher ` /chain changes.
237+ Reserve the full suite for API-shape verdicts that feed the ` jmh/README.md ` decision
238+ table.
239+
186240A same-session control run matters more than a historical baseline: when judging a
187241change, measure ` main ` and the change back-to-back on the same machine in the same
188242session. Comparing against numbers from a prior day has produced wrong verdicts — the
0 commit comments