okfgraph 0.2.4__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. {okfgraph-0.2.4 → okfgraph-0.2.6}/PKG-INFO +1 -1
  2. {okfgraph-0.2.4 → okfgraph-0.2.6}/README.md +4 -3
  3. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/cli.py +6 -3
  4. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/import_.py +34 -20
  5. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph.egg-info/PKG-INFO +1 -1
  6. {okfgraph-0.2.4 → okfgraph-0.2.6}/pyproject.toml +1 -1
  7. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_chunking.py +17 -16
  8. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_cli.py +8 -0
  9. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_router.py +10 -6
  10. {okfgraph-0.2.4 → okfgraph-0.2.6}/LICENSE +0 -0
  11. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/__init__.py +0 -0
  12. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/__init__.py +0 -0
  13. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/converters.py +0 -0
  14. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/delta.py +0 -0
  15. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/diff.py +0 -0
  16. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/doctor.py +0 -0
  17. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/embedding.py +0 -0
  18. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/export.py +0 -0
  19. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/image_assets.py +0 -0
  20. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/ingest.py +0 -0
  21. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/links.py +0 -0
  22. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/lint.py +0 -0
  23. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/purge.py +0 -0
  24. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/ranking.py +0 -0
  25. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/schema.py +0 -0
  26. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/components/search.py +0 -0
  27. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/config.py +0 -0
  28. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/images.py +0 -0
  29. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/mcp_server.py +0 -0
  30. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/models.py +0 -0
  31. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/router.py +0 -0
  32. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/security.py +0 -0
  33. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph/tools.py +0 -0
  34. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph.egg-info/SOURCES.txt +0 -0
  35. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph.egg-info/dependency_links.txt +0 -0
  36. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph.egg-info/entry_points.txt +0 -0
  37. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph.egg-info/requires.txt +0 -0
  38. {okfgraph-0.2.4 → okfgraph-0.2.6}/okfgraph.egg-info/top_level.txt +0 -0
  39. {okfgraph-0.2.4 → okfgraph-0.2.6}/setup.cfg +0 -0
  40. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_chunk_search.py +0 -0
  41. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_config.py +0 -0
  42. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_converter.py +0 -0
  43. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_delta.py +0 -0
  44. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_diff.py +0 -0
  45. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_directory_hash.py +0 -0
  46. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_doctor.py +0 -0
  47. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_export_compliance.py +0 -0
  48. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_gpu_integration.py +0 -0
  49. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_graph_enrichment.py +0 -0
  50. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_images.py +0 -0
  51. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_ingest.py +0 -0
  52. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_ingest_tool.py +0 -0
  53. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_integration.py +0 -0
  54. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_lint.py +0 -0
  55. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_logging.py +0 -0
  56. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_mcp_server.py +0 -0
  57. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_models.py +0 -0
  58. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_obsidian.py +0 -0
  59. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_okf_ingest_tool.py +0 -0
  60. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_parity.py +0 -0
  61. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_pdf_e2e.py +0 -0
  62. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_ppr_search.py +0 -0
  63. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_ranking.py +0 -0
  64. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_reconstruction.py +0 -0
  65. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_reserved.py +0 -0
  66. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_roundup_cli.py +0 -0
  67. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_router_misc.py +0 -0
  68. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_rust_backend.py +0 -0
  69. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_rust_e2e.py +0 -0
  70. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_sanitize.py +0 -0
  71. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_search_browser.py +0 -0
  72. {okfgraph-0.2.4 → okfgraph-0.2.6}/tests/test_security.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: okfgraph
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Ladybug-backed OKF knowledge graph with ONNX + Jina v5 embeddings
5
5
  License-Expression: Apache-2.0 OR MIT
6
6
  Requires-Python: >=3.11
@@ -1,4 +1,4 @@
1
- # OKFgraph 0.2.4
1
+ # OKFgraph 0.2.5
2
2
 
3
3
  [![PyPI](https://img.shields.io/pypi/v/okfgraph)](https://pypi.org/project/okfgraph/)
4
4
  [![Python](https://img.shields.io/badge/python-%3E%3D3.11-blue)](https://www.python.org/)
@@ -49,7 +49,7 @@ a swappable `DocumentConverter` seam. What isn't needed isn't installed.
49
49
  Requires Python ≥ 3.11.
50
50
 
51
51
  ```bash
52
- pip install "okfgraph[pdf,omni]" # PyPI release (okf-embed ships platform wheels)
52
+ pip install "okfgraph[pdf,omni]" # PyPI (okf-embed ships platform wheels; published)
53
53
  ```
54
54
 
55
55
  Or from source with `uv` (also builds the `okf-embed` Rust wheel from
@@ -273,6 +273,7 @@ Key design decisions:
273
273
  ```bash
274
274
  uv run --project . pytest tests/ -q # full suite (model loads; takes a while)
275
275
  uv run --project . pytest tests/test_ranking.py tests/test_mcp_server.py -q # fast subset
276
+ cd rust/okf-embed && cargo test --locked # Rust unit tests (pure, no model)
276
277
  ```
277
278
 
278
279
  Golden fixtures under `tests/fixtures/` (`ppr_graph`, `diff_a`/`diff_b`,
@@ -297,7 +298,7 @@ okfgraph/
297
298
  │ ├── images.py # IngestMode (text|optional|omni), planning helpers
298
299
  │ ├── security.py # SSRF/domain guards for remote images
299
300
  │ ├── tools.py # legacy tool definitions (superseded by mcp_server)
300
- │ └── components/ # ranking, links, search, import_, export, diff, doctor,
301
+ │ └── components/ # ranking, links, search, lint, import_, export, diff, doctor,
301
302
  │ # embedding, converters, image_assets, delta, purge, schema, ingest
302
303
  ├── rust/okf-embed/ # Jina v5 Rust loader (ort, CUDA-opportunistic) + Python wheel
303
304
  ├── skills/ # okfgraph-mcp, okfgraph-cli, okfgraph-ingest (harness-neutral source)
@@ -695,7 +695,10 @@ def _ingest(args):
695
695
  """
696
696
  logger = logging.getLogger("cli")
697
697
  router = _router(args)
698
- kind = getattr(args, "kind", "pdf") or "pdf"
698
+ kind = getattr(args, "kind", None)
699
+ if kind not in ("md", "pdf", "thoughts"):
700
+ print("[ERROR] --kind is required: md|pdf|thoughts")
701
+ return
699
702
  tags = args.tags.split(",") if getattr(args, "tags", None) else None
700
703
  if kind == "md":
701
704
  md_file = getattr(args, "md_file", None)
@@ -1171,8 +1174,8 @@ def build_parser():
1171
1174
  p = sub.add_parser("ingest", help="Add content: markdown file, PDF, or thoughts")
1172
1175
  _add_global(p)
1173
1176
  _add_logging_flags(p)
1174
- p.add_argument("--kind", default="pdf", choices=["md", "pdf", "thoughts"],
1175
- help="What to ingest (default: pdf)")
1177
+ p.add_argument("--kind", required=True, choices=["md", "pdf", "thoughts"],
1178
+ help="What to ingest")
1176
1179
  p.add_argument("--md-file", default=None, help="Markdown file (--kind md)")
1177
1180
  p.add_argument("--pdf-file", default=None, help="PDF file (--kind pdf)")
1178
1181
  p.add_argument("--thoughts", default=None, help="Raw reasoning text (--kind thoughts)")
@@ -145,6 +145,36 @@ class ImportManager:
145
145
  self.image_mgr = image_mgr
146
146
  self.purge_mgr = purge_mgr
147
147
 
148
+ def _merge_directory_contains(self, parent_id: str, child_id: str):
149
+ """Create a Directory→Directory CONTAINS edge without crashing Ladybug.
150
+
151
+ Ladybug 0.20.3 segfaults when one Cypher statement combines an
152
+ existing-node ``MERGE``, a new-node ``MERGE``, and a relationship
153
+ ``MERGE`` on a long-lived connection. Separate idempotent statements
154
+ preserve the same graph while avoiding that planner path.
155
+ """
156
+ self.conn.execute("MERGE (p:Directory {id: $id})", {"id": parent_id})
157
+ self.conn.execute("MERGE (d:Directory {id: $id})", {"id": child_id})
158
+ self.conn.execute("""
159
+ MATCH (p:Directory {id: $parent}), (d:Directory {id: $child})
160
+ MERGE (p)-[:CONTAINS]->(d)
161
+ """, {"parent": parent_id, "child": child_id})
162
+
163
+ def _merge_concept_contains(self, parent_id: str, child_id: str):
164
+ """Create a Directory→Concept CONTAINS edge without crashing Ladybug.
165
+
166
+ See ``_merge_directory_contains``: a single three-clause ``MERGE`` can
167
+ segfault ladybug 0.20.3 after matching an existing Directory and
168
+ creating the next sibling Concept. The separate statements below are
169
+ semantically equivalent.
170
+ """
171
+ self.conn.execute("MERGE (d:Directory {id: $id})", {"id": parent_id})
172
+ self.conn.execute("MERGE (c:Concept {id: $id})", {"id": child_id})
173
+ self.conn.execute("""
174
+ MATCH (d:Directory {id: $parent}), (c:Concept {id: $child})
175
+ MERGE (d)-[:CONTAINS]->(c)
176
+ """, {"parent": parent_id, "child": child_id})
177
+
148
178
  def _batch_build_directories(self, cids: List[str]):
149
179
  """Build directory hierarchy for a batch of concept IDs.
150
180
 
@@ -166,11 +196,7 @@ class ImportManager:
166
196
  for d in sorted_dirs:
167
197
  parent = "/".join(d.split("/")[:-1]) if "/" in d else None
168
198
  if parent and parent in dir_paths:
169
- self.conn.execute("""
170
- MERGE (p:Directory {id: $parent})
171
- MERGE (d:Directory {id: $child})
172
- MERGE (p)-[:CONTAINS]->(d)
173
- """, {"parent": parent, "child": d})
199
+ self._merge_directory_contains(parent, d)
174
200
  elif parent:
175
201
  # Parent is root (not a directory node)
176
202
  self.conn.execute("""
@@ -186,11 +212,7 @@ class ImportManager:
186
212
  parts = cid.split("/")
187
213
  if len(parts) > 1:
188
214
  parent_dir = "/".join(parts[:-1])
189
- self.conn.execute("""
190
- MERGE (d:Directory {id: $parent})
191
- MERGE (c:Concept {id: $child})
192
- MERGE (d)-[:CONTAINS]->(c)
193
- """, {"parent": parent_dir, "child": cid})
215
+ self._merge_concept_contains(parent_dir, cid)
194
216
 
195
217
 
196
218
  def _load_link_index(self):
@@ -867,17 +889,9 @@ class ImportManager:
867
889
  parent = "/".join(path_parts[:i])
868
890
  child = "/".join(path_parts[: i + 1])
869
891
  if i == len(path_parts) - 1:
870
- self.conn.execute("""
871
- MERGE (d:Directory {id: $parent})
872
- MERGE (c:Concept {id: $child})
873
- MERGE (d)-[:CONTAINS]->(c)
874
- """, {"parent": parent, "child": child})
892
+ self._merge_concept_contains(parent, child)
875
893
  else:
876
- self.conn.execute("""
877
- MERGE (p:Directory {id: $parent})
878
- MERGE (d:Directory {id: $child})
879
- MERGE (p)-[:CONTAINS]->(d)
880
- """, {"parent": parent, "child": child})
894
+ self._merge_directory_contains(parent, child)
881
895
 
882
896
  # NOTE: link extraction intentionally lives outside _insert_concept
883
897
  # (callers run _extract_links_for_concept after the node exists), so
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: okfgraph
3
- Version: 0.2.4
3
+ Version: 0.2.6
4
4
  Summary: Ladybug-backed OKF knowledge graph with ONNX + Jina v5 embeddings
5
5
  License-Expression: Apache-2.0 OR MIT
6
6
  Requires-Python: >=3.11
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "okfgraph"
7
- version = "0.2.4"
7
+ version = "0.2.6"
8
8
  description = "Ladybug-backed OKF knowledge graph with ONNX + Jina v5 embeddings"
9
9
  requires-python = ">=3.11"
10
10
  license = "Apache-2.0 OR MIT"
@@ -449,26 +449,27 @@ class TestPhase5_CLI:
449
449
  actual commands with real data and verify output formatting.
450
450
  """
451
451
 
452
- def test_cli_has_search_chunks_command(self):
452
+ def test_removed_granular_commands_gone(self, capsys):
453
+ # search-chunks/chunks/reconstruct were folded into search/read in the
454
+ # 0.2.0 16→5 consolidation; assert argparse rejects them outright.
453
455
  from okfgraph.cli import build_parser
454
456
  parser = build_parser()
455
- args = parser.parse_args(["search-chunks", "test"])
456
- assert args.command == "search-chunks"
457
- assert args.query == "test"
458
-
459
- def test_cli_has_chunks_command(self):
460
- from okfgraph.cli import build_parser
461
- parser = build_parser()
462
- args = parser.parse_args(["chunks", "some_id"])
463
- assert args.command == "chunks"
464
- assert args.concept_id == "some_id"
465
-
466
- def test_cli_has_reconstruct_command(self):
457
+ for dead in ("search-chunks", "chunks", "reconstruct"):
458
+ with pytest.raises(SystemExit):
459
+ parser.parse_args([dead, "test"])
460
+ capsys.readouterr() # swallow argparse usage noise
461
+
462
+ def test_replacement_verbs_cover_removed_commands(self):
463
+ # search --target chunks ≙ search-chunks; read --include chunks ≙ chunks;
464
+ # read --include document ≙ reconstruct.
467
465
  from okfgraph.cli import build_parser
468
466
  parser = build_parser()
469
- args = parser.parse_args(["reconstruct", "some_id"])
470
- assert args.command == "reconstruct"
471
- assert args.concept_id == "some_id"
467
+ args = parser.parse_args(["search", "test", "--target", "chunks"])
468
+ assert args.command == "search" and args.target == "chunks"
469
+ args = parser.parse_args(["read", "--include", "chunks", "some_id"])
470
+ assert args.concept_id == "some_id" and args.include == "chunks"
471
+ args = parser.parse_args(["read", "--include", "document", "some_id"])
472
+ assert args.concept_id == "some_id" and args.include == "document"
472
473
 
473
474
  def test_cli_has_chunking_options(self):
474
475
  from okfgraph.cli import build_parser
@@ -74,6 +74,14 @@ class TestCLIHelp:
74
74
  assert result.returncode == 0
75
75
  assert "--kind" in result.stdout
76
76
 
77
+ def test_ingest_kind_required(self):
78
+ result = subprocess.run(
79
+ [sys.executable, "-m", "okfgraph.cli", "ingest", "--md-file", "note.md"],
80
+ capture_output=True, text=True,
81
+ )
82
+ assert result.returncode == 2
83
+ assert "--kind" in result.stderr
84
+
77
85
  def test_export_help(self):
78
86
  result = subprocess.run(
79
87
  [sys.executable, "-m", "okfgraph.cli", "export", "--help"],
@@ -166,17 +166,21 @@ class TestDeviceSelection:
166
166
  assert r.device == "cpu"
167
167
  assert r.encoder.used_cuda is False
168
168
 
169
- def test_device_cuda_uses_gpu_when_available(self, tmp_path, capsys):
169
+ def test_device_cuda_uses_gpu_when_available(self, tmp_path, caplog):
170
+ # The fallback warning travels via logging (bound to the real stderr
171
+ # fd at handler creation), so capsys can never see it — assert on the
172
+ # log record instead. Deterministic on GPU and CPU machines alike.
173
+ import logging
170
174
  from okfgraph.router import OKFRouter
171
- r = OKFRouter(db_path=str(tmp_path / "test.db"), bundle_root=str(tmp_path), device="cuda")
175
+ with caplog.at_level(logging.WARNING, logger="okfgraph.router"):
176
+ r = OKFRouter(db_path=str(tmp_path / "test.db"), bundle_root=str(tmp_path), device="cuda")
172
177
  assert r.device == "cuda"
173
- err = capsys.readouterr().err
174
178
  if r.encoder.used_cuda:
175
179
  # Happy path: GPU active, no fallback warning.
176
- assert "CUDA" not in err
180
+ assert not any("CUDA" in m for m in caplog.messages)
177
181
  else:
178
- # CPU fallback: the Rust core warns on stderr.
179
- assert "CUDA" in err
182
+ # CPU fallback: the router warns about the missing EP.
183
+ assert any("no CUDA execution provider" in m for m in caplog.messages)
180
184
 
181
185
  def test_device_cuda_warning_only_once(self, tmp_path, capsys):
182
186
  from okfgraph.router import OKFRouter
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes