feat: add user-story-conversation skill and fast-check integration
Add the user-story-conversation Pi skill following Ron Jeffries' Card, Conversation, Confirmation mantra enhanced with Matt Wynne's Example Mapping. The skill guides users through six steps: 1. Card — user story 2. Conversation — Example Mapping (Rules, Examples, Questions, Answers) 3. Allium Spec — formal behavioural specification 4. fast-check Properties — executable verification 5. TypeScript ADTs — value objects and discriminated unions 6. Confirmation — verify all properties pass Also update AGENTS.md with workflow guidance and add the new skill to the Allium routing table. Add fast-check as a dependency.
This commit is contained in:
parent
6c57136e7f
commit
9a2181318e
@ -34,6 +34,7 @@ Allium does NOT specify programming language or framework choices, database sche
|
||||
| Modifying an existing spec | `/skill:tend` | User wants targeted changes to `.allium` files |
|
||||
| Checking spec-to-code alignment | `/skill:weed` | User wants to find or fix divergences between spec and implementation |
|
||||
| Generating tests from a spec | `/skill:propagate` | User wants to generate tests, PBT properties or state machine tests from a specification |
|
||||
| Full Card→Conversation→Confirmation workflow | `/skill:user-story-conversation` | User wants to walk through a user story with Example Mapping, Allium specs, and fast-check properties |
|
||||
|
||||
## Quick syntax summary
|
||||
|
||||
|
||||
426
.pi/skills/user-story-conversation/SKILL.md
Normal file
426
.pi/skills/user-story-conversation/SKILL.md
Normal file
@ -0,0 +1,426 @@
|
||||
---
|
||||
name: user-story-conversation
|
||||
description: Guide users through Ron Jeffries' Card, Conversation, Confirmation workflow using Example Mapping, Allium specs, and fast-check properties.
|
||||
disable-model-invocation: true
|
||||
license: MIT
|
||||
metadata:
|
||||
methodology: example-mapping
|
||||
references:
|
||||
- https://www.examplerelated.com/
|
||||
- https://www.ronjeffries.com/
|
||||
---
|
||||
|
||||
# User Story Conversation
|
||||
|
||||
A skill for turning user stories into formal specifications and executable properties, following Ron Jeffries' **Card, Conversation, Confirmation** mantra enhanced with Matt Wynne's **Example Mapping**.
|
||||
|
||||
## The Six Steps
|
||||
|
||||
```
|
||||
Card → User story (the "what")
|
||||
Conversation → Example Mapping (the "understanding")
|
||||
→ Allium Spec (the "formal model")
|
||||
Confirmation → fast-check Properties (the "executable verification")
|
||||
→ TypeScript ADTs (the "type-safe implementation")
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
||||
- Starting a new feature or user story
|
||||
- Clarifying ambiguous requirements
|
||||
- Breaking down a complex user story into testable properties
|
||||
- Aligning the team on domain understanding before implementation
|
||||
|
||||
## How to Run
|
||||
|
||||
1. **Start with the Card** — Ask the user for the user story (or use an existing one from `user-stories.md`)
|
||||
2. **Example Map** — Work through the story together, identifying Rules, Examples, Questions, and Answers
|
||||
3. **Allium Spec** — Formalize the rules and answers into a `.allium` spec
|
||||
4. **fast-check Properties** — Translate invariants into property-based tests
|
||||
5. **TypeScript ADTs** — Implement with value objects and discriminated unions
|
||||
6. **Confirmation** — Verify the implementation satisfies all properties
|
||||
|
||||
## Step 1: Card
|
||||
|
||||
Present the user story as a card. Use the format:
|
||||
|
||||
```
|
||||
As a [role]
|
||||
I want [feature]
|
||||
So that [benefit]
|
||||
```
|
||||
|
||||
If the user already has a user story (e.g., from `user-stories.md`), use that. Otherwise, help them write one.
|
||||
|
||||
## Step 2: Conversation — Example Mapping
|
||||
|
||||
Work through the user story using Example Mapping. For each rule, identify:
|
||||
|
||||
### Rules (yellow) — Domain concepts
|
||||
- Nouns and concepts in the domain
|
||||
- What the system manages
|
||||
- Example: "Health", "Damage", "Character", "Level", "Faction"
|
||||
|
||||
### Examples (blue) — Concrete scenarios
|
||||
- Specific instances of rules
|
||||
- Should be testable
|
||||
- Example: "Character with 500 health takes 200 damage → 300 health"
|
||||
|
||||
### Questions (pink) — Ambiguities
|
||||
- Things we don't know or aren't sure about
|
||||
- Example: "What happens when damage exceeds health?"
|
||||
|
||||
### Answers (green) — Resolved questions
|
||||
- Write directly on the pink sticky
|
||||
- Example: "Health becomes 0, character dies"
|
||||
|
||||
### Example Mapping Output Format
|
||||
|
||||
Create a structured output like this:
|
||||
|
||||
```markdown
|
||||
## Example Map: [User Story Title]
|
||||
|
||||
### Rules
|
||||
1. [Rule description]
|
||||
2. [Rule description]
|
||||
|
||||
### Examples
|
||||
- [Example 1]
|
||||
- [Example 2]
|
||||
|
||||
### Questions
|
||||
- [Question 1]
|
||||
- [Question 2]
|
||||
|
||||
### Answers
|
||||
- [Answer to Question 1]
|
||||
- [Answer to Question 2]
|
||||
```
|
||||
|
||||
## Step 3: Allium Spec
|
||||
|
||||
Formalize the rules, examples, and answers into an Allium spec. Use the skills in this repository:
|
||||
|
||||
- `/skill:elicit` — if you need to explore requirements further
|
||||
- `/skill:tend` — if you need to evolve an existing spec
|
||||
- `/skill:allium` — for language reference
|
||||
|
||||
### Spec Structure
|
||||
|
||||
Create a `.allium` file with:
|
||||
|
||||
```allium
|
||||
-- allium: 3
|
||||
|
||||
-- allium: [module-name]
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Entities and Variants
|
||||
------------------------------------------------------------
|
||||
|
||||
entity Character {
|
||||
name: String
|
||||
health: Health
|
||||
status: alive | dead
|
||||
level: Level
|
||||
factions: Set<Faction>
|
||||
}
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Rules
|
||||
------------------------------------------------------------
|
||||
|
||||
rule DamageReducesHealth {
|
||||
when: CharacterTakesDamage(character, damage)
|
||||
ensures: character.health = character.health - damage
|
||||
}
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Invariants
|
||||
------------------------------------------------------------
|
||||
|
||||
invariant HealthNonNegative {
|
||||
for c in Characters:
|
||||
c.health >= 0
|
||||
}
|
||||
|
||||
invariant DeathAtZeroHealth {
|
||||
for c in Characters:
|
||||
c.health = 0 implies c.status = dead
|
||||
}
|
||||
```
|
||||
|
||||
### Key Principles
|
||||
|
||||
- **Entities** represent domain objects with identity
|
||||
- **Rules** describe state transitions and behaviour
|
||||
- **Invariants** capture properties that must always hold
|
||||
- **Enums** and **value types** for constrained data
|
||||
- **Rules** should be testable — each one maps to a property
|
||||
|
||||
## Step 4: fast-check Properties
|
||||
|
||||
Translate Allium invariants and rules into fast-check properties.
|
||||
|
||||
### Property Patterns
|
||||
|
||||
#### Invariant Properties
|
||||
|
||||
```typescript
|
||||
import fc from 'fast-check';
|
||||
|
||||
// "Health is never negative"
|
||||
fc.property(
|
||||
fc.integer({ min: 0, max: 10000 }),
|
||||
(health) => {
|
||||
const c = new Character({ health: Health.create(health) });
|
||||
return c.health.value >= 0;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
#### Rule Properties (State Transitions)
|
||||
|
||||
```typescript
|
||||
// "Damage reduces health, capped at 0"
|
||||
fc.property(
|
||||
fc.integer({ min: 0, max: 10000 }),
|
||||
fc.integer({ min: 0, max: 10000 }),
|
||||
(health, damage) => {
|
||||
const c = new Character({ health: Health.create(health) });
|
||||
c.takeDamage(Damage.create(damage));
|
||||
return c.health.value === Math.max(0, health - damage);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
#### Edge Case Properties
|
||||
|
||||
```typescript
|
||||
// "Zero damage changes nothing"
|
||||
fc.property(
|
||||
fc.integer({ min: 0, max: 10000 }),
|
||||
(health) => {
|
||||
const c = new Character({ health: Health.create(health) });
|
||||
c.takeDamage(Damage.create(0));
|
||||
return c.health.value === health;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Property Naming
|
||||
|
||||
Name properties after the rule or invariant they verify:
|
||||
|
||||
```typescript
|
||||
describe('DamageReducesHealth', () => {
|
||||
it('property: health is reduced by damage amount', () => { ... });
|
||||
it('property: health never goes below zero', () => { ... });
|
||||
it('property: character dies when health reaches zero', () => { ... });
|
||||
});
|
||||
```
|
||||
|
||||
## Step 5: TypeScript ADTs
|
||||
|
||||
Implement with "I can't believe it's not Haskell" principles:
|
||||
|
||||
### Discriminated Unions (ADTs)
|
||||
|
||||
```typescript
|
||||
type Status = { kind: 'alive' } | { kind: 'dead' };
|
||||
|
||||
type FactionMember = { kind: 'member'; faction: Faction };
|
||||
type FactionNeutral = { kind: 'neutral' };
|
||||
type FactionMembership = FactionMember | FactionNeutral;
|
||||
```
|
||||
|
||||
### Value Objects
|
||||
|
||||
```typescript
|
||||
class Health {
|
||||
private constructor(private readonly value: number) {}
|
||||
|
||||
static create(n: number): Health {
|
||||
if (n < 0) throw new Error('Health cannot be negative');
|
||||
return new Health(n);
|
||||
}
|
||||
|
||||
static maxForLevel(level: number): number {
|
||||
return level >= 6 ? 1500 : 1000;
|
||||
}
|
||||
|
||||
get value(): number { return this.value; }
|
||||
|
||||
add(amount: number): Health {
|
||||
return Health.create(Math.min(this.value + amount, Health.maxForLevel(this.level.value)));
|
||||
}
|
||||
|
||||
sub(amount: number): Health {
|
||||
return Health.create(Math.max(0, this.value - amount));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Immutable Entities
|
||||
|
||||
```typescript
|
||||
class Character {
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly health: Health,
|
||||
readonly status: Status,
|
||||
readonly level: Level,
|
||||
readonly factions: ReadonlySet<Faction>,
|
||||
) {}
|
||||
|
||||
// Pure functions — no mutation
|
||||
takeDamage(damage: Damage): Character {
|
||||
const newHealth = this.health.sub(damage.value);
|
||||
const newStatus = newHealth.value === 0 ? { kind: 'dead' as const } : this.status;
|
||||
return new Character(this.name, newHealth, newStatus, this.level, this.factions);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key Principles
|
||||
|
||||
- **ADTs over classes** — discriminated unions for state/variants
|
||||
- **Value objects over primitives** — `Health`, `Damage`, `Level` instead of `number`
|
||||
- **Immutability** — return new instances, never mutate
|
||||
- **Invariants at construction** — invalid states are unrepresentable
|
||||
- **Pure functions** — domain logic has no side effects
|
||||
|
||||
## Step 6: Confirmation
|
||||
|
||||
Verify that the implementation satisfies all properties:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
All properties should pass. If any fail, the property reveals a gap in understanding — revisit the Example Map or Allium spec.
|
||||
|
||||
## Output Artifacts
|
||||
|
||||
After completing the workflow, produce:
|
||||
|
||||
1. **Example Map** — Markdown file with structured domain knowledge
|
||||
2. **Allium Spec** — `.allium` file with formal behavioural specification
|
||||
3. **fast-check Properties** — Test files with property-based tests
|
||||
4. **TypeScript Implementation** — ADTs, value objects, and pure functions
|
||||
|
||||
## Example: Full Workflow
|
||||
|
||||
Here's a complete example walking through "Characters can Deal Damage":
|
||||
|
||||
### Card
|
||||
|
||||
> Characters can Deal Damage to Characters.
|
||||
|
||||
### Example Map
|
||||
|
||||
**Rules:**
|
||||
1. Damage is subtracted from Health
|
||||
2. When damage exceeds health, health becomes 0 and character dies
|
||||
3. A Character cannot Deal Damage to itself
|
||||
|
||||
**Examples:**
|
||||
- Character with 1000 health takes 200 damage → 800 health
|
||||
- Character with 100 health takes 200 damage → 0 health, dead
|
||||
- Character tries to deal damage to self → no effect
|
||||
|
||||
**Questions:**
|
||||
- Can a dead character take damage?
|
||||
- Does damage stack across multiple attacks?
|
||||
|
||||
**Answers:**
|
||||
- Dead characters cannot take damage (they're already dead)
|
||||
- Yes, damage stacks (each attack reduces health further)
|
||||
|
||||
### Allium Spec
|
||||
|
||||
```allium
|
||||
entity Character {
|
||||
name: String
|
||||
health: Health
|
||||
status: alive | dead
|
||||
}
|
||||
|
||||
rule DamageReducesHealth {
|
||||
when: CharacterTakesDamage(character, damage)
|
||||
requires: character.status = alive
|
||||
ensures: character.health = character.health - damage
|
||||
ensures:
|
||||
if character.health <= 0:
|
||||
character.status = dead
|
||||
}
|
||||
|
||||
invariant HealthNonNegative {
|
||||
for c in Characters:
|
||||
c.health >= 0
|
||||
}
|
||||
|
||||
invariant SelfDamageForbidden {
|
||||
for a in Characters, t in Characters:
|
||||
a.dealDamage(t, _) implies a.name != t.name
|
||||
}
|
||||
```
|
||||
|
||||
### fast-check Properties
|
||||
|
||||
```typescript
|
||||
describe('DamageReducesHealth', () => {
|
||||
it('property: health is reduced by damage', () => { ... });
|
||||
it('property: health never goes below zero', () => { ... });
|
||||
it('property: character dies when health reaches zero', () => { ... });
|
||||
it('property: dead characters cannot take damage', () => { ... });
|
||||
it('property: self-damage is forbidden', () => { ... });
|
||||
});
|
||||
```
|
||||
|
||||
### TypeScript Implementation
|
||||
|
||||
```typescript
|
||||
type Status = { kind: 'alive' } | { kind: 'dead' };
|
||||
|
||||
class Health {
|
||||
static create(n: number): Health { ... }
|
||||
sub(amount: number): Health { ... }
|
||||
}
|
||||
|
||||
class Character {
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly health: Health,
|
||||
readonly status: Status,
|
||||
) {}
|
||||
|
||||
dealDamage(target: Character, damage: Damage): void {
|
||||
if (this.name === target.name) return; // self-damage forbidden
|
||||
if (target.status.kind === 'dead') return; // dead can't take damage
|
||||
const newHealth = target.health.sub(damage.value);
|
||||
const newStatus = newHealth.value === 0 ? { kind: 'dead' as const } : target.status;
|
||||
// ... update target
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Allium Skills
|
||||
|
||||
This skill works alongside the Allium skills:
|
||||
|
||||
- **Before starting** — Use `/skill:allium` to understand the language
|
||||
- **During spec writing** — Use `/skill:tend` to evolve specs, `/skill:propagate` to generate test obligations
|
||||
- **After implementation** — Use `/skill:weed` to check spec-code alignment
|
||||
- **When clarifying requirements** — Use `/skill:elicit` to explore with stakeholders
|
||||
|
||||
## Tips
|
||||
|
||||
- **Start small** — Map one user story at a time
|
||||
- **Examples first** — Concrete examples help surface ambiguities
|
||||
- **Properties before implementation** — Write fast-check properties before code
|
||||
- **Let properties guide design** — If a property is hard to write, the design needs work
|
||||
- **Iterate** — Example Mapping is a conversation, not a document. Revise as understanding deepens
|
||||
- **Use the Allium CLI** — Run `allium check` to validate specs
|
||||
- **Run fast-check with many iterations** — Use `fc.property(...).check({ numRuns: 1000 })` for thorough testing
|
||||
131
AGENTS.md
131
AGENTS.md
@ -2,12 +2,139 @@
|
||||
|
||||
## What this project is
|
||||
|
||||
An implementation of the RPG Combat rules engine. There are six user stories described in [user-stories.md](user-stories.md)
|
||||
An implementation of the RPG Combat rules engine. There are six user stories described in [user-stories.md](user-stories.md). We use a **spec-first, property-based testing** approach.
|
||||
|
||||
## Build and Test Scripts
|
||||
|
||||
- `npm test`: runs unit tests using vitest
|
||||
- `npm test`: runs unit tests using vitest + fast-check
|
||||
- `npm run lint:fix`: runs eslint with autofix
|
||||
- `npm run format:fix`: runs prettier with autofix
|
||||
- `npm run typecheck`: runs tsc without emit
|
||||
- `npm run checks`: runs the pre-commit gate (format:fix, lint:fix, typecheck, test)
|
||||
|
||||
## Allium + fast-check Workflow
|
||||
|
||||
This project combines three practices:
|
||||
|
||||
1. **Allium** (`.allium` specs) — formal behavioural specifications that capture *what* the system does
|
||||
2. **fast-check** — property-based testing that verifies those properties hold across thousands of random inputs
|
||||
3. **"I can't believe it's not Haskell"** — TypeScript with ADTs, value objects, and immutability
|
||||
|
||||
### Step 1: Spec with Allium
|
||||
|
||||
Use the Allium skills to formalize user stories into `.allium` specs:
|
||||
- `/skill:elicit` — explore requirements with stakeholders
|
||||
- `/skill:distill` — extract specs from existing code
|
||||
- `/skill:tend` — evolve specs as understanding deepens
|
||||
|
||||
The spec captures **invariants** (always-true properties) and **rules** (state transitions). These become the source of truth for your properties.
|
||||
|
||||
### Step 2: Properties with fast-check
|
||||
|
||||
Translate Allium invariants and rules into fast-check properties. Each invariant becomes a property:
|
||||
|
||||
```typescript
|
||||
import fc from 'fast-check';
|
||||
import { Character } from './domain';
|
||||
|
||||
// Invariant: "A character's health is never negative"
|
||||
fc.property(
|
||||
fc.integer({ min: 0, max: 1000 }),
|
||||
(initialHealth) => {
|
||||
const c = new Character({ name: 'hero', health: initialHealth });
|
||||
return c.health >= 0;
|
||||
}
|
||||
);
|
||||
|
||||
// Property: "Dealing damage reduces health, capped at 0"
|
||||
fc.property(
|
||||
fc.record({
|
||||
attacker: fc.character(),
|
||||
target: fc.character(),
|
||||
damage: fc.integer({ min: 1, max: 5000 }),
|
||||
}),
|
||||
({ attacker, target, damage }) => {
|
||||
const a = new Character({ name: attacker, health: 1000 });
|
||||
const t = new Character({ name: target, health: 1000 });
|
||||
a.dealDamage(t, damage);
|
||||
return t.health === Math.max(0, 1000 - damage);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Step 3: "I can't believe it's not Haskell"
|
||||
|
||||
Use TypeScript's type system to encode domain constraints:
|
||||
|
||||
```typescript
|
||||
// ADTs via discriminated unions
|
||||
type Status = { kind: 'alive' } | { kind: 'dead' };
|
||||
|
||||
// Value objects with invariants enforced at construction
|
||||
class Health {
|
||||
private constructor(private readonly value: number) {}
|
||||
static create(n: number): Health {
|
||||
if (n < 0) throw new Error('Health cannot be negative');
|
||||
return new Health(n);
|
||||
}
|
||||
get value() { return this.value; }
|
||||
add(n: number) { return Health.create(this.value + n); }
|
||||
sub(n: number) { return Health.create(Math.max(0, this.value - n)); }
|
||||
}
|
||||
|
||||
// Immutable entities
|
||||
class Character {
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly health: Health,
|
||||
readonly status: Status,
|
||||
readonly level: Level,
|
||||
readonly factions: ReadonlySet<Faction>,
|
||||
) {}
|
||||
|
||||
dealDamage(target: Character, amount: number): void {
|
||||
// ... pure logic, no mutation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key principles:**
|
||||
- **ADTs over classes** — use discriminated unions for state/variants
|
||||
- **Value objects over primitives** — `Health`, `Damage`, `Level` instead of `number`
|
||||
- **Immutability** — no `this.health = ...`, return new instances
|
||||
- **Invariants at boundaries** — constructors enforce invariants, not getters/setters
|
||||
- **Pure functions** — domain logic has no side effects, testable in isolation
|
||||
|
||||
### Example: Damage Property
|
||||
|
||||
From user story: *"When damage received exceeds current Health, Health becomes 0 and the character dies"*
|
||||
|
||||
```typescript
|
||||
// Allium invariant (in .allium spec)
|
||||
// invariant HealthNonNegative { for c in Characters: c.health >= 0 }
|
||||
// invariant DeathAtZeroHealth { for c in Characters: c.health = 0 implies c.status = dead }
|
||||
|
||||
// fast-check property
|
||||
fc.property(
|
||||
fc.integer({ min: 0, max: 10000 }),
|
||||
fc.integer({ min: 0, max: 10000 }),
|
||||
(health, damage) => {
|
||||
const c = new Character({ name: 'goblin', health: Health.create(health) });
|
||||
c.takeDamage(Damage.create(damage));
|
||||
return c.health.value === Math.max(0, health - damage);
|
||||
}
|
||||
).check(/* ... */);
|
||||
```
|
||||
|
||||
## Skill Invocation
|
||||
|
||||
Allium skills are available in this project:
|
||||
- `/skill:allium` — entry point and language reference
|
||||
- `/skill:elicit` — explore requirements
|
||||
- `/skill:distill` — extract specs from code
|
||||
- `/skill:propagate` — generate test obligations from specs
|
||||
- `/skill:tend` — evolve specs
|
||||
- `/skill:weed` — check spec-code alignment
|
||||
|
||||
Domain workflow skill:
|
||||
- `/skill:user-story-conversation` — Card, Conversation, Confirmation workflow with Example Mapping, Allium specs, and fast-check properties
|
||||
|
||||
41
package-lock.json
generated
41
package-lock.json
generated
@ -8,6 +8,9 @@
|
||||
"name": "rpg-combat",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-check": "^4.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^25.9.1",
|
||||
@ -1305,6 +1308,28 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-check": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.8.0.tgz",
|
||||
"integrity": "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pure-rand": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@ -2027,6 +2052,22 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pure-rand": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz",
|
||||
"integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/dubzzz"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fast-check"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||
|
||||
@ -27,5 +27,8 @@
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.60.0",
|
||||
"vitest": "^4.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"fast-check": "^4.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user