Repository navigation
Conversation
|
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 |
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
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
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
force-pushed
the
scijava-common-migration
branch
from
October 28, 2026 18:51
21fb1b2 to
e197fad
Compare
There was a problem hiding this comment.
🟡 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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, anddocs/migration.mdmaps 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
@deprecatedpointers (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
Named,Identifiable,Disposable,Versioned,GenericTyped)org.scijava.annotations. The index format is unchanged, so SJC and SJ3 can read each other's indexes.ServiceLoader. Direct byte buffers now stay off-heap after they grow.Converter.supports(Object, Type)question instead of eightcanConvertoverloads. It doesn't need a container.~/.config/<app>/settings.toml.Application model
@Plugintier served straight from the index,@Dependencyfield injection and@EventHandlerscanning.Executable/Runner/ pre- and postprocessors. A declined run comes back as an ordinary result, while cancelling goes throughFuture.cancel(true).@Menumetadata andMenuTree. Menus are still built without loading any command class.DynamicCommand's two uses.Executablelike any other, and any JSR-223 engine works. Unlike in SJC, callbacks work for scripts.UI
Compatibility
Cross-cutting decisions
Cancelable; callers useFuture.cancel(true), andProgress.checkCancellation()throwsCancellationException. scijava-concurrent now keeps a cancellation through a parallel section instead of wrapping it in aRuntimeException.<pkg>toorg.scijava.context, andprivate-fieldinjection then works while implementation packages stay unexported.common3,io3,command3, …) so the old and new versions can share a classpath.docs/versioning.mdexplains the scheme.org.scijava.commandis part of SJC, the command package here is noworg.scijava.command3.Breaking changes to existing components
Discoverer.discoverreturns lazyDiscovery<U>descriptors instead of instances, and all call sites are updated.VersionedandGenericTypedmove from scijava-common3 to scijava-spi, with no deprecated aliases left behind.scijava-prefix. Artifact IDs are unchanged.Also included
FileLocation, which SCIFIO's API requires.Context.doDispose()holds its lock for less time, ported from scijava/scijava-common@5dba100.Not done yet
@deprecatedpointers on the SJC side.