Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/tender-wombats-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@st8craft/core": patch
---

withMultiChain
85 changes: 85 additions & 0 deletions .cursor/skills/impl-doc-consistency/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
name: impl-doc-consistency
description: Enforce consistency between fixture implementation in packages/core/src and documentation in docs/pages by checking names, options, context fields, and lifecycle semantics. Use when adding or editing withX fixtures, updating fixture docs, or reviewing terminology drift across implementation and docs.
---

# Implementation-Documentation Consistency (Statecraft)

Use this skill to keep `packages/core/src` and `docs/pages` aligned for fixture APIs, terminology, and behavior.

## Scope

Focus on fixture parity across:

- `packages/core/src/scenarios/fixtures/with*.ts`
- `docs/pages/fixtures/**/with*.mdx`

## Canonical Rule

Treat implementation as source of truth for executable behavior.

Allowed exceptions:

- Docs can be clearer than code comments.
- Docs can add examples and caveats not present in code.
- If docs intentionally diverge from implementation, explicitly state why in the docs.

## Core Checks

For each `withX` fixture:

1. **Existence parity**
- Every `withX.ts` has a matching `withX.mdx`.
- Every `withX.mdx` maps to a real fixture.

2. **API signature parity**
- Function name and call shape match.
- Config option names match.
- Required vs optional options match.

3. **Context parity**
- Docs "Adds to context" section matches properties actually written to context.
- Docs "Context requirements" match runtime prerequisites in code.

4. **Lifecycle parity**
- Docs "Lifecycle" and caveats match startup/teardown behavior in code (`start`, `stop`, `finally`, ownership rules).

5. **Terminology parity**
- Keep terms stable across code and docs:
- `chainKey` for runtime keying
- `chain` for chain selection in chain-scoped wallet/token fixtures
- `runtimeMode`, `publicClient`, `altPublicClient`, `walletClient`, `testClient`
- Avoid introducing synonyms for the same concept.

## Workflow

1. Build a quick fixture inventory from implementation and docs.
2. Compare each `withX` pair for API signature, options, context, lifecycle, and terms.
3. Fix docs first when code is correct.
4. Fix implementation if docs reveal an actual API bug.
5. Re-check the full fixture set once after edits.
6. Summarize any intentional deviations.

## Output Format

When applying this skill, produce:

```markdown
## Consistency Report
- Fixture coverage: X/Y implemented fixtures documented
- Missing docs: ...
- Orphan docs: ...
- Option mismatches: ...
- Terminology mismatches: ...
- Lifecycle/context mismatches: ...

## Changes Made
- ...

## Remaining Risks
- ...
```

## References

- [Fixture consistency reference](reference.md)
49 changes: 49 additions & 0 deletions .cursor/skills/impl-doc-consistency/reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Fixture Consistency Reference

## Expected doc file shape

Each fixture page should keep these sections in order:

1. Title and one-line description (frontmatter)
2. Signature heading (`##` with `withX(...)`)
3. `### Why it is useful`
4. `### Example`
5. `### Options` table
6. `### Adds to context`
7. `### Context requirements`
8. `### Lifecycle`
9. `### Notes and caveats`

## Current fixture surface (core)

- `withChain`
- `withFork`
- `withExternalRuntime`
- `withMultiChain`
- `withSnapshot`
- `withFundedWallet`
- `withImpersonation`
- `withErc20Balance`
- `withContracts`
- `withDeployments`
- `withBundler`
- `withBridge`

## Terminology conventions

- **Runtime keying:** use `chainKey`
- **Chain selection in chain-scoped fixtures:** use `chain`
- **Context map:** `ctx.chains[<key>]`
- **Client aliases:** `publicClient`, `altPublicClient`
- **Chain clients:** `publicClient`, `walletClient`, `testClient`
- **Modes:** `runtimeMode` with `"chain"` or `"fork"`

## Naming note

Config types are typically `WithXConfig`.

