mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-27 07:52:03 +00:00
Merge branch 'overstory/builder-atlas-handoff/SeedPass-63f4' into beta
# Conflicts: # src/seedpass/tui_v3/widgets/inspector.py
This commit is contained in:
commit
f2cbb1b7ba
3 files changed
+171
-7
No files matched your search
@@ -1815,6 +1815,126 @@ class SearchService:
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
def graph_pivot_results(
|
||||
self,
|
||||
pivot_entry_id: int,
|
||||
*,
|
||||
pivot_scope: str | None = None,
|
||||
direction: str = "both",
|
||||
include_archived: bool = True,
|
||||
limit: int = 50,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Return neighbors of ``pivot_entry_id`` as :class:`SearchResult`-compatible dicts.
|
||||
|
||||
Wraps graph-traversal results in the unified SearchResult model so that
|
||||
atlas-pivot navigation produces results with proper ``match_reasons``,
|
||||
``score``, and relation context — identical in shape to
|
||||
:meth:`search` output and ready for any UI surface that consumes it.
|
||||
|
||||
Scoring:
|
||||
- Direct (hop-1) outgoing links score highest (~1.0).
|
||||
- Incoming links get a small direction penalty (−0.1).
|
||||
- Each additional hop beyond 1 reduces the score by 0.3.
|
||||
"""
|
||||
with self._lock:
|
||||
scope_path = self._scope_path()
|
||||
target_id = int(pivot_entry_id or 0)
|
||||
if target_id <= 0:
|
||||
return []
|
||||
|
||||
neighbors = self.linked_neighbors(
|
||||
target_id,
|
||||
direction=direction,
|
||||
include_archived=include_archived,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
em = getattr(self._manager, "entry_manager", None)
|
||||
results: list[dict[str, Any]] = []
|
||||
|
||||
for neighbor in neighbors:
|
||||
neighbor_id = int(neighbor.get("entry_id", 0) or 0)
|
||||
if neighbor_id <= 0:
|
||||
continue
|
||||
|
||||
# Fetch full entry for excerpt / meta
|
||||
entry: dict[str, Any] = {}
|
||||
if em is not None:
|
||||
try:
|
||||
entry = em.retrieve_entry(neighbor_id) or {}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
kind = str(neighbor.get("kind", "")).strip()
|
||||
relation = str(neighbor.get("relation", "")).strip()
|
||||
direction_val = str(neighbor.get("direction", "")).strip()
|
||||
hop = int(neighbor.get("hop", 1) or 1)
|
||||
|
||||
# Score: hop-1 outgoing ≈ 1.0, incoming −0.1, each extra hop −0.3
|
||||
hop_penalty = (hop - 1) * 0.3
|
||||
direction_penalty = 0.1 if direction_val == "incoming" else 0.0
|
||||
total_score = round(max(0.05, 1.0 - hop_penalty - direction_penalty), 6)
|
||||
|
||||
match_reasons: list[str] = ["graph_pivot"]
|
||||
if relation:
|
||||
match_reasons.append(f"relation:{relation}")
|
||||
if direction_val:
|
||||
match_reasons.append(f"direction:{direction_val}")
|
||||
if pivot_scope:
|
||||
match_reasons.append(f"atlas:{pivot_scope}")
|
||||
|
||||
# Relation context visible as meta string
|
||||
arrow = "->" if direction_val == "outgoing" else "<-"
|
||||
meta_parts: list[str] = []
|
||||
if relation:
|
||||
meta_parts.append(f"{arrow}[{relation}]")
|
||||
if pivot_scope:
|
||||
meta_parts.append(f"atlas:{pivot_scope}")
|
||||
meta_str = " ".join(meta_parts) or self._meta(entry, kind)
|
||||
|
||||
modified_ts = int(entry.get("modified_ts", 0) or 0)
|
||||
|
||||
result = SearchResult(
|
||||
entry_id=neighbor_id,
|
||||
label=str(neighbor.get("label", "")).strip(),
|
||||
kind=kind,
|
||||
scope_path=scope_path,
|
||||
archived=bool(neighbor.get("archived", False)),
|
||||
score=total_score,
|
||||
score_breakdown={
|
||||
"lexical": 0.0,
|
||||
"semantic": 0.0,
|
||||
"structural": total_score,
|
||||
"recency": 0.0,
|
||||
},
|
||||
match_reasons=sorted(set(match_reasons)),
|
||||
excerpt=(
|
||||
""
|
||||
if kind in self._SECRET_EXCERPT_KINDS
|
||||
else self._safe_excerpt(entry, kind)
|
||||
),
|
||||
linked_hits=[
|
||||
{
|
||||
"target_id": target_id,
|
||||
"relation": relation,
|
||||
}
|
||||
],
|
||||
tags=list(neighbor.get("tags", [])),
|
||||
modified_ts=modified_ts,
|
||||
meta=meta_str,
|
||||
).model_dump()
|
||||
|
||||
results.append(result)
|
||||
|
||||
results.sort(
|
||||
key=lambda r: (
|
||||
-float(r.get("score", 0.0)),
|
||||
str(r.get("label", "")).lower(),
|
||||
int(r.get("entry_id", 0)),
|
||||
)
|
||||
)
|
||||
return results[: max(1, int(limit))]
|
||||
|
||||
|
||||
class AtlasService:
|
||||
"""Thread-safe wrapper around canonical atlas/index0 read operations."""
|
||||
|
||||
@@ -114,6 +114,17 @@ class AtlasWayfinderScreen(Screen):
|
||||
classes="maintenance-footer",
|
||||
)
|
||||
|
||||
def _propagate_atlas_context(self, scope_path: str) -> None:
|
||||
"""Set atlas_source_scope on the inspector LinkedItemsPanel if present."""
|
||||
if not scope_path:
|
||||
return
|
||||
try:
|
||||
from ..widgets.inspector import LinkedItemsPanel # noqa: PLC0415
|
||||
panel = self.app.query_one(LinkedItemsPanel)
|
||||
panel.atlas_source_scope = scope_path
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
button_id = event.button.id or ""
|
||||
if button_id == "atlas-filter-all":
|
||||
@@ -133,6 +144,7 @@ class AtlasWayfinderScreen(Screen):
|
||||
entry_id = int(button_id.removeprefix("atlas-open-entry-"))
|
||||
except ValueError:
|
||||
return
|
||||
self._propagate_atlas_context(str(self.payload.get("scope_path", "")))
|
||||
self.app.selected_entry_id = entry_id
|
||||
self.app.pop_screen()
|
||||
return
|
||||
@@ -141,5 +153,6 @@ class AtlasWayfinderScreen(Screen):
|
||||
entry_id = int(button_id.removeprefix("atlas-open-recent-"))
|
||||
except ValueError:
|
||||
return
|
||||
self._propagate_atlas_context(str(self.payload.get("scope_path", "")))
|
||||
self.app.selected_entry_id = entry_id
|
||||
self.app.pop_screen()
|
||||
@@ -786,6 +786,11 @@ class LinkedItemsPanel(Vertical):
|
||||
background: #1a1a2e;
|
||||
color: #aaaaff;
|
||||
}
|
||||
#linked-atlas-context {
|
||||
color: #7777cc;
|
||||
margin-bottom: 1;
|
||||
height: auto;
|
||||
}
|
||||
.linked-nav-hint {
|
||||
color: #555555;
|
||||
margin-top: 1;
|
||||
@@ -808,11 +813,22 @@ class LinkedItemsPanel(Vertical):
|
||||
self._focused_idx: int = -1
|
||||
# Set by the app when the user arrived here from an atlas wayfinder view.
|
||||
# Format: scope_path string or None
|
||||
self.atlas_source_scope: str | None = None
|
||||
self._atlas_source_scope: str | None = None
|
||||
|
||||
@property
|
||||
def atlas_source_scope(self) -> str | None:
|
||||
return self._atlas_source_scope
|
||||
|
||||
@atlas_source_scope.setter
|
||||
def atlas_source_scope(self, value: str | None) -> None:
|
||||
self._atlas_source_scope = value
|
||||
if self.is_mounted:
|
||||
self._update_atlas_context_label()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield PivotBreadcrumbBar(id="pivot-breadcrumb-bar")
|
||||
yield Label("Linked Items", id="linked-items-title")
|
||||
yield Label("", id="linked-atlas-context")
|
||||
yield Label("No linked items.", id="linked-items-summary", classes="linked-summary")
|
||||
yield Label("", id="linked-filter-bar", classes="linked-filter-bar")
|
||||
with Vertical(id="linked-items-list"):
|
||||
@@ -950,13 +966,24 @@ class LinkedItemsPanel(Vertical):
|
||||
current_label = self._get_entry_label(current_entry_id)
|
||||
bar.set_crumbs(crumbs, current_label)
|
||||
|
||||
def _update_atlas_context_label(self) -> None:
|
||||
"""Refresh the atlas context label based on current atlas_source_scope."""
|
||||
try:
|
||||
ctx_label = self.query_one("#linked-atlas-context", Label)
|
||||
if self.atlas_source_scope:
|
||||
ctx_label.update(f"[dim]◈ Atlas context: {self.atlas_source_scope}[/dim]")
|
||||
else:
|
||||
ctx_label.update("")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def update_entry(self, entry_id: int | None) -> None:
|
||||
summary_label = self.query_one("#linked-items-summary", Label)
|
||||
filter_bar = self.query_one("#linked-filter-bar", Label)
|
||||
list_container = self.query_one("#linked-items-list", Vertical)
|
||||
nav_hint = self.query_one("#linked-nav-hint", Label)
|
||||
|
||||
self._refresh_breadcrumb(entry_id)
|
||||
self._update_atlas_context_label()
|
||||
|
||||
if entry_id is None:
|
||||
summary_label.update("No linked items.")
|
||||
@@ -1105,8 +1132,12 @@ class LinkedItemsPanel(Vertical):
|
||||
except ValueError:
|
||||
self.app.notify("Invalid linked item target", severity="error")
|
||||
return
|
||||
if hasattr(self.app, "pivot_to_entry"):
|
||||
self.app.pivot_to_entry(entry_id)
|
||||
else:
|
||||
self.app.selected_entry_id = entry_id
|
||||
self.app.notify(f"Opened linked entry #{entry_id}")
|
||||
# Preserve atlas context across linked-panel pivots so the scope
|
||||
# label remains visible and the back button stays available.
|
||||
current_scope = self.atlas_source_scope
|
||||
self.app.selected_entry_id = entry_id
|
||||
if current_scope:
|
||||
# Re-apply after entry selection (selected_entry_id may trigger
|
||||
# update_entry which clears the panel but keeps atlas_source_scope).
|
||||
self.atlas_source_scope = current_scope
|
||||
self.app.notify(f"Opened linked entry #{entry_id}")
|
||||
Reference in new issue
Block a user