You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/CLAUDE.md

523 lines
25 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
> **Branch note (`active-uix`):** the legacy layers `src/lib/`, `src/uix/terra/`,
> `src/uix/air/` and the old `src/routes/` test tree were removed during the
> UIX refactor. The new ecosystem lives in `src/arts/`, `src/libs/`, `src/svrs/`,
> and `src/uix/{active-uix,soma,sema,eidos,morfo}`. Demos are being re-authored
> under `web/routes/` from scratch.
## Build / Test Commands
```bash
npm run dev # Start dev server (routes resolve from web/routes)
npm run build # Production build (static via adapter-static)
npm run test # Run all tests once
npm run test:unit # Run tests in watch mode
npx vitest run src/uix/morfo/compile.test.ts # Run a single test file
npx vitest run -t "describe name" # Run tests matching a pattern
npm run check # Type check with svelte-check
npm run lint # Check formatting (Prettier)
npm run format # Auto-format
```
## Code Style
- Tabs for indentation, single quotes, no trailing commas, 100 char print width (Prettier).
- Comments in **English**. (Some legacy Spanish comments remain — don't add new ones.)
- Svelte 5 runes mode is enforced globally (`svelte.config.js` `dynamicCompileOptions`).
- DOM event handlers: lowercase (`onclick`, `onfocus`). User callbacks: camelCase (`onComplete`, `onInteractOutside`).
- State: `$state()` without `#`. Derived: `readonly x = $derived.by(...)`.
## Path Aliases
Aliases are kept in sync between `svelte.config.js` and `vite.config.ts`. Source
of truth is `vite.config.ts` (single `aliases` const reused by both top-level
resolve and the server-test project).
| Alias | Target |
| --- | --- |
| `@` | `src/` |
| `$active-app`, `$adom`, `$auth`, `$bus`, `$cache`, `$connection`, `$format`, `$frontend`, `$http`, `$lang`, `$logger`, `$orca`, `$perm`, `$prefs`, `$session`, `$sium`, `$storage`, `$timer` | `src/arts/{name}` |
| `$libs`, `$locale`, `$reactive` | `src/libs`, `src/libs/locale`, `src/libs/reactive` |
| `$svrs` | `src/svrs/` |
| `$uix`, `$active-uix`, `$soma` | `src/uix`, `src/uix/active-uix`, `src/uix/soma` |
## Vitest Two-Project Structure
`vite.config.ts` defines two test projects:
- **client**: Browser tests via Playwright for `*.svelte.{test,spec}.{js,ts}`.
- **server**: Node environment for `*.{test,spec}.{js,ts}` (excludes svelte tests).
## Architecture
The UIX is layered around a declarative contract (morfo) that the other layers
consume. Each consumer reads the morfo via `compileMorfo(morfo)`, never by
walking the raw declaration.
```
arts/ → runtime artifacts: active-app, adom, perm, prefs, ...
libs/ → pure helpers (zero-dep): reactive, days, dom, locale, ...
svrs/ → server-authoritative engines
uix/morfo → declarative contract: parts, data-attrs, ARIA, keyboard,
events with semantic family/intent. Compiler at compile.ts
emits CompiledMorfo (cached by WeakMap).
uix/sema → semantic engine + channels (visual / sound / vibra).
The visual channel writes data-event* during a hold window.
State attrs (data-state, data-disabled, ...) are NEVER
touched by sema — they belong to the morfo runtime.
uix/soma → headless component layer. Each provider consumes morfo +
sema via `runtime.trigger(eventName)` which sequences:
prewrite → semantic.emit → handler → effect-driven state attrs.
uix/eidos → CSS layer that selects against the data-attrs the morfo
contract promises. lint.ts validates a CSS file against
compileMorfo(morfo).contracts.cssSelectors.
uix/active-uix → composition root. Two boot modes:
- createActiveUix(options) — standalone (owns services)
- attachActiveUix(activeApp) — attach to external app
```
### morfo (`src/uix/morfo/`)
Single source of truth for a component. A `Morfo` declares:
- `parts[]` — kebab + archetype + `data` + `aria` + optional `keyboard`
- `events[]` — name + `target` partRef + `semantic` + optional `prewrite` + `commits`
- `focus` — initial / trap / return / restore (for overlay components)
`compileMorfo(morfo)` returns a `CompiledMorfo` with pre-resolved `AttrPlan[]`,
`KeyboardPlan[]`, `ActionPlan[]`, and `contracts.cssSelectors` (the closed set
of selectors eidos is allowed to use). Cached by morfo identity.
### eidos linter
`scripts/eidos-lint.ts <component>` and `scripts/eidos-lint-all.ts` classify every
`[data-*]` selector in eidos CSS as morfo-backed / eidos-only / invalid. Use
this to detect drift between the morfo contract and the eidos rules.
## Key Conventions
- **Never modify the morfo declaration files** without updating the eidos
selectors and provider sources that depend on them.
- **Read before acting.** When told to read a file, read it. Don't paraphrase.
- **Verify before reporting done.** Run `npm run check` and the relevant
vitest scope. UI claims need a browser check; if you can't run a browser, say so.
- **No backward-compat shims** when relocating code. Update consumers and delete
the old path; don't leave a re-export.
- **No re-export façades** between layers (e.g. soma must not re-export dias —
consume `$libs/days` directly).
- **Provider naming**: child providers reference the parent as `provider`, never
`root`. The exported component is `<Xxx.Provider>`. `createAttrs` still uses
the part kebab `'provider'` internally.
- **Data-attr naming**: `data-{component}` (provider) and `data-{component}-{kebab}`
(sub-parts). Never `data-soma-*`. The compiler emits these via
`compiled.parts.attrs`; runtime queries match exactly.
- **CSS token naming (eidos recipes)**: bare-prefixed by component, never
by layer. Public tokens: `--{component}-…` (e.g. `--dialog-content-bg`,
`--toggle-radius-md`). Internal recipe tokens: `--_{component}-…`.
Never `--eidos-…`, `--air-…`, `--terra-…`, `--soma-…`. Migrating
`air-` baselines means stripping the `air-` prefix, not replacing it.
- **DOM helpers**: `$adom` is the single public surface for DOM-related
functionality at component / soma level. It re-exports all of
`$libs/dom` wholesale, plus the reactive `createActiveDom` runtime
and stateful primitives (`BodyScrollLock`, `DOMContext`, `RovingFocusGroup`).
Components and soma NEVER import from `$libs/dom` directly —
consume `$adom`. Only `arts/adom/*` (the implementation), `arts/frontend`
(peer service fallback), and tests of `$libs/dom` may reach into it.
This includes responsive types (`ResponsiveProp<T>`, `Breakpoint`,
`Breakpoints`) — they travel with the runtime, so they live in `$adom`.
- **Iframe / popup correctness**: when you have a `uix.dom`, prefer
`uix.dom.getDocument(node?)` and `uix.dom.getWindow(node?)` over the
bare functions of the same name. The instance methods respect the
active-dom's `targetWindow`; the bare functions fall back to the
global `document` / `window`, which is wrong in iframe / popup /
happy-dom-test contexts.
## Critical Rules
- **NEVER delete files without explicit instruction** ("delete", "remove",
"borra", "elimina"). If ambiguous, ASK first.
- **NEVER create fake translators / mocked services** in tests for components.
Use real instances composed via `createActiveUix` or `attachActiveUix`.
- **NEVER skip hooks (`--no-verify`) or bypass signing** unless explicitly asked.
If a hook fails, fix the root cause.
- **`$reactive` survives, `$lib` does not.** The `$reactive` alias points to the
current `src/libs/reactive`; the singular `$lib` alias was removed in the
cleanup phase and any code that resurfaces it is a regression.
## Reference Documents
Architecture overview and motivations: [`src/uix/active_architecture.md`](src/uix/active_architecture.md).
Per-layer references:
- [`src/uix/morfo/README.md`](src/uix/morfo/README.md)
- [`src/uix/sema/README.md`](src/uix/sema/README.md)
- [`src/uix/soma/SOMA_ARCHITECTURE.md`](src/uix/soma/SOMA_ARCHITECTURE.md) +
[`src/uix/soma/COMPONENT_GUIDE.md`](src/uix/soma/COMPONENT_GUIDE.md)
- [`src/uix/eidos/README.md`](src/uix/eidos/README.md) +
[`src/uix/eidos/components/README.md`](src/uix/eidos/components/README.md)
The canonical guide for sema vocabulary, morfo event shape, color
tokens, persistence and a11ySemantic lives in
[`src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md`](src/docs/GUIA_IMPLEMENTACION_SEMAUIX.md).
That document is authoritative — when in doubt about families, intents,
verbs, color subsets per component, or holds, that's the source.
## Sema cascade selectors must use the typed builder
Cascade rules in `src/uix/sema/components/*.ts` MUST construct their
`selector` via `semaSelector(morfo, partKebab, matchers?)` from
`$uix/morfo`. **Never hand-write selector strings** that target morfo-
emitted attrs (`[data-{component}]`, `[data-{component}-{part}]`,
`[data-event="..."]`). The helper:
- Compile-error if `partKebab` doesn't exist on `morfo.parts`.
- Compile-error if `matchers.eventName` doesn't match `morfo.events`.
- Type-checks `eventFamily`/`eventIntent` against the canonical sema
unions.
- Output is the same CSS string the cascade rule needs — zero runtime
cost beyond concatenation.
Hand-written selectors in cascade rules are an architecture violation:
when morfo renames a part or event, the strings go stale silently. The
helper wires the cascade to the morfo's contract so renames break at
the type level.
```ts
// good
import { semaSelector } from '$uix/morfo'
import { dialogMorfo } from '$uix/morfo/components/dialog'
{
selector: semaSelector(dialogMorfo, 'content', { eventFamily: 'commit' }),
haptic: { kind: 'tap' }
}
// bad — string drifts silently if morfo renames
{
selector: '[data-dialog-content][data-event-family="commit"]',
haptic: { kind: 'tap' }
}
```
## Sema: intent policy per family
The doctrinal stance on intent declaration is encoded as a policy const,
**`SEMA_FAMILY_POLICY`** in `src/uix/sema/types.ts`:
```ts
{
contact: { intentPolicy: 'allowed' }, // intent OPTIONAL — neutral default
commit: { intentPolicy: 'expected' }, // intent REQUIRED — every consummation evaluates
signal: { intentPolicy: 'expected' }, // intent REQUIRED — alarms inherently evaluative
handle: { intentPolicy: 'allowed' }, // intent OPTIONAL — drag/scrub usually neutral
emerge: { intentPolicy: 'optional' }, // intent OPTIONAL — Dialog confirming threat declares
shift: { intentPolicy: 'optional' },
sustain: { intentPolicy: 'optional' }
}
```
Each entry is an object so future per-family policy fields (sequence
default, allowed channels, hold preferences, gesture phases, …) live
alongside without restructuring consumers.
Type derivation: `SemaEvent` and `MorfoEventSemantic` discriminate over this
policy. Editing the const reshapes the discriminated union: `'expected'` →
`intent` REQUIRED at compile-time; `'allowed'` / `'optional'` → optional.
Runtime enforcement: `validateSemaEvent` throws when an `'expected'` family
declares an event without intent. `'optional'` families pass silently when
intent is absent.
**This replaces the prior valenced/transitional split as the gatekeeper
for intent**. The valenced/transitional distinction still exists as a
family classification but no longer dictates intent rules — the policy
const does. Real-world UX needs (a Dialog opening to confirm a destructive
action carries threat in its very emergence) made the strict rule
counter-productive.
## Sema: open channel registry + flat CSS-style cascade
The perceptual layer is **NOT 4-channel-closed**. The framework ships 5
canonical channels (`motion`, `sound`, `color`, `presence`, `haptic`)
declared in `SemaChannelSignatures`. Apps add more via TypeScript
declaration merging.
**The semantic tokens are `data-event-*`**: the engine stamps `data-event`,
`data-event-family`, `data-event-intent`, `data-event-phase`, `data-event-id`
on `signal.target` BEFORE the cascade resolves. Cascade rules use CSS
selectors that target those tokens directly — same surface that eidos CSS
already reads. Engine owns stamp/unstamp; VisualChannel only owns the hold.
Cascade rules are **flat** (no `overrides: { eventLabel: ... }` middle
layer): a single selector + per-channel deltas, like a CSS rule.
```ts
{
selector: '[data-dialog-content][data-event-intent="threat"]',
sound: { sampleUrl: '/sounds/dialog-fail.wav' },
haptic: { kind: 'error', pattern: [50, 80, 50, 80, 50] }
}
```
Resolution cascade (each layer overrides the previous):
```
1. SEMA_MAP.families[F].base — per-channel signatures
2. SEMA_MAP.intents[I].deltas — valenced families only;
numbers ADD (modificadores)
3. signal.overrides + signal.channels — per-event morfo overrides;
numbers REPLACE
4. engineOpts.overrides.runtime — path-based globals; REPLACE
5a. engineOpts.components[].cascade — per-component packs
(sema/components/{name}.ts)
5b. engineOpts.overrides.cascade — app-level rules; appended
AFTER packs so they win
on equal specificity
```
**Per-component packs live in `src/uix/sema/components/{name}.ts`** —
symmetric to morfo / soma / eidos. Each exports a `Sema` with
`cascade` rules + optional `preloadSamples` (WAVs decoded at boot).
**Override semantics — numbers**:
- intent.deltas (capa 2): numbers ADD (modificadores compositivos).
- overrides (capas 4, 5, 6): numbers REPLACE (`color: red`, not "add red").
- Use `{ op: 'add' }` / `{ op: 'multiply' }` / `{ op: 'replace' }` for
explicit semantics either way.
**`intent.deltas` owns the perceptual signature of the intent** — the
sound primitives `pitch`, `gain`, `contour` (and the haptic `intensity`
when shaped per-intent) come from capa 2. **Cascade rules (capas 6a/6b)
MUST NOT override these primitives** — doing so flattens the perceptual
difference between `risk` / `threat` / `loss` (or `affirm` / `fulfill`).
Cascade rules add character (`haptic.kind`, `haptic.pattern`,
`sound.sampleUrl`, `motion.easing`, `presence.backdrop`) — not the
intent's evaluative profile. If a per-component cascade rule needs to
*shift* a numeric primitive (e.g. close-* in emerge goes descending),
use `{ op: 'add', value: -150 }` so it composes on top of the intent
rather than erasing it.
`HapticChannel` (V1, real — Vibration API) is opt-in via
`new EngineSemantic({ haptic: true })`. Sound and haptic both default
off because they have audible / tactile side effects.
Canonical sources: `src/uix/sema/sema-map.ts` (SEMA_MAP value +
SemaChannelSignatures registry + Sema), `src/uix/sema/resolver.ts`
(cascade implementation + CSS specificity), `src/uix/sema/stamp.ts`
(stamp/unstamp), `src/uix/sema/components/*.ts` (per-component packs),
`src/uix/sema/README.md`.
## Eidos drift defense — types over lint
Selector drift between morfo's emitted attrs and the layers that style /
react to them (eidos CSS, sema cascade) is defended **at compile time
via typed builders**, not at lint time.
- **Sema cascade rules** (`sema/components/*.ts`) consume
`semaSelector(morfo, partKebab, matchers?)` from `$uix/morfo`. Renames
in morfo break the cascade at type-check time. See "Sema cascade
selectors must use the typed builder" above.
- **Eidos plain `.css` recipes** are still raw CSS today — there is no
CSS-side typed builder. For those, `scripts/eidos-lint.ts` remains as
an **opt-in safety net** that classifies each `[data-*]` selector as
`morfo-backed` / `eidos-only` / `invalid`. It is not the architectural
contract; the contract is the morfo declaration.
Rule: when a layer can consume the morfo via TypeScript (anything in
`.ts` / `.svelte`), it MUST use the typed builder. Lint is for the
remaining surface (plain CSS recipes) until those gain a builder of
their own. Hand-written morfo-targeting selector strings in TypeScript
files are an architecture violation, not a lint warning.
## Session hand-off — 2026-05-09 (selector discipline + lint reframing)
- **Typed selector builder applied** in
`src/uix/sema/components/dialog.ts`. Every cascade rule now constructs
its selector via `semaSelector(dialogMorfo, 'content', matchers?)`
instead of hand-writing strings. The helper lives in
`src/uix/morfo/selectors.ts` and is exported from `$uix/morfo`.
- **Eidos-lint reframed** from "architectural drift mechanism" to
"opt-in safety net for plain CSS recipes". The architectural mechanism
is compile-time typing. The lint script + scripts continue to work
unchanged but are no longer the doctrinal source of safety.
- **Intent.deltas signature ownership documented** — cascade rules MUST
NOT override `pitch` / `gain` / `contour` on `sound`, because doing so
collapses the per-intent perceptual difference. Captured both inline
in the dialog cascade (NOTE comment on the intent block) and in the
"Override semantics — numbers" rule above.
- **Documentation updated**:
- `src/uix/morfo/README.md` — new "Typed selector builder —
`semaSelector`" section.
- `src/uix/eidos/README.md` — replaced "Linter" section with
"Defensa contra drift de selectores" framing the two-tier defense
(compile-time builder for TS/Svelte, opt-in lint for plain CSS).
- `src/uix/sema/README.md` — already documented the builder + the
cascade discipline + the SEMA_FAMILY_POLICY shape (from the prior
refactor).
**Verification at hand-off**: `npm run check` reports 0 errors, 2
pre-existing Svelte warnings unrelated to this work. `npm run test`
reports 1883/1883 tests green across 154 files (~22s).
## Session hand-off — 2026-05-08 (post-canon alignment)
The codebase has been aligned to the canonical guide. Most recent
architectural move:
- **Runtime relocated + renamed** — what was `MorfoRuntime` in
`src/uix/morfo/runtime.svelte.ts` is now `SomaRuntime` in
`src/uix/soma/runtime.svelte.ts`. Reason: morfo must remain pure
declarative TypeScript (DNA); the runtime imports Svelte runes,
`$adom`, `$uix/sema`, and `$libs/reactive` — those are soma's
territory. Per the doctrine "morfo declares, soma executes" the
runtime now lives where it conceptually belongs. Active-uix's
`uix.runtime(morfo, sources)` factory stays unchanged; it imports
`createSomaRuntime` from `$soma`.
- **Sema is now genuinely ornamental** — `ActiveUix.semantic` returns
`EngineSemantic | undefined` (no longer throws). `SomaRuntime.trigger()`
skips the `emit` step and the target-resolution check when no engine
is present. Components stay functional in SSR / headless tests /
audio-disabled environments.
The earlier swept phases (still in effect):
- **Sema verbs** (`src/uix/sema/verbs.ts`): restructured to
family-keyed `Record`. `select`/`toggle`/`acknowledge` moved to
`commit`; `edit` removed from `handle` (now expressed as
`shift.enter-mode`). New verbs added per the canon (tap, focus,
release, complete, restore, remind, route, step, syncing, etc.).
`validateEventName` recognises both `{verb}-{variant}` and
`{family}-{verb}` shapes.
- **Morfo event shape** (`src/uix/morfo/types.ts`): `target` moved
inside `semantic`; added optional `verb` (advisory, validated against
`SEMA_VERBS[family]`) and `sequence: 'pre' | 'coincident' | 'post'`.
All 22 events across toggle/toast/popover/drawer/dialog refactored
to the new shape.
- **Color tokens** (`src/uix/eidos/themes/base/{light,dark}.css`):
doctrinal 8 tokens — `primary, secondary, neutral, affirm, fulfill,
risk, threat, loss`. Renamed `success → fulfill`, `warning → risk`,
`danger → threat` (same hex). New primitives for `secondary`
(slate-blue), `affirm` (teal-mint, low activation), `loss` (deep
violet-grave, posterior). `info` palette deleted entirely. Legacy
aliases in `_static.css` deleted (clean cut, no transition).
29 consumers (recipe CSS + tokens) migrated to doctrinal names.
Earlier in the session:
- **Toggle is the eidos pilot** (`src/uix/eidos/components/toggle/`):
recipe + Svelte wrapper + types + index + README. The pattern is
documented in `src/uix/eidos/components/README.md`.
- **Eidos has a shared lib** at `src/uix/eidos/lib/types.ts`. First
type is `Size` (`'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'full'`).
Components narrow with `Extract<Size, 'sm' | 'md' | 'lg'>` per the
per-component-subset doctrine.
- **No `Eidos` prefix on types**. The path
`$uix/eidos/components/{x}` already conveys layer. Public types are
`ToggleProps`, `ToggleVariant`, `ToggleSize`.
- **API doctrine**: soma stays compound (`Toggle.Provider`); eidos
exports both `default` and `Provider` for single-part components so
`<Toggle>` works for the ergonomic case while `<Toggle.Provider>`
stays available.
- **SoundChannel eager-init landed** in `src/uix/sema/chans/sound.ts`
to fix the autoplay race: the AudioContext is created + resumed
synchronously on the first user gesture (capture-phase listener on
`document`).
- **Demo page (`web/routes/toggle/+page.svelte`)** has the live
preview rendered ALWAYS above the tablist (so Sema-tab Play buttons
can fire on the real toggle) and the motion preview amplifies scale
×8 visually only — doctrinal values stay in the `<dl>`.
**Mandatory checklist before any eidos component migration**:
1. Recover the air baseline from git:
```
git ls-tree -r 0a391408^ --name-only | Select-String "air.*{name}"
git show 0a391408^:src/uix/air/components/{name}/types.ts
git show 0a391408^:src/uix/air/components/{name}/{name}.svelte
```
2. Inventory air's props, slots, effects, perceptual emits.
3. Build a comparison table: feature × (air had it | canon allows it |
eidos exposes it). Flag each gap as `regression`, `enhanced`,
`dropped-by-canon`, or `nuevo`.
4. Present the table BEFORE coding. Get explicit per-gap decision.
5. Only then write the wrapper.
Switch was migrated WITHOUT this discipline and shipped a regression
(no `ResponsiveProp<Size>` for breakpoint-aware sizing). Don't repeat.
Next concrete steps:
1. Migrate `collapsible → dialog → drawer → popover → toast → avatar`
to the eidos wrapper pattern. **For each: walk the air-baseline
checklist above first.** Toggle / switch are templates for the
subdirectory shape, but the per-component prop surface must come
from air's recovery, not from copying toggle's props.
2. **Persistence + holds-by-intent** (guide §6) — defer until the
first concrete `signal.warn` / `signal.alert` consumer appears.
Today every signal is `transient` with a numeric hold. Need to add
`persistence: 'transient' | 'untilAction' | 'untilFix' | 'stateBound'`
to `SemanticSignal` + the holds-by-intent table per the canon.
3. **a11ySemantic per event** (guide §9) — defer until there's a
reduced-motion runtime helper + live-region helper to consume the
contract.
4. **Polymorphic events** (guide §5.3, `allowedFamilies` +
`defaultSemantic`) — defer; no current component needs polymorphism.
5. The docs site's topbar prefs (sound mute toggle, theme switcher)
are wired up but the sound mute does NOT yet drive
`SoundChannel.masterGain`. Wiring belongs on layout.svelte's `$effect`.
Baseline `npm run check` shows 39 pre-existing errors in test files
unrelated to this work. Tests are 217/217 green across uix.
---
# Behavioral Guidelines
These reduce common LLM coding mistakes. They bias toward caution over speed —
for trivial tasks, use judgment.
## 1. Think before coding
Don't assume. Don't hide confusion. Surface tradeoffs.
- State assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them — don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity first
Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
## 3. Surgical changes
Touch only what you must. Clean up only your own mess.
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it — don't delete it.
When your changes create orphans, remove the imports / variables / functions
your changes made unused. Don't remove pre-existing dead code unless asked.
## 4. Goal-driven execution
Define success criteria. Loop until verified.
- "Add validation" → "Write tests for invalid inputs, then make them pass."
- "Fix the bug" → "Write a test that reproduces it, then make it pass."
- "Refactor X" → "Ensure tests pass before and after."
Strong success criteria let you loop independently. Weak criteria require
constant clarification.

Powered by TurnKey Linux.