pjfanning commented on PR #4006:
URL: https://github.com/apache/fory/pull/4006#issuecomment-5471056433

   ## AI review loop (AI_POLICY.md §5)
   
   Model: Claude Opus 5 (`claude-opus-5[1m]`), via Claude Code, for both the 
authoring and the two
   reviewers. The reviewers ran as separate clean-context sessions with 
different instructions:
   
   - **Fory-guided reviewer** — instructed to read `AGENTS.md` and follow the 
review workflow in
     `.agents/ci-and-pr.md`, plus `.agents/languages/{scala,java}.md`.
   - **Independent general reviewer** — explicitly forbidden from loading 
`.agents/ci-and-pr.md` or
     any copied Fory checklist, per the independent-review carve-out in 
`AGENTS.md`.
   
   Both were read-only: no edits, builds, tests, or pushes. Four rounds, each 
on the diff as it stood.
   Current PR head is `a86a870b4`.
   
   ### Round 1 — `e493fdb86`
   
   Both reviewers independently found the same four issues:
   
   | Finding | Resolution |
   | --- | --- |
   | GraalVM: the companion class, `MODULE$`, and `apply` were never 
registered, so a nested case class would resolve on the JVM but fail in a 
native image | Added `registerScalaCompanion`, following the existing 
`registerScalaEnumerationOwner` precedent |
   | A comment claimed an instance default's "bound invoker already lives in 
the creator metadata" — false; `buildDefaultInvokers` unreflects at runtime for 
both static and instance defaults | Corrected; the real reason to skip 
`registerCreator` is that `creatorHandle` spreads an argument array over the 
exact parameter count and cannot describe a receiver |
   | Recognition forced the companion's `<clinit>` just to answer 
`isCaseClass`, running user `object` bodies during type resolution | Split 
recognition from binding: `Class.forName(..., initialize = false)`, `MODULE$` 
read only once the model is committed |
   | `receiver == null` conflated "static forwarders" with "null singleton" | 
Explicit `staticForwarders` flag |
   
   One suggestion was **rejected with evidence**: both reviewers proposed 
hoisting the per-parameter
   receiver fetch in generated code. Implementing it broke three tests — 
generated code for an
   `Expression` instance is emitted once at its first use site, and each use 
here is a separate
   missing-argument block, so a shared instance referenced a local declared in 
a sibling block. Reverted
   with a comment recording why.
   
   The requested doubly-nested test then exposed a **pre-existing bug in 
`fory-core`**:
   `ReflectionUtils.getLiteralName` skipped its nested-Scala-object correction 
for any canonical name
   ending in `$`, which is every companion module class. A companion two levels 
inside an object
   produced `pkg.A$B$.C$` — Janino: `"pkg.A$B$" declares no member type "C$"`. 
Fixed at the root.
   
   ### Round 2 — `ec34ed31b`
   
   The independent reviewer found a **blocker**: `defaultsReceiver` had been 
inserted into the public
   full `JsonObjectModel` constructor, whose 15-argument form 
`KotlinMetadataModels.kt:182` calls, so
   `fory-json-kotlin` would not compile. The Fory-guided reviewer did not catch 
this.
   
   Resolved by making the full constructor private and restoring the previous 
public signature, so no
   Kotlin file is touched. Verified with `mvn -pl fory-json-kotlin compile` on 
unmodified Kotlin
   sources.
   
   Also resolved this round: the `copy`/`productPrefix` marker was claiming 
types with a *reachable*
   companion but an unsupported primary constructor (varargs, non-public), 
turning previously working
   generic-model handling into a hard failure — now restricted to unreachable 
companions; and the
   workspace-path receiver local ran on every construction, so it was reverted 
to a conditional fetch.
   
   ### Round 3 — `c0d49bae6`
   
   No blockers from either reviewer. Both confirmed the round-2 fixes, and the 
independent reviewer
   re-enumerated every `JsonObjectModel`/`JsonCreatorInfo` call site to confirm 
each binds
   unambiguously.
   
   The notable finding was about the tests: the two rejection tests both passed 
for the wrong reason.
   An outer-bound case class *also* has no reachable companion, so deleting the 
`$outer` check would
   have left both green. Asserting the messages then exposed a second layer — 
