11// Copyright 2026 jfarcand@apache.org
22// Licensed under the Apache License, Version 2.0
33//
4- // ABOUTME: Protocol abstractions for system boundaries (mirroring bridge, input, capture, recording, OCR).
4+ // ABOUTME: Protocol abstractions for system boundaries (mirroring bridge, input, capture, recording, OCR, navigation graph ).
55// ABOUTME: Enables dependency injection for testing without requiring real macOS system APIs.
66
77import AppKit
@@ -210,7 +210,7 @@ protocol Exploring: AnyObject, Sendable {
210210 var stats : ( nodeCount: Int , edgeCount: Int , actionCount: Int , elapsedSeconds: Int ) { get }
211211
212212 /// The navigation graph tracking screen transitions and visited elements.
213- var graph : NavigationGraph { get }
213+ var graph : any NavigationGraphing { get }
214214
215215 /// Generate the final skill bundle from the exploration session.
216216 func generateBundle( ) -> SkillBundle
@@ -219,6 +219,163 @@ protocol Exploring: AnyObject, Sendable {
219219 func generateReport( ) -> String
220220}
221221
222+ /// Abstracts the navigation graph used during app exploration.
223+ /// Enables swappable graph implementations for testing and alternative strategies.
224+ protocol NavigationGraphing : AnyObject , Sendable {
225+
226+ // MARK: - Lifecycle
227+
228+ /// Initialize the graph with the root screen, resetting all state.
229+ func start(
230+ rootElements: [ TapPoint ] ,
231+ icons: [ IconDetector . DetectedIcon ] ,
232+ hints: [ String ] ,
233+ screenshot: String ,
234+ screenType: ScreenType
235+ )
236+
237+ /// Record a navigation transition. Returns whether the screen is new, revisited, or duplicate.
238+ func recordTransition(
239+ elements: [ TapPoint ] ,
240+ icons: [ IconDetector . DetectedIcon ] ,
241+ hints: [ String ] ,
242+ screenshot: String ,
243+ actionType: String ,
244+ elementText: String ,
245+ displayLabel: String ? ,
246+ screenType: ScreenType ,
247+ edgeType: EdgeType
248+ ) -> TransitionResult
249+
250+ /// Export an immutable snapshot of the current graph state.
251+ func finalize( ) -> GraphSnapshot
252+
253+ // MARK: - Properties
254+
255+ /// Number of distinct screens discovered.
256+ var nodeCount : Int { get }
257+
258+ /// Number of navigation edges recorded.
259+ var edgeCount : Int { get }
260+
261+ /// Fingerprint of the current screen.
262+ var currentFingerprint : String { get }
263+
264+ /// Fingerprint of the root (first) screen.
265+ var rootFingerprint : String { get }
266+
267+ /// Whether the graph has been initialized with a root screen.
268+ var started : Bool { get }
269+
270+ /// The set of labels marked globally visited (breadth_navigation items).
271+ var globalVisitedLabels : Set < String > { get }
272+
273+ // MARK: - Node Access
274+
275+ /// Get the node for a given fingerprint.
276+ func node( for fingerprint: String ) -> ScreenNode ?
277+
278+ /// Get the most recent incoming edge that led to a given screen fingerprint.
279+ func incomingEdge( to fingerprint: String ) -> NavigationEdge ?
280+
281+ /// Find a node with similar structural elements using title-aware similarity.
282+ func findMatchingNode( elements: [ TapPoint ] ) -> String ?
283+
284+ /// Find a node matching the viewport using both Jaccard similarity and containment.
285+ func findMatchingNodeWithContainment( elements: [ TapPoint ] ) -> String ?
286+
287+ // MARK: - Visited State
288+
289+ /// Mark an element as visited on the specified screen.
290+ func markElementVisited( fingerprint: String , elementText: String )
291+
292+ /// Update the current fingerprint after backtracking to sync graph state.
293+ func setCurrentFingerprint( _ fingerprint: String )
294+
295+ // MARK: - Screen Plans
296+
297+ /// Store a ranked exploration plan for a screen.
298+ func setScreenPlan( for fingerprint: String , plan: [ RankedElement ] )
299+
300+ /// Get the exploration plan for a screen, if one has been built.
301+ func screenPlan( for fingerprint: String ) -> [ RankedElement ] ?
302+
303+ /// Get the next unvisited plan element, skipping per-screen and global visited sets.
304+ func nextPlannedElement( for fingerprint: String ) -> RankedElement ?
305+
306+ /// Clear the exploration plan for a screen, forcing a rebuild on next access.
307+ func clearScreenPlan( for fingerprint: String )
308+
309+ // MARK: - Scout Phase
310+
311+ /// Record the result of scouting an element on a screen.
312+ func recordScoutResult( fingerprint: String , elementText: String , result: ScoutResult )
313+
314+ /// Get all scout results for a screen.
315+ func scoutResults( for fingerprint: String ) -> [ String : ScoutResult ]
316+
317+ /// Get the current traversal phase for a screen.
318+ func traversalPhase( for fingerprint: String ) -> TraversalPhase
319+
320+ /// Set the traversal phase for a screen.
321+ func setTraversalPhase( for fingerprint: String , phase: TraversalPhase )
322+
323+ // MARK: - Breadth Navigation
324+
325+ /// Register breadth_navigation labels (e.g. tab bar items) for global tracking.
326+ func registerBreadthLabels( _ labels: Set < String > )
327+
328+ /// Check if a displayLabel belongs to a breadth_navigation component.
329+ func isBreadthLabel( _ label: String ) -> Bool
330+
331+ /// Mark a breadth_navigation component as globally visited across all screens.
332+ func markGloballyVisited( label: String )
333+
334+ // MARK: - Tap Area Cache
335+
336+ /// Record a tap at the given coordinates on a screen.
337+ func recordTap( fingerprint: String , x: Double , y: Double )
338+
339+ /// Check whether a point was already tapped on a screen (within proximity radius).
340+ func wasAlreadyTapped( fingerprint: String , x: Double , y: Double ) -> Bool
341+
342+ /// Number of tapped areas recorded for a screen.
343+ func tapCount( for fingerprint: String ) -> Int
344+
345+ // MARK: - Dead Edge Tracking
346+
347+ /// Mark an edge as dead (tap had no effect on the screen).
348+ func markEdgeDead( fromFingerprint: String , elementText: String )
349+
350+ // MARK: - Recovery Events
351+
352+ /// Append a recovery event for post-hoc diagnosis.
353+ func appendRecoveryEvent( _ event: RecoveryEvent )
354+
355+ // MARK: - Scroll Support
356+
357+ /// Merge scrolled elements into a screen node. Returns novel count.
358+ func mergeScrolledElements( fingerprint: String , newElements: [ TapPoint ] ) -> Int
359+
360+ /// Get the number of scroll actions performed on a screen.
361+ func scrollCount( for fingerprint: String ) -> Int
362+
363+ /// Increment the scroll count for a screen.
364+ func incrementScrollCount( for fingerprint: String )
365+
366+ /// Mark a screen as having infinite scroll (content never exhausts).
367+ func markInfiniteScroll( fingerprint: String )
368+
369+ /// Mark a screen as scroll-exhausted (all content has been revealed).
370+ func markScrollExhausted( fingerprint: String )
371+
372+ /// Check if a screen has been marked as having infinite scroll.
373+ func isInfiniteScroll( fingerprint: String ) -> Bool
374+
375+ /// Check if a screen has been marked as scroll-exhausted.
376+ func isScrollExhausted( fingerprint: String ) -> Bool
377+ }
378+
222379// MARK: - Conformances
223380
224381extension MirroringBridge : MenuActionCapable { }
@@ -242,3 +399,5 @@ extension VisionScreenDescriber: ScreenDescribing {}
242399extension BFSExplorer : Exploring { }
243400
244401extension DFSExplorer : Exploring { }
402+
403+ extension NavigationGraph : NavigationGraphing { }
0 commit comments