Skip to content

Refactor: Unify Node Inventory Logic between topology.py and nodes.py #91

@DarinShapiro

Description

@DarinShapiro

Problem

\get_mesh_state\ and \list_all_nodes\ should remain separate external MCP tools, but they currently rebuild overlapping node facts in two different places:

  • \pipeline/topology.py::build_topology()\ builds graph-oriented node rows plus links and partition summaries.
  • \pipeline/nodes.py::list_nodes_enriched()\ builds inventory-oriented node rows for tables and operator inspection.

The overlap is not just conceptual. Both paths independently derive or project many of the same node-level facts:

  • identity: \eui64, \ riendly_name\
  • placement: \�rea_name, \partition_id\
  • mesh role:
    ole,
    outing_role\
  • parent relationship: \parent_eui64\
  • freshness/lifecycle: \last_seen, \status, \last_referenced_at\
  • partition leadership context: leader discovery by
    outing_role == leader\
  • phantom filtering behavior

At the same time, each tool adds different higher-level projections:

  • \get_mesh_state: graph links, partition summaries, split flag, weak/error/asymmetric link tagging, graph diagnostics
  • \list_all_nodes: \display_name, \device_kind, HA metadata, availability fields, router peer summaries,
    ext_hop_to_otbr, SED-specific mesh-alive fields, optional \signal_strength\

This means the public tools are correctly distinct, but the internal node snapshot logic is not centralized.

Why This Should Not Collapse Into One Public Tool

Do not merge \get_mesh_state\ and \list_all_nodes\ into one MCP tool.

They serve different consumers and payload shapes:

  • \get_mesh_state\ is graph-first and should stay optimized for topology reasoning / rendering.
  • \list_all_nodes\ is inventory-first and should stay optimized for table views, filtering, and operator inspection.

The refactor goal is shared internal data assembly, not API consolidation.

Current Code Anchors

  • \�ddons/thread-observability/app/src/thread_observability/pipeline/topology.py\
  • \�ddons/thread-observability/app/src/thread_observability/pipeline/nodes.py\

Relevant current behavior:

  • \�uild_topology()\ directly queries node fields, derives \parent_eui64, computes per-node \stale, and then builds partition summaries and links.
  • \list_nodes_enriched()\ starts from \store.list_nodes(), derives parent/leader/peer/OTBR-hop context, and projects a richer inventory row.

Proposed Refactor

Introduce one shared internal node snapshot builder that produces a canonical base row per visible node. Then project that base row into:

  1. a graph-facing node shape for \get_mesh_state\
  2. an inventory-facing row shape for \list_all_nodes\

Proposed Internal Shape

Create an internal helper or dataclass, e.g. \CanonicalNodeSnapshot, containing only the common base facts needed by both projections.

Suggested fields:

  • \eui64\
  • \ riendly_name\
  • \�rea_name\

  • ole\

  • outing_role\
  • \partition_id\
  • \leader_router_id\

  • outer_id\
  • \�endor_id\
  • \product_id\
  • \serial_number\
  • \manufacturer\
  • \model\
  • \device_id\
  • \ irst_seen\
  • \last_seen\
  • \status\
  • \status_changed_at\
  • \last_referenced_at\
  • \�vailable\
  • \�vailability_source\
  • \�vailability_checked_at\
  • \parent_eui64\
  • \last_rssi\
  • \last_lqi\
  • \is_phantom\
  • \suppressed_duplicate_euis\

Notes:

  • Keep this internal shape free of graph-only artifacts like \links, \split, or edge tags.
  • Keep it free of inventory-only projections like \display_name, \device_kind,
    outer_peers, or
    ext_hop_to_otbr\ unless those are computed in a second projection step.
  • \stale\ in topology should likely remain a topology projection derived from \last_seen\ + \ reshness_minutes, not a stored base field.

Implementation Outline

  1. Extract a shared helper in \pipeline/\ for collecting canonical node rows from storage.
  2. Move duplicate identity / partition / lifecycle / parent assembly into that helper.
  3. Make \�uild_topology()\ consume the shared canonical rows and then add:
    • topology-specific \stale\
    • partition summaries
    • links
    • graph annotations / diagnostics
  4. Make \list_nodes_enriched()\ consume the same canonical rows and then add:
    • \display_name\
    • \device_kind\
    • partition leader name
    • router peer counts / peer names
    • \parent_name\

    • ext_hop_to_otbr\
    • SED-specific fields
    • optional \signal_strength\
  5. Preserve existing response schemas for both tools.

Non-Goals

  • No MCP tool rename.
  • No contract merge between \get_mesh_state\ and \list_all_nodes.
  • No dashboard behavior changes required as part of this refactor.
  • No broad topology/diagnostics redesign.

Acceptance Criteria

  • There is one shared internal node snapshot assembly path used by both \�uild_topology()\ and \list_nodes_enriched().
  • \get_mesh_state\ response shape remains unchanged.
  • \list_all_nodes\ response shape remains unchanged.
  • Existing phantom filtering behavior remains intact.
  • Parent resolution remains consistent across both tools.
  • Partition leader derivation is not duplicated in both modules.
  • Existing tests covering topology and node inventory continue to pass.
  • Add focused tests for the shared helper covering:
    • phantom filtering
    • duplicate hardware collapse passthrough
    • parent derivation
    • partition/leader consistency across both projections

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions