@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 "Env Vars\n──────────\nLENS_ADMIN_KEY\nLENS_API_KEY\nLENS_ACTOR" as envvars

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

  component "get_current_user()\n──────────────────\n1. Admin-key bypass\n2. DB bcrypt lookup\n→ 401 if unmatched\n\nrequire_role(level)\nviewer<developer\n<owner<admin" 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 | lens_users" 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(chunk_size, overlap, min_tokens)\n_merge_micro_chunks() — floor enforcement\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)\nFields: topics | entities\n        audience | key_questions\n        description (3-5 sentences)" 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 (context_chunks, filter_doc_ids)\ndocuments (/chunks endpoint) | ingest\nanalytics | graph | chat\nschedules | snapshots | export\nusers | whoami" as routes

  component "dependencies.py\nget_settings()     — env vars to Settings\nget_qdrant()       — QdrantLensClient\n                     (lens_actor=settings.lens_actor)\nget_db()           — LensSidestore\nget_current_user() — admin-key bypass or DB lookup\nrequire_role()     — role-level guard" as deps
}

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

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

  component "Views\nTreeView | UploadView\nHistoryView | AnalyticsView\nGraphView | SchedulesView\nAdminView (owner/admin only)" 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(get_current_user)\nrequire_role() guards each route\nby level (viewer/developer/owner/admin)

' 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.

  New OKF fields stored in _lens:
  okf_topics | okf_entities
  okf_audience | okf_key_questions
  Also at payload root (topics, entities)
  for graph edge construction.
end note

note bottom of routes
  Graph edge types (build_graph()):
  same_doc       — chunks of same doc_id
  same_tags      — shared payload tags
  same_author    — shared author field
  same_directory — shared doc_id prefix
  same_topic     — shared OKF topics[]
  same_entity    — shared OKF entities[]

  Search extras (SearchRequest):
  context_chunks  — fetch N adjacent chunks per hit
  filter_doc_ids  — restrict to specific doc_ids
  max_per_doc     — cap chunks per document
end note

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

note bottom of authfn
  Actor priority:
  1. --actor CLI flag
  2. LENS_ACTOR env var
  3. OS username (getpass)
  4. "unknown"

  Roles: viewer<developer<owner<admin
  Admin key bypasses DB lookup.
  lens_users: bcrypt-hashed keys.
end note

@enduml
