VexelRay
Framework docs

What v1 means

v1 is a promise about applications, not a count of features, and it has two conditions. One is positive and one is negative.

Positive: an application built on v1 does not need a major refactor for the foreseeable future. Later releases add functionality to the abstractions v1 already has.

Negative: there is no such thing as legacy in v1. Nothing stays in the code for the sake of backwards compatibility.

They work together. The first makes v1’s shape permanent, so whatever is in it is there for good. The second is why the shape has to be clean when it is frozen: v1 is the last release where a breaking cleanup is free. Anything kept “for now” at v1 stays forever.

So v1 is reached when the abstractions stop moving, not when the list of capabilities is long. A capability that is missing at v1 is fine if the abstraction it will attach to is already there. An abstraction that is still being drawn is not fine, however much works on top of it. status.html grades where each capability lives; this page grades whether the shape it lives in is final. The two are different questions, and only the second decides the version number.

TODO.md is organised by this page: what blocks v1, what can land after it, what is upstream.

What counts as breaking

A change is breaking if an application that compiled, passed and behaved correctly on v1.x would not on v1.x+1 without the application’s author touching it. That is wider than a removed method:

Additive means: a new method with a default, a new annotation attribute with a default, a new starter, a new verb, a new Shell accessor, a new module. It never means a new obligation on existing code.

The contract surface

Six surfaces, and v1 freezes each. The state of each is in status.html.

Surface What is promised
The annotations (-api) Every annotation, attribute and default; and the processor semantics behind them, which are the larger half: type matching, phase inference, @Default and back-off, generated names, the checks that exist
Shell and the phases Every accessor, the phase each is legal in, the order parts are built in, and what a wiring may do at each
The component model How a component declares its lane, its channels and its loss class; what may cross a lane; what a wedged component does to the rest. Lanes and Placement are the threads and mailboxes; @Component(lane), @Subscribe and @Publishes are the declaration. Ruled to freeze at v1, multithreaded by default on a shared default lane that is not the main thread, not to defer
The build shape Module coordinates, starters, the BOM, the reachability metadata an application inherits, and what new vexel-desktop writes
The launch surface Launch, FRAMEWORK_KEYS, the reserved keys, --automation and --profile, exit behaviour, what is reported when a capability is dropped
The wire protocol The automation line protocol: framing, ok and err, the verbs and their replies

A seventh surface is not the framework’s, and bounds all six. Shell hands over Gui, GuiApp, KronoGui, TitleBar, Settings, Atchung, ComputeQueue and Icon, and its Windows, Window and Dialogs hand over Gui, TitleBar and Modal in turn, so an application holds vexelray-gui’s types directly. v1 cannot be more stable than they are. Either those repos freeze what Shell exposes, or Shell stops exposing it. That is a decision, not a task, and it is in TODO.md.

When an abstraction is frozen

An abstraction is ready to freeze when all five hold. Anything short of that is experimental, however well it works.

  1. It has more than one witness. The framework’s own method, one component is not a census, applies to the framework itself. The three ported applications were deleted on purpose; what validates the abstractions now is the template, the three built witnesses (W1 with several windows, W2 component-heavy, W3 long-running) and the applications generated from it, most of them one window and its dialogs. Freezing something drawn from one shape freezes that shape’s accidents, which is what the witnesses are for.
  2. Its semantics are decided in writing. Not “works”, but “in this case it does this, and here is why” in architecture.md. The open ones (exact-type matching, a null provider) are cheap today and a breaking change afterwards.
  3. Its extension points are named. For each thing v1 leaves out, the abstraction it will attach to exists already. That is the extension test, below.
  4. Nothing in it is legacy (see below), so that the freeze does not preserve a compatibility shim.
  5. No entry under Blocks v1 in TODO.md names it.

The liveness guarantee

The point of multithreaded-by-default is that there is never another frozen GUI. Stated as a contract v1 freezes: whatever a component does, including looping forever, the window keeps responding, the user can always close the application, and the application can end itself. Three parts, each with what would break it:

What is not promised: that a wedged component’s own work continues, or that a frozen main thread is fixed. Stalls names it, and the exit path is the way out. An application that blocks its own main thread has broken the first rule from the other side, and the guarantee is honest only about the framework’s share. A wedged main thread leaves the window itself unresponsive, so there the way out is the watchdog ending the process once the stall threshold passes: guaranteed, and delayed rather than immediate.

The policy is the application’s, and its default is not part of the contract

What happens when a lane wedges is a policy the application can override. The frozen parts are the guarantee above and the seam that takes the policy; the default itself is deliberately not contractual, so it can improve without a break, provided every default upholds all three parts. Today the default is to report and then exit. The target is a fallback on the main thread that offers the user a restart of the GUI.

The seam is built for that target now, because it is the frozen part. An application supplies a policy the way it supplies a look or an input backend, by a provider handed back to Shell. The policy is told what happened (which lane, which components, for how long, and whether it was a lane, a component or the main thread) and acts by calling methods on a context object it is handed (exit, later restart, later others). It does not return a choice from an enum, so a new action is a new method and not a change for every existing policy.

