Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/effect-4-0-1-floor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@systemfsoftware/effect-atom": patch
"@systemfsoftware/effect-atom-react": patch
"@systemfsoftware/storybook-gherkin": patch
---

The published `effect` and `@effect/*` dependency ranges now start at 4.0.1 (`^4.0.1`, was `^4`), so a consumer resolves at least Effect 4.0.1. `@systemfsoftware/discern` also bundles `@effect/platform-node` and `@effect/platform-node-shared` 4.0.1 instead of 4.0.0.
11 changes: 11 additions & 0 deletions .changeset/effect-4-0-5-peer-floor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@systemfsoftware/effect-atom-react": patch
"@systemfsoftware/effect-atom": patch
"@systemfsoftware/storybook-gherkin": patch
---

The Effect dependency now resolves to 4.0.5, and so does every `@effect/*` package installed with it.

- Effect 4.0.5 requires `effect@^4.0.5` as a peer, so a tree mixing 4.0.5 with an earlier 4.0.x fails at import, not at install. Install the versions your tree's `@effect/*` packages name.
- `FileSystem.File.Info` reports `dev`, `ino`, `rdev`, `nlink` and `blocks` as `bigint` where it reported `number`. Convert at your own boundary with `BigInt` or `Number`.
- A decision's `instructions` and its classify and probability `criteria` accept a JSON object or array as well as a string, and a rating's `criteria` are `{ label, description }` entries rather than bare labels. Read a level's `label`, not the entry.
7 changes: 7 additions & 0 deletions .changeset/full-version-dependency-ranges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@systemfsoftware/effect-atom": patch
"@systemfsoftware/effect-atom-react": patch
"@systemfsoftware/storybook-gherkin": patch
---

Published dependency ranges now name a full version: `vitest`, `@vitest/*` and `@types/node` ranges start at 5.0.1 and 26.6.2 (were `^5` and `^26`), `typescript` at 7.0.2 (was `^7`) and `playwright` at 1.63.0 (was `^1`). The `vitest` peer dependency of `@systemfsoftware/effect-gherkin-spec`, `@systemfsoftware/effect-schema-law`, `@systemfsoftware/effect-schema-vite`, `@systemfsoftware/effect-spec-runtime`, `@systemfsoftware/storybook-gherkin`, `@systemfsoftware/trace-spec`, `@systemfsoftware/vitest` and `@systemfsoftware/vitest-config` is now `^5.0.1`, so a consumer needs at least Vitest 5.0.1. `@systemfsoftware/trace-spec` depends on `@opentelemetry/semantic-conventions` `^1.43.0` (was `^1.33`).
7 changes: 7 additions & 0 deletions .changeset/funny-goats-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@systemfsoftware/effect-atom": patch
"@systemfsoftware/effect-atom-react": patch
"@systemfsoftware/storybook-gherkin": patch
---

The packed `devDependencies` range on `@systemfsoftware/oxlint-config-recommended` moves to its new major.
5 changes: 5 additions & 0 deletions .changeset/hungry-doors-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@systemfsoftware/oxlint-config-frontend": patch
---

The frontend oxlint preset moved into the frontend repository with its rules unchanged.
9 changes: 9 additions & 0 deletions .changeset/loose-moons-assert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@systemfsoftware/storybook-gherkin": minor
---

Step contexts now carry `expect`: the assertions of whichever runner the play runs in.

Under Storybook it stays `storybook/test`'s `expect`. In vitest browser mode (through `@storybook/addon-vitest`) it is vitest's own `expect`, so a step handler can reach the matchers only vitest has — `toMatchScreenshot`, `toMatchAriaInlineSnapshot`, `toMatchTextContent` — as `await ctx.expect(locator).toMatchAriaInlineSnapshot(…)`, without importing `vitest` where a project bans that import.

The assertions are promise-shaped: `await` them. `vitest` is an optional peer dependency.
12 changes: 12 additions & 0 deletions .changeset/quiet-hooks-subscribe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@systemfsoftware/effect-atom-react": major
---

The code that mounts the app owns the registry; the binding never creates one or writes registry state during render. Call `Atom.Hydration.hydrate(registry, state)` before the first render, while the atoms the state names have not been read yet: hydration assigns each named atom its saved value and replaces a value the registry already holds.

