@startuml er_system_architecture
!theme plain
skinparam componentStyle rectangle
skinparam roundcorner 8
skinparam packageBackgroundColor #F8F8F8
skinparam packageBorderColor #AAAAAA
skinparam componentBackgroundColor #FAFAFA
skinparam componentBorderColor #888888
skinparam databaseBackgroundColor #EEF4FF
skinparam arrowColor #555555
skinparam noteBorderColor #CCCCCC
skinparam noteBackgroundColor #FFFFF0

title qdrant-lens — System Architecture

' ─── Storage ─────────────────────────────────────────────────────────────────

database "Qdrant\n(vector index)" as qdrant
database "lens.db\n(SQLite or Postgres)" as db
storage ".okf.md files\n& index.md\n(on disk)" as okffiles

' ─── Auth & Identity ──────────────────────────────────────────────────────────

package "Auth & Identity" {
  component "Environment Variables\n─────────────────────\nLENS_API_KEY  ← protects API routes\nLENS_ACTOR    ← default actor label" as envvars

  component "Settings\n(Pydantic)\n──────────────\n.lens_api_key\n.lens_actor" as settings

  component "verify_lens_api_key()\n─────────────────────\nreads  X-Api-Key header\nchecks → LENS_API_KEY\n→ 403 if mismatch" as authfn
}

' ─── Core library ─────────────────────────────────────────────────────────────

package "qdrant_lens/  (core library)" {

  component "QdrantLensClient\n(extends QdrantClient)\n\nIntercepts: upsert, delete,\nset_payload, overwrite_payload,\ndelete_payload, update_vectors,\ndelete_vectors, upload_collection,\nupload_points\n\n_lens_actor → AuditEvent.actor" as client

  component "LensSidestore\n(SQLAlchemy)\n\nlens_events | lens_chunks\nlens_retrievals" as sidestore

  component "LensSchema\n(validator)\n\nvalidate_point()\nenforce()" as schema

  package "Ingest Pipeline" {
    component "discovery.py\ndiscover_files()\n(.lensignore + *.okf.md skip)" as disc
    component "router.py\nselect_parser()\n(text-ratio sampling)" as router
    component "parsers/\nLiteParseParser\nPlainTextParser\nOKFParser\nNotebookParser\nMixedParser / OCRParser" as parsers
    component "chunker.py\nchunk_text()\n(chonkie backend)" as chunker
    component "pipeline.py\n_ingest_file()  — single file\ningest_folder() — post-loop\ndirectory OKF pass" as pipeline
  }

  package "OKF Layer" {
    component "okf_generator.py\ngenerate_okf_files()\n  — per file (ingest_file)\ngenerate_okf_for_directory()\n  — manifest-based (ingest_folder)" as gen
    component "okf_editor.py\nOKFEditor\n  prune_orphans()  <- pure Python\n  fix_index()      <- LLM agent\n  update_sidecar() <- LLM agent" as editor
  }
}

' ─── FastAPI backend ──────────────────────────────────────────────────────────

package "lens_api/  (FastAPI — port 2873)" {

  component "main.py\napp + lifespan\nAPScheduler startup" as apimain

  component "routes/\ncollections | tree | history\nsearch | documents | ingest\nanalytics | graph | chat\nschedules | snapshots | export\nwhoami" as routes

  component "dependencies.py\nget_settings()  — env vars to Settings\nget_qdrant()    — QdrantLensClient\n                  (lens_actor=settings.lens_actor)\nget_db()        — LensSidestore\nverify_lens_api_key()" as deps
}

' ─── React UI ─────────────────────────────────────────────────────────────────

package "lens_ui/  (React — served at /)" {

  component "Views\nTreeView | UploadView\nHistoryView | AnalyticsView\nGraphView | SchedulesView" as views

  component "Components\nHeader (actor pill)\nSidebar | TreeSidebar\nConfirmModal" as comps

  component "useLensStore\n(Zustand)\nconnected | collections\nactor | activeView\nselectedChunk" as store_ui

  component "api/client.js\nfetch wrapper\nhealth | collections | tree\nhistory | search | analytics\ndocuments | ingest/upload\nwhoami | schedules | snapshots" as apiclient
}

' ─── External ─────────────────────────────────────────────────────────────────

component "deepagents\n(FilesystemBackend)" as deepagents
component "fastembed\nTextEmbedding\nSparseTextEmbedding" as fastembed
component "LiteLLM / OpenAI\n/ Anthropic / etc." as llm

' ─── Relationships ─────────────────────────────────────────────────────────────

' Auth & Identity flow
envvars --> settings     : loaded at startup via get_settings()
settings --> authfn      : .lens_api_key
settings --> deps        : .lens_actor flows into\nget_qdrant() → QdrantLensClient(lens_actor=)
authfn --> routes        : Depends(verify_lens_api_key)\nguards all protected routes

' Core data path
client --> schema        : validates _lens payload
client --> sidestore     : write_event() / write_chunk()\n(AuditEvent.actor = _lens_actor)
client --> qdrant        : upsert / delete / scroll / search

sidestore --> db         : SQL read / write

pipeline --> disc
pipeline --> router
pipeline --> parsers
pipeline --> chunker
pipeline --> client      : upsert via QdrantLensClient
pipeline --> gen         : ingest_folder post-loop

gen --> deepagents       : create_deep_agent(FilesystemBackend)
editor --> deepagents    : create_deep_agent(FilesystemBackend)
deepagents --> llm       : LLM API call
gen --> okffiles         : write sidecars + index.md
editor --> okffiles      : prune / rebuild / update

client --> fastembed     : embed_fn (dense + sparse)

apimain --> routes
apimain --> deps
routes --> client        : get_qdrant()
routes --> sidestore     : get_db()

apiclient --> apimain    : HTTP (JSON)
views --> store_ui
store_ui --> apiclient
comps --> store_ui

' /whoami actor resolution: request carries X-Api-Key, route returns actor to UI
routes --> store_ui      : GET /whoami\n→ {actor: settings.lens_actor}\n→ Zustand actor state\n→ Header actor pill

' ─── Notes ─────────────────────────────────────────────────────────────────────

note bottom of gen
  generate_okf_for_directory():
  One agent call per directory.
  Manifest: added | updated |
  deleted_sidecars
  Rebuilds index.md from scratch.
end note

note bottom of editor
  Exposed via CLI:
  lens okf prune
  lens okf fix-index
  lens okf update
end note

note right of authfn
  Actor identity (priority order):
  1. CLI --actor flag
     passed as QdrantLensClient(lens_actor=...)
  2. LENS_ACTOR env var (default: "pipeline")
     loaded into Settings.lens_actor
     get_qdrant() sets it on the client
  Actor is written to every AuditEvent
  and returned by GET /whoami.
end note

@enduml