Current exception in implementation:

- `withImpersonationConfig` (lowercase leading `w`)

If this remains intentional, docs and exports should keep it consistent and avoid introducing a conflicting type name.
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@ Build small, composable pieces:
- `withSmartAccount`
- `withPaymaster`

## Multi-chain (future)
## Multi-chain

Represent as namespaced contexts:
`ctx.chains.ethereum`, `ctx.chains.base`.
Do not simulate cross-chain logic in v1.
Represent chain state as namespaced contexts: `ctx.chains.<key>` (for example `ctx.chains.ethereum`, `ctx.chains.base`).
Use `withMultiChain` for multiple runtimes in one scenario, or repeat single-chain fixtures with different `chainKey` values.
Do not simulate cross-chain messaging or bridges in v1 unless you add explicit test doubles for that scope.

## Development Priorities

Expand Down
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ test(
withFundedWallet({
balance: 1_000_000_000_000_000_000n, // 1 ETH in wei
}),
async ({ wallet, publicClient }) => {
const balance = await publicClient.getBalance({ address: wallet });
async ({ chains, publicClient }) => {
const balance = await publicClient.getBalance({ address: chains!.default.wallet! });
expect(balance).toBe(1_000_000_000_000_000_000n);
},
),
Expand Down Expand Up @@ -100,12 +100,12 @@ test(
token: USDC_MAINNET,
amount: 1_000_000n, // 1 USDC (6 decimals)
}),
async ({ walletClient, publicClient }) => {
async ({ chains, publicClient }) => {
const usdc = await publicClient.readContract({
address: USDC_MAINNET,
abi: erc20Abi,
functionName: "balanceOf",
args: [walletClient.account.address],
args: [chains!.default.wallet!],
});

expect(usdc).toBe(1_000_000n);
Expand All @@ -117,13 +117,15 @@ test(
## Core Primitives

- `scenario(...steps, testFn)`: composes setup steps into one async test function (examples wrap it with Vitest `test`).
- `withChain()`: starts a fresh local Anvil runtime.
- `withChain()`: starts a fresh local Anvil runtime (under `ctx.chains.default` unless `chainKey` is set).
- `withFork({ rpcUrl, blockNumber })`: starts a pinned local fork for deterministic mainnet state.
- `withFundedWallet({ balance, erc20? })`: creates and funds a test wallet.
- `withMultiChain({ ... })`: starts or attaches multiple named chains on `ctx.chains`.
- `publicClient` / `altPublicClient`: top-level convenience aliases for the primary chain and optional secondary chain.
- `withFundedWallet({ balance, erc20?, chain? })`: creates and funds a test wallet on a chain entry.
- `withErc20Balance({ token, amount })`: seeds ERC-20 balance on compatible local or forked nodes.
- `withSnapshot()`: snapshots before inner steps and reverts in `finally`.
- `withContracts(...)`: injects runtime bytecode at known addresses.
- `withDeployments(...)`: performs real deployments with constructor semantics.
- `withContracts({ contracts, chain? })`: injects runtime bytecode at known addresses on a chain entry.
- `withDeployments({ deployments, chain? })`: performs real deployments with constructor semantics on a chain entry.

## Use It When / Skip It When

Expand Down Expand Up @@ -156,7 +158,8 @@ bun run test

Included examples in `packages/examples/examples/scenarios.test.ts`:

- fresh local chain plus funded wallet
- fresh local chain plus funded wallet (`ctx.chains.default`)
- two local chains via `withMultiChain` plus per-chain funded wallets
- forked mainnet plus funded wallet plus real contract call
- forked mainnet plus funded wallet plus USDC via `withFundedWallet.erc20` or `withErc20Balance`
- runtime bytecode injection with `withContracts`
Expand Down
121 changes: 121 additions & 0 deletions docs/pages/core-api-agent.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
title: Core API (Agent)
description: Machine-readable contracts for agent planning, preflight simulation, execution, constraints, and error handling.
---

# Core API (Agent)

This page documents the most agent-relevant contracts in Statecraft: explicit constraints, structured errors, preflight APIs, and execution hooks.

## Global constraints

```ts
import { describeScenarioConstraints } from "@st8craft/core";

const constraints = describeScenarioConstraints();
// {
// maxChains: 2,
// publicClientAliasPolicies: ["prefer-default-then-lexical", "lexical"],
// defaultAliasPolicy: "prefer-default-then-lexical",
// supportedBundlerModes: ["alto"],
// }
```

## Structured errors

Statecraft throws `StatecraftError` for machine-branchable failures.

```ts
type StatecraftError = {
name: "StatecraftError";
code: string;
reason: string;
context: Record<string, unknown>;
suggestedAction?: string;
cause?: unknown;
};
```

### Common error codes

| Code | Meaning | Typical next action |
| --- | --- | --- |
| `SC_CONTEXT_MISSING` | Required context key/chain client not present | Reorder fixtures so prerequisites run first |
| `SC_PRECONDITION_FAILED` | Invalid argument or unmet runtime precondition | Fix config/inputs and retry |
| `SC_CONSTRAINT_VIOLATION` | Violates SDK constraints (for example >2 chains) | Split workflow or use valid configuration |
| `SC_RUNTIME_START_TIMEOUT` | Runtime did not become JSON-RPC ready | Retry and inspect startup diagnostics |
| `SC_RUNTIME_START_FAILED` | Runtime process failed during startup | Validate process/env and config |
| `SC_BUNDLER_START_FAILED` | Bundler startup or readiness probe failed | Inspect Alto install/config and retry |
| `SC_BUNDLER_UNSUPPORTED_MODE` | Bundler mode is unsupported | Use `mode: "alto"` |

## Action planning + preflight

Use action primitives to separate plan/simulate/execute.

```ts
import {
planCall,
simulateCall,
planDeployment,
simulateDeployment,
executePlan,
summarizePreflight,
assertPreflight,
} from "@st8craft/core";
```

### Preflight result contract

```ts
type ActionPreflight = {
canExecute: boolean;
reasons: Array<{
code: string;
reason: string;
context?: Record<string, unknown>;
}>;
assumptions: string[];
estimatedEffects: Record<string, unknown>;
};
```

Recommended flow:

1. Build plan (`planCall` / `planDeployment`)
2. Simulate (`simulateCall` / `simulateDeployment`)
3. Gate mutation with `assertPreflight(result)` or your own policy
4. Execute (`executePlan`)

## Scenario tracing hooks

`scenario(...)` accepts optional run options:

```ts
scenario(
{
options: {
onStepStart: (event) => {},
onStepSuccess: (event) => {},
onStepFailure: (event) => {},
onCleanup: (event) => {},
},
},
// ...withX steps,
async (ctx) => {},
);
```

Event payloads expose:

- step index + label
- duration
- context delta keys
- normalized `StatecraftError` on failures

## Retry and idempotency

- `bridge.execute(...)` accepts optional `idempotencyKey`.
- Reusing the same key with the same payload returns cached execution output.
- Reusing the same key with a different payload throws `SC_CONSTRAINT_VIOLATION`.

For nonce-sensitive operations (raw tx/deployments), use external nonce coordination if retries are automated.
2 changes: 2 additions & 0 deletions docs/pages/fixtures/contracts/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ description: Inject runtime bytecode at fixed addresses with withContracts, or d

These fixtures set up contract code at deterministic addresses on your scenario runtime.

Pick `withContracts` when fixed addresses and setup speed matter most, or `withDeployments` when constructor args, deployment order, and deployment-time behavior are part of the test intent.

## Fixtures

- [`withContracts({ ... })` (bytecode injection)](/fixtures/contracts/withContracts)
Expand Down
Loading
Loading