- `RegistryProvider` requires a `registry`. Make it with `Atom.Registry.make({ initialValues, scheduleTask: AtomReact.scheduleTask })` before the first render and dispose it at teardown.
- `HydrationBoundary` is removed: call `Atom.Hydration.hydrate(registry, state)` before `hydrateRoot`.
- `useAtomInitialValues` and `AnyInitialValue` are removed: pass `initialValues` to `Atom.Registry.make`.
- `useAtomSubscribe` is removed: use `Atom.Registry.subscribe` in a scoped Effect.

An island makes its registry at module scope; a Storybook story makes it in its loader and disposes it in `beforeEach`'s cleanup.
7 changes: 7 additions & 0 deletions .compound-engineering/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
packs:
- source: compound-packs/ui-state
- source: compound-packs/storybook-testing
- source: https://github.com/systemfsoftware/systemfsoftware
ref: d1ab631935c9439bcd16abf2894b09b843494701
path: compound-packs
pack: [cell-architecture, boundary-testing, schema-laws, package-topology, mutation-discipline, turbo-task-graph]
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ apps/site-e2e/artifacts/
.journeys/
.sfs-deps
.cache/
temp/
.vitest/
40 changes: 40 additions & 0 deletions .mergify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Mergify merges every PR into main. A PR that meets queue_conditions is queued automatically; nobody comments.
# The first PR in this repository (SYS-440) is a bootstrap merge: kiro-systemf merges it once with
# --match-head-commit. From the next PR the queue takes over.
queue_rules:
- name: default
merge_method: squash
queue_conditions:
- base = main
- -draft
- -label = hold
- "approved-reviews-by = kiro-systemf[bot]"
- "#changes-requested-reviews-by = 0"
- check-success = check (format)
- check-success = check (lint)
- check-success = check (typecheck)
- check-success = check (test)
- check-success = check (dist)
- check-success = check (sfs-sources)
- check-success = journeys
- check-success = rules
- check-success = lint PR commits
- check-success = changeset · shared tooling
merge_conditions:
- check-success = check (format)
- check-success = check (lint)
- check-success = check (typecheck)
- check-success = check (test)
- check-success = check (dist)
- check-success = check (sfs-sources)
- check-success = journeys
- check-success = rules
- check-success = lint PR commits
- check-success = changeset · shared tooling

