# Project export: Ledger

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Claude Code makes developers faster. Ledger makes sure they still own what they ship.
- Devpost: https://devpost.com/software/ledger-ybhkg2
- GitHub: https://github.com/owenarnst/ledger
- Video: https://www.youtube.com/embed/u_kCPekmkAo?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Owen Arnst (37 commits), Your name (17 commits), Claude Opus 4.8 (1M context) (14 commits)

## Devpost submission (written by the team)

### Inspiration

“You can outsource your thinking, but you cannot outsource your understanding.” — Andrej Karpathy Coding agents make it possible to ship software faster than ever. But speed can hide a new kind of technical debt: code that works, but that its developer never built the mental model to maintain. We felt this firsthand. Under pressure to ship, it is easy to let Claude Code make one more decision, resolve one more failure, or explain one more unfamiliar subsystem. Each interaction is productive. Over time, though, even a capable developer can become a fragile expert: able to build with an agent, but unable to safely operate the resulting system without it. Anthropic's study, “How AI assistance impacts the formation of coding skills”, made the risk concrete. In its randomized trial, the AI-assisted group scored about 17 percentage points lower on a comprehension assessment than the group that coded by hand. The largest gap was in debugging—the exact skill developers need when AI-generated code fails. That led us to Ledger: a tool that does not ask developers to stop using coding agents, but makes sure they still own what they ship.

### What it does

Ledger is a local Claude Code companion that maintains an ownership ledger for AI-assisted code. It indexes a repository's code, tests, Git history, documentation, and local Claude Code sessions. A read-only Claude Topic Analyst investigates that evidence and proposes an ordered worklist of decisions a maintainer may need to own: a tenant-isolation boundary, a confidence cutoff, a context-window rule, or another choice whose failure would matter. Deterministic code then resolves every cited file, line, and trace segment before the proposal can become part of the ledger. The dashboard keeps the result deliberately modest. Each row shows the Topic, its observable ownership status, a verified evidence summary, and a categorical impact level. Ledger never claims, “You do not understand this.” It says, “Ownership check recommended,” then lets the developer test the question directly. A Topic page explains the maintenance obligation and its consequences, then progressively reveals the evidence behind it: Code anchors: the exact repository locations that encode the decision. Agent trace: the relevant prompt and tool-call sequence read from the developer's real local Claude Code session—not a fabricated chain of thought or a committed transcript fixture. Ownership history: prior checks, attempts, elapsed time, conceptual help used, and whether the code changed afterward. When a developer starts a check, they choose a level: Easy is recognition-focused. Medium combines conceptual questions with guided debugging. Hard goes directly to the sandbox with less scaffolding. For the working prototype, Claude generated the topic-specific question plans, which are cached for demo reliability. Debugging checks create a temporary copy of the Python demo repository and inject a curated semantic defect targeted at the selected Topic. The developer must inspect the failure, edit the code in an embedded code workspace, and make the real pytest suite pass. A built-in Claude coach can explain concepts, ask diagnostic questions, and suggest observations. It cannot provide the patch. Ledger launches the coach through the user's existing Claude Code CLI, gives it the Topic and test failure but not the original implementation or mutation diff, and denies all file, shell, search, and web tools. The developer can select Claude Haiku, Sonnet, or Opus without weakening that boundary. When the behavior is restored, Ledger records an ownership event. Green tests are necessary, but Ledger does not turn them into a claim of mastery or an opaque score. It preserves the observable cost of getting there. Ledger also stays close to the development loop. Its Claude Code SessionStart hook quietly lists ready checks for the current repository with deep links into the relevant Topic. Hooks never block Claude Code or Git; if Ledger is unavailable, events spool locally. In short: Ledger finds load-bearing code decisions with thin evidence of ownership, then asks you to prove you can still operate them—by breaking one and making you fix it.

### How we built it

