Sheratan — pre-release

Architectural boundaries, enforced.

A frontend framework with one legal way to structure an app, checked by a tool whose errors carry their own fix, with async and live data built into the core.

View on GitHub Docs
$ sheratan create my-app — arrives with v0.1
γ β α
01 — The problem

Too many legal ways is the bug.

A code-generating agent doesn't fail React because a linter is missing. It fails because Zustand, Context, Redux and Query are all correct answers to one question, so it picks at random and the codebase drifts. A boundary plugin can't delete that choice. A runtime designed around one answer can.

Async state lives outside the core

Every app rebuilds loading, errors, race cancellation and retry from a third-party library.

Causality isn't observable

"Why did this update?" still takes a browser extension and a guess.

Layers are a convention

Frameworks organise code. Nothing stops I/O inside a component.

A build step is mandatory

Hundreds of megabytes of tooling before the first hello world.

02 — One table

The rule set fits in one table.

Sheratan enforces an import matrix statically, plus a few rules the matrix can't express. When code crosses a boundary, the checker says which boundary it crossed, what's allowed instead, and how to fix it. The error is written for an agent to act on in one turn.

file ↓libuiservicesown
module
other
module
lib/·····
ui/···
services/···
*.state.ts···
*.effects.ts·contractsstateindex.ts
*.view.ts·state·
app.ts

Plus: no I/O globals in views, file set matches module kind, acyclic module graph. Writing state from effects isn’t a checker rule at all — a state module exports accessors and transitions, never the signals themselves, so state.rows.set(…) is a type error in any editor.

$ sheratan check --json
{
  "code": "SHR-L001",
  "severity": "error",
  "file": "modules/todo/todo.view.ts",
  "range": { "line": 3, "column": 1 },
  "message": "view cannot import effects;
              allowed: lib, ui, own state.",
  "fix": "Move the call into todo.effects.ts
          and expose the result via todo.state.ts.",
  "docs": "https://sheratan.dev/errors/L001"
}

A stable code, a location and a fix. The JSON shape is a versioned public API.

03 — The module

Four files. One direction.

A module's file set is fixed by its kind. State holds signals and pure transitions, effects hold everything impure, and the view is a pure function of state that emits intents and never acts on them. Dependencies arrive as factory parameters, with no container and no event bus.

modules/orders/
orders.state.tssignals and pure transitions
orders.effects.tsfetch, storage, timers, streams
orders.view.ts(state) ⇒ template
index.tsthe only public surface
orders.state.test.tsgenerated — pure, no mocks
orders.effects.test.tsgenerated — one fake transport
effects state view 1 transition reads intent — typed payload, never the raw event

An effect that gathers several responses commits them in one transition, so the view never renders a half-applied state.

04 — Live data in the core

Async and streams are primitives, not a stack.

resource() gives you status, data and error, aborts on key change and on unmount, de-duplicates, discards out-of-order responses, revalidates stale data and retries with backoff. stream() takes WebSockets or Server-Sent Events and folds messages into one commit per frame. Views update per hole with no virtual DOM, and everything a module creates is disposed with it.

const user = resource({
  key:   () => ['user', userId()],
  fetch: ({ signal, key }) =>
           api.getUser(key[1], { signal }),
  staleAfter: 30_000,
  retry: { attempts: 3, backoff: 'exponential' },
});

user.status();  // idle · loading · ready · error · refreshing
1,000msg/s
into a 500-row table
60fps
frame-coalesced writes

The MVP performance target, not a published result. The benchmark ships with the release, against React, Vue, Svelte and Solid on the same feed.

05 — One package

No bundler, no config. Type stripping only.

The core ships as plain ESM with zero runtime dependencies and runs from a <script type="module">. Routing, async state, layer enforcement and debugging are all in that one package. The checker is a dev tool with typescript as a peer dependency.

0
runtime deps
1
package
6.9KB
gzip, measured
// inside an existing React or Vue app
const dispose = render(view,
  document.querySelector('#live-table'));

Widget mode: mount one live table into someone else's dashboard. It owns only its subtree and disposes cleanly on route change.

06 — Stated plainly

What it builds on, and what it won't do.

None of the parts are new. Elm enforced architecture through a compiler, Solid has signals and resources, Lit has no-build templates, and boundary linters exist for every stack. What's new is a runtime designed so that an import matrix is enough: one state model, one async model, one place for I/O.

  • Server renderingA stated no. Sheratan is for authenticated, high-frequency UIs: ops consoles, trading screens, internal tools. Content sites should use Next or Nuxt.
  • React compatibilityNot a goal. Widget mode is the bridge.
  • A UI kitcreate copies a few stateless primitives into your project. They're yours to edit.
  • Nested layoutsFlat routes over URLPattern are in the core. Nested routing is a separate optional package later.
sheratan
β Arietis

Aš-šaraṭān, "the two signs". For centuries this star and its neighbour marked the vernal equinox, the fixed point where one season ends and the next begins and the whole year is measured from. Sheratan borrows the name for the same kind of line: a boundary drawn precisely, and kept.

View on GitHub Docs