@firfi/quint-connect
Version:
Model-based testing framework connecting Quint specifications to TypeScript implementations
300 lines (219 loc) • 9.09 kB
Markdown
---
name: quint-connect-ts-itf-decoding
description: >
Decode Quint ITF JSON encoding to native JS types for quint-connect drivers
and state checks. ITFBigInt for {"#bigint":"N"} to bigint, ITFSet for
{"#set":[...]} to Set, ITFMap for {"#map":[[k,v],...]} to Map, ITFTuple,
ITFVariant, ItfOption for Quint Option (Some/None to T | undefined).
Fully-qualified state variable names ("testmod::specmod::var"). Zod
(@firfi/quint-connect/zod) and Effect Schema (@firfi/quint-connect/effect)
variants. transformITFValue. Structural deep comparison for Maps and Sets.
Use when writing ITF decoders, debugging decode errors, or comparing
complex state.
type: core
library: quint-connect-ts
library_version: "0.6.0 (Effect 3, @latest) / 1.0.0-effect4 (Effect 4, @effect4)"
sources:
- "dearlordylord/quint-connect-ts:README.md"
- "dearlordylord/quint-connect-ts:src/itf/schema.ts"
- "dearlordylord/quint-connect-ts:src/zod.ts"
- "dearlordylord/quint-connect-ts:test/itf-schema.test.ts"
- "dearlordylord/quint-connect-ts:test/itf-picks.test.ts"
---
# quint-connect-ts -- ITF Type Decoding
Quint outputs state and picks in ITF (Informal Trace Format) JSON encoding. Each Quint type has a specific JSON representation that must be decoded to native JS types before use.
## ITF Type Mapping
| Quint type | ITF JSON | JS type | Schema (Effect) | Schema (Zod) |
|---|---|---|---|---|
| `int` | `{"#bigint":"42"}` | `bigint` | `ITFBigInt` | `ITFBigInt` |
| `str` | `"hello"` | `string` | `Schema.String` | `z.string()` |
| `bool` | `true` | `boolean` | `Schema.Boolean` | `z.boolean()` |
| `Set(T)` | `{"#set":[...]}` | `Set<T>` | `ITFSet(inner)` | `ITFSet(inner)` |
| `T -> U` (Map) | `{"#map":[[k,v],...]}` | `Map<K,V>` | `ITFMap(k, v)` | `ITFMap(k, v)` |
| `(T1, T2)` (Tuple) | `{"#tup":[...]}` | `[T1, T2]` | `ITFTuple(...)` | `z.tuple([...])` |
| `{ f: T }` (Record) | `{"f": ...}` | `{ f: T }` | `Schema.Struct({...})` | `z.object({...})` |
| `Option(T)` | `{tag:"Some",value:...}` / `{tag:"None",...}` | `T \| undefined` | `ItfOption(inner)` | -- |
All Quint `int` values become `bigint`, never `number`. Use `0n` literals.
## Effect 3 vs Effect 4
This only matters if the project uses `effect` as a dependency. The Zod / Simple API is identical across both versions. Install the correct dist-tag: `@firfi/quint-connect@latest` for `effect@^3`, `@firfi/quint-connect@effect4` for `effect@^4`. See `quint-connect-ts-setup` skill for full details and API difference table.
The Effect Schema examples below use Effect 3 syntax. For Effect 4, the ITF schema imports (`ITFBigInt`, `ITFSet`, `ITFMap`, `ItfOption`) are the same — only the underlying Effect/Schema API calls differ (see setup skill).
## Setup -- Effect Schema
```ts
import { ITFBigInt, ITFSet, ITFMap, ItfOption } from "@firfi/quint-connect/effect"
import { Schema } from "effect"
const CacheState = Schema.Struct({
data: ITFMap(Schema.String, ITFBigInt),
keys: ITFSet(Schema.String),
limit: ITFBigInt,
pending: ItfOption(Schema.String),
})
```
## Setup -- Zod
```ts
import { ITFBigInt, ITFSet, ITFMap } from "@firfi/quint-connect/zod"
import { z } from "zod"
const CacheState = z.object({
data: ITFMap(z.string(), z.bigint()),
keys: ITFSet(z.string()),
limit: z.bigint(),
})
```
ITF values (`{"#bigint":"5"}`) are automatically transformed to native types (`5n`) by `transformITFValue` before schema validation. For Zod state schemas, use `z.bigint()` (not `ITFBigInt`) because the transformation already happened. For picks in `defineDriver`, use `ITFBigInt` from `@firfi/quint-connect/zod`.
## Core Patterns
### Compose schemas for nested Quint types
Quint: `Set(int) -> int` (a map from sets of ints to ints):
```ts
import { ITFBigInt, ITFSet, ITFMap } from "@firfi/quint-connect/effect"
const ComplexState = Schema.Struct({
lookup: ITFMap(ITFSet(ITFBigInt), ITFBigInt),
})
```
### Use fully-qualified state variable names
ITF state keys include module path. Run `quint run --mbt` manually to see actual keys:
```bash
npx @informalsystems/quint run --mbt --max-samples 1 --max-steps 1 specs/cache.qnt
```
Output shows keys like `"cache_test::cache::data"`, not `"data"`.
```ts
const State = Schema.Struct({
"cache_test::cache::data": ITFMap(Schema.String, ITFBigInt),
"cache_test::cache::keys": ITFSet(Schema.String),
})
```
Alternative: use `config.statePath` to scope the state:
```ts
return {
getState: () => ({ data, keys }),
config: () => ({ statePath: ["cache_test::cache::data"] }),
}
```
### Write a structural comparator for Maps
```ts
import { stateCheck } from "@firfi/quint-connect"
stateCheck(
(raw) => CacheState.parse(raw),
(spec, impl) => {
if (spec.data.size !== impl.data.size) return false
for (const [k, v] of spec.data) {
if (!impl.data.has(k) || impl.data.get(k) !== v) return false
}
return spec.limit === impl.limit
},
)
```
### Decode driver picks (action nondet values)
Per-field schemas in `defineDriver` decode the picks for each action:
```ts
import { defineDriver } from "@firfi/quint-connect"
import { ITFBigInt, ITFSet } from "@firfi/quint-connect/zod"
const driver = defineDriver(
{
Insert: { key: z.string(), value: ITFBigInt },
BatchDelete: { keys: ITFSet(z.string()) },
},
() => {
const data = new Map<string, bigint>()
return {
Insert: ({ key, value }) => { data.set(key, value) },
BatchDelete: ({ keys }) => { for (const k of keys) data.delete(k) },
getState: () => ({ data: new Map(data) }),
}
}
)
```
## Common Mistakes
### CRITICAL Hallucinate ITFInt or ITFNumber
Wrong:
```ts
import { ITFInt } from "@firfi/quint-connect/effect"
const State = Schema.Struct({ count: ITFInt })
```
Correct:
```ts
import { ITFBigInt } from "@firfi/quint-connect/effect"
const State = Schema.Struct({ count: ITFBigInt })
```
No `ITFInt`, `ITFNumber`, or `ITFInteger` exists. All Quint integers are `{"#bigint":"N"}` and decode to `bigint` via `ITFBigInt`.
Source: src/itf/schema.ts
### CRITICAL Use Schema.Number or z.number() on ITF integer values
Wrong:
```ts
const State = Schema.Struct({ count: Schema.Number })
```
Correct:
```ts
const State = Schema.Struct({ count: ITFBigInt })
```
ITF integers are `{"#bigint":"42"}`, not raw numbers. `Schema.Number` silently fails or throws a cryptic decode error.
Source: src/itf/schema.ts, README.md
### HIGH Use short variable names in state schema
Wrong:
```ts
// Quint spec has: module cache_test { import cache.* }
const State = Schema.Struct({
data: ITFMap(Schema.String, ITFBigInt),
})
```
Correct:
```ts
const State = Schema.Struct({
"cache_test::cache::data": ITFMap(Schema.String, ITFBigInt),
})
```
ITF state keys are fully qualified with module path. Short names decode to `undefined` with no error. Run `quint run --mbt` manually first to see actual keys.
Source: test/runner.test.ts
### CRITICAL Use === or JSON.stringify for Map/Set comparison
Wrong:
```ts
stateCheck(deserialize, (spec, impl) => spec.data === impl.data)
// or:
stateCheck(deserialize, (spec, impl) =>
JSON.stringify(spec) === JSON.stringify(impl)
)
```
Correct:
```ts
stateCheck(deserialize, (spec, impl) => {
if (spec.data.size !== impl.data.size) return false
for (const [k, v] of spec.data) {
if (!impl.data.has(k) || impl.data.get(k) !== v) return false
}
return true
})
```
`===` on Maps/Sets is reference equality (always false for distinct instances). `JSON.stringify` on a Map produces `"{}"`. Both silently produce wrong comparisons.
Source: maintainer interview
### MEDIUM Number(bigint) without bounds checking
Wrong:
```ts
Increment: ({ amount }) => {
count += Number(amount)
}
```
Correct:
```ts
Increment: ({ amount }) => {
count += amount // keep as bigint throughout
}
```
Quint `bigint` can exceed `Number.MAX_SAFE_INTEGER`. Small test constants (1-30) work, but if the spec evolves to larger values, silent precision loss causes false-positive state matches.
Source: maintainer interview
### MEDIUM Write custom Schema.transform instead of using provided schemas
Wrong:
```ts
const MyBigInt = Schema.transform(
Schema.Struct({ "#bigint": Schema.String }),
Schema.BigIntFromSelf,
{ decode: (v) => BigInt(v["#bigint"]), encode: (n) => ({ "#bigint": n.toString() }) }
)
```
Correct:
```ts
import { ITFBigInt } from "@firfi/quint-connect/effect"
// Use the provided schema directly — no custom transform needed
```
The library provides tested ITF schemas. Custom transforms duplicate logic, require encode branches that never run, and mislead readers.
Source: src/itf/schema.ts
### HIGH Tension: type safety vs ITF decoding complexity
Fully typed ITF decoding is verbose (fully-qualified keys, `ITFBigInt` everywhere). Shortcuts (raw JSON access, short variable names) produce cleaner code but silently decode to `undefined` or wrong types at runtime. Always use the ITF schemas and verify key names by running `quint run --mbt` manually.
See also: quint-connect-ts-debug/SKILL.md -- ITF decode errors surface as TraceReplayError