← 输入框|github.com|刷新
Skip to content

Migrate many SciJava Common subsystems - #295

Open
ctrueden wants to merge 59 commits into
mainfrom
scijava-common-migration
Open

ctrueden wants to merge 59 commits into
mainfrom
scijava-common-migration

Conversation

@ctrueden

Copy link
Copy Markdown
Member

This branch starts phasing out SciJava Common (SJC), porting the (many) parts still worth having into small, layered SciJava3 modules. Anything the Java standard library already covers is dropped and documented. The plan is in docs/migration-plan.md, and docs/migration.md maps each SJC class to its replacement, a JDK equivalent, or "dropped".

The aim is not to reimplement SJC; SJC itself is left alone apart from @deprecated pointers (still to come). Downstream code moves over by swapping out the scijava-common dependency in favor of the new modular ones here, updating the imports, and (if any API leakage caused backwards incompatibilities) bumping the major version.

New components

Foundation

  • scijava-spi: interfaces with no dependencies (Named, Identifiable, Disposable, Versioned, GenericTyped)
  • scijava-index: annotation indexing, ported from org.scijava.annotations. The index format is unchanged, so SJC and SJ3 can read each other's indexes.
  • scijava-io3: locations, data handles and byte operations, with no framework and plain ServiceLoader. Direct byte buffers now stay off-heap after they grow.
  • scijava-events: a typed event bus. Subscribers are held strongly, there is no shared global instance, and the vendored bushe fork is gone.
  • scijava-convert3: one Converter.supports(Object, Type) question instead of eight canConvert overloads. It doesn't need a container.
  • scijava-settings: a single, human-editable ~/.config/<app>/settings.toml.

Application model

  • scijava-context (+ -context-test): lazily created services, the @Plugin tier served straight from the index, @Dependency field injection and @EventHandler scanning.
  • scijava-execute: Executable / Runner / pre- and postprocessors. A declined run comes back as an ordinary result, while cancelling goes through Future.cancel(true).
  • scijava-command3: discoverable commands, stackable @Menu metadata and MenuTree. Menus are still built without loading any command class.
  • scijava-harvest: the parameter-dialog model with no UI toolkit, including computed choices and generated groups. Between them these cover DynamicCommand's two uses.
  • scijava-script3: a script is an Executable like any other, and any JSR-223 engine works. Unlike in SJC, callbacks work for scripts.
  • scijava-cli: runs the same commands with no display.

UI

  • scijava-ui3 (+ -ui3-test): widget contracts that don't depend on a toolkit.
  • scijava-swing, scijava-javafx, scijava-awt: three bindings. Together they show the contracts really are toolkit-agnostic. JavaFX needed two contract changes and AWT needed none.

Compatibility

  • scijava-compat: SJC commands usable from SJ3.
  • scijava-ops-compat: SJ3 ops usable from an SJC app.
  • scijava-imagej-compat: ImageJ2 types, readable by scijava-types.

Cross-cutting decisions

  • Cancellation is thread interruption. There is no Cancelable; callers use Future.cancel(true), and Progress.checkCancellation() throws CancellationException. scijava-concurrent now keeps a cancellation through a parallel section instead of wrapping it in a RuntimeException.
  • JPMS works without losing encapsulation. I measured this rather than assumed it. A plugin module adds one line, opens <pkg> to org.scijava.context, and private-field injection then works while implementation packages stay unexported.
  • Incarnation numbers. Breaking changes go to a new artifactId and package prefix (common3, io3, command3, …) so the old and new versions can share a classpath. docs/versioning.md explains the scheme.
  • No split packages with SJC: e.g. because org.scijava.command is part of SJC, the command package here is now org.scijava.command3.

Breaking changes to existing components

  • Discoverer.discover returns lazy Discovery<U> descriptors instead of instances, and all call sites are updated.
  • Versioned and GenericTyped move from scijava-common3 to scijava-spi, with no deprecated aliases left behind.
  • scijava-legacy is split into scijava-ops-compat and scijava-imagej-compat.
  • Submodule folders drop the redundant scijava- prefix. Artifact IDs are unchanged.

Also included

  • Every component POM now pins the version of every SciJava artifact it uses. Six components were silently resolving released 1.0.0 artifacts instead of the reactor's snapshots.
  • scijava-ops-image tests no longer use SJC utilities, except FileLocation, which SCIFIO's API requires.
  • Context.doDispose() holds its lock for less time, ported from scijava/scijava-common@5dba100.
  • README badge fix; corrected scijava-ops-indexer version.

Not done yet

  • @deprecated pointers on the SJC side.
  • An enforcer rule for the version-pinning convention.
  • display, and checkbox/radio rendering for selectable/selectionGroup.
  • scijava-persist, deferred until something needs it.

@imagesc-bot

Copy link
Copy Markdown

This pull request has been mentioned on Image.sc Forum. There might be relevant details there:

https://forum.image.sc/t/fiji-friends-dev-update-thread/103718/151

ctrueden and others added 29 commits October 28, 2026 13:50
Adds docs/migration-plan.md (strategy, phases, cross-cutting design
decisions) and docs/migration.md (the class-by-class mapping table from
SciJava Common to its SciJava3 replacement, standard library equivalent,
or "dropped").

Both are excluded from the Sphinx build; they are development docs, not
part of the user-facing site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Switch the tests to the SciJava3 equivalents that already exist:

- org.scijava.util.MersenneTwisterFast -> org.scijava.common3 (16 usages)
- org.scijava.util.LongArray -> org.scijava.collections
- org.scijava.util.ListUtils.first -> inlined; it is two lines, and
  ListUtils has no SciJava3 home yet

The remaining scijava-common usage in these tests is
io.location.FileLocation, which is required by SCIFIO's ImgOpener API,
so it cannot go away until SCIFIO migrates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Discoverer.discover now returns List<Discovery<U>> rather than List<U>.
A Discovery is a descriptor with three access levels of increasing cost:
implClassName()/attrs() load nothing, type() loads the class, get()
constructs the object. Discoverer.instances(Class) covers the common case
of wanting the objects.

This is what lets an index-backed Discoverer report what is available
without loading classes -- the property Fiji's menu construction depends
on, and which the old signature made impossible to express. Op YAML
metadata and ServiceLoader providers both map onto the same shape.

Note a JPMS constraint found while implementing this: ServiceLoader
resolves `uses` declarations against its *caller's* module, so
scijava-discovery cannot perform a lookup on another module's behalf. The
lookup function is therefore supplied by the consuming module, via
Discoverer.usingProviders(c -> ServiceLoader.load(c).stream()). The one
exception is Discoverer.all(), which looks up Discoverer itself -- a type
this module does declare `uses` for.

scijava-discovery now depends on scijava-priority, so that discoveries
can carry a priority without being constructed.

This is a breaking API change, which the repository's current adoption
level permits. All call sites are updated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Six components consumed scijava-discovery transitively without pinning
its version property, so they resolved the released 1.0.0 artifact from
the repository rather than the reactor's 1.0.1-SNAPSHOT. That was
invisible while the API was unchanged, and became 1116 NoSuchMethodErrors
as soon as it was not.

Note that the "these versions must match across all component POMs"
convention has no enforcement behind it: a version property that is
merely absent silently pulls a released artifact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Six components resolved released 1.0.0 artifacts instead of the reactor's
snapshots, because these modules inherit from pom-scijava rather than
this repository's aggregator POM: an absent version property does not
fail, it silently falls back to the last released version in
pom-scijava's dependency management.

scijava-legacy, for instance, pulled released scijava-meta and
scijava-struct 1.0.0, whose classes reference org.scijava.common3
interfaces. That only surfaces when an API changes, as a
NoClassDefFoundError far from its cause.

Also defines scijava-priority.version in scijava-discovery, which
referenced the property without declaring it.

Audited with dependency:list against the reactor versions; zero
mismatches remain. Nothing enforces this convention -- an enforcer rule
would be worth adding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
A new dependency-free module for interfaces that any object can
implement without knowing about a SciJava framework, so that code
wanting only these contracts takes on the smallest possible dependency.

Contains Versioned and GenericTyped, moved out of scijava-common3, plus
Named, Identifiable and Disposable ported from SciJava Common. No
deprecated aliases are left in scijava-common3, which the repository's
current adoption level permits.

Method names are modernized rather than preserved: getIdentifier() is
now id(), and Disposable.dispose() is abstract rather than a no-op
default, since a default that does nothing means implementing the
interface proves nothing to callers. Named exposes name() only --
mutability is a separate contract, rather than a setter that throws
UnsupportedOperationException and forces every caller to feature-detect.

Deliberately omitted for now, pending a consumer:

- Locatable, whose only real content was a default implementation
  requiring common3.Classes.location, which this module cannot depend on.
- Typed, which has no consumer until the plugin layer exists, and whose
  Class<T> type() would silently satisfy GenericTyped.type() as well,
  since Class implements Type.

Both are cheap to add on demand and expensive to remove once published.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Adds four classes, porting only what earns its place and recording the
rest as dropped in docs/migration.md:

- Numbers (from NumberUtils, plus ArrayUtils.safeMultiply32/64)
- Strings (from StringUtils: padStart, padEnd, isNullOrEmpty,
  splitUnquoted)
- Digests (from DigestUtils)
- Processes (from ProcessUtils, reimplemented on ProcessBuilder)

Decisions were driven by counting real usages across the scijava and
imagej repositories:

- ArrayUtils is dropped apart from safeMultiply32/64 (11 usages, and
  worth keeping as a readable wrapper for Math.multiplyExact). Its 18
  contains/indexOf methods have 3 usages between them and are a loop or
  a stream in the standard library.
- ListUtils (first: one line; string: StringJoiner) and UnitUtils (one
  usage ecosystem-wide) are dropped.
- Bytes is deferred to scijava-io3, its natural consumer.
- StringUtils.sanitize, sanitizeDouble, stripNulls and samePrefix have
  no usages anywhere and are dropped.

Behavioral changes worth noting, both recorded in the migration table:

- Numbers.toNumber no longer consults a conversion framework, since
  scijava-common3 must stay dependency-free; it handles numbers and
  strings only.
- Digests.digest throws IllegalArgumentException for an unknown
  algorithm rather than returning null, which in SciJava Common surfaced
  as a NullPointerException somewhere else entirely. The best* family is
  therefore plain SHA-1, which every Java platform guarantees.

Tests are ported from SciJava Common to JUnit 5 and extended. The
Processes tests cover what its reimplementation could plausibly break:
closing the process's stdin once input is exhausted, not closing the
caller's streams, a multi-byte character split across two reads, a
non-zero exit status, and interruption.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
FileUtils splits cleanly along a line drawn by java.nio.file: its
filesystem half is now standard library, while its filename logic --
notably versioned JAR names, which Fiji's updater depends on -- has no
replacement anywhere.

FilePaths therefore carries normalizeSeparators (was getPath), extension,
stripVersion, allVersions, matchVersionedFilename, and deleteRecursively,
which java.nio.file still lacks. It is named FilePaths rather than Files
because any implementation would import java.nio.file.Files, and a
clashing simple name in a foundational utility module is a lasting
annoyance.

Dropped, with replacements recorded in docs/migration.md: readFile,
writeFile, createTemporaryDirectory and getModifiedTime, all covered by
java.nio.file.Files; and shortenPath and limitPath, whose only usages
anywhere are inside SciJava Common itself. findResources is left for
scijava-index, which needs classpath resource scanning anyway.

Two deliberate behavioral changes:

- deleteRecursively now deletes a plain file passed to it. SciJava
  Common returned true without deleting anything, since listFiles()
  returns null for a non-directory.
- It is implemented with Files.walk in reverse order rather than
  recursion, so it no longer recurses into symlinked directories.

Tests are ported from SciJava Common, keeping the full versioned-filename
corpus (jogamp, jinput and ffmpeg native-binary naming), which is where
the real complexity lives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Ports SciJava Common's org.scijava.annotations as org.scijava.index, and
adds IndexDiscoverer, which is what the Discovery refactor was for: an
index records each implementation's class name and annotation values, so
filtering, sorting and display happen on metadata alone, and only type()
or get() touches a class.

The index format (META-INF/json/<annotation>) is deliberately unchanged,
so an index written by either SciJava Common or SciJava3 is readable by
the other. That keeps a jar built against one usable with the other
during the migration.

IndexDiscoverer stays annotation-agnostic: which type an indexed item
provides is supplied as a function, since it must be answerable from
metadata alone. For an annotation shaped like @plugin(type =
Service.class) that is item -> item.annotation().type().getName(), which
loads the declared type but never the implementation.

Two tests cover the properties that matter:

- Discovery never asks the class loader for the implementation class.
  This needs a class loader that records what it is asked for; a static
  flag on the implementation cannot show it, because reading the flag
  would itself load the class. The flag test remains, but proves only
  that nothing was constructed, and is named accordingly.
- The annotation processor works through javac, with this component on
  the processor path and nothing else configured -- the path a downstream
  project actually takes. The test asserts the index is written with its
  metadata and is then discoverable lazily.