the method-local test was
   declared inside the suite *class*, so it captured an outer instance and hit 
the outer branch,
   never reaching the companion branch it claimed to cover. It now lives in a 
method of an `object`.
   
   Also fixed: a `LinkageError` from a companion that exists but cannot be 
linked was reported as
   "companion is not reachable, such as a case class declared in a method"; the 
receiver invariant in
   `JsonObjectModel.validate` was made total; `CompanionOwner` carries the 
`MODULE$` `Field`.
   
   ### Round 4 — `637b5063f`
   
   **Fory-guided reviewer: no actionable findings.** **Independent reviewer: 
would approve**, with
   four non-blocking notes.
   
   The two disagreed on one point, and the independent reviewer was right: the 
round-3 fix for
   `LinkageError` handling had been an overcorrection. Rethrowing inside 
`companionOwner` put a hard
   failure into `isCaseClass`, which is consulted for every non-tuple `Product` 
reaching this module —
   including types it does not own and never claims. An unrelated `Product` 
with a stale companion
   could therefore fail resolution that previously succeeded through the core 
object model, where
   before this PR `isCaseClass` did no class loading at all.
   
   Also from this round, and fixed in `a86a870b4`:
   
   - A comment stated the wrong cause. It said the deeply nested canonical name 
"names no resolvable
     type"; javac resolves it, and the actual evidence was a Janino error
     (`"pkg.A$B$" declares no member type "C$"`). The comment now names the 
real failure.
   - `getLiteralName` is a `fory-core` change affecting all serializer codegen, 
and had only been
     exercised through the JSON path. The `fory-scala` binary serializer suites 
now cover it —
     88 tests, including `SingleObjectSerializerTest`, which exercises exactly
     `object A { object B { case class C } }`.
   - The `$outer` rejection also fires for a case class enclosed by a trait; 
message and doc updated.
   - The native-image main additionally round-trips a nested case class with no 
defaults (which still
     needs the companion to match `apply`) and a doubly nested one.
   
   **`a86a870b4` (the current head) contains these fixes and has not itself 
been re-reviewed**, so this
   is not a clean final review of the head.
   
   **Artifacts:** 
https://gist.github.com/pjfanning/6b9fcd687e22450399df72dabc368e99 — the 
verbatim
   final report of all eight review sessions, extracted from their transcripts, 
with an index.
   
   ## Verification
   
   | Suite | Scala 2.13.18 | Scala 3.3.8 |
   | --- | --- | --- |
   | `ScalaJsonSuite` | 20 passed | 20 passed |
   | `ScalaJsonEnumerationSuite` | 4 passed | 4 passed |
   | `ScalaJsonDerivationSuite` | n/a | 10 passed |
   
   `fory-core` maven suite: 2294 passed, 0 failures, re-run because the 
`ReflectionUtils` change feeds
   every serializer's codegen; `fory-scala` binary serializer suites: 88 
passed, for the same reason. `fory-json-kotlin` compiles against unmodified 
Kotlin sources. Generated
   readers were dumped with `FORY_CODE_DIR` and confirmed to contain the new 
call in both codegen
   paths, including a default that consumes a preceding constructor argument.
   
   ## Known gaps
   
   - **The GraalVM path is unverified.** No GraalVM in the dev environment, and 
nothing in CI runs the
     Scala native-image mains — `ScalaJsonEnumerationNativeImageMain` and 
`ScalaJsonNativeImageMain` are
     referenced by no sbt task or workflow job (pre-existing; the 
`graalvm_json` job builds the Java
     main from `integration_tests/graalvm_tests`). `registerScalaCompanion` is 
therefore checked by
     inspection only. Both reviewers flagged this in every round. The harness 
gained a nested case class
     round-trip so the coverage exists if someone wires it up.
   - **`mvn spotless:check` could not run locally** — google-java-format throws
     `NoClassDefFoundError` under JDK 17 on an unrelated test file. Formatting 
was checked by hand
     against the surrounding style; CI's format job is the first real check.
   
   ## Behavior change
   
   A case class declared inside a `class` or a method previously serialized and 
silently decoded back
   to an all-default instance. Both directions now raise 
`UnsupportedJsonTypeException`. Writing a
   value that can never be read back is the trap this PR fixes, so the write is 
rejected with the read.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to