Design Button Behavioral Stability Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Make native Button intent explicit, test its behavioral contract, and block design-system changes from merging when they break any direct monorepo consumer.Architecture: Keep the runtime-safe type="button" fallback in the shared component while making direct JSX usage mandatory through a focused ESLint rule. The Button type is lint-enforced, with a defensive runtime default. Audit the dependency graph from package metadata, migrate each direct Button use according to its form/event context, add DOM-level component and dashboard regression tests, and extend the existing design-system workflow with metadata-derived downstream checks that use each package’s canonical build surface.Tech Stack: React 19, TypeScript, ESLint 9 RuleTester, Vitest/jsdom, pnpm, Nix/exapkgs, GitHub Actions.
Add the existing repository versions of Vitest/jsdom and React DOM test helpers to the design package, plus a test:button/unit-test script that runs DOM tests without weakening the existing Node test suite.
Write Button behavioral tests that assert the rendered DOM type, submit activation by click and keyboard, non-submission for type="button", and no click/submit action while disabled or loading.
Extract only the shared team-settings dialog form markup into AddableTableDialogForm so it can be exercised without mocking the entire 1,600-line page; keep styling, request handlers, and the merged type="submit" hotfix unchanged.
Write dashboard DOM tests that fill and click the visible submit control for both “Send invite” and “Add IP” configurations and assert the supplied submit handler fires; add pending/disabled coverage.
Run the new focused tests once against the pre-contract code and confirm they encode actual interaction rather than source-string presence.
Keep ButtonProps.type?: "button" | "submit" | "reset" and retain type = "button" inside the runtime implementation for JavaScript/untyped callers; enforce explicit source intent with the focused ESLint rule.
Add @exa/design/require-button-type, tracking named imports and aliases from @exa/design/ui/button and reporting direct JSX uses with no explicit type attribute.
Document the deterministic policy: a spread or dynamic type does not satisfy the direct-JSX lint rule because intent must be visible at the use site; wrappers/re-exports must preserve native type intent at their forwarding boundaries.
Add RuleTester cases for all three valid literal types, aliases, unrelated local Button components, missing type, spread-only props, and explicit dynamic type.
Enable the rule in the shared preset and keep the docs/rule catalog drift test green.
Run node --test src/eslint/rules.test.js src/eslint/consumer-rules-doc.test.js and the focused Button test.
Modify: direct Button consumers under every package discovered from package.json metadata, including go/cortex/ui, infra/*, node/apps/*, and typescript/*
Modify: Button examples and internal consumers under node/apps/design/src as required by the new type contract
Generate an audit table from the TypeScript AST that records package, file, imported local name, wrapper/re-export status, enclosing form (where statically visible), handler, disabled/loading state, and chosen button|submit|reset intent.
Review form ancestry and handler behavior before editing: use submit only for controls intended to invoke a form’s onSubmit, reset only for actual resets, and button for triggers/actions (especially dialogs/popovers nested in forms).
Apply the reviewed mechanical edits, including aliased imports, wrappers, styled wrappers, docs examples, and tests; do not change visual props or product behavior.
Re-run an AST inventory and TypeScript checks to prove no typed Button or wrapper callsite remains ambiguous.
Record the total changed callsites and the reviewed submit/reset list for the final handoff.
Export a semantic-safety config (or rule block) containing require-button-type independently from visual migration rules, without relaxing the existing recommended preset.
Update dashboard so require-button-type applies to all application TS/JS while MIGRATED_PATHS continues to scope visual/component compliance.
Apply the same app-wide safety rule to every direct consumer with ESLint; where a consumer uses oxlint or has no ESLint, rely on the TypeScript gate and record that limitation explicitly.
Add a metadata-derived checker that discovers every direct @exa/design package and verifies its source is covered either by the ESLint safety rule or by a TypeScript compatibility check.
Test discovery against fixtures plus the live monorepo graph so newly added consumers cannot silently skip the gate.
Modify: node/apps/design/project.nix only if a reusable Nix passthru is needed
Modify: relevant canonical consumer project.nix files only when their existing output does not expose the package’s build/typecheck/test target
Implement consumer discovery by scanning tracked application package.json files for dependencies/devDependencies/peerDependencies on @exa/design; emit stable JSON with package name, directory, nearest canonical Nix project/flake, and available typecheck, build, lint, and behavioral-test scripts.
Add unit tests for workspace, file:, nested-package, missing-script, and newly discovered consumer cases.
Extend the design-system PR workflow with a discovery job and a blocking matrix. Always run design lint/typecheck/unit tests plus every consumer typecheck and relevant contract test; run production builds through each consumer’s canonical Nix/build definition where available.
Avoid a handwritten package list: the matrix must come from the discovery script, and an unclassifiable consumer must fail discovery with an actionable error rather than being skipped.
Add a scheduled full consumer matrix only if the blocking PR build set must be reduced for measured cost; preserve at least one blocking downstream check capable of detecting compile/API incompatibility.
Validate workflow syntax and dry-run/inspect the emitted matrix locally.
Task 6: Document behavioral compatibility as public API
Files:
Modify: node/apps/design/AGENTS.md
Modify: node/apps/design/public/AGENTS.md
Modify: node/apps/design/public/CHECKLIST.md
Modify: node/apps/design/DECISIONS.md
Modify: node/apps/design/docs/eslint.md
Modify: node/apps/dashboard/AGENTS.md
Add a new durable decision stating that DOM element choice, form semantics, callbacks/payloads, controlled state, focus, dismissal, keyboard interaction, and disabled/loading behavior are public API and require downstream interaction tests.
Require an explicit Button type in contributor and consumer guidance; state why the runtime default remains button and why visual parity is insufficient.
Modify: only formatter output in files already changed
Run format --branch origin/master per the monorepo code-quality contract and inspect the resulting diff for unrelated rewrites.
From node/apps/design, run lint, typecheck, unit tests, the ESLint RuleTester suite, the Button DOM tests, and production build.
From node/apps/dashboard, run format/check, lint (including app-wide semantic safety), typecheck, the shared team-settings regression tests, relevant unit tests, and production build with documented fake environment variables.
Execute the compatibility checker/matrix locally for all metadata-derived direct consumers, using canonical Nix outputs where available; capture exact command/failure/reason for any environment-blocked package.
Run git diff --check, recount changed Button callsites, and compare the final consumer inventory with the initial package/import scans.
Review git diff origin/master... for UI redesign, accidental runtime-default changes, broad lint suppressions, stale generated artifacts, or changes outside the requested failure class.