Adopt @fission-ai/openspec 1.9.0, held back in #1926, and reconcile the
change backlog that adopting it exposed.
1.9.0 adds a stricter MODIFIED-requirement rule: a MODIFIED block replaces
the whole requirement, so it must repeat every scenario the current spec
still has. It rejected add-button-allow-focus-when-disabled, whose block
renamed "Disabled state delegation" to "... (default)". That rename kept all
three THEN/AND bullets verbatim, but the validator compares scenario names
literally and cannot detect a rename, so the original name is restored.
Archive 17 changes. Each was verified as actually shipped rather than trusted
from its task checkboxes: every target symbol was confirmed present in the
built public type surface (Tree, ActivityIndicator, useSlidingIndicator,
Breadcrumbs, allowFocusWhenDisabled, ChatMessage, ChatMessageList, DropZone,
FileTrigger, Item, Markdown, Region, useResponsiveSplitterSizes, Skeleton,
Slider, Splitter).
Four were marked in-progress but were complete. Their remaining items sit
under headings their own authors marked out of scope: Figma Code Connect
deferred until a design source exists, Chromatic baseline acceptance done
out-of-band in the Chromatic UI, and a splitter fix explicitly "tracked
separately". The tree change's validation checkboxes were simply stale --
lint, typecheck, build and tests all pass, and its changeset had already
shipped in the published CHANGELOG.
No requirement or scenario was dropped anywhere: every archive reported zero
removals, and no two changes touched the same spec capability, so there were
no ordering hazards.
Also add a nimbus-splitter requirement, "Layout is applied before paint",
recording a guarantee that was implemented and regression-tested but never
specified: the initial layout is seeded synchronously during render, and a
controlled `size` change is reconciled in a layout effect, so no frame is
ever painted at the 50/50 fallback. It is its own requirement rather than a
clause duplicated into the controlled and uncontrolled requirements, both of
which it spans.
Remove two empty, untracked change directories (add-menubar-component,
add-sidebar-component). Git cannot track empty directories, so they failed
`openspec validate` locally while being invisible in git status.
The openspec gate now passes for the first time: 120 passed, 0 failed
(main was 120 passed, 2 failed).
Summary
Adopts
@fission-ai/openspec1.9.0 — the second of the two packages held back in #1926 — and reconciles the change backlog that adopting it exposed.The
openspec validate --all --strictgate now passes for the first time: 120 passed / 0 failed (main is 120 passed / 2 failed).Why 1.9.0 was blocked
1.9.0 adds a stricter rule: a
## MODIFIED Requirementsblock replaces the whole requirement, so it must repeat every scenario the current spec still has. It rejectedadd-button-allow-focus-when-disabled:This was a rename, not a dropped scenario. The change renamed
Disabled state delegation→Disabled state delegation (default)and narrowed itsWHEN, keeping all threeTHEN/ANDbullets verbatim, then added a sibling for the new focusable-disabled behavior. The validator compares scenario names literally and cannot detect a rename.The fix restores the original scenario name (one line), so the default-path scenario keeps its identity and the new behavior lands as a sibling.
Archiving 17 completed changes
Every change was verified as actually shipped, not trusted from its task checkboxes — each target symbol was confirmed present in the built public type surface (1131
.d.tsfiles):Tree·ActivityIndicator·useSlidingIndicator·Breadcrumbs·allowFocusWhenDisabled·ChatMessage·ChatMessageList·DropZone·FileTrigger·Item·Markdown·Region·useResponsiveSplitterSizes·Skeleton·Slider·SplitterTwelve were already marked complete. Four were marked
in-progressbut were finished — their open items sit under headings their own authors marked out of scope:add-responsive-splitter-sizes-hook40/41## 11. Tracked separately (not part of this change)— "land as an independent fix". Already implemented (see below).add-item-component21/22## 9. Figma Code Connect — DEFERRED, blocked on a Figma source existingadd-breadcrumbs-component33/342026-06-08-add-tree-component28/34Post-development (follow-up, out of scope for this PR); §8 isValidation (follow-up changes). 8.1/8.2 are satisfied (lint, typecheck, build and tests pass;SubTree/useTree/TreeItemin built types) and 8.3 already shipped — the CHANGELOG documents both, with zero pending changesets. Stale checkboxes.Nothing was dropped. Every archive reported zero removals (
- 0), a diff scan confirms no### Requirementor#### Scenarioline was removed from any tracked spec, and no two changes touched the same spec capability, so there were no ordering hazards.Archive count: 31 → 48. Active changes: 0.
New requirement:
nimbus-splitter→ "Layout is applied before paint"While auditing the leftovers, one turned out to be a genuine spec gap rather than pending work.
Splitteralready guarantees the configured size is applied before the first paint, andsplitter.reconcile-timing.spec.tsxis a dedicated "Regression guard for the first-paint 50/50 flash." The spec never said so —Initial size from defaultSizepromised the size would be applied but not when, and the controlledsizerequirement was silent on timing.Reading the code and test showed the guarantee has two mechanisms the original task text didn't separate:
useState(initialDisplay), not adopted in a mount effect;sizechanges reconciled in a layout effect, so they land in the same commit.Both are now specified, with scenarios for the uncontrolled, controlled and collapsed-on-mount seeds plus the same-commit reconcile. Timing that is implemented and regression-tested but unspecified is free to be refactored away; this protects it.
It is an ADDED requirement rather than MODIFIED because the guarantee spans both the controlled and uncontrolled paths — MODIFIED would have meant re-transcribing 10 scenarios across two requirements, risking exactly the scenario loss the 1.9.0 rule exists to prevent, and duplicating the clause in two places.
No runtime code changed, so no changeset is required.
Removing two empty change directories
add-menubar-componentandadd-sidebar-componentcontained zero files. Git cannot track empty directories, so they failedopenspec validatein local checkouts while being invisible togit status— which is why this gate was red on main and green in CI.On Figma Code Connect
Two archived changes carry deferred Figma Code Connect items. Nothing was changed for them, because there is nothing to change:
openspec/specs/tree/spec.mdcontains no Figma requirement..figma.tsxappears only in thenimbus-code-connectskill (a generator), not inCLAUDE.md,docs/,.claude/commands/, or the component templates.If it should ever be specified without being forced upfront,
nimbus-chat-messagehas the pattern: a conditional scenario — "WHEN the component is mapped in Figma → THEN SHALL provide a Code Connect mapping."Testing
Every gate from
.github/workflows/build-and-test.yml, run on the final rebased tree:pnpm build(full, incl. docs)pnpm openspec validate --all --strictpnpm lintpnpm typecheck:strictpnpm check:package-shapepnpm testOne verification run of this branch failed 2 tests in
confirmation-dialog.stories.tsx(Destructive Intent,Async Confirm); an immediate re-run of the identical tree passed 3225/3225. This is a pre-existing flake onmain, not caused by this PR — nothing here touches runtime code, and it reproduces withaxe-core4.12.1, the versionmainalready ships.It also corrects the diagnosis in #1926, which attributed this failure to
axe-core4.13.0. That was wrong. The root cause is a design-token contrast shortfall that both axe versions detect:subtle:bg: colorPalette.3,color: colorPalette.11,_hover: { bg: colorPalette.4 }.colorPalette="critical"trigger, so the pointer stays over it and the a11yafterEachmeasures the hover background.critical.11#ce2c31oncritical.4#ffdbdc= 57.96Lc, against the repo's own 60Lc threshold. The base state (critical.3#feebec) measures 64.89Lc and passes — which is exactly why the test only fails when hover happens to still be active.Sweeping every solid palette for the
subtlehover pair (.11on.4) shows this is systemic rather than critical-only — 8 of 27 fail:orangepinktomatocrimsonred(=critical)rubytealjade(
*Alphapalettes are excluded — they are transparent, so APCA against a nominal hex is not meaningful for them.)This needs a design decision — nudge
colorPalette.11darker, lightencolorPalette.4, or revisit the 60Lc threshold — so it is deliberately not addressed here. Two consequences worth noting:axe-coredid not actually need holding back in #1926, and adopting 4.13.0 will likely make this flake fire more often, so the token fix should land first.Review notes
The diff is large but almost entirely mechanical: 75 file moves into
openspec/changes/archive/, 17 new capability specs underopenspec/specs/, and two one-line edits topnpm-workspace.yaml/pnpm-lock.yaml.The three parts worth actual review are small:
Layout is applied before paintrequirement,in-progresschanges were finished — the table above shows the evidence for each.