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:
- A new compile error is a breaking change. The processor’s whole job is to move failures leftward, so every new check is a way to break a build that was accepted yesterday. A check may land after v1 only if it rejects something that was already a bug. Anything else lands before v1, or behind an opt-in.
- A different resolution is a breaking change. Which provider satisfies a parameter, which phase a part
is built in, what a generated wiring is named, what a
nullprovider does: an application depends on these without ever writing them. - A different lifecycle order is a breaking change.
PhaseandFrameStageare contracts. Adding a phase between two existing ones moves every part built around it. - The generated project is application code. Once
vexel-desktophas produced a project, the template is a promise about what that project keeps compiling against. A change to the builder’s public API is also a changemainframecompiles against. - The wire protocol is an interface. A script that drove the application on 1.0 must still drive it. New verbs are additive. A verb’s replies are not.
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.
- 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.
- 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
nullprovider) are cheap today and a breaking change afterwards. - 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.
- Nothing in it is legacy (see below), so that the freeze does not preserve a compatibility shim.
- 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:
- The main thread never waits on a component. A send from the main thread to a lane must not block. A
channel that blocks a full publisher (
Backpressure.BLOCK) is exactly how a wedged lane freezes a window: TODO already records a real freeze from it, a full 65,536-slot mailbox blocking a tree’s node setters for good with nothing thrown. So code on the main thread may only send on a channel that coalesces, drops or fails. The processor checks it for declared sends (T4.7); a send nobody declared is not seen. - Closing always ends the process. A close request is the user’s explicit intent, so it is honoured within
a bound even if a lane will not drain. Shutdown once joined each lane for about a second, which for n
wedged lanes was n seconds and then nothing. The contract is a total bound after which the process halts, and it is built:
Disposer.beforeClosearms it, 10 s by default (LivenessPolicy). A close gate that waits on a component would break it, and so would any gate run on a wedged lane. - The application can end itself. The watchdog runs on a thread that belongs to no lane, so a wedged main
thread cannot stop it, and an explicit
haltreachesRuntime.haltfrom that thread. What supervision does by default is a policy the application can override and is not itself part of the contract (below).
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:
- Compatibility shims. A prior constructor,
Configform or overload retained so old callers still compile. The engine refactor kept every prior constructor deliberately, which was right for a stack that had to keep building, and is exactly what v1 does not carry. - Two ways to do one thing. A superseded seam left beside its replacement, or a redundant call the framework still makes. One name per idea.
- Reserved names nothing honours. A flag, key or accessor that parses and does nothing. It is either built by v1 or deleted, since a frozen no-op is a promise nobody can keep.
- Test seams in the public surface. An overload that exists so a test can reach it, on a type an application also sees.
- Dead API. A public member with no caller outside its own tests. Frozen, it is a permanent cost and a permanent question for the next reader.
- Prose that describes a past. A doc, Javadoc or comment that still says how something used to work, or qualifies a rule with for now, until, or deliberately not yet. A stale description is a second spec, and it will disagree with the first.
- A deprecation.
@Deprecatedand anything like it. v1 does not ship with a way out already open; it removes the thing.
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:
- 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.
- Decide the semantics nobody has argued. Exact-type matching,
nullproviders, which defaults are replaceable, whether a newPhaseis ever additive, and how a new processor check is allowed to land. - Settle the boundary with the siblings. What
Shellexposes ofvexelray-gui, and the upstream bus that admits one tree. Then the composition root:GuiAppandVexelEngine. - 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.
- The build shape. A second starter, the BOM, and aggregated reachability metadata, once the abstractions they package have stopped moving.
- Sweep legacy. Go through the framework and the sibling types
Shellexposes, 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. - 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:
- Spike each row of the extension test. Write a throwaway version of the anticipated feature (a second
window, a Linux backend,
@Subscribe, supervision) against the API as it stands, and record how many existing signatures it forces to change. The target is zero, and the count goes in the entry. A pass proves only that one known future fits; a failure is proof the shape is wrong, and it is found while a breaking change is still free. - Witnesses are out-of-sample tests. An application shaped differently from the one the API was drawn from finds what the first could not.
- Read the surface’s own history. Days since an app-facing removal, and whether behaviour changed under an unchanged signature. Retrospective, and real.
- Prefer a small surface. Unshipped API cannot break. A ratio of exposed to used is worth reading. Prefer concepts with one obvious home for a future feature.
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.