From ee28b7302fd2737f22fc14d68817a23ff87a30f5 Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Fri, 19 Jun 2026 14:25:31 +0200 Subject: [PATCH] =?UTF-8?q?docs(wiki):=20decide=20vision=20service=20packa?= =?UTF-8?q?ging=20=E2=80=94=20apps/vision/=20in=20the=20monorepo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settle WHERE the host-side ANPR service lives and how it joins the build: in this monorepo at apps/vision/ (not a separate repo), still a separate OS process called over localhost HTTP, wired into the Turbo graph via a thin package.json shim whose scripts shell to Python tooling (uv/uvicorn/ruff/pytest). Co-located source honors the vision-service runtime+license isolation decision (AGPL reach is a linking boundary, not a folder); the fast-alpr MIT baseline removes most of the split-repo pressure anyway. New page vision-service-packaging; updates vision-service, opencv-anpr-service, the CLAUDE.md layout, index, log. Not built yet — packaging decision only. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V --- CLAUDE.md | 3 +- wiki/decisions/vision-service-packaging.md | 95 ++++++++++++++++++++++ wiki/decisions/vision-service.md | 4 +- wiki/entities/opencv-anpr-service.md | 4 +- wiki/index.md | 3 +- wiki/log.md | 4 + 6 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 wiki/decisions/vision-service-packaging.md diff --git a/CLAUDE.md b/CLAUDE.md index 9e3d578..f798134 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,8 @@ parking-system/ ├── turbo.json ├── apps/ │ ├── server/ # Fastify backend (device drivers, API, auth); serves the SPA -│ └── web/ # React + Vite SPA (operator UI) +│ ├── web/ # React + Vite SPA (operator UI) +│ └── vision/ # Python/FastAPI ANPR service (planned; separate process, Turbo shim — see wiki/decisions/vision-service-packaging.md) ├── packages/ │ ├── db/ # Drizzle ORM schema + migrations (SQLite local; PostgreSQL sync target) │ ├── devices/ # device adapters behind shared interfaces (reader/printer/relay) diff --git a/wiki/decisions/vision-service-packaging.md b/wiki/decisions/vision-service-packaging.md new file mode 100644 index 0000000..97f5cfd --- /dev/null +++ b/wiki/decisions/vision-service-packaging.md @@ -0,0 +1,95 @@ +--- +type: decision +tags: [parking, decisions, vision, anpr, monorepo, packaging] +sources: [] +updated: 2026-06-19 +status: settled +--- + +# Decision: the vision service lives in this monorepo (apps/vision/), wired into Turbo via a shim + +Taken 2026-06-19, when planning how to *implement* the host-side [[opencv-anpr-service|vision +service]] decided in [[vision-service]]. That decision settled WHAT (a separate localhost Python +process) and the recognizer baseline ([[opencv-anpr-service|fast-alpr]]); this one settles WHERE the +source lives and how it joins the build. + +## Decision + +1. **In THIS monorepo, at `apps/vision/`** — a Python/FastAPI service co-located with the Node + backend, **not** a separate repository. One git history, atomic cross-cutting commits (the + `/analyze` contract + the Node-side adapter change together), one wiki. +2. **Still a separate OS process** — co-location is source-level only. It runs as its own process + (`uvicorn`), called over **localhost HTTP** by the Node backend, with its own failure domain. + Nothing about putting it in `apps/vision/` weakens the runtime isolation [[vision-service]] + requires. +3. **Wired into the Turbo task graph via a thin `package.json` shim.** `pnpm-workspace.yaml` already + globs `apps/*`, so an `apps/vision/package.json` auto-joins the workspace. Its `scripts` shell out + to Python tooling, so the existing `turbo run` tasks cover it: + - `dev` → `uv run uvicorn app:app --reload` (matches `turbo.json` `dev`: persistent, uncached) + - `lint` → `ruff check` · `test` → `pytest` · `typecheck` → `ruff`/`mypy` + - `build` → **no-op or model-fetch** (Python has no `dist/**`; the `build` task's `outputs: + ["dist/**"]` simply won't match — fine). If models are fetched/cached at build, point outputs at + the model dir. + Python **dependencies** stay managed by `uv` + `pyproject.toml` (NOT pnpm) — the shim only exposes + *tasks*, not deps. +4. **Node talks to it through an interface** (`VisionClient` behind a port, the + [[device-adapter-pattern]] style) so the recognizer/service is swappable without touching business + logic — as [[opencv-anpr-service]] already specifies. + +## Why co-located beats a separate repo + +- **Atomic changes.** The service contract (`POST /analyze` shape) and its Node consumer evolve + together; one repo = one PR, no two-repo version skew. +- **`uv` makes Python-in-monorepo painless** — fast, lockfile-based, offline-friendly (fits + [[offline-first]]); the appliance build pulls a pinned env. +- **Turbo still orchestrates it.** The shim makes `turbo run lint`/`test` include the Python service + as a first-class node — one command lints front, back, AND vision — even though Turbo can't *build* + Python. Turbo orchestrates **tasks**, and a task can be a Python command. +- **One knowledge base.** The wiki + CLAUDE.md already describe the whole system; a split repo + fragments that. + +## Why this still honors the isolation decision + +The "[[vision-service|separate process]]" decision is about **runtime isolation** (own process + +failure domain) and **license isolation** (AGPL obligations don't reach the Node/React code because +it is **not linked** — it's a separate program over HTTP). **Neither depends on a separate +repository.** AGPL's reach is a linking/distribution-boundary question between *programs*, not a +which-folder question. A Python service in `apps/vision/` that Node calls over localhost is exactly as +isolated, license-wise, as one in its own repo. + +- With the **[[opencv-anpr-service|fast-alpr]] MIT-end-to-end baseline**, the AGPL pressure to split + the repo out **largely evaporates** (pending the weight-provenance caveat). Co-location is the + low-friction default. +- If a true-AGPL model (Ultralytics YOLO) is later adopted, its weights live under `apps/vision/` — + still fine (separate process), and that dir is the natural place to document the license boundary + + the `[[standing-decisions|scoped exception]]`. + +## Rejected + +- **Separate repo** — strongest separation, but loses atomic contract changes and adds coordination + overhead; justified only if a different team owns it or the AGPL concern becomes acute. Kept as the + fallback if either happens. +- **Embed Python in the Node process** (opencv4nodejs / a child-process module) — already rejected by + [[vision-service]] (native-build pain, no process isolation, shares the app's failure + license + surface). Unchanged. +- **A Python package under `packages/`** — `packages/` is for shared *JS* libraries imported by other + workspaces; the vision service is a deployable app, so `apps/vision/` is the right bucket. + +## Consequences + +- Add `apps/vision/` (`pyproject.toml` + `uv.lock`, FastAPI `app.py`, a thin `package.json` shim); + `apps/*` glob picks it up. Update the repo-layout block in the root `CLAUDE.md` + this wiki. +- A `Dockerfile`/process unit builds the Python service as its own image/process for the appliance; + CI runs `ruff`/`pytest` (via the shim or a dedicated job). +- The Node backend gains a `VisionClient` adapter (localhost HTTP) + per-camera opt-in wiring (the + open item in [[opencv-anpr-service]]). +- **Not built yet** — this is the packaging decision; scaffolding follows when the vision work starts + (the "scaffold as the work reaches them" rule in CLAUDE.md). + +## Open + +- `uv` vs. `pip-tools`/`poetry` for the Python env (leaning `uv` — speed + lockfile + offline). +- Whether `build` should fetch/cache model weights (and set Turbo `outputs` to the model dir) or keep + weights out of the build entirely (baked into the Docker image instead). +- Container/runtime supervision on the appliance (systemd unit vs. compose) — deployment detail, + defer to the install/hardening pass. diff --git a/wiki/decisions/vision-service.md b/wiki/decisions/vision-service.md index d74f8db..05ca02d 100644 --- a/wiki/decisions/vision-service.md +++ b/wiki/decisions/vision-service.md @@ -21,7 +21,9 @@ Taken 2026-06-15, as part of the business-layer build ([[session-model]]). option). 3. **Deployment: a separate local Python/OpenCV microservice** on the appliance, called over **localhost HTTP** by the Node backend. Fully offline ([[offline-first]]); its own process and - failure domain; the host falls back to the ticket path if it's unavailable. + failure domain; the host falls back to the ticket path if it's unavailable. **Source lives in THIS + monorepo at `apps/vision/`, wired into Turbo via a thin `package.json` shim** — separate *process*, + co-located *source*; see [[vision-service-packaging]]. 4. **Licensing exception:** AGPL components (e.g. YOLO plate/vehicle models, OpenALPR) are **permitted inside this service only**, because it's a separate process not linked into the app — the app stays strictly MIT/Apache/BSD. Amends [[standing-decisions]]. diff --git a/wiki/entities/opencv-anpr-service.md b/wiki/entities/opencv-anpr-service.md index 0978858..32f672f 100644 --- a/wiki/entities/opencv-anpr-service.md +++ b/wiki/entities/opencv-anpr-service.md @@ -34,7 +34,9 @@ recognition **host-side on ordinary IP-camera snapshots**, replacing the dedicat - A **Python service** (e.g. FastAPI) running **on the appliance**, called by the Node backend over **localhost HTTP** (`POST /analyze` with the JPEG bytes the camera driver already pulls — see - [[lpr-camera]] "driver/storage boundary": `Snapshot.bytes`). + [[lpr-camera]] "driver/storage boundary": `Snapshot.bytes`). **Source lives in this monorepo at + `apps/vision/`** (Turbo shim; `uv`-managed deps) — co-located source, separate process; see + [[vision-service-packaging]]. - **Fully offline** ([[offline-first]]): all inference is local, no cloud. Model weights ship on the appliance. - **Process isolation is deliberate** — it keeps a heavy Python/native/AGPL stack out of the diff --git a/wiki/index.md b/wiki/index.md index a1fcfb5..6a37c10 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -7,7 +7,7 @@ updated: 2026-06-19 # Index Content catalog for the wiki. Start at [[overview]]. Maintained on every ingest. -Counts: 4 sources · 19 entities · 44 concepts · 5 decision records. +Counts: 4 sources · 19 entities · 44 concepts · 6 decision records. ## Overview & navigation - [[overview]] — the top-level synthesis and entry point. @@ -115,4 +115,5 @@ Counts: 4 sources · 19 entities · 44 concepts · 5 decision records. - [[dingtian-vs-mqtt]] — transport choice: direct HTTP/UDP now, MQTT parked until multi-lane scale. - [[session-model]] — business layer start: session = projection; transient-first; pay-on-foot. New event types. - [[vision-service]] — build a host-side ANPR + vehicle-verification service; replaces edge-LPR; scoped AGPL exception. +- [[vision-service-packaging]] — the vision service lives in this monorepo (apps/vision/), separate process, wired into Turbo via a package.json shim; uv-managed Python. - [[event-streams-split]] — split the signed business ledger (ledger_events) from unsigned device telemetry (device_events). diff --git a/wiki/log.md b/wiki/log.md index f682cec..945e529 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -908,3 +908,7 @@ Added a THIRD data stream (`app_logs`) alongside the signed ledger and device te ## [2026-06-19] query | ANPR recognizer options — fast-alpr evaluated as the baseline Q: LPR/ANPR options — YOLO, OpenCV, both, another framework? Reframed: "YOLO vs OpenCV" is a category error — they're different pipeline LAYERS (YOLO = plate detector; OpenCV = Apache-2.0 image-handling glue, used regardless; plus an OCR stage). The real choice is which end-to-end recognizer. Researched [fast-alpr](https://github.com/ankandrew/fast-alpr) (latest **v0.4.0, 15 Mar 2026, MIT**): a thin orchestrator over two swappable ONNX stages — detection via [open-image-models](https://github.com/ankandrew/open-image-models) (`yolo-v9-t-384-license-plate-end2end`, MIT) + OCR via [fast-plate-ocr](https://github.com/ankandrew/fast-plate-ocr) (`cct-xs-v2-global-model`, MIT; also has a EUROPEAN model trained on 40+ countries — relevant for AL plates). MIT top-to-bottom (code AND published weights), one maintainer across all three repos, CPU-only + fully offline, backend extras for CPU/CUDA/OpenVINO/DirectML/QNN. KEY FINDING: its detector is open-image-models' OWN YOLOv9 ONNX export, NOT the Ultralytics AGPL package — so fast-alpr is a PERMISSIVE baseline that may not even need the scoped AGPL exception from [[vision-service]]. CAVEAT (flagged, not closed): a repo's LICENSE covers code, not necessarily redistributed model WEIGHTS (YOLOv9 upstream is GPL-3.0; Ultralytics YOLO AGPL) — verify weight provenance before relying on "MIT weights". fast-alpr is PLATE-ONLY → Job 2 (vehicle-attribute anti-spoofing) is still ours to build, but shares the same ONNX runtime. Recommendation: prototype fast-alpr now; Ultralytics-YOLO+PaddleOCR fine-tune only if accuracy disappoints. Recorded as an evaluated-options note; decision kept status:open pending the provenance check + an AL-plate accuracy benchmark. Updated [[opencv-anpr-service]] (new "Recognizer evaluation" section + licensing nuance), [[vision-service]] (open/next), index. + +## [2026-06-19] decision | Vision service packaging — apps/vision/ in this monorepo, Turbo shim + +Q: how to IMPLEMENT the vision service — can we use this Turborepo? Settled (status:settled): the Python/FastAPI ANPR service lives in THIS monorepo at `apps/vision/`, NOT a separate repo. Key clarification: Turbo orchestrates JS/TS package.json TASKS (+ caches outputs); it has no native Python build — but "in the repo" ≠ "in the Turbo graph", and "separate process" ≠ "separate repo". Decision: (1) co-locate source at apps/vision/ (pnpm-workspace already globs apps/*, so it auto-joins) for atomic cross-cutting changes (the /analyze contract + the Node adapter together), one wiki/history; (2) still a SEPARATE OS process (uvicorn over localhost HTTP) — co-location is source-level only, runtime isolation intact; (3) wire into Turbo via a THIN package.json shim whose scripts shell to Python (dev→uv run uvicorn, lint→ruff, test→pytest, build→no-op/model-fetch since Python has no dist/**), so `turbo run lint/test` covers vision too — deps stay uv/pyproject, not pnpm; (4) Node talks to it via a VisionClient interface (device-adapter style), swappable. WHY co-location honors the [[vision-service]] isolation decision: that decision is about RUNTIME + LICENSE isolation (separate process; AGPL doesn't reach Node because it's not LINKED, just HTTP) — AGPL's reach is a linking/distribution-boundary question, NOT a which-folder question. And with the MIT-end-to-end [[opencv-anpr-service|fast-alpr]] baseline the AGPL pressure to split the repo largely evaporates anyway. Rejected: separate repo (loses atomic changes; fallback if AGPL acute or another team owns it), embed-in-Node (already rejected by vision-service), packages/ (that's for shared JS libs, vision is a deployable app). NOT built yet — packaging decision only; scaffold when vision work starts. New page [[vision-service-packaging]]; updated [[vision-service]], [[opencv-anpr-service]], CLAUDE.md layout, index.