"""venomqa llm-docs — Print a complete LLM context document.

Run `venomqa llm-docs` and paste the output into any AI assistant (Claude,
ChatGPT, Cursor, Copilot) so it knows the exact VenomQA API and can help
you write tests without hallucinating wrong signatures.
"""

from __future__ import annotations

import click

# ---------------------------------------------------------------------------
# The document itself — plain text so it pastes cleanly into any LLM chat
# ---------------------------------------------------------------------------

LLM_CONTEXT = """\
# VenomQA — Complete LLM Context Document
# Paste this into any AI assistant before asking for help writing VenomQA tests.
# Generated by: venomqa llm-docs

## What VenomQA is

VenomQA is an autonomous API QA agent. Instead of writing linear test scripts,
you define:
  - Actions  — callables that call your API
  - Invariants — rules that must always hold after every action

VenomQA then exhaustively explores every reachable sequence of actions using
BFS (breadth-first search), checkpointing and rolling back state between
branches. Every path through the action graph starts from a clean state.

---

## CRITICAL: Why You Need Database Access

VenomQA explores state graphs by BRANCHING:

    State S0 ──[create_order]──▶ S1
       │                         │
       │                    [cancel]──▶ S2 (branch A)
       │                         │
       │                    [refund]──▶ S3 (branch B) ← BUG FOUND!
       │
       └──[list_orders]──▶ S4 (branch C)

To explore both branch A and B from S1, VenomQA must:
  1. Execute create_order → reach S1
  2. Execute cancel → reach S2
  3. ROLLBACK DATABASE TO S1 ← This is why you need DB access!
  4. Execute refund → reach S3 (finds the bug)

WITHOUT database rollback, VenomQA can only test ONE linear path.

### WHICH database to connect?

The EXACT SAME database your API writes to:
  - Your API uses postgresql://prod:5432/myapp? Connect VenomQA to that URL
  - VenomQA wraps exploration in a transaction, rolls back when done
  - NO data is permanently written

### Database adapter choice:

    | Your API's DB | VenomQA Adapter    | Strategy to use |
    |---------------|--------------------|--------------------|
    | PostgreSQL    | PostgresAdapter    | DFS() only         |
    | SQLite        | SQLiteAdapter      | BFS() or DFS()     |
    | No database   | state_from_context | BFS() (limited)    |

PostgreSQL note: BFS/CoverageGuided don't work with PostgreSQL SAVEPOINTs.
Always use DFS() with PostgresAdapter.

---

## Installation

    python3 -m venv .venv
    source .venv/bin/activate
    pip install venomqa

Python 3.10+ required.

---

## Core imports (v1 API)

    from venomqa.v1 import (
        Action,
        ActionResult,
        Invariant,
        Violation,
        Severity,
        InvariantTiming,
        Agent,
        World,
        BFS, DFS, Random, CoverageGuided, Weighted,
        ExplorationResult,
    )
    from venomqa.v1.adapters.http import HttpClient
    from venomqa.v1.adapters.postgres import PostgresAdapter   # optional
    from venomqa.v1.adapters.redis import RedisAdapter         # optional
    # reporters — importable from top-level or submodule (both work)
    from venomqa.v1 import ConsoleReporter, JSONReporter, HTMLTraceReporter, MarkdownReporter, JUnitReporter
    # or equivalently:
    # from venomqa.v1.reporters.console import ConsoleReporter

---

## Action — the ONLY correct signature

Actions always receive TWO arguments: (api, context), in that order.

    def my_action(api, context):
        # api      — HttpClient, has .get() .post() .put() .patch() .delete()
        # context  — Context object, NOT a dict. Use .get()/.set(), not ["key"]
        ...

WRONG (do not write):
    def bad(ctx, api):    # reversed order — will silently break
    def bad(context):     # missing api — will crash
    def bad(api, ctx):    # 'ctx' name is fine but order must be api-first

The framework auto-detects whether your function has 2 parameters and passes
context only if it does. A 1-parameter function receives only api.

### Action examples

CRITICAL: Actions must VALIDATE their results. Don't silently ignore errors.
Use the built-in validation helpers: expect_status(), expect_json_field(), etc.

    # GOOD: Using validation helpers (recommended)
    def create_user(api, context):
        resp = api.post("/users", json={"name": "Alice"})
        resp.expect_status(201)                     # raises if not 201
        data = resp.expect_json_field("id", "name") # raises if fields missing
        context.set("user_id", data["id"])
        return resp

    # GOOD: List endpoint
    def list_users(api, context):
        resp = api.get("/users")
        resp.expect_status(200)
        users = resp.expect_json_list()  # raises if not array
        context.set("users", users)
        return resp

    # GOOD: Validates response matches request
    def get_user(api, context):
        user_id = context.get("user_id")
        resp = api.get(f"/users/{user_id}")
        resp.expect_status(200)
        data = resp.expect_json()
        if data.get("id") != user_id:
            raise AssertionError(f"Wrong user returned: expected {user_id}, got {data}")
        return resp

    # BAD - Don't do this (silent no-op):
    def delete_user_BAD(api, context):
        if not context.has("user_id"):
            return api.get("/noop")   # WRONG: silently does nothing
        return api.delete(f"/users/{context.get('user_id')}")

    # GOOD: Use preconditions instead of silent no-ops
    def delete_user(api, context):
        user_id = context.get("user_id")
        resp = api.delete(f"/users/{user_id}")
        resp.expect_status(200, 204)  # raises if not 200 or 204
        context.delete("user_id")     # Clean up context
        return resp

### Validation helpers on ActionResult

    resp.expect_status(201)              # raises AssertionError if not 201
    resp.expect_status(200, 201)         # raises if not 200 OR 201
    resp.expect_success()                # raises if not 2xx/3xx
    data = resp.expect_json()            # raises if not JSON, returns body
    data = resp.expect_json_field("id")  # raises if field missing, returns dict
    items = resp.expect_json_list()      # raises if not array, returns list

### Wrapping a function as an Action

    Action(
        name="create_user",         # unique string identifier
        execute=create_user,        # the function above
        description="Create a user account",   # optional, human-readable
        tags=["users", "write"],    # optional, for filtering
        expected_status=[201],      # optional, checks response status
        expect_failure=False,       # set True to expect 4xx/5xx
        max_calls=10,               # optional, prevent data explosion (e.g., 1000 users)
    )

### Preventing data explosion with max_calls

    # Without max_calls, CoverageGuided might create 1000+ test records
    Action(name="create_connection", execute=create_conn, max_calls=20)
    Action(name="create_user", execute=create_user, max_calls=5)

    # Read-only actions don't need max_calls
    Action(name="list_connections", execute=list_conns)  # can run unlimited

---

## Context — shared state between actions

Context is NOT a dict. It uses explicit .get() / .set() methods.

    context.set("key", value)           # store any value
    value = context.get("key")          # returns None if missing
    value = context.get("key", default) # with default
    exists = context.has("key")         # bool
    context.delete("key")               # remove key
    all_keys = context.keys()           # list of all keys
    context.update({"a": 1, "b": 2})    # bulk set

DO NOT USE:
    context["key"]          # TypeError — not subscriptable
    context["key"] = val    # TypeError — not subscriptable

Scoped contexts (namespace isolation):
    user_ctx = context.scope("user")
    user_ctx.set("id", 123)     # stores as "user.id"
    user_ctx.get("id")          # reads "user.id"

---

## Invariant — the ONLY correct signature

Invariants receive ONE argument: the World object.

    def my_invariant(world):
        # world.context — the shared Context (read state set by actions)
        # world.api     — the HttpClient (can make live API calls)
        # world.systems — dict of registered rollbackable systems
        return True   # True = pass, False = violation

WRONG (do not write):
    def bad(state, ctx):   # wrong — this was the old v0 signature
    def bad(state):        # wrong
    def bad():             # wrong

### Invariant constructor

    Invariant(
        name="my_invariant",            # unique string
        check=my_invariant,             # the function above
        message="Explanation of rule",  # shown on violation — use 'message', NOT 'description'
        severity=Severity.CRITICAL,     # CRITICAL, HIGH, MEDIUM (default), LOW
        timing=InvariantTiming.POST_ACTION,  # POST_ACTION (default), PRE_ACTION, BOTH
    )

### Invariant examples

    # Check context has required keys
    def user_is_set(world):
        return world.context.has("user_id")

    # Check a value stored by an action
    def refund_within_bounds(world):
        refund  = world.context.get("refund_amount") or 0
        payment = world.context.get("payment_amount") or 0
        return refund <= payment

    # Make a live API call inside the invariant
    def list_shows_only_open(world):
        resp = world.api.get("/issues?state=open")
        issues = resp.json()
        return all(i["state"] == "open" for i in issues)

    # Example with severity + timing
    Invariant(
        name="refund_within_bounds",
        check=refund_within_bounds,
        message="Refunded amount must not exceed original payment",
        severity=Severity.CRITICAL,
    )

---

## World — the execution sandbox

    World(
        api=HttpClient("http://localhost:8000"),   # required
        systems={                                   # optional
            "db":    PostgresAdapter("postgresql://user:pass@localhost/mydb"),
            "cache": RedisAdapter("redis://localhost:6379"),
        },
    )

Accessing context from outside actions/invariants:
    world.context.set("stripe", stripe_client)   # inject external clients
    world.context.get("stripe")                  # read in actions

---

## Agent — runs the exploration

    agent = Agent(
        world=world,
        actions=[...],        # list[Action]
        invariants=[...],     # list[Invariant]
        strategy=BFS(),       # exploration strategy
        max_steps=500,        # hard stop (default 1000)
    )

    result = agent.explore()  # returns ExplorationResult

---

## Exploration strategies

    BFS()             # breadth-first search — best for finding bugs, explores shortest paths first
    DFS()             # depth-first search — finds deep sequences faster
    Random()          # random walk — useful for fuzzing
    CoverageGuided()  # prioritises states not yet seen

---

## ExplorationResult — reading results

    result.states_visited          # int — total distinct states reached
    result.transitions_taken       # int — total action executions
    result.action_coverage_percent # float — % of actions tried at least once (primary metric)
    result.coverage_percent        # float — % of (state × action) pairs tried (can never reach 100%)
    result.truncated_by_max_steps  # bool — True if exploration hit max_steps limit early
    result.duration_ms             # float — wall-clock time
    result.violations              # list[Violation]
    result.success                 # bool — True if no violations
    result.graph                   # Graph object with full transition history
    result.summary()               # dict with all metrics above

    for v in result.violations:
        v.invariant_name      # str
        v.message             # str — the invariant's message field
        v.severity            # Severity enum
        v.severity.value      # "critical" | "high" | "medium" | "low"
        v.reproduction_path   # list[Transition] — how to reach this state
        v.action              # Action that triggered the violation
        v.action_result       # ActionResult (request + response)

NOTE: Use action_coverage_percent as the primary coverage metric.
coverage_percent can never reach 100% in state-growing systems — each new state
adds N new (state, action) pairs. action_coverage_percent answers the useful
question: "have we tried every action at least once?"

---

## Reporters

    # Console (prints to stdout)
    from venomqa.v1.reporters.console import ConsoleReporter
    ConsoleReporter().report(result)

    # HTML (D3 force graph — returns str, write to file yourself)
    from venomqa.v1.reporters.html_trace import HTMLTraceReporter
    html_str = HTMLTraceReporter().report(result)
    with open("trace.html", "w") as f:
        f.write(html_str)

    # JSON
    from venomqa.v1.reporters.json import JSONReporter
    json_str = JSONReporter().report(result)

DO NOT call HTMLTraceReporter().report(result, path="trace.html") —
the path= kwarg does not exist. The method only takes result and returns a string.

---

## HttpClient — available methods

    api.get(path, **kwargs)
    api.post(path, json=None, data=None, **kwargs)
    api.put(path, json=None, **kwargs)
    api.patch(path, json=None, **kwargs)
    api.delete(path, **kwargs)

All return ActionResult (NOT httpx.Response). Convenience proxies exist:
    resp = api.post("/items", json={"name": "x"})
    resp.json()           # parsed body dict/list (proxy to resp.response.json())
    resp.status_code      # int (proxy to resp.response.status_code)
    resp.ok               # bool — True if 2xx/3xx
    resp.text             # body as string
    resp.success          # bool — same as ok
    resp.response         # HTTPResponse object (full access)
    resp.response.body    # raw parsed body
    resp.request          # HTTPRequest object

Headers / auth:
    HttpClient("http://localhost:8000", headers={"Authorization": "Bearer token"})

---

## Rollbackable adapters

PostgreSQL (uses SAVEPOINT — entire run is one uncommitted transaction):
    from venomqa.v1.adapters.postgres import PostgresAdapter
    PostgresAdapter("postgresql://user:pass@localhost/mydb")

Redis (DUMP + FLUSHALL + RESTORE per rollback):
    from venomqa.v1.adapters.redis import RedisAdapter
    RedisAdapter("redis://localhost:6379")

In-memory mocks (for unit testing without real services):
    from venomqa.v1.adapters import MockQueue, MockMail, MockStorage, MockTime

    queue   = MockQueue(name="tasks")
    mail    = MockMail()
    storage = MockStorage(bucket="uploads")
    clock   = MockTime(start=datetime(2024, 1, 1))   # auto-frozen at start

    # MockQueue — pop() returns a Message object, not the raw payload
    queue.push({"type": "job", "id": 1})   # enqueue item (any payload)
    queue.pending_count                    # int: unprocessed messages
    queue.processed_count                  # int: consumed messages
    msg = queue.pop()                      # returns Message | None (marks as processed)
    msg.payload                            # the original dict/value you pushed
    msg.id                                 # auto-generated "msg_1", "msg_2", …
    msg.processed                          # True after pop()

    # MockMail — get_sent() returns list of Email objects (not dicts)
    mail.send("to@example.com", "Subject", "Body text")
    mail.sent_count                        # int: total emails sent
    emails = mail.get_sent()              # list[Email]
    emails[0].subject                     # str
    emails[0].to                          # str (recipient)
    emails[0].body                        # str

    # MockStorage — get() returns a StoredFile object, not raw bytes
    storage.put("file.pdf", b"PDF content")   # content must be str or bytes (not dict)
    stored = storage.get("file.pdf")          # returns StoredFile | None
    stored.content                            # bytes — the actual file content
    stored.path                               # str — the key used in put()
    storage.file_count                        # int: number of files stored
    storage.delete("file.pdf")
    # PostgresAdapter observe_tables keys are "<table>_count" (not the table name):
    obs = pg.observe()
    obs.data["subs_users_count"]   # NOT obs.data["subs_users"]

    # MockTime
    from datetime import datetime
    clock = MockTime(start=datetime(2024, 1, 1))  # frozen — .now returns start
    clock.now                   # datetime (property, NOT a method — no ())
    clock.advance(days=1)       # only works when frozen
    clock.freeze(at=datetime(2024, 6, 1))   # freeze at a specific time
    clock.unfreeze()            # resume live wall-clock time
    clock.set(datetime(2025, 1, 1))         # set + freeze

    world = World(api=api, systems={"queue": queue, "mail": mail, "clock": clock})
    # Observe in invariant: world.systems["queue"].pending_count

Custom mock server: subclass MockHTTPServer, implement 3 methods:
    from venomqa.v1.adapters.mock_http_server import MockHTTPServer

    class MyMock(MockHTTPServer):
        def get_state_snapshot(self) -> dict: ...
        def rollback_from_snapshot(self, snapshot: dict) -> None: ...
        def observe_from_state(self, state_snapshot: dict) -> Observation: ...

---

## Common mistakes to avoid

1. Action signature reversed:
   BAD:  def my_action(ctx, api)
   GOOD: def my_action(api, context)

2. Dict-style context access:
   BAD:  context["key"] = val   → TypeError
   GOOD: context.set("key", val)

3. Invariant receives World, not (state, ctx):
   BAD:  def check(state, ctx)
   GOOD: def check(world)

4. Invariant uses 'description=' instead of 'message=':
   BAD:  Invariant(name=..., check=..., description="...")   → TypeError
   GOOD: Invariant(name=..., check=..., message="...")

5. HTMLTraceReporter passes path= kwarg:
   BAD:  html.report(result, path="out.html")   → TypeError
   GOOD: open("out.html","w").write(html.report(result))

6. MockTime.now called as a method:
   BAD:  clock.now()   → TypeError (it's a property)
   GOOD: clock.now

7. MockTime not frozen — start= not provided:
   BAD:  clock = MockTime(); clock.advance(days=1)  → RuntimeError: not frozen
   GOOD: clock = MockTime(start=datetime(2024,1,1)); clock.advance(days=1)

8. MockStorage.put() given a dict:
   BAD:  storage.put("key", {"data": 1})   → TypeError
   GOOD: import json; storage.put("key", json.dumps({"data": 1}))

9. Accessing State.observations by Observation.system field:
   BAD:  state.get_observation("queue:orders")   # 'queue:orders' is Observation.system metadata
   GOOD: state.get_observation("queue")          # use the systems dict KEY you passed to World

10. Action missing return statement:
   BAD:  def my_action(api, context): api.post("/items")  → TypeError at runtime
   GOOD: def my_action(api, context): return api.post("/items")

11. Using python instead of python3 on macOS:
   BAD:  python main.py
   GOOD: python3 main.py

12. CRITICAL - Actions that silently no-op:
   BAD:  def my_action(api, ctx):
           if not ctx.has("id"):
               return api.get("/noop")  # silently does nothing!
   GOOD: Use preconditions on the Action:
         Action(name="my_action", execute=..., preconditions=[lambda s: s.context.has("id")])

13. CRITICAL - Actions that don't validate responses:
   BAD:  def create_user(api, ctx):
           resp = api.post("/users", json={"name": "test"})
           ctx.set("user_id", resp.json()["id"])  # assumes success!
           return resp
   GOOD: def create_user(api, ctx):
           resp = api.post("/users", json={"name": "test"})
           if resp.status_code != 201:
               raise AssertionError(f"Expected 201: {resp.text}")
           data = resp.json()
           if "id" not in data:
               raise AssertionError(f"Missing 'id': {data}")
           ctx.set("user_id", data["id"])
           return resp

14. CRITICAL - No database connection (most common setup error):
   BAD:  world = World(api=HttpClient("http://localhost:8000"))
         # No database → VenomQA can't rollback → only ONE linear path tested
   GOOD: from venomqa.v1.adapters.postgres import PostgresAdapter
         world = World(
             api=HttpClient("http://localhost:8000"),
             systems={"db": PostgresAdapter("postgresql://user:pass@localhost/mydb")}
         )

15. CRITICAL - Using wrong database (not the one your API writes to):
   BAD:  Setting up a separate test database that the API doesn't use
   GOOD: Connect to the SAME database your API writes to.
         VenomQA checkpoints and rolls back THAT database.

---

## Minimal working example (copy-paste ready)

CRITICAL: You must connect to your database OR use state_from_context.

### Option A: With PostgreSQL (recommended)

    import os
    from venomqa.v1 import Action, Invariant, Agent, World, DFS, Severity
    from venomqa.v1.adapters.http import HttpClient
    from venomqa.v1.adapters.postgres import PostgresAdapter

    # Actions MUST validate responses — use expect_* helpers!
    def create_item(api, context):
        resp = api.post("/items", json={"name": "widget"})
        resp.expect_status(201)                    # raises if not 201
        data = resp.expect_json_field("id")        # raises if field missing
        context.set("item_id", data["id"])
        return resp

    def list_items(api, context):
        resp = api.get("/items")
        resp.expect_status(200)
        items = resp.expect_json_list()            # raises if not array
        context.set("items", items)
        return resp

    def delete_item(api, context):
        item_id = context.get("item_id")
        resp = api.delete(f"/items/{item_id}")
        resp.expect_status(200, 204)               # raises if not 200 or 204
        context.delete("item_id")
        return resp

    def list_returns_array(world):
        # GOOD: Make live API call — don't just check context
        resp = world.api.get("/items")
        return resp.status_code == 200 and isinstance(resp.json(), list)

    # Connect to the SAME database your API writes to
    api = HttpClient("http://localhost:8000")
    db = PostgresAdapter(os.environ["DATABASE_URL"])

    agent = Agent(
        world=World(api=api, systems={"db": db}),  # ← REQUIRED for exploration
        actions=[
            Action(name="create_item", execute=create_item, expected_status=[201]),
            Action(name="list_items",  execute=list_items,  expected_status=[200]),
            Action(name="delete_item", execute=delete_item,
                   preconditions=["create_item"]),  # ← only run after create_item
        ],
        invariants=[
            Invariant(
                name="list_returns_array",
                check=list_returns_array,
                message="GET /items must always return a JSON array",
                severity=Severity.CRITICAL,
            ),
        ],
        strategy=DFS(),   # ← Use DFS with PostgreSQL (BFS doesn't work with PG savepoints)
        max_steps=100,
    )

    result = agent.explore()
    for v in result.violations:
        print(f"[{v.severity.value.upper()}] {v.invariant_name}: {v.message}")
    print(f"Done. States={result.states_visited}, Coverage={result.action_coverage_percent:.0f}%")

### Option B: Without database (limited exploration)

    # If your API is stateless or you don't have DB access, use state_from_context.
    # WARNING: This only explores based on context values, not actual DB state.

    world = World(
        api=HttpClient("http://localhost:8000"),
        state_from_context=["item_id", "items"],  # These context keys define state identity
    )

---

## CLI commands

V1 stateful exploration (recommended entry points):

    venomqa explore  journey.py --base-url http://localhost:8000
    venomqa validate journey.py
    venomqa record   journey.py --base-url http://localhost:8000

    Options for explore:
      --strategy [bfs|dfs|random]   default: bfs
      --max-steps INTEGER           default: 1000
      --format [console|json|markdown|junit]
      --output FILE                 write report to file instead of stdout
      --db-url TEXT                 PostgreSQL URL for DB rollback
      --redis-url TEXT              Redis URL for Redis rollback

General commands:
    venomqa doctor     # system diagnostics
    venomqa llm-docs   # this document
    venomqa --help

---

## COMMON MISTAKES — read this before writing code

### Mistake 1: states_visited = 1 (no adapter registered)

SYMPTOM: Exploration finishes instantly with states_visited=1 even though many
actions fired. Coverage says 100% but all actions ran from the same state.

CAUSE: No system adapter means world.observe() returns empty observations.
Every state hashes identically, so BFS sees only one state forever.

FIX: Register at least one adapter in World so that system state is part of the
state fingerprint:

    # BAD — no systems = no state diversity
    world = World(api=api)

    # GOOD — DB state is part of the state hash
    world = World(api=api, systems={"db": PostgresAdapter("postgresql://...")})

    # GOOD — use MockHTTPServer if you have a mock API
    world = World(api=api, systems={"api": my_mock_server})

VenomQA will emit a warning at startup if no systems are registered.

---

### Mistake 2: All actions valid from step 0 (pre-seeded context IDs)

SYMPTOM: VenomQA warns "All N actions are valid from the initial state."
Exploration is shallow — all actions fire but from the same state.

CAUSE: You set context.set("connection_id", 123) in World.setup() or before
exploration starts. This makes every precondition pass immediately, so BFS
finds nothing new to branch on.

FIX: Set IDs inside action functions, not in setup. Setup is only for
one-time bootstrap (creating a user, getting an auth token):

    # BAD — pre-seeding IDs kills state diversity
    def setup(api, context):
        context.set("connection_id", 123)   # wrong: everything is now unlocked
        context.set("rule_id", 456)

    # GOOD — setup only bootstraps auth, actions set their own IDs
    def setup(api, context):
        resp = api.post("/auth/token", json={"user": "admin", "pass": "secret"})
        context.set("token", resp.json()["token"])   # only auth, not data IDs

    def create_connection(api, context):
        resp = api.post("/connections", json={...})
        context.set("connection_id", resp.json()["id"])   # set here, not in setup
        return resp

---

### Mistake 3: expected_status=[404] seems ignored (pre-0.4.0 bug)

In versions before 0.4.0, writing Action(expected_status=[404]) still reported
"Expected success, got 404" because the ok/fail check ran after the status check.

FIX: Upgrade to venomqa>=0.4.0. Since 0.4.0, if expected_status is set and the
response status is in the list, the ok/fail check is skipped entirely:

    # Works correctly in 0.4.0+
    Action(
        name="get_missing",
        execute=get_missing,
        expected_status=[404],       # no expect_failure=True needed
    )

---

### Mistake 4: Action returns None

SYMPTOM: TypeError: Action 'my_action' returned None. Actions must return ActionResult.

CAUSE: Forgot to return the result of the API call.

    # BAD
    def my_action(api):
        api.get("/items")        # result discarded

    # GOOD
    def my_action(api):
        return api.get("/items") # always return

---

### Mistake 5: preconditions=["action_name"] — string shorthand (0.4.1+)

Since 0.4.1, strings in the preconditions list are automatically converted to
precondition_action_ran() calls:

    # These are equivalent:
    Action(name="get_conn", preconditions=["create_connection"], ...)
    Action(name="get_conn", preconditions=[precondition_action_ran("create_connection")], ...)

    # Also works:
    Action(name="delete_rule", preconditions=["create_connection", "create_rule"], ...)

---

### Mistake 6: Using context.get() as precondition without precondition_has_context

    # BAD — no-op guard inside action doesn't affect BFS scheduling
    def get_connection(api, context):
        if not context.has("connection_id"):
            return ActionResult(success=True, ...)   # BFS still tries this action

    # GOOD — use precondition so BFS skips the action until precondition is met
    Action(
        name="get_connection",
        execute=get_connection_fn,
        preconditions=["create_connection"],   # string shorthand (0.4.1+)
    )

---

End of VenomQA LLM context document.
"""


@click.command("llm-docs")
@click.option(
    "--output",
    "-o",
    type=click.Path(),
    default=None,
    help="Write to a file instead of stdout.",
)
def llm_docs(output: str | None) -> None:
    """Print a complete LLM context document for VenomQA.

    Paste the output into ChatGPT, Claude, Cursor, or any AI assistant
    so it knows the exact API signatures and won't give you wrong code.

    Examples:

    \b
        venomqa llm-docs                     # print to terminal
        venomqa llm-docs | pbcopy            # copy to clipboard (macOS)
        venomqa llm-docs -o context.txt      # save to file
    """
    if output:
        with open(output, "w") as f:
            f.write(LLM_CONTEXT)
        click.echo(f"LLM context written to {output}")
    else:
        click.echo(LLM_CONTEXT)
