loosecanvas / CONTRIBUTING.md
Joshua Sundance Bailey
loosecanvas: local AI thought-mapping canvas with a trust-tagged knowledge graph
6d1438c
|
Raw
History Blame Contribute Delete
9.71 kB
# Contributing to loosecanvas
This file is the single-source operational reference for anyone working in this repo β€” human, Claude Code, or Copilot agent. [CLAUDE.md](CLAUDE.md) and [AGENTS.md](AGENTS.md) add agent-specific framing; they do NOT duplicate the content here.
## Commands
All from the repo root unless noted. Call the venv by absolute path; do not `cd` or mutate `PATH` (frontend builds pollute the shared terminal). If `.venv` is missing, bootstrap with `uv sync` (`uv.lock` is committed β€” reproducible install); the runtime always calls `.\.venv\Scripts\python.exe` by absolute path.
```powershell
& .\.venv\Scripts\python.exe -m pytest tests/ -v # all tests (PYTHONPATH=src via pyproject)
& .\.venv\Scripts\python.exe -m pytest tests/test_reducer.py # one file
& .\.venv\Scripts\python.exe -m pytest tests/test_reducer.py::test_x # one test (use --lf to stay on failures)
& .\.venv\Scripts\ruff.exe check src tests --fix # lint + autofix
& .\.venv\Scripts\python.exe -m mypy src # type-check (strict)
docker compose up # llama.cpp server (needs .env) + Prometheus :9090 + Grafana :3000
$env:PYTHONPATH = "src"; & .\.venv\Scripts\python.exe -m uvicorn loosecanvas.main:app --port 8000 # THE APP
```
Frontend (run from `cytoscapecanvas/`):
```powershell
& ..\.venv\Scripts\gradio.exe cc dev --python-path ..\.venv\Scripts\python.exe # component-only demo (7860/7861), NOT the app
& ..\.venv\Scripts\gradio.exe cc build --python-path ..\.venv\Scripts\python.exe # build wheel into backend/.../templates/
npm run typecheck # tsc --noEmit (from cytoscapecanvas/frontend/)
npm run lint # eslint . (from cytoscapecanvas/frontend/)
npm test # vitest (from cytoscapecanvas/frontend/)
```
Prefer the VS Code tasks (exact ids): `fastapi: dev server`, `frontend: gradio cc dev`, `frontend: gradio cc build`, `pytest: run all`, `ruff: check + fix`, `docker: llama-server up` / `down`, `dev: start all (watch + server)`.
## Two different servers β€” do NOT confuse them
- **The app** (graph + chat + LLM turn loop) = uvicorn on **:8000**. If the user is testing the app, this is the only correct server.
- `gradio cc dev` on :7860/7861 is a bare component demo (`cytoscapecanvas/demo/app.py`) β€” no LLM, no backend. Use it ONLY when hacking on `cytoscapecanvas/frontend/*.svelte|*.ts`. Stray demo processes pile up; kill them via `Get-CimInstance Win32_Process -Filter "Name='python.exe'"` β†’ `Stop-Process`.
- `uvicorn --reload` cannot be trusted here: reload is blocked while a Gradio browser tab holds the SSE connection open, so backend edits silently don't apply mid-session (the `fastapi: dev server` task does pass `--reload`, but this still bites). To apply backend changes, kill the uvicorn terminal and restart it.
## Quality gate = definition of done
**Backend:** `pytest` green + `ruff` clean + `mypy --strict` clean; coverage floor is 70% enforced via `pytest --cov-fail-under=70`.
**Frontend items additionally:** `npm run typecheck` + `npm run lint` + `npm test` + `gradio cc build` succeeding.
Enforced by pre-commit ([.pre-commit-config.yaml](.pre-commit-config.yaml)): ruff, black, mypy, prettier, eslint, actionlint, gitleaks, bandit, typos.
Pre-commit gotchas:
- The mypy hook is a `repo: local` hook running the project `.venv` (must be active; do not reintroduce `mirrors-mypy` β€” it resolves pydantic to `Any` and floods false errors).
- Black wraps at **88** while ruff allows 120, so inline `# noqa`/`# nosec` can get wrapped off its line β€” prefer pyproject `per-file-ignores`.
- A module-level attribute docstring (triple-quoted string after a top-level assignment) trips `check-docstring-first` once black wraps the assignment β€” use a `#` comment instead.
## Conventions
- **Imports use the package root**: `from loosecanvas.contracts import Node` β€” never `from contracts import Node` (src layout; `pythonpath = ["src"]`).
- **Pytest** `asyncio_mode = "auto"` β€” `async def test_*` needs no marker.
- Ruff line-length 120; selects security (`S`), naming (`N`), datetimez (`DTZ`), no-print (`T20`), no-commented-code (`ERA`). `yaml.safe_load` only.
- Config is env-driven via `.env` (see [.env.example](.env.example)): `LLAMA_CPP_IMAGE/PORT/CTX`, `GEMMA_MODEL_FILENAME`, `APP_PORT` β€” plus `LOOSECANVAS_ALLOWED_BASE` (note: `.env.example`'s previous `ALLOWED_BASE_DIR=` line is drift; the code never reads that name). (`TRACE_LOGGING` was removed as PoC legacy.)
- Delegate recon to read-only subagents to keep the main context lean. Parallelize independent module builds only in **disjoint-file batches** (each agent edits only its module + test, scopes its lint/type gate to its own files); the coordinator runs the full `mypy src` + `pytest` reconcile at the end.
- Long shell captures lie: redirect to `logs/x.log` and read the file; don't treat an empty capture as "no output".
## Tests
41+ flat files in `tests/`, named `test_<module>.py` per source module; `tests/conftest.py` holds shared autouse fixtures (session-store/lock isolation) and shared builder helpers β€” individual files keep additional fixtures inline. The LLM boundary is faked with `httpx.MockTransport`; the default suite needs **no live server**. Live-LLM integration tests (`test_llm_integration.py`, `test_turn_loop_integration.py`) are module-level-skipped unless `LOOSECANVAS_RUN_LLM_INTEGRATION=1` and the server is reachable; the real-repo corpus test reads `LOOSECANVAS_TEST_REPO` (must resolve under `ALLOWED_BASE_DIR`). Graph fixtures live in `fixtures/` (`small_graph.json`, `annotated_graph.json`). For live runs, redirect output to a file under `logs/` and read the file β€” don't trust terminal scrollback.
## Frontend rules (`cytoscapecanvas/`, from .github/instructions/frontend.instructions.md)
- **Svelte 5 runes only** β€” no legacy `export let` / `$:` reactivity.
- **Never `.innerHTML`** (ESLint-enforced); use `.textContent`.
- **Explicit positions always**: every `cy.add` gets a backend-supplied `position: {x, y}`; init with `preset` layout (no reflow).
- Fog uses `events: 'no'` (CSS `pointer-events` doesn't apply to canvas).
- Patch application is idempotent: dedupe by monotonic `patch_id`; guard `add_element` with `cy.getElementById(id).length`.
- Structure: `Index.svelte` is a thin shell; `renderer.ts` is pure, host-agnostic TypeScript; `types.ts` defines the TS `RendererCommand` discriminated union corresponding to the flat M18 Pydantic model (`op` field) in `contracts.py` β€” keep them in sync.
- Component changes reach the app only after `gradio cc build` (the app imports the editable-installed `gradio_cytoscapecanvas` wheel).
## Pitfalls
- **Gradio `cc` toolchain on Windows** needs `PYTHONUTF8=1`, the `.vscode/bin` `python3` shim, and explicit `--python-path` (the VS Code tasks set these). `gradio cc create` prints an emoji banner that crashes cp1252 _after_ writing files β€” don't re-run on that error.
- **llama.cpp structured output is build-sensitive.** Use `/v1/chat/completions` + the OpenAI-standard `response_format` wrapper (`{"type": "json_schema", "json_schema": {"name": …, "strict": true, "schema": …}}`); the bare shape is silently ignored. Disable thinking per request (`chat_template_kwargs={"enable_thinking": false}`) β€” with thinking on, structured calls return empty content. Never trust HTTP 200: grammar failures fail open, so always `model_validate` the body and reject on parse failure. Schemas are inlined (no `$defs` refs) with `maxLength` on every string field. Full matrix in [plan/03-resolved-foundational-decisions.md](plan/03-resolved-foundational-decisions.md) (Q2). Server flags live in [docker-compose.yaml](docker-compose.yaml) (Q3): `-ngl all`, `--flash-attn on`, `--jinja` (mandatory for tool calling), `--reasoning off`, `--parallel 1`. `LLAMA_CPP_CTX` defaults to 262144 (`.env.example`) β€” a deliberate long-context trade-off; 65536 is the measured lower-latency fallback on 16 GB VRAM.
- **Long shell captures lie**: redirect to `logs/x.log` and read the file; don't treat an empty capture as "no output".
- **The terminal's decorated multi-line prompt can swallow `run_in_terminal` output.** The interactive PowerShell prompt confuses shell-integration output markers β€” prefer the dedicated tools for inspection.
## Security design notes
### SEC-08: LLM trust boundary
Repo file content (node labels, summaries, edge labels) flows into LLM context unsanitized **by design**. Three independent layers prevent exploitation:
1. **Validator existence checks** on all IDs (`validator.py`) β€” the LLM cannot reference nodes/edges that do not exist in the current graph state.
2. **Immutable `origin="model_inferred"`** on all LLM claims (`reducer.py`, plan/06) β€” model output is permanently marked and never silently elevated to a higher-trust origin.
3. **Strict Pydantic `ScenePlan` validation** (`sceneplan_generator.py`) β€” structural well-formedness is enforced before any action reaches the reducer.
Residual risk is LLM semantic interpretation (prompt injection in file content), not a validation bypass. Input sanitization is intentionally absent β€” adding it would be false assurance against that residual without fixing it.
### EFF-05: Intentional immutability pattern
`SceneState` uses `model_copy(update={"node_positions": dict(scene.node_positions)})` (a shallow copy with a fresh dict) as a deliberate immutability/diff-safety pattern. The two `deep=True` copies (undo snapshot + restore) are also intentional. These are NOT candidates for a mutable-scene refactor.