@startuml er_data_model
!theme plain
skinparam linetype ortho
skinparam roundcorner 8
skinparam entityBackgroundColor #FAFAFA
skinparam entityBorderColor #AAAAAA
skinparam noteBorderColor #CCCCCC
skinparam noteBackgroundColor #FFFFF0

title qdrant-lens — Data Model

' ─── Qdrant side ─────────────────────────────────────────────────────────────

entity "QdrantPoint\n(in-cluster)" as point {
  * id : UUID
  --
  vector : float[]
  sparse_vector.indices : int[]
  sparse_vector.values : float[]
  ..._lens payload namespace...
  * _lens.doc_id : TEXT
  * _lens.doc_type : TEXT
  * _lens.source : TEXT
  * _lens.ingested_at : ISO8601
  --
  _lens.chunk_index : INT
  _lens.chunk_hash : TEXT (sha256:...)
  _lens.ingested_by : TEXT
  _lens.chunk_tokens : INT
  _lens.parser_used : TEXT
  _lens.language : TEXT
  _lens.line_start : INT
  _lens.line_end : INT
  _lens.ocr_used : BOOL
  _lens.is_okf : BOOL
  _lens.original_format : TEXT
  --
  text : TEXT
}

' ─── Sidestore (SQLite / Postgres) ──────────────────────────────────────────

entity "lens_chunks\n(sidestore)" as chunk {
  * chunk_id : TEXT <<PK>>
  --
  * chunk_hash : TEXT
  * collection : TEXT
  * doc_id : TEXT
  * doc_type : TEXT
  * source : TEXT
  chunk_index : INT
  * text : TEXT
  vector : TEXT (JSON float[])
  sparse_indices : TEXT (JSON int[])
  sparse_values : TEXT (JSON float[])
  * ingested_at : TEXT
  * actor : TEXT
  pipeline_version : TEXT
  extra : TEXT (JSON)
}

entity "lens_events\n(sidestore)" as event {
  * event_id : TEXT <<PK>>
  --
  * action : TEXT
  * collection : TEXT
  * actor : TEXT
  doc_ids : TEXT (JSON string[])
  chunk_count : INT
  prev_hash : TEXT
  new_hash : TEXT
  * timestamp : TEXT
  pipeline_version : TEXT
  extra : TEXT (JSON)
}

entity "lens_retrievals\n(sidestore)" as retrieval {
  * retrieval_id : TEXT <<PK>>
  --
  * collection : TEXT
  * query_text : TEXT
  * search_mode : TEXT
  * chunk_ids : TEXT (JSON string[])
  * doc_ids : TEXT (JSON string[])
  result_count : INT
  actor : TEXT
  * timestamp : TEXT
}

' ─── OKF side (on disk) ──────────────────────────────────────────────────────

entity "OKF Sidecar\n(<filename>.okf.md)" as okf {
  * path : <source-file>.okf.md
  --
  frontmatter.type : code|pdf|markdown|text|notebook|slide
  frontmatter.title : TEXT
  * frontmatter.description : TEXT
  frontmatter.tags : TEXT[]
  * frontmatter.resource : TEXT (= doc_id)
  frontmatter.timestamp : ISO8601
  --
  body : TEXT
  related_concepts : TEXT[] (links)
}

entity "index.md\n(per directory)" as index {
  * path : <dir>/index.md
  --
  # <directory> Knowledge Index
  --
  ## Code — [Title](file.py.okf.md)
  ## Markdown — [Title](file.md.okf.md)
  ## PDFs — [Title](file.pdf.okf.md)
  ## Notebooks — [Title](nb.ipynb.okf.md)
  ## Text — [Title](file.txt.okf.md)
}

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

point ||--o{ chunk : "mirrored to sidestore\n(chunk_id shared)"
point }o--|| event : "upsert / delete\ntriggers AuditEvent"
retrieval }o--|{ point : "search returns\npoint IDs"
okf }|--|| point : "resource = _lens.doc_id\n(conceptual link)"
okf }o--|| index : "listed in\ndirectory index"

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

note bottom of event
  action values:
  upsert | delete | restore
end note

note bottom of retrieval
  search_mode values:
  dense | sparse |
  hybrid-rrf | hybrid-weighted
end note

note right of okf
  Sidecar naming:
  readme.txt → readme.txt.okf.md
  auth.py    → auth.py.okf.md
  (strip .okf.md to recover source)
end note

@enduml
