Skip to content

docs(skills): ui-capabilities — view state also dies to a re-fire, with nothing unmounting - #489

Merged
JArmandoAnaya merged 1 commit into
mainfrom
docs/identity-deps-precedent
Aug 10, 2026
Merged

docs(skills): ui-capabilities — view state also dies to a re-fire, with nothing unmounting#489
JArmandoAnaya merged 1 commit into
mainfrom
docs/identity-deps-precedent

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

Documentation only — one skill file, +15 −1.

What this records

The skill already carries "a query key that names a value the page itself can change is an
unmount trigger"
(#379, cf. #368). #482 was dispatched against that rule and the query key
turned out to be innocent: the viewport reset had nothing unmounting at all. The two
mechanisms are the confusable pair, so they now sit next to each other.

Written from the fix rather than from a summary of it — git show 9135411 -- frontend/annotator/src/adapters/react/AnnotatorCanvas.tsx:

// before
const fit = useCallback(() => {  fitToViewport(asset, ) }, [asset, applyViewport]);
useLayoutEffect(fit, [fit]);

// after
const { id: assetId, width: assetWidth, height: assetHeight } = asset;
const fit = useCallback(() => {
   fitToViewport({ id: assetId, width: assetWidth, height: assetHeight }, )
}, [assetId, assetWidth, assetHeight, applyViewport]);

asset is snapshot.document.asset; documentFromWire mints a fresh AssetDescriptor on
every rebuild; a save's refetch rebuilds. So fit's identity moved, the layout effect that
seeds zoom and pan ran again, and the camera went back to the fitted view — in a component
that never remounted and with no key having changed. The repair anchors the identity to the
three numbers a fit is actually a function of.

The three things the new section adds

  1. The generalized rule. An identity-unstable value in a hook's dependency array
    re-fires its consumer, and a re-fire resets state exactly as an unmount does — with no
    unmount to find. Nothing remounts, no key moves, no loading state flashes.
  2. The tell that separates it from the rule above it, which is the part that would have
    saved Saving resets the editor viewport (zoom/pan) #482 a wrong turn: sibling state in the same component. An unmount takes all the
    local state in that subtree and flashes a loading state on the way; a re-fire disturbs
    only what that one hook writes. In Saving resets the editor viewport (zoom/pan) #482 the hidden-annotation set, the interaction state
    and the hover point all survived the reset that took the viewport.
  3. How it misdirects. TanStack Query shares results structurally, so a background
    refetch returning identical JSON returns the same array and nothing re-fires — only a
    write ever trips it. The reset therefore looks like a consequence of saving, and the
    search goes to the mutation, which is innocent.

Plus the two habits that follow: depend on the values a hook is really a function of rather
than the object carrying them, and put the primitives in the callback's dependency list
rather than the effect's — react-hooks/exhaustive-deps is an error in
frontend/annotator and refuses a widened list, which is the correct refusal.

Deduplication

The rule was briefly written to this agent's private memory while #485 was in flight. That
file has been deleted and the shipped-work record it belonged to now points at this section
instead: a rule kept outside its home rots into misinformation, and the next agent hunting a
silently reset piece of view state will search the skill, not another tool's storage. One
detail that existed only in that note — the exhaustive-deps constraint above — was carried
into the skill before the note was removed, so nothing was lost.

A repo-wide sweep found no other copy: docs/, CLAUDE.md, AGENTS.md, the other skills
and the tests/ tree contain nothing about hook dependency identity. PR #485's body is the
historical record of the fix and is untouched.

The skill's description gains a state-lifetime clause. It described only capabilities and
refusals, so neither this section nor the query-key one above it was discoverable by
description — an agent debugging state a refetch reset had no reason to open the file.

Scope

No code, no tests, no wire, no generated artifacts. .claude/skills/ and .cursor/skills/
are git-ignored symlinks to this path, so both tools pick the change up with no setup re-run.

Local gate

Full run, staged against this box's ~10-minute command ceiling, pytest split by directory
derived from ls tests/ at run time. Run in full despite the markdown-only diff, per the
dispatch. Every stage's exit code:

Stage Exit
pytest tests/architecture 0
pytest tests/cli 0
pytest tests/examples 0
pytest tests/fixtures 0
pytest tests/formats 0
pytest tests/inference 0
pytest tests/jobs 0
pytest tests/kernel 0
pytest tests/mcp 0
pytest tests/packaging 0
pytest tests/scripts 5
pytest tests/server 0
pytest tests/test_versioning.py 0
ruff check . 0
ruff format --check . 0
mypy src/visionset/kernel 0
lint-imports 0
check.sh frontend generated 0
check.sh browser 0 — 238 e2e passed, 1 cycle passed

tests/scripts exits 5 by design — nothing pytest-shaped lives there; it is node --test
and runs under check.sh generated. That group is the one with a genuine stake in this
diff: docs_links.test.mjs scans every tracked *.md, skills included.

cf. #482, cf. #485.

…th nothing unmounting (cf. #482)

The skill records that a query key naming a mutable value is an unmount
trigger. #482 was dispatched against that rule and the query key turned out to
be innocent: `AnnotatorCanvas`'s initial-fit layout effect re-fired because
`fit` depended on the `AssetDescriptor` object, which `documentFromWire` mints
afresh on every rebuild — so a save's refetch reset zoom and pan in a component
that never remounted. The hunt for an unmount that never happened is the cost
of the two mechanisms not being written down together.

They are adjacent now, with the tell that separates them: sibling state in the
same component. An unmount takes all of it and flashes a loading state on the
way; a re-fire disturbs only what that one hook writes and leaves everything
beside it untouched.

Also records the two habits that follow — depend on the values a hook is really
a function of rather than the object carrying them, and put the primitives in
the callback's dependency list rather than the effect's, since
`react-hooks/exhaustive-deps` is an error in `frontend/annotator` and refuses a
widened list.

The skill's `description` gains the state-lifetime clause, so the next agent
debugging a silently reset piece of view state finds this file by searching for
what they are actually looking at. cf. #485.
@JArmandoAnaya
JArmandoAnaya merged commit 015c7a2 into main Aug 10, 2026
13 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the docs/identity-deps-precedent branch August 10, 2026 05:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant