Skip to content

ADR-003 — CONTRACT_VERSION compatibility policy

Status: Proposed. The question is open — this record exists to state it precisely, not to settle it. Nothing here is in force; what is in force is the precedent described under Context.

Context

CONTRACT_VERSION is exported from src/core/contract.ts and is 2. An extension may declare contractVersion; core compares the two.

What a mismatch does today: it logs a console.warn, once per extension id, and nothing else. The extension still renders, still starts, still contributes commands and diagnostics. An absent contractVersion is silent, and nothing warns when the toolbar is enabled={false}. The field is documentation, not a gate.

That much is deliberate and is not the open question. Core has no basis to decide what a mismatch means — a 2 extension against a 1 core may be entirely compatible — and refusing to render would turn a warning into an outage in somebody's development tooling.

The open question is when the number should change. What exists is precedent, not a rule. Seven first-party extensions were built against the contract before 0.1.0, and every change any of them forced was additive or a semantic correction:

ChangeShape
togglePanel() on CompactSlotPropsNew field
--dtb-ok / --dtb-warn and their -bg pairsNew tokens
commands may be a functionWidened existing field
The overlay slotNew optional slot
diagnostics(), getDiagnostics()New optional field, new api method
hidden means absent everywhere, not unpaintedSemantic correction

None bumped, on the reasoning that bumping for an additive change spends the one signal a version number carries. That reasoning leaned partly on "nothing has been published yet", which stopped being true at 0.1.0.

One has since bumped. Contract 2ToolbarCommand gaining description, input and a typed run(input) that may resolve a value, plus ExtensionRuntimeApi.invokeCommand — is additive for an extension author (every v1 command is a valid v2 command, unedited; but see the paragraph below for the one constructor case where it is not) and would have fallen under the precedent above. It was bumped deliberately, and stated in the PR rather than decided in a commit, per the interim rule below. That makes it a data point for option B, not a settlement of the question: the number now reads as a feature level, which means every first-party extension's hand-maintained copy had to move with it — exactly the cost B is charged with. The question stays open.

Contract 2 also exposed a gap none of the options above can express. It is additive for anyone writing an extension — every v1 command object compiles unchanged — and non-additive for anyone constructing an ExtensionRuntimeApi: the new required invokeCommand member is a compile error for a hand-rolled fake api, which is what a test suite has. Core provides the real object, so no shipped extension breaks; ten fake ones inside this repo did. "Additive" was doing duty for two different claims and only one of them held. Whichever option is picked has to say which side of the contract it means.

Two further facts constrain any answer:

  • The field cannot become a gate cheaply. Making core refuse a mismatched extension would be a breaking change for every extension that declares a number, and it is the outage-instead-of-warning trade above.
  • Extensions on their own subpath cannot import the constantADR-001 forbids value imports from core — so any extension that states a version hand-maintains a copy of the number, closed only by an equality assertion in its tests. Every bump therefore has to be propagated by hand across every extension that declares one.

Decision

Undecided. The candidate policies, with what each costs:

OptionWhat it saysCost
A. Bump only on breaking changes.The number is a compatibility boundary: same number means an extension written for it works. Additive changes never bump.An extension declaring 1 may still need a newer core than the consumer has, because the feature it uses arrived additively. The number cannot express "needs at least".
B. Bump on any contract change, additive included.The number is a feature level, and an extension declaring 3 is saying it needs core >= 3.Turns every additive widening into a coordinated bump across every first-party extension's hand-maintained copy. Makes the common case noisy, so the number stops being read.
C. Drop contractVersion from the contract.Package semver is already the compatibility signal, and it is one nobody has to maintain by hand.Removing a published field is itself a breaking change, and it loses the mismatch warning, which is genuinely useful when a consumer has two copies of core in the tree.

The interim rule, which is what CONTRIBUTING.md says today: do not bump silently. If you are changing the contract in a way that is not purely additive, say so in the PR description and raise the version question there. Do not assume the additive precedent covers you.

Risk accepted

While this is open:

RiskLikelihoodImpactMitigation
An extension written against a newer core is installed against an older one, uses an additively-added field, and gets undefined behaviour with no warning — because both declare 1Medium, and rising with each additive changeA slot that silently does not render, or a command that never appearsPackage semver: the extension's peerDependencies on @nejcm/dev-toolbar is the real signal, and is not hand-maintained.
The number is bumped for an additive change and consumers read it as breakingLowUnnecessary migration workThe interim rule: raise it in the PR rather than deciding it in a commit.
The hand-maintained copies drift from core's constant and a wrong version number is printed into an outbound bug reportLowA wrong fact in somebody's ticket about a version they cannot checkThe equality assertion pattern in src/ext/diagnostics/__tests__/diagnostics.test.tsx. Copy the assertion, not just the constant.

Consequences

Until this is settled:

  • contractVersion is described everywhere as documentation rather than a check, and should not be documented as a compatibility guarantee.
  • The mismatch warning stays a warning.
  • Any extension that needs to state the version copies the equality assertion alongside the constant.

Settling it means picking A, B or C, superseding this record, and — for A or B — writing the rule into CONTRIBUTING.md next to the commit convention, because the person who needs it is writing a PR description.

Released under the MIT License.