drydock-cli 3.0.136__tar.gz → 3.0.137__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.
- {drydock_cli-3.0.136/drydock_cli.egg-info → drydock_cli-3.0.137}/PKG-INFO +1 -1
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/graphrag.py +86 -17
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/tui/app.py +43 -19
- {drydock_cli-3.0.136 → drydock_cli-3.0.137/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/pyproject.toml +1 -1
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_graphrag_sqlite.py +31 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/LICENSE +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/NOTICE +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/README.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/agent.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/nist-ai-rmf.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/nist-csf.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/cci.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/cli.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/config.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/detect.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/events.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/extract.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/guards.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/poam.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/providers.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/recipes.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/skills.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/stig.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/suggest.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/task_state.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/tool_result.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/verification.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock/web.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock_cli.egg-info/SOURCES.txt +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/setup.cfg +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_config.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_events.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_recipes.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_rolling_plan.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_suggest.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_task_state.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_tool_result.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.136 → drydock_cli-3.0.137}/tests/test_xccdf.py +0 -0
|
@@ -270,16 +270,16 @@ def _iter_text_files(paths: list[str]):
|
|
|
270
270
|
yield fp
|
|
271
271
|
|
|
272
272
|
|
|
273
|
-
def
|
|
274
|
-
"""
|
|
275
|
-
|
|
276
|
-
|
|
273
|
+
def _iter_file_chunks(paths, cwd, skip_sources, progress=None):
|
|
274
|
+
"""Yield (rel_source, [(body, entities), …]) for each ingestible file, one file
|
|
275
|
+
at a time (so callers never hold every file's text at once). Per-file isolation:
|
|
276
|
+
one unreadable/odd file (a locked Word doc, weird encoding, PDF quirk) is skipped,
|
|
277
|
+
not fatal. Calls progress(files_done, rel) after each file so a long build can
|
|
278
|
+
report instead of appearing frozen."""
|
|
277
279
|
cleaned = [_unquote(p) for p in paths]
|
|
280
|
+
files_done = 0
|
|
278
281
|
for fp in _iter_text_files([str(Path(cwd) / p) if not os.path.isabs(p) else p
|
|
279
282
|
for p in cleaned]):
|
|
280
|
-
# Per-file isolation: ONE unreadable/odd file (a locked Word doc, a weird
|
|
281
|
-
# encoding, a PDF backend quirk) must never crash a whole-folder build —
|
|
282
|
-
# skip it and keep indexing the rest.
|
|
283
283
|
try:
|
|
284
284
|
rel = os.path.relpath(str(fp), cwd)
|
|
285
285
|
if rel in skip_sources:
|
|
@@ -289,14 +289,25 @@ def _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip_sources):
|
|
|
289
289
|
else:
|
|
290
290
|
text = fp.read_text("utf-8", "ignore")
|
|
291
291
|
if not text or not text.strip():
|
|
292
|
-
continue
|
|
293
|
-
# Build this file's chunks locally; commit only if it ALL succeeds so a
|
|
294
|
-
# mid-file failure leaves no partial state and isn't miscounted.
|
|
292
|
+
continue
|
|
295
293
|
local = [(body, extract_entities(body)) for body in _chunk_text(text)]
|
|
296
294
|
except Exception: # noqa: BLE001 — isolate a bad file, never abort the build
|
|
297
295
|
continue
|
|
296
|
+
skip_sources.add(rel)
|
|
297
|
+
files_done += 1
|
|
298
|
+
if progress:
|
|
299
|
+
try:
|
|
300
|
+
progress(files_done, rel)
|
|
301
|
+
except Exception: # noqa: BLE001 — a progress callback must never break a build
|
|
302
|
+
pass
|
|
303
|
+
yield rel, local
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip_sources, progress=None):
|
|
307
|
+
"""In-memory ingest into the accumulators (legacy JSON path — small indexes)."""
|
|
308
|
+
added = 0
|
|
309
|
+
for rel, local in _iter_file_chunks(paths, cwd, skip_sources, progress):
|
|
298
310
|
added += 1
|
|
299
|
-
skip_sources.add(rel) # don't double-ingest the same file in one call
|
|
300
311
|
for body, ents in local:
|
|
301
312
|
cid = len(chunks)
|
|
302
313
|
chunks.append({"id": cid, "source": rel, "text": body, "entities": ents})
|
|
@@ -309,6 +320,56 @@ def _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip_sources):
|
|
|
309
320
|
return added
|
|
310
321
|
|
|
311
322
|
|
|
323
|
+
def _build_sqlite_stream(paths, store_path, cwd, skip_sources, progress=None, append=False):
|
|
324
|
+
"""Streaming SQLite build: chunk text is inserted per-file (never held for the
|
|
325
|
+
whole corpus, so memory stays bounded even for a huge folder). Entities/edges
|
|
326
|
+
aggregate in memory (far smaller than the text) and are written at the end."""
|
|
327
|
+
sp = Path(store_path)
|
|
328
|
+
sp.parent.mkdir(parents=True, exist_ok=True)
|
|
329
|
+
if not append and sp.exists():
|
|
330
|
+
sp.unlink()
|
|
331
|
+
con = sqlite3.connect(str(sp))
|
|
332
|
+
entity_chunks: dict[str, set[int]] = defaultdict(set)
|
|
333
|
+
edges: dict[str, Counter] = defaultdict(Counter)
|
|
334
|
+
added = 0
|
|
335
|
+
try:
|
|
336
|
+
con.executescript(_SCHEMA)
|
|
337
|
+
# continue chunk ids after any existing rows (append mode)
|
|
338
|
+
row = con.execute("SELECT COALESCE(MAX(id), -1) FROM chunks").fetchone()
|
|
339
|
+
next_id = (row[0] if row else -1) + 1
|
|
340
|
+
if append: # seed the in-memory graph with what's already stored
|
|
341
|
+
for r in con.execute("SELECT entity, chunk_id FROM entities"):
|
|
342
|
+
entity_chunks[r[0]].add(r[1])
|
|
343
|
+
for r in con.execute("SELECT a, b, weight FROM edges"):
|
|
344
|
+
edges[r[0]][r[1]] = r[2]
|
|
345
|
+
con.execute("DELETE FROM entities"); con.execute("DELETE FROM edges")
|
|
346
|
+
for rel, local in _iter_file_chunks(paths, cwd, skip_sources, progress):
|
|
347
|
+
added += 1
|
|
348
|
+
rows = []
|
|
349
|
+
for body, ents in local:
|
|
350
|
+
cid = next_id; next_id += 1
|
|
351
|
+
con.execute("INSERT INTO chunks(id,source,text) VALUES(?,?,?)", (cid, rel, body))
|
|
352
|
+
con.execute("INSERT INTO chunks_fts(rowid,text) VALUES(?,?)", (cid, body))
|
|
353
|
+
for e in ents:
|
|
354
|
+
entity_chunks[e].add(cid)
|
|
355
|
+
for i, a in enumerate(ents):
|
|
356
|
+
for b in ents[i + 1:]:
|
|
357
|
+
edges[a][b] += 1; edges[b][a] += 1
|
|
358
|
+
rows.append(cid)
|
|
359
|
+
if added % 200 == 0:
|
|
360
|
+
con.commit()
|
|
361
|
+
con.executemany("INSERT INTO entities(entity,chunk_id) VALUES(?,?)",
|
|
362
|
+
((e, cid) for e, cids in entity_chunks.items() for cid in cids))
|
|
363
|
+
con.executemany("INSERT INTO edges(a,b,weight) VALUES(?,?,?)",
|
|
364
|
+
((a, b, w) for a, nbrs in edges.items() for b, w in nbrs.items()))
|
|
365
|
+
con.execute("INSERT OR REPLACE INTO meta(key,value) VALUES('version','2')")
|
|
366
|
+
con.commit()
|
|
367
|
+
return {"files": added, "chunks": next_id, "entities": len(entity_chunks),
|
|
368
|
+
"edges": sum(len(n) for n in edges.values()) // 2}
|
|
369
|
+
finally:
|
|
370
|
+
con.close()
|
|
371
|
+
|
|
372
|
+
|
|
312
373
|
def _save(store_path, chunks, entity_chunks, edges, files_added):
|
|
313
374
|
# SQLite by default (scalable). A caller can still target a .json path
|
|
314
375
|
# explicitly (small/portable indexes, tests) and get the legacy format.
|
|
@@ -334,13 +395,17 @@ def _save(store_path, chunks, entity_chunks, edges, files_added):
|
|
|
334
395
|
}
|
|
335
396
|
|
|
336
397
|
|
|
337
|
-
def build_index(paths: list[str], store_path: str | Path, *, cwd: str = "."
|
|
398
|
+
def build_index(paths: list[str], store_path: str | Path, *, cwd: str = ".",
|
|
399
|
+
progress=None) -> dict:
|
|
338
400
|
"""Build (or REBUILD from scratch) the knowledge graph from paths; persist to
|
|
339
|
-
store_path. Returns {files, chunks, entities, edges}.
|
|
401
|
+
store_path. Returns {files, chunks, entities, edges}. `progress(files_done, src)`
|
|
402
|
+
is called per file (for a UI). SQLite stores stream to disk (bounded memory)."""
|
|
403
|
+
if _is_sqlite(store_path):
|
|
404
|
+
return _build_sqlite_stream(paths, store_path, cwd, set(), progress, append=False)
|
|
340
405
|
chunks: list[dict] = []
|
|
341
406
|
entity_chunks: dict[str, set[int]] = defaultdict(set)
|
|
342
407
|
edges: dict[str, Counter] = defaultdict(Counter)
|
|
343
|
-
added = _ingest_files(paths, cwd, chunks, entity_chunks, edges, set())
|
|
408
|
+
added = _ingest_files(paths, cwd, chunks, entity_chunks, edges, set(), progress)
|
|
344
409
|
return _save(store_path, chunks, entity_chunks, edges, added)
|
|
345
410
|
|
|
346
411
|
|
|
@@ -366,13 +431,17 @@ def _read_all(store_path) -> dict:
|
|
|
366
431
|
return json.loads(Path(sp).read_text("utf-8"))
|
|
367
432
|
|
|
368
433
|
|
|
369
|
-
def add_to_index(paths: list[str], store_path: str | Path, *, cwd: str = "."
|
|
434
|
+
def add_to_index(paths: list[str], store_path: str | Path, *, cwd: str = ".",
|
|
435
|
+
progress=None) -> dict:
|
|
370
436
|
"""Incrementally ADD documents to an existing index (build it if none yet).
|
|
371
437
|
Files already indexed (by relative path) are skipped — clear+build to refresh
|
|
372
438
|
changed files. Returns {files (added), chunks, entities, edges} totals."""
|
|
373
439
|
sp = _resolve_store(store_path)
|
|
374
440
|
if not sp.exists():
|
|
375
|
-
return build_index(paths, store_path, cwd=cwd)
|
|
441
|
+
return build_index(paths, store_path, cwd=cwd, progress=progress)
|
|
442
|
+
if _is_sqlite(sp):
|
|
443
|
+
skip = {r[0] for r in sqlite3.connect(str(sp)).execute("SELECT DISTINCT source FROM chunks")}
|
|
444
|
+
return _build_sqlite_stream(paths, sp, cwd, skip, progress, append=True)
|
|
376
445
|
existing = _read_all(sp)
|
|
377
446
|
chunks: list[dict] = list(existing.get("chunks", []))
|
|
378
447
|
entity_chunks: dict[str, set[int]] = defaultdict(set)
|
|
@@ -382,7 +451,7 @@ def add_to_index(paths: list[str], store_path: str | Path, *, cwd: str = ".") ->
|
|
|
382
451
|
for a, nbrs in existing.get("edges", {}).items():
|
|
383
452
|
edges[a] = Counter(nbrs)
|
|
384
453
|
skip = {c["source"] for c in chunks}
|
|
385
|
-
added = _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip)
|
|
454
|
+
added = _ingest_files(paths, cwd, chunks, entity_chunks, edges, skip, progress)
|
|
386
455
|
return _save(sp, chunks, entity_chunks, edges, added)
|
|
387
456
|
|
|
388
457
|
|
|
@@ -1069,6 +1069,44 @@ class DrydockApp(App):
|
|
|
1069
1069
|
"(recent turns are always preserved)."
|
|
1070
1070
|
)
|
|
1071
1071
|
|
|
1072
|
+
def _graphrag_build(self, sub: str, rest: str, store, cwd: str) -> None:
|
|
1073
|
+
"""Worker: build/add a knowledge base off the UI thread, streaming a
|
|
1074
|
+
throttled progress line so a huge folder never looks frozen."""
|
|
1075
|
+
from drydock import graphrag
|
|
1076
|
+
import time as _t
|
|
1077
|
+
|
|
1078
|
+
last = [0.0]
|
|
1079
|
+
|
|
1080
|
+
def progress(files_done: int, src: str) -> None:
|
|
1081
|
+
now = _t.monotonic()
|
|
1082
|
+
if now - last[0] >= 1.0: # throttle UI updates to ~1/s
|
|
1083
|
+
last[0] = now
|
|
1084
|
+
self.call_from_thread(
|
|
1085
|
+
self.query_one("#working", Static).update,
|
|
1086
|
+
f" ⚓ indexing… {files_done} files ({src[-48:]})")
|
|
1087
|
+
|
|
1088
|
+
try:
|
|
1089
|
+
fn = graphrag.build_index if sub == "build" else graphrag.add_to_index
|
|
1090
|
+
stats = fn([rest], store, cwd=cwd, progress=progress)
|
|
1091
|
+
except Exception as e: # noqa: BLE001 — surface, never crash the TUI
|
|
1092
|
+
self.call_from_thread(self.query_one("#working", Static).update, "")
|
|
1093
|
+
self.call_from_thread(self._mount, ErrorMessage(f"graphrag {sub} failed: {e}"))
|
|
1094
|
+
return
|
|
1095
|
+
self.call_from_thread(self.query_one("#working", Static).update, "")
|
|
1096
|
+
if sub == "add" and stats["files"] == 0:
|
|
1097
|
+
self.call_from_thread(
|
|
1098
|
+
self._info, f"No new documents under {rest} (already indexed, or no text found).")
|
|
1099
|
+
return
|
|
1100
|
+
if not stats["chunks"]:
|
|
1101
|
+
self.call_from_thread(self._info, f"No text found under {rest}. Nothing was indexed.")
|
|
1102
|
+
return
|
|
1103
|
+
verb2 = "built" if sub == "build" else f"updated (+{stats['files']} new files)"
|
|
1104
|
+
self.call_from_thread(
|
|
1105
|
+
self._info,
|
|
1106
|
+
f"✓ Knowledge base {verb2}: {stats['chunks']} chunks · "
|
|
1107
|
+
f"{stats['entities']} entities · {stats['edges']} edges.\n"
|
|
1108
|
+
f"Stored at {store}. The agent draws on it via the Knowledge tool.")
|
|
1109
|
+
|
|
1072
1110
|
def _cmd_graphrag(self, arg: str) -> None:
|
|
1073
1111
|
"""Build / inspect / clear the project's GraphRAG knowledge base. Once
|
|
1074
1112
|
built, the agent retrieves from it via the read-only Knowledge tool."""
|
|
@@ -1085,25 +1123,11 @@ class DrydockApp(App):
|
|
|
1085
1123
|
self._info(f"usage: /graphrag {sub} <path> (a file or directory of docs/code)")
|
|
1086
1124
|
return
|
|
1087
1125
|
verb = "Rebuilding" if sub == "build" else "Ingesting into"
|
|
1088
|
-
self._info(f"{verb} knowledge base from {rest} …"
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
self._mount(ErrorMessage(f"graphrag {sub} failed: {e}"))
|
|
1094
|
-
return
|
|
1095
|
-
if sub == "add" and stats["files"] == 0:
|
|
1096
|
-
self._info(f"No new documents under {rest} (already indexed, or no text found).")
|
|
1097
|
-
return
|
|
1098
|
-
if not stats["chunks"]:
|
|
1099
|
-
self._info(f"No text found under {rest}. Nothing was indexed.")
|
|
1100
|
-
return
|
|
1101
|
-
verb2 = "built" if sub == "build" else f"updated (+{stats['files']} new files)"
|
|
1102
|
-
self._info(
|
|
1103
|
-
f"✓ Knowledge base {verb2}: {stats['chunks']} chunks · "
|
|
1104
|
-
f"{stats['entities']} entities · {stats['edges']} edges.\n"
|
|
1105
|
-
f"Stored at {store}. The agent draws on it via the Knowledge tool."
|
|
1106
|
-
)
|
|
1126
|
+
self._info(f"{verb} knowledge base from {rest} … (runs in the background; "
|
|
1127
|
+
"progress below — the TUI stays responsive)")
|
|
1128
|
+
# Run OFF the UI thread so a large folder can't freeze the interface,
|
|
1129
|
+
# and stream progress (files indexed) back so it never looks locked up.
|
|
1130
|
+
self.run_worker(lambda: self._graphrag_build(sub, rest, store, cwd), thread=True)
|
|
1107
1131
|
elif sub == "query":
|
|
1108
1132
|
if not rest:
|
|
1109
1133
|
self._info("usage: /graphrag query <question> (test what the KB returns)")
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# PyPI distribution name is drydock-cli (the established install name, continued
|
|
8
8
|
# from the retired v2 fork); the import package + CLI command stay `drydock`.
|
|
9
9
|
name = "drydock-cli"
|
|
10
|
-
version = "3.0.
|
|
10
|
+
version = "3.0.137"
|
|
11
11
|
description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -68,3 +68,34 @@ def test_resolve_prefers_db_then_json():
|
|
|
68
68
|
|
|
69
69
|
def test_missing_store_returns_none():
|
|
70
70
|
assert g.load_index("/no/such/graphrag.db") is None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_streaming_build_reports_progress():
|
|
74
|
+
"""SQLite build streams to disk and calls progress per file (so the UI can show
|
|
75
|
+
it and never looks frozen)."""
|
|
76
|
+
d, docs = _docs()
|
|
77
|
+
open(os.path.join(docs, "c.md"), "w").write("# More\nExtra content about billing.")
|
|
78
|
+
seen = []
|
|
79
|
+
store = os.path.join(d, ".drydock", "graphrag.db")
|
|
80
|
+
stats = g.build_index([docs], store, cwd=d, progress=lambda n, s: seen.append((n, s)))
|
|
81
|
+
assert stats["files"] == 3 and len(seen) == 3
|
|
82
|
+
assert seen[-1][0] == 3 # last progress reports 3 files done
|
|
83
|
+
# index is queryable
|
|
84
|
+
assert g.query_index(g.load_index(store), "billing", k=2)["chunks"]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_streaming_append_skips_existing_and_reports():
|
|
88
|
+
d, docs = _docs()
|
|
89
|
+
store = os.path.join(d, ".drydock", "graphrag.db")
|
|
90
|
+
g.build_index([docs], store, cwd=d)
|
|
91
|
+
open(os.path.join(docs, "n.md"), "w").write("# New\nThe RefundBot processes claims.")
|
|
92
|
+
seen = []
|
|
93
|
+
s2 = g.add_to_index([docs], store, cwd=d, progress=lambda n, s: seen.append(s))
|
|
94
|
+
assert s2["files"] == 1 and seen == ["docs/n.md"] # only the new file ingested
|
|
95
|
+
assert g.query_index(g.load_index(store), "refundbot claims", k=2)["chunks"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_build_without_progress_is_backward_compatible():
|
|
99
|
+
d, docs = _docs()
|
|
100
|
+
store = os.path.join(d, ".drydock", "graphrag.db")
|
|
101
|
+
assert g.build_index([docs], store, cwd=d)["files"] == 2 # no progress arg → still works
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|