Skip to content

Release 14.2 - #55

Merged
CesarCoelho merged 45 commits into
masterfrom
v14.2
Aug 28, 2026
Merged

Release 14.2#55
CesarCoelho merged 45 commits into
masterfrom
v14.2

Conversation

@CesarCoelho

Copy link
Copy Markdown
Collaborator

Release of version 14.2.

Changes since 14.1

  • Adds the api-generator-lib, which will replace the three existing generators at v15.0
  • Fixes the TCP/IP transport handing back closed sockets from the client pool
  • Skips the address lookup for PUBLISH error messages (fixes [mal-impl] LookupAddress failed to find local endpoint #9)
  • Drops the subscriptions of a consumer as soon as it disconnects
  • Extracts the endpoint registry and the URI addressing scheme out of Transport
  • Multiple fixes to the HTTP transport and re-enables the MAL testbed over HTTP
  • Encodes the Blob length as 64-bit so that Blobs above 2 GB work
  • Creates the Elements of an Area on demand instead of holding one of each (Java optimized)
  • Adds the JDK 25 build and testbed jobs

To be Squash and Merged, per RELEASING.md.

🤖 Generated with Claude Code

CesarCoelho and others added 30 commits July 18, 2026 14:37
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The testbeds workflow passes built artifacts down a job chain via the
Maven local repository cache. The keys were rotated weekly by date, but
actions/cache keys are immutable (written only on a miss), so after a
change within the same week the build jobs restored a stale cache and
could not re-save, and downstream jobs tested against outdated
artifacts. The version bump to 14.2-SNAPSHOT surfaced this sharply.

Key every artifact-passing cache on github.sha so each commit gets its
own cache. The root build jobs restore before building (with a
restore-keys prefix to keep external dependencies warm) and save the
freshly built artifacts; downstream jobs restore the exact-SHA cache
with no fallback, so they always get the current commit's build. Also
removes the now-unused Get Date steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
actions/checkout, actions/setup-java, actions/cache and
actions/upload-artifact were pinned to v4, which targets the deprecated
Node.js 20 runtime. Bump them to v5 (Node.js 24) to clear the
deprecation warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TCPIPClientSocketsManager.get() returned a pooled Socket keyed by port
without checking whether it was still usable. A java.net.Socket is
single-use: once closed it can never be reconnected. When a consumer's
channel was torn down, its socket was closed but left in the pool; a
later getRandomClientPort() collision then handed back the dead socket
and connect() threw "SocketException: Socket is closed", surfacing as a
MALTransmitError / DeliveryFailed (65536). This was an intermittent
failure whose likelihood grew with the number of closed sockets in the
pool, causing flaky e2e test failures.

Fixes:
- get() now evicts and recreates a pooled socket that is closed or
  already connected, so it never returns an unusable Socket.
- Sockets are removed from the pool when their connection is torn down
  (TCPIPTransportDataTransceiver.close -> TCPIPClientSocketsManager.remove),
  so dead entries no longer accumulate. Server-side accepted sockets are
  not pooled, so their eviction is a harmless no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the existing JDK 21 job set for JDK 25: build-jdk25,
build-testbeds-jdk25, and the testbed-mal/com/mpd/encoders jobs, plus a
disabled testbed-malspp-jdk25 to match its siblings. Each job uses its
own jdk25 Maven cache keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cosmetic formatting only; no behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A publish error is returned to the publisher as a PUBLISH message with
the isError flag set. The publisher is not a MAL provider for the PUBLISH
operation, so the unconditional lookupAddress at the _PUBLISH_STAGE
dispatch always failed for it and logged a spurious warning:
"lookupAddress failed to find local endpoint for ...". Delivery of the
error was unaffected because handlePublish already ignores the address
for error messages and dispatches via the publish listener map.

Only perform the address lookup for non-error PUBLISH messages (the
broker path). The NOTIFY path already avoided the lookup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
receivedExpectedTransportActivity sampled the received-events list only
once. The transport activity events arrive asynchronously over pub-sub,
so the last one (e.g. ACCEPTANCE) could still be in flight when the check
ran, intermittently failing ActivityTestScenario.MonitorCase.MultiHop
with "NO MATCH for ACCEPTANCE" even though the event arrived a moment
later.

Poll until all expected events have been received or a timeout elapses
(2 * COM_PERIOD_LONG, checked every COM_PERIOD_SHORT), matching the wait
constants already used by the sibling methods. The fast path is
unchanged when the events are already present, and a genuine absence
still fails after the timeout with the same NO MATCH diagnostics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convenience constructor that wraps a local file as a URL-based Blob, so
callers can pass a java.io.File directly instead of building the file URI
by hand. Reuses the existing URL-backed machinery. Rejects a null file
with IllegalArgumentException, per the documented contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
encodeBlob previously called Blob.getValue(), which loads the entire
content into a single byte array before writing. For large (URL/file-
backed) Blobs this holds the whole payload in memory and fails outright
above ~2 GB (the Java array limit).

Add a streaming path:
- Blob: getAsStream() (lazy InputStream over the URL/file or wrapped
  array) and getLengthLong() (long length, e.g. File.length()), plus the
  File constructor already added.
- StreamHolder: writeStream(InputStream, long) with a materialising
  default (reads fully then writeBytes) so non-overriding encodings
  (string, xml) are unchanged.
- BaseBinaryStreamHolder / FixedBinaryStreamHolder override writeStream
  to copy the content in fixed-size chunks after the length prefix, so
  the payload is never fully held in memory. A guard rejects lengths
  above the 32-bit length field with a clear message.
- Encoder.encodeBlob now streams via writeStream(getAsStream(),
  getLengthLong()).

Add LargeBlobTest, an @ignore'd probe that generates a 3 GB file and
attempts to encode/decode it, documenting the remaining 32-bit length
field limitation. It is disabled so it does not break CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enable the previously @ignore'd probe and run it over fixed binary
(FixedBinaryEncoder/Decoder), so it exercises a full 3 GB Blob round-trip
rather than documenting the limitation. Extract the random-file generation
into a reusable public static generateRandomFile(File, long) helper that
writes in fixed-size chunks, so other tests can reuse it and generation
never holds the whole file in memory.

The test still guards on available heap/disk and skips (via assumeTrue)
when they are insufficient.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Blob body was framed with a 32-bit length, capping a Blob at 2 GB even
after the encode path was made streaming. Frame it with a 64-bit length
instead:

- Encode: BaseBinaryStreamHolder.writeStream writes the length via
  writeUnsignedLong and streams the body in chunks. FixedBinaryStreamHolder
  no longer overrides writeStream, so both inherit it and dispatch to their
  own writeUnsignedLong. For the variable-length encoding a varint of a
  value <= Integer.MAX_VALUE is byte-identical to the old 32-bit length, so
  existing data stays readable; only lengths above 2 GB use the new range.
- Decode: BaseBinaryDecoder overrides decodeBlob to read the 64-bit length,
  return a byte-array-backed Blob up to the max array size, and spool larger
  bodies to a temporary file (streamed via readBytesInto), returning a
  file-backed Blob so a Blob above 2 GB is never held in memory. The byte[]
  boundary is Integer.MAX_VALUE - 8: the JVM rejects arrays within a few
  bytes of Integer.MAX_VALUE.

LargeBlobTest now round-trips a range of sizes (512 KB, 32 MB, 1 GB, 3 GB,
5 GB) through a shared method, asserts the decoded length matches, logs
each step with its duration, and cleans up source, encoded and spooled
temporary files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JUnit's default method order is hash-based, so the size cases ran in an
arbitrary order. Add @FixMethodOrder(NAME_ASCENDING) and give the methods
a numeric prefix so they execute smallest to largest, making the logged
timings read in order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drops 10 imports that no longer resolve to any code reference in
apis/api-area004-v002-mc and services-impl/services-area004-v002-mc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Transport held two parallel HashMaps indexing every endpoint by MAL local
name and by transport routing name, touched from eleven places spread
across endpoint creation, deletion, message dispatch, the in-process
shortcut and error replies.

Move both maps and the operations over them into a new EndpointRegistry,
so Transport delegates rather than manipulating the two maps in step. No
behaviour change: the maps stay unsynchronised HashMaps as before.

The two maps were protected, so this is source and binary incompatible
for any transport outside this repository that reached into them. Nothing
in mo-services-java or nanosat-mo-framework did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Transport carried six fields describing the shape of its URIs, plus the
routing part cache, and spread the parsing of them across the constructor,
init(), sendMessage(), getRoutingPart() and manageCommunicationChannel().

Move them into a new TransportAddressing, which also takes over the three
computations that were inline: deriving the service delimiter count, building
the base URI, and the cached routing part lookup. init() is now one line.

getRoutingPart() stays an overridable method on Transport delegating to the
new class, because SPPBaseTransport overrides it to parse on the protocol
delimiter instead.

The six fields were protected, so this is source and binary incompatible for
any transport outside this repository that read them. The five in this
repository are updated here; nanosat-mo-framework has no references.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
returnErrorMessage picked an arbitrary endpoint to build the reply from, so
the error carried the URI, supplements and transport specific header of
whichever endpoint the map happened to iterate first. dispatchMessage did
have the right endpoint, but declared it inside the try block where the
catch clauses could not see it.

Hoist it, and pass it down, so an error caused by a failing delivery is
returned from the endpoint the message was actually addressed to. The
endpoint-not-found path still has no endpoint to attribute to and keeps
falling back to any of them.

Move the construction and sending of the reply into a new ErrorReplyBuilder
while here. The existing three argument returnErrorMessage keeps its
signature and delegates, so the two callers outside this class, both decode
failures with no endpoint context, are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The HTTP profile of the MAL testbed failed every PubSub procedure and the
INVOKE response, all of them with a connection refused to the consumer. The
consumer was never listening: its transport died during init with

  NoClassDefFoundError: com/sun/net/httpserver/HttpServer

maven-surefire-plugin 2.7.2 predates Java 9. Its isolated class loader has
no parent, so classes of platform modules such as jdk.httpserver cannot be
seen from the tests. Only the HTTP transport needs one, which is why the
other transports were unaffected. Move to surefire 3.2.5.

Under surefire 3 the class loader running the tests is no longer a
URLClassLoader, so RemoteProcessRunner could no longer read the classpath
back out of it and started the remote provider with an empty one. Fall back
to the java.class.path system property when the cast is not possible.

With both in place the HTTP profile passes, so re-enable its workflow job.

Checked for regressions against the other profiles of the MAL testbed, ESA,
ESA_TCPIP and ESA_ZMTP, and against testbed-com, testbed-mpd and
testbed-encoders. Not checked against testbed-malspp, which needs artifacts
that are not built here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three senders formed a chain of concrete classes, each replacing the
send method of the one above rather than refining it, so the inheritance
bought no reuse. What was genuinely duplicated sat elsewhere: the mapping of
the MAL header onto HTTP headers existed twice, once writing to a client
request and once to a server response, differing only in the setter called.

Give the three a small abstract base holding what they really share, and
move the mapping into MALHttpHeaderEncoder, which writes through a
HttpHeaderSink so it can serve either direction.

The request and the response mappings are not identical, and the three
differences are kept rather than smoothed over: an invalid From or To field
is fatal on the response path but only logged on the request path, the
interaction stage is written unguarded on the response path, and the error
flag is capitalised on one and not on the other. They are noted in the
javadoc of encodeResponseHeaders.

Also drop encodeAscii, which returned its argument unchanged and was never
overridden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The scenario was commented out of the test document in d422927, a large
release commit, with no change to the scenario or to its fixtures. It was
most likely turned off to get that release green rather than because the
test was wrong, and nothing has touched the access control code since.

It passes again, so let it run. Checked against all four transports of the
MAL testbed, ESA, ESA_TCPIP, ESA_ZMTP and ESA_HTTP, each reporting 78
passing assertions for the scenario and no failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
XMLSchema.xsd imports the schema for the XML namespace by its published
URL, and nothing resolved that locally, so every clean build fetched it
from www.w3.org. An offline build, a network outage or a rate limit at the
far end therefore failed the build, and did so with the misleading symptom
of xml:lang being reported as an undefined attribute, the import having
silently failed first.

Keep a copy of that schema next to the sources and point an XML catalog at
it. Verified by building with the network blocked, which fails before this
change and succeeds after it, generating the same 68 classes as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A header with no value was sent carrying the string
<![CDATA[EmptyStringPlaceholder]]> instead. The MAL HTTP binding does not
define it, so a peer has no way to tell it apart from a real value. An
empty authentication id, for one, arrived as a string that is not the
hexadecimal representation of a Blob.

Leave the header out instead. The placeholder is still understood on
reception, so deployments that still send it keep working, and the constant
is kept and deprecated for that reason alone.

The placeholder was also load bearing: it guaranteed that every header was
present, and four of the five readers dereferenced the value without
checking for null first. Absent headers are now normal, so they read back
as an empty value, which is what an empty header already produced.

Closes #35

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A server registered one context handler and drove processRequest,
processResponse and finishHandling on it for every request, while the
handler keeps the decoded header and the body of the request it is serving
in fields. Requests are served concurrently, so two of them overwrote each
other, and processResponse then decided what to answer from a header that
belonged to another request, or to none yet.

The symptom was a 204 sent where a MAL message was expected. The client
then read a response with no MAL headers and failed with

  Unknown Enumeration for the provided string:

which killed the thread processing the response, and the interaction that
was waiting for it never completed.

Let a handler answer with the instance to serve one request with, and have
both servers ask for one. The default answers with itself, which stays
correct for a handler that holds nothing between the three calls, so
handlers outside this repository are unaffected.

Report a missing mandatory header by its name while here. The absence of a
header used to surface as whatever the parser of that particular field threw,
naming neither the header nor the message it arrived with, which is what
made the above hard to read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The transport noticed a consumer closing its connection and tore down its
side of it, but told the MAL nothing. A broker therefore kept every
subscription that consumer had made, and only learnt of the loss one
subscription at a time, each after a NOTIFY had failed to reach a consumer
that was no longer there. Every one of them cost a failed delivery, a
reconnection attempt by the transport, and a pair of warnings.

Report the loss instead. MALMessageListener gains a method for it, with a
body that does nothing, so that listeners which hold no state per peer, and
listeners outside this repository, are unaffected. The transport calls it
from closeConnection, which is where every transport in this repository ends
up when a connection goes, whether it was closed cleanly or lost to an error.

The peer is named by the prefix its endpoint URIs share, ending at the
delimiter, so that a peer on port 1025 is not mistaken for one on port 10250.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LargeEnumerationFactory and MediumEnumerationFactory are left over from an
earlier design, where an element was registered through a factory of its
own. They extend MALElementsRegistry and declare a createElement() that
takes no argument, so they override nothing on it, and nothing refers to
either of them.

They were also the only classes extending MALElementsRegistry, so removing
them leaves it free to change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every Area held a live instance of each of its types, only so that
createElement() could be called on it to make a copy. That meant the
class of every type was loaded as soon as the Area was, whether or not
a deployment ever exchanged that type.

The generator now writes an ElementFactory for each Area, which switches
on the service number and then on the type number, so a class is loaded
only once a message carries that type. The element arrays of the Areas
and Services are left empty.

The type numbers of an Area are handed out from 1 upwards, so the switch
takes the widest band that starts at 1 and still compiles to a jump
table. What lies past that band goes to a second method. Where those
numbers lie far from zero, each side of zero is switched on its own:
together they would have to span the whole way across zero and would
fall back to a binary search.

On a Raspberry Pi the Areas now load in 411 ms rather than 1199 ms.

MALElementsRegistry no longer keeps a map of live Elements, so
addElement(), removeCallableElement() and howMany() are gone with it.
More than one factory can be registered for the same Area, which is how
the MAL/SPP testbed reaches its two hand written enumerations: their
type numbers are past what the XML schema allows, so no generated
factory can know about them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…em all

Most Areas declare every one of their types at Area level, and their
services declare none. The switch over the service number then had one
branch that led anywhere, and a method for each service that only ever
returned nothing.

Where no service of an Area declares a type, createElement() now holds
the switch over the type numbers itself. The MAL reaches its jump table
straight away rather than through a call that HotSpot would not have
inlined, the switch over the service numbers is gone from twelve of the
seventeen generated factories, and about forty methods that returned
nothing are no longer written at all. Areas whose services do declare
types are written as before.

The factories are registered while the messages of the ones already
registered are being decoded, so the list they are held in is now one
that can be read through at the same time. Which factory answers when
two of them claim a type is written down: the first registered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every Element that was created walked the registered factories asking
each one in turn which Area it belonged to. Those two questions go
through an interface that several classes implement, so they could not
be bound to any one of them, and answering them cost more than the
switch they were there to reach: a profile of the walk put a third of
the time in it.

Each factory is now asked once, when it is registered, and the answer is
kept beside it as a single number that holds the area number and the
version together. Finding a factory is then a walk over an array of ints,
which is small enough that it is folded into its caller, and only the
factory that matches is called into.

The factories and their numbers are held in one object that is replaced
as a whole, so a walk still reads a set that cannot change underneath it
while an Area is being registered on another thread.

Creating the Elements of a spread of forty MAL and NMF types, against a
map of live Elements as it was before the factories: a stream that
carries many types is now reached 12 to 29 percent faster, while asking
for one type over and over is 17 percent slower, that being the case
where the map collapses into the caller.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The File Management area spells the attribute that names an area as
"are" rather than "area" on four type references, so the area of the
error each one points at was never given. JAXB drops an attribute it
does not recognise without saying so, which is why nothing has ever
reported it; the references have been reading as though they named no
area at all.

The Mission Data Product area gives operation number 3 to both
unsubscribe and listSubscriptions in the Delivery service. Numbers have
to be unique within a service, and the schema says so, so the file has
never satisfied its own schema. listSubscriptions takes the next free
number rather than the one it was probably meant to have, so that the
operations already numbered 4 and 5 keep the numbers they have.

The Basics area was a copy of the COM area that kept its identity, so
both declare area COM, number 2, version 1. It was only ever a
prototype and is removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CesarCoelho and others added 15 commits August 22, 2026 15:06
The Mission Data Product area gives short form part 80 to both the
Frequency and the Subscription structures. A short form has to be unique
within an area, and the schema says so, so the file did not satisfy it.
The duplicate was hidden until now behind the operation numbers that
were fixed alongside it, because validation reports the first breach it
meets and stops.

Subscription is declared last, and the short forms of this area ascend
in declaration order, so it is the one that moves. It takes 81, the next
free number, rather than a number that would make the ones already given
out move with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The area declared ProductToken, ProductPath and SubscriptionId as
composites that extend String, URI and Identifier, each with no fields
of its own. They were not structures but attempts at naming an
attribute, and the MAL data model has no way to say that: Composite and
Attribute are separate branches of the type hierarchy, and the set of
attributes is closed by the MAL area.

The schema could not object, because it types the extends element as a
plain reference and only its documentation says the target is meant to
be a Composite. The document generator could, and did: it looks a super
type up among the composites, finds nothing, and abandons the whole
area. ProductToken is the first type declared, so the area has been
producing no document at all, while the run that produced nothing
reported success.

The three are removed and the eighteen fields that used them now name
String, URI and Identifier directly, which is what they carried anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The document generator was given each specification and asked to write
its document in the same breath. An area may name a type that another
area declares, and a type is only known once the area declaring it has
been loaded, so a reference across areas resolved only when the file
declaring it happened to be read first. The order the files are read in
is the order the file system lists them.

The Mission Data Product area names a type of the COM area and is listed
before it, so it found nothing and abandoned the area. The run that
produced no document for it still reported success, because each file is
caught on its own.

The specifications are now all loaded, and only then generated, which is
what the Maven plugin has always done. Thirteen prototype areas produce
a document where twelve did before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The area was a prototype that nothing builds and nothing refers to. No
pom names it, no workflow names it, and no other specification names a
type or an error of it; the only thing it produced was a document of its
own.

It also declared itself as area MC, taking the name the Monitor and
Control area already has under number 4. Nothing resolved the two the
wrong way round, because nothing referred to either of them by name from
outside, but a reference carries no area number and so could not have
said which was meant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The existing generators are built on a model that xjc derives from the XSD and
that external libraries populate, so the shape of the specifications is decided
outside this repository and everything downstream inherits it. This library
carries its own model of areas, services, operations and fields, reads and
writes the XML itself, and generates from the model rather than from a parse
tree.

The Java generator is complete. It is held against the output of the existing
one file by file, and reproduces all 950 files of the seven api modules byte for
byte, plus the 546 files of the four NanoSat MO Framework modules, whose
specifications this repository has never generated from.

The module is deliberately outside the reactor for now: the existing generators
continue to build the APIs throughout 14.x, and the two are swapped over at
v15.0. golden.sh captures and compares the output of the two in the meantime;
the captured baseline is regenerable and stays out of version control.

DESIGN.md records the model, the four invariants it holds to, and a log of the
decisions behind them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An error name is not unique across the specifications: MPS declares INVALID as
1 and COM declares it as 70000, MC declares Invalid as 3 and MPD declares it as
1. The registry held error definitions in a map keyed by the name alone, so
whichever area was loaded last took the name.

An operation always names the area of the error it references, so the document
generator asked for something the specification had already made unambiguous and
was given whatever answered to the name. MPS::INVALID was documented as 70000
and MC::Invalid as 1. Which document was affected depended on what else was
loaded: the standards set holds no MPD, so nothing competed for Invalid there.

The Java generator uses the same lookup for the comment on a handler's throws
clause. It happens to produce the same result today, because the fallback is
only reached where a reference carries no comment of its own and no name
collides there, but it was as exposed as the other one.

An error is now held under its area as well as its name. The generated Java is
unchanged; two documents now state the number their own XML gives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- docx: one Word document per area, matching the current output exactly,
  126 of 126 parts byte for byte. Adds Batik, to rasterise declared diagrams.
- MOSpec: the text format the navigator will edit, with a hand-written lexer,
  parser and exporter, held to three round-trip tests.
- XHTML: the browsable page. The old generator crashed on composites that
  contain themselves, overwrote pages of areas sharing a name, and had seven
  broken links. All fixed; the drawings are reproduced (84 of 84 type diagrams,
  258 of 260 message diagrams).

The linker now records the service that declares a type; the field existed and
nothing had ever filled it.

The module joins the reactor, and its 39 corpus tests move to
testbeds/testbed-api-generator, so building the library stays a compile. The 73
unit tests stay with the code they test.

Nine of those 39 are golden trees that need a baseline captured from the old
generators, so they skip in CI. To be addressed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The baseline is the output of the old generators, and the golden tests compare
the new ones against it. It was gitignored as "~20 MB and regenerable", so it
was absent on a CI runner and those tests skipped there. 20 MB is the size on
disk; it is repetitive generated text and git stores it in 1.17 MB, against a
repository of 8.6 MB. It now lives beside the tests that read it, and not under
src/test/resources, where Maven would copy 20 MB into target on every build.

A quarter of that is ten PNGs, which neither compress nor delta. They come from
<mal:diagram>, which wraps raw SVG inside the specification XML. That is going
away: an SVG is a rendering of the model, not part of it, so nothing can check
it, and a diagram that silently disagrees with its own specification is worse
than no diagram. Sixteen of the eighteen in the corpus belong to MC v001, and
the NMF has none. DESIGN.md section 8.3 sets out the reasoning and the order:
the diagrams come out of the XML, both generators then stop emitting them, and
the baseline is re-captured - rather than dropping them from the tests, which
would mean choosing to stop checking something still being produced.

Software Management goes first, being a prototype. Its two diagrams are removed
and the baseline re-captured; the old generator stopped emitting the images by
itself, so there is nothing to record as an intended difference. Two importer
tests that used it as their diagram example now use MC v001, which still has
eight, and go when it does.

DocxBodyTest and DocxNumberingTest move to the testbed as well. They read a
captured document, so they belong there; they had stayed behind and were
skipping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A diagram declared inside a specification is SVG, and the document generator
rasterised it with Batik to embed in the .docx. That is gone: an SVG inside a
specification is a picture of the model that nothing can check against the
model, so it can disagree with what it describes and no validator would ever
say so. It was also the only third-party dependency the library had.

About 90 lines across three files: DocxDocument.rasterise, addDiagram, the image
relationships, and DocxBody.appendDrawing.

MC v001 is the only file left that declares diagrams, and it is in the standards
folder, so it cannot be edited. The old generator still rasterises them and the
new one does not, which affects two documents. golden.sh stops capturing
word/media, and DocxGeneratorGoldenTest strips the drawing markup off the
reference side before comparing, so both document.xml files are still compared
in full rather than given a budget - 108 of 108 parts match. The reasoning is in
intended-differences.txt and DESIGN.md section 8.3.

The validator now warns when a specification declares a diagram, because a
diagram that quietly produces no figure would be the same failure in a new form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The plugin used org.reflections to walk every jar on its classpath looking for
subtypes of Generator. That cost about 300 ms of every module's build to
discover five classes, roughly ten times what generating the code itself takes.

Each module that supplies a generator now names it in a service file, in the
form the JDK's own service loading uses, and the plugin reads those. The
constructor still takes the Log, so the file is read directly rather than
through ServiceLoader. org.reflections is no longer a dependency.

Measured on api-area002-v001-com, as the goal's cost above a bare clean: 450 ms
before, 151 ms after. Over a whole reactor build of apis/*, 3811 ms to 2525 ms.

Also restores src/main/resources in the three generator modules. They override
<resources> to pick up LICENCE.md, which silently dropped the directory the
parent declares, so nothing under src/main/resources had ever been packaged
there - which is why the service files did not appear until it was fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wired the new library into the maven plugin, so both generators can be selected
from the same build.

NewJavaGenerator is an adapter in the plugin: it reaches api-generator-lib
through the interface the plugin expects and registers as "Java2". Generation is
deferred to close(), because the library links every specification before
generating any of them.

The default is unchanged. esa.stubgen.generator defaults to "Java", so a plain
mvn install builds every API with the existing generator and never constructs
the adapter. -Pnew-generator selects the library for a whole build; setting the
property in one module's pom selects it for that module alone. Flipping the
default is the v15.0 cut-over.

Verified: the whole reactor built with -Pnew-generator produces 950 of 950 files
byte-identical to the existing generator's output, compiles, and passes 295
tests across the eight testbeds. One module on the new generator inside an
otherwise old build was checked separately, and also matches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The classes xjc derives from the schemas carry the schema's own
documentation, which contains markup that Javadoc rejects: the W3C schema
describes a type as annotatable by anything "other than <schema> itself",
and Javadoc reads that as an unknown tag.

w3c.xsd was already excluded, but only on the attach-javadocs execution
inside the release profile, so javadoc:javadoc failed. The exclusion is now
on the plugin itself, and covers esa.mo.xsd as well, whose generated classes
carry the same markup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The exclusion was already declared on the attach-javadocs execution, and
Maven lets the execution override the plugin, so the added block never
applied to the release jar. For javadoc:javadoc it excluded esa.mo.xsd,
which javadoc documents without complaint, dropping 104 generated classes
from the output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@CesarCoelho
CesarCoelho merged commit 028751a into master Aug 28, 2026
112 of 113 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[mal-impl] LookupAddress failed to find local endpoint

1 participant