# Playbook — Bounded working-set retrieval on a supernode-heavy investigation graph

Reproduce, on your own machine, the difference between an unbounded traversal and a budgeted recall over a graph whose shared-hosting node announces 240 domains.

## What you need

- Docker 24 or later
- Python 3.9 or later
- About 4 minutes: the dataset is small, but it is written through the durable mutation path

## 1. Start a disposable Corrobore

```bash
mkdir -p .corrobore-tls .corrobore-secrets
printf '%s\n' 'change-me' > .corrobore-secrets/http-token
openssl req -x509 -newkey rsa:2048 -sha256 -nodes \
  -keyout .corrobore-tls/server.key -out .corrobore-tls/server.crt \
  -days 30 -subj '/CN=localhost' \
  -addext 'basicConstraints=critical,CA:FALSE' \
  -addext 'keyUsage=critical,digitalSignature,keyEncipherment' \
  -addext 'extendedKeyUsage=serverAuth' \
  -addext 'subjectAltName=DNS:localhost,IP:127.0.0.1'

docker run --rm -d --name corrobore-playbook \
  -p 127.0.0.1:8080:8080 \
  -e CORROBORE_STORAGE_MODE=ephemeral \
  -v "$PWD/.corrobore-secrets/http-token:/run/secrets/corrobore-http-token:ro" \
  -v "$PWD/.corrobore-tls/server.crt:/run/secrets/tls.crt:ro" \
  -v "$PWD/.corrobore-tls/server.key:/run/secrets/tls.key:ro" \
  ghcr.io/estance-labs/corrobore:latest

curl --insecure -H 'Authorization: Bearer change-me' https://127.0.0.1:8080/health/ready
```

## 2. Install the notebook dependencies

```bash
pip install requests pandas matplotlib
```

## 3. Run the notebook

```bash
jupyter lab notebook.ipynb
```

The notebook loads `dataset.json` from the same directory. Keep the three files together.

## 4. What to observe

1. The unbounded traversal returns 266 records and reports `complete: true`. Only 12.8% of them concern the campaign — complete and useless.
2. The budgeted recall returns 35 records and names the bound it hit: `supernode_blocked` in `completeness.outcomes`, not a silently shortened list.
3. None of the 231 unrelated tenant domains reach the working set, while all 9 campaign domains do.
4. The shared-hosting node itself is still returned. The guard refused to walk its 258 edges; it did not hide the pivot.
5. Each record carries `selection_reasons`, so the working set can be audited without replaying the traversal.
6. `trace` on the `recall_id` returns the selection paths, the evidence source ids, and the actor/session attribution the server injected.

## 5. Change one thing and re-run

- Raise `supernode_threshold` above 258 and watch the 231 tenant domains flood the working set.
- Drop `max_cost` to 40 and observe `cost_budget_exhausted` instead — a different bound, named differently.
- Set `max_depth` to 1 and confirm the campaign domains become unreachable: the budget is a real constraint, not a hint.
- Send a recall with `max_depth: 0` and confirm it is refused with `INVALID_BUDGET` rather than treated as unlimited.
- Replay the whole notebook: idempotency keys make the second run a no-op with `receipt.replayed: true`.

## Boundaries

- The dataset is synthetic. Addresses use the documentation ranges (203.0.113.0/24, AS64500) and domains use `.example`.
- Seed ranking through `POST /v1/seed/search` depends on the configured semantic provider; this notebook uses explicit seeds plus the objective's lexical matching so it runs with no provider installed.
- Pheromone-guided ordering and next-best-evidence ranking are `graph-core` primitives with no HTTP route in this release.

## Tear down

```bash
docker rm -f corrobore-playbook
```

---

Documentation: https://docs.corrobore.org — Source: https://github.com/Estance-Labs/corrobore