The extension test

The bar is “future releases add to the abstractions we have”. So for every capability v1 leaves out, name the seam it will use. If there isn’t one, the seam is a v1 item and the capability is not.

Left out of v1 The seam it attaches to Does the seam exist?
Diagnostics, --profile A starter plus @ConditionalOnType; atchung-probe Yes. The module’s name is undecided, and it is Maven coordinates
Framework SDF vocabulary A @Provides and a placed component Only once the component model is final
elektro-Q, cross-process actors The channel declaration Partly. @Subscribe and @Publishes exist on a component; a @Subscribe off a component, which this needs, is not built
Simulation vocabulary FrameStage.APP, and a placed component Partly. APP is an empty slot by design
Reactive automation, macros, extensions The channel declaration Partly. The same seam, and the same missing half
A second automation socket Driver.open(shell, gui, controls, offset) Yes; additive
Supervision and recovery The application-supplied policy and the context it acts through Yes: LivenessPolicy, handed back like the look, acting through a context with exit and interrupt. Actions (restart) and the default are additive
A GUI fallback that offers a restart The same policy seam, plus the main thread, which is not a lane Yes, if the seam ships; covers a wedged lane and never a wedged main thread
A BOM, more starters @Default and @VexelApp naming One starter exists; a second is the test of the shape
Linux and macOS native builds The per-OS backend modules and the metadata each jar carries, and a facade in front of every native extension Only if metadata lives in the backend jars. Metadata in an app-local file would make each new OS an edit to every app

Two rows pass only in part, and on the same seam: a @Subscribe that is not on a component. That is why it sits ahead of everything else.

What must not remain

Legacy is anything kept only because something used to depend on it, or because removing it would break someone. A feature that earns its place on its own terms is not legacy, however old. The test is: would we write this, in this form, if we were starting today with the v1 abstractions? If the honest answer is “no, but removing it breaks a caller”, it is a cleanup to do before v1, when a breaking change costs nothing and the callers are the author’s own.

What that rules out, concretely:

The rule binds the framework and every sibling whose types an application holds through Shell, because the freeze covers them (see the seventh surface above). Deleting something is a breaking change, and before v1 it is allowed. After v1 it is not, so this is the moment.

Greppable markers to start from, none of them conclusive: legacy, @Deprecated, kept for, backward, for now, until, deliberately not, reserved. A hit is a question, not a finding.

How to get there

In this order, because each step decides what the next bakes in:

  1. Finish the component model. The channel declaration, the colour rule’s copier and capture halves, the message-graph checks, supervision, and the completion path for work that outlasts a frame. It reshapes how every component is written, and it is where new compile errors come from.
  2. Decide the semantics nobody has argued. Exact-type matching, null providers, which defaults are replaceable, whether a new Phase is ever additive, and how a new processor check is allowed to land.
  3. Settle the boundary with the siblings. What Shell exposes of vexelray-gui, and the upstream bus that admits one tree. Then the composition root: GuiApp and VexelEngine.
  4. Add witnesses, and spike the extension test. Two or three generated applications with deliberately different shapes: several windows, component-heavy, long-running with nothing to settle. Each is expected to find something. What they find is written up, as the ports were.
  5. The build shape. A second starter, the BOM, and aggregated reachability metadata, once the abstractions they package have stopped moving.
  6. Sweep legacy. Go through the framework and the sibling types Shell exposes, and delete what the section above rules out. Do it before the freeze pass, because the pass would otherwise freeze it, and after the component model, because the model is what makes some current seams redundant.
  7. Freeze. Walk the whole public surface. Anything that would be renamed or restructured today is fixed first. Anything not yet frozen is marked. Then cut 1.0.

Until then releases are 0.x, and 0.x says what it means: the shape may still move.

How future-proofness is measured

It can only be confirmed in retrospect: a later release either added to the abstractions or it did not. What can be measured beforehand is failure, and the method is to try to cause it:

What none of it reaches is the future nobody imagined. v1 is a bet, and no-legacy means a lost bet is a v2 that breaks cleanly, so the way to lower the stake is to ship less and shape it well.

How the surface is marked

The version is 0.1.0-SNAPSHOT, and each public type in -api, -core, -shell and -automation carries @Stability(FROZEN) or @Stability(EXPERIMENTAL), so a reader can tell without asking. It is CLASS retention and nothing reads it: the processor does not check it, because a check would be a new compile error. Every type is EXPERIMENTAL today. The freeze pass is what changes one to FROZEN, and only when the five conditions above hold. The wire protocol is graded ready in status.html, but it is a text protocol and not a type; Driver and AutomationStarter are its Java side and stay EXPERIMENTAL until the pass. A new public type is added with a mark. A public type with none has not been looked at, which is a different thing from either.

What v1 is not

Not feature-complete. Rendering vocabulary, the game-engine half, cross-process actors and the diagnostics module can all arrive after v1, each behind a seam the extension test names. It is also not a claim that the design is right, only that we will stop changing it under people.