TranslationArtifactVersionIdTag.parse was declared to return DialectIdTag? and
constructed one. The tag-name guard was right -- it compared against this
class's own TAG_NAME -- so the event id came off the right slot of the right
tag, and nothing behaved differently: all nine call sites reach straight through
to .ref, and the two classes have identical shape. A caller that branched on the
type, or collected parse results across the family, would have got a dialect
reference where a translation reference belongs.
The twenty-four tags in that package were written from one template, so the
whole family was checked: this was the only mismatch. Every other parse returns
and constructs its enclosing class.
The wire names are a separate matter and stay as they are. Six tags carry a
stray Tag suffix -- dialectIdTag, chapterIdTag, chunkIdTag, translationIdTag,
translationChapterIdTag, translationChunkIdTag -- which ProjectIdTag already
documents as a copy-paste slip that cannot be fixed, because those strings sit
inside event ids the group has signed. Renaming one invalidates every signature
over it.
TagRoundTripTest covers all twenty-four, grouped by the four shapes they take,
plus a matrix asserting no parse accepts any of the other twenty-three arrays --
the same template's other failure mode, a copied TAG_NAME.
The mechanism is the type argument, and it has to be written out. The first
draft of the test left T to inference and passed against the bug when it was
reintroduced to check: T is read off the function reference, so a parse
returning DialectIdTag simply makes T be DialectIdTag, every member the two
classes share resolves, and assertIs<T> is a tautology. With the class named
explicitly at each call site, a parse whose declared return type is a sibling
no longer satisfies (Array<String>) -> T? and the file stops compiling on that
tag's own line -- earlier and more precisely than any runtime check. Verified
by re-breaking the file: the build fails naming the offending parse. The KDoc
says so, because tidying the type argument away would silently defeat the test.
One more test, on arrayOfNotNull rather than on the family: it strips only
trailing nulls and writes "" for an interior one, so a reference assembled with
an author but no relay hint is [name, id, "", author] and the author stays in
slot 3. That is the shape most of these tags are assembled in, and if it ever
changed every author would be read as a relay hint and dropped.
605 unit tests pass on --rerun-tasks, and m3Audit is clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>