Follow-up to 0f1626ece, found when 85 persisted .board files stopped
deserializing: the ancestor walk reflected on classes compiled from the
STRIPPED sources, but javac binds the emitted super.readObject to the nearest
declaration in the FINAL sources. CargoTank extends Counter extends Prop —
Counter regenerates a method covering `count`, so CargoTank's tail computed
against Prop streamed `count` twice and desynced the stream.
findStreamMethodAncestor now asks willDeclare(): an ancestor counts iff it
declares the method in current source (hand-written / substituted frame) OR
this run will generate one for it (concrete, in-fileset, nonempty own tail —
recursive, memoized; the filesets are pre-scanned so out-of-fileset ancestors
are never assumed generatable). Consequence: a subclass whose tail below the
NEAREST post-regen ancestor is empty generates nothing (CargoTank inherits
Counter's method).
The hand-written-custom skip is also per-SIDE now, not per-class: Prop has a
custom readObject but no writeObject, and its write side must regenerate both
for legacy parity and because subclasses emit super.writeObject against it
(the old whole-class skip left those uncompilable).
Verified: all 165 persisted .board files load with the regenerated streamers;
regen is idempotent (second run converts 0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhfHCXYp2ctWi76Z91y9Ut
Three structural fixes to flatten mode, found bringing up games on-device
(Epic A1 Phase 5):
1. Nested classes: processFlatten now recurses through getDeclaredClasses()
(not gated on the outer class's eligibility), so nested Streamables like
BangConfig.Round get deterministic methods too. All detection/substitution/
insertion is scoped to each class's own body region (brace-matched,
comment/string-aware, member-class bodies masked during detection) so an
instrumented outer class can't hide — or poison — a nested one.
2. Ancestor-aware generation: a class whose ancestor declares read/writeObject
now generates `super.readObject(ins)` + only the fields declared below that
ancestor, instead of a self-contained full-set method. A self-contained
subclass method silently dropped the ancestor's side effects for every
subclass that previously inherited it: Prop.init() (server NPE cloning
board props at startRound), Item's _nondb identity extras (itemId=0 on
every wire-streamed item), InvocationMarshaller's _invdir rebind. Byte
layout is unchanged: the ancestor's method streams exactly the flattened
prefix. When the subclass adds no fields, nothing is generated — plain
inheritance is already complete (the field-free marshaller/unit case).
3. Abstract classes: substitution-only. Their default*Object() pass-throughs
are replaced in place with the class's own flattened prefix (making them
safe super-call targets — the reflective pass-through reads the full
dynamic-class set and would double-read the subclass tail), but whole
methods are never generated for them. Requires every Streamable ancestor
to be covered by the task's filesets; see findStreamMethodAncestor doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhfHCXYp2ctWi76Z91y9Ut
The flatten-mode codegen gave each *Marshaller subclass a self-contained
read/writeObject, which overrode InvocationMarshaller.readObject and thus
skipped its side-effect of binding the transient _invdir from the stream's
client -> null _invdir -> NPE on the first invocation request (hit by ART
AND HotSpot clients). Fix: give InvocationMarshaller explicit read/writeObject
(GenStreamUtil, deterministic order, byte-identical to the old reflective
default) that keeps the _invdir binding; field-free subclasses inherit it.
narya 1.22.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhfHCXYp2ctWi76Z91y9Ut
Pre-crash safety checkpoint (box rebooted mid-implementation; NOT verified
complete). GenStreamableTask flatten-mode + GenStreamUtil runtime helper +
ObjectInputStream changes; explicit read/writeObject instrumentation across
presents net + dobj event classes so ART matches HotSpot's ClassUtil.getFields
order without the global sort flag (no data migration). narya 1.21.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhfHCXYp2ctWi76Z91y9Ut
A clean minor-version bump (retiring the Epic 2 -bangN patched-fork suffix —
that scheme is superseded; we now own these forks outright and version them
normally). GitHub Packages won't let us re-publish 1.19, and 1.20 makes the
TLS-capable build obvious in dependency listings.