pull_request_rules:
- name: queue when ready
conditions:
- base = main
actions:
queue:
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ To remove it:
6. `apps/site/alchemy.run.ts`: drop the `migrations` option from `Cloudflare.D1.Database('Database', …)`.
7. `pnpm-workspace.yaml`: drop the `@systemfsoftware/xstate` line under `catalog:` and its mirror under `overrides:`; `apps/site/package.json`: drop the `@systemfsoftware/xstate` dependency.
8. Run `pnpm install --no-frozen-lockfile` from the shell you already have open. It rewrites `pnpm-lock.yaml` without its xstate entries; `--frozen-lockfile` (pnpm's default when `CI` is set) refuses that rewrite. The devshell builds its pnpm store from `pnpm-lock.yaml`, so it can't start again until the lockfile drops xstate.
9. `flake.nix`: drop the `systemfsoftware-xstate` input, its name in the `outputs` arguments, and its two places in the `sfs-deps` derivation (the copy loop and the `jq -s add` merge); then run `nix flake lock`.
9. `flake.nix`: drop the `systemfsoftware-xstate` input, its name in the `outputs` arguments, and its place in the `extraDepsOf` copy loop; then run `nix flake lock`.
10. Delete this README's guestbook text, from "The site ships one example feature" through the D1 note below.

Removal leaves a deployed D1 as it is: the `guestbook_entries` table and its `0001_create_guestbook_entries.sql` and `0002_add_guestbook_entry_state.sql` rows in `__alchemy_migrations` stay; drop them from the D1 console in the Cloudflare dashboard with `DROP TABLE guestbook_entries; DELETE FROM __alchemy_migrations WHERE name IN ('0001_create_guestbook_entries.sql', '0002_add_guestbook_entry_state.sql');`.
Expand Down
16 changes: 16 additions & 0 deletions compound-packs/storybook-testing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Storybook Testing Compound Pack

How a UI's behaviour is proven with Storybook stories: where a story gets its I/O and how each story's fakes start fresh, how a journey reads, how a story finds and asserts, where stories run and what fails them, when a layout claim counts, which test kind proves what a person sees, and where a story's visual evidence comes from.

Rules:

- `stories-compose-at-their-own-root`: the preview gives stories port fakes through the provider the production entry uses, each root building its registry before it renders; no first-party module is substituted by `sb.mock`, alias or subpath import.
- `fresh-fakes-per-story`: every story starts from freshly built fakes in a registry its loader builds and its `beforeEach` cleanup disposes, and sets I/O state only through their configurable responses.
- `journeys-are-embedded-gherkin`: a journey is one short embedded scenario whose Given sets state, whose one When acts, whose Then asserts what a person observes, and whose steps hold every query.
- `real-browser-axe-as-error`: stories run in a real browser with axe violations failing the command-line run; an exception disables one named rule with its reason.
- `assert-through-accessible-queries`: play functions and steps find elements by role, label or text, never by test id or selector.
- `pin-the-viewport-a-claim-needs`: a layout claim pins `globals.viewport`, and a responsive claim is proven on each side of the breakpoint.
- `stories-are-the-ui-integration-tests`: user-visible behaviour is proven by stories over the real app, not by a simulated-DOM suite or a shell test against a double.
- `visual-evidence-comes-from-the-runner`: each story's image comes from one capture hook the runner runs for every story, and failure images from Vitest; both folders are ignored run output that CI uploads as they are, and no story calls `page.screenshot`.

What makes a port fake admissible, and how adapters are tested against local oracles, is governed by boundary-testing (`port-fakes-pass-the-port-contract.md`, `real-system-oracles.md`, `no-mocks-on-internal-glue.md`). The step DSL's API is documented by the `@systemfsoftware/storybook-gherkin` README and its API report.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: Play functions and steps find elements by role, label or text and assert what a person perceives
applies_when:
- writing a play function or a step definition that finds or asserts on an element
- an AI agent proposes a test id, a CSS selector or `querySelector` in a story
- reviewing an assertion on markup structure or on a class name
tags: [storybook, queries, accessibility, testing-library, play]
---

Play functions and steps find elements by role, label or visible text and assert what a person perceives, never by test id or markup selector. A query by role and accessible name is the query a person using assistive technology makes, so it fails when a control loses its name or role. A test id or selector finds an element no person can reach and passes on a restyle that changed what a person sees.

Testing Library ranks its queries by how closely they resemble the way users interact with the page: queries accessible to everyone first, then semantic queries, with test ids last, for "cases where you can't match by role or text or it doesn't make sense".

1. **Query through the canvas.** A play function destructures `canvas` from its context, and a step uses the `canvas` on its step context; both are queries scoped to the story's root. Assertions use `expect` from `storybook/test`, so failures render in the Interactions panel.
2. **Role, label, text, in that order.** Find a control by `getByRole` with its accessible name, a field by `getByLabelText`, and content by `getByText`. Scope a query inside a found region with `within`.
3. **No test id, no selector.** Never query by `getByTestId`, `querySelector`, a class name or a DOM path. If no accessible query can find the element, the element lacks an accessible name, and that is the defect to fix.
4. **Assert the perceived result.** Assert visibility, accessible text, and the enabled or pressed state a person perceives. Never assert a class name or attribute only the markup carries.

```tsx
import type { Meta, StoryObj } from '@storybook/react-vite'
import { expect, within } from 'storybook/test'

const meta = { component: MessageList, args: { channel: 'General' } } satisfies Meta<typeof MessageList>
export default meta
type Story = StoryObj<typeof meta>

// WRONG: a test id and a class selector find nodes no person can name, so the story
// passes when the list loses its accessible name.
export const ShowsMessageByTestId: Story = {
play: async ({ canvas, canvasElement }) => {
await expect(canvas.getByTestId('message-list')).toBeInTheDocument()
await expect(canvasElement.querySelector('li.message')).toHaveTextContent('Hello all')
},
}

// RIGHT: the list is found by its accessible name and the message inside it by its text.
export const ShowsPostedMessage: Story = {
play: async ({ canvas }) => {
const messages = canvas.getByRole('list', { name: 'Messages' })
await expect(within(messages).getByText('Hello all')).toBeVisible()
},
}
```

Grounds: Testing Library, query priority (https://testing-library.com/docs/queries/about/#priority); Storybook 10.6, Interaction tests (https://storybook.js.org/docs/writing-tests/interaction-testing).

Gate: `review` — reject a play function or step that queries by test id, `querySelector`, class name or DOM path, or that asserts on markup a person does not perceive.
Loading
Loading