Ledger is a local-first web application with a Python-first architecture: FastAPI provides the backend and REST API. React and Vite power the worklist, Topic page, and ownership-check workspace. A custom React editor provides the embedded code-editing experience. SQLite stores projects, evidence, Topics, revisions, checks, and attempts locally. Claude Code and Git adapters normalize development activity into provider-labeled evidence. Claude Code and Git hooks capture activity and surface relevant checks without interrupting normal work. For the demo, we used one real Git repository: a small Python documentation-search service with retrieval, reranking, tenant isolation, and context-packing decisions. The seeded worklist was captured from a live Claude Opus analysis of that repository. The committed seed contains code anchors and cached exercise plans, while the Agent trace is loaded live from the real Claude Code sessions on the machine. Each of the three demo Topics has a curated, topic-specific mutation caught by the repository's real tests.

### Challenges we ran into

Our first idea was to infer cognitive offloading from Claude Code conversations. We built an engagement classifier to distinguish thoughtful use from passive delegation. It failed. Even with full transcripts, the signal was too ambiguous to support a trustworthy judgment. That failure changed the project. We stopped trying to infer a developer's internal state from chat behavior and moved toward observable repository evidence plus performance in a real task. Topic selection created a second problem. An early deterministic approach used syntax and code fan-in as proxies for importance. It surfaced generic helpers such as get, set, and render: frequently used code, but not necessarily meaningful decisions. The current architecture lets Claude investigate the bounded repository context and identify defendable maintenance obligations, while deterministic verification prevents uncited model claims from becoming facts. That division proved much stronger than asking either heuristics or an LLM to do both jobs. Provenance was also harder than it looked. A session that touched a file does not prove that every decision in that file came from Claude. Ledger therefore treats the Agent trace as supporting evidence, attaches explicit link confidence, and never uses AI authorship to validate a Topic or grade a check. Verification presented a different trap. An LLM-graded explanation felt subjective and easy to bluff, so we built Debug-to-Own around real code and tests. A spike then showed that fixing a mutation still does not prove ownership: elapsed time can be dominated by how widely the mutant breaks the suite, and a subtle operator swap may test visual search rather than understanding. Ledger therefore records struggle as evidence, avoids a universal score, and keeps the demo mutations narrow and decision-specific. Finally, we had to build an AI coach that helps without taking over. Prompt instructions alone were not a meaningful safety boundary. Withholding the solution context and denying Claude's tools made the restriction architectural.

### Accomplishments we're proud of

We are proud of building the complete end-to-end workflow. Ledger connects a non-blocking Claude Code hook, an evidence-backed worklist, exact code anchors, a live prompt-and-tool-call Agent trace, difficulty-aware checks, a real mutated sandbox, an embedded editor, a restricted conceptual coach, and persistent ownership history in one experience. Claude Code is native on both sides: it supplies the development receipts and performs the semantic repository investigation, then becomes a coach whose permissions prevent it from completing the exercise for the developer. Most importantly, Ledger is pro-AI without being uncritical. Claude Code makes developers faster. Ledger is designed to make sure they can still maintain what they ship.

### What we learned

The hardest part of building a tool for understanding is that understanding is not directly observable. Conversation patterns, confidence, and successful task completion are all imperfect proxies. A responsible product should not turn those proxies into a confident verdict. We also learned that the verifier is not the whole product. A developer can already ask Claude to explain a file. The harder and more valuable problem is allocating limited attention: selecting which decisions across an entire repository deserve a check, remembering what has already been practiced, and resurfacing a Topic when its code changes. Productive friction is part of the design. Coding agents optimize for removing friction, but some struggle—forming a hypothesis, running an experiment, and debugging a failure—is how a developer builds the model needed to maintain a system later. The goal is not to maximize friction. It is to spend it selectively on decisions important enough to justify it. Finally, scope and disclosure mattered. Live agent analysis is variable, so the demo worklist and Claude-generated questions are cached from real runs. General mutation generation and arbitrary-repository sandboxing were not realistic in one hackathon, so the demo uses curated mutations against one real, testable Python repository. Keeping that boundary explicit made the working system more credible.

### What's next

