You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fixes the two dogfooding failures in cli-got28: components could not be
reliably isolated, and notes were associated with the wrong code.
F1 — anchored, code-locating selectors:
- SelectorEngine.generate anchors a non-identified target to its nearest
identified ancestor and scopes indices within that component
(#Settings.Models >> @save / text="…" / AXButton[n-in-scope]); component-local
indices survive unrelated same-role tree changes.
- Emit text= steps; add Selector.fromPath so a #<slash/path> that resolves to
nothing is never emitted.
F2 — deterministic component isolation:
- One pure, unit-tested AnnotationTargetRule (deepest actionable, else deepest
meaningful; never window/chrome/ghost) shared by all three sources (AX,
NSView-tree, iOS UIView), replacing the macOS "deepest meaningful" vs iOS
"nearest identified" split. A click inside a button binds to the button, not
its glyph.
- ComponentLadderSource + widen affordance to step selection up to the
enclosing component. Docs (DECISIONS/README/spike/PARITY) corrected.
F3 — deterministic note->code mapping:
- AnnotationNote carries component / elementRole / elementText / unseeded,
surfaced in markdown + JSON.
Geometric card isolation (dogfood follow-up):
- VirgilHUD's .axCardSurface seeds a card's id on a .background leaf that is a
SIBLING of the content, not an ancestor, so pure-ancestry anchoring/widening
could never reach the card. componentLadder now resolves enclosing components
by frame containment (ancestor chain + each ancestor's direct children); the
note component field and widening use it. The hover highlight shows a name tag
so an element vs its enclosing card are distinguishable at a glance.
74 tests, incl. anchored-selector / mutation-stability / ambiguity / target-rule
/ widening / geometric-ladder / payload suites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: DECISIONS.md
+54Lines changed: 54 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,13 +9,67 @@ Resolves the open decisions from the plan (planning/annotkit in the cli repo). P
9
9
| Repo location | Standalone repo at `~/Development/annotkit`| VirgilHUD consumes it during dev via a local SwiftPM path dependency; public remote created by the maintainer. |
10
10
| Versioning | SemVer, 0.x pre-1.0 | Breaking changes allowed while 0.x; 1.0 marks a stable public API. |
11
11
| Default element source | Accessibility hierarchy | The only strategy that surfaces SwiftUI `accessibilityIdentifier` values. |
12
+
| Annotation target rule | Deepest actionable, else deepest meaningful; anchor the selector to the nearest identifier | One rule on both platforms. Supersedes the earlier macOS "deepest meaningful" and iOS "nearest identified" split. See below. |
12
13
| Opt-in element source | View tree (NSView/UIView) | Surfaces concrete view class names; richer for AppKit/UIKit hosts. Collapses to hosting views in pure SwiftUI. |
13
14
|`pathname` mapping | Host-supplied route, inferred fallback | A native app has no URL routes; the host sets a route, else infer from the key window title or identifier. |
14
15
| Overlay coverage | Primary screen (MVP) | The overlay covers the primary display; SwiftUI-local points map to AX screen coordinates there. Full multi-display placement is deferred (cli-a99qm.4.2). |
15
16
| Dev-only gating |`#if DEBUG` default + env override | On in DEBUG unless `ANNOTKIT_DISABLE`; off in release unless `ANNOTKIT_ENABLE`. Mirrors VirgilHUD `InspectMode`. |
16
17
| MCP bridge in v1 | Deferred to F6 (optional) | The file and clipboard sinks cover the agent loop; the MCP/HTTP bridge is an optional later target, not part of the 1.0 critical path. |
17
18
| Concurrency | Swift 6 language mode, strict | Public `Element`/`CapturedImage`/`AnnotationNote` are `Sendable`; `ElementSource`/`Annotation` are `@MainActor`. |
18
19
20
+
## Annotation target rule (cli-got28.2)
21
+
22
+
A click must isolate the component the user meant and produce a selector that
23
+
locates its code. One rule, both platforms:
24
+
25
+
1. Hit-test to the deepest node at the point, then walk its ancestor chain.
26
+
2.**Target = the deepest ACTIONABLE control** in the chain (button, link,
27
+
checkbox, popup, slider, menu item, or anything exposing `AXPress`). A click
28
+
anywhere inside a button binds to the button, not the static-text glyph that
29
+
happens to be its deepest descendant.
30
+
3. Else **target = the deepest MEANINGFUL element** — one carrying an identifier,
31
+
a label, or a displayed value. A standalone `Text` inside a card is annotated
32
+
in its own right (the card is not actionable, so it does not swallow the
33
+
text); the selector engine then anchors it to the card's identifier.
34
+
4. Never a target: the window, the application, window chrome (traffic lights),
35
+
or a structural, unidentified, content-less group that spans (nearly) the
36
+
whole window (an `NSHostingView` root `AXGroup` — the window in disguise).
37
+
5. When the point hits nothing annotatable (decoration, dividers, padding beyond
38
+
any frame), a `RegionAnchorSource` anchors the click to the nearest meaningful
39
+
element as a REGION note rather than dropping it.
40
+
41
+
Why not "prefer the deepest *identified* ancestor"? Because seeding is partial: a
42
+
button may be seeded but a standalone text inside a seeded card is usually not.
43
+
Preferring the identified container would collapse every click inside a card onto
44
+
the card and lose the specific element. Preferring the actionable/meaningful leaf
45
+
and letting the SELECTOR anchor to the nearest identifier (`#Card >> text="…"`)
46
+
keeps both the specificity and the code-locating anchor. Selection *widening*
47
+
(cli-got28.2.3) exists for the times the user does want the enclosing component.
48
+
49
+
This replaces the earlier asymmetry (macOS "deepest meaningful", iOS "nearest
50
+
identified") documented in `docs/spike-ax-pointquery.md`, now corrected. The pure
51
+
decision lives in `AnnotationTargetRule` and is unit-tested independent of AX.
52
+
53
+
### Component containment is GEOMETRIC, not tree-ancestry (cli-got28.2)
54
+
55
+
"Which component encloses this element" is resolved by frame containment, not by
56
+
walking the AX parent chain. A SwiftUI card seeded with `.axCardSurface` (the
57
+
dominant VirgilHUD pattern) is a clear `Color.clear` background leaf carrying the
58
+
identifier — and `.background` makes it a **sibling of the card's content, not an
59
+
ancestor**. So the card's identifier never appears in the content's ancestor
60
+
chain, and pure-ancestry anchoring/widening cannot reach it (it only worked for
61
+
`.accessibilityElement(children: .contain)` containers, which *are* ancestors).
62
+
63
+
The macOS `componentLadder` therefore collects every identified element whose
64
+
frame **encloses the point** and is larger than the target, smallest-first —
65
+
scanning the ancestor chain plus each ancestor's direct children (where those
66
+
background surfaces live), so it reaches sibling card surfaces without a full
67
+
snapshot. This ladder drives selection widening and the note's `component` field.
68
+
Selector *anchoring* (`#Card >> …`) still requires a true ancestor because the
69
+
`>>` operator is descendant-based; when the component is a sibling surface the
70
+
selector may be positional or text-based while the `component` field still names
71
+
the card, so the note locates the right code either way.
72
+
19
73
## IP hygiene (carried into the F7 legal gate)
20
74
21
75
- Do not copy original Agentation source (PolyForm Shield 1.0.0, non-compete). Only the `AGENTATION_NOTES.md` file format is reused, reimplemented clean-room.
Copy file name to clipboardExpand all lines: PARITY.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,7 +10,9 @@ row; each asymmetry is closed by code or has a tracked mitigation.
10
10
| Opt-in element source | NSView tree | (the default already walks the view tree) | macOS adds a view-tree opt-in alongside its AX default; iOS only needs the one source |
11
11
|`accessibilityIdentifier`| via AX | via `UIView.accessibilityIdentifier`| both surface SwiftUI identifiers |
12
12
| Concrete view class name | via NSView opt-in source | via the default UIView walk | none |
13
-
| Hit test |`AXUIElementCopyElementAtPosition` + NSView `hitTest`|`UIView.hitTest(_:with:)`| iOS has no global AX point query; uses view hitTest. Tracked: F5.2 |
13
+
| Hit test primitive |`AXUIElementCopyElementAtPosition` + NSView `hitTest`|`UIView.hitTest(_:with:)`| iOS has no global AX point query; uses view hitTest. Tracked: F5.2 |
14
+
| Annotation target rule | shared `AnnotationTargetRule` over an AX candidate chain | shared `AnnotationTargetRule` over a UIView candidate chain | none — both build a `[TargetCandidate]` chain and apply the SAME rule (deepest actionable, else deepest meaningful). Closes the earlier split (macOS "deepest meaningful" vs iOS "nearest identified"), cli-got28.2 |
15
+
| Component widening |`ComponentLadderSource` (AX chain) |`ComponentLadderSource` (UIView chain) | none — same ladder (target, then enclosing identified components) |
14
16
| Coordinate space | Cocoa bottom-left to AX top-left flip | UIKit top-left native | iOS needs no flip; shared `ScreenSpace` used only on macOS |
15
17
| Screenshot | ScreenCaptureKit / `cacheDisplay`|`UIGraphicsImageRenderer` + `drawHierarchy`| both capture own hierarchy only; no cross-window or secure overlays |
16
18
| Overlay host | resizing `NSPanel` (toolbar corner idle, full screen annotating) | pass-through `UIWindow`| both interactive; selection via the shared SwiftUI catcher, not a global monitor |
0 commit comments