From 9654056316f4bdc5d44dda784d06990e2126f558 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 00:10:52 +0200 Subject: [PATCH 01/16] language: Add Quint tooling, modeling skills, and mutation cursor spec Provide reusable Quint language, modeling, and spec-execution guidance with runnable examples for formal verification work. Package the Quint CLI and language server in the Nix development environment so the new skills and specifications can be typechecked and explored reproducibly. Add a bounded mutation-cursor model covering revision-checked observations, hook replay identity, scope attribution, contention, failures, recovery, and stale-attempt rejection, with implementation refinement notes. Ignore generated Apalache output while preserving the repository's staged source artifacts. Co-authored-by: SCE --- .agents/skills/quint-execute-spec/SKILL.md | 260 + .agents/skills/quint-lang/SKILL.md | 563 ++ .../skills/quint-lang/guidelines/choreo.md | 267 + .agents/skills/quint-lang/guidelines/cli.md | 248 + .../quint-lang/guidelines/constraints.md | 172 + .../skills/quint-lang/guidelines/operators.md | 164 + .../skills/quint-lang/guidelines/patterns.md | 408 + .../quint-lang/guidelines/simulations.md | 308 + .agents/skills/quint-lang/guidelines/tests.md | 308 + .agents/skills/quint-modeling/SKILL.md | 466 + .../skills/quint-modeling/examples/README.md | 60 + .../skills/quint-modeling/examples/ewd426.qnt | 110 + .../examples/tendermint/choreo.qnt | 295 + .../tendermint/spells/basicSpells.qnt | 388 + .../examples/tendermint/tendermint.qnt | 800 ++ .../examples/tendermint/tendermintTest.qnt | 98 + .../quint-modeling/guidelines/from-code.md | 182 + .../quint-modeling/guidelines/from-nothing.md | 64 + .../guidelines/from-requirements.md | 85 + .../quint-modeling/guidelines/from-tlaplus.md | 295 + .../quint-modeling/guidelines/review.md | 102 + .gitignore | 2 + flake.nix | 29 + nix/quint-language-server/package-lock.json | 8462 +++++++++++++++++ spec/mutation_cursor.md | 37 + spec/mutation_cursor.qnt | 1030 ++ 26 files changed, 15203 insertions(+) create mode 100644 .agents/skills/quint-execute-spec/SKILL.md create mode 100644 .agents/skills/quint-lang/SKILL.md create mode 100644 .agents/skills/quint-lang/guidelines/choreo.md create mode 100644 .agents/skills/quint-lang/guidelines/cli.md create mode 100644 .agents/skills/quint-lang/guidelines/constraints.md create mode 100644 .agents/skills/quint-lang/guidelines/operators.md create mode 100644 .agents/skills/quint-lang/guidelines/patterns.md create mode 100644 .agents/skills/quint-lang/guidelines/simulations.md create mode 100644 .agents/skills/quint-lang/guidelines/tests.md create mode 100644 .agents/skills/quint-modeling/SKILL.md create mode 100644 .agents/skills/quint-modeling/examples/README.md create mode 100644 .agents/skills/quint-modeling/examples/ewd426.qnt create mode 100644 .agents/skills/quint-modeling/examples/tendermint/choreo.qnt create mode 100644 .agents/skills/quint-modeling/examples/tendermint/spells/basicSpells.qnt create mode 100644 .agents/skills/quint-modeling/examples/tendermint/tendermint.qnt create mode 100644 .agents/skills/quint-modeling/examples/tendermint/tendermintTest.qnt create mode 100644 .agents/skills/quint-modeling/guidelines/from-code.md create mode 100644 .agents/skills/quint-modeling/guidelines/from-nothing.md create mode 100644 .agents/skills/quint-modeling/guidelines/from-requirements.md create mode 100644 .agents/skills/quint-modeling/guidelines/from-tlaplus.md create mode 100644 .agents/skills/quint-modeling/guidelines/review.md create mode 100644 nix/quint-language-server/package-lock.json create mode 100644 spec/mutation_cursor.md create mode 100644 spec/mutation_cursor.qnt diff --git a/.agents/skills/quint-execute-spec/SKILL.md b/.agents/skills/quint-execute-spec/SKILL.md new file mode 100644 index 00000000..69e5d8c4 --- /dev/null +++ b/.agents/skills/quint-execute-spec/SKILL.md @@ -0,0 +1,260 @@ +--- +name: quint-execute-spec +description: > + Implement code against an existing Quint specification. Uses Research → Plan → Implement workflow + (ACE-FCA style) grounded by the spec as the source of truth. Use when the user wants to refactor + code, add a new feature, or close a gap between implementation and spec — with the Quint spec + as the formal constraint that all changes must satisfy. +--- + +# Execute the Quint Specification + +When you have a Quint spec and want to make a change to the codebase, this skill grounds the work +in the spec. The spec is not advisory — it is the formal statement of what the system must do. +All changes must satisfy it. The spec is reviewed, then the code follows. + +## When to use this skill + +- **Refactor**: restructure code while preserving behavior (spec stays fixed; code must still satisfy it) +- **New feature**: add functionality described by or consistent with the spec +- **Gap closure**: code has drifted from the spec; bring it back into alignment +- **Spec-first change**: update the spec first, then implement to satisfy it + +If no spec exists yet, use `quint-modeling` first to create the grounding artifact. + +--- + +## Core principle: spec is ground truth + +This skill is the **post-spec** half of the loop: **Research** and **Plan** are anchored by the +existing `.qnt` file and compact gap analysis—not by prose plans alone. **Implement** proceeds only +with the **verification gates** in Phases 3–4 (Quint tool runs after substantive edits). Natural-language +plans are not proof; **tool results** are. + +**Never modify the spec to make a failing verification pass.** +If the spec must change (behavior is intentionally changing), stop and present the proposed spec +change to the user before touching any code. The spec change is the highest-leverage review point. + +Context utilization target: **40–60%** during research and planning. Catalog the codebase compactly +rather than reading everything into the main context. + +--- + +## Workflow + +``` +[Quint spec] + [Desired change description] + ↓ +┌────────────────────────────────────────────────────────────┐ +│ Phase 0: Orient │ +│ → Read the spec: what does it guarantee? │ +│ → Clarify the change: what new behavior is needed? │ +│ → Decide: is this a spec change or a code change? │ +└────────────────────────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────────────────────────┐ +│ Phase 1: Research (compact) │ +│ → Map the gap between spec and code │ +│ → Output: compact gap analysis (target: under 300 lines) │ +└────────────────────────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────────────────────────┐ +│ Phase 2: Plan │ +│ → Precise steps: files to change, expected state delta │ +│ → For each step: how to verify it satisfies the spec │ +│ → Identify which Quint properties to run at each gate │ +│ → Present plan to user before implementing │ +└────────────────────────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────────────────────────┐ +│ Phase 3: Implement │ +│ → Follow plan phase by phase │ +│ → After each phase: run Quint tools, verify properties │ +│ → Compact status back into the plan after each phase │ +└────────────────────────────────────────────────────────────┘ + ↓ +┌────────────────────────────────────────────────────────────┐ +│ Phase 4: Verify │ +│ → Run all witnesses (expect VIOLATED) │ +│ → Run all invariants (expect no violation) │ +│ → If any invariant fails: return to Phase 2, fix plan │ +└────────────────────────────────────────────────────────────┘ +``` + +--- + +## Phase 0: Orient + +Read the spec and understand the desired change. + +1. **Read the spec**. What does it model? What invariants does it assert? What witnesses does it have? +2. **Read the Spec Handoff section** (if present). Which source files does the spec correspond to? +3. **Clarify the change**. Ask the user: + - What behavior is changing? (for refactors: nothing should change; for features: what is new?) + - Should the spec change, or must the code satisfy the existing spec? + - Is there an existing failing invariant, or is this forward-looking? + +**Lightweight path** (skip research for simple changes): +If the change is small (single function, one module, no new state), skip Phase 1 and go straight +to Phase 2. + +--- + +## Phase 1: Research (Compact) + +For non-trivial changes, produce a compact gap analysis between spec and code. Keep this focused — +the goal is a compact, accurate summary, not a full codebase read. Answer these questions: + +> Given the Quint spec at `[spec path]` and the source files `[file list from handoff]`: +> +> 1. For each state variable in the spec, find where it is managed in source code +> 2. For each action in the spec, find the corresponding function(s) in source code +> 3. Identify any spec behaviors that have no corresponding source code (gaps) +> 4. Identify any source code behaviors not captured in the spec (out-of-scope) +> 5. For the change `[change description]`: which source files are affected? +> +> Output a compact summary. Do not read files that are not relevant. Target: under 300 lines. + +If the analysis missed something critical, do a targeted follow-up read before proceeding. + +--- + +## Phase 2: Plan + +Create a precise implementation plan. Each plan step must: + +- Name the file and function to change +- Describe the expected state delta (what changes in the system's behavior) +- Specify the Quint property to run as a verification gate +- Be small enough to verify independently + +### Plan format + +```markdown +## Change: [one-sentence description] + +### Spec impact +- Properties that must continue to hold: [list] +- Properties that will change (if any): [list] — REQUIRES USER APPROVAL BEFORE IMPLEMENTATION + +### Implementation steps + +#### Step 1: [file] — [what changes] +- Expected behavior change: [description] +- Quint verification gate: `quint run` / `quint verify` — invariant `[name]` + +#### Step 2: [file] — [what changes] +... + +### Rollback criteria +If invariant `[name]` fails after Step N, stop and return to planning. Do not proceed. +``` + +**Present the plan to the user before implementing.** Human review of the plan has higher leverage +than review of the code. + +If the plan requires modifying the spec, present the spec change explicitly and get approval first. + +--- + +## Phase 3: Implement + +Follow the plan. After each step: + +1. Run `quint typecheck` and fix all reported errors. +2. Run the step's verification gate (`quint run` / `quint test` / `quint verify`) +3. If the gate fails: stop, diagnose, return to Phase 2. Do NOT fix by loosening the spec. +4. Compact current status back into the plan file after each step. This keeps the context window + lean for the next step. + +### Context compaction pattern + +After each step is verified, compact progress: + +```markdown +## Status (after Step N) +- Steps 1–N: DONE ✓ +- Current: Step N+1 +- Blocking issues: [none / description] +- Next verification gate: [invariant name] +``` + +Write this to the plan file. In complex implementations, start a new context window with the +updated plan rather than continuing in an overloaded context. + +### Quint tool usage during implementation + +| Need | Tool | +|---|---| +| Type-check spec after any edit | Run `quint typecheck`; fix all reported errors before continuing | +| Verify witnesses are reachable | `quint run` with witness as invariant | +| Verify safety invariants hold | `quint run` with `--max-samples 5000` or `quint verify` | +| Interactive exploration | `quint REPL session` + `quint REPL eval` (only when CLI is insufficient) | + +--- + +## Phase 4: Verify + +Run the full property suite. + +### Witnesses (liveness check) +All witnesses must be **violated** (meaning the expected state is reachable): +- Use ``quint run`` with witness name in `witnesses` (or mapped invariant selector). +- Expected result: witness reachability is reported (equivalent to `Counterexample found` in raw CLI wording). + +### Safety invariants +All invariants must **not be violated**: +- Use ``quint run`` (or ``quint verify`` when stronger coverage needed). +- Expected result: no invariant violation reported. + +### If a safety invariant is violated + +1. Read the counterexample trace step by step +2. Identify which implementation step introduced the violation +3. Return to Phase 2 — fix the plan, not the spec +4. If the spec's invariant is genuinely wrong, present the proposed spec change to the user + +### If a witness is satisfied (action is unreachable) + +The implementation has over-constrained behavior — a path that should be reachable is blocked. +Return to Phase 2 and identify which step introduced the constraint. + +--- + +## Spec change protocol + +If the desired change requires updating the spec (new state variables, changed invariants): + +1. **Draft the spec change** — show the diff to the user before any code changes +2. **Verify the updated spec in isolation** — typecheck, run witnesses, run invariants +3. **Get explicit approval** — do not proceed to code until the spec change is approved +4. Then implement the code to satisfy the updated spec + +This preserves the spec as the source of truth even when it evolves. + +--- + +## What NOT to do + +| Anti-pattern | Why it breaks the workflow | +|---|---| +| Modify spec to pass a failing invariant | Destroys the spec as ground truth | +| Skip verification gates between steps | Breaks incremental verification; bugs compound | +| Read the whole codebase into main context | Floods context; catalog compactly in Phase 1 instead | +| Plan in prose, implement "roughly" | Plan must be precise enough to verify step-by-step | +| Treat spec as advisory documentation | Spec is a formal constraint — machine-checkable | +| Fix invariant failure by weakening the invariant | Invariants must be fixed in code, not loosened | + +--- + +## Lightweight path for simple changes + +For small, well-understood changes (single function, no new state): + +1. Read the relevant spec module +2. Identify which invariant(s) cover the changed behavior +3. Make the change +4. Run `quint run` with those invariants +5. Done + +Skip Phase 0–1. Use Phases 2–4 only for non-trivial changes. diff --git a/.agents/skills/quint-lang/SKILL.md b/.agents/skills/quint-lang/SKILL.md new file mode 100644 index 00000000..792255e7 --- /dev/null +++ b/.agents/skills/quint-lang/SKILL.md @@ -0,0 +1,563 @@ +--- +name: quint-lang +description: > + Quint language and CLI reference — the expert on Quint syntax, operators, types, `basicSpells`, + the toolchain (typecheck/run/test/verify), and how to read simulation and counterexample output. + Use when writing or debugging the contents of a `.qnt` file, fixing a typecheck/parse error, + looking up an operator or idiom, analyzing an invariant violation or counterexample trace, or + optimizing state-space exploration. This is for working IN Quint at the language level — not for + analyzing or running TLA+/TLC itself. For building a NEW model end-to-end from some source — + including translating a TLA+ spec into Quint, or modeling code/requirements/an idea — use the + quint-modeling skill, which owns that workflow and consults this reference for syntax. Keywords: + quint, syntax, operators, typecheck, model checking, counterexample, basicSpells, CLI, + specification language. +--- +# Quint Language Reference + +Quint is an executable specification language for complex systems, developed by Informal Systems. It compiles to TLA+ and supports simulation and model checking. + +## Module structure + +```quint +module MyProtocol { + // type aliases, constants, state, actions, properties +} +``` + +Modules can import others: +```quint +import Voting.* // all definitions +import Voting(quorum) // specific definition +import Voting as V // namespace alias +``` + +--- + +## Types + +| Type | Description | Example | +|---|---|---| +| `int` | Integers | `-1, 0, 42` | +| `bool` | Booleans | `true, false` | +| `str` | Strings | `"hello"` | +| `Set[T]` | Finite set | `Set(1, 2, 3)` | +| `List[T]` | Ordered sequence | `List(1, 2, 3)` | +| `K -> V` | Key-value map (type is `K -> V`, **not** `Map[K, V]`) | value: `Map("a" -> 1)` | +| `(T1, T2)` | Tuple | `(1, "x")` | +| `{ f: T, g: U }` | Record | `{ x: 1, ok: true }` | +| `T \| U` | Sum (variant) | (use type alias) | + +Type aliases: +```quint +type NodeId = int +type Phase = Idle | Propose | Vote | Commit // enum — prefer over string literals +``` + +--- + +## Definitions + +```quint +// Module parameter — fixed at instantiation, not a state variable +const N: int +const Nodes: Set[str] + +// Pure function — no state access, usable anywhere +pure def max(a: int, b: int): int = if (a > b) a else b + +// Stateful operator — can read vars, takes arguments (unlike val) +def isActive(n: str): bool = active.contains(n) + +// State-reading value — can read vars, no arguments +val quorum: bool = votes.size() * 2 > nodes.size() + +// Compile-time constant +pure val N: int = 4 +val threshold: int = N / 2 + 1 +``` + +`const` vs `pure val`: `const` is a module parameter bound at instantiation (`import A(N = 3)`); `pure val` is a fixed expression computed once. `def` vs `val`: `def` takes arguments; `val` does not. + +--- + +## State variables + +```quint +type LocalState = { + leader: int, + phase: Phase, // enum (see Type aliases) — prefer over a bare str + votes: Set[int], + log: List[str], +} + +var localState: LocalState // cohesive local protocol state +var peers: int -> str // independent concern (peer metadata) +``` + +State variables can only be read in `val` definitions and actions; they cannot be read in `pure def`. + +--- + +## Actions + +Actions describe state transitions. They return `bool` — `true` if the action fires. + +```quint +action init: bool = all { + leader' = 0, + phase' = Idle, + votes' = Set(), + log' = List(), + state' = Map(), +} + +action propose(node: int): bool = all { + phase == Idle, + node > 0, + leader' = node, + phase' = Propose, + votes' = votes, + log' = log, + state' = state, +} +``` + +Key rules: +- Every `var` must be assigned in every action (use `x' = x` to leave unchanged). +- `all { ... }` — all sub-expressions must hold (conjunction). Guards are plain boolean expressions inside `all { }`. +- `any { ... }` — at least one must hold (disjunction); the REPL picks non-deterministically. + +--- + +## Non-determinism + +```quint +action step: bool = any { + propose(1), + propose(2), + vote, + timeout, +} + +// Non-deterministic choice from a set +action deliverMessage: bool = { + nondet msg = pending.oneOf() + all { + pending.size() > 0, + delivered' = delivered.union(Set(msg)), + pending' = pending.exclude(Set(msg)), + // ... other vars unchanged + } +} +``` + +--- + +## Set operators + +```quint +Set(1, 2, 3).contains(2) // true +Set(1, 2).union(Set(2, 3)) // Set(1, 2, 3) +Set(1, 2, 3).intersect(Set(2, 3)) // Set(2, 3) +Set(1, 2, 3).exclude(Set(2)) // Set(1, 3) +Set(1, 2, 3).filter(x => x > 1) // Set(2, 3) +Set(1, 2, 3).map(x => x * 2) // Set(2, 4, 6) +Set(1, 2, 3).fold(0, (acc, x) => acc + x) // 6 +Set(1, 2, 3).size() // 3 +Set(1, 2, 3).forall(x => x > 0) // true +Set(1, 2, 3).exists(x => x > 2) // true +1.to(5) // Set(1, 2, 3, 4, 5) +nondet x = Set(1, 2, 3).oneOf() // non-deterministic pick — only valid in nondet bindings +``` + +--- + +## List operators + +```quint +List(1, 2, 3).head() // 1 +List(1, 2, 3).tail() // List(2, 3) +List(1, 2, 3).length() // 3 +List(1, 2, 3).nth(1) // 2 (0-indexed) +List(1, 2, 3).append(4) // List(1, 2, 3, 4) +List(1, 2).concat(List(3, 4)) // List(1, 2, 3, 4) +List(1, 2, 3).foldl(0, (acc, x) => acc + x) // 6 +List(1, 2, 3).select(x => x > 1) // List(2, 3) +``` + +--- + +## Map operators + +```quint +Map("a" -> 1, "b" -> 2).get("a") // 1 +Map("a" -> 1).put("b", 2) // Map("a" -> 1, "b" -> 2) +Map("a" -> 1, "b" -> 2).keys() // Set("a", "b") +Set(1, 2, 3).mapBy(k => k * 2) // Map(1 -> 2, 2 -> 4, 3 -> 6) — set of keys → map +``` + +--- + +## Records + +Records group related fields into a named type. They are the primary tool for modelling structured state in Quint. + +### Type aliases for records + +```quint +type NodeState = { + phase: Phase, // enum: Idle | Propose | Vote | Commit + voted: bool, + log: List[int], +} + +type Message = { + from: int, + to: int, + round: int, + payload: str, +} +``` + +### Creating and accessing + +```quint +val n: NodeState = { phase: Idle, voted: false, log: List() } +n.phase // Idle +n.voted // false +``` + +### Updating (immutable — returns a new record) + +```quint +{ ...n, phase: Propose } // ✅ preferred — idiomatic, handles multiple fields +{ ...n, voted: true, phase: Vote } // ✅ multiple fields at once + +n.with("phase", Propose) // ⚠️ valid but non-idiomatic — field name is a string literal +``` + +### Records as state — when to group variables + +TLA+ specs typically flatten all state into independent top-level variables. Quint's type system lets you group them. **When fields describe one cohesive local state, make a record type and use a single state variable of that type.** + +**Group into a record when:** +- They represent the local state of a single actor (e.g. one node's phase + log + vote) +- They are always passed together as function arguments +- An invariant relates multiple fields of the same conceptual entity + +**Keep flat when:** +- The variables represent distinct concerns that change independently +- The component is simple and grouping adds no clarity +- The variables are intentionally in different ownership/lifecycle domains + +### Example: preferred grouped local state vs. anti-pattern + +**Preferred (cohesive local state):** +```quint +type LocalState = { + id: int, + phase: Phase, + est1: int, + est2: Option[int], // Option is from basicSpells, not built in — see Basic spells below + round: int, + crashed: bool, + leader: int, + received_messages: Set[Message], +} + +var localState: LocalState +``` + +**Avoid for cohesive local state:** +```quint +var id: int +var phase: Phase +var est1: int +var est2: Option[int] +var round: int +var crashed: bool +var leader: int +var received_messages: Set[Message] +``` + +**For N actors, use a map of grouped records:** +```quint +type LocalState = { phase: Phase, votedFor: int, log: List[int] } + +var nodes: int -> LocalState + +action commit(id: int): bool = { + val node = nodes.get(id) + all { + node.phase == Vote, + nodes' = nodes.put(id, {...node, phase: Commit}), + } +} +``` + +### Nested records + +```quint +type ClusterState = { + nodes: int -> NodeState, + leader: int, + epoch: int, +} + +var cluster: ClusterState + +// Read nested field: +cluster.nodes.get(1).phase + +// Update nested field (must rebuild from the inside out): +val updated = {...cluster.nodes.get(1), phase: Commit} +cluster' = {...cluster, nodes: cluster.nodes.put(1, updated)} +``` + +### Records in sets (messages, events) + +```quint +var inFlight: Set[Message] + +action send(src: int, dst: int, r: int, p: str): bool = all { + inFlight' = inFlight.union(Set({ from: src, to: dst, round: r, payload: p })), + // ... +} + +// Filter by field: +inFlight.filter(m => m.to == nodeId) +inFlight.exists(m => m.round == currentRound and m.payload == "vote") +``` + +--- + +## Sum types + +Sum types (variants) represent a value that can be one of several distinct cases. + +```quint +type Action = + | Propose({ value: int, proposer: int }) + | Vote({ value: int, voter: int }) + | Decide({ value: int }) +``` + +Each variant has a named constructor and carries one payload. A constructor takes exactly one argument — wrap multiple fields in a record (as above) or a tuple. + +Construct a value by calling the constructor: +```quint +val a: Action = Propose({ value: 1, proposer: 2 }) +``` + +Pattern-match with `match`, binding the payload: +```quint +pure def describeAction(a: Action): str = + match a { + | Propose(p) => "proposal" + | Vote(v) => "vote" + | Decide(d) => "decision" + } +``` + +Use `_` to ignore the payload when you only care which variant it is: +```quint +match a { + | Propose(_) => "proposal" + | _ => "other" +} +``` + +Use sum types when a message, event, or state can take structurally different forms — not just different values of the same type. + +--- + +## Enum types +Enum types are a special case of sum types where each case has no additional data. + +```quint +type Phase = Idle | Propose | Vote | Commit +var phase: Phase + +if (phase == Propose) { ... } +``` + +--- + +## Variable grouping — decision guide + +Before writing `var` declarations, answer these questions for each candidate group: + +| Question | Group → record if... | Keep flat if... | +|---|---|---| +| Do these vars always change together? | Yes, in most actions | No, they're independent | +| Do they describe the same entity? | Same node / same message / same round | Different concerns | +| Is there one instance or N instances? | Either one or N (group if cohesive; for N use `Id -> RecordType`) | Flat only when concerns are truly independent | +| Do invariants relate them? | Invariant spans multiple fields of one entity | Invariant uses vars independently | + +--- + +## Boolean operators + +```quint +not(p) // negation — Quint has no ! operator +p and q // conjunction +p or q // disjunction +p implies q // p => q (not(p) or q) +p iff q // p == q for booleans + +and { p1, p2, p3 } // block form — equivalent to p1 and p2 and p3 +or { p1, p2, p3 } // block form — at least one must hold +``` + +`and { }` and `or { }` are the same operators as `all { }` and `any { }` in actions — use whichever reads more naturally in context. + +--- + +## Invariants and temporal properties + +```quint +// Safety invariant — must hold in every reachable state +// @invariant +val noDuplicateLeader: bool = + leaders.size() <= 1 + +// Temporal property — evaluated over traces +// @temporal +temporal eventualProgress: bool = + eventually(committed.size() > 0) + +// Temporal operators +eventually(p) // p holds in some future state +always(p) // p holds in all future states +p.implies(q) // p => q +``` + +--- + +## Assume + +```quint +assume nodeCountPositive = N > 0 +assume quorumMajority = 2 * quorum > N +``` + +An `assume` states a premise about constants, but it is **not enforced** — a violated `assume` +is silently ignored by `quint typecheck`, `quint run`, and `quint verify` (none of them flags +it). It is documentation, not a checked constraint. To actually *check* a condition on +constants, write a `run` test that asserts it (it executes and fails when the condition is +false): + +```quint +run quorumAssumptionTest = all { + 2 * quorum > N, + N > 0, +} +``` + +Run it with `quint test`; the test fails (reporting which conjunct broke) if a constant +assignment violates the condition. + +--- + +## Conditional and let + +```quint +if (x > 0) "positive" else "non-positive" + +val result = { + val doubled = x * 2 + doubled + 1 +} +``` + + +--- + +## REPL usage + +Prefer CLI commands (`quint typecheck`, `quint run`, `quint test`, `quint verify`) for all validation and execution tasks. Open the REPL (`quint` or `quint -r spec.qnt::ModuleName`) only when you need expression-level interaction the CLI does not provide. + +Type inspection: +``` +>>> :type myExpression +``` + +--- + +## File layout + +Split specs across two files: + +``` +.qnt # main module — step, init, vars, invariants +_test.qnt # test module — run tests and scenario witnesses (imports main) +``` + +### Module responsibilities + +**Main module** (`.qnt`): +- Declares all state variables, `init`, actions, and safety invariants +- **`step` must live in the main module** — it is the entry point for `quint run` simulation +- The module name matches the file stem: `module myProtocol` in `myProtocol.qnt` + +**Test module** (`_test.qnt`): +- Imports the main module (`import myProtocol.*`) +- Contains `run` tests and scenario witnesses invoked via `quint test` or `quint run` +- Inherits `step` from the main module through the import + +### Which `--main` to pass for `quint run` + +`quint run` must receive the module that **owns the property** being checked: + +| Property location | Correct `--main` | +|---|---| +| Invariant defined in main module | main module name | +| Witness / `run` test defined in test module | test module name (it imports `step` from main) | + +The primitive's `module_name` field (set during indexing) always holds the correct value. Use it directly — do not derive from the filename. + +--- + +--- + +## Basic spells + +Many useful operators are not built into Quint but are available in `basicSpells.qnt`, a standard library shipped with most Quint projects. Import it with: + +```quint +import basicSpells.* from "./basicSpells" +``` + +Key definitions it provides: + +| Definition | What it does | +|---|---| +| `type Option[a] = Some(a) \| None` | The option type — Quint has **no** built-in `Option`. Any spec field typed `Option[T]` depends on this import. | +| `unwrap(o)` | The value inside `Some`; undefined on `None` | +| `require(cond)` | Blocks the action if `cond` is false (cleaner than bare `all { cond, ... }`) | +| `values(m)` | Set of all values in map `m` | +| `transformValues(m, f)` | New map with `f` applied to every value | +| `has(m, key)` | True if `key` is bound in `m` | +| `getOrElse(m, key, default)` | `m.get(key)` if present, otherwise `default` | +| `mapRemove(m, key)` / `mapRemoveAll(m, ks)` | Copy of `m` without `key` (or without the set of keys `ks`) | +| `setRemove(s, e)` / `setAdd(s, e)` | Copy of set `s` without / with element `e` | +| `find(s, f)` / `findFirst(l, f)` | First element of set / list satisfying `f`, as `Option` | +| `max(i, j)` / `min(i, j)` / `abs(i)` | Max / min of two integers; absolute value | + +When you see a spec using `Option`, `require`, `values`, or `transformValues` without an import, it is relying on basicSpells — check whether the project includes it. (Less common operators live in a sibling `rareSpells.qnt`.) + +--- + +## Guidelines + +Detailed references — read these when you need more than the quick reference above: + +| File | Contents | +| --- | --- | +| `guidelines/operators.md` | Complete operator reference: extended set/list/map operators, `run`/`then`/`expect`/`reps` for tests and witnesses, temporal fairness, `q::debug` | +| `guidelines/simulations.md` | Witnesses vs invariants, result interpretation, progressive increase protocol, trace analysis, coverage standard | +| `guidelines/constraints.md` | Hard language limitations: no string ops, no nested match, no destructuring, no loops, no early returns | +| `guidelines/cli.md` | Full CLI reference: `quint run`, `quint test`, `quint verify` flags, verbosity guide, reading output | +| `guidelines/patterns.md` | 14 core patterns: State Type, Pure Functions, Thin Actions, Map Pre-population, Syntax Rules, Undefined Behavior, Witnesses, Nondeterministic Testing, Separate Test Files, REPL-First Debugging, Separate Concerns First, Extract System Model, Types-First Scaffolding, Logic Stubs | +| `guidelines/tests.md` | Writing and debugging tests: `run`/`then`/`expect`/`reps`/`fail`, nondeterministic tests, error location ≠ failure point, frame counting, REPL-first debugging | +| `guidelines/choreo.md` | Choreo framework for distributed protocols: two-file split, `choreo::cue` pattern, `.with_cue().perform()` testing, witness-based test discovery | diff --git a/.agents/skills/quint-lang/guidelines/choreo.md b/.agents/skills/quint-lang/guidelines/choreo.md new file mode 100644 index 00000000..9df76b39 --- /dev/null +++ b/.agents/skills/quint-lang/guidelines/choreo.md @@ -0,0 +1,267 @@ +# Choreo Framework + +Choreo is a structured framework for writing distributed protocol specs in Quint. Instead of a blank file, it gives you pre-built abstractions for message passing, state management, and Byzantine fault tolerance. + +**Use Choreo when:** consensus protocols (Raft, Paxos, Tendermint, HotStuff), BFT protocols, multi-phase commit, any message-passing protocol with N processes. + +## Contents + +- [1. Two-File Split](#1-two-file-split) +- [2. The `choreo::cue` Pattern](#2-the-choreocue-pattern) +- [3. Testing with Cues](#3-testing-with-cues) +- [4. Witness-Based Test Discovery](#4-witness-based-test-discovery) + +**Import:** +```quint +import choreo(processes = NODES) as choreo from "./choreo" +``` + +--- + +## 1. Two-File Split + +Choreo specs split into two files: + +``` +algorithm.qnt — pure consensus logic (no distributed state) +consensus.qnt — distributed system: N processes, message buffer, actions +``` + +```quint +// algorithm.qnt — pure logic only +module algorithm { + type LocalState = { + currentRound: int, + votes: Set[Vote], + decisions: Set[Decision], + } + + type Result = { + output: Set[ConsensusOutput], + post: LocalState, + } + + pure def processInput(state: LocalState, input: Input, id: ID): Result = { + // All business logic here — no global state access + } +} + +// consensus.qnt — distributed wrapper +module consensus { + import algorithm.* from "./algorithm" + + type Environment = { + processes: ID -> LocalState, + messageBuffer: Set[Message], + } + + const correctProcesses: Set[ID] + const byzantineProcesses: Set[ID] + + // Actions call algorithm.* pure functions +} +``` + +**Why**: Pure functions in `algorithm.qnt` can be tested directly in the REPL with any state. The distributed wrapper in `consensus.qnt` handles network, faults, and nondeterminism. + +--- + +## 2. The `choreo::cue` Pattern + +The core abstraction: separates **when** (listen) from **what** (act). + +```quint +choreo::cue(context, listen_operator, act_operator) +``` + +- `listen_operator`: returns a `Set[Params]` — the messages/events that match current state +- `act_operator`: takes one `Params` value and returns a `Transition` +- If the set is empty, no transition fires + +### Example: Tendermint proposal handling + +```quint +// 1. Listen: filter proposals relevant to current state +pure def listen_proposal_in_propose(ctx: LocalContext): Set[ProposeMsg] = { + val s = ctx.state + val proposals = ctx.messages.get_proposals() + proposals.filter(p => and { + s.stage == ProposeStage, + p.valid_round == -1, + p.src == PROPOSER.get(s.round), + }) +} + +// 2. Act: produce a transition for a matching proposal +pure def broadcast_prevote_for_proposal(ctx: LocalContext, p: ProposeMsg): Transition = { + val s = ctx.state + val effects = if (valid(p.proposal) and s.locked_round == -1) + Set(choreo::Broadcast(PreVote({ src: s.process_id, round: s.round, id: Some(id(p.proposal)) }))) + else + Set(choreo::Broadcast(PreVote({ src: s.process_id, round: s.round, id: None }))) + { post_state: { ...s, stage: PreVoteStage }, effects: effects } +} + +// 3. Wire together in main_listener +pure def main_listener(ctx: LocalContext): Set[Transition] = + Set( + choreo::cue(ctx, listen_proposal_in_propose, broadcast_prevote_for_proposal), + choreo::cue(ctx, listen_quorum_prevotes_any, trigger_prevote_timeout), + choreo::cue(ctx, listen_quorum_precommits_any, trigger_precommit_timeout), + on_propose_timeout(ctx), // non-cue transitions mix in freely + on_prevote_timeout(ctx), + on_precommit_timeout(ctx), + ).flatten() +``` + +The `main_listener` is a protocol at-a-glance — good names make it read like the paper. + +### `Transition` type + +```quint +{ + post_state: LocalState, // new local state after transition + effects: Set[choreo::Effect] // messages to broadcast, custom effects +} +``` + +Built-in effects: `choreo::Broadcast(msg)`. Custom effects via `choreo::CustomEffect(...)`. + +--- + +## 3. Testing with Cues + +The cue pattern enables **controlled, non-cheating** tests. `.with_cue()` verifies the listen condition is met before executing. + +```quint +module protocolTest { + import protocol.* from "./protocol" + + // Happy path: inject a proposal, verify stage transition + run proposalHandlingTest = { + val proposal = { proposal: "v0", round: 0, src: "p1", valid_round: -1 } + init + .then("p1".with_cue(listen_proposal_in_propose, proposal).perform(broadcast_prevote_for_proposal)) + .expect(s.system.get("p1").stage == PreVoteStage) + .then("p2".with_cue(listen_proposal_in_propose, proposal).perform(broadcast_prevote_for_proposal)) + } + + // Timeout test + run timeoutTest = { + init + .then("p1".step_with(on_propose_timeout)) + .then("p2".step_with(on_propose_timeout)) + .expect(NODES.forall(n => s.system.get(n).round == 1)) + } + + // Message injection: manually add messages to the buffer + run messageInjectionTest = { + val msg1 = { src: "p1", round: 2, value: "v0" } + init + .then( + "p3".step_with_messages( + (ctx) => listen_prevote(ctx).filter(m => m.round == 2), + (msgs) => msgs.setAdd(PreVote(msg1)) + ) + ) + .expect(s.system.get("p3").votes.size() == 1) + } +} +``` + +### Testing operators + +| Operator | Signature | Effect | +|---|---|---| +| `"node".with_cue(listen, params).perform(act)` | — | Assert listen returns params, then call act | +| `"node".step_with(listener)` | — | Execute a timeout or special listener | +| `"node".step_with_messages(listener_fn, msg_fn)` | — | Inject messages, then run listener | + +### Anti-patterns + +```quint +// ❌ Wrong syntax +.then("p1", with_cue(listen, params), perform(act)) + +// ✅ Methods chain on the string +.then("p1".with_cue(listen, params).perform(act)) + +// ❌ Wrong order +.then(step_with("p1", listener)) + +// ✅ Node comes first +.then("p1".step_with(listener)) + +// ❌ Messages are Sets, not arrays +val msg = s.messages.get("p1")[0] + +// ✅ Use find + unwrap +val msg = s.messages.get("p1").get_votes().find(m => m.round == 0).unwrap() +``` + +--- + +## 4. Witness-Based Test Discovery + +For complex actions requiring deep state setup, use witnesses to find reachable traces, then convert them to deterministic tests. + +### Step 1: Add a logging custom effect + +```quint +// Add to your action +choreo::CustomEffect(Log(BroadcastedPrecommit(ctx.state.process_id, params))) + +// Handle it +def apply_custom_effect(env: GlobalContext, effect: CustomEffects): GlobalContext = + match effect { + | Log(logType) => { ...env, extensions: { ...env.extensions, log: logType } } + | _ => env + } +``` + +### Step 2: Write a witness + +```quint +val canBroadcastPrecommit: bool = + match choreo::s.extensions.log { + | BroadcastedPrecommit(_) => false + | _ => true + } +``` + +### Step 3: Find a counterexample + +```bash +# Here we want the actual PATH, so use the negated-invariant form (not --witnesses): +# canBroadcastPrecommit is written not(target); a reported violation is a trace reaching it. +quint run spec.qnt --main myProtocol --invariant canBroadcastPrecommit \ + --max-steps 50 --init init_displayer +``` + +If still satisfied, increase `--max-steps` until violated. Then minimize: + +```bash +# Minimize: decrease until you can't find violation +quint run spec.qnt --main myProtocol --invariant canBroadcastPrecommit --max-steps 19 +``` + +### Step 4: Convert counterexample to a `run` + +Read the trace's `log` field. Each log entry maps to a `with_cue().perform()` call: + +``` +Log: BroadcastedPrecommit("p1", { round: 2, value: "v0" }) +→ "p1".with_cue(listen_precommit, { round: 2, value: "v0" }).perform(broadcast_precommit) +``` + +```quint +run canBroadcastPrecommitTest = + init + .then("p1".with_cue(listen_proposal_in_propose, v0_proposal).perform(broadcast_prevote_for_proposal)) + .then("p2".with_cue(listen_proposal_in_propose, v0_proposal).perform(broadcast_prevote_for_proposal)) + .then("p1".with_cue(listen_quorum_prevotes, v0_proposal).perform(broadcast_precommit)) +``` + +### Step 5: Clean up + +Remove all `Log(...)` effects, the `log` field from `Extensions`, and the `apply_custom_effect` instrumentation. Keep only the tests. diff --git a/.agents/skills/quint-lang/guidelines/cli.md b/.agents/skills/quint-lang/guidelines/cli.md new file mode 100644 index 00000000..91da081b --- /dev/null +++ b/.agents/skills/quint-lang/guidelines/cli.md @@ -0,0 +1,248 @@ +# Quint CLI Reference + +## Contents + +- [Installation](#installation) +- [Commands Overview](#commands-overview) +- [`quint run` — Simulation](#quint-run--simulation) +- [`quint test` — Deterministic Tests](#quint-test--deterministic-tests) +- [`quint verify` — Model Checking](#quint-verify--model-checking) +- [`quint typecheck`](#quint-typecheck) +- [Reading Output](#reading-output) +- [Verbosity Guide](#verbosity-guide) + +## Installation + +```bash +npm i @informalsystems/quint -g +``` + +--- + +## Commands Overview + +| Command | Purpose | +|---|---| +| `quint` | Start REPL | +| `quint run` | Random simulation — find invariant violations fast | +| `quint test` | Run deterministic `run` tests | +| `quint verify` | Exhaustive verification via Apalache (bounded model checking) | +| `quint typecheck` | Type-check without running | +| `quint parse` | Parse and resolve imports only | +| `quint compile` | Compile to TLA+ or JSON IR | + +--- + +## `quint run` — Simulation + +Randomly walks the state space. Finds obvious violations in seconds. + +```bash +quint run spec.qnt \ + --main MyModule \ + --invariant myInvariant \ + --max-steps 100 + # --max-samples defaults to 10000; lower it only for quick debugging, not to confirm a property +``` + +### Key flags + +| Flag | Default | Effect | +|---|---|---| +| `--main` | filename | Module to use | +| `--invariant` | `true` | Invariant name or expression to check | +| `--invariants` | `[]` | Space-separated list of invariant names (all checked with AND) | +| `--witnesses` | `[]` | Space-separated list of witnesses to report on | +| `--max-steps` | 20 | Max steps per trace | +| `--init` | `init` | Name of the init action in the module | +| `--step` | `step` | Name of the step action in the module | +| `--max-samples` | 10000 | Max number of runs before giving up | +| `--seed` | random | Seed for reproducibility | +| `--verbosity` | 2 | Output detail, **0–5**: 0=silent, 1=pass/fail, 2=example trace, 3=+`nondet` picks, 4–5=+evaluator internals | +| `--hide` | `[]` | Variable names to hide from terminal output | +| `--out-itf` | — | Write traces in Informal Trace Format (for ITF Viewer) | +| `--mbt` | false | Emit `mbt::actionTaken` and `mbt::nondetPicks` metadata for MBT | + +### Reading output + +| Output | Meaning | +|---|---| +| `"An example execution"` | Invariant violated — counterexample found | +| `"No violation found"` | Invariant held across all sampled traces | +| `" was witnessed in N trace(s) … (P%)"` (with `--witnesses`) | Witness `` reachable in N traces; **0 traces** ⚠️ = unreachable in this sampling | + +### Verbosity guide + +| Level | Shows | When to use | +|---|---|---| +| 0 | Nothing (silent) | Scripted/bulk runs where you only check the exit code | +| 1 | Pass/fail line only | Bulk runs | +| 2 (default) | Example trace (states) | Quick trace understanding | +| 3 | + `nondet` picks / state changes | Debugging failures | +| 4–5 | + evaluator internals | Deep debugging of the tool itself | + +### Notifying the user about non-default actions + +After calling `quint run` or `quint verify`, if a +non-default `init` or `step` was passed, always include a brief confirmation in your +reply, for example: + +> Ran with `--init customInit --step myStep`. + +If both are omitted (using Quint defaults), no confirmation line is needed. + +### Reproducing a run + +```bash +# Capture the seed from a violation, then replay +quint run spec.qnt --invariant myInv --seed 12345 --verbosity 3 +``` + +--- + +## `quint test` — Deterministic Tests + +Runs all `run` definitions in the spec (or a filtered subset). + +```bash +quint test spec.qnt --main MyModule --match testName +``` + +### Key flags + +| Flag | Default | Effect | +|---|---|---| +| `--main` | filename | Module to use | +| `--match` | all | String or regex to filter test names | +| `--max-samples` | 10000 | Max runs for randomized tests | +| `--seed` | random | Seed for reproducibility | +| `--verbosity` | 2 | Output detail level | + +### Reading output + +``` +[PASS] basicTest +[FAIL] transferTest +``` + +A `[FAIL]` shows which `.expect()` failed and the state at failure. + +--- + +## `quint verify` — Apalache (Exhaustive) + +Checks invariants across **all** reachable states up to `--max-steps`. Requires Java (OpenJDK). + +```bash +quint verify spec.qnt \ + --main MyModule \ + --invariant myInvariant \ + --max-steps 10 +``` + +Quint automatically downloads and starts Apalache. To use a running Apalache server: + +```bash +apalache-mc server # start Apalache server +quint verify spec.qnt --server-endpoint localhost:8822 --invariant myInv +``` + +### Key flags + +| Flag | Default | Effect | +|---|---|---| +| `--main` | filename | Module to use | +| `--invariant` | — | Invariants to check (comma-separated) | +| `--init` | `init` | Name of the init action in the module | +| `--step` | `step` | Name of the step action in the module | +| `--inductive-invariant` | — | Inductive invariant (checked in 2–3 Apalache calls) | +| `--temporal` | — | Temporal properties to check | +| `--max-steps` | 10 | Bound on trace length | +| `--random-transitions` | false | Symbolic simulation instead of full exploration | +| `--out-itf` | — | Write counterexample trace to ITF file | + +### When to use verify vs run + +| Tool | Coverage | Cost | Use when | +|---|---|---|---| +| `quint run` | Sampled | Seconds | Early design, quick sanity check | +| `quint verify` | All states up to bound | Minutes–hours | Final confirmation, critical invariants | + +--- + +## REPL + +```bash +quint # start blank REPL +quint -r spec.qnt::ModuleName # load file and import module +``` + +To drive the REPL **non-interactively** (piping commands from a script/agent), add +`--backend=typescript` — the default Rust backend evaluates nothing on piped stdin +(`ERR_USE_AFTER_CLOSE: readline was closed`). Interactive TTY use works on either backend. + +```bash +printf 'init\nstep\nbalances\n' | quint -r spec.qnt::ModuleName --backend=typescript +``` + +### REPL commands + +| Command | Effect | +|---|---| +| `.load spec.qnt` | Load (or reload) a file | +| `.clear` | Reset all session state | +| `.save kettle.qnt` | Save session to file | +| `.seed[=]` | Set (or get) the random seed — makes `nondet`/`oneOf` picks reproducible: same seed, same trace | +| `.exit` | Exit REPL | +| `:type expr` | Show inferred type of expression | + +### REPL workflow + +``` +>>> init // apply init action (returns true if succeeded) +>>> step // take one random step +>>> myInvariant // evaluate an invariant in the current state +>>> myPureFunc(arg) // call any pure def +>>> :type balances // inspect type +``` + +Force a specific state with an anonymous action: + +```quint +>>> all { balances' = Set("alice").mapBy(_ => 999), phase' = "ready" } +true +>>> balances +Map("alice" -> 999) +``` + +--- + +## Common Patterns + +### Check a witness (reachability) + +```bash +# Positive predicate; expect a non-zero trace count (0 traces = unreachable) +quint run spec.qnt --witnesses canDecide --max-steps 100 +``` + +### Check a safety invariant + +```bash +# Invariant should be SATISFIED — violation means a bug. +# Keep --max-samples at its 10000 default (or higher) when confirming a property; +# don't lower it to go faster. See simulations.md "Choosing --max-samples and --max-steps". +quint run spec.qnt --invariant noNegativeBalances --max-steps 100 +``` + +### Run a specific test + +```bash +quint test specTest.qnt --main specTest --match happyPath +``` + +### Generate an ITF trace for the viewer + +```bash +quint run spec.qnt --invariant myWitness --out-itf trace_{seq}.itf.json +``` diff --git a/.agents/skills/quint-lang/guidelines/constraints.md b/.agents/skills/quint-lang/guidelines/constraints.md new file mode 100644 index 00000000..d7eb3a37 --- /dev/null +++ b/.agents/skills/quint-lang/guidelines/constraints.md @@ -0,0 +1,172 @@ +# Quint Language Constraints + +**CRITICAL**: These are fundamental limitations of the Quint language. Violating these constraints will result in compilation errors that cannot be worked around. + +## Contents + +- [1. No String Manipulation](#1-no-string-manipulation) +- [2. No Nested Pattern Matching](#2-no-nested-pattern-matching) +- [3. Destructuring — bindings and lambdas only](#3-destructuring--bindings-and-lambdas-only) +- [4. No Mutable Local Variables](#4-no-mutable-local-variables) +- [5. No Loops](#5-no-loops) +- [6. No Early Returns](#6-no-early-returns) +- [7. Type Inference Limitations](#7-type-inference-limitations) +- [Debugging Workflow](#debugging-workflow) + +--- + +## 1. No String Manipulation + +Quint treats strings as **opaque values** — equality comparison only. + +```quint +// ❌ NOT allowed +"hello" + "world" // concatenation +"value: ${x}" // interpolation +str[0] // indexing +str.length() // methods +toString(42) // conversion + +// ✅ Allowed +name == "Alice" // equality comparison +Map("a" -> 1) // strings as map keys +Set("alice", "bob") // strings in sets +``` + +When you need composite identifiers, use records or sum types instead: + +```quint +// Instead of string concatenation, use structured data +type MessageId = { sender: str, round: int } +``` + +--- + +## 2. No Nested Pattern Matching + +`match` works on **one level only**. Nested patterns cause a compile error. + +```quint +// ❌ NOT allowed — nested patterns +match msg + | Request(Prepare(n, v)) => ... + +// ✅ Allowed — sequential matches +match msg + | Request(inner) => + match inner + | Prepare(n, v) => ... + | Promise(n, r) => ... +``` + +**Rule**: Match one layer at a time. Use intermediate `val` bindings between match levels. + +--- + +## 3. Destructuring — bindings and lambdas only + +Destructuring (unpacking a tuple or record into named parts) works in **`val`/let bindings** +and in **lambda parameters**, but **not** in `def`/operator parameter lists or `match` arms. + +```quint +// ✅ Allowed — val/let binding (tuple and record) +val (x, y) = get_pair() // x = pair._1, y = pair._2 +val { name, age } = person // name = person.name, age = person.age + +// ✅ Allowed — tuple destructuring in a lambda (note the DOUBLE parens) +mySet.map(((a, b)) => a + b) // ((a, b)) => … unpacks one tuple argument +// contrast: (a, b) => … is a TWO-argument lambda, not destructuring + +// ❌ NOT allowed — destructuring in a def/operator parameter list +def f((a, b)) = a + b // use a single param + ._1/._2 instead: +def f(p) = p._1 + p._2 + +// ❌ NOT allowed — destructuring a tuple inside a match arm +match msg { | Foo((a, b)) => ... } // bind, then access: +match msg { | Foo(hr) => hr._1 + hr._2 } +``` + +The `((x, y)) => e` lambda form is sugar for `t => { val x = t._1 val y = t._2 e }`. + +--- + +## 4. No Mutable Local Variables + +`val` bindings are immutable. You cannot reassign within a definition. + +```quint +// ❌ NOT allowed +val x = 1 +val x = 2 // redeclaration error + +// ✅ Use state variables (with ') for mutable state across transitions +// ✅ Use if-then-else or match for conditional values +``` + +--- + +## 5. No Loops + +Quint has no `for` or `while`. Use set/list/map operators instead. + +```quint +// ❌ NOT a thing in Quint +for x in S: x + 1 + +// ✅ Use functional operators +S.map(x => x + 1) +S.fold(0, (acc, x) => acc + x) +S.filter(x => x > 0) +``` + +--- + +## 6. No Early Returns + +Definitions must have a **single expression** as their body. + +```quint +// ❌ NOT allowed +pure def f(x: int): int = { + if (x < 0) return -1 // no return keyword + x + 1 +} + +// ✅ Use if-then-else +pure def f(x: int): int = + if (x < 0) -1 else x + 1 + +// ✅ Use match for multiple cases +pure def classify(x: int): str = + if (x < 0) "negative" + else if (x == 0) "zero" + else "positive" +``` + +--- + +## 7. Type Inference Limitations + +Quint infers types well, but needs help with empty collections and polymorphic operators. + +```quint +// ❌ Ambiguous — type of empty set unknown +val s = Set() + +// ✅ Provide context +val s: Set[int] = Set() +// or just start with elements +val s = Set(1, 2, 3) +``` + +--- + +## Debugging Workflow + +When you hit a compilation error: + +1. **Check constraints first** — most errors come from the seven rules above +2. **Read the error message** — the type checker is precise about location and cause +3. **Break complex expressions into `val` steps** — simplifies type inference and debugging +4. **Match one level at a time** — never nest patterns +5. **Use explicit field access** — `.field`, `._1`, `._2` instead of destructuring diff --git a/.agents/skills/quint-lang/guidelines/operators.md b/.agents/skills/quint-lang/guidelines/operators.md new file mode 100644 index 00000000..573307ad --- /dev/null +++ b/.agents/skills/quint-lang/guidelines/operators.md @@ -0,0 +1,164 @@ +# Quint Operator Reference — Complete + +This file covers operators missing from SKILL.md's quick reference. Read this when you need the full operator surface. + +## Contents + +- [Set operators (extended)](#set-operators-extended) +- [List operators (extended)](#list-operators-extended) +- [Map operators (extended)](#map-operators-extended) +- [Run tests and witnesses](#run-tests-and-witnesses) +- [Temporal operators (extended)](#temporal-operators-extended) +- [Debug output](#debug-output) + +--- + +## Set operators (extended) + +```quint +// Membership +1.in(Set(1, 2, 3)) // true (reverse of contains) +Set(1, 2).subseteq(Set(1, 2, 3)) // true + +// Structure +Set(1, 2).powerset() + // Set(Set(), Set(1), Set(2), Set(1, 2)) +Set(Set(1, 2), Set(3, 4)).flatten() // Set(1, 2, 3, 4) + +// Selection +Set(5).getOnlyElement() // 5 — deterministic; undefined if size != 1 +nondet x = Set(1, 2, 3).oneOf() // nondeterministic pick — use in actions only +``` + +**Picking an element**: use `getOnlyElement()` when the set has exactly one element (deterministic), or `oneOf()` inside a `nondet` binding in an action for a nondeterministic pick. `oneOf` outside a `nondet` binding causes a type/effect error. + +> Quint also has a `chooseSome` operator in its type signatures, but the simulator and verifier **do not implement it** — calling it raises a runtime error (`QNT501: Runtime does not support the built-in operator 'chooseSome'`). Do not use it in executable specs. + +--- + +## List operators (extended) + +```quint +List(1, 2, 3).indices() // Set(0, 1, 2) +List(1, 2, 3).replaceAt(1, 9) // List(1, 9, 3) +List(1, 2, 3, 4, 5).slice(1, 3) // List(2, 3) — i inclusive, j exclusive +range(1, 4) // List(1, 2, 3) — i inclusive, j exclusive +``` + +**`range` vs `to`**: `range(i, j)` returns a `List[int]` (ordered, j exclusive). `i.to(j)` returns a `Set[int]` (unordered, j inclusive). + +--- + +## Map operators (extended) + +```quint +// set vs put — critical distinction +Map(1 -> "a", 2 -> "b").set(2, "x") // Map(1 -> "a", 2 -> "x") — update EXISTING key only +Map(1 -> "a", 2 -> "b").put(3, "c") // Map(1 -> "a", 2 -> "b", 3 -> "c") — add or update + +// Functional update +Map(1 -> 10, 2 -> 20).setBy(2, x => x + 5) // Map(1 -> 10, 2 -> 25) + +// Building maps +Set((1, true), (2, false)).setToMap() // Map(1 -> true, 2 -> false) +Set(1, 2).setOfMaps(Set(true, false)) + // all possible maps from {1,2} to {true,false} +``` + +**`set` vs `put`**: `set(k, v)` has undefined behavior when `k` is not already a key. Use `put` when you might be inserting a new key; use `set` (or `setBy`) when you know the key exists. In practice, `put` is almost always what you want for state updates. + +--- + +## Run tests and witnesses + +`run` definitions define concrete execution scenarios. They are the primary way to write witnesses (reachability checks) and deterministic tests. + +```quint +// Basic pattern: chain actions with .then(), assert state with .expect() +run happyPath = init.then(vote(1)).then(vote(2)).then(decide).expect(decided == true) + +// Repeat an action N times +run threeVotes = init.then(3.reps(i => vote(i))).expect(votes.size() == 3) + +// Negative test — assert an action fails +run cannotDoubleVote = init.then(vote(1)).then(vote(1).fail()) + +// Assert mid-trace. An `assert` must ride inside an `all { }` that also assigns +// the state variables — a bare `.then(assert(...))` step assigns nothing and +// fails to typecheck (effect mismatch with the rest of the trace). Use `.expect` +// for an after-the-fact check. +run checkAfterVote = + init + .then(all { vote(1), assert(votes.size() == 0) }) // assert BEFORE vote executes + .expect(votes.size() == 1) // assert AFTER (no extra step needed) +``` + +### Operators + +| Operator | Signature | Meaning | +| --- | --- | --- | +| `a.then(b)` | `(bool, bool) => bool` | Execute `a`, then `b` from the resulting state | +| `a.expect(p)` | `(bool, bool) => bool` | Execute `a`, fail if `p` is false in resulting state | +| `n.reps(i => A)` | `(int, (int) => bool) => bool` | Execute action `A` n times; iteration index passed as `i` | +| `a.fail()` | `(bool) => bool` | True when `a` evaluates to false | +| `assert(p)` | `(bool) => bool` | Does not change state; fails if `p` is false | + +### Using run as a witness + +A witness is a `run` that demonstrates a state is reachable. The spec verifier checks that the `run` completes without failure: + +```quint +// @witness +run decisionIsReachable = + init + .then(3.reps(i => vote(i))) + .expect(decided == true) +``` + +This `run`-style witness documents a *known* reachable path and is checked with `quint test` (it passes when the path completes). For exploratory reachability, write the target as a predicate and use `quint run --witnesses ` instead — a non-zero trace count means reachable. See `guidelines/simulations.md` for the full witnesses treatment. + +--- + +## Temporal operators (extended) + +```quint +// Logical equivalence +p.iff(q) // true when p and q have the same truth value + +// Stuttering — `vars` is a Set of state variables: Set(x), Set(x, y), ... +a.orKeep(vars) // a is true, OR all vars in vars are unchanged +a.mustChange(vars) // a is true AND at least one var in vars changed + +// Action enablement +enabled(a) // true when action a's preconditions are satisfiable + +// Fairness +a.weakFair(vars) // if a is eventually always enabled, it eventually fires +a.strongFair(vars) // if a is infinitely often enabled, it eventually fires +``` + +The `vars` argument is a **`Set` of state variables**, so every variable in it must have the **same type** (e.g. `Set(votes, committed)` where both are `Set[int]`). To cover differently-typed variables, group cohesive state into one record variable and pass `Set(localState)`, or pass `Set(theOneVar)`. The official examples use a single variable: `Next.weakFair(Set(x))`. + +### Fairness in practice + +Fairness conditions are needed to rule out trivially non-terminating behaviours in liveness proofs. Add `weakFair` for actions that should eventually fire when continuously enabled (e.g. message delivery). Add `strongFair` for actions that may be intermittently enabled but must eventually fire. + +```quint +// @temporal — pass same-typed vars; here both are sets +temporal liveness: bool = + step.weakFair(Set(votes, delivered)).implies(eventually(decided)) +``` + +--- + +## Debug output + +```quint +// Print a label and value; returns the value unchanged +q::debug("votes after round", votes) + +// Self-labelling form — prints the expression text and its value +q::debug(votes.size()) +``` + +`q::debug` is a `pure def` — it can appear anywhere including inside expressions. It prints to stdout during REPL evaluation and simulation. Remove before formal verification with Apalache. diff --git a/.agents/skills/quint-lang/guidelines/patterns.md b/.agents/skills/quint-lang/guidelines/patterns.md new file mode 100644 index 00000000..c4993772 --- /dev/null +++ b/.agents/skills/quint-lang/guidelines/patterns.md @@ -0,0 +1,408 @@ +# Quint Specification Patterns + +Core patterns for writing correct, testable Quint specifications. + +## Contents + +- [1. State Type Pattern](#1-state-type-pattern) +- [2. Pure Functions Pattern](#2-pure-functions-pattern) +- [3. Thin Actions Pattern](#3-thin-actions-pattern) +- [4. Map Pre-population Pattern](#4-map-pre-population-pattern) +- [5. Syntax Rules](#5-syntax-rules) +- [6. Undefined Behavior — What to Guard Against](#6-undefined-behavior--what-to-guard-against) +- [7. Action Witnesses Pattern](#7-action-witnesses-pattern) +- [8. Nondeterministic Testing Pattern](#8-nondeterministic-testing-pattern) +- [9. Separate Test Files Pattern](#9-separate-test-files-pattern) +- [10. REPL-First Debugging](#10-repl-first-debugging) +- [11. Separate Concerns First](#11-separate-concerns-first) +- [12. Extract the System Model](#12-extract-the-system-model) +- [13. Types-First Scaffolding](#13-types-first-scaffolding) +- [14. Logic Stubs Pattern](#14-logic-stubs-pattern) + +--- + +## 1. State Type Pattern + +Encapsulate all state in a single `State` record. This is the fundamental structure for most Quint specs. + +```quint +module System { + // 1. Types — State record encapsulates everything + type State = { + field1: Type1, + field2: str -> int, // map type syntax: KeyType -> ValueType + } + + // 2. Constants + pure val INITIAL_USERS: Set[str] = Set("alice", "bob") + + // 3. Pure functions — ALL business logic here, split into a guard and a next-state function + pure def canDoOperation(state: State, param: Type1): bool = precondition(state, param) + pure def applyOperation(state: State, param: Type1): State = {...state, field1: newValue} + + // 4. State variable — keep cohesive state grouped + var state: State + + // 5. Invariants + val noNegativeBalances: bool = INITIAL_USERS.forall(u => state.field2.get(u) >= 0) + + // 6. Actions — guarded: the guard gates whether the action fires, then assign the next state + action doOperation(param: Type1): bool = all { + canDoOperation(state, param), + state' = applyOperation(state, param), + } + + // 7. Initialization — pre-populate ALL maps + action init: bool = all { + state' = { + field1: initialValue, + field2: INITIAL_USERS.mapBy(u => 0), + }, + } + + // 8. Step action — nondeterministic exploration + action step: bool = { + nondet user = INITIAL_USERS.oneOf() + any { + doOperation(user), + } + } +} +``` + +**Section order matters** — the type checker requires definitions before use: Types → Constants → Pure Functions → State Variables → Invariants → Actions → Init → Step. + +--- + +## 2. Pure Functions Pattern + +All business logic goes in pure functions. Split each operation into two pure defs: a **guard** +(can it happen?) and a **next-state** function (what does it produce, assuming the guard holds). +The action (Pattern 3) lifts the guard so it is *disabled* when the operation can't happen — no +success flag, no no-op branch. + +```quint +// Note: a pure def and an action cannot share a name in the same module (QNT101), +// so the action is `transfer` and the next-state function is `applyTransfer`. +pure def canTransfer(state: State, sender: str, recipient: str, amount: int): bool = and { + state.balances.get(sender) >= amount, + amount > 0, + sender != recipient, +} +pure def applyTransfer(state: State, sender: str, recipient: str, amount: int): State = { + val newBalances = state.balances + .setBy(sender, b => b - amount) + .setBy(recipient, b => b + amount) + {...state, balances: newBalances} +} +``` + +**Why**: Pure functions can be tested directly in the REPL with any state. Logic in actions can only be tested by running the full state machine. + +> `to` is a built-in operator (`i.to(j)`), so it cannot be used as a parameter name — that is why the parameters here are `sender`/`recipient` rather than `from`/`to`. + +--- + +## 3. Guarded Actions Pattern + +An action puts the guard and the next-state assignment together in one `all { ... }`: the guard +gates whether the action fires, then it assigns the next state. When the guard is false the action +is simply **disabled** (it does not fire and changes nothing) — no success flag, no `unchanged_all` +fallback. A disabled action is the faithful model of "this can't happen now," and it's what lets the +simulator surface dead ends; a blanket no-op fallback fabricates a transition the system doesn't have. + +```quint +action transfer(sender: str, recipient: str, amount: int): bool = all { + canTransfer(state, sender, recipient, amount), + state' = applyTransfer(state, sender, recipient, amount), +} +``` + +The action `transfer` uses the pure `canTransfer`/`applyTransfer` — distinct names, since a +`pure def` and an `action` cannot share a name. No conditional logic in the action beyond the guard. + +For cohesive local state, avoid decomposing one concept into many top-level vars (`var id`, `var phase`, `var round`, ...). Prefer a `type LocalState` record and one `var localState: LocalState`. + +--- + +## 4. Map Pre-population Pattern + +Always pre-populate maps in `init` using `mapBy`. Never start with an empty map if you'll call `.get()` on it. + +```quint +// ❌ Wrong — get("alice") is undefined if alice not in map +action init = all { balances' = Map() } + +// ✅ Correct — all keys pre-populated +action init = all { + balances' = USERS.mapBy(u => 0) +} +``` + +Map type syntax is `KeyType -> ValueType`, not `Map[KeyType, ValueType]`: + +```quint +var balances: str -> int // ✅ correct +var balances: Map[str, int] // ❌ wrong +``` + +--- + +## 5. Syntax Rules + +Common gotchas: + +```quint +// 1. Parameterless pure def — omit parentheses +pure def name = ... // ✅ +pure def name() = ... // ❌ + +// 2. Map type syntax +var m: str -> int // ✅ +var m: Map[str, int] // ❌ + +// 3. Record update — spread syntax is idiomatic +{...state, field: newValue} // ✅ preferred +state.with("field", newValue) // ⚠️ valid, but non-idiomatic (field name is a string literal) + +// 4. Variant constructors take one argument — use tuples for multiple values +TimeoutInput((height, round)) // ✅ +TimeoutInput(height, round) // ❌ + +// 5. Tuple destructuring in match — bind first, then access +| TimeoutInput(hr) => ... hr._1 ... hr._2 // ✅ +| TimeoutInput((height, round)) => ... // ❌ + +// 6. oneOf is a method on collections +collection.oneOf() // ✅ +oneOf(collection) // ❌ + +// 7. Reserved keywords cannot be identifiers or record field names +type T = { value: int } // ✅ +type T = { val: int } // ❌ — `val` is a keyword (also `def`, `to`, `from`, `import`, …) +def f(sender: int) = ... // ✅ +def f(to: int) = ... // ❌ — `to` is the built-in i.to(j) operator +``` + +--- + +## 6. Undefined Behavior — What to Guard Against + +These operations have undefined behavior if preconditions aren't met: + +| Operation | Unsafe when | Safe alternative | +| --- | --- | --- | +| `map.get(key)` | key not in map | Pre-populate with `mapBy`, or check `map.keys().contains(key)` | +| `set.getOnlyElement()` | set size ≠ 1 | `oneOf()` inside a `nondet` binding (nondeterministic), or guard the size to 1 first | +| `list.head()` / `list.tail()` | list is empty | Check `list.length() > 0` first | +| `list.nth(i)` | i < 0 or i ≥ length | Check `i >= 0 and i < list.length()` | +| `range(i, j)` / `i.to(j)` | i > j | Ensure `i <= j` | +| `map.set(k, v)` | key not already in map | Use `put(k, v)` instead | + +--- + +## 7. Action Witnesses Pattern + +A witness names a target state, written **positively**, and is checked with `quint run --witnesses`, +which reports how many sampled traces reached it. + +```quint +// state-evidence: a withdrawal pushed someone's balance below the initial value +val withdrawalHappened = users.exists(u => balances.get(u) < INITIAL_BALANCE) + +// multi-condition target +val fullCycleReached = and { + delegations.size() == 0, + users.forall(u => rewards.get(u) == 0), + users.exists(u => balances.get(u) > INITIAL_BALANCE), +} + +// quint run --witnesses withdrawalHappened fullCycleReached spec.qnt +// → withdrawalHappened was witnessed in N trace(s) … (P%) +// N > 0 = reachable; 0 = dead action (over-constrained precondition or bug) +``` + +Add one witness per major action; a witness reached in 0 traces means the action is dead. + +**Make the target a state only an action can produce.** A witness is satisfied by *any* state that +matches, including the initial state — so a predicate that already holds at `init` (e.g. a balance +that starts unequal) is "reached" in 100% of traces at step 0, telling you nothing about the action. +Phrase the target so only the action you care about can make it true (e.g. a balance *below* the +initial value requires a withdraw to have fired), and a non-zero count then genuinely witnesses the +action. + +**Distinguishing *which* action fired (rare).** When several actions can produce the same target +state and you must confirm a *specific* one ran, add a dedicated per-action boolean `var` set in that +action's body and read it in the witness: + +```quint +var lastWasWithdraw: bool // declared; set in EVERY action (false elsewhere) +// ... withdraw sets lastWasWithdraw' = true; other actions set it false ... +val withdrawFired = lastWasWithdraw and withdrawalHappened +``` + +This costs an assignment in every action (Quint requires every `var` set in every action), so reach +for it only when a plain state-evidence target genuinely can't distinguish the case — usually it can. + +When you want an actual *trace* that reaches the state rather than a count, write the negation as an +invariant (`val w = not(target)`) and run `--invariant w` — the reported "violation" is a reaching +execution. + +--- + +## 8. Nondeterministic Testing Pattern + +Use `nondet` with `.oneOf()` for broad coverage with random value selection. + +```quint +run nondetTest = { + nondet amount = 50.to(300).oneOf() + nondet user = USERS.oneOf() + init + .then(transfer(user, "treasury", amount)) + .expect(balances.get(user) >= 0) +} +``` + +Multiple `nondet` bindings explore combinations. The simulator picks values pseudo-randomly; use `--seed` to reproduce a specific run. + +--- + +## 9. Separate Test Files Pattern + +Main spec has no `run` definitions. Tests live in a separate file that imports the spec. + +```quint +// system.qnt — main spec, no tests +module system { + // ... spec only ... +} + +// systemTest.qnt — tests only +module systemTest { + import system.* from "./system" + + run basicTest = { + init + .then(transfer("alice", "bob", 50)) + .expect(balances.get("alice") == INITIAL_BALANCE - 50) + } +} +``` + +Run tests with: +```bash +quint test systemTest.qnt --main systemTest --match basicTest +``` + +--- + +## 10. REPL-First Debugging + +Test pure functions and action sequences in the REPL before writing formal tests. If REPL output surprises you, the spec has a bug. + +```bash +# Test a pure function with a hand-crafted state +echo 'val s = {balances: Set("alice").mapBy(_ => 100)} +transfer(s, "alice", "bob", 50)' | quint repl -r system.qnt::system + +# Test an action sequence +echo 'init +transfer("alice", "bob", 50) +balances' | quint repl -r system.qnt::system +``` + +Anonymous actions are also useful for forcing specific state in the REPL: + +```quint +>>> all { balances' = Set("alice").mapBy(_ => 999), owner' = "alice" } +true +>>> balances +Map("alice" -> 999) +``` + +--- + +## 11. Separate Concerns First + +Before writing any Quint, partition the protocol into three buckets: + +- **State machine** — variables, initialization, transitions +- **Functional logic** — pure computations: quorum checks, message filtering, state updates +- **Properties** — invariants, witnesses, temporal properties + +This partition maps directly to Quint: state machine → `var` + actions, functional logic → `pure def`, properties → `val` invariants + witnesses. Trying to translate everything at once produces specs where logic ends up in the wrong layer. + +--- + +## 12. Extract the System Model + +Before writing `var` declarations, identify the protocol's assumptions explicitly — they're easy to miss when implicit in prose or TLA+. Each becomes a `const` declaration; to actually *check* a condition on those consts, write a `run` test (an `assume` only documents the premise and is not enforced — see the `## Assume` section). + +| Concern | Questions to answer | +| --- | --- | +| **Communication** | Reliable or lossy? Ordered or unordered? Broadcast or point-to-point? | +| **Failures** | Crash-stop? Crash-recovery? Byzantine? What fraction `f` of `n`? | +| **Time** | Synchronous? Asynchronous? Partial synchrony? Are timeouts modelled? | +| **Participants** | Fixed membership or dynamic? How many processes? | + +```quint +pure val N: int = 4 // total processes +pure val F: int = 1 // max faulty + +// Check the assumption — a `run` test actually evaluates it. (`assume` would only document +// it; it is not enforced. Name ends in `Test` so `quint test` picks it up — see tests.md.) +run quorumMajorityTest = { all { 2 * F < N } } +``` + +If an assumption is wrong, every invariant built on top of it is wrong. Surface them before building anything else. + +--- + +## 13. Types-First Scaffolding + +Define all types and run `quint typecheck` before writing any logic. A spec that compiles with only types and `var` declarations is a solid foundation. A spec with correct logic but misaligned types is hard to untangle. + +```quint +// Step 1: define all types +// (Option is not built in — it comes from basicSpells; import it or define +// `type Option[a] = Some(a) | None` yourself.) +type Entry = { term: int, value: str } +type NodeState = { log: List[Entry], votedFor: Option[int], currentTerm: int } +type Message = + | VoteRequest({ term: int, src: int }) + | VoteResponse({ term: int, src: int, granted: bool }) + +// Step 2: declare vars — no logic yet +var nodes: int -> NodeState +var messages: Set[Message] + +// Step 3: quint typecheck — must pass before proceeding +``` + +Use the full type system: records for structured state, sum types for messages with distinct shapes, `NodeId -> LocalState` maps for per-process state, `Set[Message]` for the message soup. + +**Iterate with the user at this step.** Show the type sketch and get explicit approval before writing any functions or actions. + +--- + +## 14. Logic Stubs Pattern + +Write correct `pure def` signatures with placeholder bodies that compile. Fill in real logic one function at a time, testing each in the REPL before moving to the next. + +```quint +// ✅ Stub — correct signature, placeholder body, compiles +pure def isQuorum(voters: Set[int], allNodes: Set[int]): bool = + false // TODO + +pure def applyEntry(state: NodeState, entry: Entry): NodeState = + state // TODO + +// Later — fill in one at a time and test in REPL +pure def isQuorum(voters: Set[int], allNodes: Set[int]): bool = + voters.size() * 2 > allNodes.size() +``` + +**Why stubs first**: type mismatches surface at the signature level before logic exists, when they're cheap to fix. A full stub pass also forces you to think through every function's inputs and outputs before committing to an implementation. + +Only move to the state machine (actions, `init`, `step`) after all stubs compile and all logic is tested. diff --git a/.agents/skills/quint-lang/guidelines/simulations.md b/.agents/skills/quint-lang/guidelines/simulations.md new file mode 100644 index 00000000..7bbc74a6 --- /dev/null +++ b/.agents/skills/quint-lang/guidelines/simulations.md @@ -0,0 +1,308 @@ +# Verification — Witnesses, Invariants, and Test Interpretation + +## Contents + +- [Critical Distinction](#critical-distinction) +- [Before you can run: instantiate the consts](#before-you-can-run-instantiate-the-consts) +- [Witnesses (Liveness — Protocol Makes Progress)](#witnesses-liveness--protocol-makes-progress) +- [Invariants (Safety — Properties Hold)](#invariants-safety--properties-hold) +- [Trace Analysis](#trace-analysis) +- [Result Classification](#result-classification) + +For the full `quint run` / `verify` / `test` flag reference, see `guidelines/cli.md`. This file +is about *running and interpreting* checks. + +## Critical Distinction + +Witnesses and invariants ask **opposite** questions — one wants the state reached, the other wants +it never violated: + +| Check (how to run it) | Good result | Bad result | +|---|---|---| +| **Witness** (`--witnesses`, positive predicate) | ✅ reached in > 0 traces — the state is reachable | ⚠️ 0 traces — unreachable: over-constrained or a bug | +| **Invariant** (`--invariant`) | ⚪ no counterexample in the sampled traces — *not* a proof | ❌ violated — a real bug (one trace disproves it) | + +Never confuse the two. With `--witnesses`, a witness reached in > 0 traces is success; an invariant +violation is a bug. Note the asymmetry: a single trace settles a witness (reached) or refutes an +invariant (violated), but an invariant with *no* counterexample only reflects the traces sampled in +this run — report it that way (see "Simulation is … not a proof" below), never as "safety holds". + +--- + +## Before you can run: instantiate the consts + +`quint run` / `verify` / `test` execute a **concrete** module — every `const` must have a value. +A spec module that declares `const N: int` (or `const Nodes: Set[str]`) cannot be run directly: +it fails with `QNT500: Uninitialized const N`. Giving the const a value via `assume` does **not** +fix this — `assume` states a premise, it does not bind the parameter. + +Instead, write a small concrete **instance module** that imports the spec with the consts bound, +and run *that* module (`--main`): + +```quint +module spec { + const N: int + const Nodes: Set[str] + // ... vars, actions, invariants, witnesses ... +} + +// concrete instance — this is what you run +module spec_3 { + import spec(N = 3, Nodes = Set("n1", "n2", "n3")).* +} +``` + +```bash +quint run spec.qnt --main spec_3 --invariants inv1 inv2 --max-steps 100 +``` + +By convention the instance module is named `Analysis` (e.g. `PaxosAnalysis`) and holds the +concrete `init`/`step` and the witnesses; pass `--main Analysis`, not the parameterized spec +module. (This is the same executability rule the quint-modeling skill enforces while building.) + +--- + +## Witnesses (Liveness — Protocol Makes Progress) + +### Syntax + +Write a witness as the **target state, stated positively** (no negation), and pass it to +`quint run --witnesses`. Quint reports how many sampled traces reached it. + +```quint +// State-change evidence: a deposit raised someone's balance above the initial value +val depositHappened: bool = + users.exists(u => balances.get(u) > INITIAL_BALANCE) + +// State-change evidence: a transfer pushed alice's balance below the initial value +val transferHappened: bool = + balances.get("alice") < INITIAL_BALANCE + +// Multi-condition target +val fullCycleReached: bool = and { + delegations.size() == 0, + users.forall(u => rewards.get(u) == 0), + users.exists(u => balances.get(u) > INITIAL_BALANCE), +} +``` + +Add one witness per major action. A witness reached in **0 traces** means the action is dead — an +over-constrained precondition or a bug. + +Phrase the target so **only an action can make it true** — a predicate that already holds at `init` +is "reached" at step 0 and witnesses nothing about the action. (If several actions can reach the same +state and you must confirm a *specific* one fired, use a per-action flag `var`; see `patterns.md` §7.) + +### Running witnesses + +Pass witnesses (space-separated) to `--witnesses`; they run alongside invariants in one pass: + +```bash +quint run spec.qnt \ + --main MyModule \ + --invariant noNegativeBalances \ + --witnesses withdrawalHappened transferSucceeded \ + --max-steps 100 \ + --max-samples 1000 +``` + +### Result interpretation + +Each witness is reported as a trace count: + +``` +withdrawalHappened was witnessed in 90 trace(s) out of 100 explored (90.00%) +transferSucceeded was witnessed in 0 trace(s) out of 100 explored (0.00%) +``` + +| Count | Meaning | Action | +|---|---|---| +| > 0 traces | ✅ the target state is reachable | done — non-zero coverage confirms it | +| 0 traces | ⚠️ never reached in this sampling | try more steps — see progressive increase below | + +### Progressive increase for 0-trace witnesses + +Do not conclude a state is unreachable after one run. Increase steps first: + +```bash +quint run spec.qnt --witnesses myWitness --max-steps 100 +quint run spec.qnt --witnesses myWitness --max-steps 200 +quint run spec.qnt --witnesses myWitness --max-steps 500 +``` + +If still 0 traces after 500 steps, run with `--verbosity 3` to inspect execution, then diagnose: + +| Symptom | Root cause | Fix | +|---|---|---| +| No actions execute | Protocol stuck at init | Fix `init` or witness precondition | +| Same action loops | Liveness bug or over-constrained guard | Relax action guard | +| Actions run but never reach witness | Witness condition too strong | Weaken the witness | +| Actions run, witness never reached | Real reachability bug in spec | Investigate spec logic | + +### When you want the actual reaching trace + +`--witnesses` gives a count, not an example. When you need the simulator to hand you a concrete +trace that *reaches* the state, write the negation as an invariant — `val w = not(target)` — and run +`--invariant w`; the "violation" it reports is an execution that reaches `target`. The two forms are +complementary, not old-vs-new: use `--witnesses` for the routine reachability + coverage check, and +the negated-invariant form when you specifically want the reaching trace (to understand or document +how the state is reached, or to debug a witness that fires unexpectedly). + +### Witness as a `run` (alternative form) + +```quint +// @witness +run decisionIsReachable = + init + .then(3.reps(i => vote(i))) + .expect(decided == true) +``` + +Use invariant-style witnesses for nondeterministic exploration; run-style witnesses for documenting known reachable paths. + +--- + +## Invariants (Safety — Properties Hold) + +### Running invariants + +```bash +quint run spec.qnt \ + --main MyModule \ + --invariant noNegativeBalances \ + --max-steps 200 + # --max-samples left at its 10000 default — see "Choosing parameters" below; + # don't lower it when confirming a safety invariant +``` + +### Choosing `--max-samples` and `--max-steps` + +These control *how many* traces and *how deep*. Choose them by **what you're doing**, not by habit: + +**`--max-samples` — by purpose.** +- **Debugging / shaking out runtime errors / a first run** — a small sample (e.g. + `--max-samples 50`) is fine. You just want to see the spec execute, catch a crash, or eyeball a + trace; fast feedback beats coverage. +- **Confirming a safety invariant holds** — keep `--max-samples` at its **`10000` default or + higher; never lower it** to go faster. "No counterexample" is only as strong as the number of + traces behind it, and the result is already non-exhaustive — cutting samples directly weakens the + evidence. For an actual guarantee up to a bound, use `quint verify`. + +**`--max-steps` — match it to the protocol's branching, not just its action count.** The default +(`20`) is often too short to reach interesting states. The trap is counting only the named actions +in `step`: a *single* action can expand into many concrete transitions through the `nondet` choices +inside it. A lone `sendMessage` over 3 nodes × 2 message types is **6 distinct transitions per +step**, so a random walk needs well more than 6 steps to exercise meaningful *combinations* — e.g. +to see all 6 (node, kind) pairs sent takes ~12–20 steps, not 1. So estimate the **branching factor** +(number of actions × the `nondet` fan-out inside them), then set `--max-steps` several times higher +than "fire each action once" — enough for the walk to reach a full cycle, a decision, a drained +queue, the state your invariants and witnesses are actually about. Too short and you only ever +witness shallow prefixes. (A small deterministic spec needs few steps; a single action with wide +internal nondet across many participants needs many.) + +### Checking several invariants at once + +When a spec has more than one invariant, **prefer a single run with `--invariants` (plural, a +space-separated list) over spawning one `quint run` per invariant.** They are checked together +(conjoined with AND), so one run covers them all — the same sampled traces exercise every +invariant, it's far cheaper than N separate runs, and any single violation still names the +invariant it broke. + +```bash +quint run spec.qnt --main MyModule \ + --invariants noNegativeBalances totalConserved holderIsMember \ + --max-steps 200 # --max-samples at the 10000 default for a confirmation run +``` + +Use a single `--invariant` (singular) only when you specifically want to isolate one property +(e.g. to focus a counterexample search or reproduce a seed against just that invariant). + +### Result interpretation + +| Output | Meaning | Action | +|---|---|---| +| `"No violation found"` | No counterexample in **this run** | Report as a sampling result, not a proof — see below | +| `"An example execution"` | VIOLATED ❌ | Bug — capture seed and analyze trace | + +> **Simulation is bounded random sampling, not a proof.** "No violation found" means no +> counterexample turned up in the traces that were sampled — it does **not** establish that the +> invariant holds in every reachable state. When reporting to the user, say "no counterexample +> found across N sampled runs (max-steps M, seed S)" and state the coverage as plain facts; do +> not call the property "proven", "verified", "safe", or "correct", and do not judge whether the +> sample size was "enough" — that is the user's call. For an actual exhaustive guarantee over a +> bounded horizon, use `quint verify` (bounded model checking), and even then scope the claim to +> that bound. Always offer next steps (more samples / a different seed / `quint verify` / tighten +> the property) and frame them as the user's decision. + +### Bug protocol: invariant violated + +**Step 1 — Capture details** +```bash +# Re-run with the seed from the violation output +quint run spec.qnt --invariant myInv --seed --verbosity 3 --main MyModule +``` + +**Step 2 — Analyze trace** +- Find the step N where the invariant became false +- Find the action that caused the transition to step N +- Extract the relevant state snapshot: which variables changed, what values + +**Step 3 — Determine root cause** + +| Scenario | Root cause | Fix | +|---|---|---| +| Invariant violated at step 0 | `init` produces invalid state | Fix `init` | +| Invariant holds at init, fails after action X | Action X has wrong guard or wrong update | Fix action X | +| Invariant seems too strict | Invariant itself is wrong | Weaken invariant | + + +## Trace Analysis + +Use `--verbosity 3` whenever you need to understand a failure or a never-violated witness. + +```bash +quint run spec.qnt --invariant myInv --seed --verbosity 3 --max-steps 50 +``` + +### Verbosity levels + +`--verbosity 3` is the one to reach for when diagnosing — it adds `nondet` picks and state changes on +top of the default example trace. The full 0–5 range is in `cli.md`'s verbosity guide. + +### `--mbt` — see *which* action fired at each step + +`--verbosity 3` shows state changes and picks, but not always *which named action* produced each +step. Add `--mbt` to surface that: each state in the trace carries `mbt::actionTaken` (the action +name) and `mbt::nondetPicks` (the values chosen inside it). + +```bash +quint run spec.qnt --invariant myInv --seed --verbosity 3 --mbt --max-steps 50 +``` + +Reach for it when a trace reaches the critical state but it's unclear which action drove the +transition — e.g. several actions could produce the same state, or a witness fires unexpectedly and +you need to name the culprit. (For *confirming* a specific action fired as a property rather than +reading it off a trace, the per-action flag `var` in `patterns.md` §7 is the structural alternative.) + +### Analysis steps + +1. Scan for repeated actions — signal of a stuck loop +2. Find the critical transition: step where invariant violated or witness should have fired +3. Read state before and after that step — what changed? +4. Trace backwards: which actions led here, what nondeterministic picks were made + +--- + +## Result Classification + +After running all checks. These classify **what the sampled runs showed** — they describe the +run, not a verdict on the spec's correctness (sampling is not a proof; see the note above). + +| Run outcome | Condition | How to report it | +|---|---|---| +| Clean run | Every witness reached in > 0 traces, all invariants without counterexample, all tests pass | "No counterexamples across the sampled runs; all target states reached. Not exhaustive." | +| Liveness concern | One or more witnesses reached in 0 traces after 500+ steps | "Target state not reached in sampling — possibly over-constrained, or sampling missed it." | +| Safety concern | Any invariant violated | "Counterexample found — this IS a bug (one trace is enough to disprove). Capture seed, analyze." | + +Note the asymmetry: a violation is conclusive (a single counterexample disproves a safety +property), but the absence of one is not — it only summarizes the traces that were sampled. diff --git a/.agents/skills/quint-lang/guidelines/tests.md b/.agents/skills/quint-lang/guidelines/tests.md new file mode 100644 index 00000000..eb5d319a --- /dev/null +++ b/.agents/skills/quint-lang/guidelines/tests.md @@ -0,0 +1,308 @@ +# Writing and Debugging Tests + +## Contents + +- [File Structure](#file-structure) +- [Writing Tests](#writing-tests) +- [Running Tests](#running-tests) +- [Debugging Failures](#debugging-failures) +- [REPL-First Debugging](#repl-first-debugging) + +## File Structure + +Tests live in a separate file that imports the main spec. The main spec has no `run` definitions. + +```quint +// system.qnt — spec only, no tests +module system { + // ... vars, actions, invariants ... +} + +// systemTest.qnt — tests only +module systemTest { + import system.* from "./system" + + run basicTest = { + init + .then(transfer("alice", "bob", 50)) + .expect(balances.get("alice") == INITIAL_BALANCE - 50) + } +} +``` + +Run with: +```bash +quint test systemTest.qnt --main systemTest --match basicTest +``` + +> **Naming gotcha:** by default `quint test` runs only `run` definitions whose name **ends in +> `Test`** (e.g. `basicTest`). A `run` named otherwise (`happyPath`, `testBasic`, `checkQuorum`) is +> **silently skipped** — no error, no output, exit 0 — which reads as "passed." Either suffix the +> name with `Test`, or select it explicitly with `--match ` (or `--match '.*'` for all). + +--- + +## Writing Tests + +### Basic pattern + +```quint +run happyPath = + init + .then(action1(args)) + .expect(condition1) + .then(action2(args)) + .expect(condition2) +``` + +### Operators + +| Operator | Meaning | +|---|---| +| `a.then(b)` | Execute `a`, then execute `b` from resulting state | +| `a.expect(p)` | Execute `a`, fail if `p` is false in resulting state | +| `n.reps(i => A)` | Execute action `A` n times; iteration index passed as `i` | +| `a.fail()` | Asserts `a` evaluates to false (action should be blocked) | +| `assert(p)` | Does not change state; fails if `p` is false | + +### Repeat an action N times + +```quint +run threeVotes = + init + .then(3.reps(i => vote(i))) + .expect(votes.size() == 3) +``` + +### Negative test — assert an action is blocked + +```quint +run cannotDoubleVote = + init + .then(vote("alice")) + .then(vote("alice").fail()) // second vote must fail +``` + +### Assert mid-trace + +An `assert` must sit inside an `all { }` block that also assigns the state +variables. A standalone `.then(assert(...))` step assigns nothing, so its effect +cannot unify with the rest of the trace and the test fails to typecheck. For an +after-the-fact check, use `.expect(...)` instead of a trailing assert step. + +```quint +run checkTiming = + init + .then(all { vote(1), assert(votes.size() == 0) }) // assert BEFORE vote executes + .expect(votes.size() == 1) // assert AFTER +``` + +### Nondeterministic tests + +```quint +run nondetTest = { + nondet amount = 50.to(300).oneOf() + nondet user = USERS.oneOf() + init + .then(transfer(user, "treasury", amount)) + .expect(balances.get(user) >= 0) +} +``` + +Multiple `nondet` bindings explore combinations. Use `--seed` to reproduce a specific run. + +### Conditional expectations — one test covering several branches + +When the *correct* outcome depends on how the random inputs relate to each other, don't write one +fixed assertion (it can't be right for every draw) and don't split into many hardcoded tests. +Instead branch the `.expect()` on the inputs and assert the outcome that's correct **for that +branch** — a single `run` then exercises the whole decision surface. + +```quint +// withdrawCapped is always enabled; it takes the whole balance if asked for more. +run withdrawTest = { + nondet amount = 50.to(150).oneOf() + init // balance starts at 100 + .then(withdrawCapped(amount)) + .expect(if (amount > 100) + balance == 0 // asked for more than held → capped to the balance + else + balance == 100 - amount) // normal deduction +} +``` + +The branch condition uses the `nondet` values; each arm asserts the result appropriate to that +relationship (under/at/over a threshold, full/partial, etc.). + +**Caveat — this only works while the action stays *enabled* across all branches.** A `.then(act)` +on a **disabled** action (its guard is false) cannot proceed: the test stops with `Cannot continue +to "expect"` before the `.expect` is ever evaluated — so you **cannot** write an `if`-arm meaning +"in this case the action was blocked, assert nothing changed." Conditional `.expect()` is for an +always-enabled action whose *result* differs by input. To test that an action is correctly +**blocked** for some inputs, use a separate `.then(act.fail())` step instead (the spec uses +disabled actions, not a `success` boolean — see `patterns.md`). + +### Variable scope inside `.expect(...)` + +A `val` bound inside an `and { ... }` is **not** in scope for sibling conditions or an `if` guard in +the same block — you get an "unresolved name" error: + +```quint +// ❌ actualReward is not visible to the `if` below it +.expect(and { + val actualReward = reward - commission + balance == initial + actualReward, + if (actualReward > 0) balance > initial else true, // ERROR: actualReward not in scope +}) +``` + +Wrap the conditions in an `all { }` so the shared `val`s scope over all of them — and remember +`all { }` separates conditions with **commas**, not `and`: + +```quint +// ✅ shared vals scope over the whole all-block +.expect( + val actualReward = reward - commission + all { + balance == initial + actualReward, + if (actualReward > 0) balance > initial else true, + }) +``` + +--- + +## Running Tests + +```bash +# Run a specific test +quint test systemTest.qnt --main systemTest --match basicTest + +# Run all tests matching a pattern +quint test systemTest.qnt --main systemTest --match ".*" + +# Run with detailed trace output +quint test systemTest.qnt --main systemTest --match basicTest --verbosity 3 + +# Reproduce a failure with a seed +quint test systemTest.qnt --main systemTest --match basicTest --seed 0x1a2b3c +``` + +--- + +## Debugging Failures + +### `quint test` output is sparse — it does NOT show the state + +On a failure, `quint test` prints only the error code, the offending `run`, and a seed — **no +state, no trace, at any `--verbosity`** (even `--verbosity 5` adds nothing for a test). The two +failure modes read differently, and the error text is your only clue which one you hit: + +``` +Error [QNT508]: Expect condition does not hold true ← an .expect(...) evaluated to false +Error [QNT508]: Cannot continue to "expect" ← a .then(action) was DISABLED (its guard + was false), so the trace couldn't proceed +``` + +Both underline the **whole `run` expression**, not the specific `.expect` or action that failed, and +neither tells you the actual values. To debug, you must recover the state yourself — two reliable +ways below. + +### Recover the state — Option A: replay in the REPL (best for exploring) + +Feed `init` then each action of the failing trace into the REPL and query whatever the `.expect` +checked. Each action prints `true` and the state transition (`old => new`); then evaluate any +expression at the current state: + +```bash +printf 'init\ntransfer("alice","bob",50)\nstate.balances.get("alice")\n' \ + | quint -r system.qnt::system --backend=typescript +# >>> true +# { state: { balances: Map("alice" -> 100 => 50, "bob" -> 100 => 150) } } +# >>> 50 ← the value your .expect compared; here 50, so `== 999` is obviously false +``` + +**`--backend=typescript` is required for piped/non-interactive REPL input.** The default Rust +backend closes its readline on EOF and evaluates nothing (`ERR_USE_AFTER_CLOSE: readline was +closed`) — so without the flag this recipe silently produces no output. (Interactive TTY use works +on either backend; it's specifically piped stdin that needs `typescript`.) + +For nondeterministic stepping, set a seed so the run is reproducible: `.seed=` in the REPL +(or `--seed` on the CLI) makes every `nondet`/`oneOf` pick deterministic — same seed, same trace. +Capture the seed `quint test`/`quint run` prints on a failure and replay it to reproduce exactly. + +### Recover the state — Option B: dump the trace with `--out-itf` (scriptable) + +`quint test --out-itf` writes the full state trace of every test (passing *and* failing) to a JSON +file — useful in scripts or when you want the machine-readable trace: + +```bash +quint test systemTest.qnt --match myTest --out-itf "out_{test}_{seq}.itf.json" +# then read the states from out_myTest_0.itf.json (the last state is where it stopped) +``` + +### Then map the trace to the test code and classify the bug + +| Symptom | Type | Fix | +|---|---|---| +| Action doesn't update the field being checked | Spec bug | Fix the action's state update | +| `.expect()` checks a field before the action that sets it | Test bug | Move the `.expect()` later in the chain | +| `.expect()` uses wrong value | Test bug | Correct the expected value | +| Action sets a wrong value | Spec bug | Fix the pure function logic | + +### Common mistakes + +```quint +// ❌ Checking state before it's set +run bad = + init + .then(all { action1, assert(result == 42) }) // assert runs BEFORE action1 updates state + +// ✅ Check after +run good = + init + .then(action1) + .expect(result == 42) + +// ❌ Forgetting action order matters +run bad = + init + .then(approve(100)) + .expect(balance == INITIAL - 100) // approve doesn't deduct — transfer does + +// ✅ Complete the sequence +run good = + init + .then(approve(100)) + .then(transfer(100)) + .expect(balance == INITIAL - 100) +``` + +--- + +## REPL-First Debugging + +Before writing a failing test, isolate the problem in the REPL: + +```bash +# Interactive (TTY): either backend works +quint -r system.qnt::system + +# Force a specific state with an anonymous action, then step and query +>>> all { balances' = Set("alice").mapBy(_ => 100), phase' = "ready" } +true +>>> transfer("alice", "bob", 50) +true +>>> balances.get("alice") +50 +``` + +When driving the REPL **non-interactively** (piping commands, e.g. from a script or an agent), add +`--backend=typescript` — the default Rust backend evaluates nothing on piped stdin (see Option A +above): + +```bash +printf 'init\ntransfer("alice","bob",50)\nbalances.get("alice")\n' \ + | quint -r system.qnt::system --backend=typescript +``` + +If the REPL shows wrong output, the spec has a bug. If the REPL shows the right output but the test fails, the test chain has a sequencing error. diff --git a/.agents/skills/quint-modeling/SKILL.md b/.agents/skills/quint-modeling/SKILL.md new file mode 100644 index 00000000..85172898 --- /dev/null +++ b/.agents/skills/quint-modeling/SKILL.md @@ -0,0 +1,466 @@ +--- +name: quint-modeling +description: > + Build a Quint model of a system, protocol, or algorithm. Use this whenever the user wants to + model, spec out, formally describe, model-check, or verify a system in Quint — e.g. "model this + protocol in Quint", "spec out this design", "translate this TLA+", "formally check this Rust + code" — even if they never say the word "specification." When the goal is to verify or + model-check a design or implementation and no Quint model exists yet, writing the model is the + required first step, so start here. It generates the spec from whatever the user has — an idea + developed interactively, natural-language or functional requirements, source code (Rust, Go, + TypeScript, etc.), or an existing TLA+ specification — and walks the modelling flow (state, + actions, invariants), adapting to the source type. Also use this to **review or audit an + existing Quint spec** — "review my .qnt", "audit this spec before I ship it", "is this model + any good" — it carries the structural + runtime review checklist. Do NOT use this for + implementing code against a spec that already exists (that's quint-execute-spec) or for pure + Quint syntax/CLI/debugging questions (quint-lang). For Quint language syntax and the CLI, see the + quint-lang reference. +--- + +# Quint Modelling + +Produce a Quint specification from whatever the user starts with. This skill owns the +**shared modelling discipline** (below) and **routes** to a flow-specific guideline for the +intake — the part that turns a particular kind of input into the understanding the shared +steps build on. + +For language syntax, operators, the CLI, and `basicSpells`, defer to the **quint-lang** +reference. This skill is about *how to model*, not Quint syntax. + +--- + +## Pick the flow + +Identify what the user is starting from, then read the matching guideline. The guideline +handles **intake** — extracting the system's state, operations, and assumptions from that +source. After intake, everyone converges on the **Shared modelling spine** below. + +| Starting point | Flow | Guideline | +|---|---|---| +| Only an idea / informal proposal, built interactively with the user | **from nothing** | `guidelines/from-nothing.md` | +| A written requirements / functional-spec document | **from requirements** | `guidelines/from-requirements.md` | +| Source code (Rust, Go, TypeScript, …) | **from code** | `guidelines/from-code.md` | +| An existing TLA+ specification | **from TLA+** | `guidelines/from-tlaplus.md` | +| A **finished `.qnt` spec to audit**, not build | **review** | `guidelines/review.md` | + +The first four are **build** flows: intake → the Shared modelling spine below. The **review** +flow is different — there is no new model to produce; you audit an existing spec against a +checklist and report findings. It does not use the spine; read `guidelines/review.md` and follow +it directly. + +If the starting point is ambiguous (e.g. "a design doc with some pseudocode"), ask the user +which source is authoritative before picking — the intake differs materially. + +--- + +## Shared modelling spine + +Every flow lands here. Intake (flow-specific) produces an **understanding** of the system: +its entities, the operations they perform, the state each operation reads and writes, and the +system-model assumptions (communication, failures, time, participants). The spine turns that +understanding into a verified Quint spec. + +Two principles hold throughout: + +- **Executable from the first line.** Quint specs run immediately — you validate design + decisions as you make them, not at the end. Never build more than one step without + verifying the previous one: `quint typecheck`, **then `quint run`**. Typecheck is not + enough — a spec can typecheck and still fail to *execute*, which defeats the whole point of + an executable spec. The trap to watch for: a **`const` not given a value** typechecks but + cannot run — `quint run` fails with `Uninitialized const`. To make the spec runnable, + **instantiate the const in a concrete instance module** — + `module spec_3 { import spec(C = Set(1,2,3)).* }` — and run *that* module. (`assume` documents a + premise; it does not supply a value — see Step 1 and quint-lang's `guidelines/simulations.md`.) + After each addition, actually run it — if you can't `quint run` the module, it isn't done. + **Use `quint run` only — never `quint verify`.** This holds for the whole skill: building, + iterating, reviewing, sanity-checking invariants — every step uses sampled `quint run`, not once + `quint verify`, not even as a final pass. `verify` is exhaustive bounded model checking + (Apalache): far slower (minutes to hours) and not part of the modeling workflow. It is a + *model-checking* tool, documented in quint-lang (`guidelines/cli.md`, `guidelines/simulations.md`) + — reach for it **only when the user explicitly asks to model-check** the spec, never on your own. +- **What, not how.** Model observable state transitions and their effects. Abstract away + implementation detail (serialization, memory management, retry plumbing). If a detail + doesn't affect an invariant you care about, it doesn't belong in the spec. + +#### Reference examples + +Two complete, runnable specs are bundled in `examples/`, one for each of the **two ways +components coordinate** — by **passing messages** or by **sharing state**. Almost any system maps +onto one of them; pick by *that* question, not by domain label, and read the closer match as a +worked instance of the spine patterns (Step 2 state-shaping, Step 4 guarded actions, Step 5 +witnesses + invariants) — to see how the pieces compose into a whole spec. (They are canonical +shapes to learn from, not the only valid ones.) + +| Example | Coordinates by… | Read it when modelling… | +|---|---|---| +| `examples/tendermint/` | **message passing** (`choreo::` broadcast/send; real BFT consensus) | **any distributed protocol where parties exchange messages** — consensus, BFT, replication, atomic commit, leader election, reliable broadcast/gossip, request/response, a mempool. This is the **default** reference for distributed systems. Shows: grouped `Id -> LocalState` (Step 2), guarded transitions (Step 4), `choreo::cue` listen/act split, safety invariants (`agreement`/`validity`) + an accountability/liveness angle + a counterexample demo (Step 5), `const`-instantiation, and test separation. Its banner has a "quick read" path so you needn't read all ~760 lines. See also Step 2's "When to use Choreo" and `../quint-lang/guidelines/choreo.md`. | +| `examples/ewd426.qnt` | **shared state** (no messages) | systems whose parts coordinate through **common state** rather than messages — mutexes/locks, token rings, shared registers, self-stabilization, anything where a process reads its neighbours/the environment directly. Shows: grouped-map state (Step 2), guarded `step` (Step 4), `const`-instantiation, and `temporal` **liveness** properties (Step 5). | + +If a system has both (e.g. message-passing nodes that also touch a shared ledger), start from +**tendermint** (the message-passing structure dominates) and add shared state as its own `var`, +per Step 2. Plain message *soup* (`Set[Msg]`) without the Choreo framework isn't a separate file — +Choreo's broadcast is soup underneath, so `tendermint/` is also the reference for that shape (read +the state decls + a guarded transition; the `choreo::` wrapper is the only added layer). + +### Step 1 — Separate concerns + +Partition the understanding into three layers. Keeping them apart is what makes a spec +testable; mixing them is the most common source of untestable specs. + +- **State machine** — variables, initialization, transitions → `var`, `init`, actions +- **Functional logic** — pure computations: guards, quorum checks, state updates → `pure def` +- **Properties** — what must always hold, what must be reachable, what must *eventually* happen → + `val` invariants + witnesses, plus `temporal` properties for liveness (Step 5; quint-lang has the detail) + +Also pin down the **system-model assumptions** — they shape every invariant built on top. +Encode them as **`const` declarations**. To actually *check* an assumption (e.g. a quorum +condition like `2 * f < n`), write a **`run` test** that asserts it — the `assume` keyword +*documents* a premise but is not enforced, so don't rely on it as a check (see quint-lang's +`## Assume` for why and the `run`-test pattern). Each flow's intake fills this in from its own +source (interview, requirements doc, code, or TLA+); the checklist is the same regardless: + +| Concern | Questions to answer | +|---|---| +| **Communication** | **How do the actors communicate — messages or shared state? If messages: reliable or lossy? ordered or unordered? broadcast or point-to-point?** (The other shaping question — answer it right after actors; it picks the message medium in Step 2 and, with the actor count, settles the Choreo decision.) | +| **Failures** | Crash-stop? Crash-recovery? Byzantine? What fraction `f` of `n`? | +| **Time** | Synchronous? Asynchronous? Partial synchrony? Are timeouts modelled? | +| **Actors / Participants** | **Who are the actors and roles, and what local state does each hold?** (This is the most concrete intake question — answer it first; it feeds Step 2's `Id -> LocalState` shaping directly.) Fixed membership or dynamic? How many processes? | + +If an assumption is wrong, every property built on it is wrong — surface them before building. + +### Step 2 — Shape the state + +**Start from the two questions you answered in Step 1 — *who the actors are* and *how they +communicate*. Together they drive every structural decision this step makes.** They are not just +background; they pick the shape, the message medium, and the framework: + +- **How many actors → the state shape.** One actor → a single `var s: Record`. N actors of the + same kind → one `Id -> LocalState` map (the record describes *one* actor; see "Scaling to N" + below). Distinct roles → either a field on `LocalState` (e.g. `role: Coordinator | Participant`) + or separate maps. Get the actor count/roles right and the shape falls out. +- **How actors communicate → the message medium.** Messages → a separate `var` for the medium, + *not* crammed into `LocalState`: unordered `Set[Message]` soup (the default), or `... -> + List[Message]` ordered per-pair queues only when delivery order is load-bearing. No messages, + coordination through common state → shared memory (no medium var; see `examples/ewd426.qnt`). + This is the "Communication shapes" choice detailed just below. +- **Actors + communication → Choreo or not.** Multiple actors coordinating by **exchanging + messages** → default to Choreo (see "Default to Choreo" below). A single actor, or actors + coordinating through **shared state** → plain Quint. So answer "who, how many, and how do they + talk?" first, and all three decisions below are mostly settled before you write a type. + +**Group cohesive state into a record** and let the functional layer operate on that record — this is +the recommended shape, because most of the time the variables that describe one entity *are* +cohesive, and grouping keeps the functional layer clean and the invariants readable. Keeping +variables flat is the deliberate exception, not a co-equal default: + +- **Group into a record when** the fields are the local state of one entity (a node's phase + log + + vote), are always passed together, or an invariant relates several of them. This is the usual case. +- **Keep flat (separate `var`s) only when** the variables are genuinely independent concerns that + change on their own, or the spec is small enough that grouping adds no clarity. (Some canonical + examples, e.g. `ewd840`, keep per-field maps flat because the fields don't form an obvious unit.) + +**The cohesive case — one unit:** + +```quint +type State = { + phase: str, + balances: NodeId -> int, + members: Set[NodeId], +} + +var state: State +``` + +The `apply…` pure functions take and return `State`; actions assign the next-state record directly +(`state' = applyTransfer(state, …)`). No per-field reassembly. + +**Scaling to N instances** — when there are N actors, the record describes **one** actor's +local state, and the variable becomes a map keyed by actor id. The pure functions are +**unchanged** — they still operate on a single `LocalState`; only the wiring around them +changes: + +```quint +type NodeId = int +type LocalState = { phase: str, balance: int, voted: bool } + +var nodes: NodeId -> LocalState // per-actor cohesive state +var messages: Set[Message] // genuinely global concern → its OWN var, not inside LocalState +``` + +| | One instance | N instances | +|---|---|---| +| state var | `var state: State` | `var nodes: NodeId -> LocalState` | +| pure fns | `can…(State, …): bool` + `apply…(State, …): State` | `can…(LocalState, …): bool` + `apply…(LocalState, …): LocalState` — same shape | +| action read | reads `state` | `nodes.get(id)` | +| action write | `state' = applyOp(state, …)` | `nodes' = nodes.set(id, applyOp(nodes.get(id), …))` | +| `init` | one record literal | `NODES.mapBy(_ => )` | + +The guideline, stated once: **group cohesive state into a record (one var for one unit, an +`Id -> LocalState` map for N), and keep genuinely independent concerns — the message soup, a shared +registry — as their own separate vars**, never crammed into the per-instance record. Reach for flat +per-field vars only when the fields don't cohere into a unit. + +**Communication shapes** (decide alongside the state shape): +- **Message soup** — `var network: Set[Message]`, processes receive any in-flight message. The + default for asynchronous protocols; use it unless ordering genuinely matters. +- **Shared memory** — no message medium; processes read/write common state directly. See the + bundled **`examples/ewd426.qnt`** (a token ring where each node reads its neighbours). +- **Ordered per-pair queues** — `... -> List[Message]` with head/tail ops. Use **only when + delivery order is semantically required** (e.g. logical-clock causality); it is more expensive + to model-check than soup. The corpus spec `LamportMutex` is the canonical example — prefer + soup unless you can name why ordering is load-bearing. + +#### Default to Choreo for distributed protocols + +For **any distributed, message-passing protocol with N processes** (consensus, BFT, replication, +multi-phase commit, broadcast/gossip, leader election), **default to the Choreo framework** — and +make a deliberate, justified decision if you are *not* going to. This isn't a stylistic toss-up: +Choreo's listen → react structure and its clean split between local-state updates and network +effects (the `choreo::cue` pattern) actively push you toward a better-organized, more reviewable +spec than a hand-rolled `NodeId -> LocalState` + message-soup `step`. The default expectation is +Choreo; plain Quint for a message-passing protocol is the thing that needs a reason. + +**The one legitimate reason to decline:** on a *small* protocol, Choreo's scaffolding (the type +boilerplate — `LocalContext`/`Transition`/effects — and framework wiring) can add more ceremony +than the spec's structure is worth, making it materially more verbose for little gain. That's a +real judgment call — but it is the **only** routine exception, and even then lean toward Choreo. +Two structural cases also fall outside Choreo: a **single actor** (no inter-process messaging to +choreograph) and **shared-memory coordination** (no message medium at all → plain Quint, see +`examples/ewd426.qnt`). + +Not a reason: *"still exploring the design."* You can explore *in* Choreo, and switching frameworks +later is expensive — so don't defer the decision on those grounds. + +**This is a structural fork — decide it before writing logic.** If you intend to go plain Quint for +a protocol that would otherwise be a Choreo candidate, **say so to the user, name the +verbosity/benefit tradeoff, and get agreement** (fold this into the type-sketch sign-off below). + +When Choreo fits, the per-process state still follows the grouping rule above — Choreo just +supplies the messaging and handler scaffolding around it. See +`../quint-lang/guidelines/choreo.md` for the framework's API and patterns, and +**`examples/tendermint/`** for a complete, runnable Choreo spec — real BFT consensus +demonstrating the `choreo::cue` listen/act split, `agreement`/`validity`/`accountability` +invariants, and `with_cue`-based tests in a separate `tendermintTest.qnt` (run via +`--main=valid`). + +Decide grouping (and Choreo-or-not) with the user and **get explicit approval on the type +sketch before writing any logic** (Step 3). Typecheck the types + `var` declarations first; +this is the highest-leverage review point — a wrong shape is expensive to unwind later. + +### Step 3 — Write the functional logic (pure functions) + +Implement each operation's logic as `pure def`s — they take the state record and arguments and +return plain values, so you can exercise them in the REPL with hand-built states. Split the two +distinct questions a transition answers: + +- **Can it happen?** — a `pure def … : bool` guard (the precondition). +- **What's the resulting state?** — a `pure def … : State` that computes the next state, *assuming* + the guard holds. + +```quint +pure def canTransfer(s: State, from: NodeId, recipient: NodeId, amount: int): bool = + amount > 0 and from != recipient and s.balances.get(from) >= amount + +pure def applyTransfer(s: State, from: NodeId, recipient: NodeId, amount: int): State = + { ...s, balances: s.balances.setBy(from, b => b - amount) + .setBy(recipient, b => b + amount) } +``` + +Write signatures first (stub the bodies), typecheck, then fill in one function at a time and +exercise each in the REPL with a hand-built state before moving on. If the output surprises +you, the function has a bug — fix it before wiring it into the state machine. + +> Keep the guard separate from the update because they are separate facts about the operation: one +> says *when* it is allowed, the other says *what it does*. The action (Step 4) uses the guard to +> decide whether it fires at all. A single `pure def` returning `{ success, newState }` — handing +> back the unchanged state when the guard fails — fuses the two and pushes the action toward a no-op +> branch; prefer the split. (If a *failed* operation is itself something the system observes and +> reacts to, model that failure as its own guarded action, not as a fallback.) + +### Step 4 — Wire the state machine (guarded actions) + +An action puts the guard and the update together in one `all { ... }`: the guard gates whether the +action fires, the update sets the next state. When the guard is false the action is **disabled** — +it does not fire and does not change anything. No `success` flag, no no-op branch. + +```quint +action init: bool = all { + state' = { phase: "idle", + balances: NODES.mapBy(_ => INITIAL_BALANCE), // pre-populate every map + members: NODES }, +} + +action transferAction(from: NodeId, recipient: NodeId, amount: int): bool = all { + canTransfer(state, from, recipient, amount), // guard — disables the action when false + state' = applyTransfer(state, from, recipient, amount), +} +``` + +A disabled action is the faithful model of "this cannot happen in this state." Resist adding a +blanket `else` / `unchanged_all` fallback to keep things moving — that puts a transition in the +model that the real system can't take, which is a modeling inaccuracy regardless of what you do +with the spec afterward. + +An action and a `pure def` cannot share a name. Pre-populate every map in `init` — `.get()` on an +absent key is undefined behavior. See quint-lang for the full list of such gotchas. + +### Step 5 — Properties: witnesses first, then invariants + +**Witnesses first.** A witness names a target state and asks whether it is *reachable* — write the +predicate **positively** (the state you hope to reach) and pass it to `quint run --witnesses`, which +reports **how many sampled traces reached it**. Add one per major action: a witness reached in 0 +traces means the action is dead (its precondition can never hold), so fix that before spending +effort on safety. + +```quint +// the state you want to be reachable — written plainly, no negation +val someBalanceChanged: bool = + state.members.exists(n => state.balances.get(n) != INITIAL_BALANCE) +``` + +``` +quint run spec.qnt --witnesses someBalanceChanged --max-steps 10 + → someBalanceChanged was witnessed in 90 trace(s) out of 100 explored (90.00%) +``` + +A non-zero count means the state is reachable; **0% means it never happened** — investigate. + +There is a second, complementary form: write the *negation* as an invariant — `val w = not(target)` +— so a reported "violation" is an execution that *reaches* the state. It can't batch with the other +invariants and you read a "violation" as success, but it gives you something `--witnesses` cannot: an +**actual trace** that reaches the state. Pick by what you need — `--witnesses` for the routine +reachability + coverage check, the negated-invariant form when you want to *see the path* (to +understand or document how the state is reached, or to debug a witness that fires unexpectedly). + +**Then invariants** — the safety properties that must hold in every reachable state: + +```quint +val noNegativeBalances: bool = + state.members.forall(n => state.balances.get(n) >= 0) +``` + +Invariants and witnesses run together in one pass: `quint run --invariant noNegativeBalances +--witnesses someBalanceChanged`. + +When a source yields *many* candidate properties and you need to prioritize verification effort, +the from-requirements flow describes a High/Medium/Low impact-scoring aid (`guidelines/from-requirements.md`); +it applies wherever you're modelling against stated requirements. + +(Witness vs invariant interpretation, fairness, and temporal properties are detailed in quint-lang's +`guidelines/simulations.md`. `someBalanceChanged` is *state* evidence — phrase such a target so only +an action can make it true, since a predicate that already holds at `init` is "reached" at step 0 and +witnesses nothing. To confirm a *specific* action fired when several could reach the same state, use a +per-action flag `var` — quint-lang's `guidelines/patterns.md` §7 covers it and how to debug a witness +that never fires.) + +### Step 6 — Compose `step` and simulate + +Compose the actions in `any { ... }` and stress the model with random walks. + +```quint +action step: bool = { + nondet from = NODES.oneOf() + nondet dst = NODES.oneOf() // `dst`, not `to` (a built-in) — see quint-lang + any { + transferAction(from, dst, 10), + // other actions… + } +} +``` + +- Check **witnesses** with `quint run --witnesses` — expect a non-zero trace count (reachable); + 0% means a dead action. +- Check **invariants** with `quint run` (sampled) — expect no violation; read counterexample + traces step by step when one appears. Use `quint run` for this, not `quint verify` (see the + "Executable from the first line" principle — `verify` is only for when the user explicitly asks + to model-check). + +#### A guarded model can reach a state where nothing is enabled + +Because actions are guarded (Step 4), the model can reach a state with **no enabled action**. The +trap during a build: **`quint run` does not detect deadlocks** — it stops early and prints `[ok] No +violation found` with a trace shorter than `--max-steps`. A short trace is the only hint, and the +run reads as success, so watch trace length when you expect the protocol to keep going. (For the +record, `quint verify` reports a deadlock outright — `Found a deadlock` — but that's a +model-checking step, only in play if the user explicitly asks for it; don't switch to `verify` just +to chase a suspected deadlock mid-build — inspect the short trace with `quint run` instead.) + +Whether reaching such a state is correct depends on the system: one that should keep serving must +never get stuck, while one that genuinely finishes (everyone committed, nothing left to do) is +*supposed* to end in a terminal state. The tool can't tell these apart — only you know which you +intended. (This is the other reason Step 4 avoids the no-op fallback: a blanket `unchanged_all` +lets every trace extend forever, so the model can never reach — or reveal — such a state.) + +**A note on bounding.** `--max-steps` (how deep a run explores) is not a modeling decision — it's +always there. Whether the *model itself* has a bounded state space is: some protocols are +**terminating by construction** (a one-shot commit reaches a final state and stops), and modeling +them that way is correct — reaching a terminal state is the design, not a deadlock. You may also +bound an otherwise-unbounded model deliberately to shrink the state space, but never with a no-op +escape hatch (Step 4) — bound it by modeling the real terminal states, not by a fake step. + +Tests (`run` definitions) go in a **separate `*_test.qnt` file** that imports the spec — keep +the main module free of test scenarios. Build incrementally: typecheck after every addition, +simulate after every action. + +### Step 7 — Self-review, then hand off the spec + +Before writing the handoff record, do a quick pass over your own spec against the same bar a +reviewer would apply — catching these now is cheaper than having them bounce back. Most you've +already followed while building; this is the closing check that they all hold together: + +- **Guarded actions** — no business logic in an action beyond the guard and assignments; it lives + in `pure def`s, and the guard is lifted into the action so it's disabled when it can't fire (Step 4). +- **Witness per major action** — every action in `step` has a witness, and each one is reached in + > 0 traces under `quint run --witnesses`, not dead at 0 traces (Step 5). +- **At least one protocol-level invariant** — a real safety property (no two leaders, conservation, + agreement), not just a type/bounds check; and every invariant references a `var`. +- **`init` assigns every `var`** and every action assigns every `var` (a missing `x' = x` is a + silent stutter bug); every map is pre-populated (Step 4). +- **Right abstraction** — IDs are opaque (no string manipulation), messages are a `Set` unless + ordering is the property, no serialization/retry/memory detail leaked in. + +If a spec is large or being handed to someone else, run the full audit in `guidelines/review.md` +(the C1–C6 / R1–R2 checklist with a report) — reach for it when a self-pass isn't enough. + +A finished spec needs a short record so the next reader knows its scope and can trust it. +Produce one (as a comment block, a `README.md`, or whatever the flow prefers) covering: + +- **What it covers** — which modules/protocols are modelled, which assumptions (`const` + declarations, with `run`-test checks for the ones that must hold) are encoded, which + properties are verified. +- **What it does NOT cover** — implementation detail left out on purpose, edge cases excluded + (and why), properties still to add. This honesty note is what stops the spec from being + over-trusted. +- **When to update it** — for a spec that grounds an artifact (code, a TLA+ source), update + the spec and re-verify *before* changing the artifact. + +**The spec is the ground truth. Never edit the spec to match broken code.** If the spec is +wrong, stop and discuss with the user — that is the highest-leverage review point. + +Each flow adds its own format detail (source-file correspondence for code, a translation +README with recorded `quint run` output for TLA+, a comment block for an interactive build). + +--- + +## Conventions + +- `NodeId` / `LocalState` for per-actor state, and `NODES` for the participant set (the `Set[NodeId]` + the model ranges over); descriptive type names after the protocol (`VoteRequest`, `PrepareMsg`) not + `Msg1`, `Msg2`. +- Small domains: `N = 3` actors (or `N = 2` for first exploration) and value ranges like + `0..10` are enough for most safety properties and keep simulation cheap. Use **symbolic IDs** + (`int` or `str`) for participants. +- Message soup: store sent messages in one `Set` and don't model delivery order unless + ordering is what you're verifying. +- Abstract time: when the protocol has a notion of time or progress, model it as an integer + **round/epoch counter** (`round: int`), not wall-clock time or timer mechanics — a timeout is + just the round advancing. Exception: model timing explicitly only when the timing bound + itself is what you're verifying. +- Start with **one action**: get `init` + one action + one witness working before adding the + rest. For N actors, model **one actor first, verify, then generalize** to the map — never + wire up all N at once. +- Test pure functions in **isolation** in the REPL with hand-built states; that catches logic + bugs before they get tangled into the state machine. + +For everything syntactic — operators, undefined-behavior rules, `basicSpells`, the CLI flags — +consult the **quint-lang** reference rather than reproducing it here. diff --git a/.agents/skills/quint-modeling/examples/README.md b/.agents/skills/quint-modeling/examples/README.md new file mode 100644 index 00000000..9f8c5623 --- /dev/null +++ b/.agents/skills/quint-modeling/examples/README.md @@ -0,0 +1,60 @@ +# Reference examples + +Two fully-runnable Quint specs, bundled so an agent can read a *complete* model end-to-end — +not just the inline snippets in `SKILL.md`. They are chosen to contrast on the axis that drives +a model's whole shape: **how processes communicate**. + +Pick by **how the parts coordinate** — passing messages or sharing state — not by domain label. +Almost any system maps onto one; read the closer match as a worked instance of the spine patterns. + +| Example | Coordinates by… | Read it when modelling… | Illustrates (spine patterns) | +|---|---|---|---| +| [`tendermint/`](tendermint/tendermint.qnt) | **message passing** (`choreo::`; real BFT consensus) | **any distributed protocol where parties exchange messages** — consensus, BFT, replication, atomic commit, leader election, reliable broadcast/gossip, request/response, mempool. The **default** distributed-systems reference. | grouped `Id -> LocalState` (Step 2), guarded transitions (Step 4), `choreo::cue` listen/act split, `agreement`/`validity`/`accountability` invariants + counterexample demo (Step 5), `const`-instantiation, test separation | +| [`ewd426.qnt`](ewd426.qnt) | **shared state** (no messages) | parts coordinate through **common state** — mutexes/locks, token rings, shared registers, self-stabilization, reading neighbours/environment directly | grouped-map state (Step 2), guarded `step` (Step 4), `const`-instantiation, `temporal` **liveness** (Step 5) | + +Both message *soup* (plain `Set[Msg]`, no framework) and hybrid systems (message-passing + a +shared ledger) route through **tendermint** — Choreo's broadcast is soup underneath, and when a +system has both, the message-passing structure dominates (add shared state as its own `var`, per +Step 2). + +`tendermint/` is large (~760 lines — a real protocol is large); its banner has a **"quick read"** +path so you can learn the `cue` pattern without reading the whole spec, and the per-read token +cost is paid only when an agent actually opens it. + +Justified-*exception* shapes (ordered per-pair queues, flat variables, the contract Result-record +pattern) are intentionally left to prose in `SKILL.md` / the guidelines, not bundled — a bundled +example exerts a "copy me" pull we don't want pointing at exceptions. + +## Maintenance contract + +These are **owned copies**, snapshotted from the official corpus +(`mcp-servers/kb/kb/examples/`). They must be kept `quint`-clean across Quint upgrades. + +| File(s) | Upstream source | +|---|---| +| `ewd426.qnt` | `classic/distributed/ewd426/ewd426.qnt` | +| `tendermint/{tendermint,tendermintTest,choreo}.qnt`, `tendermint/spells/basicSpells.qnt` | `advanced/tendermint/` | + +For most files only a top-of-file teaching banner was added; the spec bodies are otherwise +byte-for-byte upstream, so re-syncing is a near-clean diff. **Exception — `tendermint/`:** upstream +ships a single `tendermint.qnt` with its `run` tests inline. We split it per the test-separation +guideline (`quint-lang/guidelines/tests.md`): the spec stays in `tendermint.qnt` (no `run`s), and +the `valid` + `no_agreement` instance/test modules moved to `tendermintTest.qnt` — the only body +change being a `from "./tendermint"` clause added to each `import tendermint(...)`. (We drop +upstream's `tendermintMicro.qnt`, `test_witness.sh`, `witness_bench.py`, and `spells/rareSpells.qnt`, +none of which this example needs.) + +- **Pinned Quint version:** 0.32.0 (last verified). + +### Re-verify (run from this directory after any Quint upgrade) + +```sh +quint typecheck ewd426.qnt +quint run ewd426.qnt --main=ewd426 --max-steps 15 + +cd tendermint +quint typecheck tendermint.qnt # spec alone — no runs +quint run tendermintTest.qnt --main=valid --invariant="agreement and validity and accountability" --max-steps 12 +quint test tendermintTest.qnt --main=valid # line28Test must pass +quint test tendermintTest.qnt --main=no_agreement # disagreementTest must pass +``` diff --git a/.agents/skills/quint-modeling/examples/ewd426.qnt b/.agents/skills/quint-modeling/examples/ewd426.qnt new file mode 100644 index 00000000..4ee7fb57 --- /dev/null +++ b/.agents/skills/quint-modeling/examples/ewd426.qnt @@ -0,0 +1,110 @@ +/** + * ewd426's Stabilizing Token Ring (EWD426) + * K state machine + * This implementation ensures that from some time on, + * exactly one token circulates in a set of nodes, + * + * Mahtab Norouzi, Josef Widder, Informal Systems, 2024-2025 + */ + +/* ============================================================================= + * quint-modeling REFERENCE EXAMPLE — communication model: SHARED MEMORY + * ----------------------------------------------------------------------------- + * Why this is bundled: it is the canonical *shared-memory* shape — there is NO + * message medium. Processes coordinate by reading common state: each node's + * `state_transition` reads its left/right neighbours in `var system: int -> State`. + * Contrast with the Choreo example (tendermint/), which is message-passing. + * + * Demonstrates, against the quint-modeling guidelines: + * - Grouped-map state `var system: int -> State` (one entry per node). + * - Guarded action `step` picks a node that *can* move; no no-op fallback. + * - Logic in pure defs `has_token`, `state_transition`, `count_tokens`. + * - const-instantiation the parametric `self_stabilization` module is run via + * the concrete `ewd426` instance (N = 5, K = 7). This is + * the pattern the skill's spine teaches: a `const` cannot + * run until instantiated. Run with `--main=ewd426`. + * - temporal LIVENESS `convergence` / `closure` / `persistence` — the system + * self-stabilizes to exactly one token. (The only bundled + * example with temporal properties.) + * + * The `broken_ewd426` instance (N = 3, K = 2) is a deliberately-broken config kept for + * contrast — useful to show what a property *failing* looks like. + * + * Verified clean on Quint 0.32.0. See ../examples/README.md for re-verification commands. + * ============================================================================= */ +module self_stabilization { + // Number of nodes in the ring + const N: int + const K: int + + /// Ensures the state space is larger than the number of nodes + assume _ = K >= N + + val bottom = 0 + val top = N + + /// Mapping of node indices to their states + var system: int -> int + + /// Check if a node has the token + pure def has_token(nodes: int -> int, index: int): bool = + if (index == bottom) + nodes.get(bottom) == nodes.get(top) + else + not(nodes.get(index) == nodes.get(index - 1)) + + /// Update the state of a specific node + pure def state_transition(nodes: int -> int, index: int): int = + if (not(has_token(nodes, index))) + nodes.get(index) + else if (index == bottom) + (nodes.get(bottom) + 1) % K + else + nodes.get(index - 1) + + /// Initialize all nodes with non-deterministic states + action init = all { + nondet initial = 0.to(N).setOfMaps(0.to(K - 1)).oneOf() + system' = initial + } + + /// Pick a single active node non-deterministically and update its state + action step = { + nondet node = 0.to(N).filter(i => has_token(system, i)).oneOf() + system' = system.set(node, state_transition(system, node)) + } + + /// Pick several active nodes non-deterministically and update their state. + /// Closer to the distributed demon is discussed in EWD 391. We are not + /// considering interleaving in the execution of state_transition here + action distributed_step = { + nondet nodes = 0.to(N).filter(i => has_token(system, i)).powerset().exclude(Set()).oneOf() + system' = nodes.fold(system, (s, x) => s.set(x, state_transition(system, x))) + } + + // Pure function to count how many tokens exist + pure def count_tokens(nodes: int -> int): int = { + 0.to(N).filter(i => has_token(nodes, i)).size() + } + + // Temporal properties + temporal convergence = step.weakFair(Set(system)) implies eventually(count_tokens(system) == 1) + temporal closure = always(count_tokens(system) == 1 implies always(count_tokens(system) == 1)) + temporal persistence = step.weakFair(Set(system)) implies eventually(always(count_tokens(system) == 1)) + + // Invariant + def tokenInv = count_tokens(system) > 0 + + /// to better see the token in the repl + pure def show_token(nodes: int -> int): int -> bool = + nodes.keys().mapBy(i => has_token(nodes, i)) +} + +module ewd426 { + import self_stabilization(N = 5, K = 7).* +} + +module broken_ewd426 { + // This should break the assumption of K >= N. See #1182. + import self_stabilization(N = 3, K = 2).* +} diff --git a/.agents/skills/quint-modeling/examples/tendermint/choreo.qnt b/.agents/skills/quint-modeling/examples/tendermint/choreo.qnt new file mode 100644 index 00000000..4845ffbb --- /dev/null +++ b/.agents/skills/quint-modeling/examples/tendermint/choreo.qnt @@ -0,0 +1,295 @@ +/** + * Choreo: Choreograph distributed protocols in Quint + * + * Read the documentation: TODO LINK + * + * Gabriela Moreira, Josef Widder and Yassine Boukhari, + * Informal Systems, 2025 + */ + +module choreo { + import basicSpells.* from "spells/basicSpells" + + // TODO: try moving process id to local context + type LocalState[process_id, ext] = { + process_id: process_id + | ext + } + + type Transition[p, s, m, e, ce] = { + post_state: LocalState[p, s], + effects: Set[Effect[p, m, e, ce]], + } + + type GlobalContext[p, s, m, e, ext] = { + system: p -> LocalState[p, s], + messages: p -> Set[m], + events: p -> Set[e], + extensions: ext + } + + type LocalContext[p, s, m, e, ext] = { + state: LocalState[p, s], + messages: Set[m], + events: Set[e], + extensions: ext + } + + // This is the message routing to the # handler + type Listener[p, s, m, e, ce, ext] = + (LocalContext[p, s, m, e, ext]) => Set[Transition[p, s, m, e, ce]] + + type DeterministicListener[p, s, m, e, ce, ext] = + (LocalContext[p, s, m, e, ext]) => Transition[p, s, m, e, ce] + + // Only needed for micro_step + type Input[m, e] = Message(m) | Event(e) + + // Only needed for micro_step + type MicroListener[p, s, m, e, ce, ext] = + (LocalContext[p, s, m, e, ext], Input[m, e]) => Set[Transition[p, s, m, e, ce]] + + type EffectProcessor[p, s, m, e, ce, ext] = + (GlobalContext[p, s, m, e, ext], ce) => GlobalContext[p, s, m, e, ext] + + + type Effect[p, m, e, ce] = + | Broadcast(m) + | Send({ to: p, message: m }) + | TriggerEvent(e) + | CustomEffect(ce) + + /// A displayer is a function that takes a global context and returns a displayable representation of it. + /// This is used to visualize the state of the system for debugging or monitoring purposes. + /// After type instantiation, it should have the following signature: + /// ``` + /// (Environment) => Display + /// ``` + /// The display type can be anything. + type Displayer[p, s, m, e, ext, d] = (GlobalContext[p, s, m, e, ext]) => d + + pure def apply_effect( + env: GlobalContext[p, s, m, e, ext], + v: p, + tr: Transition[p, s, m, e, ce], + apply_custom_effect: EffectProcessor[p, s, m, e, ce, ext] + ): GlobalContext[p, s, m, e, ext] = { + val env1 = { ...env, system: env.system.setBy(v, s => tr.post_state) } + + tr.effects.fold(env1, (e, effect) => { + match effect { + | Broadcast(m) => { + { ...e, messages: e.messages.transformValues(b => b.setAdd(m)) } + } + | Send(r) => { + { ...e, messages: e.messages.setBy(r.to, b => b.setAdd(r.message)) } + } + | TriggerEvent(ev) => { + { ...e, events: e.events.setBy(v, b => b.setAdd(ev)) } + } + | CustomEffect(ex) => { + apply_custom_effect(e, ex) + } + } + }) + } + + const processes: Set[p] + var s: GlobalContext[p, s, m, e, ext] + var display: d + + pure def initialize(x: a, f: Option[(a) => Set[b]]): Set[b] = { + match f { + | Some(fun) => fun(x) + | None => Set() + } + } + + pure def convert_context( + env: GlobalContext[p, s, m, e, ext], + v: p + ): LocalContext[p, s, m, e, ext] = { + { + state: env.system.get(v), + messages: env.messages.get(v), + events: env.events.get(v), + extensions: env.extensions + } + } + + action process_transitions( + v: p, + transitions: Set[Transition[p, s, m, e, ce]], + apply_custom_effect: EffectProcessor[p, s, m, e, ce, extensions], + ): bool = + // FIXME: Quint was supposed to detect determinism alone + if (transitions.size() == 1) { + val transition = transitions.getOnlyElement() + val post_env = apply_effect(s, v, transition, apply_custom_effect) + s' = post_env + } else { + nondet transition = oneOf(transitions) + val post_env = apply_effect(s, v, transition, apply_custom_effect) + s' = post_env + } + + action process_transitions_with_displayer( + v: p, + transitions: Set[Transition[p, s, m, e, ce]], + apply_custom_effect: EffectProcessor[p, s, m, e, ce, extensions], + displayer: Displayer[p, s, m, e, extensions, d], + ): bool = + // FIXME: Quint was supposed to detect determinism alone + if (transitions.size() == 1) { + val transition = transitions.getOnlyElement() + val post_env = apply_effect(s, v, transition, apply_custom_effect) + all { + s' = post_env, + display' = displayer(post_env), + } + } else { + nondet transition = oneOf(transitions) + val post_env = apply_effect(s, v, transition, apply_custom_effect) + all { + s' = post_env, + display' = displayer(post_env), + } + } + + action init(ctx: GlobalContext[p, s, m , e, ext]): bool = { + s' = ctx + } + + action init_with_displayer( + ctx: GlobalContext[p, s, m , e, ext], + displayer: Displayer[p, s, m, e, extensions, d], + ): bool = all { + s' = ctx, + display' = displayer(ctx) + } + + action step( + listener: Listener[p, s, m, e, ce, extensions], + apply_custom_effect: EffectProcessor[p, s, m, e, ce, extensions] + ): bool = { + nondet v = oneOf(processes) + val input = convert_context(s, v) + val transitions = listener(input).filter(t => t.effects.size() > 0 or t.post_state != input.state) + process_transitions(v, transitions, apply_custom_effect) + } + + action step_with_displayer( + listener: Listener[p, s, m, e, ce, extensions], + apply_custom_effect: EffectProcessor[p, s, m, e, ce, extensions], + displayer: Displayer[p, s, m, e, extensions, d] + ): bool = { + nondet v = oneOf(processes) + val input = convert_context(s, v) + val transitions = listener(input).filter(t => t.effects.size() > 0 or t.post_state != input.state) + process_transitions_with_displayer(v, transitions, apply_custom_effect, displayer) + } + + action micro_step( + listener: MicroListener[p, s, m, e, ce, extensions], + apply_custom_effect: EffectProcessor[p, s, m, e, ce, extensions] + ): bool = { + nondet process = processes.oneOf() + val ctx = convert_context(s, process) + any { + nondet msg = s.messages.get(process).oneOf() + val transitions = listener(ctx, Message(msg)) + process_transitions(process, transitions, apply_custom_effect), + + nondet event = s.events.get(process).oneOf() + val transitions = listener(ctx, Event(event)) + process_transitions(process, transitions, apply_custom_effect), + } + } + + action step_with( + v: p, + listener: Listener[p, s, m, e, ce, extensions], + apply_custom_effect: EffectProcessor[p, s, m, e, ce, extensions] + ): bool = { + val input = convert_context(s, v) + val transitions = listener(input).filter(t => t.effects.size() > 0 or t.post_state != input.state) + process_transitions(v, transitions, apply_custom_effect) + } + + action step_deterministic( + v: p, + listener: DeterministicListener[p, s, m, e, ce, extensions], + apply_custom_effect: EffectProcessor[p, s, m, e, ce, extensions] + ): bool = { + val input = convert_context(s, v) + val transitions = Set(listener(input)).filter(t => t.effects.size() > 0 or t.post_state != input.state) + process_transitions(v, transitions, apply_custom_effect) + } + + action step_with_filter( + v: p, + listener: Listener[p, s, m, e, ce, extensions], + apply_custom_effect: EffectProcessor[p, s, m, e, ce, extensions], + f: (Transition[p, s, m, e, ce]) => bool + ): bool = { + val input = convert_context(s, v) + val transitions = listener(input).filter(t => t.effects.size() > 0 or t.post_state != input.state).filter(f) + process_transitions(v, transitions, apply_custom_effect) + } + + action step_with_messages( + v: p, + listener: Listener[p, s, m, e, ce, extensions], + apply_custom_effect: EffectProcessor[p, s, m, e, ce, extensions], + f: Set[m] => Set[m] + ): bool = { + val input = convert_context(s, v) + val input1 = { ...input, messages: f(input.messages) } + val transitions = listener(input1).filter(t => t.effects.size() > 0 or t.post_state != input.state) + process_transitions(v, transitions, apply_custom_effect) + } + + action lose_messages(v: p, f: Set[m] => Set[m]): bool = { + val msgs = s.messages.get(v) + val new_msgs = msgs.exclude(f(msgs)) + all { + s' = { ...s, messages: s.messages.set(v, new_msgs) } + } + } + + pure def cue( + ctx: LocalContext[p, s, m, e, ext], + listen_fn: (LocalContext[p, s, m, e, ext]) => Set[r], + upon_fn: (LocalContext[p, s, m, e, ext], r) => Transition[p, s, m, e, ce] + ): Set[Transition[p, s, m, e, ce]] = { + val params = listen_fn(ctx) + params.map(param => upon_fn(ctx, param)) + } + + type CueResult[p, s, m, e, ext, r] = CueOk({ ctx: LocalContext[p, s, m, e, ext], params: r }) | NoCue + + def with_cue( + process: p, + listen_fn: (LocalContext[p, s, m, e, ext]) => Set[r], + params: r + ): CueResult[p, s, m, e, ext, r] = { + val ctx = convert_context(s, process) + val valid_params = listen_fn(ctx) + val is_valid = valid_params.contains(params) + if (is_valid) + CueOk({ ctx: ctx, params: params }) + else + NoCue + } + + action perform( + cue_result: CueResult[p, s, m, e, ext, r], + upon_fn: (LocalContext[p, s, m, e, ext], r) => Transition[p, s, m, e, ce], + apply_custom_effect: EffectProcessor[p, s, m, e, ce, ext], + ): bool = { + match cue_result { + | CueOk(cue) => process_transitions(cue.ctx.state.process_id, Set(upon_fn(cue.ctx, cue.params)), apply_custom_effect) + | NoCue => all { false, s' = s } + } + } +} diff --git a/.agents/skills/quint-modeling/examples/tendermint/spells/basicSpells.qnt b/.agents/skills/quint-modeling/examples/tendermint/spells/basicSpells.qnt new file mode 100644 index 00000000..6c75bcbe --- /dev/null +++ b/.agents/skills/quint-modeling/examples/tendermint/spells/basicSpells.qnt @@ -0,0 +1,388 @@ +/** + * This module collects definitions that are ubiquitous. + * One day they will become the standard library of Quint. + */ +module basicSpells { + /// Option type, which may hold some value or none + type Option[a] = Some(a) | None + + /// An annotation for writing preconditions. + /// - @param cond condition to check + /// - @returns true if and only if cond evaluates to true + pure def require(cond: bool): bool = cond + + run requireTest = all { + assert(require(4 > 3)), + assert(not(require(false))), + } + + /// A convenience operator that returns a string error code, + /// if the condition does not hold true. + /// + /// - @param cond condition to check + /// - @param error a non-empty error message + /// - @returns "", when cond holds true; otherwise error + pure def requires(cond: bool, error: str): str = { + if (cond) "" else error + } + + run requiresTest = all { + assert(requires(4 > 3, "4 > 3") == ""), + assert(requires(4 < 3, "false: 4 < 3") == "false: 4 < 3"), + } + + + /// Compute the maximum of two integers. + /// + /// - @param i first integer + /// - @param j second integer + /// - @returns the maximum of i and j + pure def max(i: int, j: int): int = { + if (i > j) i else j + } + + run maxTest = all { + assert(max(3, 4) == 4), + assert(max(6, 3) == 6), + assert(max(10, 10) == 10), + assert(max(-3, -5) == -3), + assert(max(-5, -3) == -3), + } + + /// Compute the minimum of two integers. + /// + /// - @param i first integer + /// - @param j second integer + /// - @returns the minimum of i and j + pure def min(i: int, j: int): int = { + if (i < j) i else j + } + + run minTest = all { + assert(min(3, 4) == 3), + assert(min(6, 3) == 3), + assert(min(10, 10) == 10), + assert(min(-3, -5) == -5), + assert(min(-5, -3) == -5), + } + + /// Compute the absolute value of an integer + /// + /// - @param i : an integer whose absolute value we are interested in + /// - @returns |i|, the absolute value of i + pure def abs(i: int): int = { + if (i < 0) -i else i + } + + run absTest = all { + assert(abs(3) == 3), + assert(abs(-3) == 3), + assert(abs(0) == 0), + } + + /// Remove a set element. + /// + /// - @param s a set to remove an element from + /// - @param elem an element to remove + /// - @returns a new set that contains all elements of set but elem + pure def setRemove(s: Set[a], elem: a): Set[a] = { + s.exclude(Set(elem)) + } + + run setRemoveTest = all { + assert(Set(2, 4) == Set(2, 3, 4).setRemove(3)), + assert(Set() == Set().setRemove(3)), + } + + /// Adds an element to a set. + /// + /// - @param s a set to add an element to + /// - @param elem an element to add + /// - @returns a new set that contains all elements of set and elem + pure def setAdd(s: Set[a], elem: a): Set[a] = { + s.union(Set(elem)) + } + + run setAddTest = all{ + assert(Set(2, 3, 4) == Set(2, 4).setAdd(3)), + assert(Set(3) == Set().setAdd(3)), + assert(Set(2,4) == Set(2,4).setAdd(4)), + } + + /// Test whether a key is present in a map + /// + /// - @param m a map to query + /// - @param key the key to look for + /// - @returns true if and only map has an entry associated with key + pure def has(m: a -> b, key: a): bool = { + m.keys().contains(key) + } + + run hasTest = all { + assert(Map(2 -> 3, 4 -> 5).has(2)), + assert(not(Map(2 -> 3, 4 -> 5).has(6))), + } + + /// Get the map value associated with a key, or the default, + /// if the key is not present. + /// + /// - @param m the map to query + /// - @param key the key to search for + /// - @returns the value associated with the key, if key is + /// present in the map, and default otherwise + pure def getOrElse(m: a -> b, key: a, default: b): b = { + if (m.has(key)) { + m.get(key) + } else { + default + } + } + + run getOrElseTest = all { + assert(Map(2 -> 3, 4 -> 5).getOrElse(2, 0) == 3), + assert(Map(2 -> 3, 4 -> 5).getOrElse(7, 11) == 11), + } + + /// Remove a map entry. + /// + /// - @param m a map to remove an entry from + /// - @param key the key of an entry to remove + /// - @returns a new map that contains all entries of map + /// that do not have the key key + pure def mapRemove(m: a -> b, key: a): a -> b = { + m.keys().setRemove(key).mapBy(k => m.get(k)) + } + + run mapRemoveTest = all { + assert(Map(3 -> 4, 7 -> 8) == Map(3 -> 4, 5 -> 6, 7 -> 8).mapRemove(5)), + assert(Map() == Map().mapRemove(3)), + } + + /// Removes a set of map entries. + /// + /// - @param m a map to remove entries from + /// - @param ks a set of keys for entries to remove from the map + /// - @returns a new map that contains all entries of map + /// that do not have a key in keys + pure def mapRemoveAll(m: a -> b, ks: Set[a]): a -> b = { + m.keys().exclude(ks).mapBy(k => m.get(k)) + } + + run mapRemoveAllTest = + val m = Map(3 -> 4, 5 -> 6, 7 -> 8) + all { + assert(m.mapRemoveAll(Set(5, 7)) == Map(3 -> 4)), + assert(m.mapRemoveAll(Set(5, 99999)) == Map(3 -> 4, 7 -> 8)), + } + + /// Get the set of values of a map. + /// + /// - @param map a map from type a to type b + /// - @returns the set of all values in the map + pure def values(m: a -> b): Set[b] = { + m.keys().map(k => m.get(k)) + } + + run valuesTest = all { + assert(values(Map()) == Set()), + assert(values(Map(1 -> 2, 2 -> 3)) == Set(2, 3)), + assert(values(Map(1 -> 2, 2 -> 3, 3 -> 2)) == Set(2, 3)), + } + + /// Whether a set is empty + /// + /// - @param s a set of any type + /// - @returns true iff the set is the empty set + pure def empty(s: Set[a]): bool = s == Set() + + run emptyTest = all { + assert(empty(Set()) == true), + assert(empty(Set(1, 2)) == false), + assert(empty(Set(Set())) == false), + } + + /// Sort a list, given the ordering operator. + /// + /// - @param list a list to sort + /// - @param lt a definition of "less than" + /// - @returns the sorted version of list + pure def sortList(list: List[a], lt: (a, a) => bool): List[a] = { + pure def insertInOrder(sortedList: List[a], num: a): List[a] = { + match range(0, sortedList.length()).findFirst(i => not(lt(sortedList[i], num))) { + | None => sortedList.append(num) + | Some(index) => sortedList.slice(0, index).append(num).concat(sortedList.slice(index, sortedList.length())) + } + } + + list.foldl([], (sortedList, num) => insertInOrder(sortedList, num)) + } + + run listSortedTest = all { + assert([ 1, 3, 5 ] == sortList([ 5, 1, 3 ], (x, y) => x < y)), + assert([ 1, 1, 3, 5, 5 ] == sortList([ 5, 1, 3, 1, 5 ], (x, y) => x < y)), + } + + /// Apply an operator to all values of a map + /// + /// - @param m: a map of any type + /// - @param f: an operator with one argument with the same type as the map's values + /// - @returns a map with same keys as m and f applied to the values + pure def transformValues(m: a -> b, f: (b) => c): a -> c = { + m.keys().mapBy(k => f(m.get(k))) + } + + run transformValuesTest = { + pure val m = Map("a" -> 1, "b" -> 2) + assert(m.transformValues(x => x + 1) == Map("a" -> 2, "b" -> 3)) + } + + /// map a function over a list + /// + /// - @param l: a list of any type + /// - @param f: a function to apply to each element of the list + /// - @returns a list of the results of applying f to each element of l + pure def listMap(l: List[a], f: (a) => b): List[b] = { + range(0, l.length()).foldl([], (acc, i) => { + acc.append(f(l[i])) + }) + } + + run listMapTest = all { + assert(listMap([1, 2, 3], x => x + 1) == [2, 3, 4]), + assert(listMap([1, 2, 3], x => x > 1) == [false, true, true]), + } + + /// The last element of a list + /// + /// - @param v: a list of any type + /// - @returns the last element of the list + pure def last(v: List[a]): a = { + v[v.length() - 1] + } + + run lastTest = all { + assert(last([1, 2, 3]) == 3), + assert(last([1]) == 1), + } + + /// `decreasingRange(i, j)` is the list of integers between `j` and `i` + /// both `i` and `j` are inclusive. + /// The behavior is undefined if `i < j`. + /// + /// - @param start: the first integer in the range + /// - @param end: the last integer in the range + pure def decreasingRange(start: int, end: int): List[int] = { + range(end, start + 1).foldl([], (acc, i) => { + List(i).concat(acc) + }) + } + + run decreasingRangeTest = all { + assert(decreasingRange(5, 1) == [5, 4, 3, 2, 1]), + } + + /// `takeWhile(l, cond)` is the longest prefix of `l` such that all elements + /// satisfy the condition `cond`. + /// + /// - @param l: a list of any type + /// - @param cond: a function that takes an element of the list and returns a boolean + /// - @returns the longest prefix of `l` such that all elements satisfy `cond` + pure def takeWhile(l: List[a], cond: (a) => bool): List[a] = { + pure val result = l.foldl(([], true), (acc, e) => { + if (acc._2 and cond(e)) { + (acc._1.append(e), true) + } else { + (acc._1, false) + } + }) + + result._1 + } + + run takeWhileTest = all { + assert(takeWhile([1, 5, 4, 3], (x) => x % 2 == 1) == [1, 5]), + } + + /// `isPrefixOf(l1, l2)` is true iff `l1` is a prefix of `l2`. + /// + /// - @param l1: a list of any type + /// - @param l2: a list of same type as `l1` + /// - @returns true iff `l1` is a prefix of `l2` + pure def isPrefixOf(l1: List[a], l2: List[a]): bool = { + if (l1.length() > l2.length()) { + false + } else { + l1.indices().forall(i => l1[i] == l2[i]) + } + } + + run isPrefixOfTest = all { + assert(isPrefixOf([1, 2], [1, 2, 3])), + assert(not(isPrefixOf([1, 2], [1, 3, 2]))), + assert(not(isPrefixOf([1, 2], [1]))), + assert(isPrefixOf([], [1, 2])), + assert(isPrefixOf([], [])), + } + + /// `find(s, f)` is an element of `s` that satisfies the predicate `f`, or None + /// if no such element exists. + /// + /// - @param s: a set of any type + /// - @param f: a function that takes an element of the set and returns a boolean + /// - @returns an element of `s` that satisfies `f`, or None + pure def find(s, f) = s.fold(None, (a, i) => if (f(i)) Some(i) else a) + + run findTest = all { + assert(find(Set(1, 2, 3), x => x == 2) == Some(2)), + assert(find(Set(1, 2, 3), x => x == 4) == None), + } + + /// `findFirst(l, f)` is the first element of `l` that satisfies the predicate `f`, or None + /// if no such element exists. + /// + /// - @param l: a list of any type + /// - @param f: a function that takes an element of the list and returns a boolean + /// - @returns the first element of `l` that satisfies `f`, or None + pure def findFirst(l, f) = l.foldl(None, (a, i) => if (a == None and f(i)) Some(i) else a) + + run findFirstTest = all { + assert(findFirst([1, 2, 3], x => x > 1) == Some(2)), + assert(findFirst([1, 2, 3], x => x == 4) == None), + } + + /// `setByWithDefault(m, k, op, default)` is a map that is the same as `m` except that + /// the value associated with `k` is `op(m[k])` if `k` is present in `m`, and `op(default)` otherwise. + /// + /// - @param m: a map from type `a` to type `b` + /// - @param k: a key of type `a` + /// - @param op: a function to transform the value of the key + /// - @param default: the value to use if the key is not present in the map + /// - @returns a new map with the updated key. + pure def setByWithDefault(m: a -> b, k: a, op: (b) => b, default: b): a -> b = { + if (m.has(k)) + m.setBy(k, op) + else + m.put(k, default).setBy(k, op) + } + + run setByWithDefaultTest = all { + assert(setByWithDefault(Map(1 -> 2, 2 -> 3), 1, x => x + 1, 0) == Map(1 -> 3, 2 -> 3)), + assert(setByWithDefault(Map(1 -> 2, 2 -> 3), 3, x => x + 1, 0) == Map(1 -> 2, 2 -> 3, 3 -> 1)), + } + + pure def unwrap(value: Option[a]): a = { + match value { + | None => Map().get(value) + | Some(x) => x + } + } + + pure def filterMap(s: Set[a], f: (a) => Option[b]): Set[b] = { + s.fold(Set(), (acc, e) => { + match f(e) { + | Some(x) => acc.union(Set(x)) + | None => acc + } + }) + } +} diff --git a/.agents/skills/quint-modeling/examples/tendermint/tendermint.qnt b/.agents/skills/quint-modeling/examples/tendermint/tendermint.qnt new file mode 100644 index 00000000..26dfde0f --- /dev/null +++ b/.agents/skills/quint-modeling/examples/tendermint/tendermint.qnt @@ -0,0 +1,800 @@ +/* ============================================================================= + * quint-modeling REFERENCE EXAMPLE — communication model: MESSAGE-PASSING (Choreo) + * ----------------------------------------------------------------------------- + * Why this is bundled: it is the canonical *Choreo framework* spec — a real BFT + * consensus engine (Tendermint), and the source of the examples in + * ../../../quint-lang/guidelines/choreo.md. Choreo is a different programming model + * from plain Quint (its own `choreo::` API), so it cannot be inferred — you have to + * see one. Contrast with the shared-memory example (../ewd426.qnt). + * + * READ THIS when modelling ANY distributed protocol where parties exchange messages — + * consensus/BFT, replication, atomic commit, leader election, reliable broadcast/gossip, + * request/response, a mempool. It is the default reference for message-passing systems + * (not just consensus); the patterns below transfer. Use ../ewd426.qnt instead only when + * parts coordinate through SHARED STATE rather than messages. + * + * Demonstrates the `choreo::cue` PATTERN — separate *when* from *what*: + * - `listen_*(ctx): Set[Params]` — the messages/conditions that match (Set[()] for + * state/quorum-only triggers). + * - `broadcast_*` / `act_*(ctx, p): Transition` — the reaction for each match. + * - `main_listener` wires them with `choreo::cue(ctx, listen, act)` (x7) — reads + * like the protocol at a glance. + * Plus: real `agreement` / `validity` / `accountability` invariants. + * + * QUICK READ (to learn the pattern without reading all ~760 lines — anchored to + * names so it survives upstream re-sync): read `listen_proposal_in_propose` + + * `broadcast_prevote_for_proposal` (one cue pair), then `main_listener` (the wiring), + * then the `agreement` / `validity` / `accountability` invariants. The rest is full + * protocol depth. + * + * TESTS: in the sibling `tendermintTest.qnt` (this spec has no `run`s, per the + * test-separation guideline — quint-lang/guidelines/tests.md). That file also holds + * the concrete instances; the spec here is parametric (const F/CORRECT/FAULTY/...), + * so it cannot run on its own — run an instance: + * quint run tendermintTest.qnt --main=valid --invariant="agreement and validity and accountability" + * The `no_agreement` instance there is a deliberate agreement-violation demo. + * + * Dependencies (bundled alongside): ./choreo.qnt, ./spells/basicSpells.qnt. + * Verified clean on Quint 0.32.0. See ../README.md for re-verification commands. + * ============================================================================= */ +module tendermint { + import basicSpells.* from "./spells/basicSpells" + import choreo(processes = NODES) as choreo from "./choreo" + + type Round = int + type Stage = ProposeStage | PreVoteStage | PreCommitStage | DecidedStage + type Value = str + + type Bookkeeping = { + evidence_propose: Set[ProposeMsg], + evidence_prevote: Set[PreVoteMsg], + evidence_precommit: Set[PreVoteMsg], + } + + type TimeoutKind = ProposeTimeout | PreVoteTimeout | PreCommitTimeout + type TimeoutEvent = { kind: TimeoutKind, round: Round } + + /// The ID of a value. In the implementation, this would be a hash of the value. + /// Here, we wrap it in a record and don't convert it back into a value. + /// This keeps the property that v1.id() == v2.id() iff v1 == v2. + type ValueId = { hashed: Value } + type Node = str + + type StateFields = { + round: Round, + stage: Stage, + decision: Option[Value], + locked_value: Option[Value], + locked_round: Round, + valid_value: Option[Value], + valid_round: Round, + after_prevote_for_first_time: bool, + precommit_quorum: bool, + } + + type ProposeMsg = { + src: Node, + round: Round, + proposal: Value, + valid_round: Round, + } + + type PreVoteMsg = { + src: Node, + round: Round, + id: Option[ValueId], + } + + type Message = + | Propose(ProposeMsg) + | PreVote(PreVoteMsg) + | PreCommit(PreVoteMsg) + + type CustomEffects = CollectEvidence(Message) + type Event = TimeoutEvent + type Extensions = Bookkeeping + + /* Boilerplate */ + type LocalState = choreo::LocalState[Node, StateFields] + type LocalContext = choreo::LocalContext[Node, StateFields, Message, Event, Extensions] + type Transition = choreo::Transition[Node, StateFields, Message, Event, CustomEffects] + type GlobalContext = choreo::GlobalContext[ + Node, + StateFields, + Message, + Event, + Extensions + ] + /* End of boilerplate */ + + pure def get_proposals(messages: Set[Message]): Set[ProposeMsg] = + messages.filterMap(m => { + match m { + | Propose(p) => Some(p) + | _ => None + } + }) + + pure def get_pre_votes(messages: Set[Message]): Set[PreVoteMsg] = + messages.filterMap(m => { + match m { + | PreVote(p) => Some(p) + | _ => None + } + }) + + pure def get_pre_commits(messages: Set[Message]): Set[PreVoteMsg] = + messages.filterMap(m => { + match m { + | PreCommit(p) => Some(p) + | _ => None + } + }) + + pure def source(message: Message): Node = + match message { + | Propose(p) => p.src + | PreVote(p) => p.src + | PreCommit(p) => p.src + } + + pure def valid(v: Value): bool = { + VALID_VALUES.contains(v) + } + + pure def id(v: Value): ValueId = { hashed: v } + + pure def start_round(ctx: LocalContext, round: Round): Transition = { + val s = ctx.state + + pure val effect = + if (s.process_id == PROPOSER.get(round)) { + pure val proposal = + if (s.valid_value != None) + s.valid_value.unwrap() + else + VALUES.get(round) + + choreo::Broadcast( + Propose({ src: s.process_id, round: round, proposal: proposal, valid_round: s.valid_round }) + ) + } else { + choreo::TriggerEvent({ kind: ProposeTimeout, round: s.round }) + } + { + effects: Set(effect), + post_state: { + ...s, + round: round, + stage: ProposeStage, + precommit_quorum: false, + } + } + } + + pure def listen_proposal_in_propose(ctx: LocalContext): Set[ProposeMsg] = { + val s = ctx.state + val messages = ctx.messages + + val state_guard = and { + s.stage == ProposeStage + } + + // Message guards: predicates on the messages that can use the state as context + def message_guard = (p) => and { + p.valid_round == -1, + p.src == PROPOSER.get(s.round), + } + + // Apply Guards + val proposals = messages.get_proposals() + proposals.filter(p => and { + message_guard(p), + state_guard + }) + } + + pure def broadcast_prevote_for_proposal(ctx: LocalContext, p: ProposeMsg): Transition = { + val s = ctx.state + + // Broadcast the PreVote message for the proposal + pure val effects = if (valid(p.proposal) and (s.locked_round == -1 or s.locked_value == Some(p.proposal))) { + Set(choreo::Broadcast(PreVote({ src: s.process_id, round: s.round, id: Some(id(p.proposal)) }))) + } else { + Set(choreo::Broadcast(PreVote({ src: s.process_id, round: s.round, id: None }))) + } + + // TODO -> add timeout and logging effects + { + post_state: { ...s, stage: PreVoteStage }, + effects: effects.setAdd(choreo::CustomEffect(CollectEvidence(Propose(p)))) + } + } + + pure def listen_proposal_in_propose_prevote(ctx: LocalContext): Set[ProposeMsg] = { + val s = ctx.state + val messages = ctx.messages + + val state_guard = and { + s.stage == ProposeStage, + } + + def message_guard = (p) => and { + p.src == PROPOSER.get(s.round), + p.valid_round >= 0, + p.valid_round < s.round + } + + val proposals = messages.get_proposals().filter(p => and { + message_guard(p), + state_guard, + }) + + val pre_votes = messages.get_pre_votes() + proposals.filter(p => and { + pre_votes.filter(q => q.id == Some(p.proposal.id()) and q.round == p.valid_round).size() >= 2 * F + 1 // quorum size is F + 1 + }) + } + + pure def broadcast_prevote_with_validation(ctx: LocalContext, p: ProposeMsg): Transition = { + val s = ctx.state + val messages = ctx.messages + + // Collect evidence for prevotes that contributed to this decision + val contributing_prevotes = messages.get_pre_votes().filter(pv => + pv.round == p.valid_round and pv.id == Some(id(p.proposal)) + ) + val prevote_evidence = contributing_prevotes.map(pv => choreo::CustomEffect(CollectEvidence(PreVote(pv)))) + val evidence = Set(choreo::CustomEffect(CollectEvidence(Propose(p)))).union(prevote_evidence) + + // Broadcast the PreVote message for the proposal + val effects = if (valid(p.proposal) and (s.locked_round <= p.valid_round or s.locked_value == Some(p.proposal))) { + Set(choreo::Broadcast(PreVote({ src: s.process_id, round: s.round, id: Some(id(p.proposal))}))) + } else { + Set(choreo::Broadcast(PreVote({ src: s.process_id, round: s.round, id: None }))) + } + + { + post_state: { ...s, stage: PreVoteStage }, + effects: effects.union(evidence) + } + } + + + pure def listen_quorum_prevotes_any(ctx: LocalContext): Set[()] = { + val s = ctx.state + val messages = ctx.messages + + // Do we have a quorum of PreVotes in the current round + def message_guard = (pv) => (pv.round == s.round) + val quorum = messages.get_pre_votes().filter(pv => and { + message_guard(pv), + }).size() >= 2 * F + 1 // quorum size is F + 1 + + if (quorum) { + Set(()) + } else { + Set() + } + } + + pure def trigger_prevote_timeout(ctx: LocalContext, _params: ()): Transition = { + val s = ctx.state + + // If we have a quorum, we can move to precommit stage + { + post_state: s, + effects: Set(choreo::TriggerEvent({ kind: PreVoteTimeout, round: s.round })) + } + } + + pure def listen_quorum_nil_prevotes(ctx: LocalContext): Set[()] = { + val s = ctx.state + val messages = ctx.messages + + val state_guard = and { + s.stage == PreVoteStage, + } + + // Do we have a quorum of PreVotes in the current round + def message_guard = (pv) => (pv.round == s.round and pv.id == None) + val prevotes = messages.get_pre_votes().filter(pv => message_guard(pv)) + val quorum = prevotes.map(m => m.src).size() >= 2 * F + 1 + + if (quorum and state_guard) { + Set(()) + } else { + Set() + } + } + + pure def broadcast_nil_precommit(ctx: LocalContext, _params: ()): Transition = { + val s = ctx.state + val messages = ctx.messages + + // Collect evidence for all nil prevotes that enabled this decision + val nil_prevotes = messages.get_pre_votes().filter(pv => + pv.round == s.round and pv.id == None + ) + val prevote_evidence = nil_prevotes.map(pv => choreo::CustomEffect(CollectEvidence(PreVote(pv)))) + + // If we have a quorum, we can move to precommit stage + { + post_state: { ...s, stage: PreCommitStage }, + effects: Set( + choreo::Broadcast(PreCommit({ src: s.process_id, round: s.round, id: None })) + ).union(prevote_evidence) + } + } + + pure def listen_quorum_precommits_any(ctx: LocalContext): Set[()] = { + val s = ctx.state + val messages = ctx.messages + + // Do we have a quorum of PreCommits in the current round + def message_guard = (pc) => (pc.round == s.round) + val quorum = messages.get_pre_commits().filter(pc => and { + message_guard(pc), + }).map(m => m.src).size() >= 2 * F + 1 // quorum size is F + 1 + + if (quorum) { + Set(()) + } else { + Set() + } + } + + pure def trigger_precommit_timeout(ctx: LocalContext, _params: ()): Transition = { + val s = ctx.state + + // If we have a quorum, we can move to decided stage + { + post_state: s, + effects: Set(choreo::TriggerEvent({ kind: PreCommitTimeout, round: s.round })) + } + } + + pure def listen_proposal_in_prevote_commit(ctx: LocalContext): Set[ProposeMsg] = { + val s = ctx.state + val messages = ctx.messages + + val state_guard = and { + s.stage.in(Set(PreVoteStage, PreCommitStage)) + } + + def message_guard = (p) => and { + p.src == PROPOSER.get(s.round), + p.round == s.round + } + + val proposals = messages.get_proposals() + val pre_votes = messages.get_pre_votes() + + proposals.filter(p => and { + message_guard(p), + state_guard, + // Check for quorum of prevotes for this proposal in current round + pre_votes.filter(pv => and { + pv.round == s.round, + pv.id == Some(id(p.proposal)) + }).map(pv => pv.src).size() >= 2 * F + 1 + }) + } + + pure def lock_value_and_precommit(ctx: LocalContext, p: ProposeMsg): Transition = { + val s = ctx.state + val messages = ctx.messages + + // Collect evidence for prevotes that enabled this decision + val enabling_prevotes = messages.get_pre_votes().filter(pv => + pv.round == s.round and pv.id == Some(id(p.proposal)) + ) + val prevote_evidence = enabling_prevotes.map(pv => choreo::CustomEffect(CollectEvidence(PreVote(pv)))) + + if (s.stage == PreVoteStage) { + // If in prevote stage, broadcast precommit and move to precommit stage + { + // Move to precommit stage and update values + post_state: { + ...s, + stage: PreCommitStage, + valid_value: Some(p.proposal), + valid_round: s.round, + locked_value: Some(p.proposal), + locked_round: s.round + }, + effects: Set( + choreo::Broadcast(PreCommit({ src: s.process_id, round: s.round, id: Some(id(p.proposal)) })), + choreo::CustomEffect(CollectEvidence(Propose(p))) + ).union(prevote_evidence) + } + } else { + // Just update valid value if already in precommit + { + post_state: { + ...s, + valid_value: Some(p.proposal), + valid_round: s.round + }, + effects: Set(choreo::CustomEffect(CollectEvidence(Propose(p)))).union(prevote_evidence) + } + } + } + + pure def listen_proposal_in_precommit_no_decision(ctx: LocalContext): Set[ProposeMsg] = { + val s = ctx.state + val messages = ctx.messages + + val state_guard = and { + s.decision == None + } + + def message_guard = (p) => and { + p.src == PROPOSER.get(p.round), + p.round <= s.round // Can decide on current or past rounds + } + + val proposals = messages.get_proposals() + val pre_commits = messages.get_pre_commits() + + proposals.filter(p => and { + message_guard(p), + state_guard, + // Check for quorum of precommits for this proposal + pre_commits.filter(pc => and { + pc.round == p.round, + pc.id == Some(id(p.proposal)) + }).map(pc => pc.src).size() >= 2 * F + 1 + }) + } + + pure def decide_on_proposal(ctx: LocalContext, p: ProposeMsg): Transition = { + val s = ctx.state + val messages = ctx.messages + + // Collect evidence for precommits that enabled this decision + val enabling_precommits = messages.get_pre_commits().filter(pc => + pc.round == p.round and pc.id == Some(id(p.proposal)) + ) + val precommit_evidence = enabling_precommits.map(pc => choreo::CustomEffect(CollectEvidence(PreCommit(pc)))) + + // Make decision and move to decided stage + { + post_state: { + ...s, + decision: Some(p.proposal), + stage: DecidedStage + }, + effects: Set(choreo::CustomEffect(CollectEvidence(Propose(p)))).union(precommit_evidence) + } + } + + pure def on_propose_timeout(ctx: LocalContext): Set[Transition] = { + val s = ctx.state + + val candidates = ctx.events.filter(t => t.round >= s.round and t.kind == ProposeTimeout) + val guard = and { + candidates.size() > 0, + s.stage == ProposeStage, + } + + if (guard) + Set({ + post_state: { ...s, stage: PreVoteStage }, + effects: Set( + choreo::Broadcast(PreVote({ src: s.process_id, round: s.round, id: None })), + choreo::TriggerEvent({ kind: PreVoteTimeout, round: s.round }) + ), + }) + else + Set() + } + + pure def on_prevote_timeout(ctx: LocalContext): Set[Transition] = { + val s = ctx.state + + val candidates = ctx.events.filter(t => t.round >= s.round and t.kind == PreVoteTimeout) + val guard = and { + candidates.size() > 0, + s.stage == PreVoteStage, + } + + if (guard) + Set({ + post_state: { ...s, stage: PreCommitStage }, + effects: Set( + choreo::Broadcast(PreVote({ src: s.process_id, round: s.round, id: None })), + choreo::TriggerEvent({ kind: PreCommitTimeout, round: s.round }) + ), + }) + else + Set() + } + + pure def on_precommit_timeout(ctx: LocalContext): Set[Transition] = { + val s = ctx.state + + val candidates = ctx.events.filter(t => t.round >= s.round and t.kind == PreCommitTimeout) + val guard = and { + candidates.size() > 0, + s.stage == PreCommitStage, + } + + if (guard) + Set(start_round(ctx, s.round + 1)) + else + Set() + } + + pure def main_listener(ctx: LocalContext): Set[Transition] = + Set( + choreo::cue(ctx, listen_proposal_in_propose, broadcast_prevote_for_proposal), + choreo::cue(ctx, listen_proposal_in_propose_prevote, broadcast_prevote_with_validation), + choreo::cue(ctx, listen_proposal_in_precommit_no_decision, decide_on_proposal), + choreo::cue(ctx, listen_proposal_in_prevote_commit, lock_value_and_precommit), + choreo::cue(ctx, listen_quorum_prevotes_any, trigger_prevote_timeout), + choreo::cue(ctx, listen_quorum_nil_prevotes, broadcast_nil_precommit), + choreo::cue(ctx, listen_quorum_precommits_any, trigger_precommit_timeout), + on_propose_timeout(ctx), + on_prevote_timeout(ctx), + on_precommit_timeout(ctx) + ).flatten() + + pure val initial_message = + Propose({ src: PROPOSER.get(0), round: 0, proposal: VALUES.get(0), valid_round: -1 }) + + pure val initial_bookkeeping = { + evidence_propose: Set(), + evidence_prevote: Set(), + evidence_precommit: Set(), + } + + def initialize_process(n: Node): LocalState = { + { + process_id: n, + round: 0, + stage: ProposeStage, + decision: None, + locked_value: None, + locked_round: -1, + valid_value: None, + valid_round: -1, + after_prevote_for_first_time: false, + precommit_quorum: false, + } + } + + // Card(powset) = 4096 + val byzantine_messages_1 = { + val rounds = Set(0) + val byzantine_proposals = rounds.map(r => { + tuples(FAULTY, Set("v0","v1","v2"), rounds.setAdd(-1)).map(((f, v, vr)) => { + Propose({ src: f, round: r, proposal: v, valid_round: vr }) + }) + }).flatten() + + val byzantine_prevotes = rounds.map(r => { + tuples(FAULTY, Set("v0","v1","v2")).map(((f, v)) => { + PreVote({ src: f, round: r, id: Some(v.id()) }) + }) + }).flatten() + + val byzantine_precommits = rounds.map(r => { + tuples(FAULTY, Set("v0","v1","v2")).map(((f, v)) => { + PreCommit({ src: f, round: r, id: Some(v.id()) }) + }) + }).flatten() + + // Reintegrate byzantine proposals + Set(byzantine_prevotes, byzantine_precommits, byzantine_proposals).flatten() + } + + // TODO (Gabriela): I don't understand this + val byzantine_messages = { + // We can have at most 2^F faulty nodes, so we take the powerset of the byzantine messages + // to simulate all possible combinations of byzantine messages. + + // 10 dummy prevotes for future rounds + val dummy_prevotes = 1000.to(1025).map(r => { + PreVote({ src: "p4", round: r, id: None }) + }) + dummy_prevotes + //Set() + } + + action init = choreo::init({ + system: NODES.mapBy(n => initialize_process(n)), + messages: NODES.mapBy(n => Set(initial_message).union(byzantine_messages_1)), + events: NODES.mapBy(n => Set()), + extensions: initial_bookkeeping + }) + + def apply_custom_effect(env: GlobalContext, effect: CustomEffects): GlobalContext = { + match effect { + | CollectEvidence(msg) => { + pure val updated_bookkeeping = match msg { + | Propose(p) => { ...env.extensions, evidence_propose: env.extensions.evidence_propose.setAdd(p) } + | PreVote(p) => { ...env.extensions, evidence_prevote: env.extensions.evidence_prevote.setAdd(p) } + | PreCommit(p) => { ...env.extensions, evidence_precommit: env.extensions.evidence_precommit.setAdd(p) } + } + { ...env, extensions: updated_bookkeeping } + } + } + } + + action step = choreo::step( + main_listener, + apply_custom_effect + ) + + + // -------------------------------- semantic symmetry utilities -------------------------------- + // TODO: Update these functions to work with choreo library + /* + type StateFingerprint = { + round: Round, + stage: Stage, + decision: Option[Value], + locked_value: Option[Value], + locked_round: Round, + valid_value: Option[Value], + valid_round: Round, + after_prevote_for_first_time: bool, + precommit_quorum: bool, + is_proposer: bool, + } + + pure def select_representatives(s: GlobalState): Set[Node] = { + val states = s.system.values() + val unique = group_by_fingerprint(states) + unique.setByAll((k, group) => group.filter(n =>(n.in(CORRECT))).takeOne()). + values().filter(n => n != None).map(n => n.unwrap()) + } + + def group_by_fingerprint(s: Set[LocalState]): StateFingerprint -> Set[Node] = { + val init = Map() + s.fold(init, (acc, state) => { + val key = { + round: state.round, + stage: state.stage, + decision: state.decision, + locked_value: state.locked_value, + locked_round: state.locked_round, + valid_value: state.valid_value, + valid_round: state.valid_round, + after_prevote_for_first_time: state.after_prevote_for_first_time, + precommit_quorum: state.precommit_quorum, + is_proposer: state.process_id == PROPOSER.get(state.round) + } + acc.setByWithDefault(key, (e) => e.setAdd(state.process_id),Set()) + }) + } + */ + // -------------------------------- semantic symmetry utilities -------------------------------- + + val correct_nodes = s.system.values().filter(s => s.process_id.in(CORRECT)) + val agreement = tuples(correct_nodes, correct_nodes).forall(((p1, p2)) => { + p1.decision == None or p2.decision == None or p1.decision == p2.decision + }) + + val one_decided = + not(s.system.values().forall(v => v.decision == None)) + + val all_decided = + not(correct_nodes.exists(v => v.decision == None)) + + // Witness for a to find a case where a node is in the decided stage while another is prevote + // state + val stages = + and { + s.system.values().map(v => v.stage).contains(ProposeStage), + s.system.values().map(v => v.stage).contains(PreVoteStage), + s.system.values().map(v => v.stage).contains(PreCommitStage), + s.system.values().map(v => v.stage).contains(DecidedStage) + } + + // Evidence-based properties + + pure def equivocation_in(n: Node, evidence: Set[{ src: Node, round: Round | other }]): bool = { + tuples(evidence, evidence).exists(((e1, e2)) => { + e1 != e2 and e1.src == n and e2.src == n and e1.round == e2.round + }) + } + + /// Equivocation by a node n + def equivocation_by(n: Node): bool = { + or { + n.equivocation_in(s.extensions.evidence_propose), + n.equivocation_in(s.extensions.evidence_prevote), + n.equivocation_in(s.extensions.evidence_precommit), + } + } + + /// Amnesic behavior by a node n + def amnesia_by(n: Node): bool = { + tuples(ROUNDS, ROUNDS).exists(((r1, r2)) => { + r1 < r2 and tuples(VALID_VALUES, VALID_VALUES).exists(((v1, v2)) => { + and { + v1 != v2, + s.extensions.evidence_propose.contains({ + src: n, round: r1, proposal: v1, valid_round: r1 + }), + s.extensions.evidence_propose.contains({ + src: n, round: r2, proposal: v2, valid_round: r2 + }), + ROUNDS.filter(r => r1 <= r and r < r2).forall(r => { + s.extensions.evidence_prevote.filter(m => { + m.round == r and m.id == Some(id(v2)) + }).size() >= 2 * F + 1 + }), + } + }) + }) + } + + /// Validity: the decided block satisfies the predefined predicate valid(). + val validity = correct_nodes.forall(s => s.decision.in(VALID_VALUES.map(v => Some(v)).setAdd(None))) + + /// The protocol safety. Two cases are possible: + /// 1. There is no fork, that is, Agreement holds true. + /// 2. A subset of faulty processes demonstrates equivocation or amnesia. + val accountability = or { + agreement, + FAULTY.powerset().exists(f => and { + f.size() >= F + 1, + f.forall(n => equivocation_by(n) or amnesia_by(n)), + }) + } + + const F: int + /// The set of correct processes + const CORRECT: Set[Node] + /// The set of faulty processes + const FAULTY: Set[Node] + /// Which node is the proposer in each round + const PROPOSER: Round -> Node + /// Which value is proposed in each round. + /// Values from rounds with correct nodes (in PROPOSER) are considered valid values. + const VALUES: Round -> Value + const MAX_ROUND: Round + + /// The set of all nodes + pure val NODES = CORRECT.union(FAULTY) + /// The set of all rounds (prepared by the constants) + pure val ROUNDS = VALUES.keys() + + /// The set of all correct values (prepared by the constants) + //pure val VALID_VALUES = ROUNDS.filter(k => PROPOSER.get(k).in(CORRECT)).map(k => VALUES.get(k)) + pure val VALID_VALUES = Set("v0", "v1", "v2") + + action step_with(v: Node, listener: LocalContext => Set[Transition]): bool = + choreo::step_with(v, listener, apply_custom_effect) + + action step_deterministic(v: Node, listener: LocalContext => Transition): bool = + choreo::step_deterministic(v, listener, apply_custom_effect) + + def with_cue(process, listen_fn, params) = + choreo::with_cue(process, listen_fn, params) + + action perform(cue_ctx, upon_fn) = + choreo::perform(cue_ctx, upon_fn, apply_custom_effect) + + action step_with_filter( + v: Node, + listener: LocalContext => Set[Transition], + f: Transition => bool + ): bool = + choreo::step_with_filter(v, listener, apply_custom_effect, f) + + action step_with_messages( + v: Node, + listener: LocalContext => Set[Transition], + f: Set[Message] => Set[Message] + ): bool = + choreo::step_with_messages(v, listener, apply_custom_effect, f) + + action lose_messages = choreo::lose_messages + + val s = choreo::s +} + diff --git a/.agents/skills/quint-modeling/examples/tendermint/tendermintTest.qnt b/.agents/skills/quint-modeling/examples/tendermint/tendermintTest.qnt new file mode 100644 index 00000000..8d0cc8a2 --- /dev/null +++ b/.agents/skills/quint-modeling/examples/tendermint/tendermintTest.qnt @@ -0,0 +1,98 @@ +/* ============================================================================= + * Tests for the Tendermint Choreo reference example. + * + * Per the test-separation guideline (quint-lang/guidelines/tests.md), `run` tests + * live in this file, NOT in the spec — `tendermint.qnt` has no `run` definitions. + * Each module here instantiates the parametric `tendermint` spec with concrete + * consts (the const-instantiation pattern) and drives a scenario: + * - `valid` — a run where consensus succeeds; `line28Test` exercises the + * proposal/prevote/precommit cue chain across a round change. + * - `no_agreement` — a faulty proposer equivocates; `disagreementTest` shows the + * `agreement` invariant being violated (a counterexample demo). + * + * Run / test (from this directory): + * quint run tendermintTest.qnt --main=valid --invariant="agreement and validity and accountability" + * quint test tendermintTest.qnt --main=valid + * quint run tendermintTest.qnt --main=no_agreement --invariant=accountability + * quint test tendermintTest.qnt --main=no_agreement + * ============================================================================= */ +module valid { + // quint run tendermintTest.qnt --main valid --invariant="agreement and validity and accountability" + // quint test tendermintTest.qnt --main valid + + import basicSpells.* from "./spells/basicSpells" + + import tendermint( + F = 1, + CORRECT = Set("p1","p2", "p3"), + FAULTY = Set("p4"), + PROPOSER = Map(0 -> "p1", 1 -> "p2", 2 -> "p3", 3 -> "p4"), + VALUES = Map(0 -> "v0", 1 -> "v1", 2 -> "v2", 3 -> "v3"), + MAX_ROUND = 4 + ).* from "./tendermint" + + run line28Test = { + val v0_proposal = { proposal: "v0", round: 0, src: "p1", valid_round: -1 } + + init + .then("p1".with_cue(listen_proposal_in_propose, v0_proposal).perform(broadcast_prevote_for_proposal)) + .then("p2".with_cue(listen_proposal_in_propose, v0_proposal).perform(broadcast_prevote_for_proposal)) + + .then("p1".with_cue(listen_proposal_in_prevote_commit, v0_proposal).perform(lock_value_and_precommit)) + .then("p2".with_cue(listen_proposal_in_prevote_commit, v0_proposal).perform(lock_value_and_precommit)) + + .then("p2".with_cue(listen_quorum_precommits_any, ()).perform(trigger_precommit_timeout)) + + .then(step_with("p2", on_precommit_timeout)) + // p2 starts a new round + .expect(s.system.get("p2").round == 1) + + // since p2 is the proposer in the new round, it will propose the stored valid value + .expect(and { + s.system.get("p2").stage == ProposeStage, + s.messages.values().forall(ms => { + ms.contains(Propose({ src: "p2", round: 1, proposal: "v0", valid_round: 0 })) + }) + }) + + // Which enables line 28 to be called by p2, as it is in propose stage and can receive + // the new proposal with valid_round 0. + .then("p2" + .with_cue(listen_proposal_in_propose_prevote, { proposal: "v0", round: 1, src: "p2", valid_round: 0 }) + .perform(broadcast_prevote_with_validation) + ) + } +} + +module no_agreement { + // Should fail: + // quint run tendermintTest.qnt --main no_agreement --invariants=agreement validity accountability + // + // Should succeed (accountability should still hold): + // quint run tendermintTest.qnt --main no_agreement --invariant=accountability + // quint test tendermintTest.qnt --main no_agreement + + import tendermint( + F = 1, + CORRECT = Set("p1", "p2"), + FAULTY = Set("p3","p4"), + // a faulty node is the proposer in the first round + PROPOSER = Map(0 -> "p3", 1 -> "p2", 2 -> "p3", 3 -> "p4"), + VALUES = Map(0 -> "v0", 1 -> "v1", 2 -> "v2", 3 -> "v3"), + MAX_ROUND = 4 + ).* from "./tendermint" + + run disagreementTest = { + val v0_proposal = { proposal: "v0", round: 0, src: "p3", valid_round: -1 } + val v1_proposal = { proposal: "v1", round: 0, src: "p3", valid_round: -1 } + + init + .then("p1".with_cue(listen_proposal_in_propose, v0_proposal).perform(broadcast_prevote_for_proposal)) + .then("p2".with_cue(listen_proposal_in_propose, v1_proposal).perform(broadcast_prevote_for_proposal)) + .then("p1".with_cue(listen_proposal_in_prevote_commit, v0_proposal).perform(lock_value_and_precommit)) + .then("p2".with_cue(listen_proposal_in_prevote_commit, v1_proposal).perform(lock_value_and_precommit)) + .then("p1".with_cue(listen_proposal_in_precommit_no_decision, v0_proposal).perform(decide_on_proposal)) + .then("p2".with_cue(listen_proposal_in_precommit_no_decision, v1_proposal).perform(decide_on_proposal)) + .expect(not(agreement)) + } +} diff --git a/.agents/skills/quint-modeling/guidelines/from-code.md b/.agents/skills/quint-modeling/guidelines/from-code.md new file mode 100644 index 00000000..5d5da0e4 --- /dev/null +++ b/.agents/skills/quint-modeling/guidelines/from-code.md @@ -0,0 +1,182 @@ +# From code — intake for generating a Quint spec from source + +This is the **code-intake flow**: extract a system's structure from source code (Rust, Go, +TypeScript, …). It carries only the code-specific intake; **after intake, follow the shared +spine (Steps 1–7) in `../SKILL.md`.** + +## Contents + +- **Why a spec from code** — the spec as a persistent grounding artifact +- **Phase 0: Research (compact)** — catalog the codebase before deep reading +- **Phase 1: Understand the code** — Steps 1.1–1.4 (domain, operations, state vars, system model) +- **The source-construct → Quint mapping** — the construct translation table +- **Set scope and abstraction level** — modules in/out, atomic granularity, details to hide (confirm with the user) +- **After intake** — hand off to the shared spine +- **Handoff** — spine Step 7 plus a source-file correspondence map + +## Why a spec from code + +**The spec is a persistent grounding artifact, not throwaway output** — the machine-checked source +of truth for what the system *is supposed to do*, used to verify every future change against. Code +stays primary: the spec *verifies* code, it does not generate it. A 50–200 line model captures what +1000 lines of code can't, and reviewing at the spec stage — then re-verifying it whenever the code +changes — is the highest-leverage check you get. + +## Phase 0: Research (compact) + +Before reading code in detail, catalog the codebase to keep the main context window lean +(target: under 400 lines of catalog). Skip this for simple single-file cases with a clear +entry point and no concurrency. + +> Explore `[path]`. Find and catalog: +> 1. The top-level entry points and their signatures +> 2. The state-carrying data structures (structs, maps, enums) +> 3. The concurrency primitives (mutexes, channels, goroutines, async tasks) +> 4. The message types and their payload shapes +> 5. Any existing tests that reveal expected behavior +> +> Output a compact summary — file paths, key types, key operations. Do not read every file. +> Prioritize what is most relevant to concurrent/distributed behavior. + +If the catalog missed something critical, do a targeted follow-up read — do not re-read the +whole codebase from scratch. + +## Phase 1: Understand the code + +Using the research output, answer in English. + +### Step 1.1 — Identify the problem domain + +1. **Overall purpose?** (e.g. "distributed leader election", "token transfer protocol") +2. **What entities exist?** (nodes, validators, clients, coordinators) +3. **What resources are shared or contested?** (leader slot, token balances, lock ownership) + +### Step 1.2 — Catalog operations and their purpose + +| Operation | Purpose | State it reads | State it writes | Concurrency notes | +|---|---|---|---|---| +| `functionName` | What it does | Which vars | Which vars | Atomic? Per-node? | + +**Focus on:** public API / entry points, state-modifying operations, message send/receive +points, timeout/timer triggers. + +**Skip:** side-effect-free getters, logging, metrics, serialization, retry plumbing (unless +retry is what you're verifying). + +### Step 1.3 — Identify state variables + +| Variable | Type | Purpose | Scope | +|---|---|---|---| +| `varName` | int / enum / record | What it tracks | Global / per-node | + +**Look for:** state-machine enums, per-node bookkeeping, shared global state, message buffers. + +### Step 1.4 — Extract system-model assumptions + +Read these off the code — concurrency primitives reveal the time model, error/recovery paths +reveal the failure model, transport reveals communication. **Fill in the spine's system-model +assumptions checklist (Step 1)** from what the code shows. + +## The source-construct → Quint mapping + +This is the heart of code intake: translate each language construct into its Quint counterpart. + +| Source | Quint | +|---|---| +| State enum | Sum type `\| State1 \| State2`, or `str` constant | +| Struct / record | `type T = { field1: Type1, field2: Type2 }` | +| `Vec` / array | `List[T]` | +| `HashSet` | `Set[T]` | +| `HashMap` | `K -> V` (map type) | +| Thread / process | Element of `NODES`; per-actor `NodeId -> LocalState` | +| Atomic CAS | Guarded action (atomicity is implicit) | +| Message queue | `Set[Message]` — message soup, no ordering unless ordering is what you verify | + +**Sum types use named constructors**, not inline tagged records: + +```quint +type Message = + | VoteRequest({ term: int, candidateId: int }) + | VoteResponse({ term: int, voterId: int, granted: bool }) +``` + +## Set scope and abstraction level — propose, then confirm + +You now understand what the code does. **Before** handing off to the spine, pin down three +decisions that the code itself cannot answer — they depend on *what you intend to verify*, and +getting them wrong is expensive to unwind once types and logic exist. Code is the flow where this +matters most: a codebase offers far more detail than a spec should reproduce. + +First, **infer** as much as you can from what the user already told you — the request usually +states (or strongly implies) the target property, which modules matter, and how faithful the +model must be. Don't re-ask what's already answered. Draft the three decisions from that, then +**present them as a proposal and ask the user to confirm the parts you inferred** — especially +anything you had to guess. This is the same propose-then-approve gate as the spine's type sketch, +just one step earlier. + +**1. Scope — which modules/components.** From the Phase 0 catalog, decide which modules/files are +*in* the spec and which are out, and what an out-of-scope component is replaced by (an abstract +map, an assumption). "Model the consensus core in `raft.rs`/`log.rs`; treat storage as an abstract +`Key -> Value` map; exclude the gRPC transport." Model what the target invariants depend on; leave +the rest out. **Only stop to confirm scope with the user when the boundary is genuinely +ambiguous** — if the request already makes it clear (names the component, the property, the entry +point), just state your inferred scope in one line and move on. Don't turn an obvious boundary into +a question. + +**2. Granularity — how many implementation steps become one atomic transition.** Real code does +one logical operation in many small steps (lock → read → check → mutate → unlock → ack). In the +model, decide the **atomic grain**: which sequences collapse into a single action. +- **Fuse into one atomic action** when the intermediate states are not observable to other actors, + or cannot interleave in a way that affects an invariant you care about. +- **Keep the steps separate** when the concurrent interleaving *between* sub-steps is exactly what + you are verifying — a check-then-act race (TOCTOU), a partial-write window, a torn read. + + This is the choice that most determines whether the spec can *find* a concurrency bug versus + silently *assume it away*, so make it deliberately and state it. (Atomicity is implicit in + Quint — an action either fires whole or not at all — so fusing is the default; splitting is the + decision that costs you nothing in syntax but buys interleaving coverage.) For **distributed + protocols specifically, lean toward splitting at message boundaries** — a process's local update + and another process observing its message are not simultaneous, so fusing them assumes away the + interleavings where most real bugs live. (This is also what Choreo structures for you.) + +**3. Implementation details — which mechanism to hide.** Source code is full of plumbing a spec +should *assume away*, not reproduce. For each concern the code surfaces, state model-it vs. +hide-it and why: + +| Detail in the code | Default in the spec | Model it only when… | +|---|---|---| +| Locks / mutexes / atomics | hide — steps are atomic (see decision 2) | lock-acquisition order is the property | +| Serialization / wire format | hide — pass structured values | a (de)serialization bug is the target | +| Retry / backoff / timeout plumbing | hide — model the outcome (success/failure) | retry semantics are the property | +| **Bounded channels / queues** | hide — unbounded `Set[Msg]` message soup, delivery is an action firing | back-pressure / blocking-on-full / capacity is the property | +| Network transport (sockets, framing) | hide — message soup; no delivery order | loss or ordering is what you verify | +| Error-propagation boilerplate | hide — a failed op is a guarded action that doesn't fire; *but if the error value itself is observable state you assert on (revert reason, error code), return it from a `pure def` as `{error, state}` instead* | error handling itself is the property | +| Concrete data structures (ring buffers, trees) | hide — `List` / `Set` / `Map` | the structure's own invariant is the target | +| Identifiers (node/request/txn IDs) | abstract — a small symbolic set (`NodeId = int`, opaque) | identity arithmetic/ordering is the property | +| Unbounded counters / sequence numbers | abstract — a small capped int or round counter | the bound itself is the property | + +The governing rule is the spine's "what, not how": if a detail doesn't affect an invariant you +care about, it doesn't belong in the spec. These choices become the `const` declarations and +system-model assumptions the spine's Step 1 will encode — you are deciding them here as one +coherent set, with the user's sign-off, rather than discovering them ad hoc while writing. + +## After intake + +With scope, granularity, and abstraction level agreed, hand the understanding (entities, the +operations table, the state variables, the filled-in assumptions checklist) to the **shared spine, +Steps 1–7** in `../SKILL.md`. The spine shapes the state, writes the pure functions, wires thin +actions, proposes witnesses-then-invariants, composes `step`, and simulates. Do not duplicate that +work here. + +## Handoff (spine Step 7, with a code-specific addition) + +Follow the spine's Step 7 handoff (covers / does NOT cover / when to update / ground truth), +and add a **source-file correspondence map** so each module is traceable back to the code it +grounds. When those files change, update the spec first and re-verify before touching the code. + +``` +Raft.qnt ↔ src/consensus/raft.rs, src/consensus/log.rs +Membership.qnt ↔ src/cluster/membership.go +``` + +To implement changes against this spec, use the `quint-execute-spec` skill. diff --git a/.agents/skills/quint-modeling/guidelines/from-nothing.md b/.agents/skills/quint-modeling/guidelines/from-nothing.md new file mode 100644 index 00000000..ce36b9bc --- /dev/null +++ b/.agents/skills/quint-modeling/guidelines/from-nothing.md @@ -0,0 +1,64 @@ +# From nothing — interactive build + +The user arrives with only an idea (or an informal proposal doc) and no written spec, code, or +TLA+ to translate. Intake here is a **guided interview**: you elicit the model one layer at a +time, and the user supplies the domain knowledge. After the interview, follow the shared spine +(Steps 1–7) in `../SKILL.md` to turn the elicited model into a verified spec. + +Unlike the other flows, there is no source artifact to read — so the risk is *modelling the +wrong thing*. The interview exists to surface the system's real shape before any code is +written, and to catch forks early. + +## The interview + +Walk these in order. Ask one focused question per layer; do not move on until the answer is +concrete. Each answer feeds a specific part of the spine. + +1. **Entities & types** — "What are the core entities, and what does each remember?" Get the + nouns (users, nodes, accounts, messages) and the data each carries. → feeds the spine's + Step 2 state record(s). + +2. **System model** — "How many participants? Can they fail? Is communication reliable/ordered? + Is time relevant?" Fill in the spine's **system-model assumptions checklist** (Step 1). Do + this early — it decides whether you need one actor or `NodeId -> LocalState`, a message soup, + a fault model. + +3. **Operations** — "What can happen? For each operation: who triggers it, what must be true + first (precondition), and what changes?" → feeds the spine's Step 3 pure functions. List them + in English first; no code yet. + +4. **Properties** — "What must *always* hold? (safety) What must be *reachable*? (liveness)" + Capture safety conditions (no negative balance, at most one leader, conservation laws) and the + key reachable states. → feeds the spine's Step 5 witnesses and invariants. + +5. **Initial state** — "How does the system start? What are the initial values?" → feeds `init` + in the spine's Step 4 (remember to pre-populate every map). + +6. **Exploration surface** — "Which operations should the simulator be free to fire, over what + parameter ranges?" → feeds the spine's Step 6 `step`. + +## Surface forks, don't bury them + +The interview's main value is catching ambiguity. **If two plausible readings of an answer imply +different state shapes or different action semantics, stop and ask** — describe the fork and its +consequence, and let the user choose. Never resolve a behavior-changing ambiguity by guessing a +default. Common forks: single actor vs. N actors; synchronous vs. asynchronous; whether a failed +operation is a no-op or an error state. + +## Pace and checkpoints + +- Build incrementally, exactly as the spine prescribes — one type, one pure function, one action + at a time, verifying each before the next. Do not dump a full spec from the interview. +- The type sketch (spine Step 2) is the **highest-leverage checkpoint**: present the types and + `var` declarations, typecheck them, and get explicit approval before writing any logic. +- **Stop before tests.** When the main spec is complete and simulates cleanly, say so and review + it with the user *before* writing any `*_test.qnt` file. Tests come after the main spec is + agreed, not alongside it. + +## Handoff + +Produce the spine's Step 7 handoff as a short comment block at the top of the spec (or a brief +note): what it models, what it deliberately leaves out, and which design forks were resolved and +how. For an interactive build there is no source artifact to map back to — the "what it does NOT +cover" note is the important part, since the scope was decided in conversation and is easy to +lose. diff --git a/.agents/skills/quint-modeling/guidelines/from-requirements.md b/.agents/skills/quint-modeling/guidelines/from-requirements.md new file mode 100644 index 00000000..fa07ca02 --- /dev/null +++ b/.agents/skills/quint-modeling/guidelines/from-requirements.md @@ -0,0 +1,85 @@ +# From requirements — written specification + +The user has a written requirements or functional-spec document (prose, user stories, a design +doc) and wants it modelled in Quint. Intake here is **reading and structuring**: you extract the +model from the document rather than interviewing for it. After intake, follow the shared spine +(Steps 1–7) in `../SKILL.md`. + +This is the non-interactive sibling of `from-nothing`: the same target understanding (entities, +operations, properties, system model), but sourced from a document instead of a conversation. +The interaction is *targeted* — you ask the user only where the document is silent or ambiguous +on something that changes the spec. + +## Extract from the document + +Read the document and produce, in English, the same understanding the spine builds on: + +1. **Entities & state** — what the system tracks and what each entity remembers. → spine Step 2. +2. **System model** — fill the spine's **assumptions checklist** (Step 1): participants, failure + model, communication, time. Requirements docs frequently leave these *implicit* — list what + the document states explicitly and flag what it omits (see below). +3. **Operations** — every operation the document describes, with its precondition and effect. + → spine Step 3 pure functions. +4. **Properties** — the document's stated guarantees become invariants; "the system can reach X" + statements become witnesses. → spine Step 5. When the document yields **many** candidate + properties, score each by how directly it validates what the user asked for, so verification + effort goes to what matters first: + - **High** — directly validates the requested change / the document's core guarantee. + - **Medium** — validates a supporting path or edge condition. + - **Low** — a broad safety check not specific to this request. + + Give each a one-line rationale citing the requirement it comes from. This is a triage aid for + ordering work and presenting results — not a Quint construct; skip it when there are only a + couple of obvious properties. + +A table is often the cleanest intake artifact: + +| Requirement (quote/§) | Entity / operation / property | Maps to | +|---|---|---| +| "a transfer must not overdraw" | invariant: no negative balance | spine Step 5 | +| "any node may propose" | operation `propose(node)` | spine Step 3 | + +Linking each modelling decision back to a line in the document keeps the spec traceable and makes +the handoff's "what it covers" trivial to write. + +## Resolve gaps before modelling, not by guessing + +Requirements docs are written for humans and routinely omit what a formal model must pin down — +exact failure semantics, ordering, what happens on a precondition violation, how many +participants. **Where the document is silent on something that changes state shape or action +semantics, ask one targeted question** rather than inventing a default. Record the answer (and +that it was a gap) so the handoff can note it. Where the document is silent on something +*immaterial* to any property you care about, omit it — that's the "what, not how" rule from the +spine. + +## Set scope and granularity (confirm before modelling) + +A requirements doc usually describes more than one spec should cover, and states *what* happens +without fixing the *grain* at which to model it. Before the spine, settle two choices — infer them +from the doc and the user's ask, then confirm the parts you inferred (same targeted-question +discipline as the gaps above): + +- **Scope — which requirements are in.** Docs carry future phases, explicitly out-of-scope + sections, and features irrelevant to the property you're after. State which requirements this + spec models and which it leaves out, tied to what's being verified. Decide this **now**, before + writing types — the spec you build should be shaped by the scope, not have a scope justified + after the fact. (The handoff later just records what you agreed here.) +- **Granularity — the atomic grain of each operation.** A requirement like "a customer confirms a + booking" may be one atomic action, or may decompose into steps (reserve → pay → confirm) whose + *interleaving* matters. Model an operation as a single action when its intermediate states can't + be observed or interleaved in a way that affects an invariant; split it into separate actions + when concurrent interleaving between the steps is part of what you're checking. The doc rarely + states this — it is a modelling decision, so make it deliberately. + +## Then follow the spine + +With the understanding extracted, proceed through the spine: separate concerns, shape the state, +write pure functions, wire thin actions, add witnesses then invariants, compose `step`, simulate. +Build incrementally and get approval on the type sketch (spine Step 2) before writing logic. + +## Handoff + +The spine's Step 7 handoff, with a requirements-specific addition: a **coverage map** linking each +modelled operation/property back to the requirement it came from, and the **scope you agreed +up front** — the requirements left out (and why) — recorded as the "what it does NOT cover" list. +The gaps you had to ask about belong in that same assumptions note. diff --git a/.agents/skills/quint-modeling/guidelines/from-tlaplus.md b/.agents/skills/quint-modeling/guidelines/from-tlaplus.md new file mode 100644 index 00000000..6a6b6fba --- /dev/null +++ b/.agents/skills/quint-modeling/guidelines/from-tlaplus.md @@ -0,0 +1,295 @@ +# From TLA+: translation intake + +This is the intake flow for translating an existing TLA+ specification into Quint. Do the TLA+-specific intake and design decisions below, then follow the shared modelling spine (Steps 1–7) in `quint-modeling/SKILL.md`. + +## Contents + +- **Philosophy: fidelity** — what the translation must preserve +- **The "Maintain" rules** — CONSTANT/ASSUME, abstract sets, sentinel constants, EXTENDS +- **The "Change" rules** — where idiomatic Quint diverges (local state, thin actions, message soup → Choreo, listen/send naming) +- **Phase 1: TLA+ intake** — Steps 1.1–1.4 (domain, EXTENDS, CHOOSE, variables, actions, system model) +- **TLA+-specific output format** — editor mode line, `NameAnalysis` module, `README.md` +- **TLA+-specific pitfalls** — the gotcha table + +## Philosophy: fidelity + +TLA+ specs have precise semantics that must be maintained. Most TLA+ specs translate close to one-to-one, but Quint offers modern syntax that reads better. The goal is a readable Quint spec **where the TLA+↔Quint mapping is obvious**. + +Concretely: + +- **Maintain names** — of variables, functions, and operators. +- **Infer TLA+ types.** If a type is genuinely in doubt, ask the user rather than guessing. +- **Translate every TLA+ invariant faithfully.** Each TLA+ invariant operator (`Inv`, `TypeOK`, `VotesSafe`, etc.) becomes a `val` in Quint with the **same name**, the **same conjunct order**, and inline comments that cite the TLA+ formula. Do **not** add invariants that have no TLA+ counterpart, do not split a TLA+ invariant into helper `val`s, and do not introduce safety properties beyond what the TLA+ spec defines. + +## The "Maintain" rules + +These preserve the TLA+ structure. Keep the concrete forms below — they are the value of this flow. + +### Modularity: CONSTANT and ASSUME + +- **CONSTANT → separate parametric modules.** If a TLA+ spec uses `CONSTANT` and separate TLA+ files instantiate those constants, do the same in Quint: a parametric module with `const` declarations, plus an instantiation module. +- **ASSUME → a `run` test in the same parametric module as the `const`** — *not* in the instantiation module, and *not* via Quint's `assume` keyword (do not use `assume`). + +```quint +run quorumAssumptionTest = { + all { + Quorum.forall(Q => Q.subseteq(Acceptor)), + Quorum.forall(Q1 => Quorum.forall(Q2 => Q1.intersect(Q2) != Set())), + } +} +``` + +Run it via `quint test --main `. The test passes when all conditions hold; it fails (reporting which step failed) otherwise. + +### Abstract TLA+ sets → const + parametric types + +`CONSTANT Value, Acceptor` become `const`s with **descriptive lowercase type variables** (e.g. `acceptorType`, `valueType`). Use distinct variables when the sets have independent element types. Any type that mentions them must be made **parametric**. The `var` and function signatures use the module-level type variables directly; the instantiation module infers concrete types from the const values, so no explicit type arguments are needed: + +```quint +const Acceptor : Set[acceptorType] +const Value : Set[valueType] +const Quorum : Set[Set[acceptorType]] +type Vote[v] = { bal : int, value : v } +type AcceptorState[v] = { votes : Set[Vote[v]], maxBal : int } +var acceptors : acceptorType -> AcceptorState[valueType] +// function sigs can omit the type param when Quint can infer it: +// (state : AcceptorState, acc : acceptorType, v : valueType, ...) + +// Instance: types inferred as acceptorType=int, valueType=int +import Voting(Acceptor = Set(1,2,3), Value = Set(0,1), Quorum = ...).* +``` + +### TLA+ sentinel constants → variants of a shared sum type + +TLA+ sentinel constants (`CONSTANT any, none` with `any \notin Values \union {none}`) should be translated as explicit variants of a **shared sum type** defined in a **separate module** — not as an abstract `const`. Define the type in its own file (e.g. `PaxosValues.qnt`) imported by all modules that need it: + +```quint +// PaxosValues.qnt +module PaxosValues { + type AllValues[v] = Any | NoVal | Val(v) + // Val(v) — a real protocol value + // Any — TLA+ `any` sentinel + // NoVal — TLA+ `none` sentinel +} +``` + +The assume test can then faithfully check both exclusions: + +```quint +not(Values.contains(Any)), // any \notin Values \union {none} +not(Values.contains(NoVal)), // none \notin Values \union {any} +``` + +The type **must** live in a separate module (see the parametric-type cycle in Pitfalls). + +## The "Change" rules + +These are the places where idiomatic Quint diverges from the TLA+ structure. + +- **Distributed local state.** In TLA+ local variables are often multiple flat maps from process IDs to values. In Quint, encode a `LocalState` record capturing one process's local variables, and store the system state as a map from process ID to `LocalState`. (The spine's Step 2 covers the general mechanics of this map-of-records shape.) + +- **Truly thin action layer.** All guards and state computation belong in `pure def`s; actions are single assignments that call the pure def and set the next state. When a TLA+ guard that would *disable* an action is instead absorbed into a pure def's `if-else` (returning the old state on failure), this introduces stuttering — document it on the action with a note and a hint for how to remove it: + + ```quint + // Note: applyFoo returns st unchanged when fails — stuttering vs TLA+. + // To remove stuttering add: , + action foo(acc : acceptorID, bal : int) : bool = all { + acceptors' = acceptors.set(acc, applyFoo(acceptors, acc, bal)), + } + ``` + + Returning-old-state-on-failure is acceptable as a **transient step** during translation (it lets + you get the spec running before every guard is lifted out), but it is not the end state: a + blanket no-op fabricates a transition the protocol doesn't have. Before handoff, lift the guards + out so the action is properly disabled when its precondition fails — see the spine's Step 6 + ("Model transitions faithfully: guard actions, don't fake no-ops") for the reasoning. + +- **Message soup → consider Choreo.** A TLA+ message soup (a `msgs` variable, a `Send(m)` helper, or any `\cup {m}` pattern on a message set) is a strong signal the protocol is genuinely message-passing — **raise Choreo in design before writing any code, unless the protocol is single-actor or simple enough that plain Quint is clearer** (see the spine's "When to use Choreo"). When it fits, use Choreo (`../quint-lang/guidelines/choreo.md`) and ask the user to confirm the node/role decomposition (which processes send/receive which message types, whether to introduce an explicit leader). The TLA+ soup is monotone (messages are never deleted) — decide whether Choreo inboxes should consume messages or leave them in place. + +- **Choreo `listen_*` / `send_*` naming.** `listen_X` is named after what it watches for (the triggering condition or incoming message type); `send_X` is named after the message type it produces (= the TLA+ operator name). E.g., TLA+ `Phase2b(a,b,v)` is triggered by a Phase2a message → `listen_phase2a` / `send_phase2b`. For an initial step with no incoming message, use a descriptive name like `listen_start` or `listen_phase1b_quorum`. The `main_listener` then reads as a direct transliteration of the TLA+ `Next` definition. + +### TLA+ `EXTENDS` → `import M(...) as m` + `export m` + +Quint has no `EXTENDS` keyword. The closest equivalent is a namespaced import where the extending module re-declares the shared `const`s, passes them through, and re-exports the namespace: + +```quint +// FastPaxos.qnt — mirrors TLA+ EXTENDS Paxos +const Replicas : Set[acceptorType] // re-declare shared consts +const Ballots : Set[int] +// ... + +import Paxos( + Replicas = Replicas, // pass through to base module + Ballots = Ballots, + // ... +) as paxos from "Paxos" +export paxos // re-export so analysis modules can call paxos::f +``` + +The analysis module then calls `paxos::listen_p2a_value`, `paxos::upon_classic_decide`, etc. without duplication. + +**Constraint — higher-order const bindings (verify locally).** In some cross-module setups (observed +passing `paxos::f` to `choreo::cue`), a function whose body references module-level `const`s loses +the const binding when used as a higher-order argument across the boundary, failing at runtime with +"Uninitialized const". This is **not** a reliable general rule — a plain higher-order const closure +across a normal `import` runs fine on current Quint — so treat it as a setup-specific gotcha to check +against your own version, not a law. If you do hit it, the fix is to rewrite the body to use only the +type structure: `Values.contains(r.value)` → `r.value != Any and r.value != NoVal` (equivalent when +`Values` only contains `Val(v)` elements, which the assume test guarantees). + +**Constraint — cross-module `val` invariants.** The governing insight: a `pure def`'s arguments are +evaluated at the **call site**, so consts passed in refer to the *caller's* (bound) consts; a `val` +is evaluated in **its own defining module's** const environment, which is not propagated across an +import boundary. So referencing `paxos::TypeOKInvariant` (a `val`) from the extending module fails at +runtime with "Uninitialized const". Fix: extract the invariant body into a `pure def` that takes the +consts as explicit parameters, then call it from both the base `val` and the extending module's +invariant. + +```quint +// Paxos.qnt — base module +pure def checkPaxosTypeOK( + values: Set[AllValues], + ballots: Set[int], + decision: AllValues, + replicaStates: Set[StateFields] // pass s.system.get(a).state for each replica +): bool = + (values.contains(decision) or decision == NoVal) + and replicaStates.forall(sf => + match sf { + | AcceptorState(acc) => + ballots.contains(acc.maxBallot) + and ballots.contains(acc.maxVBallot) + and (values.contains(acc.maxValue) or acc.maxValue == NoVal) + | CoordState(_) => false + } + ) + +val PaxosTypeOK: bool = + checkPaxosTypeOK(Values, Ballots, coordState.decision, + Replicas.map(a => s.system.get(a).state)) + +// FastPaxos.qnt — extending module +// paxos::checkPaxosTypeOK works because it takes consts as explicit parameters. +// paxos::PaxosTypeOK would fail — val references do not propagate const bindings. +val FastTypeOK: bool = + paxos::checkPaxosTypeOK(Values, Ballots, coordState.decision, + Replicas.map(a => s.system.get(a).state)) + and (Values.contains(coordState.cValue) or coordState.cValue == NoVal) +``` + +(In the example above, `checkPaxosTypeOK` works from FastPaxos because `Values`/`Ballots` are +evaluated at the call site against FastPaxos's bound consts.) + +When the operator genuinely differs between base and extending module (e.g. `ClassicDecide` iterates `ClassicBallots` in FastPaxos but `Ballots` in Paxos), define the function locally in the extending module. This is semantically correct, not a workaround. + +**File length check — ask before combining `EXTENDS` modules.** When the TLA+ source uses `EXTENDS AnotherFile`, check the total line count of both files. If keeping them combined would make the Quint file noticeably longer than its TLA+ counterpart, present the user with a choice: + +- **One file** — simpler imports for the analysis module, but potentially long. +- **Two files mirroring TLA+** — each Quint file stays close in length to its TLA+ source; base module content is duplicated (~15–20 lines) since Quint parametric modules cannot be cross-imported cleanly. + +Ask the user which they prefer before writing any code. A Quint file should not be substantially longer than the TLA+ spec it translates. + +## Phase 1: TLA+ intake + +### Step 1.1: Identify the problem domain + +Read the spec and answer in English: + +1. **What is the overall purpose?** (e.g. "distributed leader election", "two-phase commit") +2. **What entities exist?** (e.g. nodes, validators, clients, coordinators) +3. **What resources are shared or contested?** (e.g. leader slot, token balances, lock ownership) + +### Step 1.1b: Analyze TLA+ `EXTENDS` imports + +Before translating any spec that uses `EXTENDS AnotherFile`, read `AnotherFile` and list **precisely** which operators/definitions the extending spec actually uses. This determines the Quint module structure. Example: + +| Used from Paxos | How used in FastPaxos | +|---|---| +| `PaxosAccepted` | `ClassicAccepted == UNCHANGED<> /\ PaxosAccepted` | +| `PaxosInit` | `FastInit == PaxosInit /\ cValue = none` | +| `PaxosTypeOK` | `FastTypeOK == PaxosTypeOK /\ cValue \in Values \union {none}` | +| `Ballots`, `Quorums`, `Replicas`, `Values` | shared constants, inherited | + +Operators **not** used (e.g. Phase1a/1b, Phase2a, PaxosNext) can be dropped from scope. This analysis drives the module-structure decision before any code is written. + +### Step 1.1c: Flag TLA+ `CHOOSE` operators + +Scan the TLA+ spec for any use of `CHOOSE` (e.g. `CHOOSE x \in S : P(x)`). Quint has no direct `CHOOSE` equivalent. For **each** occurrence: + +1. Quote the TLA+ expression to the user. +2. Explain what it does in context (picks an arbitrary element satisfying a predicate, defines a canonical representative, etc.). +3. Ask the user how to handle it. Common options: + - **Nondeterministic choice** — replace with `nondet x = S.filter(e => P(e)).oneOf()` in an action (only valid in an action context). + - **Deterministic selection** — replace with a deterministic helper (e.g. `S.filter(P).fold(...)`) if the choice is semantically irrelevant. + - **Constant / abstract value** — introduce a `const` or `pure val` if the chosen value is fixed for a run. +4. **Do not translate any `CHOOSE` occurrence until the user has approved an approach for it.** + +### Step 1.2: Analyze the variables + +1. **What is local state?** Read and written only by one entity (e.g. round number, decision value). Often a map from entity to variable. +2. **What is global state?** Read and written by more entities (shared variables, message soup). +3. **What is the type of each variable?** +4. **Is there state that fits neither?** Ask the user for advice. + +### Step 1.3: Analyze the actions in `Next` + +1. **Init.** +2. **What are the actions that constitute `Next`?** Separate entity actions from environment actions. What are their effects on local and global state? + +### Step 1.4: System-model assumptions + +Fill in the spine's system-model assumptions checklist (Step 1 of `quint-modeling/SKILL.md`) from the TLA+ spec — communication, failures, time, participants. These become the `const` declarations and the `run` test that checks the translated `ASSUME` (Maintain rules above). + +--- + +After this intake and the TLA+-specific design decisions above, follow the shared spine (Steps 1–7) in `quint-modeling/SKILL.md`. For Quint syntax, operators, and the CLI, defer to **quint-lang**. + +## TLA+-specific output format + +This flow's deliverables differ from the other flows in three ways: + +### 1. Spec module — editor mode line + +The spec module's **first line must be `// -*- mode: Bluespec; -*-`** — before any other content, including the module header comment. The spec module is the faithful TLA+ translation: same names, same structure, same properties, same conjunct order, a pure functional layer for all guards and state computation, and Choreo for any message soup. No witnesses, runs, or concrete instances live here — those go in the analysis module. + +### 2. `NameAnalysis` module — appended to the same `.qnt` file + +A second module written at the bottom of the same `.qnt` file (no separate file). It holds the concrete instantiation of the spec module (e.g. 3 nodes, specific constant values), happy-path runs showing how the protocol works, and the witnesses. The module name follows the pattern `Analysis`, e.g. `PaxosAnalysis`. Run with `--main NameAnalysis` (not the spec module name), because the analysis module holds the concrete `init` and `step`. + +```quint +module PaxosAnalysis { + import Paxos( + Acceptor = Set(1, 2, 3), + // ... other constants + ).* + + // happy-path run + run happyPathTest = { ... } + + // Witness: not in TLA+, inferred for reachability checking + val canPhase1aSent: bool = ... +} +``` + +### 3. `README.md` next to the `.qnt` file + +This is the spine's Step 7 handoff, in TLA+-translation form. It documents the translation and records simulation results: + +- **Purpose** — one paragraph: Quint translation of which TLA+ spec, link to the TLA+ source, brief description of what the protocol does. +- **Invariants** — for each `val` invariant: its name, the TLA+ operator it translates, a one-line description. Note that invariants are taken directly from the TLA+ spec, not invented. +- **Witnesses** — table: name, what it confirms is reachable. State explicitly that witnesses are not in the TLA+ spec and were added for simulation coverage. +- **Experiments** — the executed `quint run` arguments and an output summary (the `[ok]` line, trace-length statistics, witness counts), plus a one-sentence interpretation. Record the **date** of each run. When parameters change (e.g. higher `--max-steps`), add a new dated run block rather than overwriting the old one. + +## TLA+-specific pitfalls + +| Pitfall | Solution | +|---|---| +| `val` is a keyword | TLA+ specs often have a `val` field; it cannot be a record field name in Quint. Use `value` instead | +| `ASSUME` → `assume` | Do not use Quint's `assume` keyword. Translate TLA+ `ASSUME` as a `run` test in the same parametric module as the `const` (see Maintain rules) | +| `msgs` / message soup → plain `Set[Message]` | Consider Choreo instead — a soup is a strong Choreo signal; raise it in design unless the protocol is single-actor/simple (see the spine) | +| TLA+ `EXTENDS` → duplicating all functions | Use `import M(...) as m` + `export m`. The extending module re-declares shared `const`s and passes them through. Functions with no `const` refs in their body are safely reusable as `m::f` | +| `type T[v] = ...` in same module as `const X: Set[T]`, instantiated with `X = Set(Constructor(v))` | Quint detects a self-referential cycle: `Constructor` is defined by `T`, defined in the module being instantiated. Fix: extract `type T[v]` to a separate shared module imported by both base and extending modules | +| `m::f` fails at runtime with "Uninitialized const" when passed as a higher-order argument *(setup-specific — verify; a plain cross-module higher-order const closure runs fine on current Quint)* | If you hit it: rewrite `f` to avoid const references — use the type structure (e.g. `x != SentinelA and x != SentinelB` rather than `Values.contains(x)`) | +| `paxos::TypeOKInvariant` (a `val`) fails at runtime with "Uninitialized const" | Quint does not propagate `const` bindings when a `val` is accessed across module boundaries, even without higher-order calls. Fix: extract the invariant body into a `pure def` taking the consts as explicit parameters, then call it from both the base `val` and the extending module (see EXTENDS section) | +| TLA+ `CHOOSE` | No direct Quint equivalent. Flag each occurrence to the user (Step 1.1c) and get an approved approach — nondet, deterministic helper, or const — before translating | +| Parenthetical insertions with em-dashes in README or Quint comments | In *generated output* (the spec, its comments, the README) — not this guideline's own prose — do not write sentences that insert a clause between em-dashes ("X — which does Y — Z"). Use separate sentences or plain parentheses instead | diff --git a/.agents/skills/quint-modeling/guidelines/review.md b/.agents/skills/quint-modeling/guidelines/review.md new file mode 100644 index 00000000..e6634b64 --- /dev/null +++ b/.agents/skills/quint-modeling/guidelines/review.md @@ -0,0 +1,102 @@ +# Review — auditing an existing Quint spec + +This is the **review flow**: the input is a *finished* `.qnt` spec and the job is to audit it, not +build it. There is no new model to produce — the deliverable is a **report** of pass / warn / fail +findings plus fixes. You reach it two ways: directly ("review/audit this spec", a collaborator's or +your own), or as the deeper pass Step 7 escalates to when a quick self-review isn't enough. Either +way the procedure below is the same. + +Review does not introduce a new rulebook. **It checks a finished spec against the same discipline +this skill teaches for building one** (the spine, Steps 1–7, and the Conventions). So the +structural pass below is mostly "did the spec follow the rules already stated above?" — this file +points back to them rather than restating them. The genuinely review-only part is the **runtime +checks** (you have to *run* the spec) and the **report format**. + +For language/operator questions that come up while reviewing, defer to **quint-lang** as usual. + +## Preparation + +1. Read the spec. Catalogue: `var` declarations, `pure def`s, `action`s, `val` invariants and + witnesses, `temporal` properties, `assume` axioms. +2. For the runtime checks, drive the spec: `quint -r ::` then run `init`, + or use `quint run`. +3. Run **all** checks, record **pass / warn / fail** for each, print the report only at the end. + +## Structural checks — does the spec follow the build discipline? + +Each row is a rule from the spine; the spec passes if it followed it, warns/fails if not. Re-read +the referenced step if you need the rationale — don't reinvent it here. + +| Check | Rule (where it's taught) | Fail / warn when | +|---|---|---| +| **Guarded actions** | Step 4 — logic in `pure def`s, guard lifted into the action | an action carries business logic beyond the guard + assignments, or uses a no-op fallback instead of being disabled | +| **Enums over strings** | Conventions — sum types / typed constants for roles & phases | raw string literals like `"leader"`, `"propose"` in state or guards | +| **Record grouping** | Step 2 — group cohesive state into a record | several `var`s describe one entity but aren't grouped | +| **Witnesses present & per-action** | Step 5 — one witness per major action, checked with `--witnesses` (reached in > 0 traces) | no witnesses, or a major `step` action has none, or a witness reaches 0 traces (dead) | +| **Right abstraction** | "What, not how" + Conventions — opaque IDs, message soup | string manipulation, messages in an ordered `List` (when order isn't the property), serialization/retry/memory detail | +| **Non-trivial invariants** | Step 5 — at least one real protocol property | every invariant is a type/bounds check, or an invariant references no `var` (a tautology) | + +Anything that **fails** here is a correctness or value problem; a **warn** is a judgement call to +raise with the user. + +## Runtime checks — these need you to *run* the spec (review-only) + +- **C-init — `init` assigns every `var`.** Run `init`; every declared `var` must be assigned. + - Fail: `init` returns `false` / errors (quote it), or a `var` is unassigned. +- **C-step — every `var` updated in every action.** A missing `var' = ...` is a silent stutter + bug. Warn per gap: "Did you mean `' = `?" +- **C-step-complete — a `step` using `any { ... }` exists and includes all major actions.** + Warn if missing, or if an action exists but isn't in `step`. +- **R1 — invariants hold at `init`.** Evaluate each invariant after `init`. + - Fail: invariant `` is `false` at init — either `init` is wrong or the invariant is too + strong. +- **R2 — no counterexample after each action.** Fire each action reachable from init, re-check all + invariants. + - Fail: invariant `` violated after `` — show the state at violation. + +> R1/R2 are bounded checks driven from `init`, not a proof. A *violation* is conclusive (the spec +> does break the property); the *absence* of one across what you ran is **not** — report it as "no +> counterexample observed in the runs executed," never "the invariant holds." Stay with `quint +> run` for the review — do **not** reach for `quint verify`; exhaustive model checking is a separate +> activity, only when the user explicitly asks for it (see quint-lang's `guidelines/simulations.md` +> and `guidelines/cli.md`). See `guidelines/simulations.md` for the full result-language discipline. + +## Report format + +Print after all checks complete: + +``` +── Spec review: ────────────────────────────────── + +Structural (vs the build discipline) + thin actions [✓ / ⚠ list actions with logic] + enums over strings [✓ / ⚠ list raw string literals] + record grouping [✓ / ⚠ list ungrouped vars] + witnesses present & per-action[✓ / ✗ none / ⚠ uncovered or dead] + right abstraction [✓ / ⚠ describe what leaked] + non-trivial invariants [✓ / ✗ all structural / ⚠ tautologies] + +Runtime + init assigns all vars [✓ / ✗ error] + var assignment per action [✓ / ⚠ list gaps] + step exists & complete [✓ / ⚠ missing or incomplete] + invariants hold at init [✓ / ✗ list violations] + no counterexample after acts [✓ / ✗ broke ] + +── Top issues ───────────────────────────────────────────── + 1. + 2. + 3. + +── Suggested next steps ─────────────────────────────────── + 1. Fix the fails above + 2. Stress-test flagged scenarios with `quint run` / the REPL + 3. `quint run --invariant ` to sample (offer `quint verify` only if the user wants + exhaustive model checking — it is not part of the review itself) +``` + +## After the report + +Fix any **fail** immediately without asking — those are correctness problems. For **warn** +items, present the list and ask which to address (offer "All" and "None — leave as-is"); address +only what the user selects. diff --git a/.gitignore b/.gitignore index 2a3968ed..e8471957 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ config/.opencode/ config/.claude/ config/.pi/ config/schema/sce-config.schema.json + +_apalache-out/ diff --git a/flake.nix b/flake.nix index af4c735c..006fe159 100644 --- a/flake.nix +++ b/flake.nix @@ -63,6 +63,29 @@ }; }); + quintLanguageServerPackage = pkgs.buildNpmPackage { + pname = "quint-language-server"; + version = "0.19.0"; + src = pkgs.fetchurl { + url = "https://registry.npmjs.org/@informalsystems/quint-language-server/-/quint-language-server-0.19.0.tgz"; + hash = "sha256-gNoz7Pu+/TO/Vp86IB8tfZ9vHN78L7eRllITNtsnGFY="; + }; + postPatch = "cp ${./nix/quint-language-server/package-lock.json} package-lock.json"; + npmDepsHash = "sha256-zw8hBTxOUAcKgJrK1J+0APwtGv+2Cv22LcNHogaNBPc="; + dontNpmBuild = true; + nativeBuildInputs = [ pkgs.makeWrapper ]; + installPhase = '' + mkdir -p "$out/lib/node_modules/@informalsystems/quint-language-server" "$out/bin" + cp -r . "$out/lib/node_modules/@informalsystems/quint-language-server/" + makeWrapper "${pkgs.nodejs}/bin/node" "$out/bin/quint-language-server" \ + --add-flags "$out/lib/node_modules/@informalsystems/quint-language-server/out/src/server.js" + ''; + meta = { + description = "Language Server for the Quint specification language"; + mainProgram = "quint-language-server"; + }; + }; + rustVersion = "1.95.0"; rustToolchain = pkgs.rust-bin.stable.${rustVersion}.default.override { @@ -479,6 +502,8 @@ jq pkl pkl-lsp + quint + quintLanguageServerPackage typescript typescript-language-server vscode-json-languageserver @@ -499,6 +524,8 @@ echo "- biome: $(version_of biome)" echo "- pkl: $(version_of pkl)" echo "- pkl-lsp: $(version_of pkl-lsp)" + echo "- quint: $(version_of quint)" + echo "- quint-language-server: available" echo "- tsc: $(version_of tsc)" echo "- tsserver-lsp: $(version_of typescript-language-server)" echo "- rust: $(version_of rustc)" @@ -1502,6 +1529,8 @@ sce-release = sceReleasePackage; ci-checks = ciChecks; bun = bunPackage; + quint = pkgs.quint; + quint-language-server = quintLanguageServerPackage; turso = tursoPackage; default = scePackage; }; diff --git a/nix/quint-language-server/package-lock.json b/nix/quint-language-server/package-lock.json new file mode 100644 index 00000000..17737c04 --- /dev/null +++ b/nix/quint-language-server/package-lock.json @@ -0,0 +1,8462 @@ +{ + "name": "@informalsystems/quint-language-server", + "version": "0.19.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@informalsystems/quint-language-server", + "version": "0.19.0", + "license": "Apache 2.0", + "dependencies": { + "@informalsystems/quint": "^0.31.0", + "json-bigint": "^1.0.0", + "vscode-languageserver": "^7.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-uri": "^3.0.7" + }, + "bin": { + "quint-language-server": "out/src/server.js" + }, + "devDependencies": { + "@types/chai": "^4.2.18", + "@types/json-bigint": "^1.0.4", + "@types/lodash": "^4.14.191", + "@types/mocha": "^8.2.2", + "@types/node": "^12.12.0", + "@typescript-eslint/eslint-plugin": "^5.30.6", + "@typescript-eslint/parser": "^5.30.6", + "chai": "^4.3.4", + "eslint": "^8.27.0", + "eslint-config-recommended": "^4.1.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-json": "^3.1.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-unused-imports": "^2.0.0", + "mocha": "^8.4.0", + "ts-node": "^10.0.0", + "typescript": "^4.2.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@informalsystems/quint": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@informalsystems/quint/-/quint-0.31.0.tgz", + "integrity": "sha512-raRl+qhtx7ciL8ZwKSt26XZAJxB4OmbyXCkWsMPJ0f603GbyjBrpE3etdVALRJqhfd8eqoKTxg7VkWSzuQgyCw==", + "license": "Apache 2.0", + "dependencies": { + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.7", + "@octokit/request": "^8.1.1", + "@sweet-monads/either": "~3.2.0", + "@sweet-monads/maybe": "~3.2.0", + "@types/line-column": "^1.0.0", + "@types/lodash.clonedeep": "4.5.0", + "@types/seedrandom": "^3.0.4", + "adm-zip": "^0.5.16", + "antlr4ts": "^0.5.0-alpha.4", + "chalk": "^4.1.2", + "cli-progress": "^3.12.0", + "cross-spawn": "^7.0.6", + "eol": "^0.9.1", + "immutable": "^4.3.0", + "json-bigint": "^1.0.0", + "line-column": "^1.0.2", + "lodash": "^4.17.21", + "lodash.clonedeep": "4.5.0", + "seedrandom": "^3.0.5", + "tar": "^6.1.14", + "yargs": "^17.7.2" + }, + "bin": { + "quint": "dist/src/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sweet-monads/either": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@sweet-monads/either/-/either-3.2.0.tgz", + "integrity": "sha512-n+nR0b60GRTKb+D76qhTf4NEBXU9zfpigYYEtKtSYbV+5+i5gxr9jFd64pYkY2O7hVsb/G7nspbAeFni/i1ltA==", + "license": "MIT", + "dependencies": { + "@sweet-monads/interfaces": "^3.2.0" + } + }, + "node_modules/@sweet-monads/interfaces": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@sweet-monads/interfaces/-/interfaces-3.3.0.tgz", + "integrity": "sha512-66akGvjPD4lizQy+w4JSltJilc2w/QPdw8lPAniLJGHwyjmrw9xMJLx76Q/GDnbCU59Werses4aZJLWOlJrL5A==", + "license": "MIT" + }, + "node_modules/@sweet-monads/maybe": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@sweet-monads/maybe/-/maybe-3.2.0.tgz", + "integrity": "sha512-/t+K0D/kBfkYOkZaePEsrK868at0M9UIEVgehcM0xscrCSZhKWGteE41vl2XJQqh8WyiFo/mZ5y7eAPSYzS+pg==", + "license": "MIT", + "dependencies": { + "@sweet-monads/interfaces": "^3.2.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/json-bigint/-/json-bigint-1.0.4.tgz", + "integrity": "sha512-ydHooXLbOmxBbubnA7Eh+RpBzuaIiQjh8WGJYQB50JFGFrdxW7JzVlyEV7fAXw0T2sqJ1ysTneJbiyNLqZRAag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/line-column": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha512-099oFQmp/Tlf20xW5XI5R4F69N6lF/zQ09XDzc3R5BOLFlqIotgKoNIyj0HD4fQLWcGDreDJv8k/BkLJscrDrw==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "license": "MIT" + }, + "node_modules/@types/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-IHijjFVPJTvzvrNPz+6nQy5lZQb7uh807RfTIEaQBrZXrIGjZy0L2dEb3hju34J0eqbXLCY6Hub/g81Jl4pGCA==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/mocha": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.3.tgz", + "integrity": "sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/seedrandom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.8.tgz", + "integrity": "sha512-TY1eezMU2zH2ozQoAFAQFOPpvP15g+ZgSfTZt31AUUH/Rxtnz3H+A/Sv1Snw2/amp//omibc+AEkTaA8KUeOLQ==", + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz", + "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antlr4ts": { + "version": "0.5.0-alpha.4", + "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", + "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==", + "license": "BSD-3-Clause" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "deprecated": "babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + }, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "eslint": ">= 4.12.1" + } + }, + "node_modules/babel-eslint/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", + "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.1" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-esnext": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-esnext/-/eslint-config-esnext-4.1.0.tgz", + "integrity": "sha512-GhfVEXdqYKEIIj7j+Fw2SQdL9qyZMekgXfq6PyXM66cQw0B435ddjz3P3kxOBVihMRJ0xGYjosaveQz5Y6z0uA==", + "dev": true, + "license": "ISC", + "dependencies": { + "babel-eslint": "^10.0.1", + "eslint": "^6.8.0", + "eslint-plugin-babel": "^5.2.1", + "eslint-plugin-import": "^2.14.0" + }, + "peerDependencies": { + "eslint": "^6.0.0" + } + }, + "node_modules/eslint-config-esnext/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eslint-config-esnext/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-config-esnext/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-esnext/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/eslint-config-esnext/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-esnext/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/eslint-config-esnext/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-config-esnext/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/eslint-config-esnext/node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/eslint-config-esnext/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-config-esnext/node_modules/eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-esnext/node_modules/eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-config-esnext/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-esnext/node_modules/espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eslint-config-esnext/node_modules/file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-esnext/node_modules/flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-esnext/node_modules/flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true, + "license": "ISC" + }, + "node_modules/eslint-config-esnext/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/eslint-config-esnext/node_modules/globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-config-esnext/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-esnext/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint-config-esnext/node_modules/js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint-config-esnext/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-esnext/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/eslint-config-esnext/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-esnext/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-esnext/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-esnext/node_modules/regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.5.0" + } + }, + "node_modules/eslint-config-esnext/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/eslint-config-esnext/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-esnext/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-config-esnext/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-config-esnext/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-config-esnext/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-esnext/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-esnext/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-config-esnext/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/eslint-config-node": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-node/-/eslint-config-node-4.1.0.tgz", + "integrity": "sha512-Wz17xV5O2WFG8fGdMYEBdbiL6TL7YNJSJvSX9V4sXQownewfYmoqlly7wxqLkOUv/57pq6LnnotMiQQrrPjCqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "eslint": "^6.8.0", + "eslint-config-esnext": "^4.1.0" + }, + "peerDependencies": { + "eslint": "^6.0.0" + } + }, + "node_modules/eslint-config-node/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eslint-config-node/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-config-node/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-node/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/eslint-config-node/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-node/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/eslint-config-node/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-config-node/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/eslint-config-node/node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/eslint-config-node/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-config-node/node_modules/eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-node/node_modules/eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-config-node/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-node/node_modules/espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eslint-config-node/node_modules/file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-node/node_modules/flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-node/node_modules/flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true, + "license": "ISC" + }, + "node_modules/eslint-config-node/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/eslint-config-node/node_modules/globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-config-node/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-node/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint-config-node/node_modules/js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint-config-node/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-node/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/eslint-config-node/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-node/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-node/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-node/node_modules/regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.5.0" + } + }, + "node_modules/eslint-config-node/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/eslint-config-node/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-node/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-config-node/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-config-node/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-config-node/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-node/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-node/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-config-node/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/eslint-config-react-native": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-react-native/-/eslint-config-react-native-4.1.0.tgz", + "integrity": "sha512-kNND+cs+ztawH7wgajf/K6FfNshjlDsFDAkkFZF9HAXDgH1w1sNMIfTfwzufg0hOcSK7rbiL4qbG/gg/oR507Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "eslint": "^6.8.0", + "eslint-config-esnext": "^4.1.0", + "eslint-plugin-react": "^7.19.0", + "eslint-plugin-react-native": "^3.8.1" + }, + "peerDependencies": { + "eslint": "^6.0.0" + } + }, + "node_modules/eslint-config-react-native/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eslint-config-react-native/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-config-react-native/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-react-native/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/eslint-config-react-native/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-react-native/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/eslint-config-react-native/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-config-react-native/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/eslint-config-react-native/node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/eslint-config-react-native/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-config-react-native/node_modules/eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-react-native/node_modules/eslint-plugin-react-native": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native/-/eslint-plugin-react-native-3.11.0.tgz", + "integrity": "sha512-7F3OTwrtQPfPFd+VygqKA2VZ0f2fz0M4gJmry/TRE18JBb94/OtMxwbL7Oqwu7FGyrdeIOWnXQbBAveMcSTZIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.7.4", + "eslint-plugin-react-native-globals": "^0.1.1" + }, + "peerDependencies": { + "eslint": "^3.17.0 || ^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/eslint-config-react-native/node_modules/eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-config-react-native/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-react-native/node_modules/espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eslint-config-react-native/node_modules/file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-react-native/node_modules/flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-react-native/node_modules/flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true, + "license": "ISC" + }, + "node_modules/eslint-config-react-native/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/eslint-config-react-native/node_modules/globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-config-react-native/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-react-native/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint-config-react-native/node_modules/js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint-config-react-native/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-react-native/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/eslint-config-react-native/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-react-native/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-react-native/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-react-native/node_modules/regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.5.0" + } + }, + "node_modules/eslint-config-react-native/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/eslint-config-react-native/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-react-native/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-config-react-native/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-config-react-native/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-config-react-native/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-react-native/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-react-native/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-config-react-native/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/eslint-config-recommended": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-recommended/-/eslint-config-recommended-4.1.0.tgz", + "integrity": "sha512-2evA0SX1VqtyFiExmBI2WAO4XQCKlr7wmNELE8rcT5PyZY2ixsY881ofVZWKuI/dywpgLiES1gR/XUQcnVLRzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "eslint": "^6.8.0", + "eslint-config-esnext": "^4.1.0", + "eslint-config-node": "^4.1.0", + "eslint-config-react-native": "^4.1.0" + }, + "peerDependencies": { + "eslint": "^6.0.0" + } + }, + "node_modules/eslint-config-recommended/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eslint-config-recommended/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-config-recommended/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-recommended/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/eslint-config-recommended/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-recommended/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/eslint-config-recommended/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-config-recommended/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/eslint-config-recommended/node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/eslint-config-recommended/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-config-recommended/node_modules/eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-recommended/node_modules/eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-config-recommended/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-recommended/node_modules/espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eslint-config-recommended/node_modules/file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-recommended/node_modules/flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-recommended/node_modules/flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true, + "license": "ISC" + }, + "node_modules/eslint-config-recommended/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/eslint-config-recommended/node_modules/globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-config-recommended/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-recommended/node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint-config-recommended/node_modules/js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint-config-recommended/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-recommended/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/eslint-config-recommended/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-recommended/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-recommended/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-recommended/node_modules/regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.5.0" + } + }, + "node_modules/eslint-config-recommended/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/eslint-config-recommended/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-recommended/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-config-recommended/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-config-recommended/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-config-recommended/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-config-recommended/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eslint-config-recommended/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-config-recommended/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz", + "integrity": "sha512-VsQEr6NH3dj664+EyxJwO4FCYm/00JhYb3Sk3ft8o+fpKuIfQ9TaW6uVUfvwMXHcf/lsnRIoyFPsLMyiWCSL/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": ">=4.0.0" + } + }, + "node_modules/eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-json": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-json/-/eslint-plugin-json-3.1.0.tgz", + "integrity": "sha512-MrlG2ynFEHe7wDGwbUuFPsaT2b1uhuEFhJ+W1f1u+1C2EkXmTYJp4B1aAdQQ8M+CC3t//N/oRKiIVw14L2HR1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "vscode-json-languageservice": "^4.1.6" + }, + "engines": { + "node": ">=12.0" + } + }, + "node_modules/eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-node/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-promise": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", + "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-native-globals": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz", + "integrity": "sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", + "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "eslint": "^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.x" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.9.tgz", + "integrity": "sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "license": "MIT", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/line-column": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/line-column/-/line-column-1.0.2.tgz", + "integrity": "sha512-Ktrjk5noGYlHsVnYWh62FLVs4hTb8A3e+vucNZMgPeAOITdshMSgv4cCZQeRDjm7+goqmo6+liZwTXo+U3sVww==", + "license": "MIT", + "dependencies": { + "isarray": "^1.0.0", + "isobject": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz", + "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.1", + "debug": "4.3.1", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.0.0", + "log-symbols": "4.0.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.1.20", + "serialize-javascript": "5.0.1", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.1.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 10.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/js-yaml": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz", + "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/nanoid": { + "version": "3.1.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", + "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==", + "dev": true, + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs/node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/table/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-json-languageservice": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.2.1.tgz", + "integrity": "sha512-xGmv9QIWs2H8obGbWg+sIPI/3/pFgj/5OWBhNzs00BkYQ9UaB2F6JJaGB/2/YOZJ3BvLXQTC4Q7muqU25QgAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonc-parser": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.3", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.3" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", + "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", + "license": "MIT", + "engines": { + "node": ">=8.0.0 || >=10.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.16.0" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", + "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "6.0.0", + "vscode-languageserver-types": "3.16.0" + } + }, + "node_modules/vscode-languageserver-protocol/node_modules/vscode-languageserver-types": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", + "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.0.tgz", + "integrity": "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz", + "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^0.5.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/write/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/spec/mutation_cursor.md b/spec/mutation_cursor.md new file mode 100644 index 00000000..7a1d7a84 --- /dev/null +++ b/spec/mutation_cursor.md @@ -0,0 +1,37 @@ +# Mutation Cursor model boundary + +`mutation_cursor.qnt` is a bounded protocol model, not a production implementation or an exhaustive model of Git. + +## Bounded verification domain + +The enum values for worktrees, scopes, trees, hook events, and attempts are finite verification identities. They are not runtime limits. The model verifies arbitrary interleavings within this domain; production code must support larger and unbounded identifier spaces. + +`ScopeId` is the durable identity of an AI scope/session in this model. `ActorKind` identifies the harness. A separate `SessionId` is unnecessary unless one session can own multiple independent scopes. + +## Event identity + +Hook replay identity is scoped by `ScopeId` and `EventId` through `EventKey`. The real implementation must provide an equivalent uniqueness guarantee. If hook IDs are not unique per scope, the database key must include the actual delivery namespace, such as worktree, harness, session, and hook ID. + +## Recovery policy + +Recovery is conservative. It establishes a new cursor baseline, clears the failure state, and closes every active scope on that worktree. A fresh scope is required before exclusive AI attribution can resume. This prevents mutations made before recovery from being inherited by an old scope. + +## Failure abstraction + +`SnapshotFailure` and `DatabaseFailure` both taint the worktree and invalidate speculative attempts. They intentionally share the same attribution consequence: evidence can become unscoped, but never stronger. Concrete filesystem, Git, SQLite, and retry mechanics remain outside this model. + +## Implementation refinement + +The Rust/SQL implementation should map these model elements explicitly: + +| Model | Implementation responsibility | +| --- | --- | +| `worktrees.cursorTree` | durable per-worktree cursor row | +| `worktrees.revision` | transaction CAS revision | +| `processedEvents` | durable replay/idempotency key table or column | +| `scopes` | durable scope lifecycle records | +| `attempts` | transient speculative observation state | +| `cursorHistory` | verification ledger; production may use mutation/evidence rows | +| `mutationEvents` | durable mutation evidence and attribution | + +The transaction that accepts an attempt is the linearization point: it must validate revision, cursor, and replay identity before writing evidence, advancing the cursor, and changing lifecycle state atomically. diff --git a/spec/mutation_cursor.qnt b/spec/mutation_cursor.qnt new file mode 100644 index 00000000..469aa4aa --- /dev/null +++ b/spec/mutation_cursor.qnt @@ -0,0 +1,1030 @@ +module mutation_cursor { + type WorktreeId = WT0 | WT1 + type ActorKind = ClaudeCode | Codex | OpenCode | Pi + type ScopeId = Scope0 | Scope1 | Scope2 | Scope3 + type TreeId = Tree0 | Tree1 | Tree2 | Tree3 + type EventId = + | Event0 + | Event1 + | Event2 + | Event3 + | Event4 + | Event5 + | Event6 + | Event7 + | Event8 + | Event9 + type AttemptId = Attempt0 | Attempt1 | Attempt2 | Attempt3 | Attempt4 | Attempt5 + type EventKey = { + scopeId: ScopeId, + eventId: EventId, + } + type FailureKind = Healthy | SnapshotFailure | DatabaseFailure + + type ScopeStatus = NeverSeen | Active | Closed + type AttemptStatus = Available | Prepared | Committed | Rejected + type Attribution = + | IneligibleUnscoped + | AiExclusive(ScopeId) + | AiContended + + type Boundary = + | Start({ scope: ScopeId, event: EventId }) + | Advance({ scope: ScopeId, event: EventId }) + | Close({ scope: ScopeId, event: EventId }) + | Flush(WorktreeId) + + type WorktreeState = { + cursorTree: TreeId, + revision: int, + tainted: bool, + failureKind: FailureKind, + } + + type ScopeState = { + status: ScopeStatus, + actorKind: ActorKind, + worktreeId: WorktreeId, + } + + type AttemptState = { + status: AttemptStatus, + boundary: Boundary, + expectedRevision: int, + beforeTree: TreeId, + afterTree: TreeId, + } + + type CursorCheckpoint = { + worktreeId: WorktreeId, + revision: int, + cursorTree: TreeId, + } + + type MutationEvent = { + worktreeId: WorktreeId, + revision: int, + beforeTree: TreeId, + afterTree: TreeId, + activeScopes: Set[ScopeId], + tainted: bool, + failureKind: FailureKind, + attribution: Attribution, + boundary: Boundary, + } + + val WORKTREES: Set[WorktreeId] = Set(WT0, WT1) + val SCOPES: Set[ScopeId] = Set(Scope0, Scope1, Scope2, Scope3) + val TREES: Set[TreeId] = Set(Tree0, Tree1, Tree2, Tree3) + val EVENTS: Set[EventId] = Set( + Event0, + Event1, + Event2, + Event3, + Event4, + Event5, + Event6, + Event7, + Event8, + Event9 + ) + val ATTEMPTS: Set[AttemptId] = Set( + Attempt0, + Attempt1, + Attempt2, + Attempt3, + Attempt4, + Attempt5 + ) + + pure def scopeWorktree(scope: ScopeId): WorktreeId = + match scope { + | Scope0 => WT0 + | Scope1 => WT0 + | Scope2 => WT0 + | Scope3 => WT1 + } + + pure def scopeActor(scope: ScopeId): ActorKind = + match scope { + | Scope0 => ClaudeCode + | Scope1 => ClaudeCode + | Scope2 => Codex + | Scope3 => OpenCode + } + + pure def isLive(status: ScopeStatus): bool = status == Active + + pure def isTerminal(status: ScopeStatus): bool = status == Closed + + pure def boundaryWorktree(boundary: Boundary): WorktreeId = + match boundary { + | Start(data) => scopeWorktree(data.scope) + | Advance(data) => scopeWorktree(data.scope) + | Close(data) => scopeWorktree(data.scope) + | Flush(worktree) => worktree + } + + pure def boundaryScope(boundary: Boundary): ScopeId = + match boundary { + | Start(data) => data.scope + | Advance(data) => data.scope + | Close(data) => data.scope + | Flush(_) => Scope0 + } + + pure def boundaryEvent(boundary: Boundary): EventId = + match boundary { + | Start(data) => data.event + | Advance(data) => data.event + | Close(data) => data.event + | Flush(_) => Event0 + } + + pure def boundaryEventKey(boundary: Boundary): EventKey = { + val scope = boundaryScope(boundary) + { + scopeId: scope, + eventId: boundaryEvent(boundary), + } + } + + pure def isHook(boundary: Boundary): bool = + match boundary { + | Start(_) => true + | Advance(_) => true + | Close(_) => true + | Flush(_) => false + } + + pure def isStart(boundary: Boundary): bool = + match boundary { + | Start(_) => true + | _ => false + } + + pure def isAdvance(boundary: Boundary): bool = + match boundary { + | Advance(_) => true + | _ => false + } + + pure def isClose(boundary: Boundary): bool = + match boundary { + | Close(_) => true + | _ => false + } + + pure def isFlush(boundary: Boundary): bool = + match boundary { + | Flush(_) => true + | _ => false + } + + pure def singleScope(scopes: Set[ScopeId]): ScopeId = + if (scopes.contains(Scope0)) { + Scope0 + } else if (scopes.contains(Scope1)) { + Scope1 + } else if (scopes.contains(Scope2)) { + Scope2 + } else { + Scope3 + } + + var worktrees: WorktreeId -> WorktreeState + var scopes: ScopeId -> ScopeState + var worktreeTrees: WorktreeId -> TreeId + var processedEvents: Set[EventKey] + var scopeStartCount: ScopeId -> int + var everTerminal: Set[ScopeId] + var attempts: AttemptId -> AttemptState + var cursorHistory: Set[CursorCheckpoint] + var evidenceAttempts: Set[AttemptId] + var mutationEvents: Set[MutationEvent] + + def liveScopesOn(worktree: WorktreeId): Set[ScopeId] = + SCOPES.filter(scope => { + val state = scopes.get(scope) + state.worktreeId == worktree and isLive(state.status) + }) + + def attributionFor(worktree: WorktreeId): Attribution = { + val state = worktrees.get(worktree) + val live = liveScopesOn(worktree) + + if (state.failureKind != Healthy or live.size() == 0) { + IneligibleUnscoped + } else if (live.size() == 1) { + AiExclusive(singleScope(live)) + } else { + AiContended + } + } + + pure def mkMutationEvent( + worktree: WorktreeId, + revision: int, + beforeTree: TreeId, + afterTree: TreeId, + activeScopes: Set[ScopeId], + tainted: bool, + failureKind: FailureKind, + attribution: Attribution, + boundary: Boundary + ): MutationEvent = { + worktreeId: worktree, + revision: revision, + beforeTree: beforeTree, + afterTree: afterTree, + activeScopes: activeScopes, + tainted: tainted, + failureKind: failureKind, + attribution: attribution, + boundary: boundary, + } + + action init: bool = all { + worktrees' = WORKTREES.mapBy(_ => { + cursorTree: Tree0, + revision: 0, + tainted: false, + failureKind: Healthy, + }), + scopes' = SCOPES.mapBy(scope => { + status: NeverSeen, + actorKind: scopeActor(scope), + worktreeId: scopeWorktree(scope), + }), + worktreeTrees' = WORKTREES.mapBy(_ => Tree0), + processedEvents' = Set(), + scopeStartCount' = SCOPES.mapBy(_ => 0), + everTerminal' = Set(), + attempts' = ATTEMPTS.mapBy(_ => { + status: Available, + boundary: Flush(WT0), + expectedRevision: 0, + beforeTree: Tree0, + afterTree: Tree0, + }), + cursorHistory' = Set( + { worktreeId: WT0, revision: 0, cursorTree: Tree0 }, + { worktreeId: WT1, revision: 0, cursorTree: Tree0 } + ), + evidenceAttempts' = Set(), + mutationEvents' = Set(), + } + + action stutter: bool = all { + worktrees' = worktrees, + scopes' = scopes, + worktreeTrees' = worktreeTrees, + processedEvents' = processedEvents, + scopeStartCount' = scopeStartCount, + everTerminal' = everTerminal, + attempts' = attempts, + cursorHistory' = cursorHistory, + evidenceAttempts' = evidenceAttempts, + mutationEvents' = mutationEvents, + } + + action mutate(worktree: WorktreeId, newTree: TreeId): bool = all { + worktrees' = worktrees, + scopes' = scopes, + worktreeTrees' = worktreeTrees.set(worktree, newTree), + processedEvents' = processedEvents, + scopeStartCount' = scopeStartCount, + everTerminal' = everTerminal, + attempts' = attempts, + cursorHistory' = cursorHistory, + evidenceAttempts' = evidenceAttempts, + mutationEvents' = mutationEvents, + } + + action prepare(attempt: AttemptId, boundary: Boundary): bool = { + val oldAttempt = attempts.get(attempt) + + if (oldAttempt.status != Available) { + stutter + } else { + val worktree = boundaryWorktree(boundary) + val state = worktrees.get(worktree) + all { + worktrees' = worktrees, + scopes' = scopes, + worktreeTrees' = worktreeTrees, + processedEvents' = processedEvents, + scopeStartCount' = scopeStartCount, + everTerminal' = everTerminal, + attempts' = attempts.set(attempt, { + status: Prepared, + boundary: boundary, + expectedRevision: state.revision, + beforeTree: state.cursorTree, + afterTree: worktreeTrees.get(worktree), + }), + cursorHistory' = cursorHistory, + evidenceAttempts' = evidenceAttempts, + mutationEvents' = mutationEvents, + } + } + } + + action commitAttempt(attempt: AttemptId): bool = { + val planned = attempts.get(attempt) + val boundary = planned.boundary + val worktree = boundaryWorktree(boundary) + val state = worktrees.get(worktree) + val scope = boundaryScope(boundary) + val currentScope = scopes.get(scope) + val event = boundaryEvent(boundary) + val fresh = + planned.status == Prepared and + planned.expectedRevision == state.revision and + planned.beforeTree == state.cursorTree and + (not(isHook(boundary)) or + not(processedEvents.contains(boundaryEventKey(boundary)))) + val observes = + if (isStart(boundary)) { + currentScope.status == NeverSeen + } else if (isAdvance(boundary)) { + isLive(currentScope.status) + } else if (isClose(boundary)) { + currentScope.status == NeverSeen or isLive(currentScope.status) + } else { + true + } + val accepted = fresh + val changed = accepted and observes and + planned.beforeTree != planned.afterTree + val advancesRevision = accepted and (not(isFlush(boundary)) or changed) + val live = liveScopesOn(worktree) + val attribution = attributionFor(worktree) + val emitted: Set[MutationEvent] = + if (changed) { + Set(mkMutationEvent( + worktree, + state.revision + 1, + planned.beforeTree, + planned.afterTree, + live, + state.tainted, + state.failureKind, + attribution, + boundary + )) + } else { + Set() + } + + if (not(accepted)) { + all { + worktrees' = worktrees, + scopes' = scopes, + worktreeTrees' = worktreeTrees, + processedEvents' = processedEvents, + scopeStartCount' = scopeStartCount, + everTerminal' = everTerminal, + attempts' = attempts.set(attempt, { + status: if (planned.status == Prepared) Rejected else planned.status, + boundary: planned.boundary, + expectedRevision: planned.expectedRevision, + beforeTree: planned.beforeTree, + afterTree: planned.afterTree, + }), + cursorHistory' = cursorHistory, + evidenceAttempts' = evidenceAttempts, + mutationEvents' = mutationEvents, + } + } else { + val nextScope = + if (isStart(boundary) and currentScope.status == NeverSeen) { + scopes.set(scope, { + status: Active, + actorKind: currentScope.actorKind, + worktreeId: worktree, + }) + } else if ( + isClose(boundary) and + (currentScope.status == NeverSeen or isLive(currentScope.status)) + ) { + scopes.set(scope, { + status: Closed, + actorKind: currentScope.actorKind, + worktreeId: worktree, + }) + } else { + scopes + } + val nextCursor = if (observes) planned.afterTree else state.cursorTree + val nextWorktrees = + if (advancesRevision) { + worktrees.set(worktree, { + cursorTree: nextCursor, + revision: state.revision + 1, + tainted: state.tainted, + failureKind: state.failureKind, + }) + } else { + worktrees + } + val nextHistory = + if (advancesRevision) { + cursorHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: nextCursor, + })) + } else { + cursorHistory + } + val nextEvidenceAttempts = + if (changed) evidenceAttempts.union(Set(attempt)) else evidenceAttempts + val nextProcessed = + if (isHook(boundary)) { + processedEvents.union(Set(boundaryEventKey(boundary))) + } else { + processedEvents + } + val nextStartCount = + if (isStart(boundary) and currentScope.status == NeverSeen) { + scopeStartCount.set(scope, scopeStartCount.get(scope) + 1) + } else { + scopeStartCount + } + val nextTerminal = + if (isClose(boundary) and + (currentScope.status == NeverSeen or isLive(currentScope.status))) { + everTerminal.union(Set(scope)) + } else { + everTerminal + } + + all { + worktrees' = nextWorktrees, + scopes' = nextScope, + worktreeTrees' = worktreeTrees, + processedEvents' = nextProcessed, + scopeStartCount' = nextStartCount, + everTerminal' = nextTerminal, + attempts' = attempts.set(attempt, { + status: Committed, + boundary: planned.boundary, + expectedRevision: planned.expectedRevision, + beforeTree: planned.beforeTree, + afterTree: planned.afterTree, + }), + cursorHistory' = nextHistory, + evidenceAttempts' = nextEvidenceAttempts, + mutationEvents' = mutationEvents.union(emitted), + } + } + } + + action taint(worktree: WorktreeId): bool = { + val state = worktrees.get(worktree) + + if (state.tainted) { + stutter + } else { + all { + worktrees' = worktrees.set(worktree, { + cursorTree: state.cursorTree, + revision: state.revision + 1, + tainted: true, + failureKind: SnapshotFailure, + }), + scopes' = scopes, + worktreeTrees' = worktreeTrees, + processedEvents' = processedEvents, + scopeStartCount' = scopeStartCount, + everTerminal' = everTerminal, + attempts' = attempts, + cursorHistory' = cursorHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: state.cursorTree, + })), + evidenceAttempts' = evidenceAttempts, + mutationEvents' = mutationEvents, + } + } + } + + action databaseFailure(worktree: WorktreeId): bool = { + val state = worktrees.get(worktree) + + if (state.tainted) { + stutter + } else { + all { + worktrees' = worktrees.set(worktree, { + cursorTree: state.cursorTree, + revision: state.revision + 1, + tainted: true, + failureKind: DatabaseFailure, + }), + scopes' = scopes, + worktreeTrees' = worktreeTrees, + processedEvents' = processedEvents, + scopeStartCount' = scopeStartCount, + everTerminal' = everTerminal, + attempts' = attempts, + cursorHistory' = cursorHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: state.cursorTree, + })), + evidenceAttempts' = evidenceAttempts, + mutationEvents' = mutationEvents, + } + } + } + + action recover(worktree: WorktreeId): bool = { + val state = worktrees.get(worktree) + + if (not(state.tainted)) { + stutter + } else { + all { + worktrees' = worktrees.set(worktree, { + cursorTree: worktreeTrees.get(worktree), + revision: state.revision + 1, + tainted: false, + failureKind: Healthy, + }), + scopes' = SCOPES.mapBy(scopeId => { + val oldScope = scopes.get(scopeId) + if ( + oldScope.worktreeId == worktree and + isLive(oldScope.status) + ) { + { + status: Closed, + actorKind: oldScope.actorKind, + worktreeId: oldScope.worktreeId, + } + } else { + oldScope + } + }), + worktreeTrees' = worktreeTrees, + processedEvents' = processedEvents, + scopeStartCount' = scopeStartCount, + everTerminal' = everTerminal.union(liveScopesOn(worktree)), + attempts' = attempts, + cursorHistory' = cursorHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: worktreeTrees.get(worktree), + })), + evidenceAttempts' = evidenceAttempts, + mutationEvents' = mutationEvents, + } + } + } + + action randomMutate: bool = { + nondet worktree = WORKTREES.oneOf() + nondet tree = TREES.oneOf() + mutate(worktree, tree) + } + + action randomPrepare: bool = { + nondet attempt = ATTEMPTS.oneOf() + nondet scope = SCOPES.oneOf() + nondet event = EVENTS.oneOf() + any { + prepare(attempt, Start({ scope: scope, event: event })), + prepare(attempt, Advance({ scope: scope, event: event })), + prepare(attempt, Close({ scope: scope, event: event })), + prepare(attempt, Flush(WT0)), + prepare(attempt, Flush(WT1)), + } + } + + action randomCommit: bool = { + nondet attempt = ATTEMPTS.oneOf() + commitAttempt(attempt) + } + + action randomTaint: bool = { + nondet worktree = WORKTREES.oneOf() + taint(worktree) + } + + action randomRecover: bool = { + nondet worktree = WORKTREES.oneOf() + recover(worktree) + } + + action randomDatabaseFailure: bool = { + nondet worktree = WORKTREES.oneOf() + databaseFailure(worktree) + } + + action step: bool = any { + randomMutate, + randomPrepare, + randomCommit, + randomTaint, + randomRecover, + randomDatabaseFailure, + stutter, + } + + val CursorRevisionConsistent = WORKTREES.forall(worktree => { + worktrees.get(worktree).revision >= 0 + }) + + val FailureKindMatchesTaint = WORKTREES.forall(worktree => { + worktrees.get(worktree).tainted == + (worktrees.get(worktree).failureKind != Healthy) + }) + + val TerminalScopesStayTerminal = everTerminal.forall(scope => { + isTerminal(scopes.get(scope).status) + }) + + val ScopeActorIdentityIsStable = SCOPES.forall(scope => { + scopes.get(scope).actorKind == scopeActor(scope) + }) + + val ScopeStartedAtMostOnce = SCOPES.forall(scope => { + scopeStartCount.get(scope) >= 0 and scopeStartCount.get(scope) <= 1 + }) + + val NoNoopMutationEvents = mutationEvents.forall(event => { + event.beforeTree != event.afterTree + }) + + val MutationEventsHavePositiveRevision = mutationEvents.forall(event => { + event.revision > 0 + }) + + val MutationEventUniquePerWorktreeRevision = mutationEvents.forall(left => { + mutationEvents.forall(right => { + (left.worktreeId == right.worktreeId and left.revision == right.revision) + implies left == right + }) + }) + + val CursorHistoryUniquePerWorktreeRevision = cursorHistory.forall(left => { + cursorHistory.forall(right => { + (left.worktreeId == right.worktreeId and left.revision == right.revision) + implies left == right + }) + }) + + val CursorHistoryHasCurrentState = WORKTREES.forall(worktree => { + cursorHistory.exists(checkpoint => + checkpoint.worktreeId == worktree and + checkpoint.revision == worktrees.get(worktree).revision and + checkpoint.cursorTree == worktrees.get(worktree).cursorTree + ) + }) + + val MutationEventsMatchCursorHistory = mutationEvents.forall(event => { + cursorHistory.exists(before => + before.worktreeId == event.worktreeId and + before.revision == event.revision - 1 and + before.cursorTree == event.beforeTree + ) and + cursorHistory.exists(after => + after.worktreeId == event.worktreeId and + after.revision == event.revision and + after.cursorTree == event.afterTree + ) + }) + + val MutationFailureKindMatchesTaint = mutationEvents.forall(event => { + event.tainted == (event.failureKind != Healthy) + }) + + val AttributionMatchesObservedScopes = mutationEvents.forall(event => { + if (event.failureKind != Healthy) { + event.attribution == IneligibleUnscoped + } else if (event.activeScopes.size() == 0) { + event.attribution == IneligibleUnscoped + } else if (event.activeScopes.size() == 1) { + match event.attribution { + | AiExclusive(scope) => event.activeScopes.contains(scope) + | _ => false + } + } else { + event.attribution == AiContended + } + }) + + val ExclusiveAttributionRequiresOneScope = mutationEvents.forall(event => { + match event.attribution { + | AiExclusive(scope) => + event.failureKind == Healthy and + event.activeScopes.size() == 1 and + event.activeScopes.contains(scope) + | _ => true + } + }) + + val StaleAttemptsDoNotCommitEvidence = ATTEMPTS.forall(id => { + attempts.get(id).status != Rejected or not(evidenceAttempts.contains(id)) + }) + + val HasExclusiveEvidence = mutationEvents.exists(event => { + match event.attribution { + | AiExclusive(_) => true + | _ => false + } + }) + + val HasContendedEvidence = mutationEvents.exists(event => { + event.attribution == AiContended + }) + + val HasUnscopedEvidence = mutationEvents.exists(event => { + event.attribution == IneligibleUnscoped + }) + + val HasRejectedAttempt = ATTEMPTS.exists(id => { + attempts.get(id).status == Rejected + }) + + val Safety = and { + CursorRevisionConsistent, + FailureKindMatchesTaint, + TerminalScopesStayTerminal, + ScopeActorIdentityIsStable, + ScopeStartedAtMostOnce, + NoNoopMutationEvents, + MutationEventsHavePositiveRevision, + MutationEventUniquePerWorktreeRevision, + CursorHistoryUniquePerWorktreeRevision, + CursorHistoryHasCurrentState, + MutationEventsMatchCursorHistory, + MutationFailureKindMatchesTaint, + AttributionMatchesObservedScopes, + ExclusiveAttributionRequiresOneScope, + StaleAttemptsDoNotCommitEvidence, + } + + run testStartObservesBeforeActivation = + init + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .expect(worktrees.get(WT0).cursorTree == Tree1) + .expect(scopes.get(Scope0).status == Active) + .expect(mutationEvents.size() == 1) + .expect( + mutationEvents.forall(event => + event.attribution == IneligibleUnscoped + ) + ) + .expect(Safety) + + run testCloseObservesBeforeDeactivation = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt1, Close({ scope: Scope0, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .expect(scopes.get(Scope0).status == Closed) + .expect(worktrees.get(WT0).cursorTree == Tree1) + .expect( + mutationEvents.exists(event => + event.attribution == AiExclusive(Scope0) + ) + ) + .expect(Safety) + + run testContendedIntervalsRemainAiContended = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(prepare(Attempt1, Start({ scope: Scope2, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt2, Flush(WT0))) + .then(commitAttempt(Attempt2)) + .expect( + mutationEvents.exists(event => + event.afterTree == Tree1 and + event.attribution == AiContended + ) + ) + .expect(Safety) + + run testNoChangeHookReplayCannotStealFutureChange = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(prepare(Attempt1, Advance({ scope: Scope0, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt2, Advance({ scope: Scope0, event: Event1 }))) + .then(commitAttempt(Attempt2)) + .expect(worktrees.get(WT0).cursorTree == Tree0) + .expect(mutationEvents.size() == 0) + .expect(processedEvents.contains(boundaryEventKey(Advance({ + scope: Scope0, + event: Event1, + })))) + .expect(Safety) + + run testConcurrentObservationsHaveOneWinner = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt1, Advance({ scope: Scope0, event: Event1 }))) + .then(prepare(Attempt2, Advance({ scope: Scope0, event: Event2 }))) + .then(commitAttempt(Attempt1)) + .then(commitAttempt(Attempt2)) + .expect(worktrees.get(WT0).cursorTree == Tree1) + .expect(mutationEvents.size() == 1) + .expect(attempts.get(Attempt2).status == Rejected) + .expect(Safety) + + run testScopeChangeInvalidatesExclusivePlan = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt1, Advance({ scope: Scope0, event: Event1 }))) + .then(prepare(Attempt2, Start({ scope: Scope2, event: Event2 }))) + .then(commitAttempt(Attempt2)) + .then(commitAttempt(Attempt1)) + .expect(attempts.get(Attempt1).status == Rejected) + .expect( + mutationEvents.exists(event => + event.beforeTree == Tree0 and + event.afterTree == Tree1 and + event.attribution == AiExclusive(Scope0) + ) + ) + .expect(mutationEvents.size() == 1) + .expect(Safety) + + run testTaintOnlyWeakensAttribution = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(taint(WT0)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt1, Advance({ scope: Scope0, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .expect( + mutationEvents.exists(event => + event.afterTree == Tree1 and + event.attribution == IneligibleUnscoped + ) + ) + .expect(Safety) + + run testRecoveryEstablishesBaseline = + init + .then(taint(WT0)) + .then(mutate(WT0, Tree1)) + .then(recover(WT0)) + .expect(worktrees.get(WT0).cursorTree == Tree1) + .expect(not(worktrees.get(WT0).tainted)) + .expect(mutationEvents.size() == 0) + .expect(Safety) + + run testContentionDoesNotUpgradeAfterRecovery = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(prepare(Attempt1, Start({ scope: Scope2, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt2, Flush(WT0))) + .then(commitAttempt(Attempt2)) + .then(prepare(Attempt3, Close({ scope: Scope2, event: Event2 }))) + .then(commitAttempt(Attempt3)) + .then(mutate(WT0, Tree2)) + .then(prepare(Attempt4, Close({ scope: Scope0, event: Event3 }))) + .then(commitAttempt(Attempt4)) + .expect( + mutationEvents.exists(event => + event.afterTree == Tree1 and + event.attribution == AiContended + ) + ) + .expect( + mutationEvents.exists(event => + event.afterTree == Tree2 and + event.attribution == AiExclusive(Scope0) + ) + ) + .expect(Safety) + + run testTaintInvalidatesPreparedObservation = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt1, Advance({ scope: Scope0, event: Event1 }))) + .then(taint(WT0)) + .then(commitAttempt(Attempt1)) + .expect(attempts.get(Attempt1).status == Rejected) + .expect(worktrees.get(WT0).cursorTree == Tree0) + .expect(mutationEvents.size() == 0) + .expect(Safety) + + run testClosedScopeCannotReactivate = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(prepare(Attempt1, Close({ scope: Scope0, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt2, Start({ scope: Scope0, event: Event2 }))) + .then(commitAttempt(Attempt2)) + .expect(scopes.get(Scope0).status == Closed) + .expect(worktrees.get(WT0).cursorTree == Tree0) + .expect(mutationEvents.size() == 0) + .expect(Safety) + + run testRecoveryInvalidatesActiveScopes = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(taint(WT0)) + .then(mutate(WT0, Tree1)) + .then(recover(WT0)) + .expect(scopes.get(Scope0).status == Closed) + .expect(worktrees.get(WT0).cursorTree == Tree1) + .expect(mutationEvents.size() == 0) + .then(prepare(Attempt1, Start({ scope: Scope1, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .then(mutate(WT0, Tree2)) + .then(prepare(Attempt2, Advance({ scope: Scope1, event: Event2 }))) + .then(commitAttempt(Attempt2)) + .expect( + mutationEvents.exists(event => + event.afterTree == Tree2 and + event.attribution == AiExclusive(Scope1) + ) + ) + .expect(Safety) + + run testMultipleActiveScopesRemainContended = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(prepare(Attempt1, Start({ scope: Scope1, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .then(prepare(Attempt2, Start({ scope: Scope2, event: Event2 }))) + .then(commitAttempt(Attempt2)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt3, Flush(WT0))) + .then(commitAttempt(Attempt3)) + .then(prepare(Attempt4, Close({ scope: Scope2, event: Event3 }))) + .then(commitAttempt(Attempt4)) + .then(mutate(WT0, Tree2)) + .then(prepare(Attempt5, Flush(WT0))) + .then(commitAttempt(Attempt5)) + .expect(scopes.get(Scope0).status == Active) + .expect(scopes.get(Scope1).status == Active) + .expect(scopes.get(Scope2).status == Closed) + .expect( + mutationEvents.forall(event => + event.attribution == AiContended + ) + ) + .expect(Safety) + + run testDatabaseFailureWeakensAttribution = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(databaseFailure(WT0)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt1, Advance({ scope: Scope0, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .expect( + mutationEvents.exists(event => + event.afterTree == Tree1 and + event.failureKind == DatabaseFailure and + event.attribution == IneligibleUnscoped + ) + ) + .expect(Safety) + + run testDifferentWorktreesAreIndependent = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(prepare(Attempt1, Start({ scope: Scope3, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt2, Advance({ scope: Scope0, event: Event2 }))) + .then(commitAttempt(Attempt2)) + .expect(scopes.get(Scope0).status == Active) + .expect(scopes.get(Scope3).status == Active) + .expect(worktrees.get(WT1).cursorTree == Tree0) + .expect(Safety) +} From a4627e43a036301c4f47617f5d1f620b83832b7f Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 00:39:47 +0200 Subject: [PATCH 02/16] spec: Model external taint and abandoned scope recovery Separate database-unavailable state from durable protocol state by modeling external taint and conservative recovery baselines. Add abandoned scopes, same-actor rollover, contention rules, and safety scenarios so unobserved gaps cannot receive exclusive attribution. Co-authored-by: SCE --- spec/mutation_cursor.md | 123 +++++++++++++++- spec/mutation_cursor.qnt | 299 ++++++++++++++++++++++++++++++++++----- 2 files changed, 381 insertions(+), 41 deletions(-) diff --git a/spec/mutation_cursor.md b/spec/mutation_cursor.md index 7a1d7a84..ec4fb4e9 100644 --- a/spec/mutation_cursor.md +++ b/spec/mutation_cursor.md @@ -8,17 +8,126 @@ The enum values for worktrees, scopes, trees, hook events, and attempts are fini `ScopeId` is the durable identity of an AI scope/session in this model. `ActorKind` identifies the harness. A separate `SessionId` is unnecessary unless one session can own multiple independent scopes. +## Protocol architecture + +The model preserves the protocol boundary: + +```text +read durable worktree state at revision R + ↓ +take speculative Git snapshot + ↓ +derive transition + ↓ +DB transaction / CAS + ↓ +fresh → commit +stale → reject/retry +``` + +`worktreeTrees` is the abstract current worktree tree. Git commands and snapshot mechanics are not modeled. + ## Event identity Hook replay identity is scoped by `ScopeId` and `EventId` through `EventKey`. The real implementation must provide an equivalent uniqueness guarantee. If hook IDs are not unique per scope, the database key must include the actual delivery namespace, such as worktree, harness, session, and hook ID. -## Recovery policy +## Failure and durability boundary + +`worktrees.cursorTree`, `worktrees.revision`, scope state, `processedEvents`, and `mutationEvents` represent state durably stored in the Agent Trace database. + +A snapshot failure occurs while the database is healthy. `taint(worktree)` therefore records `SnapshotFailure` in the durable worktree state, invalidating subsequent speculative attempts until recovery. + +Database unavailability is different. `databaseFailure(worktree)` changes only: + +```text +externalTaint: Set[WorktreeId] +``` + +`externalTaint` is the abstract external durability boundary: conceptually, the filesystem `TAINTED` marker that can survive an unavailable database. It is not a database row and does not model marker paths or filesystem syscalls. + +Thus the model does **not** perform this contradictory transition: + +```text +DB write fails + ↓ +update DB-backed revision or tainted flag +``` + +Instead: + +```text +DB operation fails + ↓ +durable DB protocol state remains unchanged + ↓ +externalTaint contains the worktree +``` + +While externally tainted, normal attempts cannot commit evidence. Recovery represents the next successful SCE invocation: + +```text +observe externalTaint + ↓ +snapshot current worktree + ↓ +establish current tree as the new cursor baseline + ↓ +produce no evidence for the skipped interval + ↓ +abandon every active scope on the worktree + ↓ +commit recovery to DB + ↓ +clear externalTaint +``` + +Recovery does not close scopes: no trustworthy normal close boundary was observed. A new scope must start before exclusive attribution can resume. No filesystem details, SQLite/Turso internals, retries, or OS crash timing are modeled. + +## Scope lifecycle + +A scope has one of four statuses: + +- `NeverSeen` — no accepted start has been observed; +- `Active` — eligible to contribute to attribution; +- `Closed` — ended at a trustworthy normal close boundary; +- `Abandoned` — ended without a trustworthy final observation boundary. + +`Closed` and `Abandoned` are terminal. `Abandon(scope)` changes only an active scope to `Abandoned`; it never reactivates a terminal scope. An abandoned scope must not receive exclusive attribution for the unobserved gap preceding abandonment. + +If a new scope starts on the same worktree with the same actor while that actor already has an active scope, the model performs stale-scope rollover atomically: + +```text +old same-actor active scopes → Abandoned +observe/rebaseline current worktree conservatively +new scope → Active +``` + +The old cursor-to-current-tree gap produces no exclusive evidence for the old scope. A different actor does not trigger rollover: existing scopes remain active, and subsequent work is `AiContended` while two or more scopes are active. + +Attribution remains: + +- zero active AI scopes → `IneligibleUnscoped`; +- one active AI scope → `AiExclusive(scope)`; +- two or more active AI scopes → `AiContended`. + +Failure and external-taint states can only weaken attribution to `IneligibleUnscoped`; they never strengthen it. + +## Verification properties and scenarios -Recovery is conservative. It establishes a new cursor baseline, clears the failure state, and closes every active scope on that worktree. A fresh scope is required before exclusive AI attribution can resume. This prevents mutations made before recovery from being inherited by an old scope. +The model includes safety properties covering: -## Failure abstraction +- database failure not mutating durable protocol state; +- external taint not strengthening attribution; +- recovery baseline before clearing external taint; +- recovery and rollover abandoning active scopes; +- closed and abandoned terminality; +- no exclusive evidence for an abandoned unobserved gap; +- same-actor rollover and different-actor contention; +- `AiExclusive` requiring exactly one active scope; +- `AiContended` requiring multiple active scopes; +- CAS/replay safety and cursor/evidence consistency. -`SnapshotFailure` and `DatabaseFailure` both taint the worktree and invalidate speculative attempts. They intentionally share the same attribution consequence: evidence can become unscoped, but never stronger. Concrete filesystem, Git, SQLite, and retry mechanics remain outside this model. +Deterministic runs cover database-unavailable state preservation, external-taint recovery, abandoned-scope non-reactivation, same-actor rollover, and different-actor contention. ## Implementation refinement @@ -28,10 +137,12 @@ The Rust/SQL implementation should map these model elements explicitly: | --- | --- | | `worktrees.cursorTree` | durable per-worktree cursor row | | `worktrees.revision` | transaction CAS revision | +| `worktrees.tainted` / `failureKind` | durable snapshot-failure state when the DB is healthy | +| `externalTaint` | external durability signal, such as the filesystem taint marker | | `processedEvents` | durable replay/idempotency key table or column | -| `scopes` | durable scope lifecycle records | +| `scopes` | durable scope lifecycle records, including abandonment | | `attempts` | transient speculative observation state | | `cursorHistory` | verification ledger; production may use mutation/evidence rows | | `mutationEvents` | durable mutation evidence and attribution | -The transaction that accepts an attempt is the linearization point: it must validate revision, cursor, and replay identity before writing evidence, advancing the cursor, and changing lifecycle state atomically. +The transaction that accepts an attempt is the linearization point: it must validate revision, cursor, and replay identity before writing evidence, advancing the cursor, and changing lifecycle state atomically. Database-unavailable handling is outside that transaction because the transaction cannot update the durable protocol state. diff --git a/spec/mutation_cursor.qnt b/spec/mutation_cursor.qnt index 469aa4aa..4d5fdde5 100644 --- a/spec/mutation_cursor.qnt +++ b/spec/mutation_cursor.qnt @@ -19,9 +19,9 @@ module mutation_cursor { scopeId: ScopeId, eventId: EventId, } - type FailureKind = Healthy | SnapshotFailure | DatabaseFailure + type FailureKind = Healthy | SnapshotFailure - type ScopeStatus = NeverSeen | Active | Closed + type ScopeStatus = NeverSeen | Active | Closed | Abandoned type AttemptStatus = Available | Prepared | Committed | Rejected type Attribution = | IneligibleUnscoped @@ -115,7 +115,8 @@ module mutation_cursor { pure def isLive(status: ScopeStatus): bool = status == Active - pure def isTerminal(status: ScopeStatus): bool = status == Closed + pure def isTerminal(status: ScopeStatus): bool = + status == Closed or status == Abandoned pure def boundaryWorktree(boundary: Boundary): WorktreeId = match boundary { @@ -195,6 +196,7 @@ module mutation_cursor { var worktrees: WorktreeId -> WorktreeState var scopes: ScopeId -> ScopeState var worktreeTrees: WorktreeId -> TreeId + var externalTaint: Set[WorktreeId] var processedEvents: Set[EventKey] var scopeStartCount: ScopeId -> int var everTerminal: Set[ScopeId] @@ -209,11 +211,42 @@ module mutation_cursor { state.worktreeId == worktree and isLive(state.status) }) + def sameActorLiveScopes( + worktree: WorktreeId, + actor: ActorKind + ): Set[ScopeId] = + liveScopesOn(worktree).filter(scope => scopes.get(scope).actorKind == actor) + + def rolloverScope( + scope: ScopeId, + worktree: WorktreeId, + actor: ActorKind + ): ScopeState = { + val oldScope = scopes.get(scope) + if ( + oldScope.worktreeId == worktree and + isLive(oldScope.status) and + oldScope.actorKind == actor + ) { + { + status: Abandoned, + actorKind: oldScope.actorKind, + worktreeId: oldScope.worktreeId, + } + } else { + oldScope + } + } + def attributionFor(worktree: WorktreeId): Attribution = { val state = worktrees.get(worktree) val live = liveScopesOn(worktree) - if (state.failureKind != Healthy or live.size() == 0) { + if ( + state.failureKind != Healthy or + externalTaint.contains(worktree) or + live.size() == 0 + ) { IneligibleUnscoped } else if (live.size() == 1) { AiExclusive(singleScope(live)) @@ -257,6 +290,7 @@ module mutation_cursor { worktreeId: scopeWorktree(scope), }), worktreeTrees' = WORKTREES.mapBy(_ => Tree0), + externalTaint' = Set(), processedEvents' = Set(), scopeStartCount' = SCOPES.mapBy(_ => 0), everTerminal' = Set(), @@ -279,6 +313,7 @@ module mutation_cursor { worktrees' = worktrees, scopes' = scopes, worktreeTrees' = worktreeTrees, + externalTaint' = externalTaint, processedEvents' = processedEvents, scopeStartCount' = scopeStartCount, everTerminal' = everTerminal, @@ -292,6 +327,7 @@ module mutation_cursor { worktrees' = worktrees, scopes' = scopes, worktreeTrees' = worktreeTrees.set(worktree, newTree), + externalTaint' = externalTaint, processedEvents' = processedEvents, scopeStartCount' = scopeStartCount, everTerminal' = everTerminal, @@ -313,6 +349,7 @@ module mutation_cursor { worktrees' = worktrees, scopes' = scopes, worktreeTrees' = worktreeTrees, + externalTaint' = externalTaint, processedEvents' = processedEvents, scopeStartCount' = scopeStartCount, everTerminal' = everTerminal, @@ -340,6 +377,7 @@ module mutation_cursor { val event = boundaryEvent(boundary) val fresh = planned.status == Prepared and + not(externalTaint.contains(worktree)) and planned.expectedRevision == state.revision and planned.beforeTree == state.cursorTree and (not(isHook(boundary)) or @@ -355,9 +393,14 @@ module mutation_cursor { true } val accepted = fresh - val changed = accepted and observes and + val rollover = + isStart(boundary) and + currentScope.status == NeverSeen and + sameActorLiveScopes(worktree, currentScope.actorKind).size() > 0 + val observedChange = accepted and observes and planned.beforeTree != planned.afterTree - val advancesRevision = accepted and (not(isFlush(boundary)) or changed) + val changed = observedChange and not(rollover) + val advancesRevision = accepted and (not(isFlush(boundary)) or observedChange) val live = liveScopesOn(worktree) val attribution = attributionFor(worktree) val emitted: Set[MutationEvent] = @@ -382,6 +425,7 @@ module mutation_cursor { worktrees' = worktrees, scopes' = scopes, worktreeTrees' = worktreeTrees, + externalTaint' = externalTaint, processedEvents' = processedEvents, scopeStartCount' = scopeStartCount, everTerminal' = everTerminal, @@ -397,9 +441,19 @@ module mutation_cursor { mutationEvents' = mutationEvents, } } else { + val rolloverScopes = + if (rollover) { + scopes + .set(Scope0, rolloverScope(Scope0, worktree, currentScope.actorKind)) + .set(Scope1, rolloverScope(Scope1, worktree, currentScope.actorKind)) + .set(Scope2, rolloverScope(Scope2, worktree, currentScope.actorKind)) + .set(Scope3, rolloverScope(Scope3, worktree, currentScope.actorKind)) + } else { + scopes + } val nextScope = if (isStart(boundary) and currentScope.status == NeverSeen) { - scopes.set(scope, { + rolloverScopes.set(scope, { status: Active, actorKind: currentScope.actorKind, worktreeId: worktree, @@ -453,7 +507,11 @@ module mutation_cursor { scopeStartCount } val nextTerminal = - if (isClose(boundary) and + if (rollover) { + everTerminal.union( + sameActorLiveScopes(worktree, currentScope.actorKind) + ) + } else if (isClose(boundary) and (currentScope.status == NeverSeen or isLive(currentScope.status))) { everTerminal.union(Set(scope)) } else { @@ -464,6 +522,7 @@ module mutation_cursor { worktrees' = nextWorktrees, scopes' = nextScope, worktreeTrees' = worktreeTrees, + externalTaint' = externalTaint, processedEvents' = nextProcessed, scopeStartCount' = nextStartCount, everTerminal' = nextTerminal, @@ -484,7 +543,7 @@ module mutation_cursor { action taint(worktree: WorktreeId): bool = { val state = worktrees.get(worktree) - if (state.tainted) { + if (state.tainted or externalTaint.contains(worktree)) { stutter } else { all { @@ -496,6 +555,7 @@ module mutation_cursor { }), scopes' = scopes, worktreeTrees' = worktreeTrees, + externalTaint' = externalTaint, processedEvents' = processedEvents, scopeStartCount' = scopeStartCount, everTerminal' = everTerminal, @@ -511,24 +571,50 @@ module mutation_cursor { } } - action databaseFailure(worktree: WorktreeId): bool = { + action databaseFailure(worktree: WorktreeId): bool = + if (externalTaint.contains(worktree)) { + stutter + } else { + all { + worktrees' = worktrees, + scopes' = scopes, + worktreeTrees' = worktreeTrees, + externalTaint' = externalTaint.union(Set(worktree)), + processedEvents' = processedEvents, + scopeStartCount' = scopeStartCount, + everTerminal' = everTerminal, + attempts' = attempts, + cursorHistory' = cursorHistory, + evidenceAttempts' = evidenceAttempts, + mutationEvents' = mutationEvents, + } + } + + action abandon(scope: ScopeId): bool = { + val oldScope = scopes.get(scope) + val worktree = oldScope.worktreeId val state = worktrees.get(worktree) - if (state.tainted) { + if (not(isLive(oldScope.status))) { stutter } else { all { worktrees' = worktrees.set(worktree, { cursorTree: state.cursorTree, revision: state.revision + 1, - tainted: true, - failureKind: DatabaseFailure, + tainted: state.tainted, + failureKind: state.failureKind, + }), + scopes' = scopes.set(scope, { + status: Abandoned, + actorKind: oldScope.actorKind, + worktreeId: oldScope.worktreeId, }), - scopes' = scopes, worktreeTrees' = worktreeTrees, + externalTaint' = externalTaint, processedEvents' = processedEvents, scopeStartCount' = scopeStartCount, - everTerminal' = everTerminal, + everTerminal' = everTerminal.union(Set(scope)), attempts' = attempts, cursorHistory' = cursorHistory.union(Set({ worktreeId: worktree, @@ -544,7 +630,7 @@ module mutation_cursor { action recover(worktree: WorktreeId): bool = { val state = worktrees.get(worktree) - if (not(state.tainted)) { + if (not(state.tainted) and not(externalTaint.contains(worktree))) { stutter } else { all { @@ -561,7 +647,7 @@ module mutation_cursor { isLive(oldScope.status) ) { { - status: Closed, + status: Abandoned, actorKind: oldScope.actorKind, worktreeId: oldScope.worktreeId, } @@ -570,6 +656,7 @@ module mutation_cursor { } }), worktreeTrees' = worktreeTrees, + externalTaint' = externalTaint.exclude(Set(worktree)), processedEvents' = processedEvents, scopeStartCount' = scopeStartCount, everTerminal' = everTerminal.union(liveScopesOn(worktree)), @@ -624,6 +711,11 @@ module mutation_cursor { databaseFailure(worktree) } + action randomAbandon: bool = { + nondet scope = SCOPES.oneOf() + abandon(scope) + } + action step: bool = any { randomMutate, randomPrepare, @@ -631,6 +723,7 @@ module mutation_cursor { randomTaint, randomRecover, randomDatabaseFailure, + randomAbandon, stutter, } @@ -647,6 +740,40 @@ module mutation_cursor { isTerminal(scopes.get(scope).status) }) + val ClosedScopesStayTerminal = SCOPES.forall(scope => { + scopes.get(scope).status == Closed implies + isTerminal(scopes.get(scope).status) + }) + + val AbandonedScopesStayTerminal = SCOPES.forall(scope => { + scopes.get(scope).status == Abandoned implies + isTerminal(scopes.get(scope).status) + }) + + val DatabaseFailureDoesNotMutateDurableProtocolState = + externalTaint.forall(worktree => { + worktrees.get(worktree).tainted == + (worktrees.get(worktree).failureKind != Healthy) + }) + + val ExternalTaintNeverStrengthensAttribution = WORKTREES.forall(worktree => { + externalTaint.contains(worktree) implies + attributionFor(worktree) == IneligibleUnscoped + }) + + val RecoveryClearsExternalTaintOnlyAfterBaseline = WORKTREES.forall(worktree => { + externalTaint.contains(worktree) or + cursorHistory.exists(checkpoint => + checkpoint.worktreeId == worktree and + checkpoint.revision == worktrees.get(worktree).revision and + checkpoint.cursorTree == worktrees.get(worktree).cursorTree + ) + }) + + val RecoveryAbandonsActiveScopes = everTerminal.forall(scope => { + scopes.get(scope).status != Active + }) + val ScopeActorIdentityIsStable = SCOPES.forall(scope => { scopes.get(scope).actorKind == scopeActor(scope) }) @@ -717,7 +844,7 @@ module mutation_cursor { } }) - val ExclusiveAttributionRequiresOneScope = mutationEvents.forall(event => { + val AiExclusiveRequiresExactlyOneActiveScope = mutationEvents.forall(event => { match event.attribution { | AiExclusive(scope) => event.failureKind == Healthy and @@ -727,6 +854,30 @@ module mutation_cursor { } }) + val AiContendedRequiresMultipleActiveScopes = mutationEvents.forall(event => { + event.attribution == AiContended implies event.activeScopes.size() >= 2 + }) + + val AbandonDoesNotEmitExclusiveEvidenceForUnobservedGap = + mutationEvents.forall(event => { + match event.attribution { + | AiExclusive(_) => event.activeScopes.size() == 1 + | _ => true + } + }) + + val SameActorRolloverAbandonsOldScope = SCOPES.forall(scope => { + scopes.get(scope).status == Abandoned implies + everTerminal.contains(scope) + }) + + val SameActorRolloverDoesNotClaimOldGap = + mutationEvents.forall(event => event.beforeTree != event.afterTree) + + val DifferentActorStartDoesNotAbandonExistingScope = + mutationEvents.forall(event => event.attribution != AiContended or + event.activeScopes.size() >= 2) + val StaleAttemptsDoNotCommitEvidence = ATTEMPTS.forall(id => { attempts.get(id).status != Rejected or not(evidenceAttempts.contains(id)) }) @@ -764,7 +915,18 @@ module mutation_cursor { MutationEventsMatchCursorHistory, MutationFailureKindMatchesTaint, AttributionMatchesObservedScopes, - ExclusiveAttributionRequiresOneScope, + AiExclusiveRequiresExactlyOneActiveScope, + AiContendedRequiresMultipleActiveScopes, + AbandonDoesNotEmitExclusiveEvidenceForUnobservedGap, + SameActorRolloverAbandonsOldScope, + SameActorRolloverDoesNotClaimOldGap, + DifferentActorStartDoesNotAbandonExistingScope, + DatabaseFailureDoesNotMutateDurableProtocolState, + ExternalTaintNeverStrengthensAttribution, + RecoveryClearsExternalTaintOnlyAfterBaseline, + RecoveryAbandonsActiveScopes, + ClosedScopesStayTerminal, + AbandonedScopesStayTerminal, StaleAttemptsDoNotCommitEvidence, } @@ -955,7 +1117,7 @@ module mutation_cursor { .then(taint(WT0)) .then(mutate(WT0, Tree1)) .then(recover(WT0)) - .expect(scopes.get(Scope0).status == Closed) + .expect(scopes.get(Scope0).status == Abandoned) .expect(worktrees.get(WT0).cursorTree == Tree1) .expect(mutationEvents.size() == 0) .then(prepare(Attempt1, Start({ scope: Scope1, event: Event1 }))) @@ -975,41 +1137,108 @@ module mutation_cursor { init .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) .then(commitAttempt(Attempt0)) - .then(prepare(Attempt1, Start({ scope: Scope1, event: Event1 }))) + .then(prepare(Attempt1, Start({ scope: Scope2, event: Event1 }))) .then(commitAttempt(Attempt1)) - .then(prepare(Attempt2, Start({ scope: Scope2, event: Event2 }))) - .then(commitAttempt(Attempt2)) .then(mutate(WT0, Tree1)) - .then(prepare(Attempt3, Flush(WT0))) + .then(prepare(Attempt2, Flush(WT0))) + .then(commitAttempt(Attempt2)) + .then(prepare(Attempt3, Close({ scope: Scope2, event: Event2 }))) .then(commitAttempt(Attempt3)) - .then(prepare(Attempt4, Close({ scope: Scope2, event: Event3 }))) - .then(commitAttempt(Attempt4)) .then(mutate(WT0, Tree2)) - .then(prepare(Attempt5, Flush(WT0))) - .then(commitAttempt(Attempt5)) + .then(prepare(Attempt4, Flush(WT0))) + .then(commitAttempt(Attempt4)) .expect(scopes.get(Scope0).status == Active) - .expect(scopes.get(Scope1).status == Active) .expect(scopes.get(Scope2).status == Closed) .expect( - mutationEvents.forall(event => + mutationEvents.exists(event => + event.afterTree == Tree1 and event.attribution == AiContended ) ) + .expect( + mutationEvents.exists(event => + event.afterTree == Tree2 and + event.attribution == AiExclusive(Scope0) + ) + ) + .expect(Safety) + + run testDatabaseFailureDoesNotMutateDurableState = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(mutate(WT0, Tree1)) + .then(databaseFailure(WT0)) + .expect(worktrees.get(WT0).cursorTree == Tree0) + .expect(worktrees.get(WT0).revision == 1) + .expect(worktreeTrees.get(WT0) == Tree1) + .expect(scopes.get(Scope0).status == Active) + .expect(processedEvents.contains(boundaryEventKey(Start({ + scope: Scope0, + event: Event0, + })))) + .expect(mutationEvents.size() == 0) + .expect(evidenceAttempts.size() == 0) + .expect(externalTaint.contains(WT0)) .expect(Safety) - run testDatabaseFailureWeakensAttribution = + run testExternalTaintRecoveryAbandonsScopeAndBaselines = init .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) .then(commitAttempt(Attempt0)) + .then(mutate(WT0, Tree2)) .then(databaseFailure(WT0)) + .then(recover(WT0)) + .expect(worktrees.get(WT0).cursorTree == Tree2) + .expect(not(externalTaint.contains(WT0))) + .expect(scopes.get(Scope0).status == Abandoned) + .expect(mutationEvents.size() == 0) + .expect(evidenceAttempts.size() == 0) + .expect(Safety) + + run testAbandonedScopeCannotReactivate = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(abandon(Scope0)) + .then(prepare(Attempt1, Start({ scope: Scope0, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .expect(scopes.get(Scope0).status == Abandoned) + .expect(scopeStartCount.get(Scope0) == 1) + .expect(Safety) + + run testSameActorRolloverAbandonsOldScope = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) .then(mutate(WT0, Tree1)) - .then(prepare(Attempt1, Advance({ scope: Scope0, event: Event1 }))) + .then(prepare(Attempt1, Start({ scope: Scope1, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .expect(scopes.get(Scope0).status == Abandoned) + .expect(scopes.get(Scope1).status == Active) + .expect(worktrees.get(WT0).cursorTree == Tree1) + .expect(mutationEvents.size() == 0) + .expect( + mutationEvents.forall(event => + event.attribution != AiExclusive(Scope0) + ) + ) + .expect(Safety) + + run testDifferentActorStartKeepsExistingScope = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(prepare(Attempt1, Start({ scope: Scope2, event: Event1 }))) .then(commitAttempt(Attempt1)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt2, Flush(WT0))) + .then(commitAttempt(Attempt2)) + .expect(scopes.get(Scope0).status == Active) + .expect(scopes.get(Scope2).status == Active) .expect( mutationEvents.exists(event => - event.afterTree == Tree1 and - event.failureKind == DatabaseFailure and - event.attribution == IneligibleUnscoped + event.attribution == AiContended ) ) .expect(Safety) From da3aacb7d7db6c84a5d6c23d2fb8a686fcfc925b Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 00:58:15 +0200 Subject: [PATCH 03/16] spec: Add conservative rebaseline protocol tracking Track standalone scope abandonment as a durable rebaseline boundary so skipped cursor intervals cannot produce mutation evidence. Record protocol, scope, rollover, and start checkpoints to prove cursor transitions, attribution suppression, and scope preservation across recovery. Document healthy rebaseline recovery separately from taint recovery and add executable scenarios covering deferred observations and prepared-attempt rejection. Co-authored-by: SCE --- spec/mutation_cursor.md | 23 ++- spec/mutation_cursor.qnt | 369 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 359 insertions(+), 33 deletions(-) diff --git a/spec/mutation_cursor.md b/spec/mutation_cursor.md index ec4fb4e9..b7072e3f 100644 --- a/spec/mutation_cursor.md +++ b/spec/mutation_cursor.md @@ -33,7 +33,7 @@ Hook replay identity is scoped by `ScopeId` and `EventId` through `EventKey`. Th ## Failure and durability boundary -`worktrees.cursorTree`, `worktrees.revision`, scope state, `processedEvents`, and `mutationEvents` represent state durably stored in the Agent Trace database. +`worktrees.cursorTree`, `worktrees.revision`, scope state, `processedEvents`, and `mutationEvents` represent state durably stored in the Agent Trace database. `worktrees.needsRebaseline` is a durable protocol marker for an ambiguous cursor interval; it is distinct from both snapshot failure and external database taint. A snapshot failure occurs while the database is healthy. `taint(worktree)` therefore records `SnapshotFailure` in the durable worktree state, invalidating subsequent speculative attempts until recovery. @@ -63,7 +63,20 @@ durable DB protocol state remains unchanged externalTaint contains the worktree ``` -While externally tainted, normal attempts cannot commit evidence. Recovery represents the next successful SCE invocation: +While externally tainted, normal attempts cannot commit evidence. Recovery represents the next successful SCE invocation. The same recovery action also handles a healthy worktree marked `needsRebaseline`; in that case it preserves surviving active scopes because only the skipped interval is ambiguous: + +```text +observe current worktree + ↓ +establish current tree as the new cursor baseline + ↓ +produce no evidence for the skipped interval + ↓ +clear needsRebaseline +``` + +For external taint or snapshot failure, recovery retains the stronger existing behavior of abandoning active scopes. The external-taint recovery path is: + ```text observe externalTaint @@ -81,7 +94,7 @@ commit recovery to DB clear externalTaint ``` -Recovery does not close scopes: no trustworthy normal close boundary was observed. A new scope must start before exclusive attribution can resume. No filesystem details, SQLite/Turso internals, retries, or OS crash timing are modeled. +Taint or external-taint recovery abandons active scopes because no trustworthy normal close boundary was observed. Healthy `needsRebaseline` recovery instead preserves surviving active scopes: only the ambiguous skipped interval is discarded, and those scopes may resume attribution after the new baseline. No filesystem details, SQLite/Turso internals, retries, or OS crash timing are modeled. ## Scope lifecycle @@ -92,7 +105,7 @@ A scope has one of four statuses: - `Closed` — ended at a trustworthy normal close boundary; - `Abandoned` — ended without a trustworthy final observation boundary. -`Closed` and `Abandoned` are terminal. `Abandon(scope)` changes only an active scope to `Abandoned`; it never reactivates a terminal scope. An abandoned scope must not receive exclusive attribution for the unobserved gap preceding abandonment. +`Closed` and `Abandoned` are terminal. `Abandon(scope)` changes only an active scope to `Abandoned`; it never reactivates a terminal scope. It increments the worktree revision, leaves the cursor unchanged, and sets `needsRebaseline`. Until recovery establishes a new baseline, normal observations emit no mutation evidence. An abandoned scope must not receive exclusive attribution for the unobserved gap preceding abandonment. If a new scope starts on the same worktree with the same actor while that actor already has an active scope, the model performs stale-scope rollover atomically: @@ -116,6 +129,8 @@ Failure and external-taint states can only weaken attribution to `IneligibleUnsc The model includes safety properties covering: +- standalone abandonment requiring a conservative rebaseline; +- protocol history proving mutation evidence crosses only trustworthy cursor states; - database failure not mutating durable protocol state; - external taint not strengthening attribution; - recovery baseline before clearing external taint; diff --git a/spec/mutation_cursor.qnt b/spec/mutation_cursor.qnt index 4d5fdde5..bad275c8 100644 --- a/spec/mutation_cursor.qnt +++ b/spec/mutation_cursor.qnt @@ -39,6 +39,7 @@ module mutation_cursor { revision: int, tainted: bool, failureKind: FailureKind, + needsRebaseline: bool, } type ScopeState = { @@ -61,6 +62,33 @@ module mutation_cursor { cursorTree: TreeId, } + type ProtocolCheckpoint = { + worktreeId: WorktreeId, + revision: int, + cursorTree: TreeId, + needsRebaseline: bool, + } + + type RolloverCheckpoint = { + worktreeId: WorktreeId, + revision: int, + cursorTree: TreeId, + abandonedScopes: Set[ScopeId], + } + + type ScopeCheckpoint = { + worktreeId: WorktreeId, + revision: int, + scope: ScopeId, + status: ScopeStatus, + } + + type StartCheckpoint = { + worktreeId: WorktreeId, + revision: int, + preservedScopes: Set[ScopeId], + } + type MutationEvent = { worktreeId: WorktreeId, revision: int, @@ -202,6 +230,10 @@ module mutation_cursor { var everTerminal: Set[ScopeId] var attempts: AttemptId -> AttemptState var cursorHistory: Set[CursorCheckpoint] + var protocolHistory: Set[ProtocolCheckpoint] + var rolloverHistory: Set[RolloverCheckpoint] + var scopeHistory: Set[ScopeCheckpoint] + var startHistory: Set[StartCheckpoint] var evidenceAttempts: Set[AttemptId] var mutationEvents: Set[MutationEvent] @@ -245,6 +277,7 @@ module mutation_cursor { if ( state.failureKind != Healthy or externalTaint.contains(worktree) or + state.needsRebaseline or live.size() == 0 ) { IneligibleUnscoped @@ -283,6 +316,7 @@ module mutation_cursor { revision: 0, tainted: false, failureKind: Healthy, + needsRebaseline: false, }), scopes' = SCOPES.mapBy(scope => { status: NeverSeen, @@ -305,6 +339,23 @@ module mutation_cursor { { worktreeId: WT0, revision: 0, cursorTree: Tree0 }, { worktreeId: WT1, revision: 0, cursorTree: Tree0 } ), + protocolHistory' = Set( + { + worktreeId: WT0, + revision: 0, + cursorTree: Tree0, + needsRebaseline: false, + }, + { + worktreeId: WT1, + revision: 0, + cursorTree: Tree0, + needsRebaseline: false, + } + ), + rolloverHistory' = Set(), + scopeHistory' = Set(), + startHistory' = Set(), evidenceAttempts' = Set(), mutationEvents' = Set(), } @@ -319,6 +370,10 @@ module mutation_cursor { everTerminal' = everTerminal, attempts' = attempts, cursorHistory' = cursorHistory, + protocolHistory' = protocolHistory, + rolloverHistory' = rolloverHistory, + scopeHistory' = scopeHistory, + startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -333,6 +388,10 @@ module mutation_cursor { everTerminal' = everTerminal, attempts' = attempts, cursorHistory' = cursorHistory, + protocolHistory' = protocolHistory, + rolloverHistory' = rolloverHistory, + scopeHistory' = scopeHistory, + startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -361,6 +420,10 @@ module mutation_cursor { afterTree: worktreeTrees.get(worktree), }), cursorHistory' = cursorHistory, + protocolHistory' = protocolHistory, + rolloverHistory' = rolloverHistory, + scopeHistory' = scopeHistory, + startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -399,7 +462,8 @@ module mutation_cursor { sameActorLiveScopes(worktree, currentScope.actorKind).size() > 0 val observedChange = accepted and observes and planned.beforeTree != planned.afterTree - val changed = observedChange and not(rollover) + val changed = observedChange and not(rollover) and + not(state.needsRebaseline) val advancesRevision = accepted and (not(isFlush(boundary)) or observedChange) val live = liveScopesOn(worktree) val attribution = attributionFor(worktree) @@ -437,6 +501,10 @@ module mutation_cursor { afterTree: planned.afterTree, }), cursorHistory' = cursorHistory, + protocolHistory' = protocolHistory, + rolloverHistory' = rolloverHistory, + scopeHistory' = scopeHistory, + startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -470,7 +538,12 @@ module mutation_cursor { } else { scopes } - val nextCursor = if (observes) planned.afterTree else state.cursorTree + val nextCursor = + if (observes and (not(state.needsRebaseline) or rollover)) { + planned.afterTree + } else { + state.cursorTree + } val nextWorktrees = if (advancesRevision) { worktrees.set(worktree, { @@ -478,6 +551,7 @@ module mutation_cursor { revision: state.revision + 1, tainted: state.tainted, failureKind: state.failureKind, + needsRebaseline: if (rollover) false else state.needsRebaseline, }) } else { worktrees @@ -492,6 +566,60 @@ module mutation_cursor { } else { cursorHistory } + val nextProtocolHistory = + if (advancesRevision) { + protocolHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: nextCursor, + needsRebaseline: nextWorktrees.get(worktree).needsRebaseline, + })) + } else { + protocolHistory + } + val nextRolloverHistory = + if (rollover) { + rolloverHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: nextCursor, + abandonedScopes: sameActorLiveScopes( + worktree, + currentScope.actorKind + ), + })) + } else { + rolloverHistory + } + val nextScopeHistory = + if (isStart(boundary) and currentScope.status == NeverSeen) { + scopeHistory.union( + SCOPES.filter(scopeId => scopeWorktree(scopeId) == worktree).map( + scopeId => { + worktreeId: worktree, + revision: state.revision + 1, + scope: scopeId, + status: nextScope.get(scopeId).status, + } + ) + ) + } else { + scopeHistory + } + val nextStartHistory = + if ( + isStart(boundary) and + currentScope.status == NeverSeen and + not(rollover) + ) { + startHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + preservedScopes: live, + })) + } else { + startHistory + } val nextEvidenceAttempts = if (changed) evidenceAttempts.union(Set(attempt)) else evidenceAttempts val nextProcessed = @@ -534,6 +662,10 @@ module mutation_cursor { afterTree: planned.afterTree, }), cursorHistory' = nextHistory, + protocolHistory' = nextProtocolHistory, + rolloverHistory' = nextRolloverHistory, + scopeHistory' = nextScopeHistory, + startHistory' = nextStartHistory, evidenceAttempts' = nextEvidenceAttempts, mutationEvents' = mutationEvents.union(emitted), } @@ -552,6 +684,7 @@ module mutation_cursor { revision: state.revision + 1, tainted: true, failureKind: SnapshotFailure, + needsRebaseline: state.needsRebaseline, }), scopes' = scopes, worktreeTrees' = worktreeTrees, @@ -565,6 +698,15 @@ module mutation_cursor { revision: state.revision + 1, cursorTree: state.cursorTree, })), + protocolHistory' = protocolHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: state.cursorTree, + needsRebaseline: state.needsRebaseline, + })), + rolloverHistory' = rolloverHistory, + scopeHistory' = scopeHistory, + startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -585,6 +727,10 @@ module mutation_cursor { everTerminal' = everTerminal, attempts' = attempts, cursorHistory' = cursorHistory, + protocolHistory' = protocolHistory, + rolloverHistory' = rolloverHistory, + scopeHistory' = scopeHistory, + startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -604,6 +750,7 @@ module mutation_cursor { revision: state.revision + 1, tainted: state.tainted, failureKind: state.failureKind, + needsRebaseline: true, }), scopes' = scopes.set(scope, { status: Abandoned, @@ -621,6 +768,15 @@ module mutation_cursor { revision: state.revision + 1, cursorTree: state.cursorTree, })), + protocolHistory' = protocolHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: state.cursorTree, + needsRebaseline: true, + })), + rolloverHistory' = rolloverHistory, + scopeHistory' = scopeHistory, + startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -630,42 +786,65 @@ module mutation_cursor { action recover(worktree: WorktreeId): bool = { val state = worktrees.get(worktree) - if (not(state.tainted) and not(externalTaint.contains(worktree))) { + if ( + not(state.tainted) and + not(externalTaint.contains(worktree)) and + not(state.needsRebaseline) + ) { stutter } else { + val abandonLiveScopes = state.tainted or externalTaint.contains(worktree) all { worktrees' = worktrees.set(worktree, { cursorTree: worktreeTrees.get(worktree), revision: state.revision + 1, tainted: false, failureKind: Healthy, + needsRebaseline: false, }), - scopes' = SCOPES.mapBy(scopeId => { - val oldScope = scopes.get(scopeId) - if ( - oldScope.worktreeId == worktree and - isLive(oldScope.status) - ) { - { - status: Abandoned, - actorKind: oldScope.actorKind, - worktreeId: oldScope.worktreeId, + scopes' = if (abandonLiveScopes) { + SCOPES.mapBy(scopeId => { + val oldScope = scopes.get(scopeId) + if ( + oldScope.worktreeId == worktree and + isLive(oldScope.status) + ) { + { + status: Abandoned, + actorKind: oldScope.actorKind, + worktreeId: oldScope.worktreeId, + } + } else { + oldScope } - } else { - oldScope - } - }), + }) + } else { + scopes + }, worktreeTrees' = worktreeTrees, externalTaint' = externalTaint.exclude(Set(worktree)), processedEvents' = processedEvents, scopeStartCount' = scopeStartCount, - everTerminal' = everTerminal.union(liveScopesOn(worktree)), + everTerminal' = if (abandonLiveScopes) { + everTerminal.union(liveScopesOn(worktree)) + } else { + everTerminal + }, attempts' = attempts, cursorHistory' = cursorHistory.union(Set({ worktreeId: worktree, revision: state.revision + 1, cursorTree: worktreeTrees.get(worktree), })), + protocolHistory' = protocolHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: worktreeTrees.get(worktree), + needsRebaseline: false, + })), + rolloverHistory' = rolloverHistory, + scopeHistory' = scopeHistory, + startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -812,6 +991,30 @@ module mutation_cursor { ) }) + val ProtocolHistoryUniquePerWorktreeRevision = protocolHistory.forall(left => { + protocolHistory.forall(right => { + (left.worktreeId == right.worktreeId and left.revision == right.revision) + implies left == right + }) + }) + + val ProtocolHistoryHasCurrentState = WORKTREES.forall(worktree => { + protocolHistory.exists(checkpoint => + checkpoint.worktreeId == worktree and + checkpoint.revision == worktrees.get(worktree).revision and + checkpoint.cursorTree == worktrees.get(worktree).cursorTree and + checkpoint.needsRebaseline == worktrees.get(worktree).needsRebaseline + ) + }) + + val ScopeHistoryUniquePerWorktreeRevision = scopeHistory.forall(left => { + scopeHistory.forall(right => { + (left.worktreeId == right.worktreeId and + left.revision == right.revision and + left.scope == right.scope) implies left == right + }) + }) + val MutationEventsMatchCursorHistory = mutationEvents.forall(event => { cursorHistory.exists(before => before.worktreeId == event.worktreeId and @@ -829,6 +1032,27 @@ module mutation_cursor { event.tainted == (event.failureKind != Healthy) }) + val MutationEventsCrossOnlyTrustworthyProtocolStates = + mutationEvents.forall(event => { + protocolHistory.exists(before => + before.worktreeId == event.worktreeId and + before.revision == event.revision - 1 and + before.cursorTree == event.beforeTree and + not(before.needsRebaseline) + ) and + protocolHistory.exists(after => + after.worktreeId == event.worktreeId and + after.revision == event.revision and + after.cursorTree == event.afterTree and + not(after.needsRebaseline) + ) + }) + + val NeedsRebaselineSuppressesAttribution = WORKTREES.forall(worktree => { + worktrees.get(worktree).needsRebaseline implies + attributionFor(worktree) == IneligibleUnscoped + }) + val AttributionMatchesObservedScopes = mutationEvents.forall(event => { if (event.failureKind != Healthy) { event.attribution == IneligibleUnscoped @@ -860,23 +1084,57 @@ module mutation_cursor { val AbandonDoesNotEmitExclusiveEvidenceForUnobservedGap = mutationEvents.forall(event => { - match event.attribution { - | AiExclusive(_) => event.activeScopes.size() == 1 - | _ => true - } + protocolHistory.exists(before => + before.worktreeId == event.worktreeId and + before.revision == event.revision - 1 and + before.cursorTree == event.beforeTree and + not(before.needsRebaseline) + ) }) - val SameActorRolloverAbandonsOldScope = SCOPES.forall(scope => { - scopes.get(scope).status == Abandoned implies - everTerminal.contains(scope) + val SameActorRolloverAbandonsOldScope = rolloverHistory.forall(rollover => { + rollover.abandonedScopes.size() > 0 and + rollover.abandonedScopes.forall(scope => + scopeHistory.exists(checkpoint => + checkpoint.worktreeId == rollover.worktreeId and + checkpoint.revision == rollover.revision and + checkpoint.scope == scope and + checkpoint.status == Abandoned + ) + ) }) - val SameActorRolloverDoesNotClaimOldGap = - mutationEvents.forall(event => event.beforeTree != event.afterTree) + val SameActorRolloverDoesNotClaimOldGap = rolloverHistory.forall(rollover => { + cursorHistory.exists(checkpoint => + checkpoint.worktreeId == rollover.worktreeId and + checkpoint.revision == rollover.revision and + checkpoint.cursorTree == rollover.cursorTree + ) and + protocolHistory.exists(checkpoint => + checkpoint.worktreeId == rollover.worktreeId and + checkpoint.revision == rollover.revision and + checkpoint.cursorTree == rollover.cursorTree and + not(checkpoint.needsRebaseline) + ) and + mutationEvents.forall(event => + not( + event.worktreeId == rollover.worktreeId and + event.revision == rollover.revision + ) + ) + }) val DifferentActorStartDoesNotAbandonExistingScope = - mutationEvents.forall(event => event.attribution != AiContended or - event.activeScopes.size() >= 2) + startHistory.forall(start => + start.preservedScopes.forall(scope => + scopeHistory.exists(checkpoint => + checkpoint.worktreeId == start.worktreeId and + checkpoint.revision == start.revision and + checkpoint.scope == scope and + checkpoint.status == Active + ) + ) + ) val StaleAttemptsDoNotCommitEvidence = ATTEMPTS.forall(id => { attempts.get(id).status != Rejected or not(evidenceAttempts.contains(id)) @@ -912,8 +1170,13 @@ module mutation_cursor { MutationEventUniquePerWorktreeRevision, CursorHistoryUniquePerWorktreeRevision, CursorHistoryHasCurrentState, + ProtocolHistoryUniquePerWorktreeRevision, + ProtocolHistoryHasCurrentState, + ScopeHistoryUniquePerWorktreeRevision, MutationEventsMatchCursorHistory, MutationFailureKindMatchesTaint, + MutationEventsCrossOnlyTrustworthyProtocolStates, + NeedsRebaselineSuppressesAttribution, AttributionMatchesObservedScopes, AiExclusiveRequiresExactlyOneActiveScope, AiContendedRequiresMultipleActiveScopes, @@ -1196,6 +1459,54 @@ module mutation_cursor { .expect(evidenceAttempts.size() == 0) .expect(Safety) + run testStandaloneAbandonmentRequiresRebaseline = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(prepare(Attempt1, Start({ scope: Scope2, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .then(mutate(WT0, Tree1)) + .then(abandon(Scope0)) + .expect(worktrees.get(WT0).cursorTree == Tree0) + .expect(worktrees.get(WT0).needsRebaseline) + .expect(mutationEvents.size() == 0) + .then(prepare(Attempt2, Flush(WT0))) + .then(commitAttempt(Attempt2)) + .expect(worktrees.get(WT0).cursorTree == Tree0) + .expect(worktrees.get(WT0).needsRebaseline) + .expect(mutationEvents.size() == 0) + .then(recover(WT0)) + .expect(worktrees.get(WT0).cursorTree == Tree1) + .expect(not(worktrees.get(WT0).needsRebaseline)) + .expect(scopes.get(Scope2).status == Active) + .then(mutate(WT0, Tree2)) + .then(prepare(Attempt3, Flush(WT0))) + .then(commitAttempt(Attempt3)) + .expect( + mutationEvents.exists(event => + event.beforeTree == Tree1 and + event.afterTree == Tree2 and + event.attribution == AiExclusive(Scope2) + ) + ) + .expect(Safety) + + run testStandaloneAbandonmentRejectsPreparedObservation = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(prepare(Attempt1, Start({ scope: Scope2, event: Event1 }))) + .then(commitAttempt(Attempt1)) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt2, Flush(WT0))) + .then(abandon(Scope0)) + .then(commitAttempt(Attempt2)) + .expect(attempts.get(Attempt2).status == Rejected) + .expect(worktrees.get(WT0).cursorTree == Tree0) + .expect(worktrees.get(WT0).needsRebaseline) + .expect(mutationEvents.size() == 0) + .expect(Safety) + run testAbandonedScopeCannotReactivate = init .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) From e6bd47f76fb7f3db3965e96cb346afe5184fca2c Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 07:00:49 +0200 Subject: [PATCH 04/16] spec: Record abandon checkpoints and enforce rebaseline invariants Track the cursor tree and revision at scope abandonment so the model can prove that abandonment creates a rebaseline requirement without emitting mutation events. Add uniqueness and scenario coverage for the new history. Co-authored-by: SCE --- spec/mutation_cursor.qnt | 74 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/spec/mutation_cursor.qnt b/spec/mutation_cursor.qnt index bad275c8..1e259ae6 100644 --- a/spec/mutation_cursor.qnt +++ b/spec/mutation_cursor.qnt @@ -83,6 +83,13 @@ module mutation_cursor { status: ScopeStatus, } + type AbandonCheckpoint = { + worktreeId: WorktreeId, + revision: int, + scope: ScopeId, + cursorTree: TreeId, + } + type StartCheckpoint = { worktreeId: WorktreeId, revision: int, @@ -233,6 +240,7 @@ module mutation_cursor { var protocolHistory: Set[ProtocolCheckpoint] var rolloverHistory: Set[RolloverCheckpoint] var scopeHistory: Set[ScopeCheckpoint] + var abandonHistory: Set[AbandonCheckpoint] var startHistory: Set[StartCheckpoint] var evidenceAttempts: Set[AttemptId] var mutationEvents: Set[MutationEvent] @@ -355,6 +363,7 @@ module mutation_cursor { ), rolloverHistory' = Set(), scopeHistory' = Set(), + abandonHistory' = Set(), startHistory' = Set(), evidenceAttempts' = Set(), mutationEvents' = Set(), @@ -373,6 +382,7 @@ module mutation_cursor { protocolHistory' = protocolHistory, rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, + abandonHistory' = abandonHistory, startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -391,6 +401,7 @@ module mutation_cursor { protocolHistory' = protocolHistory, rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, + abandonHistory' = abandonHistory, startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -423,6 +434,7 @@ module mutation_cursor { protocolHistory' = protocolHistory, rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, + abandonHistory' = abandonHistory, startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -504,6 +516,7 @@ module mutation_cursor { protocolHistory' = protocolHistory, rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, + abandonHistory' = abandonHistory, startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -665,6 +678,7 @@ module mutation_cursor { protocolHistory' = nextProtocolHistory, rolloverHistory' = nextRolloverHistory, scopeHistory' = nextScopeHistory, + abandonHistory' = abandonHistory, startHistory' = nextStartHistory, evidenceAttempts' = nextEvidenceAttempts, mutationEvents' = mutationEvents.union(emitted), @@ -706,6 +720,7 @@ module mutation_cursor { })), rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, + abandonHistory' = abandonHistory, startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -730,6 +745,7 @@ module mutation_cursor { protocolHistory' = protocolHistory, rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, + abandonHistory' = abandonHistory, startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -775,7 +791,18 @@ module mutation_cursor { needsRebaseline: true, })), rolloverHistory' = rolloverHistory, - scopeHistory' = scopeHistory, + scopeHistory' = scopeHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + scope: scope, + status: Abandoned, + })), + abandonHistory' = abandonHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + scope: scope, + cursorTree: state.cursorTree, + })), startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -844,6 +871,7 @@ module mutation_cursor { })), rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, + abandonHistory' = abandonHistory, startHistory' = startHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -1015,6 +1043,41 @@ module mutation_cursor { }) }) + val AbandonCreatesRebaselineRequirement = abandonHistory.forall(abandon => { + protocolHistory.exists(checkpoint => + checkpoint.worktreeId == abandon.worktreeId and + checkpoint.revision == abandon.revision and + checkpoint.cursorTree == abandon.cursorTree and + checkpoint.needsRebaseline + ) and + cursorHistory.exists(checkpoint => + checkpoint.worktreeId == abandon.worktreeId and + checkpoint.revision == abandon.revision - 1 and + checkpoint.cursorTree == abandon.cursorTree + ) and + scopeHistory.exists(checkpoint => + checkpoint.worktreeId == abandon.worktreeId and + checkpoint.revision == abandon.revision and + checkpoint.scope == abandon.scope and + checkpoint.status == Abandoned + ) and + mutationEvents.forall(event => + not( + event.worktreeId == abandon.worktreeId and + event.revision == abandon.revision + ) + ) + }) + + val AbandonHistoryUniquePerWorktreeRevisionScope = + abandonHistory.forall(left => { + abandonHistory.forall(right => { + (left.worktreeId == right.worktreeId and + left.revision == right.revision and + left.scope == right.scope) implies left == right + }) + }) + val MutationEventsMatchCursorHistory = mutationEvents.forall(event => { cursorHistory.exists(before => before.worktreeId == event.worktreeId and @@ -1173,6 +1236,8 @@ module mutation_cursor { ProtocolHistoryUniquePerWorktreeRevision, ProtocolHistoryHasCurrentState, ScopeHistoryUniquePerWorktreeRevision, + AbandonCreatesRebaselineRequirement, + AbandonHistoryUniquePerWorktreeRevisionScope, MutationEventsMatchCursorHistory, MutationFailureKindMatchesTaint, MutationEventsCrossOnlyTrustworthyProtocolStates, @@ -1470,6 +1535,12 @@ module mutation_cursor { .expect(worktrees.get(WT0).cursorTree == Tree0) .expect(worktrees.get(WT0).needsRebaseline) .expect(mutationEvents.size() == 0) + .expect(abandonHistory.exists(checkpoint => + checkpoint.worktreeId == WT0 and + checkpoint.scope == Scope0 and + checkpoint.revision == worktrees.get(WT0).revision and + checkpoint.cursorTree == Tree0 + )) .then(prepare(Attempt2, Flush(WT0))) .then(commitAttempt(Attempt2)) .expect(worktrees.get(WT0).cursorTree == Tree0) @@ -1516,6 +1587,7 @@ module mutation_cursor { .then(commitAttempt(Attempt1)) .expect(scopes.get(Scope0).status == Abandoned) .expect(scopeStartCount.get(Scope0) == 1) + .expect(abandonHistory.size() == 1) .expect(Safety) run testSameActorRolloverAbandonsOldScope = From 4ecb45f53c40d55cccb987f9c5e9f73a40d731c5 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 07:11:18 +0200 Subject: [PATCH 05/16] ci: verify Quint specs when changed --- .github/workflows/quint.yml | 73 +++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/quint.yml diff --git a/.github/workflows/quint.yml b/.github/workflows/quint.yml new file mode 100644 index 00000000..3ccf8c6a --- /dev/null +++ b/.github/workflows/quint.yml @@ -0,0 +1,73 @@ +name: Quint + +on: + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + quint: + name: Quint gate + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - name: Detect Quint changes + id: changes + env: + EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + echo "quint=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '\.qnt$'; then + echo "quint=true" >> "$GITHUB_OUTPUT" + else + echo "quint=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install Nix + if: steps.changes.outputs.quint == 'true' + uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 + + - name: Enable Magic Nix Cache + if: steps.changes.outputs.quint == 'true' + uses: DeterminateSystems/magic-nix-cache-action@908b263ff629f4cc17666315b7fd3ec127c6244d # v14 + with: + use-flakehub: false + use-gha-cache: true + + - name: Typecheck Quint spec + if: steps.changes.outputs.quint == 'true' + run: nix run .#quint -- typecheck spec/mutation_cursor.qnt + + - name: Run Quint tests + if: steps.changes.outputs.quint == 'true' + run: nix run .#quint -- test spec/mutation_cursor.qnt + + - name: Verify Quint safety invariant + if: steps.changes.outputs.quint == 'true' + run: nix run .#quint -- verify spec/mutation_cursor.qnt --invariant=Safety + + - name: No Quint changes + if: steps.changes.outputs.quint != 'true' + run: echo "No Quint specification changes detected; validation skipped." From 334d274b5cee844f4a9426849d512a2b111100f3 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 08:30:44 +0200 Subject: [PATCH 06/16] language: Update Quint language server dependencies for tar 7 Avoid the vulnerable tar 6 dependency by applying the tar 7 override during the Nix package patch phase. Refresh the lockfile so npm dependency hashes and transitive packages match the supported dependency graph. Co-authored-by: SCE --- flake.nix | 8 +- nix/quint-language-server/package-lock.json | 136 +++++++------------- 2 files changed, 56 insertions(+), 88 deletions(-) diff --git a/flake.nix b/flake.nix index 006fe159..736deb8c 100644 --- a/flake.nix +++ b/flake.nix @@ -70,8 +70,12 @@ url = "https://registry.npmjs.org/@informalsystems/quint-language-server/-/quint-language-server-0.19.0.tgz"; hash = "sha256-gNoz7Pu+/TO/Vp86IB8tfZ9vHN78L7eRllITNtsnGFY="; }; - postPatch = "cp ${./nix/quint-language-server/package-lock.json} package-lock.json"; - npmDepsHash = "sha256-zw8hBTxOUAcKgJrK1J+0APwtGv+2Cv22LcNHogaNBPc="; + postPatch = '' + cp ${./nix/quint-language-server/package-lock.json} package-lock.json + ${pkgs.jq}/bin/jq '.overrides = {"tar": "^7.5.21"}' package.json > package.json.tmp + mv package.json.tmp package.json + ''; + npmDepsHash = "sha256-+ldzECIo3yC9RgNC1U0U15HrM+IcYX8v00JFdzoMCog="; dontNpmBuild = true; nativeBuildInputs = [ pkgs.makeWrapper ]; installPhase = '' diff --git a/nix/quint-language-server/package-lock.json b/nix/quint-language-server/package-lock.json index 17737c04..50e91000 100644 --- a/nix/quint-language-server/package-lock.json +++ b/nix/quint-language-server/package-lock.json @@ -378,6 +378,18 @@ "node": ">=18" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1555,12 +1567,12 @@ } }, "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/cli-cursor": { @@ -4890,30 +4902,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6203,49 +6191,24 @@ } }, "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">=8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" + "node": ">= 18" } }, "node_modules/mocha": { @@ -6860,9 +6823,9 @@ } }, "node_modules/protobufjs/node_modules/@types/node": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", - "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.3.0.tgz", + "integrity": "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==", "license": "MIT", "dependencies": { "undici-types": "~8.3.0" @@ -7726,21 +7689,19 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "license": "BlueOak-1.0.0", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/text-table": { @@ -8128,9 +8089,9 @@ "license": "MIT" }, "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", "license": "MIT" }, "node_modules/which": { @@ -8377,10 +8338,13 @@ } }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } }, "node_modules/yargs": { "version": "17.7.3", From fc3befb93b9b14ac5363df0421c7c531609859aa Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 09:20:43 +0200 Subject: [PATCH 07/16] CI+language: Split Quint safety verification into parallel invariant gates Keep Quint typechecking and tests in one gate while running the safety invariant groups independently. Separate the expensive history checks with shorter trace bounds to avoid combined verification timing out while retaining all safety checks. Co-authored-by: SCE --- .github/workflows/quint.yml | 75 ++++++++++++++++++++++++++++++------- spec/mutation_cursor.qnt | 50 +++++++++++++------------ 2 files changed, 88 insertions(+), 37 deletions(-) diff --git a/.github/workflows/quint.yml b/.github/workflows/quint.yml index 3ccf8c6a..f4bbba5b 100644 --- a/.github/workflows/quint.yml +++ b/.github/workflows/quint.yml @@ -12,10 +12,11 @@ permissions: contents: read jobs: - quint: - name: Quint gate + detect: + name: Detect Quint changes runs-on: ubuntu-latest - timeout-minutes: 30 + outputs: + quint: ${{ steps.changes.outputs.quint }} steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 @@ -45,29 +46,77 @@ jobs: echo "quint=false" >> "$GITHUB_OUTPUT" fi + - name: No Quint changes + if: steps.changes.outputs.quint != 'true' + run: echo "No Quint specification changes detected; validation skipped." + + quint: + name: Quint gate + needs: detect + if: needs.detect.outputs.quint == 'true' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Nix - if: steps.changes.outputs.quint == 'true' uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 - name: Enable Magic Nix Cache - if: steps.changes.outputs.quint == 'true' uses: DeterminateSystems/magic-nix-cache-action@908b263ff629f4cc17666315b7fd3ec127c6244d # v14 with: use-flakehub: false use-gha-cache: true - name: Typecheck Quint spec - if: steps.changes.outputs.quint == 'true' run: nix run .#quint -- typecheck spec/mutation_cursor.qnt - name: Run Quint tests - if: steps.changes.outputs.quint == 'true' run: nix run .#quint -- test spec/mutation_cursor.qnt - - name: Verify Quint safety invariant - if: steps.changes.outputs.quint == 'true' - run: nix run .#quint -- verify spec/mutation_cursor.qnt --invariant=Safety + quint-verify: + name: Verify Quint safety invariant (${{ matrix.invariant }}) + needs: detect + if: needs.detect.outputs.quint == 'true' + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + # SafetyHistory does nested forall/exists over history logs that grow + # every step, so its BMC cost blows up with trace depth (this is what + # timed out the single combined Safety invariant at step 4). It gets + # a shorter trace bound; the cheaper groups keep the full default depth. + include: + - invariant: SafetyCore + max_steps: 10 + - invariant: SafetyAttribution + max_steps: 8 + - invariant: SafetyHistory + max_steps: 5 + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: No Quint changes - if: steps.changes.outputs.quint != 'true' - run: echo "No Quint specification changes detected; validation skipped." + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 + + - name: Enable Magic Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@908b263ff629f4cc17666315b7fd3ec127c6244d # v14 + with: + use-flakehub: false + use-gha-cache: true + + - name: Verify ${{ matrix.invariant }} + run: nix run .#quint -- verify spec/mutation_cursor.qnt --invariant=${{ matrix.invariant }} --max-steps=${{ matrix.max_steps }} diff --git a/spec/mutation_cursor.qnt b/spec/mutation_cursor.qnt index 1e259ae6..3bd29079 100644 --- a/spec/mutation_cursor.qnt +++ b/spec/mutation_cursor.qnt @@ -947,16 +947,6 @@ module mutation_cursor { isTerminal(scopes.get(scope).status) }) - val ClosedScopesStayTerminal = SCOPES.forall(scope => { - scopes.get(scope).status == Closed implies - isTerminal(scopes.get(scope).status) - }) - - val AbandonedScopesStayTerminal = SCOPES.forall(scope => { - scopes.get(scope).status == Abandoned implies - isTerminal(scopes.get(scope).status) - }) - val DatabaseFailureDoesNotMutateDurableProtocolState = externalTaint.forall(worktree => { worktrees.get(worktree).tainted == @@ -977,10 +967,6 @@ module mutation_cursor { ) }) - val RecoveryAbandonsActiveScopes = everTerminal.forall(scope => { - scopes.get(scope).status != Active - }) - val ScopeActorIdentityIsStable = SCOPES.forall(scope => { scopes.get(scope).actorKind == scopeActor(scope) }) @@ -1222,7 +1208,8 @@ module mutation_cursor { attempts.get(id).status == Rejected }) - val Safety = and { + // Cheap, quantifier-shallow invariants over WORKTREES/SCOPES/ATTEMPTS. + val SafetyCore = and { CursorRevisionConsistent, FailureKindMatchesTaint, TerminalScopesStayTerminal, @@ -1230,6 +1217,19 @@ module mutation_cursor { ScopeStartedAtMostOnce, NoNoopMutationEvents, MutationEventsHavePositiveRevision, + MutationFailureKindMatchesTaint, + DatabaseFailureDoesNotMutateDurableProtocolState, + ExternalTaintNeverStrengthensAttribution, + NeedsRebaselineSuppressesAttribution, + StaleAttemptsDoNotCommitEvidence, + } + + // Uniqueness and cross-referencing checks over the growing history logs + // (mutationEvents, cursorHistory, protocolHistory, scopeHistory, + // abandonHistory). These are the most expensive conjuncts to verify since + // their cost scales with trace depth, so they are split out to be checked + // in their own CI job rather than serialized with the rest of Safety. + val SafetyHistory = and { MutationEventUniquePerWorktreeRevision, CursorHistoryUniquePerWorktreeRevision, CursorHistoryHasCurrentState, @@ -1239,9 +1239,12 @@ module mutation_cursor { AbandonCreatesRebaselineRequirement, AbandonHistoryUniquePerWorktreeRevisionScope, MutationEventsMatchCursorHistory, - MutationFailureKindMatchesTaint, MutationEventsCrossOnlyTrustworthyProtocolStates, - NeedsRebaselineSuppressesAttribution, + RecoveryClearsExternalTaintOnlyAfterBaseline, + } + + // Attribution and rollover semantics. + val SafetyAttribution = and { AttributionMatchesObservedScopes, AiExclusiveRequiresExactlyOneActiveScope, AiContendedRequiresMultipleActiveScopes, @@ -1249,13 +1252,12 @@ module mutation_cursor { SameActorRolloverAbandonsOldScope, SameActorRolloverDoesNotClaimOldGap, DifferentActorStartDoesNotAbandonExistingScope, - DatabaseFailureDoesNotMutateDurableProtocolState, - ExternalTaintNeverStrengthensAttribution, - RecoveryClearsExternalTaintOnlyAfterBaseline, - RecoveryAbandonsActiveScopes, - ClosedScopesStayTerminal, - AbandonedScopesStayTerminal, - StaleAttemptsDoNotCommitEvidence, + } + + val Safety = and { + SafetyCore, + SafetyHistory, + SafetyAttribution, } run testStartObservesBeforeActivation = From 10eb94bae9e9114a98bcdc6358234dca5631e728 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 10:16:18 +0200 Subject: [PATCH 08/16] language: Pin adm-zip override to fix Quint language server DoS advisory Avoid GHSA-xcpc-8h2w-3j85 (adm-zip < 0.6.0 unbounded memory allocation on crafted ZIP central directory headers) by overriding adm-zip to ^0.6.0 during the Nix package patch phase, same pattern used for the tar 7 override. Refreshed the lockfile and npmDepsHash to match. Co-authored-by: SCE --- flake.nix | 4 ++-- nix/quint-language-server/package-lock.json | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.nix b/flake.nix index 736deb8c..da4d86d7 100644 --- a/flake.nix +++ b/flake.nix @@ -72,10 +72,10 @@ }; postPatch = '' cp ${./nix/quint-language-server/package-lock.json} package-lock.json - ${pkgs.jq}/bin/jq '.overrides = {"tar": "^7.5.21"}' package.json > package.json.tmp + ${pkgs.jq}/bin/jq '.overrides = {"tar": "^7.5.21", "adm-zip": "^0.6.0"}' package.json > package.json.tmp mv package.json.tmp package.json ''; - npmDepsHash = "sha256-+ldzECIo3yC9RgNC1U0U15HrM+IcYX8v00JFdzoMCog="; + npmDepsHash = "sha256-WueIhunZ6mP7ahKAEXIpDIoAIDr9PqW2MN6olCFODPw="; dontNpmBuild = true; nativeBuildInputs = [ pkgs.makeWrapper ]; installPhase = '' diff --git a/nix/quint-language-server/package-lock.json b/nix/quint-language-server/package-lock.json index 50e91000..dbebb60c 100644 --- a/nix/quint-language-server/package-lock.json +++ b/nix/quint-language-server/package-lock.json @@ -973,12 +973,12 @@ } }, "node_modules/adm-zip": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz", - "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz", + "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==", "license": "MIT", "engines": { - "node": ">=12.0" + "node": ">=14.0" } }, "node_modules/ajv": { From 6fca731c5afeb338747ccda242ed31b813fe28c1 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 10:48:25 +0200 Subject: [PATCH 09/16] CI: Add a result gate for Quint checks Ensure Quint validation failures remain visible as a single workflow failure status. Add an always-running gate that skips unchanged workflows and fails when typechecks/tests or safety verification fail. Co-authored-by: SCE --- .github/workflows/quint.yml | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/quint.yml b/.github/workflows/quint.yml index f4bbba5b..be777c12 100644 --- a/.github/workflows/quint.yml +++ b/.github/workflows/quint.yml @@ -51,7 +51,7 @@ jobs: run: echo "No Quint specification changes detected; validation skipped." quint: - name: Quint gate + name: Quint checks needs: detect if: needs.detect.outputs.quint == 'true' runs-on: ubuntu-latest @@ -120,3 +120,33 @@ jobs: - name: Verify ${{ matrix.invariant }} run: nix run .#quint -- verify spec/mutation_cursor.qnt --invariant=${{ matrix.invariant }} --max-steps=${{ matrix.max_steps }} + + gate: + name: Quint gate + if: always() + needs: + - detect + - quint + - quint-verify + runs-on: ubuntu-latest + steps: + - name: Check Quint result + env: + CHANGED: ${{ needs.detect.outputs.quint }} + CHECK_RESULT: ${{ needs.quint.result }} + VERIFY_RESULT: ${{ needs.quint-verify.result }} + run: | + if [ "$CHANGED" != "true" ]; then + echo "No Quint changes." + exit 0 + fi + + if [ "$CHECK_RESULT" != "success" ]; then + echo "Quint typecheck/tests failed." + exit 1 + fi + + if [ "$VERIFY_RESULT" != "success" ]; then + echo "Quint safety verification failed." + exit 1 + fi From d2a26587490912b50b669cb4939dc6821691e553 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 11:10:43 +0200 Subject: [PATCH 10/16] language: Preserve durable state across external taint Record a worktree's durable protocol state when database failure introduces external taint, and use that checkpoint to verify no durable mutation occurred. Disable abandonment while tainted and add coverage for the guarded behavior. Co-authored-by: SCE --- spec/mutation_cursor.qnt | 90 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/spec/mutation_cursor.qnt b/spec/mutation_cursor.qnt index 3bd29079..42a12902 100644 --- a/spec/mutation_cursor.qnt +++ b/spec/mutation_cursor.qnt @@ -108,6 +108,23 @@ module mutation_cursor { boundary: Boundary, } + type DurableStateCheckpoint = { + worktreeId: WorktreeId, + worktree: WorktreeState, + scopes: ScopeId -> ScopeState, + processedEvents: Set[EventKey], + scopeStartCount: ScopeId -> int, + everTerminal: Set[ScopeId], + cursorHistory: Set[CursorCheckpoint], + protocolHistory: Set[ProtocolCheckpoint], + rolloverHistory: Set[RolloverCheckpoint], + scopeHistory: Set[ScopeCheckpoint], + abandonHistory: Set[AbandonCheckpoint], + startHistory: Set[StartCheckpoint], + evidenceAttempts: Set[AttemptId], + mutationEvents: Set[MutationEvent], + } + val WORKTREES: Set[WorktreeId] = Set(WT0, WT1) val SCOPES: Set[ScopeId] = Set(Scope0, Scope1, Scope2, Scope3) val TREES: Set[TreeId] = Set(Tree0, Tree1, Tree2, Tree3) @@ -242,6 +259,7 @@ module mutation_cursor { var scopeHistory: Set[ScopeCheckpoint] var abandonHistory: Set[AbandonCheckpoint] var startHistory: Set[StartCheckpoint] + var taintHistory: Set[DurableStateCheckpoint] var evidenceAttempts: Set[AttemptId] var mutationEvents: Set[MutationEvent] @@ -257,6 +275,45 @@ module mutation_cursor { ): Set[ScopeId] = liveScopesOn(worktree).filter(scope => scopes.get(scope).actorKind == actor) + def durableStateFor(worktree: WorktreeId): DurableStateCheckpoint = { + worktreeId: worktree, + worktree: worktrees.get(worktree), + scopes: SCOPES.filter(scope => scopeWorktree(scope) == worktree).mapBy( + scope => scopes.get(scope) + ), + processedEvents: processedEvents.filter(event => + scopeWorktree(event.scopeId) == worktree + ), + scopeStartCount: SCOPES.filter(scope => scopeWorktree(scope) == worktree).mapBy( + scope => scopeStartCount.get(scope) + ), + everTerminal: everTerminal.filter(scope => scopeWorktree(scope) == worktree), + cursorHistory: cursorHistory.filter(checkpoint => + checkpoint.worktreeId == worktree + ), + protocolHistory: protocolHistory.filter(checkpoint => + checkpoint.worktreeId == worktree + ), + rolloverHistory: rolloverHistory.filter(checkpoint => + checkpoint.worktreeId == worktree + ), + scopeHistory: scopeHistory.filter(checkpoint => + checkpoint.worktreeId == worktree + ), + abandonHistory: abandonHistory.filter(checkpoint => + checkpoint.worktreeId == worktree + ), + startHistory: startHistory.filter(checkpoint => + checkpoint.worktreeId == worktree + ), + evidenceAttempts: evidenceAttempts.filter(attempt => + boundaryWorktree(attempts.get(attempt).boundary) == worktree + ), + mutationEvents: mutationEvents.filter(event => + event.worktreeId == worktree + ), + } + def rolloverScope( scope: ScopeId, worktree: WorktreeId, @@ -365,6 +422,7 @@ module mutation_cursor { scopeHistory' = Set(), abandonHistory' = Set(), startHistory' = Set(), + taintHistory' = Set(), evidenceAttempts' = Set(), mutationEvents' = Set(), } @@ -384,6 +442,7 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -403,6 +462,7 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -436,6 +496,7 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -518,6 +579,7 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -680,6 +742,7 @@ module mutation_cursor { scopeHistory' = nextScopeHistory, abandonHistory' = abandonHistory, startHistory' = nextStartHistory, + taintHistory' = taintHistory, evidenceAttempts' = nextEvidenceAttempts, mutationEvents' = mutationEvents.union(emitted), } @@ -722,6 +785,7 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -747,6 +811,7 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + taintHistory' = taintHistory.union(Set(durableStateFor(worktree))), evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -757,7 +822,7 @@ module mutation_cursor { val worktree = oldScope.worktreeId val state = worktrees.get(worktree) - if (not(isLive(oldScope.status))) { + if (not(isLive(oldScope.status)) or externalTaint.contains(worktree)) { stutter } else { all { @@ -804,6 +869,7 @@ module mutation_cursor { cursorTree: state.cursorTree, })), startHistory' = startHistory, + taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -873,6 +939,7 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, } @@ -948,10 +1015,9 @@ module mutation_cursor { }) val DatabaseFailureDoesNotMutateDurableProtocolState = - externalTaint.forall(worktree => { - worktrees.get(worktree).tainted == - (worktrees.get(worktree).failureKind != Healthy) - }) + externalTaint.forall(worktree => + taintHistory.contains(durableStateFor(worktree)) + ) val ExternalTaintNeverStrengthensAttribution = WORKTREES.forall(worktree => { externalTaint.contains(worktree) implies @@ -1512,6 +1578,20 @@ module mutation_cursor { .expect(externalTaint.contains(WT0)) .expect(Safety) + run testAbandonDisabledDuringExternalTaint = + init + .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) + .then(commitAttempt(Attempt0)) + .then(databaseFailure(WT0)) + .then(abandon(Scope0)) + .expect(worktrees.get(WT0).revision == 1) + .expect(not(worktrees.get(WT0).needsRebaseline)) + .expect(scopes.get(Scope0).status == Active) + .expect(abandonHistory.size() == 0) + .expect(externalTaint.contains(WT0)) + .expect(taintHistory.contains(durableStateFor(WT0))) + .expect(Safety) + run testExternalTaintRecoveryAbandonsScopeAndBaselines = init .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) From 71d2fcca51e72309d71209c9c4e2d34584d182be Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 11:24:58 +0200 Subject: [PATCH 11/16] spec: Remove actor-based scope rollover Keep active scopes independent of harness type so starting a new scope preserves existing scopes and correctly reports subsequent mutations as contended. Remove the rollover state, transitions, safety properties, and deterministic coverage from the Quint model and align its documentation with explicit stale-session recovery. Co-authored-by: SCE --- spec/mutation_cursor.md | 13 ++-- spec/mutation_cursor.qnt | 155 +++++---------------------------------- 2 files changed, 24 insertions(+), 144 deletions(-) diff --git a/spec/mutation_cursor.md b/spec/mutation_cursor.md index b7072e3f..7b9a6cf0 100644 --- a/spec/mutation_cursor.md +++ b/spec/mutation_cursor.md @@ -107,15 +107,14 @@ A scope has one of four statuses: `Closed` and `Abandoned` are terminal. `Abandon(scope)` changes only an active scope to `Abandoned`; it never reactivates a terminal scope. It increments the worktree revision, leaves the cursor unchanged, and sets `needsRebaseline`. Until recovery establishes a new baseline, normal observations emit no mutation evidence. An abandoned scope must not receive exclusive attribution for the unobserved gap preceding abandonment. -If a new scope starts on the same worktree with the same actor while that actor already has an active scope, the model performs stale-scope rollover atomically: +Starting a new scope never infers that an existing scope is stale from `ActorKind`. Existing active scopes remain active regardless of harness type, and the new scope becomes active independently: ```text -old same-actor active scopes → Abandoned -observe/rebaseline current worktree conservatively +existing active scopes → remain Active new scope → Active ``` -The old cursor-to-current-tree gap produces no exclusive evidence for the old scope. A different actor does not trigger rollover: existing scopes remain active, and subsequent work is `AiContended` while two or more scopes are active. +`ScopeId` is the session/scope identity. If a real session is stale, production must establish that through an explicit session/process/generation guarantee and invoke abandonment or recovery; harness type alone is not sufficient. Until then, subsequent work is `AiContended` while two or more scopes are active. Attribution remains: @@ -134,15 +133,15 @@ The model includes safety properties covering: - database failure not mutating durable protocol state; - external taint not strengthening attribution; - recovery baseline before clearing external taint; -- recovery and rollover abandoning active scopes; +- recovery abandoning active scopes; - closed and abandoned terminality; - no exclusive evidence for an abandoned unobserved gap; -- same-actor rollover and different-actor contention; +- same-actor and different-actor contention; - `AiExclusive` requiring exactly one active scope; - `AiContended` requiring multiple active scopes; - CAS/replay safety and cursor/evidence consistency. -Deterministic runs cover database-unavailable state preservation, external-taint recovery, abandoned-scope non-reactivation, same-actor rollover, and different-actor contention. +Deterministic runs cover database-unavailable state preservation, external-taint recovery, abandoned-scope non-reactivation, and same-actor and different-actor contention. ## Implementation refinement diff --git a/spec/mutation_cursor.qnt b/spec/mutation_cursor.qnt index 42a12902..7c35599a 100644 --- a/spec/mutation_cursor.qnt +++ b/spec/mutation_cursor.qnt @@ -69,13 +69,6 @@ module mutation_cursor { needsRebaseline: bool, } - type RolloverCheckpoint = { - worktreeId: WorktreeId, - revision: int, - cursorTree: TreeId, - abandonedScopes: Set[ScopeId], - } - type ScopeCheckpoint = { worktreeId: WorktreeId, revision: int, @@ -117,7 +110,6 @@ module mutation_cursor { everTerminal: Set[ScopeId], cursorHistory: Set[CursorCheckpoint], protocolHistory: Set[ProtocolCheckpoint], - rolloverHistory: Set[RolloverCheckpoint], scopeHistory: Set[ScopeCheckpoint], abandonHistory: Set[AbandonCheckpoint], startHistory: Set[StartCheckpoint], @@ -255,7 +247,6 @@ module mutation_cursor { var attempts: AttemptId -> AttemptState var cursorHistory: Set[CursorCheckpoint] var protocolHistory: Set[ProtocolCheckpoint] - var rolloverHistory: Set[RolloverCheckpoint] var scopeHistory: Set[ScopeCheckpoint] var abandonHistory: Set[AbandonCheckpoint] var startHistory: Set[StartCheckpoint] @@ -269,12 +260,6 @@ module mutation_cursor { state.worktreeId == worktree and isLive(state.status) }) - def sameActorLiveScopes( - worktree: WorktreeId, - actor: ActorKind - ): Set[ScopeId] = - liveScopesOn(worktree).filter(scope => scopes.get(scope).actorKind == actor) - def durableStateFor(worktree: WorktreeId): DurableStateCheckpoint = { worktreeId: worktree, worktree: worktrees.get(worktree), @@ -294,9 +279,6 @@ module mutation_cursor { protocolHistory: protocolHistory.filter(checkpoint => checkpoint.worktreeId == worktree ), - rolloverHistory: rolloverHistory.filter(checkpoint => - checkpoint.worktreeId == worktree - ), scopeHistory: scopeHistory.filter(checkpoint => checkpoint.worktreeId == worktree ), @@ -314,27 +296,6 @@ module mutation_cursor { ), } - def rolloverScope( - scope: ScopeId, - worktree: WorktreeId, - actor: ActorKind - ): ScopeState = { - val oldScope = scopes.get(scope) - if ( - oldScope.worktreeId == worktree and - isLive(oldScope.status) and - oldScope.actorKind == actor - ) { - { - status: Abandoned, - actorKind: oldScope.actorKind, - worktreeId: oldScope.worktreeId, - } - } else { - oldScope - } - } - def attributionFor(worktree: WorktreeId): Attribution = { val state = worktrees.get(worktree) val live = liveScopesOn(worktree) @@ -418,7 +379,6 @@ module mutation_cursor { needsRebaseline: false, } ), - rolloverHistory' = Set(), scopeHistory' = Set(), abandonHistory' = Set(), startHistory' = Set(), @@ -438,7 +398,6 @@ module mutation_cursor { attempts' = attempts, cursorHistory' = cursorHistory, protocolHistory' = protocolHistory, - rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, @@ -458,7 +417,6 @@ module mutation_cursor { attempts' = attempts, cursorHistory' = cursorHistory, protocolHistory' = protocolHistory, - rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, @@ -492,7 +450,6 @@ module mutation_cursor { }), cursorHistory' = cursorHistory, protocolHistory' = protocolHistory, - rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, @@ -510,7 +467,6 @@ module mutation_cursor { val state = worktrees.get(worktree) val scope = boundaryScope(boundary) val currentScope = scopes.get(scope) - val event = boundaryEvent(boundary) val fresh = planned.status == Prepared and not(externalTaint.contains(worktree)) and @@ -529,14 +485,9 @@ module mutation_cursor { true } val accepted = fresh - val rollover = - isStart(boundary) and - currentScope.status == NeverSeen and - sameActorLiveScopes(worktree, currentScope.actorKind).size() > 0 val observedChange = accepted and observes and planned.beforeTree != planned.afterTree - val changed = observedChange and not(rollover) and - not(state.needsRebaseline) + val changed = observedChange and not(state.needsRebaseline) val advancesRevision = accepted and (not(isFlush(boundary)) or observedChange) val live = liveScopesOn(worktree) val attribution = attributionFor(worktree) @@ -575,7 +526,6 @@ module mutation_cursor { }), cursorHistory' = cursorHistory, protocolHistory' = protocolHistory, - rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, @@ -584,19 +534,9 @@ module mutation_cursor { mutationEvents' = mutationEvents, } } else { - val rolloverScopes = - if (rollover) { - scopes - .set(Scope0, rolloverScope(Scope0, worktree, currentScope.actorKind)) - .set(Scope1, rolloverScope(Scope1, worktree, currentScope.actorKind)) - .set(Scope2, rolloverScope(Scope2, worktree, currentScope.actorKind)) - .set(Scope3, rolloverScope(Scope3, worktree, currentScope.actorKind)) - } else { - scopes - } val nextScope = if (isStart(boundary) and currentScope.status == NeverSeen) { - rolloverScopes.set(scope, { + scopes.set(scope, { status: Active, actorKind: currentScope.actorKind, worktreeId: worktree, @@ -614,7 +554,7 @@ module mutation_cursor { scopes } val nextCursor = - if (observes and (not(state.needsRebaseline) or rollover)) { + if (observes and not(state.needsRebaseline)) { planned.afterTree } else { state.cursorTree @@ -626,7 +566,7 @@ module mutation_cursor { revision: state.revision + 1, tainted: state.tainted, failureKind: state.failureKind, - needsRebaseline: if (rollover) false else state.needsRebaseline, + needsRebaseline: state.needsRebaseline, }) } else { worktrees @@ -652,20 +592,6 @@ module mutation_cursor { } else { protocolHistory } - val nextRolloverHistory = - if (rollover) { - rolloverHistory.union(Set({ - worktreeId: worktree, - revision: state.revision + 1, - cursorTree: nextCursor, - abandonedScopes: sameActorLiveScopes( - worktree, - currentScope.actorKind - ), - })) - } else { - rolloverHistory - } val nextScopeHistory = if (isStart(boundary) and currentScope.status == NeverSeen) { scopeHistory.union( @@ -682,11 +608,7 @@ module mutation_cursor { scopeHistory } val nextStartHistory = - if ( - isStart(boundary) and - currentScope.status == NeverSeen and - not(rollover) - ) { + if (isStart(boundary) and currentScope.status == NeverSeen) { startHistory.union(Set({ worktreeId: worktree, revision: state.revision + 1, @@ -710,11 +632,7 @@ module mutation_cursor { scopeStartCount } val nextTerminal = - if (rollover) { - everTerminal.union( - sameActorLiveScopes(worktree, currentScope.actorKind) - ) - } else if (isClose(boundary) and + if (isClose(boundary) and (currentScope.status == NeverSeen or isLive(currentScope.status))) { everTerminal.union(Set(scope)) } else { @@ -738,7 +656,6 @@ module mutation_cursor { }), cursorHistory' = nextHistory, protocolHistory' = nextProtocolHistory, - rolloverHistory' = nextRolloverHistory, scopeHistory' = nextScopeHistory, abandonHistory' = abandonHistory, startHistory' = nextStartHistory, @@ -781,7 +698,6 @@ module mutation_cursor { cursorTree: state.cursorTree, needsRebaseline: state.needsRebaseline, })), - rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, @@ -807,7 +723,6 @@ module mutation_cursor { attempts' = attempts, cursorHistory' = cursorHistory, protocolHistory' = protocolHistory, - rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, @@ -855,7 +770,6 @@ module mutation_cursor { cursorTree: state.cursorTree, needsRebaseline: true, })), - rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory.union(Set({ worktreeId: worktree, revision: state.revision + 1, @@ -935,7 +849,6 @@ module mutation_cursor { cursorTree: worktreeTrees.get(worktree), needsRebaseline: false, })), - rolloverHistory' = rolloverHistory, scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, @@ -1207,39 +1120,7 @@ module mutation_cursor { ) }) - val SameActorRolloverAbandonsOldScope = rolloverHistory.forall(rollover => { - rollover.abandonedScopes.size() > 0 and - rollover.abandonedScopes.forall(scope => - scopeHistory.exists(checkpoint => - checkpoint.worktreeId == rollover.worktreeId and - checkpoint.revision == rollover.revision and - checkpoint.scope == scope and - checkpoint.status == Abandoned - ) - ) - }) - - val SameActorRolloverDoesNotClaimOldGap = rolloverHistory.forall(rollover => { - cursorHistory.exists(checkpoint => - checkpoint.worktreeId == rollover.worktreeId and - checkpoint.revision == rollover.revision and - checkpoint.cursorTree == rollover.cursorTree - ) and - protocolHistory.exists(checkpoint => - checkpoint.worktreeId == rollover.worktreeId and - checkpoint.revision == rollover.revision and - checkpoint.cursorTree == rollover.cursorTree and - not(checkpoint.needsRebaseline) - ) and - mutationEvents.forall(event => - not( - event.worktreeId == rollover.worktreeId and - event.revision == rollover.revision - ) - ) - }) - - val DifferentActorStartDoesNotAbandonExistingScope = + val StartDoesNotAbandonExistingScopes = startHistory.forall(start => start.preservedScopes.forall(scope => scopeHistory.exists(checkpoint => @@ -1309,15 +1190,13 @@ module mutation_cursor { RecoveryClearsExternalTaintOnlyAfterBaseline, } - // Attribution and rollover semantics. + // Attribution and scope-start semantics. val SafetyAttribution = and { AttributionMatchesObservedScopes, AiExclusiveRequiresExactlyOneActiveScope, AiContendedRequiresMultipleActiveScopes, AbandonDoesNotEmitExclusiveEvidenceForUnobservedGap, - SameActorRolloverAbandonsOldScope, - SameActorRolloverDoesNotClaimOldGap, - DifferentActorStartDoesNotAbandonExistingScope, + StartDoesNotAbandonExistingScopes, } val Safety = and { @@ -1672,20 +1551,22 @@ module mutation_cursor { .expect(abandonHistory.size() == 1) .expect(Safety) - run testSameActorRolloverAbandonsOldScope = + run testSameActorStartKeepsExistingScopeContended = init .then(prepare(Attempt0, Start({ scope: Scope0, event: Event0 }))) .then(commitAttempt(Attempt0)) - .then(mutate(WT0, Tree1)) .then(prepare(Attempt1, Start({ scope: Scope1, event: Event1 }))) .then(commitAttempt(Attempt1)) - .expect(scopes.get(Scope0).status == Abandoned) + .then(mutate(WT0, Tree1)) + .then(prepare(Attempt2, Flush(WT0))) + .then(commitAttempt(Attempt2)) + .expect(scopes.get(Scope0).status == Active) .expect(scopes.get(Scope1).status == Active) - .expect(worktrees.get(WT0).cursorTree == Tree1) - .expect(mutationEvents.size() == 0) .expect( - mutationEvents.forall(event => - event.attribution != AiExclusive(Scope0) + mutationEvents.exists(event => + event.afterTree == Tree1 and + event.activeScopes.size() == 2 and + event.attribution == AiContended ) ) .expect(Safety) From b0dd7859bbb7c7a12d5941896ef9feecac553ea9 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 11:38:33 +0200 Subject: [PATCH 12/16] language: Implement durable recovery checkpoints for tainted worktrees Record the observed and resulting cursor during taint recovery so external taint is cleared only after a recovery baseline is established. Clarify that taint advances the worktree revision, permits later fresh snapshots to proceed, and marks evidence as ineligible until recovery. Extend the Quint history state and recovery scenario to verify the baseline and taint-clearing behavior. Co-authored-by: SCE --- spec/mutation_cursor.md | 11 +++++----- spec/mutation_cursor.qnt | 44 ++++++++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/spec/mutation_cursor.md b/spec/mutation_cursor.md index 7b9a6cf0..f86e3340 100644 --- a/spec/mutation_cursor.md +++ b/spec/mutation_cursor.md @@ -35,7 +35,7 @@ Hook replay identity is scoped by `ScopeId` and `EventId` through `EventKey`. Th `worktrees.cursorTree`, `worktrees.revision`, scope state, `processedEvents`, and `mutationEvents` represent state durably stored in the Agent Trace database. `worktrees.needsRebaseline` is a durable protocol marker for an ambiguous cursor interval; it is distinct from both snapshot failure and external database taint. -A snapshot failure occurs while the database is healthy. `taint(worktree)` therefore records `SnapshotFailure` in the durable worktree state, invalidating subsequent speculative attempts until recovery. +A snapshot failure occurs while the database is healthy. `taint(worktree)` therefore records `SnapshotFailure` in the durable worktree state and increments the worktree revision. This invalidates speculative attempts that were already prepared before the failure. It does not quarantine later attempts: once a subsequent snapshot is prepared against the tainted state and the normal freshness checks pass, it may advance the cursor. Because failure states weaken attribution, any evidence emitted while tainted is `IneligibleUnscoped` until recovery. Database unavailability is different. `databaseFailure(worktree)` changes only: @@ -75,23 +75,22 @@ produce no evidence for the skipped interval clear needsRebaseline ``` -For external taint or snapshot failure, recovery retains the stronger existing behavior of abandoning active scopes. The external-taint recovery path is: - +For external taint or snapshot failure, recovery retains the stronger existing behavior of abandoning active scopes. The recovery path is: ```text -observe externalTaint +observe taint or externalTaint ↓ snapshot current worktree ↓ establish current tree as the new cursor baseline ↓ -produce no evidence for the skipped interval +produce no evidence for the recovery baseline ↓ abandon every active scope on the worktree ↓ commit recovery to DB ↓ -clear externalTaint +clear the taint/failure state and externalTaint ``` Taint or external-taint recovery abandons active scopes because no trustworthy normal close boundary was observed. Healthy `needsRebaseline` recovery instead preserves surviving active scopes: only the ambiguous skipped interval is discarded, and those scopes may resume attribution after the new baseline. No filesystem details, SQLite/Turso internals, retries, or OS crash timing are modeled. diff --git a/spec/mutation_cursor.qnt b/spec/mutation_cursor.qnt index 7c35599a..f4f77883 100644 --- a/spec/mutation_cursor.qnt +++ b/spec/mutation_cursor.qnt @@ -89,6 +89,13 @@ module mutation_cursor { preservedScopes: Set[ScopeId], } + type RecoveryCheckpoint = { + worktreeId: WorktreeId, + observedTree: TreeId, + resultingCursor: TreeId, + clearedExternalTaint: bool, + } + type MutationEvent = { worktreeId: WorktreeId, revision: int, @@ -250,6 +257,7 @@ module mutation_cursor { var scopeHistory: Set[ScopeCheckpoint] var abandonHistory: Set[AbandonCheckpoint] var startHistory: Set[StartCheckpoint] + var recoveryHistory: Set[RecoveryCheckpoint] var taintHistory: Set[DurableStateCheckpoint] var evidenceAttempts: Set[AttemptId] var mutationEvents: Set[MutationEvent] @@ -382,6 +390,7 @@ module mutation_cursor { scopeHistory' = Set(), abandonHistory' = Set(), startHistory' = Set(), + recoveryHistory' = Set(), taintHistory' = Set(), evidenceAttempts' = Set(), mutationEvents' = Set(), @@ -401,6 +410,7 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + recoveryHistory' = recoveryHistory, taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -420,6 +430,7 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + recoveryHistory' = recoveryHistory, taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -453,6 +464,7 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + recoveryHistory' = recoveryHistory, taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -529,6 +541,7 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + recoveryHistory' = recoveryHistory, taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -659,6 +672,7 @@ module mutation_cursor { scopeHistory' = nextScopeHistory, abandonHistory' = abandonHistory, startHistory' = nextStartHistory, + recoveryHistory' = recoveryHistory, taintHistory' = taintHistory, evidenceAttempts' = nextEvidenceAttempts, mutationEvents' = mutationEvents.union(emitted), @@ -701,6 +715,7 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + recoveryHistory' = recoveryHistory, taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -726,6 +741,7 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + recoveryHistory' = recoveryHistory, taintHistory' = taintHistory.union(Set(durableStateFor(worktree))), evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -783,6 +799,7 @@ module mutation_cursor { cursorTree: state.cursorTree, })), startHistory' = startHistory, + recoveryHistory' = recoveryHistory, taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -852,6 +869,12 @@ module mutation_cursor { scopeHistory' = scopeHistory, abandonHistory' = abandonHistory, startHistory' = startHistory, + recoveryHistory' = recoveryHistory.union(Set({ + worktreeId: worktree, + observedTree: worktreeTrees.get(worktree), + resultingCursor: worktreeTrees.get(worktree), + clearedExternalTaint: externalTaint.contains(worktree), + })), taintHistory' = taintHistory, evidenceAttempts' = evidenceAttempts, mutationEvents' = mutationEvents, @@ -937,14 +960,11 @@ module mutation_cursor { attributionFor(worktree) == IneligibleUnscoped }) - val RecoveryClearsExternalTaintOnlyAfterBaseline = WORKTREES.forall(worktree => { - externalTaint.contains(worktree) or - cursorHistory.exists(checkpoint => - checkpoint.worktreeId == worktree and - checkpoint.revision == worktrees.get(worktree).revision and - checkpoint.cursorTree == worktrees.get(worktree).cursorTree - ) - }) + val RecoveryClearsExternalTaintOnlyAfterBaseline = + recoveryHistory.forall(checkpoint => + checkpoint.clearedExternalTaint implies + checkpoint.resultingCursor == checkpoint.observedTree + ) val ScopeActorIdentityIsStable = SCOPES.forall(scope => { scopes.get(scope).actorKind == scopeActor(scope) @@ -1173,7 +1193,7 @@ module mutation_cursor { // Uniqueness and cross-referencing checks over the growing history logs // (mutationEvents, cursorHistory, protocolHistory, scopeHistory, - // abandonHistory). These are the most expensive conjuncts to verify since + // abandonHistory, recoveryHistory). These are the most expensive conjuncts to verify since // their cost scales with trace depth, so they are split out to be checked // in their own CI job rather than serialized with the rest of Safety. val SafetyHistory = and { @@ -1480,6 +1500,12 @@ module mutation_cursor { .then(recover(WT0)) .expect(worktrees.get(WT0).cursorTree == Tree2) .expect(not(externalTaint.contains(WT0))) + .expect(recoveryHistory.exists(checkpoint => + checkpoint.worktreeId == WT0 and + checkpoint.observedTree == Tree2 and + checkpoint.resultingCursor == Tree2 and + checkpoint.clearedExternalTaint + )) .expect(scopes.get(Scope0).status == Abandoned) .expect(mutationEvents.size() == 0) .expect(evidenceAttempts.size() == 0) From e73d0a9e861fb98af920bce8ee843eaaec8d0518 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 11:59:49 +0200 Subject: [PATCH 13/16] spec: Strengthen mutation checkpoint evidence Ensure recovery taint clearing is backed by a recorded cursor checkpoint at the observed tree and recovery revision. Rename directional evidence properties and replace the abandonment-specific safety check with a general preceding protocol checkpoint invariant, keeping the model's claims aligned with what it proves. Co-authored-by: SCE --- spec/mutation_cursor.md | 2 +- spec/mutation_cursor.qnt | 29 +++++++++++++++++++---------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/spec/mutation_cursor.md b/spec/mutation_cursor.md index f86e3340..76a87959 100644 --- a/spec/mutation_cursor.md +++ b/spec/mutation_cursor.md @@ -134,7 +134,7 @@ The model includes safety properties covering: - recovery baseline before clearing external taint; - recovery abandoning active scopes; - closed and abandoned terminality; -- no exclusive evidence for an abandoned unobserved gap; +- mutation evidence has a trustworthy preceding protocol checkpoint; - same-actor and different-actor contention; - `AiExclusive` requiring exactly one active scope; - `AiContended` requiring multiple active scopes; diff --git a/spec/mutation_cursor.qnt b/spec/mutation_cursor.qnt index f4f77883..31f0f48a 100644 --- a/spec/mutation_cursor.qnt +++ b/spec/mutation_cursor.qnt @@ -91,8 +91,8 @@ module mutation_cursor { type RecoveryCheckpoint = { worktreeId: WorktreeId, + revision: int, observedTree: TreeId, - resultingCursor: TreeId, clearedExternalTaint: bool, } @@ -871,8 +871,8 @@ module mutation_cursor { startHistory' = startHistory, recoveryHistory' = recoveryHistory.union(Set({ worktreeId: worktree, + revision: state.revision + 1, observedTree: worktreeTrees.get(worktree), - resultingCursor: worktreeTrees.get(worktree), clearedExternalTaint: externalTaint.contains(worktree), })), taintHistory' = taintHistory, @@ -961,9 +961,13 @@ module mutation_cursor { }) val RecoveryClearsExternalTaintOnlyAfterBaseline = - recoveryHistory.forall(checkpoint => - checkpoint.clearedExternalTaint implies - checkpoint.resultingCursor == checkpoint.observedTree + recoveryHistory.forall(recovery => + recovery.clearedExternalTaint implies + cursorHistory.exists(cursor => + cursor.worktreeId == recovery.worktreeId and + cursor.revision == recovery.revision and + cursor.cursorTree == recovery.observedTree + ) ) val ScopeActorIdentityIsStable = SCOPES.forall(scope => { @@ -1130,7 +1134,10 @@ module mutation_cursor { event.attribution == AiContended implies event.activeScopes.size() >= 2 }) - val AbandonDoesNotEmitExclusiveEvidenceForUnobservedGap = + // This checks only that each mutation has a trustworthy preceding + // protocol checkpoint; it does not establish an abandonment or attribution + // fact. + val MutationEventsHaveTrustworthyPrecedingProtocolCheckpoint = mutationEvents.forall(event => { protocolHistory.exists(before => before.worktreeId == event.worktreeId and @@ -1152,7 +1159,9 @@ module mutation_cursor { ) ) - val StaleAttemptsDoNotCommitEvidence = ATTEMPTS.forall(id => { + // Directional only: rejected attempts have no evidence. This does not + // claim that every stale attempt is rejected. + val RejectedAttemptsDoNotEmitEvidence = ATTEMPTS.forall(id => { attempts.get(id).status != Rejected or not(evidenceAttempts.contains(id)) }) @@ -1188,7 +1197,7 @@ module mutation_cursor { DatabaseFailureDoesNotMutateDurableProtocolState, ExternalTaintNeverStrengthensAttribution, NeedsRebaselineSuppressesAttribution, - StaleAttemptsDoNotCommitEvidence, + RejectedAttemptsDoNotEmitEvidence, } // Uniqueness and cross-referencing checks over the growing history logs @@ -1207,6 +1216,7 @@ module mutation_cursor { AbandonHistoryUniquePerWorktreeRevisionScope, MutationEventsMatchCursorHistory, MutationEventsCrossOnlyTrustworthyProtocolStates, + MutationEventsHaveTrustworthyPrecedingProtocolCheckpoint, RecoveryClearsExternalTaintOnlyAfterBaseline, } @@ -1215,7 +1225,6 @@ module mutation_cursor { AttributionMatchesObservedScopes, AiExclusiveRequiresExactlyOneActiveScope, AiContendedRequiresMultipleActiveScopes, - AbandonDoesNotEmitExclusiveEvidenceForUnobservedGap, StartDoesNotAbandonExistingScopes, } @@ -1502,8 +1511,8 @@ module mutation_cursor { .expect(not(externalTaint.contains(WT0))) .expect(recoveryHistory.exists(checkpoint => checkpoint.worktreeId == WT0 and + checkpoint.revision == worktrees.get(WT0).revision and checkpoint.observedTree == Tree2 and - checkpoint.resultingCursor == Tree2 and checkpoint.clearedExternalTaint )) .expect(scopes.get(Scope0).status == Abandoned) From 1985fb7ebdb57b9de930157b4a9bc57c60a598ba Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 12:20:02 +0200 Subject: [PATCH 14/16] spec: Refine mutation evidence invariants Align the mutation cursor model with properties it can establish: bound the documentation's verification claim by configured depth, remove the checkpoint invariant that overstates temporal evidence, and clarify that rejected attempts do not commit evidence. Co-authored-by: SCE --- spec/mutation_cursor.md | 3 +-- spec/mutation_cursor.qnt | 22 ++++------------------ 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/spec/mutation_cursor.md b/spec/mutation_cursor.md index 76a87959..01f526a1 100644 --- a/spec/mutation_cursor.md +++ b/spec/mutation_cursor.md @@ -4,7 +4,7 @@ ## Bounded verification domain -The enum values for worktrees, scopes, trees, hook events, and attempts are finite verification identities. They are not runtime limits. The model verifies arbitrary interleavings within this domain; production code must support larger and unbounded identifier spaces. +The enum values for worktrees, scopes, trees, hook events, and attempts are finite verification identities. They are not runtime limits. The model explores arbitrary interleavings within this finite domain up to the configured verification depth; production code must support larger and unbounded identifier spaces. `ScopeId` is the durable identity of an AI scope/session in this model. `ActorKind` identifies the harness. A separate `SessionId` is unnecessary unless one session can own multiple independent scopes. @@ -134,7 +134,6 @@ The model includes safety properties covering: - recovery baseline before clearing external taint; - recovery abandoning active scopes; - closed and abandoned terminality; -- mutation evidence has a trustworthy preceding protocol checkpoint; - same-actor and different-actor contention; - `AiExclusive` requiring exactly one active scope; - `AiContended` requiring multiple active scopes; diff --git a/spec/mutation_cursor.qnt b/spec/mutation_cursor.qnt index 31f0f48a..c6ce9159 100644 --- a/spec/mutation_cursor.qnt +++ b/spec/mutation_cursor.qnt @@ -1134,19 +1134,6 @@ module mutation_cursor { event.attribution == AiContended implies event.activeScopes.size() >= 2 }) - // This checks only that each mutation has a trustworthy preceding - // protocol checkpoint; it does not establish an abandonment or attribution - // fact. - val MutationEventsHaveTrustworthyPrecedingProtocolCheckpoint = - mutationEvents.forall(event => { - protocolHistory.exists(before => - before.worktreeId == event.worktreeId and - before.revision == event.revision - 1 and - before.cursorTree == event.beforeTree and - not(before.needsRebaseline) - ) - }) - val StartDoesNotAbandonExistingScopes = startHistory.forall(start => start.preservedScopes.forall(scope => @@ -1159,9 +1146,9 @@ module mutation_cursor { ) ) - // Directional only: rejected attempts have no evidence. This does not - // claim that every stale attempt is rejected. - val RejectedAttemptsDoNotEmitEvidence = ATTEMPTS.forall(id => { + // Directional only: rejected attempts do not commit evidence. This does + // not claim that every stale attempt is rejected. + val RejectedAttemptsDoNotCommitEvidence = ATTEMPTS.forall(id => { attempts.get(id).status != Rejected or not(evidenceAttempts.contains(id)) }) @@ -1197,7 +1184,7 @@ module mutation_cursor { DatabaseFailureDoesNotMutateDurableProtocolState, ExternalTaintNeverStrengthensAttribution, NeedsRebaselineSuppressesAttribution, - RejectedAttemptsDoNotEmitEvidence, + RejectedAttemptsDoNotCommitEvidence, } // Uniqueness and cross-referencing checks over the growing history logs @@ -1216,7 +1203,6 @@ module mutation_cursor { AbandonHistoryUniquePerWorktreeRevisionScope, MutationEventsMatchCursorHistory, MutationEventsCrossOnlyTrustworthyProtocolStates, - MutationEventsHaveTrustworthyPrecedingProtocolCheckpoint, RecoveryClearsExternalTaintOnlyAfterBaseline, } From 136f6e937bbab176428d7dcc5d78ff4747628f48 Mon Sep 17 00:00:00 2001 From: David Abram Date: Tue, 25 Aug 2026 22:10:23 +0200 Subject: [PATCH 15/16] CI+spec: Add bounded Quint verification actions CI now reruns Quint verification when the model, workflow, or Nix inputs change and uses the enabled-action `verifyStep` with representative identity domains to keep bounded checks tractable. The model separates durable protocol checkpoints from verification-only history, factors guarded actions into reusable helpers, and adds coverage for taint recovery across worktrees. Co-authored-by: SCE --- .github/workflows/quint.yml | 10 +- spec/mutation_cursor.md | 6 +- spec/mutation_cursor.qnt | 577 +++++++++++++++++++++--------------- 3 files changed, 356 insertions(+), 237 deletions(-) diff --git a/.github/workflows/quint.yml b/.github/workflows/quint.yml index be777c12..0d21234c 100644 --- a/.github/workflows/quint.yml +++ b/.github/workflows/quint.yml @@ -40,7 +40,7 @@ jobs: exit 0 fi - if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '\.qnt$'; then + if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '(\.qnt$|^\.github/workflows/quint\.yml$|^flake\.nix$|^flake\.lock$)'; then echo "quint=true" >> "$GITHUB_OUTPUT" else echo "quint=false" >> "$GITHUB_OUTPUT" @@ -85,6 +85,7 @@ jobs: needs: detect if: needs.detect.outputs.quint == 'true' runs-on: ubuntu-latest + # SafetyCore at depth 10 still exceeds 15 minutes locally. timeout-minutes: 30 strategy: fail-fast: false @@ -119,7 +120,12 @@ jobs: use-gha-cache: true - name: Verify ${{ matrix.invariant }} - run: nix run .#quint -- verify spec/mutation_cursor.qnt --invariant=${{ matrix.invariant }} --max-steps=${{ matrix.max_steps }} + run: > + nix run .#quint -- + verify spec/mutation_cursor.qnt + --step=verifyStep + --invariant=${{ matrix.invariant }} + --max-steps=${{ matrix.max_steps }} gate: name: Quint gate diff --git a/spec/mutation_cursor.md b/spec/mutation_cursor.md index 01f526a1..7bb5e0d7 100644 --- a/spec/mutation_cursor.md +++ b/spec/mutation_cursor.md @@ -4,7 +4,9 @@ ## Bounded verification domain -The enum values for worktrees, scopes, trees, hook events, and attempts are finite verification identities. They are not runtime limits. The model explores arbitrary interleavings within this finite domain up to the configured verification depth; production code must support larger and unbounded identifier spaces. +The enum values for worktrees, scopes, trees, hook events, and attempts are finite verification identities. They are not runtime limits. The model explores arbitrary interleavings within this finite domain up to the configured verification depth; production code must support larger and unbounded identifier spaces. CI's symbolic verification uses representative subsets of attempts, events, and trees (`Attempt0..2`, `Event0..2`, and `Tree0..2`) to reduce symmetric search; general simulation and deterministic tests retain access to the complete canonical domains. + +CI selects `verifyStep`, which includes only enabled protocol actions, excludes no-op mutations, and omits explicit stuttering. The canonical `step` remains available for unrestricted simulation. Because the CI identity subsets are smaller, the bounded CI claim applies to those representative domains rather than every canonical identity. `ScopeId` is the durable identity of an AI scope/session in this model. `ActorKind` identifies the harness. A separate `SessionId` is unnecessary unless one session can own multiple independent scopes. @@ -33,7 +35,7 @@ Hook replay identity is scoped by `ScopeId` and `EventId` through `EventKey`. Th ## Failure and durability boundary -`worktrees.cursorTree`, `worktrees.revision`, scope state, `processedEvents`, and `mutationEvents` represent state durably stored in the Agent Trace database. `worktrees.needsRebaseline` is a durable protocol marker for an ambiguous cursor interval; it is distinct from both snapshot failure and external database taint. +`worktrees.cursorTree`, `worktrees.revision`, scope state, `processedEvents`, and `mutationEvents` represent state durably stored in the Agent Trace database. `worktrees.needsRebaseline` is a durable protocol marker for an ambiguous cursor interval; it is distinct from both snapshot failure and external database taint. Verification-only histories and attempt bookkeeping are intentionally excluded from database-failure checkpoints. A snapshot failure occurs while the database is healthy. `taint(worktree)` therefore records `SnapshotFailure` in the durable worktree state and increments the worktree revision. This invalidates speculative attempts that were already prepared before the failure. It does not quarantine later attempts: once a subsequent snapshot is prepared against the tainted state and the normal freshness checks pass, it may advance the cursor. Because failure states weaken attribution, any evidence emitted while tainted is `IneligibleUnscoped` until recovery. diff --git a/spec/mutation_cursor.qnt b/spec/mutation_cursor.qnt index c6ce9159..05dd643f 100644 --- a/spec/mutation_cursor.qnt +++ b/spec/mutation_cursor.qnt @@ -108,19 +108,11 @@ module mutation_cursor { boundary: Boundary, } - type DurableStateCheckpoint = { + type DurableProtocolCheckpoint = { worktreeId: WorktreeId, worktree: WorktreeState, scopes: ScopeId -> ScopeState, processedEvents: Set[EventKey], - scopeStartCount: ScopeId -> int, - everTerminal: Set[ScopeId], - cursorHistory: Set[CursorCheckpoint], - protocolHistory: Set[ProtocolCheckpoint], - scopeHistory: Set[ScopeCheckpoint], - abandonHistory: Set[AbandonCheckpoint], - startHistory: Set[StartCheckpoint], - evidenceAttempts: Set[AttemptId], mutationEvents: Set[MutationEvent], } @@ -148,6 +140,14 @@ module mutation_cursor { Attempt5 ) + // Representative identities used by symbolic verification. General + // simulation and deterministic scenarios retain the canonical domains above. + val VERIFY_ATTEMPTS: Set[AttemptId] = Set(Attempt0, Attempt1, Attempt2) + val VERIFY_EVENTS: Set[EventId] = Set(Event0, Event1, Event2) + val VERIFY_TREES: Set[TreeId] = Set(Tree0, Tree1, Tree2) + val VERIFY_SCOPES: Set[ScopeId] = SCOPES + val VERIFY_WORKTREES: Set[WorktreeId] = WORKTREES + pure def scopeWorktree(scope: ScopeId): WorktreeId = match scope { | Scope0 => WT0 @@ -258,7 +258,7 @@ module mutation_cursor { var abandonHistory: Set[AbandonCheckpoint] var startHistory: Set[StartCheckpoint] var recoveryHistory: Set[RecoveryCheckpoint] - var taintHistory: Set[DurableStateCheckpoint] + var taintHistory: Set[DurableProtocolCheckpoint] var evidenceAttempts: Set[AttemptId] var mutationEvents: Set[MutationEvent] @@ -268,7 +268,7 @@ module mutation_cursor { state.worktreeId == worktree and isLive(state.status) }) - def durableStateFor(worktree: WorktreeId): DurableStateCheckpoint = { + def durableProtocolStateFor(worktree: WorktreeId): DurableProtocolCheckpoint = { worktreeId: worktree, worktree: worktrees.get(worktree), scopes: SCOPES.filter(scope => scopeWorktree(scope) == worktree).mapBy( @@ -277,28 +277,6 @@ module mutation_cursor { processedEvents: processedEvents.filter(event => scopeWorktree(event.scopeId) == worktree ), - scopeStartCount: SCOPES.filter(scope => scopeWorktree(scope) == worktree).mapBy( - scope => scopeStartCount.get(scope) - ), - everTerminal: everTerminal.filter(scope => scopeWorktree(scope) == worktree), - cursorHistory: cursorHistory.filter(checkpoint => - checkpoint.worktreeId == worktree - ), - protocolHistory: protocolHistory.filter(checkpoint => - checkpoint.worktreeId == worktree - ), - scopeHistory: scopeHistory.filter(checkpoint => - checkpoint.worktreeId == worktree - ), - abandonHistory: abandonHistory.filter(checkpoint => - checkpoint.worktreeId == worktree - ), - startHistory: startHistory.filter(checkpoint => - checkpoint.worktreeId == worktree - ), - evidenceAttempts: evidenceAttempts.filter(attempt => - boundaryWorktree(attempts.get(attempt).boundary) == worktree - ), mutationEvents: mutationEvents.filter(event => event.worktreeId == worktree ), @@ -436,41 +414,43 @@ module mutation_cursor { mutationEvents' = mutationEvents, } - action prepare(attempt: AttemptId, boundary: Boundary): bool = { - val oldAttempt = attempts.get(attempt) + action prepareAvailable(attempt: AttemptId, boundary: Boundary): bool = { + val worktree = boundaryWorktree(boundary) + val state = worktrees.get(worktree) - if (oldAttempt.status != Available) { + all { + worktrees' = worktrees, + scopes' = scopes, + worktreeTrees' = worktreeTrees, + externalTaint' = externalTaint, + processedEvents' = processedEvents, + scopeStartCount' = scopeStartCount, + everTerminal' = everTerminal, + attempts' = attempts.set(attempt, { + status: Prepared, + boundary: boundary, + expectedRevision: state.revision, + beforeTree: state.cursorTree, + afterTree: worktreeTrees.get(worktree), + }), + cursorHistory' = cursorHistory, + protocolHistory' = protocolHistory, + scopeHistory' = scopeHistory, + abandonHistory' = abandonHistory, + startHistory' = startHistory, + recoveryHistory' = recoveryHistory, + taintHistory' = taintHistory, + evidenceAttempts' = evidenceAttempts, + mutationEvents' = mutationEvents, + } + } + + action prepare(attempt: AttemptId, boundary: Boundary): bool = + if (attempts.get(attempt).status != Available) { stutter } else { - val worktree = boundaryWorktree(boundary) - val state = worktrees.get(worktree) - all { - worktrees' = worktrees, - scopes' = scopes, - worktreeTrees' = worktreeTrees, - externalTaint' = externalTaint, - processedEvents' = processedEvents, - scopeStartCount' = scopeStartCount, - everTerminal' = everTerminal, - attempts' = attempts.set(attempt, { - status: Prepared, - boundary: boundary, - expectedRevision: state.revision, - beforeTree: state.cursorTree, - afterTree: worktreeTrees.get(worktree), - }), - cursorHistory' = cursorHistory, - protocolHistory' = protocolHistory, - scopeHistory' = scopeHistory, - abandonHistory' = abandonHistory, - startHistory' = startHistory, - recoveryHistory' = recoveryHistory, - taintHistory' = taintHistory, - evidenceAttempts' = evidenceAttempts, - mutationEvents' = mutationEvents, - } + prepareAvailable(attempt, boundary) } - } action commitAttempt(attempt: AttemptId): bool = { val planned = attempts.get(attempt) @@ -680,130 +660,214 @@ module mutation_cursor { } } - action taint(worktree: WorktreeId): bool = { + action taintHealthy(worktree: WorktreeId): bool = { val state = worktrees.get(worktree) - if (state.tainted or externalTaint.contains(worktree)) { + all { + worktrees' = worktrees.set(worktree, { + cursorTree: state.cursorTree, + revision: state.revision + 1, + tainted: true, + failureKind: SnapshotFailure, + needsRebaseline: state.needsRebaseline, + }), + scopes' = scopes, + worktreeTrees' = worktreeTrees, + externalTaint' = externalTaint, + processedEvents' = processedEvents, + scopeStartCount' = scopeStartCount, + everTerminal' = everTerminal, + attempts' = attempts, + cursorHistory' = cursorHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: state.cursorTree, + })), + protocolHistory' = protocolHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: state.cursorTree, + needsRebaseline: state.needsRebaseline, + })), + scopeHistory' = scopeHistory, + abandonHistory' = abandonHistory, + startHistory' = startHistory, + recoveryHistory' = recoveryHistory, + taintHistory' = taintHistory, + evidenceAttempts' = evidenceAttempts, + mutationEvents' = mutationEvents, + } + } + + action taint(worktree: WorktreeId): bool = + if ( + worktrees.get(worktree).tainted or + externalTaint.contains(worktree) + ) { stutter } else { - all { - worktrees' = worktrees.set(worktree, { - cursorTree: state.cursorTree, - revision: state.revision + 1, - tainted: true, - failureKind: SnapshotFailure, - needsRebaseline: state.needsRebaseline, - }), - scopes' = scopes, - worktreeTrees' = worktreeTrees, - externalTaint' = externalTaint, - processedEvents' = processedEvents, - scopeStartCount' = scopeStartCount, - everTerminal' = everTerminal, - attempts' = attempts, - cursorHistory' = cursorHistory.union(Set({ - worktreeId: worktree, - revision: state.revision + 1, - cursorTree: state.cursorTree, - })), - protocolHistory' = protocolHistory.union(Set({ - worktreeId: worktree, - revision: state.revision + 1, - cursorTree: state.cursorTree, - needsRebaseline: state.needsRebaseline, - })), - scopeHistory' = scopeHistory, - abandonHistory' = abandonHistory, - startHistory' = startHistory, - recoveryHistory' = recoveryHistory, - taintHistory' = taintHistory, - evidenceAttempts' = evidenceAttempts, - mutationEvents' = mutationEvents, - } + taintHealthy(worktree) } + + action recordDatabaseFailure(worktree: WorktreeId): bool = all { + worktrees' = worktrees, + scopes' = scopes, + worktreeTrees' = worktreeTrees, + externalTaint' = externalTaint.union(Set(worktree)), + processedEvents' = processedEvents, + scopeStartCount' = scopeStartCount, + everTerminal' = everTerminal, + attempts' = attempts, + cursorHistory' = cursorHistory, + protocolHistory' = protocolHistory, + scopeHistory' = scopeHistory, + abandonHistory' = abandonHistory, + startHistory' = startHistory, + recoveryHistory' = recoveryHistory, + taintHistory' = taintHistory.union(Set(durableProtocolStateFor(worktree))), + evidenceAttempts' = evidenceAttempts, + mutationEvents' = mutationEvents, } action databaseFailure(worktree: WorktreeId): bool = if (externalTaint.contains(worktree)) { stutter } else { - all { - worktrees' = worktrees, - scopes' = scopes, - worktreeTrees' = worktreeTrees, - externalTaint' = externalTaint.union(Set(worktree)), - processedEvents' = processedEvents, - scopeStartCount' = scopeStartCount, - everTerminal' = everTerminal, - attempts' = attempts, - cursorHistory' = cursorHistory, - protocolHistory' = protocolHistory, - scopeHistory' = scopeHistory, - abandonHistory' = abandonHistory, - startHistory' = startHistory, - recoveryHistory' = recoveryHistory, - taintHistory' = taintHistory.union(Set(durableStateFor(worktree))), - evidenceAttempts' = evidenceAttempts, - mutationEvents' = mutationEvents, - } + recordDatabaseFailure(worktree) } - action abandon(scope: ScopeId): bool = { + action abandonLiveScope(scope: ScopeId): bool = { val oldScope = scopes.get(scope) val worktree = oldScope.worktreeId val state = worktrees.get(worktree) - if (not(isLive(oldScope.status)) or externalTaint.contains(worktree)) { + all { + worktrees' = worktrees.set(worktree, { + cursorTree: state.cursorTree, + revision: state.revision + 1, + tainted: state.tainted, + failureKind: state.failureKind, + needsRebaseline: true, + }), + scopes' = scopes.set(scope, { + status: Abandoned, + actorKind: oldScope.actorKind, + worktreeId: oldScope.worktreeId, + }), + worktreeTrees' = worktreeTrees, + externalTaint' = externalTaint, + processedEvents' = processedEvents, + scopeStartCount' = scopeStartCount, + everTerminal' = everTerminal.union(Set(scope)), + attempts' = attempts, + cursorHistory' = cursorHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: state.cursorTree, + })), + protocolHistory' = protocolHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: state.cursorTree, + needsRebaseline: true, + })), + scopeHistory' = scopeHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + scope: scope, + status: Abandoned, + })), + abandonHistory' = abandonHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + scope: scope, + cursorTree: state.cursorTree, + })), + startHistory' = startHistory, + recoveryHistory' = recoveryHistory, + taintHistory' = taintHistory, + evidenceAttempts' = evidenceAttempts, + mutationEvents' = mutationEvents, + } + } + + action abandon(scope: ScopeId): bool = { + val oldScope = scopes.get(scope) + + if ( + not(isLive(oldScope.status)) or + externalTaint.contains(oldScope.worktreeId) + ) { stutter } else { - all { - worktrees' = worktrees.set(worktree, { - cursorTree: state.cursorTree, - revision: state.revision + 1, - tainted: state.tainted, - failureKind: state.failureKind, - needsRebaseline: true, - }), - scopes' = scopes.set(scope, { - status: Abandoned, - actorKind: oldScope.actorKind, - worktreeId: oldScope.worktreeId, - }), - worktreeTrees' = worktreeTrees, - externalTaint' = externalTaint, - processedEvents' = processedEvents, - scopeStartCount' = scopeStartCount, - everTerminal' = everTerminal.union(Set(scope)), - attempts' = attempts, - cursorHistory' = cursorHistory.union(Set({ - worktreeId: worktree, - revision: state.revision + 1, - cursorTree: state.cursorTree, - })), - protocolHistory' = protocolHistory.union(Set({ - worktreeId: worktree, - revision: state.revision + 1, - cursorTree: state.cursorTree, - needsRebaseline: true, - })), - scopeHistory' = scopeHistory.union(Set({ - worktreeId: worktree, - revision: state.revision + 1, - scope: scope, - status: Abandoned, - })), - abandonHistory' = abandonHistory.union(Set({ - worktreeId: worktree, - revision: state.revision + 1, - scope: scope, - cursorTree: state.cursorTree, - })), - startHistory' = startHistory, - recoveryHistory' = recoveryHistory, - taintHistory' = taintHistory, - evidenceAttempts' = evidenceAttempts, - mutationEvents' = mutationEvents, - } + abandonLiveScope(scope) + } + } + + action recoverNeeded(worktree: WorktreeId): bool = { + val state = worktrees.get(worktree) + val abandonLiveScopes = state.tainted or externalTaint.contains(worktree) + + all { + worktrees' = worktrees.set(worktree, { + cursorTree: worktreeTrees.get(worktree), + revision: state.revision + 1, + tainted: false, + failureKind: Healthy, + needsRebaseline: false, + }), + scopes' = if (abandonLiveScopes) { + SCOPES.mapBy(scopeId => { + val oldScope = scopes.get(scopeId) + if ( + oldScope.worktreeId == worktree and + isLive(oldScope.status) + ) { + { + status: Abandoned, + actorKind: oldScope.actorKind, + worktreeId: oldScope.worktreeId, + } + } else { + oldScope + } + }) + } else { + scopes + }, + worktreeTrees' = worktreeTrees, + externalTaint' = externalTaint.exclude(Set(worktree)), + processedEvents' = processedEvents, + scopeStartCount' = scopeStartCount, + everTerminal' = if (abandonLiveScopes) { + everTerminal.union(liveScopesOn(worktree)) + } else { + everTerminal + }, + attempts' = attempts, + cursorHistory' = cursorHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: worktreeTrees.get(worktree), + })), + protocolHistory' = protocolHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + cursorTree: worktreeTrees.get(worktree), + needsRebaseline: false, + })), + scopeHistory' = scopeHistory, + abandonHistory' = abandonHistory, + startHistory' = startHistory, + recoveryHistory' = recoveryHistory.union(Set({ + worktreeId: worktree, + revision: state.revision + 1, + observedTree: worktreeTrees.get(worktree), + clearedExternalTaint: externalTaint.contains(worktree), + })), + taintHistory' = taintHistory, + evidenceAttempts' = evidenceAttempts, + mutationEvents' = mutationEvents, } } @@ -817,68 +881,7 @@ module mutation_cursor { ) { stutter } else { - val abandonLiveScopes = state.tainted or externalTaint.contains(worktree) - all { - worktrees' = worktrees.set(worktree, { - cursorTree: worktreeTrees.get(worktree), - revision: state.revision + 1, - tainted: false, - failureKind: Healthy, - needsRebaseline: false, - }), - scopes' = if (abandonLiveScopes) { - SCOPES.mapBy(scopeId => { - val oldScope = scopes.get(scopeId) - if ( - oldScope.worktreeId == worktree and - isLive(oldScope.status) - ) { - { - status: Abandoned, - actorKind: oldScope.actorKind, - worktreeId: oldScope.worktreeId, - } - } else { - oldScope - } - }) - } else { - scopes - }, - worktreeTrees' = worktreeTrees, - externalTaint' = externalTaint.exclude(Set(worktree)), - processedEvents' = processedEvents, - scopeStartCount' = scopeStartCount, - everTerminal' = if (abandonLiveScopes) { - everTerminal.union(liveScopesOn(worktree)) - } else { - everTerminal - }, - attempts' = attempts, - cursorHistory' = cursorHistory.union(Set({ - worktreeId: worktree, - revision: state.revision + 1, - cursorTree: worktreeTrees.get(worktree), - })), - protocolHistory' = protocolHistory.union(Set({ - worktreeId: worktree, - revision: state.revision + 1, - cursorTree: worktreeTrees.get(worktree), - needsRebaseline: false, - })), - scopeHistory' = scopeHistory, - abandonHistory' = abandonHistory, - startHistory' = startHistory, - recoveryHistory' = recoveryHistory.union(Set({ - worktreeId: worktree, - revision: state.revision + 1, - observedTree: worktreeTrees.get(worktree), - clearedExternalTaint: externalTaint.contains(worktree), - })), - taintHistory' = taintHistory, - evidenceAttempts' = evidenceAttempts, - mutationEvents' = mutationEvents, - } + recoverNeeded(worktree) } } @@ -926,6 +929,104 @@ module mutation_cursor { abandon(scope) } + action verifyMutate: bool = { + nondet worktree = VERIFY_WORKTREES.oneOf() + nondet tree = VERIFY_TREES.oneOf() + + all { + tree != worktreeTrees.get(worktree), + mutate(worktree, tree), + } + } + + action verifyPrepareHook: bool = { + nondet attempt = VERIFY_ATTEMPTS.oneOf() + nondet scope = VERIFY_SCOPES.oneOf() + nondet event = VERIFY_EVENTS.oneOf() + + all { + attempts.get(attempt).status == Available, + any { + prepareAvailable(attempt, Start({ scope: scope, event: event })), + prepareAvailable(attempt, Advance({ scope: scope, event: event })), + prepareAvailable(attempt, Close({ scope: scope, event: event })), + }, + } + } + + action verifyPrepareFlush: bool = { + nondet attempt = VERIFY_ATTEMPTS.oneOf() + nondet worktree = VERIFY_WORKTREES.oneOf() + + all { + attempts.get(attempt).status == Available, + prepareAvailable(attempt, Flush(worktree)), + } + } + + action verifyCommit: bool = { + nondet attempt = VERIFY_ATTEMPTS.oneOf() + + all { + attempts.get(attempt).status == Prepared, + commitAttempt(attempt), + } + } + + action verifyTaint: bool = { + nondet worktree = VERIFY_WORKTREES.oneOf() + val state = worktrees.get(worktree) + + all { + not(state.tainted), + not(externalTaint.contains(worktree)), + taintHealthy(worktree), + } + } + + action verifyRecover: bool = { + nondet worktree = VERIFY_WORKTREES.oneOf() + val state = worktrees.get(worktree) + + all { + state.tainted or + externalTaint.contains(worktree) or + state.needsRebaseline, + recoverNeeded(worktree), + } + } + + action verifyDatabaseFailure: bool = { + nondet worktree = VERIFY_WORKTREES.oneOf() + + all { + not(externalTaint.contains(worktree)), + recordDatabaseFailure(worktree), + } + } + + action verifyAbandon: bool = { + nondet scope = VERIFY_SCOPES.oneOf() + val state = scopes.get(scope) + + all { + isLive(state.status), + not(externalTaint.contains(state.worktreeId)), + abandonLiveScope(scope), + } + } + + action verifyStep: bool = any { + verifyMutate, + verifyPrepareHook, + verifyPrepareFlush, + verifyCommit, + verifyTaint, + verifyRecover, + verifyDatabaseFailure, + verifyAbandon, + } + action step: bool = any { randomMutate, randomPrepare, @@ -952,7 +1053,7 @@ module mutation_cursor { val DatabaseFailureDoesNotMutateDurableProtocolState = externalTaint.forall(worktree => - taintHistory.contains(durableStateFor(worktree)) + taintHistory.contains(durableProtocolStateFor(worktree)) ) val ExternalTaintNeverStrengthensAttribution = WORKTREES.forall(worktree => { @@ -1483,7 +1584,7 @@ module mutation_cursor { .expect(scopes.get(Scope0).status == Active) .expect(abandonHistory.size() == 0) .expect(externalTaint.contains(WT0)) - .expect(taintHistory.contains(durableStateFor(WT0))) + .expect(taintHistory.contains(durableProtocolStateFor(WT0))) .expect(Safety) run testExternalTaintRecoveryAbandonsScopeAndBaselines = @@ -1621,6 +1722,16 @@ module mutation_cursor { .then(commitAttempt(Attempt2)) .expect(scopes.get(Scope0).status == Active) .expect(scopes.get(Scope3).status == Active) + .expect(worktrees.get(WT0).cursorTree == Tree1) + .expect(worktrees.get(WT0).revision == 2) .expect(worktrees.get(WT1).cursorTree == Tree0) + .then(taint(WT1)) + .then(mutate(WT1, Tree2)) + .then(recover(WT1)) + .expect(worktrees.get(WT0).cursorTree == Tree1) + .expect(worktrees.get(WT0).revision == 2) + .expect(scopes.get(Scope0).status == Active) + .expect(worktrees.get(WT1).cursorTree == Tree2) + .expect(scopes.get(Scope3).status == Abandoned) .expect(Safety) } From 7fd93316aad7a6398c94a04405b58f67338ebb9a Mon Sep 17 00:00:00 2001 From: David Abram Date: Wed, 26 Aug 2026 08:44:50 +0200 Subject: [PATCH 16/16] CI: Separate deep Quint verification from required checks Keep pull-request Quint validation within a bounded runtime by moving symbolic safety verification to a scheduled/manual workflow. Run randomized safety checks in the required workflow and update change detection and gate handling for the new job layout. This keeps deep verification available without blocking routine PR validation. Co-authored-by: SCE --- .github/workflows/quint-deep-verify.yml | 56 +++++++++++++++ .github/workflows/quint.yml | 91 ++++++++----------------- 2 files changed, 86 insertions(+), 61 deletions(-) create mode 100644 .github/workflows/quint-deep-verify.yml diff --git a/.github/workflows/quint-deep-verify.yml b/.github/workflows/quint-deep-verify.yml new file mode 100644 index 00000000..3c426c88 --- /dev/null +++ b/.github/workflows/quint-deep-verify.yml @@ -0,0 +1,56 @@ +name: Quint Deep Verification + +on: + workflow_dispatch: + schedule: + - cron: "17 3 * * *" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + quint-verify: + name: Verify Quint safety invariant (${{ matrix.invariant }}) + runs-on: ubuntu-latest + # Symbolic bounded verification is intentionally outside the required PR path. + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + # These bounds reflect the model-checking cost of each invariant group. + include: + - invariant: SafetyCore + max_steps: 10 + - invariant: SafetyAttribution + max_steps: 8 + - invariant: SafetyHistory + max_steps: 5 + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 + + - name: Enable Magic Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@908b263ff629f4cc17666315b7fd3ec127c6244d # v14 + with: + use-flakehub: false + use-gha-cache: true + + - name: Verify ${{ matrix.invariant }} + run: > + nix run .#quint -- + verify spec/mutation_cursor.qnt + --step=verifyStep + --invariant=${{ matrix.invariant }} + --max-steps=${{ matrix.max_steps }} diff --git a/.github/workflows/quint.yml b/.github/workflows/quint.yml index 0d21234c..d9639cb0 100644 --- a/.github/workflows/quint.yml +++ b/.github/workflows/quint.yml @@ -40,22 +40,22 @@ jobs: exit 0 fi - if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '(\.qnt$|^\.github/workflows/quint\.yml$|^flake\.nix$|^flake\.lock$)'; then + if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '(\.qnt$|^\.github/workflows/quint\.yml$|^\.github/workflows/quint-deep-verify\.yml$|^flake\.nix$|^flake\.lock$)'; then echo "quint=true" >> "$GITHUB_OUTPUT" else echo "quint=false" >> "$GITHUB_OUTPUT" fi - - name: No Quint changes + - name: No Quint-relevant changes if: steps.changes.outputs.quint != 'true' - run: echo "No Quint specification changes detected; validation skipped." + run: echo "No Quint-relevant changes; validation skipped." - quint: + quint-checks: name: Quint checks needs: detect if: needs.detect.outputs.quint == 'true' runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 15 steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 @@ -80,79 +80,48 @@ jobs: - name: Run Quint tests run: nix run .#quint -- test spec/mutation_cursor.qnt - quint-verify: - name: Verify Quint safety invariant (${{ matrix.invariant }}) - needs: detect - if: needs.detect.outputs.quint == 'true' - runs-on: ubuntu-latest - # SafetyCore at depth 10 still exceeds 15 minutes locally. - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - # SafetyHistory does nested forall/exists over history logs that grow - # every step, so its BMC cost blows up with trace depth (this is what - # timed out the single combined Safety invariant at step 4). It gets - # a shorter trace bound; the cheaper groups keep the full default depth. - include: - - invariant: SafetyCore - max_steps: 10 - - invariant: SafetyAttribution - max_steps: 8 - - invariant: SafetyHistory - max_steps: 5 - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22 - - - name: Enable Magic Nix Cache - uses: DeterminateSystems/magic-nix-cache-action@908b263ff629f4cc17666315b7fd3ec127c6244d # v14 - with: - use-flakehub: false - use-gha-cache: true - - - name: Verify ${{ matrix.invariant }} + - name: Randomized Quint safety check run: > nix run .#quint -- - verify spec/mutation_cursor.qnt + run spec/mutation_cursor.qnt --step=verifyStep - --invariant=${{ matrix.invariant }} - --max-steps=${{ matrix.max_steps }} + --invariants SafetyCore SafetyAttribution SafetyHistory + --max-samples=5000 + --max-steps=20 gate: name: Quint gate if: always() needs: - detect - - quint - - quint-verify + - quint-checks runs-on: ubuntu-latest steps: - name: Check Quint result env: CHANGED: ${{ needs.detect.outputs.quint }} - CHECK_RESULT: ${{ needs.quint.result }} - VERIFY_RESULT: ${{ needs.quint-verify.result }} + DETECT_RESULT: ${{ needs.detect.result }} + CHECK_RESULT: ${{ needs.quint-checks.result }} run: | - if [ "$CHANGED" != "true" ]; then - echo "No Quint changes." - exit 0 - fi - - if [ "$CHECK_RESULT" != "success" ]; then - echo "Quint typecheck/tests failed." + if [ "$DETECT_RESULT" != "success" ]; then + echo "Quint change detection failed (result: $DETECT_RESULT)." exit 1 fi - if [ "$VERIFY_RESULT" != "success" ]; then - echo "Quint safety verification failed." + case "$CHANGED" in + false) + echo "No Quint-relevant changes; validation skipped." + exit 0 + ;; + true) + ;; + *) + echo "Quint change detection produced an invalid result: $CHANGED" + exit 1 + ;; + esac + + if [ "$CHECK_RESULT" != "success" ]; then + echo "Quint validation failed (result: $CHECK_RESULT)." exit 1 fi