The next step is to turn the working point-in-time prototype into a continuously reconciled ownership ledger. Ledger should preserve stable Topic identities across renames and revisions, retain immutable worklist snapshots, and resurface a practiced decision when its implementation changes. Discovery can remain an expensive, evidence-gathering pass while a cheaper context-aware ranking pass decides what matters for the current branch and path. We also want to replace curated mutations with automatic generation guarded by baseline-green, mutant-red, narrow-failure, and non-equivalence checks, then add sandbox adapters for other popular languages and runtimes such as TypeScript and C/C++. Longer term, Ledger can support additional coding agents and help developers ramp into unfamiliar codebases by turning important architectural decisions into targeted, evidence-backed practice. Teams could share learning milestones, but ownership history should never become an employee performance score. Developers need to trust that Ledger exists to help them learn, not to surveil them. The broader problem is not limited to coding. As agents move into research, operations, finance, medicine, design, and engineering, verification often gets harder, not easier. Many domains do not have a pytest suite waiting at the end. Ledger starts with code because it gives us commits, tests, and source anchors, but the larger goal is to keep humans informed enough to challenge, debug, and operate the systems agents help create. Considerations Ledger was motivated by an ethical concern with AI-assisted development: cognitive outsourcing can increase productivity while weakening critical thinking, debugging ability, and long-term skill development. Ledger preserves human agency by making AI-generated decisions inspectable and asking developers to actively diagnose and repair behavior instead of delegating the solution back to AI. Privacy is addressed through a local-first architecture. Repository evidence, Claude Code traces, user activity, and ownership history remain on the developer’s machine in SQLite. Ledger adds no cloud database, telemetry service, or AI provider beyond the developer’s existing Claude Code relationship with Anthropic. Its Claude calls still cross that existing provider boundary, so model access is tightly scoped: the analyst is read-only and limited to the enrolled repository, while the coach cannot read files, run commands, search the web, or access the original solution. Ledger also avoids turning imperfect signals into judgments about people. It does not claim that a developer lacks understanding, calculate an opaque “ownership score,” or treat passing a check as proof of mastery. It recommends checks and records observable evidence such as attempts, test results, assistance used, and code freshness. Ownership history is intended for personal learning—not employee surveillance or performance evaluation.

## README (from the GitHub repository)

# Ledger

**A local, Claude-Code-native companion that keeps a repo-level ownership ledger for AI-assisted code.** It finds load-bearing code decisions shipped without a reasoning trail, ranks them by how much they matter and how thin the ownership evidence is, then asks the maintainer to prove they can still operate them — by breaking one and asking them to fix it.

> *"Claude Code makes developers faster. Ledger makes sure they still own what they ship."*