Not ported: EclipseHelper (m2e's .factorypath generation covers it),
AnnotationCombiner (build tooling, belongs in scijava-maven-plugin), and
the legacy reader for the pre-2013 META-INF/annotations format.
DirectoryIndexer and ByteCodeAnalyzer are kept: this component needs them
to index its own test classes, and a Maven plugin needs them for the
fallback indexing path.

This component sets maven.compiler.proc to none for itself, because javac
reads META-INF/services before the processor class it names exists.
SciJava Common has the same constraint and solves it the same way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Constructor injection was the wrong call, for the reasons raised in
review: it publishes every dependency in a signature, so changing an
internal dependency becomes an API change and a binary compatibility
break unless old constructors are kept forever. It also cannot serve the
ServiceLoader tier, which requires a public no-arg constructor or a
static provider() factory.

The premise behind it was also wrong. I had assumed that letting the
container instantiate a plugin means exporting its package, which would
indeed break encapsulation -- callers could cast to implementations and
call non-API methods. But `opens` is not `exports`: a qualified
`opens <pkg> to <container>` grants deep reflective access at runtime
while leaving the package invisible at compile time.

Measured with a three-module test project on JDK 21, rather than assumed:

- ServiceLoader finds a provider declared with `provides` in a package
  that is neither exported nor opened.
- Reflective newInstance on such a package fails with
  IllegalAccessException; with a qualified `opens` it succeeds.
- setAccessible injection into a private field works with that opens.
- An ordinary module importing a class from the opened but unexported
  package fails to compile: "package ... is declared in module ..., which
  does not export it".
- A service with a private constructor and a public static provider()
  factory works; ServiceLoader prefers the factory.

So declarative field injection survives JPMS intact, encapsulation is
preserved, and the cost to a plugin module is one `opens` line. The claim
that we must give up private-field injection is removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Ports org.scijava.io.location as org.scijava.io3.location, plus ByteBank
and ByteArrayByteBank, which BytesLocation needs.

This component is deliberately standalone. It is broadly useful on its
own -- to Bio-Formats, for instance -- so its only dependency is
scijava-collections, and resolvers are discovered with plain
ServiceLoader rather than through Discoverer or a container. Nothing here
requires buying into a framework.

Plain ServiceLoader is viable precisely here, even though a generic
discovery facade cannot use it: the constraint is that ServiceLoader
resolves `uses` against its caller's module, which only hurts when
discovering arbitrary types on another module's behalf. This component
knows its own service type, so it declares `uses` in its own
module-info.java and is self-contained.

Consequent API changes, recorded in docs/migration.md:

- LocationService and DefaultLocationService become Locations, which can
  also be constructed with an explicit resolver list for embedding and
  testing.
- LocationResolver no longer extends HandlerPlugin, and carries its own
  priority() as a plain double rather than depending on scijava-priority.
  The Priority constants remain compatible with it.
- URILocation had an injected LogService that nothing used; it is gone,
  and its query decoding now uses the charset-typed URLDecoder overload
  instead of catching an impossible UnsupportedEncodingException.

Tests cover discovery without a container, priority ordering deciding
between two resolvers that support the same scheme, and the reason the
string overload exists: a Windows path is not a valid URI, so it must
fall back to being read as a file path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Ports org.scijava.io.handle as org.scijava.io3.handle, plus Bytes, which
the handles need. Same standalone approach as the locations: plain
ServiceLoader, no container, scijava-collections the only dependency.

DataHandle no longer extends WrapperPlugin. That contract was small --
getType() plus set/get -- so it is inlined as locationType(), supports(),
set(), get() and priority(). Nothing else among the fifteen handle
classes needed the plugin framework.

DataHandleService becomes DataHandles, which also absorbs the static
helpers from SciJava Common's DataHandles utility class.

Two things the port made explicit:

- Discovered handles are prototypes. A handle carries the position and
  buffers of one open stream, so create() must instantiate a fresh one
  per call rather than hand out the discovered instance. SciJava Common
  got this from WrapperService.create(); here it is explicit, with a test
  asserting that two handles on one file have independent offsets.
- DataHandles.copy reported progress to a Task, which also carried
  cancellation. Progress is now a plain LongConsumer of the running byte
  count, so callers wanting org.scijava.progress can adapt it in a
  lambda. Cancellation is therefore no longer supported by copy; if it is
  wanted it should be a deliberate mechanism, not a side effect of the
  progress type. Recorded as an open question in the plan.

Still to port in this component: io.nio.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
SciJava has accumulated several cancellation APIs -- Cancelable in
SciJava Common and ImageJ Ops, the cancel half of task.Task, Appose's
cancel() -- and SciJava Ops shipped with none of them. Rather than invent
a fourth, use the platform's: interruption as the mechanism, and
Future.cancel(true) as the API a caller holds.

Every scheme is cooperative, so that is not what distinguishes them;
composition is. An ambient thread flag is honored by code we did not
write, including every interruptible JDK blocking call, while an
isCanceled() interface only works for code that knows about it, and a
cancellation token pollutes every signature while still being unable to
abort a blocking call.

Note that an interrupt carries no reason: Thread.interrupt() takes no
argument and the JDK constructs InterruptedException with a null message.
A reason belongs to whoever requested cancellation, not to the exception.

Implemented here:

- Progress.checkCancellation() throws CancellationException, restoring
  the interrupt flag first so code further up the stack still sees it.
  Progress.update() calls it, so anything already reporting progress is
  cancellable with no new API and no extra parameter. Ops cannot throw
  InterruptedException, being plain Functions, hence the unchecked
  CancellationException, which is also what Future.get() throws.
- DataHandles.copy checks the flag once per block and throws
  InterruptedIOException, which is an IOException -- so the signature is
  unchanged -- with bytesTransferred set to the partial copy. The
  granularity matches the task.isCanceled() check it replaces; a read
  already under way cannot be aborted, since plain streams are not
  interruptible.

Cancelable is therefore not ported at all. The docs record the design,
its accepted limits (CompletableFuture.cancel does not interrupt; the
flag does not inherit to worker threads; native code ignores interrupts),
and why Appose's explicit cancel() stays as it is: interruption cannot
cross a process boundary.

Still to do: Parallelization and TaskExecutor must propagate cancellation
to worker futures, since the interrupt flag does not inherit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Interrupting a thread that had farmed work out to a TaskExecutor
cancelled the workers but lost the cancellation at the caller: the
InterruptedException was wrapped in a plain RuntimeException, and the
flag it cleared on the way out was never restored. Code further up the
stack could not tell cancellation from failure, and an outer op polling
Progress.checkCancellation() would never see it.

Half the propagation was already correct and worth noting, since it is
easy to reimplement by accident: ExecutorService.invokeAll cancels its
unfinished tasks when the awaiting thread is interrupted, and cancels
them with interruption. The missing half was the caller's own flag.

The two call sites now separate InterruptedException from
ExecutionException, restore the flag, and throw CancellationException --
the same type Progress.checkCancellation() throws, so one exception type
means "cancelled" throughout.

The test fails against the previous code with exactly the symptom
described: "expected CancellationException, got: java.lang.RuntimeException:
java.lang.InterruptedException".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
NIOService and DefaultNIOService wrap two JDK calls behind a
system-property switch, and have no usages in the scijava or imagej
repositories. ByteBufferByteBank has none either, and caps at
Integer.MAX_VALUE exactly as ByteArrayByteBank does, so it adds no
capability -- only a pluggable allocator such as ByteBuffer::allocateDirect.
Since ByteBank itself is ported, a direct-buffer bank is a small class to
write when something actually needs one.

NB: SCIFIO is the most plausible consumer and could not be checked, there
being no local checkout.

This completes scijava-io3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
I had dropped this as adding no capability over ByteArrayByteBank, since
both cap at Integer.MAX_VALUE. That was wrong: a direct ByteBuffer is
off-heap, so its bytes can be handed to native code and to shared memory
without a copy. That matters increasingly for Fiji as Appose's named
shared memory blocks become more prominent.

Ported with two additions that make the capability usable and observable:
direct() factories, and an isDirect() accessor.

It also fixes a bug that defeated the entire purpose. Growing the bank
reallocated with ByteBuffer.allocate, so a direct bank quietly became a
heap bank as soon as it outgrew its initial capacity -- and since the
default capacity is 10,000 bytes, any real payload would trip it. Growth
now goes through the bank's own provider, and a test asserts a direct bank
is still direct after growing well past its initial capacity.

NIOService stays dropped: it wraps two JDK calls behind a system-property
switch, was inherited from old Bio-Formats code, and SCIFIO only exposes
it as a gateway accessor without using it. Nothing in the ported
architecture precludes FileChannel-based access later -- a handle may open
a channel, and a byte bank may be backed by a mapped buffer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Replaces SciJava Common's event package without porting it, so the bushe
fork and its attribution obligation are gone.

subscribe(Class<E>, Consumer<? super E>) is type-safe, needs no
reflection and therefore no `opens`, and returns a Subscription. The
@eventhandler ergonomics are not lost: scijava-context already reflects
over services, so it can scan annotated methods, register them as
lambdas, and hold the subscriptions -- the convenience lives in the tier
that already pays for reflection, and a service author still never writes
cleanup code.

Subscribers are held strongly. SciJava Common held them weakly, which
produced handlers that silently stopped firing, and forced a WeakHashMap
named keepEm whose only job was to stop the proxies being collected too
early. Cleanup is structural instead: EventBus.close() drops every
subscription, and a context calls it on dispose.

There is deliberately no shared instance. A bus is nothing but shared
mutable subscriber state, so an ambient singleton would let two
application contexts hear each other's events; each context owns one bus.
NB: this is the opposite call from Locations.get() and DataHandles.get(),
which are safe to share precisely because they hold no such state.

Also:

- Class-hierarchy dispatch is kept, since it is what makes a bus more
  than a listener list, and any object may be an event -- no SciJavaEvent
  base type, so a class from another library can be one.
- Consumption is opt-in through a Consumable interface on the event,
  modeled on AWT's InputEvent.consume(), so toolkit-style input events
  can stop propagation without every event paying for the concept.
- Subscriber order is by scijava-priority, descending, ties broken by
  subscription order. It matters most for consumable events, where the
  first subscriber gets first refusal.
- A failing subscriber does not truncate delivery: everyone still
  receives the event and the failures are thrown together afterward as an
  EventDeliveryException, first as cause and the rest suppressed.
  Cancellation is the exception -- an interrupted subscriber, or one
  throwing CancellationException, stops delivery at once rather than
  being buried among unrelated failures.

EventHistory is dropped; a subscriber on Object.class reproduces it.

scijava-listeners is not folded in after all: a listener list is the
right primitive for "this object notifies its observers", and is a
different thing from a bus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
The digit in common3 and io3 is the component's incarnation, not a
workaround for a package collision: breaking changes fork to a new
incarnation with a new artifactId and package prefix, so both can sit on
one classpath and downstream code keeps working.

The rule that makes deprecate-don't-break coherent: a major version bump
within one incarnation should never happen. If compatibility must break,
the incarnation digit moves with it.

Ideally the major version equals the incarnation number, which today it
does not, because the first release of this reactor was a single
monoversioned big bang -- releasing a reactor piecemeal with
heterogeneous versions needs tooling that release-version.sh does not yet
have. Removing the interfaces that moved to scijava-spi is the
realignment opportunity, since it breaks compatibility anyway.

scijava-persist is recorded as later-phase: the idea is sound and the
discovery half is SciJava-shaped, but the code hard-wires Gson into its
SPI, vendors Gson's RuntimeTypeAdapterFactory, and is built on Context.
A rewrite would be smaller than a port, and there is no concrete core
consumer today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
The major version tracks the incarnation, per the incubator README: an
artifact named io3 is the third incarnation and should be 3.x.y. The
1.x.y of the other components is a legacy of the first monoversioned
release rather than the intended scheme, and this one is unreleased, so
it can simply start out aligned.

Releasing a reactor with heterogeneous versions still awaits the
release-version.sh improvements; nothing here depends on that landing
first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Phase 2 begins: Service, Context, lazy creation, priority selection,
lifecycle, disposal, and one EventBus per context.

Services are discovered as Service, never as their own interface, and
this is forced rather than chosen: ServiceLoader resolves `uses` against
its caller's module, so a container cannot look up an arbitrary
third-party interface on that module's behalf -- it would have to declare
`uses com.example.LogService`, which it cannot know. Declaring one base
type that this module does `uses` is what makes discovery work under JPMS
at all; the container then sorts implementations by the interfaces they
implement. Provider.type() keeps that cheap, since implementations of
other types are never constructed.

Services are created lazily, where SciJava Common built every service it
could find at context creation. Asking for one constructs it, initializes
it, and constructs whatever it asks for in turn.

scijava-context-test is a sibling module because that is the only honest
way to test what matters: a module must really declare `provides`, and
its implementation package must really be unexported, or the test proves
nothing. It verifies that a service in a package that is neither exported
nor opened is still discovered and constructed, and asserts that it is
running on the module path rather than skipping quietly should the build
ever stop being modular.

Two bugs the tests caught, both ordering:

- Context.of initialized each service as it registered, so a service
  depending on one declared after it failed. Registration and
  initialization are now separate phases.
- Services were recorded for disposal on the way in, before their
  dependencies had resolved, so reversing that list tore dependencies
  down before their dependents -- exactly backwards. They are now
  recorded once initialization finishes, so dependencies land earlier and
  reversing is correct.

Note a consequence of acquiring dependencies in initialize rather than in
the constructor: two services may depend on each other and it resolves,
where constructor injection could not do it at all. The cost, documented
on the method, is that a service in such a cycle may briefly observe a
peer that has not finished initializing.

Contextual, AbstractContextual and NullContextException are not ported: a
service receives its context in initialize, so it need not carry one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
@plugin (indexable, with @attr) and Context.plugins(Class), which returns
Discovery descriptors straight from the annotation index: class name,
metadata and priority readable with nothing loaded, and construction only
on demand. That is what keeps a menu of a thousand commands cheap to
build, and it is the payoff for the Discovery refactor.

scijava-context-test exercises the whole path through a real build:
annotated classes in a package that is opened to the container but
exported to nobody, indexed by the annotation processor at compile time,
discovered with their metadata, constructed only when asked.

Two JPMS findings, neither of which survives reasoning alone:

- An `opens` must name the module that actually performs the reflection.
  The first attempt failed with IllegalAccessException because the
  package was opened to org.scijava.context while newInstance was being
  called inside org.scijava.discovery. Generic discovery code therefore
  cannot construct a plugin on a container's behalf: Discovery.of and
  IndexDiscoverer now take an optional instantiator, and the context
  supplies one defined in its own module, so users open their packages to
  org.scijava.context exactly as @plugin's javadoc says.

- The annotation processor must be on the annotation processor path, and
  its absence is silent. The first build produced no index and reported
  nothing: javac does not find processors on the module path, and says
  nothing when it finds none at all. This is the hazard recorded in the
  plan, met first-hand. Whether pom-scijava should configure this for
  SciJava components is worth deciding, since the people most likely to
  fall into it are the hobbyists @plugin exists to serve.

SciJava Common's HandlerPlugin/WrapperPlugin/TypedPlugin/SingletonPlugin
hierarchy is not ported: each user inlines the small contract it needs,
as DataHandle already did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
@dependency fills in services, the Context and the EventBus, after
construction and before Service.initialize, so a dependency is ready by
the time initialization runs. An optional dependency (required = false)
stays null; a missing required one names the field and the type it
wanted.

It is deliberately not called @parameter as in SciJava Common. There that
annotation meant two unrelated things -- a dependency to inject, and an
input to a module -- and the second is now org.scijava.struct's business.
Keeping the name would have preserved muscle memory at the price of
re-importing the confusion.

scijava-context-test injects private fields of a plugin across a real
module boundary, which proves both halves of the JPMS claim at once: a
qualified `opens` grants the container the deep reflection it needs, and
the package remains exported to nobody, so ordinary callers still cannot
reach the implementation.

Also makes two assertions in PluginDiscoveryTest select plugins by name
rather than by position. Adding a third plugin to the test module broke
them, which is a fair warning about how they were written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
The container turns annotated methods into subscriptions, which is what
the two-tier design was protecting: the event bus itself stays
reflection-free and needs no `opens` from anyone using it directly, while
the container -- which already reflects, for injection -- provides the
convenience.

A service's handlers are subscribed when the context creates it and
dropped when the context is disposed. A service's lifetime being the
context's, the author writes no subscription or cleanup code at all, and
needs no `opens` beyond the one @dependency already requires.

Objects the context did not create are not subscribed automatically. A
plugin may be created and discarded many times in a session, and handlers
outliving their objects would pile up -- the leak that weak references
used to paper over. Context.subscribe(Object) returns the subscriptions
for the caller to close.

What a handler throws reaches the publisher unwrapped rather than as an
InvocationTargetException, so the event bus's aggregated failures say
something useful.

scijava-context-test covers the case that matters: a service with a
private handler, in a package opened only to the container, hearing
events and falling silent when the context is disposed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
The plan said Phase 2 would bring over "the core services: object
registry, prefs, thread, app/status". Measured against usage and against
what has since been built, most of that does not survive: StatusService
is superseded by scijava-progress, ThreadService is mostly EDT dispatch
and so belongs with the UI layer, AppService is largely metadata that
scijava-meta already covers, and ObjectService should land with the
widgets and conversion layer that use it, so that a consumer shapes it.

PrefService is two things. Remembering what a user last typed into a
given parameter of a given module is policy, and means nothing without
the input harvester, so it goes to Phase 4. A settings store is a general
capability whose API needs no consumer to get right, so it is deferred
but its design is settled now, and recorded: not java.util.prefs, whose
opacity is the actual complaint; YAML, since snakeyaml is already here
and unlike JSON supports comments; $XDG_CONFIG_HOME/fiji or
~/.config/fiji on every platform, printable at runtime; namespaced by
class; no migration from the old store.

One question is genuinely blocked on the harvester and is recorded as
open: stale keys accumulate for modules that no longer exist, and the
harvester is what knows which are still live.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
TOML reads like the INI files people already understand, supports
comments, and -- unlike YAML -- is neither whitespace-fragile nor
implicitly typed, so `no` stays the string `no` and `1.10` stays `1.10`.
Those traps bite exactly the values a config file holds. Jaunch already
uses TOML, so the format is familiar across the stack.

The cost is a dependency, since the JDK has no TOML parser where
snakeyaml was already present; the library is worth choosing when this is
actually built.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Phase 3 begins. @parameter on fields, a MemberParser that reads them
(inherited ones included), and Executables.run, which binds inputs by
name and hands back outputs by name -- SciJava Common's module layer,
rebuilt on scijava-struct.

@parameter here means only what it meant in SciJava Common's module
sense: an input or output of something runnable. Injecting a service is
@dependency, in scijava-context. Conflating the two is what made the
original annotation confusing.

Cancellation needed nothing of its own, as the design predicted: a run is
a Callable for an executor, and cancelling the Future interrupts it. The
test cancels a sleeping task and checks it did not reach the end.

Two bugs the tests caught:

- FieldParameterMember inherited Member's read-only createInstance, so
  every input binding threw UnsupportedOperationException. It must return
  a ValueAccessibleMemberInstance to be writable.
- The required-input check tested whether the field was null, which
  silently passes for primitives: a missing required double would have
  run as 0.0 and produced a wrong answer rather than an error. A required
  input must now be supplied, not merely non-null.

On lifting the member parsers out of ops-engine, which the plan called
for: inspecting ops.engine.struct, only about five of nineteen classes
are framework-neutral. The parsers reach into Op-specific exceptions,
Op-specific annotations (@nullable and @OpDependency, in the already
released scijava-ops-spi) and ops.engine.util helpers, so lifting them
means first deciding where an optional-parameter annotation lives and
then changing a released module. Better to build this layer first and let
a second real consumer show what is genuinely shared. The plan records
the specific couplings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Runner.run is always asynchronous, returning Future<ExecutionResult>. One
verb covers both cases, since a caller wanting to block writes
run(...).get() -- as SciJava Common's ModuleService has long shown.

Declining is not cancellation, and the two now have different shapes. A
preprocessor may stop a run before it happens, because the user closed a
dialog or a precondition is unmet; that is an ordinary outcome and comes
back as a result carrying reason() and declinedBy(). Interrupting work
already under way remains Future.cancel(true), surfacing as
CancellationException. A test asserts both, side by side.

ExecutionResult is deliberately not itself a Future: isCancelled() and
get() are coupled by contract, so a declined run modelled as a cancelled
future would be an exception again, which returning a result was meant to
avoid. The asynchrony lives in the future, what happened lives in the
result.

declinedBy() returns the preprocessor instance rather than its class,
since a chain may hold two instances of one class -- there is a test with
exactly that, a permissive and a strict Gate, asserting the result names
the one that objected.

Declining lives on the execution rather than the preprocessor. SciJava
Common put isCanceled()/getCancelReason() on the plugin, which made
preprocessors stateful and unsafe to share between runs; that matters
more here, since Context constructs a fresh plugin instance per call.
Telling the execution keeps preprocessors stateless and gives
declinedBy() the right instance for free.

Required-input checking moves from binding time to after preprocessing,
since supplying values the caller omitted is precisely what a
preprocessor such as an input harvester is for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
The plan kept saying "drop" about the application shell without saying
where it goes instead, which reads as though Fiji loses its menus. It
does not: the problem with SciJava Common was never that it had menus,
but that the core had them, so a library wanting only plugin discovery
inherited MenuPath, Display and a transitive AWT flavour.

Adds a layering section: foundation (built) / application model /
toolkit bindings / application, with only the bindings importing AWT or
Swing. All of the presentation metadata survives -- menu path, label,
icon, accelerator, weight, selection group, visibility, enablement --
since Fiji's menus are built from it. What changes is that it becomes
attributes in the annotation index rather than the
BasicDetails/UIDetails/AbstractUIDetails inheritance chain every plugin
extends.

Notes that the expensive part is already solved: SciJava Common builds
its menu from ModuleInfo, naming classes without loading them and
loading on demand, which is exactly the implClassName()/type()/get()
split of Discovery. The other seams -- harvesting as a Preprocessor,
displays as a Postprocessor, input events with Consumable -- were built
in phases 1-3 with this in mind.

Also records that the real risk is migration rather than architecture:
hundreds of commands across dozens of repositories cannot update on a
flag day, so the one-way legacy bridge matters more than its deferral
implies and should be sequenced with layer 1 rather than after it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Executable and ExecutableInstance are SciJava Common's ModuleInfo and
Module, renamed because "module" now means a JPMS module and this
codebase is full of module-info.java files.

The point of the change is that Executable mentions no Java reflection:
it is name(), struct() and create(). A script declares its parameters in
its header and holds their values in engine bindings, with no @parameter
field anywhere, and must still reach the menu, the harvester and the
runner by the same path as a Java command. Building this layer on
Runnable, as it was, would have forced scripts into a parallel mechanism
bolted on later.

JavaExecutable is now one implementation among others: the case where
parameters happen to be annotated fields. Runner.run keeps a Runnable
overload as sugar.

A test stands in for a script -- parameters from a list, values in a Map,
run() reading and writing those bindings -- and runs it through the same
Runner with the same preprocessors, for the same ExecutionResult. Another
asserts that two runs get independent bindings, the concurrency trap that
treating the executable as its own instance would invite.

Also adds an index test proving that two indexable annotations on one
class are indexed independently, each to its own file. That is what lets
presentation metadata stack onto a plugin declaration as @plugin plus
@menu, rather than every plugin type's attributes accumulating in one
annotation -- and it needs no new machinery, since the processor already
indexes anything meta-annotated @indexable. The test also needed the
component's own classes added to the compile path explicitly: these tests
run on the module path, so java.class.path alone does not contain them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
ctrueden and others added 24 commits October 28, 2026 13:50
The contracts first, toolkit-free:

- `Widget` is a view of a `ParameterNode`, and that is all. There is no
  `WidgetModel`: SciJava Common needed one because a widget could not see
  the module, whereas a widget here reads the node for label, bounds,
  style and resolved choices, and writes through `ParameterModel`.
  Writing through the model is what runs callbacks and reshapes the
  dialog, so a widget earns the dynamic behavior by doing nothing clever.
- `WidgetPanel` *is* a `Widget`, so a group nests in its parent's list
  with no special case, however deep. `WidgetPanels` walks the tree once
  for every toolkit; a binding supplies factories and nothing else.
- `InputHarvester` is a `Preprocessor`. A dismissed dialog calls
  `Execution.decline`, an ordinary outcome the `ExecutionResult`
  reports, rather than a `canceled` flag on a stateful preprocessor.

Then Swing: number (spinner, optional slider), text (field, password,
area), toggle, choice, file, the panel and the dialog. MigLayout is gone
in favor of `GridBagLayout` -- two columns of labels and fields are not
worth a third-party dependency, still less an automatic module in a JPMS
build. The button, color, date, file-list, message and radio-choice
widgets are not ported; each earns its place when something asks.

Three changes fell out of meeting a toolkit:

- `@Parameter` gains `min`, `max`, `stepSize` and `style`. All strings,
  because metadata has to survive a script header; `Widgets` reads the
  bounds back in the parameter's own type, and style hints are advisory,
  so a parameter styled for Swing still renders elsewhere.
- `Execution.executable()` returns `ExecutableInstance` rather than
  `Object`. Its one caller already cast it.
- `Executables.executableOf` takes a lookup, so an object run through a
  container needs no `opens` beyond the one its module already declares.

`Demos` in the test sources puts each tricky behavior on screen, with a
launcher window and a `main` that takes a demo's name:

  mvn -o -pl scijava-swing test-compile exec:java \
    -Dexec.classpathScope=test -Dexec.mainClass=org.scijava.swing.Demos

The kitchen sink, an advanced toggle, a generated group, computed
choices, live validation, and two values that keep each other in step.
`SwingWidgetsTest` covers the same six headlessly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
The empty dialogs were not a widget bug: with no annotation index on the
classpath no widget factory is discovered, and the dialog comes up with
nothing but OK and Cancel. javac writes that index only when
scijava-index is on the annotation processor path, and says nothing at
all when it is not, so the harvester now says it -- naming the file --
and `WidgetPanels` warns about any parameter no factory accepted. Both
through SLF4J, with slf4j-simple as a test dependency so the demos log:

  -Dorg.slf4j.simpleLogger.defaultLogLevel=debug

The stale choices were real. A widget held the `ParameterNode` it was
built from, but `ParameterModel` builds a new tree on every `set`; where
the tree's shape was unchanged nothing rebuilt, so a chooser went on
asking a dead node for its values while the model had already moved on.
`SwingWidget.node()` now reads its node back out of the current tree.

Dialog behavior, from watching one: it never moves and never shrinks
after the first build -- it may grow for parameters that have just
appeared, but re-centring or closing up around a collapsed group moves
the controls out from under the pointer. The scroll pane's border is
gone, and the widgets hug the top, a GridBagLayout having otherwise
centered its rows in whatever height the dialog was dragged to.

Gaps filled, all of them things SciJava Common had:

- a scroll bar as well as a slider, and labelled tick marks
  (`style = "slider,ticks:4"`);
- `softMin`/`softMax`, so the slider spans the useful range while the
  spinner still enforces the permitted one;
- enums, whose constants are their choices -- resolved in
  `ParameterTree`, so every toolkit gets it and no annotation repeats
  what the enum already says;
- `BigInteger` and `BigDecimal`, which go to the text widget and are
  parsed exactly, a spinner being unable to step them;
- `char`, and boxed numbers alongside primitive ones;
- message rows (`style = "message"`), whose value a callback may rewrite.

And a seventh demo, the one worth having: width, height and a lock
aspect ratio checkbox, with a message row showing the live ratio.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
`MenuCreator` and `Menus` go in scijava-command, beside the `MenuTree`
they walk: descend the tree, a menu per branch and an item per leaf, so
a toolkit implements four methods and inherits the walk. Two type
parameters, because a menu bar and a menu are different types in every
toolkit worth supporting -- and separate method names, because two
overloads would have had the same erasure.

`SwingMenus` is the first implementation: labels, icons and accelerators
in ImageJ's notation (`^O`, `^+C`, where `^` is command on a Mac) as
well as Swing's own. It loads no command class -- everything it needs
came out of the annotation index.

`Shell`, in the test sources, is the application: menus, a search bar
that reaches commands in no menu at all, a status line and a progress
bar. Choosing a command runs it through `Runner` with the Swing
harvester, so the dialog work arrives here intact.

  mvn -o -pl scijava-swing test-compile exec:java -Denforcer.skip \
    -Dexec.classpathScope=test -Dexec.mainClass=org.scijava.swing.Shell

Also recorded in the plan: what the retired `fijifx` experiment asked
and this design answers -- chiefly that menus without class loading did
*not* require every menu item to be a script, and that its `AppLoader`
sketch is the shape the legacy bridge should take.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
"Toolkit-agnostic" was an assertion. A second implementation makes it a
measurement, and the measurement is good: the whole of scijava-ui3
survived JavaFX with two changes, both of them Swing keeping something
that was never Swing's.

- `Widget.isLabeled()` -- whether a parameter wants a label beside it is
  a statement about the parameter. Every toolkit would have answered it
  identically, so every toolkit should not have had to.
- `WidgetPanel.showProblems(Map)` -- how a problem looks is a toolkit's
  business; that there is one, shown per parameter and cleared when
  fixed, is not.

Nothing else moved. `Widget`, `WidgetFactory`, `WidgetPanelFactory`,
`WidgetPanels`, `Widgets`, `InputHarvester`, `MenuCreator` and `Menus`
were untouched, and the model layer never came into question.

What JavaFX forced that Swing never would:

- The toolkit must be started, exactly once, and there is no
  `invokeAndWait`. `FxThread` is that plumbing, and it avoids
  `Application.launch` on purpose: that method seizes the process and
  returns when the last window closes, which is wrong for a binding an
  application, a script or a test may drive.
- `showAndWait` runs a nested event loop, so the dialog is built and
  shown in one visit to the JavaFX thread, where Swing wanted two.
- JavaFX spinners do not preserve a parameter's type -- their value
  factories are Integer or Double and nothing else -- and an editable
  one discards what was typed unless Enter is pressed. Both are handled
  in `FxNumber`; neither has a Swing counterpart.
- Some things are easier: a collapsible group is a `TitledPane`, and
  `KeyCombination.SHORTCUT_DOWN` says what Swing computes through a
  `Toolkit` call.

And so that the two cannot drift, `scijava-ui3-test`: the sample
commands both bindings render, and a conformance suite both extend --
eight assertions about widgets, three about menus. A binding's own tests
now cover only what is genuinely its own. The Swing demos and shell
moved onto the shared commands, which is why they look edited here.

`FxShell` is the same small application as the Swing one:

  mvn -pl scijava-javafx test-compile exec:java -Denforcer.skip \
    -Dexec.classpathScope=test -Dexec.mainClass=org.scijava.javafx.FxShell

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Three is where agnosticism gets believable, and AWT is the case that
tests it: no spinner, no slider, no titled border, no collapsible pane,
no icons in menus, and a MenuShortcut that cannot say alt. The number
widget is a text field and a scroll bar honoring min, max, stepSize and
the soft bounds; a collapsible group is a button that hides a panel; an
accelerator AWT cannot express is dropped rather than approximated into
some other key. None of it asked the contracts to change.

What it did change is where shared code lives:

- `AbstractWidget` moves into scijava-ui3. Three bindings had written
  the same bookkeeping -- find the node in the *current* tree, write
  through the model, do not mistake a refresh for the user typing. Two
  would have been a coincidence.
- `Accelerator` moves into scijava-command. Swing wants a KeyStroke,
  JavaFX a KeyCombination, AWT a MenuShortcut: three ways of saying a
  shortcut, one thing being said. A binding that parses `^+C` itself is
  a binding that can disagree about what it means.

On the four-component question. scijava-ui-swing imported exactly six
things from scijava-ui-awt -- the input, drag-and-drop and window event
dispatchers, colors, windows, clipboard -- and not one widget, because
java.awt.Choice and JComboBox have nothing in common. So Swing depends
on AWT for the *platform* and never for widgets; `Edt` is the first
instance, the dispatch thread being AWT's and SwingUtilities a forwarder
to it. The widget types stay siblings on purpose: SwingWidget could
extend AwtWidget, but discovery filters on widgetType(), so that would
mean a pure-AWT application quietly filling its dialogs with Swing
controls as soon as scijava-swing was on the classpath.

AWT also settled a difference worth recording: TextComponent.setText
does not notify text listeners, where a Swing document would.

  mvn -o -pl scijava-awt test-compile exec:java -Denforcer.skip \
    -Dexec.classpathScope=test -Dexec.mainClass=org.scijava.awt.AwtShell

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Binding an input to a parameter converts, and a script, a command line
and a text field all have only strings to offer -- so conversion cannot
be something only a container has. scijava-convert3 depends on nothing
but scijava-common3 and scijava-priority, and finds its converters
through plain ServiceLoader.

`Converter.supports(Object, Type)` asks whether *this value* can become
*that type*. SciJava Common had eight `canConvert` overloads across
Object/Class/Type pairings, a ConversionRequest to carry the
combinations, and an AbstractConverter to make that bearable. Declaring
sourceType() and destType() answers the common case by itself, so

  Converter.of(String.class, Foo.class, Foo::parse)

is a whole converter.

DefaultConverter's sequence of special cases becomes one converter per
concern -- cast, number, string-to-value, to-string, array, collection,
file/path, and a wrapping constructor last -- each separately
discoverable, orderable by priority, and replaceable by a component that
disagrees, which a monolith never allowed.

Unconvertible is not zero: tryConvert returns empty and convert throws,
where SciJava Common returned null for both "cannot" and "converted to
null". That distinction is what a widget needs to leave a parameter
alone while the user is still typing -- and with it available,
`Widgets.toType` is now one call into convert3 rather than a third copy
of text parsing. A BigDecimal from a double goes through the string, so
0.1 stays 0.1 rather than the binary double nearest it.

Wired in where it earns its keep: `Executables.bind` converts supplied
inputs, so run(cmd, Map.of("a", "1.5")) reaches a `double a`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
It runs through the same Runner, meets the same preprocessors, harvests
its inputs through the same dialog in whichever toolkit, and can sit in a
menu beside a Java command. Nothing above this layer knows which it is --
a claim now tested rather than asserted: ScriptWidgetsTest renders a
Groovy script's parameters with the Swing widgets and drives its
callbacks through an actual JSpinner.

javax.script is the first backend, not the only one. ScriptLanguage and
ScriptSession are the SPI; JsrLanguage turns every JSR-223 engine on the
classpath into one, so Groovy, Jython, JRuby, Clojure and BeanShell cost
nothing and declare nothing. GraalVM polyglot and Appose become further
ScriptLanguages without this layer changing.

Callbacks work for scripts, which SciJava Common never managed. It
resolved `callback = "foo"` by reflecting a Java method, leaving a script
nothing to name; here the name is resolved by whatever kind of code the
executable is, and for a script that means a function it defined. Three
things make it work:

- a *session* rather than a one-shot eval, so the functions still exist
  when a parameter changes;
- what the function changed is converted back into the parameters --
  Groovy's `celsius * 9 / 5 + 32` is a BigDecimal, and convert3 knows
  what the parameter's declared type wants;
- every declared parameter is bound, including unfilled ones, since a
  language that resolves names at run time fails on an unbound name
  rather than seeing null.

Validators and choicesFrom come along for the same ride.

The #@ header syntax is unchanged, because tens of thousands of scripts
are written in it. Declaration lines are blanked rather than removed, so
the language's own error messages keep pointing at the line the author is
looking at -- #@ being a comment in Python and a syntax error in Groovy,
leaving it in was not an option.

Not ported: the ScriptProcessor chain, which existed to rewrite a script
before evaluation and was used for parameters and a shebang, both of
which the header reader does directly. ScriptService has no equivalent.
ScriptREPL, ScriptInterpreter, AutoCompleter and CodeGenerator belong
with the script editor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
Answering "how does a script say where it belongs, and what language it
is written in":

- `#@script(menu = "Process>Filters>Blur It", accelerator = "^B", label,
  weight, iconPath)` -- the same keys @menu uses. These directives were
  parsed already and consumed by nothing; now they mean something.
- Where a script says nothing, the directory speaks for it:
  scripts/Process/Filters/Blur_It.groovy means the same thing,
  underscores read as spaces. That is how the scripts in the wild are
  arranged.
- `#@script(language = "jython")` or a `#!` line settles which language
  is meant, for the ones sharing an extension -- .py being Jython or
  Python. Silence means the extension decides.

Making that work needed the gap underneath filled. `MenuTree.of` took
`Collection<CommandInfo>`, and a CommandInfo is class-and-index shaped;
a script is not a class. So `ExecutableInfo` is now the question the
menu layer asks -- menuPath, label, accelerator, iconPath, weight,
visible -- and it extends Executable, so what a menu holds is the thing
to run. `CommandInfo` implements it; `ExecutableInfo.of(executable,
attrs)` makes one out of a script and its directives; a SciJava Common
ModuleInfo will make one through the bridge, with no further change to
the menu layer, the bindings or the shells.

It is deliberately not called `MenuEntry`: a menu path is optional, and
plenty of runnable things belong in no menu -- one invoked by name from
a script, one reached only through the search bar, one that exists to be
called by something else. MenuTree takes the subset with a path;
CommandsTest pins that a pathless command is still labelled, still
runnable, and simply absent from the tree.

`ScriptFinder` reads a directory into scripts plus metadata maps -- not
menu entries, since scijava-script3 knows nothing about menus. All three
shells now build their menus from the annotation index *and* a directory
of Groovy scripts, and nothing below them can tell which came from
where. That is the AppLoader shape the fijifx notes sketched, now
load-bearing rather than planned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
A command is anything a user can run, with declared parameters --
whether its implementation is a class or a script. So that is what
`CommandInfo` means now, and the class-based kind is `ClassCommandInfo`.

The old names said the opposite. `Command` claimed in its javadoc that a
script is a command whose implementation is source in some language,
while the type system had `Command` meaning the Java-class kind
specifically and scripts attaching one level lower, at `Executable`. The
javadoc was the intent; the types were what got built when scripts
arrived after the command layer. This reconciles them in the direction
of the intent.

Not `JavaCommandInfo`: Kotlin, Groovy and Scala classes implement
`Command` just as well, so the distinction is class versus script, not
Java versus everything. And `ClassCommandInfo` rather than
`CommandClassInfo`, so that the family reads as a series -- the siblings
coming are `ScriptCommandInfo` and `LegacyCommandInfo`.

The alternative was to believe the types instead: call the general thing
`ExecutableInfo` and let "command" keep meaning the class kind. That is
SciJava Common's design with "executable" substituted for "module" --
one word users do not say traded for another.

`Command` the interface is now documented as what it is: the way to
write a command as a class, not the definition of one. A script
implements nothing at all and is no less a command for it.

docs/migration-plan.md gains an "On the words" table, so the vocabulary
stops drifting: command, class command, script command, executable,
module (JPMS only), plugin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
`scijava-bridge` presents SciJava Common's modules as SciJava3 commands,
so an application can show, harvest and run everything the old world
already knows about -- every ImageJ2 command, every Fiji plugin ported to
the module system -- beside its own commands and its scripts, in its own
menus and dialogs, with neither side changed. It is the only component
that depends on SciJava Common, and the dependency goes one way.

Before any of it could work: **org.scijava.command was a split package**.
SciJava Common owns it and SciJava3 had claimed it too. Two modules
containing one package is a JVM that refuses to start; on the classpath
it is worse, two different `Command` interfaces under one name with the
first jar winning. Fiji could never have loaded both. The incarnation
rule was applied to io3, convert3 and script3 and simply missed here, so
this renames the component to scijava-command3 / org.scijava.command3,
version 3.0.0-SNAPSHOT. Checking all 77 SciJava Common packages against
all 34 SciJava3 ones: that was the only collision.

What else the bridge turned up, each of which is a translation rather
than a compromise:

- SciJava Common's @parameter means both "this is an input" and "inject
  this service"; its harvester tells them apart by having preprocessors
  fill the services first. SciJava3 split the annotations so the question
  never arises, so the bridge answers it once: a Service or Context
  parameter is the legacy context's business and never reaches a dialog.
  Without this, every DynamicCommand dialog would show four spurious rows.
- DynamicCommand needs no SciJava3 equivalent to be supported. Its
  parameters live on the module's own description, so the bridge
  initializes at create() time, as InitPreprocessor does, and reads that
  description. What SciJava3 declines to offer as a way of writing
  commands, it presents without trouble.
- Callbacks and validators resolve by name through behavior(String) --
  the SPI that exists so scripts could have callbacks turns out to be
  exactly what legacy commands need. SciJava Common's two validator
  protocols (throw, or return a message) collapse into SciJava3's one,
  and its "Error executing method" wrapper is unwrapped so the user sees
  what the validator actually said.
- Accelerators are translated rather than passed through:
  Accelerator.toString() renders Swing's "control L", which Swing parses
  back and JavaFX does not. Written ^L, all three bindings agree.
- Menu paths are rebuilt from their entries, getMenuString() rendering
  "A > B > C" with spaces that would end up inside the labels.
- LegacyContext keeps SciJava Common types out of the bridge's
  signatures: a JPMS module would otherwise need `requires org.scijava`
  merely to hold the context, and a test-scoped use cannot say that at
  all.

The bridge's tests stop at the dialog *model*, which is where its claim
ends; that a correct model becomes correct widgets is what the bindings'
shared conformance suite already establishes three times over.

NB: the monorepo already has a scijava-legacy, for ops and ImageJ2
interop, which `requires net.imagej`. Keeping the command bridge separate
is deliberate: an application wanting Fiji's existing commands in its
menus should not thereby acquire ImageJ2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
…mpat

scijava-bridge and the existing scijava-legacy were two names for what
reads as the same job, which was going to confuse people. Now the
compatibility components are named for the foreign thing each adapts to:

  scijava-compat         SciJava Common's commands, usable from SciJava3
  scijava-ops-compat     SciJava3's ops, usable from a SciJava Common app
  scijava-imagej-compat  ImageJ2's types, readable by scijava-types

The first two are the same boundary in opposite directions, kept apart
because their dependencies are disjoint: an application wanting Fiji's
existing commands in its menus should not thereby acquire ops.

Scrutinizing the dependencies, as suggested, is what shook the third one
out. scijava-legacy held two SciJava Common/ops classes and one ImageJ2
type extractor, and its module-info said `requires net.imagej` for all of
it -- so anyone wanting ops inside a SciJava Common application acquired
ImageJ2 on account of a class that has nothing to do with SciJava Common.
Being entirely about net.imagej.Dataset, it is a candidate for moving to
an ImageJ repository; its own module-info says so.

The split cost five build iterations, every one a dependency that no
import names:

- scijava-discovery, arriving through scijava-ops-api in one module and
  through scijava-types's `requires transitive` in the other;
- net.imglib2, because Dataset's supertypes live there;
- scijava-ops-engine, because OpEnvironment.build() finds its
  implementation through ServiceLoader.

So module boundaries here cannot be drawn by reading imports, and
single-module -pl builds cannot check them: they resolve released
artifacts, some a year old, rather than this reactor's. Each restored
dependency now carries an NB saying why it is there. The old POM already
warned about exactly this, in a comment I pruned past before reading.

Also: scijava-imagej-compat inherited an annotation-processor
configuration for SciJava Common's processor, in a component whose whole
point is that it has nothing to do with SciJava Common. Removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gJpzGd3ksbME4CN6Tu2ni
The menu row claimed the menu tree was still planned; it has been built
for some time. And the UIDetails row claimed all of the presentation
metadata survives, which is true of the metadata and not of the
behavior: `selectable` and `selectionGroup` reach the annotation index
and stop there, no binding rendering a checkbox or a radio item yet.

`isSelected` and `isEnabled` are not migrated at all, and not by
oversight: in SciJava Common they are *setters on the description*, the
same mutability that made ModuleInfo per-instance and defeated indexing.
They are application state rather than metadata, and the row now says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NShAz21gbEHnGzhVnC47cb
One component where SciJava Common had three packages (console, main,
run):

  scijava "Gaussian Blur" --sigma 2.5 --edges Wrap
  scijava blur.groovy --sigma=2.5
  scijava --list filters
  scijava --help "Gaussian Blur"

A command is named the way a person names it -- menu path, label, or the
tail of a class name -- matched forgivingly about case, spacing and a
trailing ellipsis. An exact label beats a partial one, so "Brightness"
still reaches Brightness... although Brightness/Contrast... exists; where
two answer equally it lists them instead of guessing.

`--option value` pairs are the inputs, arriving as strings that
scijava-convert3 turns into whatever the parameters declared -- the same
path a script header and a text field take, which is why the command
line needed no conversion code of its own. --help and --list read the
same metadata the menus and dialogs read; nothing describes a command
twice.

There is no canRunHeadless(). SciJava Common needed it because
harvesting was tangled with the UI; here the harvester is one
preprocessor among several and this runner installs none. A command that
cannot get an input it needs is declined, with a reason, through the
mechanism a dismissed dialog already uses.

Not ported: ConsoleService and ConsoleArgument, whose pluggable-argument
mechanism earns its place when a component actually needs to add an
argument -- until then a forty-line parser does. MainService has no
equivalent either: main is a method.

Also fixes a hang in the JavaFX binding, found when this machine had no
DISPLAY today. FxThread.start() treated IllegalStateException as
"somebody else started the toolkit", but JavaFX throws the same
exception for "startup called twice" -- so after a failed first start,
the second call declared success and every runLater afterwards waited on
a latch nothing would count down. The build did not fail; it hung, which
is worse. A failure is now remembered and rethrown, and the startup wait
is bounded. The wait for *work* deliberately keeps no timeout: a modal
dialog lives inside it for as long as the user takes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NShAz21gbEHnGzhVnC47cb
Settings live at ~/.config/<app>/settings.toml on every platform,
honoring XDG_CONFIG_HOME, overridable by a system property. Not what
each platform would prefer, and deliberately: what was wrong with
java.util.prefs was that the store was *opaque*, and the cure for that
is one answer to "where is it?" rather than three correct ones. Appose
made the same choice.

`Settings` is an object, not a singleton and not a service. Settings
belong to an application, and two applications sharing a JVM want two of
them. That is the line this codebase had been drawing without saying it:
static facades for stateless capabilities -- Converters, Locations,
Scripts, since converting a string to an int does not depend on which
application you are -- and scoped objects for anything with identity or
lifetime. A static settings store would have quietly spent the
multiple-contexts promise, and then the honest question would be why
there is a container at all.

A table per command, keyed by its identifier rather than by its
component's coordinates: a class moving between artifacts would
otherwise lose what the user chose, which is a refactor nobody should be
punished for. Values map onto TOML's own types by a small hardcoded
table with string as the fallback, so a List<File> is an array of
strings rather than one mangled string, and reading converts
element-wise through convert3. Widgets need no serialization API for
this: serialization is a property of the type, and a widget is a view --
which is also why it needs no answer per toolkit.

Comments survive a save. The file is one people edit, and a store that
ate the "# 0.7 because the detector is noisy" beside a value is one they
would stop editing.

The TOML is parsed here: tables, all four string forms, integers in four
bases, floats, booleans, all four date-and-time types, arrays -- with
inline tables and arrays of tables refused by line number rather than
misunderstood. Neither candidate library is modular, and an automatic
module in a foundation component is the trade that dropped MigLayout;
the API is small enough that tomlj can take its place later.

Persistence is LoadInputs and SaveInputs, both preprocessors, so a
caller can run a command without persistence interfering. Saving before
the run rather than after is Curtis's call and a good one: saving
afterwards loses the values whenever the run threw, which is exactly
when the user is about to try again. There is a test that fails under
the other design.

Two things fell out below it:

- Execution.isSupplied(key), because "did the caller pass this?" is not
  "is this parameter empty?" -- a double is never empty, so loading by
  null-check would never fill a primitive, and overwriting regardless
  would make a remembered value beat an explicit one. SciJava Common
  used a mutable `resolved` flag per parameter; the run knows what it
  was given, so it is asked instead.
- A convert3 bug: CastConverter saw a List bound for List<File>, called
  it already the right type, and returned a List<String> untouched --
  the right container full of the wrong things. It now declines a
  parameterized collection and lets the element-wise converter have it.
  The earlier tests missed this because they all converted arrays to
  lists, where the raw types differ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NShAz21gbEHnGzhVnC47cb
Submodules of this repository will always be prefixed with `scijava-`;
including it in the folder names is redundant.
We only need to hold the lock for the duration of the disposed flag
check; holding it for the entirety of the disposal operation could
deadlock in some circumstances, such as when context.dispose() is
called from within a WindowEvent listener's windowClosing method.

As per scijava/scijava-common@5dba100.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Before now, it resided in the README of the SciJava incubator project.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ServiceBasedAdder.constructed is static, so another test constructing
the adder first left the flag set before this test even began.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ctrueden
ctrueden force-pushed the scijava-common-migration branch from 21fb1b2 to e197fad Compare October 28, 2026 18:51
@ctrueden
ctrueden requested review from gselzer and a lite review from Copilot October 28, 2026 19:09

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Active issues remain, including a critical invalid ServiceLoader descriptor, broken FLIM service metadata, a stale generator template, and multiple misleading documentation and spelling errors.

1 open issue
What changed in this PR

This pull request phases out SciJava Common by introducing layered SciJava3 modules, migrating dependent APIs and components, and adding JPMS metadata, service descriptors, tests, documentation, and dependency-version updates.

Changes:

  • Adds modular SPI, discovery, context, execution, UI, scripting, harvesting, settings, I/O, compatibility, and Ops components.
  • Migrates GenericTyped, Versioned, discovery, service-loading, and type-extractor usage to the new APIs.
  • Adds module descriptors, generated templates and resources, test fixtures, tutorials, licenses, and updated documentation.
File Review
ui3/​src/​main/​java/​module-info.java Reviewed; no findings.
ui3-test/​src/​main/​java/​module-info.java Reviewed; no findings.
types/​src/​test/​java/​org/​scijava/​types/​NilTest.java Reviewed; no findings.
types/​src/​test/​java/​org/​scijava/​types/​extract/​DefaultTypeReifierTest.java Reviewed; no findings.
types/​src/​main/​resources/​META-INF/​services/​org.scijava.types.extract.TypeExtractor Reviewed; no findings.
types/​src/​main/​java/​org/​scijava/​types/​Proxyable.java Reviewed; no findings.
types/​src/​main/​java/​org/​scijava/​types/​Nil.java Reviewed; no findings.
types/​src/​main/​java/​org/​scijava/​types/​infer/​TypeInferenceException.java Reviewed; no findings.
types/​src/​main/​java/​org/​scijava/​types/​extract/​TypeReifier.java Reviewed; no findings.
types/​src/​main/​java/​org/​scijava/​types/​extract/​DefaultTypeReifier.java Reviewed; no findings.
types/​src/​main/​java/​module-info.java Reviewed; no findings.
types/​README.md Nit: Documents nonexistent TypeService.reify(); use TypeReifier.reify().
types/​pom.xml Reviewed; no findings.
types/​LICENSE.txt Reviewed; no findings.
testutil/​README.md Nit: The documented artifact ID does not match the POM’s scijava-testutil; update the dependency coordinates.
testutil/​LICENSE.txt Reviewed; no findings.
taglets/​README.md Reviewed; no findings.
taglets/​LICENSE.txt Reviewed; no findings.
swing/​src/​test/​resources/​scripts/​Say_Hello.groovy Reviewed; no findings.
swing/​src/​test/​resources/​scripts/​Process/​Filters/​Blur_It.groovy Reviewed; no findings.
swing/​src/​main/​java/​module-info.java Reviewed; no findings.
struct/​src/​test/​java/​org/​scijava/​struct/​Parameters.java Reviewed; no findings.
struct/​src/​test/​java/​org/​scijava/​struct/​Attr.java Reviewed; no findings.
struct/​src/​main/​java/​org/​scijava/​struct/​ValueAccessible.java Reviewed; no findings.
struct/​src/​main/​java/​module-info.java Reviewed; no findings.
struct/​README.md Nit: Correct particuarly to particularly.
struct/​LICENSE.txt Reviewed; no findings.
spi/​src/​main/​java/​org/​scijava/​spi/​Versioned.java Reviewed; no findings.
spi/​src/​main/​java/​org/​scijava/​spi/​GenericTyped.java Reviewed; no findings.
spi/​src/​main/​java/​org/​scijava/​spi/​Disposable.java Reviewed; no findings.
spi/​src/​main/​java/​module-info.java Reviewed; no findings.
settings/​src/​main/​java/​module-info.java Reviewed; no findings.
script3/​src/​test/​resources/​scripts/​Say_Hello.groovy Reviewed; no findings.
script3/​src/​test/​resources/​scripts/​Process/​Filters/​Blur_It.groovy Reviewed; no findings.
script3/​src/​main/​java/​module-info.java Reviewed; no findings.
README.md Reviewed; no findings.
progress/​src/​main/​java/​module-info.java Reviewed; no findings.
progress/​LICENSE.txt Reviewed; no findings.
progress/​.gitignore Reviewed; no findings.
priority/​src/​main/​java/​module-info.java Reviewed; no findings.
priority/​README.md Reviewed; no findings.
priority/​LICENSE.txt Reviewed; no findings.
priority/​.gitignore Reviewed; no findings.
ops-tutorial/​scripts/​services-test.py Reviewed; no findings.
ops-tutorial/​scripts/​ops-setup.py Nit: Correct intialized to initialized.
ops-tutorial/​pom.xml Reviewed; no findings.
ops-tutorial/​LICENSE.txt Reviewed; no findings.
ops-tutorial/​environment.yml Reviewed; no findings.
ops-spi/​src/​main/​java/​org/​scijava/​ops/​spi/​OpCollection.java Reviewed; no findings.
ops-spi/​src/​main/​java/​org/​scijava/​ops/​spi/​Op.java Reviewed; no findings.
ops-spi/​src/​main/​java/​module-info.java Reviewed; no findings.
ops-spi/​LICENSE.txt Reviewed; no findings.
ops-spi/​.gitignore Reviewed; no findings.
ops-opencv/​src/​main/​java/​module-info.java Reviewed; no findings.
ops-opencv/​README.md Reviewed; no findings.
ops-opencv/​pom.xml Reviewed; no findings.
ops-opencv/​LICENSE.txt Reviewed; no findings.
ops-indexer/​src/​main/​resources/​META-INF/​services/​javax.annotation.processing.Processor Reviewed; no findings.
ops-indexer/​src/​main/​java/​org/​scijava/​ops/​indexer/​InvalidOpImplException.java Reviewed; no findings.
ops-indexer/​src/​main/​java/​module-info.java Reviewed; no findings.
ops-indexer/​README.md Reviewed; no findings.
ops-indexer/​LICENSE.txt Reviewed; no findings.
ops-indexer/​.gitignore Reviewed; no findings.
ops-image/​templates/​test/​java/​org/​scijava/​ops/​image/​convert/​RAIConvertersTest.vm Reviewed; no findings.
ops-image/​templates/​test/​java/​org/​scijava/​ops/​image/​convert/​RAIConvertersTest.list Reviewed; no findings.
ops-image/​templates/​main/​java/​org/​scijava/​ops/​image/​adapt/​LiftFunctionsToRAI.list Reviewed; no findings.
ops-image/​templates/​main/​java/​org/​scijava/​ops/​image/​adapt/​LiftComputersToRAI.list Reviewed; no findings.
ops-image/​templates/​main/​java/​org/​scijava/​ops/​image/​adapt/​Globals.list Reviewed; no findings.
ops-image/​templates/​main/​java/​org/​scijava/​ops/​image/​adapt/​complexLift/​FunctionsToComputersAndLift.list Reviewed; no findings.
ops-image/​templates/​main/​java/​org/​scijava/​ops/​image/​adapt/​complexLift/​ComputersToFunctionsAndLift.list Reviewed; no findings.
ops-image/​src/​test/​resources/​org/​scijava/​ops/​image/​morphology/​inverted_img_with_filled_holes.png Reviewed; no findings.
ops-image/​src/​test/​resources/​org/​scijava/​ops/​image/​morphology/​img_without_holes.png Reviewed; no findings.
ops-image/​src/​test/​resources/​org/​scijava/​ops/​image/​morphology/​img_with_holes.png Reviewed; no findings.
ops-image/​src/​test/​resources/​org/​scijava/​ops/​image/​features/​hog/​HoG2DInput.png Reviewed; no findings.
ops-image/​src/​test/​resources/​org/​scijava/​ops/​image/​features/​haralick_test_img.tif Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​transform/​subsampleView/​SubsampleViewTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​transform/​rasterView/​RasterViewTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​transform/​permuteView/​PermuteViewTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​transform/​intervalView/​IntervalViewTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​transform/​interpolateView/​InterpolateViewTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​transform/​concatenateView/​ConcatenateViewTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​threshold/​AbstractThresholdTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​image/​watershed/​WatershedSeededTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​image/​distancetransform/​DistanceTransform3DTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​image/​distancetransform/​DistanceTransform2DTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​image/​distancetransform/​DefaultDistanceTransformTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​create/​CreateLabelingTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​create/​CreateImgTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​copy/​CopyRAITest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​copy/​CopyImgTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​copy/​CopyImgsTest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​copy/​CopyIITest.java Reviewed; no findings.
ops-image/​src/​test/​java/​org/​scijava/​ops/​image/​AbstractFeatureTest.java Reviewed; no findings.
ops-image/​src/​main/​resources/​META-INF/​services/​org.scijava.types.extract.TypeExtractor Reviewed; no findings.
ops-image/​src/​main/​java/​org/​scijava/​ops/​image/​thread/​chunker/​CursorBasedChunk.java Reviewed; no findings.
ops-image/​src/​main/​java/​org/​scijava/​ops/​image/​geom/​geom3d/​Facet.java Reviewed; no findings.
ops-image/​src/​main/​java/​org/​scijava/​ops/​image/​coloc/​IntComparator.java Reviewed; no findings.
ops-image/​README.md Nit: The dependency is marked test, so the documented setup does not place Ops Image on the runtime classpath; use the appropriate non-test scope.
ops-image/​pom.xml Reviewed; no findings.
ops-image/​LICENSE.txt Reviewed; no findings.
ops-flim/​src/​main/​resources/​META-INF/​services/​org.scijava.types.TypeExtractor Moderate: The descriptor uses the removed service filename and an incorrect provider package. Rename it to META-INF/services/org.scijava.types.extract.TypeExtractor and list the provider under org.scijava.ops.flim.types; otherwise classpath discovery cannot find the extractor.
ops-flim/​pom.xml Reviewed; no findings.
ops-flim/​notebooks/​README.md Reviewed; no findings.
ops-flim/​LICENSE.txt Reviewed; no findings.
ops-flim/​environment.yml Reviewed; no findings.
ops-ext-parser/​src/​test/​resources/​test-ops.yaml Reviewed; no findings.
ops-ext-parser/​src/​main/​java/​org/​scijava/​ops/​parser/​InvalidOpException.java Reviewed; no findings.
ops-ext-parser/​src/​main/​java/​module-info.java Reviewed; no findings.
ops-ext-parser/​pom.xml Reviewed; no findings.
ops-ext-parser/​LICENSE.txt Reviewed; no findings.
ops-ext-parser/​.gitignore Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​OpBuilderTestOps.list Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​matcher/​OpWrappersTest.list Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​lift/​FunctionToIterablesTest.vm Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​lift/​FunctionToIterablesTest.list Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​lift/​ComputerToIterablesTest.list Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​functional/​InplaceToFunctionAdaptTest.list Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​functional/​FunctionToComputerAdaptTestOps.vm Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​functional/​FunctionToComputerAdaptTestOps.list Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​functional/​FunctionToComputerAdaptTest.vm Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​functional/​FunctionToComputerAdaptTest.list Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​functional/​ComputerToFunctionAdaptTestOps.vm Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​functional/​ComputerToFunctionAdaptTestOps.list Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​functional/​ComputerToFunctionAdaptTest.vm Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​functional/​ComputerToFunctionAdaptTest.list Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​complexLift/​FunctionToComputerIterablesTest.list Reviewed; no findings.
ops-engine/​templates/​test/​java/​org/​scijava/​ops/​engine/​adapt/​complexLift/​ComputerToFunctionIterablesTest.list Reviewed; no findings.
ops-engine/​templates/​main/​java/​org/​scijava/​ops/​engine/​util/​FunctionUtils.list Reviewed; no findings.
ops-engine/​templates/​main/​java/​org/​scijava/​ops/​engine/​adapt/​lift/​FunctionToArrays.list Reviewed; no findings.
ops-engine/​templates/​main/​java/​org/​scijava/​ops/​engine/​adapt/​lift/​ComputerToArrays.vm Reviewed; no findings.
ops-engine/​templates/​main/​java/​org/​scijava/​ops/​engine/​adapt/​lift/​ComputerToArrays.list Reviewed; no findings.
ops-engine/​templates/​main/​java/​org/​scijava/​ops/​engine/​adapt/​functional/​FunctionsToComputers.list Reviewed; no findings.
ops-engine/​templates/​main/​java/​org/​scijava/​ops/​engine/​adapt/​functional/​ComputersToFunctionsViaSource.list Reviewed; no findings.
ops-engine/​templates/​main/​java/​org/​scijava/​ops/​engine/​adapt/​functional/​ComputersToFunctionsViaFunction.list Reviewed; no findings.
ops-engine/​templates/​main/​java/​module-info.list Reviewed; no findings.
ops-engine/​src/​test/​java/​org/​scijava/​ops/​engine/​TestUtils.java Reviewed; no findings.
ops-engine/​src/​test/​java/​org/​scijava/​ops/​engine/​matcher/​OpWrappersTest.java Moderate: The generated test uses org.scijava.spi.GenericTyped, but its source template still imports removed org.scijava.common3.GenericTyped; update the template to prevent regeneration from restoring a broken import.
ops-engine/​src/​test/​java/​org/​scijava/​ops/​engine/​impl/​ServiceLoaderDiscoveryIntegrationTest.java Reviewed; no findings.
ops-engine/​src/​test/​java/​org/​scijava/​ops/​engine/​impl/​OpCachingTest.java Reviewed; no findings.
ops-engine/​src/​test/​java/​org/​scijava/​ops/​engine/​impl/​LambdaTypeBakerTest.java Reviewed; no findings.
ops-engine/​src/​test/​java/​org/​scijava/​ops/​engine/​AbstractTestEnvironment.java Reviewed; no findings.
ops-engine/​src/​main/​resources/​META-INF/​services/​org.scijava.ops.spi.OpCollection Reviewed; no findings.
ops-engine/​src/​main/​resources/​META-INF/​services/​org.scijava.ops.engine.yaml.YAMLOpInfoCreator Reviewed; no findings.
ops-engine/​src/​main/​resources/​META-INF/​services/​org.scijava.ops.engine.OpInfoGenerator Reviewed; no findings.
ops-engine/​src/​main/​resources/​META-INF/​services/​org.scijava.ops.engine.OpDescriptionGenerator Reviewed; no findings.
ops-engine/​src/​main/​resources/​META-INF/​services/​org.scijava.ops.engine.matcher.reduce.InfoReducer Reviewed; no findings.
ops-engine/​src/​main/​resources/​META-INF/​services/​org.scijava.ops.engine.matcher.MatchingRoutine Reviewed; no findings.
ops-engine/​src/​main/​resources/​META-INF/​services/​org.scijava.ops.engine.InfoTreeGenerator Reviewed; no findings.
ops-engine/​src/​main/​resources/​META-INF/​services/​org.scijava.ops.api.OpHistory Reviewed; no findings.
ops-engine/​src/​main/​resources/​META-INF/​services/​org.scijava.ops.api.OpEnvironment Reviewed; no findings.
ops-engine/​src/​main/​resources/​META-INF/​services/​org.scijava.discovery.Discoverer Reviewed; no findings.
ops-engine/​src/​main/​java/​org/​scijava/​ops/​engine/​yaml/​impl/​YAMLOpInfoDiscoverer.java Reviewed; no findings.
ops-engine/​src/​main/​java/​org/​scijava/​ops/​engine/​util/​package-info.java Reviewed; no findings.
ops-engine/​src/​main/​java/​org/​scijava/​ops/​engine/​util/​internal/​package-info.java Nit: Correct sould to should.
ops-engine/​src/​main/​java/​org/​scijava/​ops/​engine/​struct/​ParameterData.java Reviewed; no findings.
ops-engine/​src/​main/​java/​org/​scijava/​ops/​engine/​matcher/​reduce/​InfoReducer.java Reviewed; no findings.
ops-engine/​src/​main/​java/​org/​scijava/​ops/​engine/​impl/​LambdaTypeBaker.java Reviewed; no findings.
ops-engine/​src/​main/​java/​module-info.java Reviewed; no findings.
ops-engine/​pom.xml Reviewed; no findings.
ops-engine/​LICENSE.txt Reviewed; no findings.
ops-engine/​.gitignore Reviewed; no findings.
ops-compat/​src/​test/​java/​org/​scijava/​ops/​compat/​OpEnvironmentServiceTest.java Reviewed; no findings.
ops-compat/​src/​test/​java/​org/​scijava/​ops/​compat/​OpEnvironmentPreprocessorTest.java Reviewed; no findings.
ops-compat/​src/​main/​resources/​META-INF/​services/​org.scijava.types.extract.TypeExtractor Critical: The descriptor references org.scijava.legacy.types.DatasetTypeExtractor, which is not present in this module. Remove the descriptor; the extractor belongs to scijava-imagej-compat, otherwise classpath ServiceLoader discovery fails.
ops-compat/​src/​main/​java/​org/​scijava/​ops/​compat/​OpEnvironmentService.java Reviewed; no findings.
ops-compat/​src/​main/​java/​org/​scijava/​ops/​compat/​OpEnvironmentPreprocessorPlugin.java Reviewed; no findings.
ops-compat/​src/​main/​java/​org/​scijava/​ops/​compat/​DefaultOpEnvironmentService.java Reviewed; no findings.
ops-compat/​src/​main/​java/​module-info.java Reviewed; no findings.
ops-compat/​README.md Nit: Replace the retired scijava-legacy coordinates and wording with scijava-ops-compat.
ops-compat/​LICENSE.txt Reviewed; no findings.
ops-benchmarks/​README.md Nit: The command hard-codes scijava-ops-benchmarks-1.0.0.jar, but the POM currently produces scijava-ops-benchmarks-0-SNAPSHOT.jar; use the project version or a derived filename.
ops-benchmarks/​pom.xml Reviewed; no findings.
ops-benchmarks/​LICENSE.txt Reviewed; no findings.
ops-benchmarks/​.gitignore Reviewed; no findings.
ops-api/​src/​main/​java/​org/​scijava/​ops/​api/​RichOp.java Reviewed; no findings.
ops-api/​src/​main/​java/​org/​scijava/​ops/​api/​OpInstance.java Reviewed; no findings.
ops-api/​src/​main/​java/​org/​scijava/​ops/​api/​OpEnvironment.java Reviewed; no findings.
ops-api/​src/​main/​java/​module-info.java Reviewed; no findings.
ops-api/​pom.xml Reviewed; no findings.
ops-api/​LICENSE.txt Reviewed; no findings.
ops-api/​.gitignore Reviewed; no findings.
meta/​src/​main/​java/​org/​scijava/​meta/​POM.java Reviewed; no findings.
meta/​src/​main/​java/​org/​scijava/​meta/​Manifest.java Reviewed; no findings.
meta/​src/​main/​java/​module-info.java Reviewed; no findings.
meta/​README.md Reviewed; no findings.
meta/​pom.xml Reviewed; no findings.
meta/​LICENSE.txt Reviewed; no findings.
meta/​.gitignore Reviewed; no findings.
javafx/​src/​test/​resources/​scripts/​Say_Hello.groovy Reviewed; no findings.
javafx/​src/​test/​resources/​scripts/​Process/​Filters/​Blur_It.groovy Reviewed; no findings.
javafx/​src/​main/​java/​org/​scijava/​javafx/​FxWidgetFactory.java Reviewed; no findings.
io3/​src/​main/​resources/​META-INF/​services/​org.scijava.io3.location.LocationResolver Reviewed; no findings.
io3/​src/​main/​java/​org/​scijava/​io3/​location/​DummyLocation.java Reviewed; no findings.
index/​src/​test/​java/​org/​scijava/​index/​Shape.java Reviewed; no findings.
index/​src/​test/​java/​org/​scijava/​index/​Red.java Reviewed; no findings.
index/​src/​test/​java/​org/​scijava/​index/​Color.java Reviewed; no findings.
index/​src/​test/​java/​org/​scijava/​index/​Circle.java Reviewed; no findings.
index/​src/​main/​resources/​META-INF/​services/​javax.annotation.processing.Processor Reviewed; no findings.
index/​src/​main/​java/​module-info.java Reviewed; no findings.
imagej-compat/​src/​test/​java/​org/​scijava/​imagej/​compat/​DatasetTypeExtractorTest.java Nit: The explanation for the empty extractor set is false because the module descriptor provides TypeExtractor with DatasetTypeExtractor; document the class-path limitation if that is the intended test behavior.
imagej-compat/​src/​main/​java/​org/​scijava/​imagej/​compat/​DatasetTypeExtractor.java Reviewed; no findings.
harvest/​src/​main/​java/​org/​scijava/​harvest/​Groups.java Reviewed; no findings.
harvest/​src/​main/​java/​module-info.java Reviewed; no findings.
function/​templates/​main/​java/​org/​scijava/​function/​Consumers.list Reviewed; no findings.
function/​templates/​main/​java/​org/​scijava/​function/​Computers.list Reviewed; no findings.
function/​src/​main/​java/​org/​scijava/​function/​Mutable.java Reviewed; no findings.
function/​src/​main/​java/​org/​scijava/​function/​Container.java Reviewed; no findings.
function/​src/​main/​java/​module-info.java Reviewed; no findings.
function/​LICENSE.txt Reviewed; no findings.
execute/​src/​main/​java/​org/​scijava/​execute/​BehaviorException.java Reviewed; no findings.
execute/​src/​main/​java/​module-info.java Reviewed; no findings.
events/​src/​main/​java/​module-info.java Reviewed; no findings.
docs/​conf.py Reviewed; no findings.
discovery/​src/​main/​java/​org/​scijava/​discovery/​ManualDiscoverer.java Reviewed; no findings.
discovery/​src/​main/​java/​module-info.java Reviewed; no findings.
discovery/​README.md Nit: The README refers to nonexistent implsOfType and elementsTaggedWith methods; document the actual discover(Class<T>) and instances(Class<T>) entry points.
discovery/​pom.xml Reviewed; no findings.
discovery/​LICENSE.txt Reviewed; no findings.
discovery/​.gitignore Reviewed; no findings.
discovery-test/​src/​test/​java/​org/​scijava/​discovery/​test/​ServiceLoaderDiscovererTest.java Reviewed; no findings.
discovery-test/​src/​main/​resources/​META-INF/​services/​org.scijava.ops.spi.OpCollection Reviewed; no findings.
discovery-test/​src/​main/​resources/​META-INF/​services/​org.scijava.ops.spi.Op Reviewed; no findings.
discovery-test/​src/​main/​java/​org/​scijava/​discovery/​test/​ServiceBasedAdder.java Reviewed; no findings.
discovery-test/​src/​main/​java/​module-info.java Reviewed; no findings.
discovery-test/​README.md Nit: The dependency example uses nonexistent scijava-ops-serviceLoader; use the actual scijava-discovery-test artifact ID or remove the section.
discovery-test/​pom.xml Reviewed; no findings.
discovery-test/​LICENSE.txt Reviewed; no findings.
discovery-test/​.gitignore Reviewed; no findings.
convert3/​src/​main/​resources/​META-INF/​services/​org.scijava.convert3.Converter Reviewed; no findings.
context/​src/​main/​java/​org/​scijava/​context/​ServiceException.java Reviewed; no findings.
context/​src/​main/​java/​module-info.java Reviewed; no findings.
context/​.factorypath Reviewed; no findings.
context-test/​src/​main/​java/​org/​scijava/​context/​test/​ShoutRecorded.java Reviewed; no findings.
context-test/​src/​main/​java/​org/​scijava/​context/​test/​Shouter.java Reviewed; no findings.
context-test/​src/​main/​java/​org/​scijava/​context/​test/​RecordingService.java Reviewed; no findings.
context-test/​src/​main/​java/​org/​scijava/​context/​test/​GreeterService.java Reviewed; no findings.
concurrent/​src/​test/​java/​org/​scijava/​concurrent/​package-info.java Reviewed; no findings.
concurrent/​src/​main/​java/​module-info.java Reviewed; no findings.
concurrent/​README.md Reviewed; no findings.
concurrent/​LICENSE.txt Reviewed; no findings.
common3/​README.md Reviewed; no findings.
common3/​LICENSE.txt Reviewed; no findings.
common3/​.gitignore Reviewed; no findings.
command3/​src/​test/​java/​org/​scijava/​command3/​HiddenCommand.java Reviewed; no findings.
collections/​src/​main/​java/​module-info.java Reviewed; no findings.
collections/​README.md Nit: Correct implementions to implementations and Not at full-featured to Not as full-featured.
collections/​LICENSE.txt Reviewed; no findings.
collections/​.gitignore Reviewed; no findings.
awt/​src/​test/​resources/​scripts/​Say_Hello.groovy Reviewed; no findings.
awt/​src/​test/​resources/​scripts/​Process/​Filters/​Blur_It.groovy Reviewed; no findings.
awt/​src/​main/​java/​org/​scijava/​awt/​AwtWidgetFactory.java Reviewed; no findings.
.gitignore Reviewed; no findings.

Review effort: Lite

@@ -47,7 +47,7 @@
import org.scijava.ops.api.OpBuilder;
import org.scijava.ops.engine.AbstractTestEnvironment;
import org.scijava.ops.engine.OpBuilderTestOps;
import org.scijava.common3.GenericTyped;
import org.scijava.spi.GenericTyped;
Fix the OpWrappersTest template's stale GenericTyped import, so that
regenerating the test does not break it.

Move the DatasetTypeExtractor service descriptor from ops-compat, which
no longer contains that class, to imagej-compat, which does. And give
the ops-flim descriptor the TypeExtractor's actual qualified name.

Clarify why the DatasetTypeExtractorTest's reifier finds no extractors:
its module does not declare that it uses TypeExtractor.

Correct stale and wrong information in several READMEs, and fix typos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

3 participants