Skip to the content.

An AI harness for keeping UI and State apart

An AI harness for keeping UI and State apart

The clearest boundary in a frontend architecture is between UI — components that render props and emit events — and State — the store, the reducers, the selectors. A presentational atom should not know your store exists. This separation is easy to state and easy to erode, and AI makes the erosion faster: ask a model to “make the button show the cart count” a hundred times and some fraction of those completions will reach straight into the store from inside the component, because that is locally the shortest path. No single diff looks wrong. The hundredth one has quietly deleted the boundary. The fix is not more careful prompting; it is a harness that fails the build the first time a UI file imports from the state layer.

A container connects UI to state; the guardrail forbids UI importing state directly UI atoms receive props and emit events. A container reads the store and passes props down. An arrow straight from a UI atom into the store is crossed out and labelled build fails. UI atomprops in, events out containerreads the store store / state props UI → store: build fails ✗
The container is the only file allowed to touch both sides. A direct UI-to-store import is the exact thing the harness is built to reject.

Make the boundary a check, not a convention

A convention documented in a README is enforced only when a human notices it in review. A harness enforces it on every diff. The simplest version is a lint rule that forbids importing the state layer from anywhere under ui/:

// eslint: no state imports from presentational UI
{
  files: ["src/ui/**/*.{js,jsx}"],
  rules: {
    "no-restricted-imports": ["error", {
      patterns: ["**/state/*", "**/store", "react-redux"],
    }],
  },
}

Now the hundredth completion that does import { useSelector } from "react-redux" inside an atom does not merge — the build is red, with a message pointing at the exact line. The AI can write across the boundary all it likes; it just cannot land the change.

Assert the state shape, not just the imports

Import rules catch the crude violation. A subtler one is the store’s shape drifting — a reducer growing a field that should have been derived, a slice storing server data as if it were truth. So the harness also asserts the structure of state against a spec:

// harness/state-shape.test.js — the store's contract, executable
test("cart slice stores items but never a derived total", () => {
  const keys = Object.keys(store.getState().cart);
  expect(keys).toContain("items");
  expect(keys).not.toContain("total");   // total is a selector, not a field
});

A model that “helpfully” adds a total field to avoid recomputing it now trips a named test that explains why the field does not belong.

The harness is how the boundary survives volume

The reason this matters more in an AI-heavy workflow is throughput. A team of humans erodes an architecture slowly enough that a periodic cleanup keeps up. A model generates changes faster than review can absorb, so erosion that used to take a year takes a sprint — unless the boundary is machine-checked. Encoding your architecture as executable rules turns “please keep UI and state separate” from a hope into an invariant that holds no matter how much code, or how fast, is written against it. The harness-state-shape and harness-atom-guardrail exercises build exactly these checks, which is the point where the separation stops being a principle you defend and becomes one the build defends for you.