Ledger is pro-responsible-use, not anti-AI. The villain is **silent epistemic debt**: AI makes it frictionless to ship working code without ever building the model of it in your head, and the most safety-critical casualty is debugging — [Anthropic's own RCT](https://www.anthropic.com/research/AI-assistance-coding-skills) found it's the single largest skill gap in AI-assisted developers. Ledger operationalizes that finding: it catches which decisions may be hard to maintain later, and makes developers earn the understanding back.

---

## What it does

Ledger is a **finite-attention allocator**. Nobody can deeply own their entire codebase, and checking evenly is waste — so Ledger keeps a running, decaying ledger of where ownership is thin *on the things that matter*, and surfaces the highest-leverage gaps in priority order.

* **Selects** — the **Topic Analyst** (a scoped Claude Code harness with read-only `Read`/`Grep`/`Glob`) investigates the repository and its real Claude session traces to construct an ordered **worklist** of durable maintenance obligations. Deterministic code then verifies every citation, hashes the evidence, and persists it. No item exists without exact code grounding.
* **Surfaces just-in-time** — a `SessionStart` hook reads the current `cwd`/branch and surfaces debt *on the path about to be touched*, with deep links into the app.
* **Verifies (Debug-to-Own)** — starting a **Check** spins up a real temp-dir sandbox of the committed code, injects a small blast-controlled semantic defect targeted at the flagged decision, and asks the maintainer to diagnose and repair it until the test suite goes green. The signal is *struggle* — time, attempts, coach use — never the binary solve.
* **Coaches without leaking** — the **Coach** runs on the user's own Claude Code CLI (`claude -p`, **all tools denied**), so it explains concepts, asks diagnostic questions, and suggests experiments but *architecturally cannot* hand over the patch. The user picks which Claude model answers (`haiku | sonnet | opus`, default `sonnet`).
* **Remembers** — topics, evidence, attempts, and reflections persist in a local SQLite ledger across sessions and code changes; a tracked decision that gets modified again, especially if still untrailed or previously fumbled, re-surfaces to the top.

Ledger **recommends checks; it never asserts that someone lacks understanding** and never grades code. The headline is *"this deserves a check,"* inferred from silence — only the check result turns a candidate into confirmed debt.

## How it works

```text
Git commits + code/tests/docs + real Claude session traces (~/.claude/projects/**/*.jsonl)
  → deterministic lossless ingestion + searchable indexes        (backend/ingestion.py, extraction.py)
  → Claude Code Topic Analyst investigates with scoped Read/Grep/Glob   (backend/analyst.py)
  → structured, ordered Topic proposals with source locators + confidence
  → deterministic citation verification + immutable evidence records   (backend/verifier.py)
  → create / revise / retire topics; project the ordered worklist
  → Debug-to-Own Check: sandbox + targeted mutation + real test    (backend/sandbox.py, exercise_*.py)
  → attempts + Coach use + reflection                              (backend/coach.py)
  → future commit / relevance / time trigger → repeat
```

Provenance model: **code is truth** (what gets exercised), **docs/ADRs/comments are the trail** (whether the *why* was captured), **the transcript is the receipt** (whether Claude was involved). The transcript is deliberately kept *out* of validation — diff↔session linking is fuzzy — and is stored as provenance display only.

## Architecture

* **Backend** — Python 3.11+ / FastAPI (`backend/`). SQLite-backed append-oriented ledger (`backend/db.py`, `repository.py`), the agentic analyst + deterministic verifier, the sandbox + mutation engine, the coach, and the hook/CLI surface (`backend/__main__.py`, `hooks.py`). Sandboxes are temp-dir + `subprocess` with **exit-code-as-oracle** — no containers, no interactive terminal.
* **Frontend** — React + Vite + TypeScript (`frontend/src/`). Three screens: `Dashboard` (the worklist + why each item is ranked), `Topic` (progressively-disclosed evidence + ownership history), and `Workspace` (Task / Sandbox-editor / Coach panes).
* **Hooks** — global Claude Code `SessionStart` + git `post-commit`. They are Ledger's *sensory system*, not its interface: if the server is down they spool to a local file and exit fast, and **must never block Claude Code or git.**

## Requirements

* **Python ≥ 3.11**
* **Node.js** (for the Vite frontend)
* **Claude Code CLI** on the user's `PATH` — Ledger's only Claude dependency. The Topic Analyst and Coach shell out to `claude -p`; users ride their existing Claude Code auth, so there's no API key to manage and no per-token cost.

## Quick start

```bash
# 1. Install dependencies (backend editable install + frontend npm install)
make install

# 2. Seed the curated demo worklist into ~/.ledger (wipes + re-seeds the DB and sandboxes)
make reset

# 3. Start the app (frontend on :4317, backend API on :8000)
make dev
```

Then open **http://localhost:4317**.

> The seeded demo ships a fully-grounded **tenant-isolation** hero topic and its Debug-to-Own check, so the full loop can be shown end-to-end: worklist → topic card → sandbox → coach → repair → reflection → persisted history. Curated components are disclosed as curated.

### Running against a real repository

Point the analyst at a Git repo to discover its worklist. Extraction ingests that repo's *real* `~/.claude` transcripts as Agent-trace evidence.

```bash
# Deterministic analyst (no Claude calls)
make extract REPO=~/Projects/your-repo

# Live Claude Code Topic Analyst — cites the real prompts + tool calls per topic
make extract-claude REPO=~/Projects/your-repo
```

To wire the hooks into a repo so sessions and commits are captured automatically:

```bash
.venv/bin/python -m backend install --repo ~/Projects/your-repo
```

## Make targets

| Command                          | What it does                                                                                       |
| -------------------------------- | -------------------------------------------------------------------------------------------------- |
| `make install`                   | Install frontend (`npm install`) and backend (`pip install -e ".[dev]"`) dependencies              |
| `make dev`                       | Start frontend (`:4317`) and backend (`:8000`) together                                            |
| `make frontend` / `make backend` | Start one side only                                                                                |
| `make reset`                     | Reset `~/.ledger` to the curated Claude demo worklist (wipes DB + sandboxes, re-seeds the fixture) |
| `make seed-demo`                 | Alias for `make reset`                                                                             |
| `make extract REPO=…`            | Discover the worklist for `REPO` via the deterministic analyst                                     |
| `make extract-claude REPO=…`     | Same, but run the live Claude Code Topic Analyst (`LEDGER_ANALYST=claude`)                         |
| `make clean`                     | Remove `node_modules`, `dist`, and Python build artifacts                                          |

`make help` lists them in the terminal.

## Configuration

| Variable             | Default         | Purpo

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 69 recognized source files, 605 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (80 of 80)

```
.gitignore
AGENTS.md
backend/__init__.py
backend/__main__.py
backend/analyst.py
backend/api.py
backend/coach.py
backend/db.py
backend/exercise_generation.py
backend/exercise_templates.py
backend/extraction.py
backend/fixtures/demo_seed.json
backend/fixtures/hero_repo/.gitignore
backend/fixtures/hero_repo/app.py
backend/fixtures/hero_repo/pyproject.toml
backend/fixtures/hero_repo/README.md
backend/fixtures/hero_repo/retrieval/__init__.py
backend/fixtures/hero_repo/retrieval/context.py
backend/fixtures/hero_repo/retrieval/models.py
backend/fixtures/hero_repo/retrieval/pipeline.py
backend/fixtures/hero_repo/retrieval/rerank.py
backend/fixtures/hero_repo/retrieval/store.py
backend/fixtures/hero_repo/tests/test_api.py
backend/fixtures/hero_repo/tests/test_context.py
backend/fixtures/hero_repo/tests/test_pipeline.py
backend/fixtures/hero_repo/tests/test_rerank.py
backend/hooks.py
backend/ingestion.py
backend/pseudocode.py
backend/repository.py
backend/sandbox.py
backend/tests/conftest.py
backend/tests/test_analyst.py
backend/tests/test_api.py
backend/tests/test_backend_contract.py
backend/tests/test_coach.py
backend/tests/test_extraction.py
backend/tests/test_ingestion.py
backend/tests/test_repository.py
backend/tests/test_verifier.py
backend/verifier.py
CLAUDE.md
CONTEXT.md
docs/adr/0001-dual-provider-ingestion-claude-native-spine.md
docs/adr/0002-agentic-topic-discovery-deterministic-verification.md
docs/adr/0003-idempotent-worklist-reconciliation.md
docs/adr/0004-drop-codex-single-provider-selectable-coach-model.md
docs/agents/domain.md
docs/agents/issue-tracker.md
docs/agents/triage-labels.md
docs/planning/backend-topic-initialization-note.md
docs/planning/build-plan.md
docs/planning/design/Ledger.dc.html
docs/planning/product.md
docs/planning/ui-spec.md
docs/superpowers/plans/2026-06-20-check-difficulty-plans.md
docs/superpowers/plans/2026-06-20-ledger-review-fixes.md
frontend/.gitignore
frontend/index.html
frontend/package.json
frontend/src/adapt.ts
frontend/src/api.ts
frontend/src/App.tsx
frontend/src/coachmd.tsx
frontend/src/highlight.tsx
frontend/src/index.css
frontend/src/main.tsx
frontend/src/routing.test.ts
frontend/src/routing.ts
frontend/src/screens/Dashboard.tsx
frontend/src/screens/Topic.tsx
frontend/src/screens/Workspace.tsx
frontend/src/theme.ts
frontend/tsconfig.json
frontend/tsconfig.node.json
frontend/vite.config.ts
LICENSE
Makefile
pyproject.toml
README.md
```

### Dependencies

- backend/fixtures/hero_repo/pyproject.toml: fastapi@>=0.110, httpx@>=0.27, pytest@>=8.0, uvicorn@>=0.29
- frontend/package.json: @types/react@^18.3.12, @types/react-dom@^18.3.1, @vitejs/plugin-react@^4.3.4, react@^18.3.1, react-dom@^18.3.1, typescript@^5.6.3, vite@^5.4.11, vitest@^2.1.9
- pyproject.toml: fastapi@>=0.115, httpx@>=0.28, pytest@>=8.0, uvicorn[standard]@>=0.30

### Recent commits (newest first)

- Update README.md
- update readme
- Merge pull request #34 from owenarnst/feature/agent-trace
- feat: Agent trace — live prompt + tool-call hunk from real Claude sessions
- Merge pull request #33 from owenarnst/feature/session-hook-checks
- feat: list ready checks with deep links in SessionStart nudge
- Merge pull request #32 from owenarnst/refactor/drop-codex
- refactor: drop Codex, add selectable Claude model for the coach
- Merge pull request #31 from owenarnst/fix/code-editor
- fix: editor tab key, operator ligatures, and workspace layout
- Merge pull request #30 from owenarnst/issues-23-24-verified-worklist-ui
- Harden demo: working reset, pre-cached plans, topic-aware sandboxes
- Topic page: agentic evidence, no reasoning-trail section (#24)
- Worklist rows: four verified fields, whole-row activation (#23)
- Add verified-worklist fields to frontend API types (#23, #24)
- Merge pull request #29 from owenarnst/curated-recipe-and-extraction
- Seed demo from live Claude output; stream analyst progress
- Merge origin/main: adopt check-workspace, keep #21/#22 discovery
- Add ADR-0003: idempotent worklist reconciliation (design for #25-#28)
- Agentic Topic Analyst + deterministic citation verifier (resolves #21, #22)

## Key source files (fetched from GitHub, selected and truncated for size)

### AGENTS.md

```markdown
## Agent skills

### Issue tracker

Issues and PRDs are tracked as GitHub issues in `owenarnst/ledger`, managed via the `gh` CLI. See `docs/agents/issue-tracker.md`.

### Triage labels

Default canonical vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.

### Domain docs

Single-context: one `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`.

```

### CLAUDE.md

```markdown
## Agent skills

### Issue tracker

Issues and PRDs are tracked as GitHub issues in `owenarnst/ledger`, managed via the `gh` CLI. See `docs/agents/issue-tracker.md`.

### Triage labels

Default canonical vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.

### Domain docs

Single-context: one `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`.

```

### pyproject.toml

```
[project]
name = "ledger"
version = "0.1.0"
description = "Local ownership checks for AI-assisted code"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.115",
    "uvicorn[standard]>=0.30",
]

[project.optional-dependencies]
dev = [
    "httpx>=0.28",
    "pytest>=8.0",
]

[tool.setuptools]
packages = ["backend"]
include-package-data = true

[tool.setuptools.package-data]
# Ship the whole hero-repo fixture tree (source, tests, pyproject, README) so a
# packaged install can copy it into a demo sandbox and run its own test command.
backend = ["fixtures/hero_repo/**/*"]

[tool.pytest.ini_options]
testpaths = ["backend/tests"]
pythonpath = ["."]

```

### frontend/package.json

```
{
  "name": "ledger-frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "build": "tsc --noEmit && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "@vitejs/plugin-react": "^4.3.4",
    "typescript": "^5.6.3",
    "vite": "^5.4.11",
    "vitest": "^2.1.9"
  }
}

```

### backend/fixtures/hero_repo/pyproject.toml

```
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

[project]
name = "docs-search-api"
version = "0.1.0"
description = "A small multi-tenant document retrieval service with deterministic ranking."
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.110",
    "uvicorn>=0.29",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "httpx>=0.27",
]

[tool.setuptools]
packages = ["retrieval"]
py-modules = ["app"]

[tool.pytest.ini_options]
testpaths = ["tests"]
filterwarnings = [
    # Starlette's TestClient emits a deprecation notice about its httpx usage;
    # it is third-party noise unrelated to this service.
    "ignore:Using .httpx. with .starlette.testclient. is deprecated",
]

```

### frontend/src/main.tsx

```typescript
import { createRoot } from 'react-dom/client'
import App from './App'
import './index.css'

const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')

createRoot(rootElement).render(
  <App />
)

```

### backend/fixtures/hero_repo/app.py

```python
"""HTTP layer for the document search service.

Exposes a health check and a single tenant-scoped search endpoint. The tenant
is taken from the ``X-Tenant-ID`` request header; the request body carries only
the query. The corpus is a fixed in-memory store, so the service is fully
deterministic and needs no network access, database, or secrets to run.

This demonstrates tenant-scoped retrieval. It is not a complete authorization
boundary: the tenant header is trusted as supplied.
"""

from __future__ import annotations

from dataclasses import asdict
from typing import Any

from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel, Field

from retrieval.models import Document
from retrieval.pipeline import search
from retrieval.store import DocumentStore


class SearchRequest(BaseModel):
    """Body of a search request. The tenant comes from a header, not here."""

    query: str = Field(min_length=1)


def default_store() -> DocumentStore:
    """Construct the fixed in-memory corpus the service searches."""
    return DocumentStore(
        [
            Document(
                id="alpha-billing",
                tenant_id="alpha",
                score=0.92,
                text="Alpha billing: a refund is issued to the original payment method within five business days.",
            ),
            Document(
                id="alpha-onboarding",
                tenant_id="alpha",
                score=0.74,
                text="Alpha onboarding guide: invite teammates and configure your first project workspace.",
            ),
            Document(
                id="alpha-security",
                tenant_id="alpha",
                score=0.41,
                text="Alpha security overview: documents are encrypted in transit and at rest.",
            ),
            Document(
                id="beta-billing",
                tenant_id="beta",
                score=0.95,
                text="Beta billing: a refund requires manager approval before the payment is reversed.",
            ),
            Document(
                id="beta-onboarding",
                tenant_id="beta",
                score=0.68,
                text="Beta onboarding guide: import existing documents and set retention policies.",
            ),
            Document(
                id="beta-support",
                tenant_id="beta",
                score=0.20,
                text="Beta support: weekday support hours and contact details.",
            ),
        ]
    )


def create_app(store: DocumentStore) -> FastAPI:
    """Build a FastAPI app that searches ``store``."""
    app = FastAPI(title="Docs Search API", version="0.1.0")

    @app.get("/health")
    def health() -> dict[str, str]:
        return {"status": "ok"}

    @app.post("/search")
    def search_documents(
        request: SearchRequest,
        x_tenant_id: str | None = Header(default=None, alias="X-Tenant-ID"),
    ) -> dict[str, Any]:
        if not x_tenant_id:
            raise HTTPException(status_code=400, detail="X-Tenant-ID header is required")
        response = search(store, request.query, tenant_id=x_tenant_id)
        return asdict(response)

    return app


app = create_app(default_store())

```

### frontend/src/App.tsx

```typescript
// Issue #4 — App shell. Holds the demo state machine and routes between the
// three screens. Data is live now: topics, receipts, sandbox files, runs, and
// coaching all come from the FastAPI backend (src/api.js). The persistent rail +
// breadcrumb live here. State-based navigation; react-router can replace it to
// match the SessionStart nudge URL shape (#10) later.
import { useCallback, useEffect, useRef, useState } from 'react'
import Dashboard from './screens/Dashboard'
import Topic from './screens/Topic'
import Workspace from './screens/Workspace'
import * as api from './api'
import { toCards, isReady, testPathFor, Card } from './adapt'
import { parseRoute, buildPath, type Route } from './routing'

const mono = "'JetBrains Mono', monospace"

const sleep = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms))

interface RailProps {
  projectName: string
  topicCount: number
  readyCount: number
  isDemo?: boolean
}

function Rail({ projectName, topicCount, readyCount, isDemo }: RailProps) {
  const label = () => ({
    fontFamily: mono,
    fontSize: 10.5,
    letterSpacing: '0.08em',
    textTransform: 'uppercase',
    color: 'var(--faint)',
    padding: '8px 6px 6px',
  })
  const readyLabel = `${readyCount} ${readyCount === 1 ? 'check' : 'checks'} ready`
  return (
    <aside style={{ width: 248, flex: 'none', borderRight: '1px solid var(--bd)', background: 'var(--panel)', display: 'flex', flexDirection: 'column' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '18px 18px 16px' }}>
        <div style={{ width: 26, height: 26, borderRadius: 7, background: 'var(--accent)', display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }}>
          <svg width="15" height="15" viewBox="0 0 16 16" fill="none">
            <path d="M3 3.2h10M3 6.4h10M3 9.6h7M3 12.8h10" stroke="#1c140f" strokeWidth="1.5" strokeLinecap="round" />
          </svg>
        </div>
        <div style={{ fontWeight: 600, fontSize: 15, letterSpacing: '-0.01em' }}>Ledger</div>
      </div>

      <div style={{ padding: '6px 14px 8px' }}>
        <div style={label()}>Tracked repos</div>
        <div style={{ display: 'flex', alignItems: 'flex-start', gap: 9, padding: '9px 10px', borderRadius: 8, background: 'var(--panel2)', border: '1px solid var(--bd2)' }}>
          <div style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--accent)', marginTop: 6, flex: 'none' }} />
          <div style={{ minWidth: 0 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
              <span style={{ fontWeight: 500, fontSize: 13.5 }}>{projectName || '—'}</span>
              {isDemo && (
                <span
                  style={{
                    fontFamily: mono,
                    fontSize: 9,
                    letterSpacing: '0.08em',
                    color: 'var(--faint)',
                    border: '1px solid var(--bd2)',
                    borderRadius: 4,
                    padding: '1px 5px',
                  }}
                >
                  DEMO
                </span>
              )}
            </div>
            <div style={{ fontSize: 11.5, color: 'var(--mut)', marginTop: 2 }}>
              {topicCount} topics · {readyLabel}
            </div>
          </div>
        </div>
      </div>

      <div style={{ padding: '2px 14px 8px' }}>
        <div style={label()}>Log sources</div>
        {[
          { name: 'Claude Code', path: '~/.claude' },
        ].map((s) => (
          <div key={s.name} style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '7px 10px' }}>
            <span style={{ width: 6, height: 6, borderRadius: '50%', background: 'var(--accent)', flex: 'none' }} />
            <span style={{ fontSize: 13 }}>{s.name}</span>
            <span style={{ marginLeft: 'auto', fontFamily: mono, fontSize: 10, color: 'var(--faint)' }}>{s.path}</span>
          </div>
        ))}
      </div>

      <div style={{ flex: 1 }} />

      <div style={{ margin: 14, padding: '13px', border: '1px dashed var(--bd2)', borderRadius: 9, background: 'rgba(255,255,255,0.012)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 7, color: 'var(--mut)', fontSize: 11, fontFamily: mono, marginBottom: 6 }}>
          <svg width="12" height="12" viewBox="0 0 16 16" fill="none">
            <circle cx="8" cy="8" r="6.2" stroke="currentColor" strokeWidth="1.3" />
            <path d="M8 5.2v3.4M8 10.6h.01" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
          </svg>
          HOW LEDGER SEES THIS REPO
        </div>
        <div style={{ fontSize: 12, color: 'var(--mut)', lineHeight: 1.5 }}>
          Ledger reads the Claude Code logs already on your machine to record what your agents touched.
          Turning that activity into ranked decisions is curated today — automatic discovery is on the roadmap.
        </div>
      </div>
    </aside>
  )
}

interface TopBarProps {
  projectName: string
  crumbTopic?: string
  isWorkspace: boolean
  statsLabel: string
  onExit: () => void
}

function TopBar({ projectName, crumbTopic, isWorkspace, statsLabel, onExit }: TopBarProps) {
  return (
    <div style={{ height: 52, flex: 'none', borderBottom: '1px solid var(--bd)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 22px', background: 'var(--bg)' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 9, fontSize: 13 }}>
        <span style={{ color: 'var(--faint)' }}>Ledger</span>
        <span style={{ color: 'var(--faint)', fontSize: 11 }}>›</span>
        <span style={{ color: 'var(--tx)', fontWeight: 500 }}>{projectName || '—'}</span>
        {crumbTopic && (
          <>
            <span style={{ color: 'var(--faint)', fontSize: 11 }}>›</span>
            <span style={{ color: 'var(--mut)' }}>{crumbTopic}</span>
          </>
        )}
      </div>
      {isWorkspace && (
        <div style={{ displ
[truncated — 20547 more characters]
```

### backend/__init__.py

```python
"""Ledger backend package."""


```

### frontend/vite.config.ts

```typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// Monorepo dev layout (build-plan.md): Vite dev server proxies API calls to uvicorn.
// For the demo build, FastAPI serves the static bundle from dist/.
export default defineConfig({
  plugins: [react()],
  server: {
    host: '0.0.0.0',
    port: 4317,
    allowedHosts: ['gkls-mac-mini.tail42b45.ts.net'],
    proxy: {
      '/api': 'http://localhost:8000',
    },
  },
})

```

[57 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]