Skip to content

feat(#65): render typed cross-scope connections + far-end proxies#72

Merged
CuriouslyCory merged 5 commits into
mainfrom
feat/65-typed-cross-scope-connections
Jun 2, 2026
Merged

feat(#65): render typed cross-scope connections + far-end proxies#72
CuriouslyCory merged 5 commits into
mainfrom
feat/65-typed-cross-scope-connections

Conversation

@CuriouslyCory

@CuriouslyCory CuriouslyCory commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Closes #65.

The client-render slice on top of #63's getCanvas cross-scope derivation (ADR-0031). The Canvas previously ignored boundaryProxies and the *Repr fields; this PR consumes them.

What changed

Arrowheads (ADR-0027). New pure helper ~/lib/connection-direction.tsarrowEnds(interaction) → { atSource, atTarget } — the single source of truth, framework-agnostic (imports only the client-safe Interaction type; no @xyflow/react in ~/lib). toRFEdge maps it to React Flow markerStart/markerEnd. The exporter (#67) will derive its / glyph from the same booleans.

Far-end proxies (ADR-0031, ADR-0016). boundaryProxies render as passive boundary-proxy nodes, re-introducing the isPassiveNode/CanvasRFNode extension point in its simpler per-edge form (no boundary-group). Lineal/ingress proxies (real endpoint on the breadcrumb trail) are labelled "Inbound from {ancestor}" so they don't read as the host inside itself. A "Go to {title}" affordance navigates to the off-scope Component's own scope (/n/[realEndpointId]) — no getCanvas shape change.

Interaction picker. An inline segmented control on the selected Connection upgrades its interaction. New updateEdgeInteraction mutation/service re-checks the directional de-dupe key (mirrors connectNodes, excludes the edge's own id) and returns ConflictError on collision; draw order is preserved so the arrow points the way it was drawn. updateEdge stays label-only.

Repr-swap correctness. Incident-edge removal now matches on reprs (not raw endpoint ids), and orphaned proxies are cleaned up on Component delete/undo — otherwise the repr swap would strand cross-scope edges/proxies in the store (invisible to pnpm check).

Scope boundary

Docs (travel with the slice)

CONTEXT.md tense flips (realized in #65); ADR-0027 amended with interaction-edit semantics; ADR-0031/0016 realized notes; stale ADR-0023 / boundary-group comments fixed.

Verification

  • pnpm check green.
  • New connection-direction (5 cases) + updateEdgeInteraction service tests pass.
  • dev-browser: ASSOCIATION = plain line; REQUEST → arrow at target, SUBSCRIBE → arrow at source; both proxy types render with correct labels; picker round-trip persists to the server preserving direction; "Go to" navigates to the far endpoint's scope.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Passive boundary‑proxy nodes for cross‑scope connections with “go to real endpoint” navigation; arrowheads now reflect interaction type; inline interaction picker with optimistic updates and conflict-aware handling.
  • Bug Fixes

    • Deletion/undo now correctly restores/removes incident proxies; edge label/interaction edits have more robust conditional rollback.
  • Tests

    • Unit tests covering arrow‑end logic and interaction upgrade/conflict scenarios.
  • Documentation

    • Updated ADRs, plan, and quality guidance for cross‑scope rendering and workflow expectations.

CuriouslyCory and others added 2 commits June 2, 2026 12:06
- Bulletproof-plan skill now documents that plans must include the full SDLC envelope: branch creation → implementation → commit/push → CodeRabbit review + self-review → autofix → commit → PR
- Adds detailed implementation plan for #65 (typed cross-scope connection rendering with boundary proxies)
- Quality bar updated to emphasize that plans end in a PR, not just implementation steps
Make the Canvas consume the cross-scope read shape #63 derives (ADR-0031):
Connections now attach to each endpoint's on-scope representative, arrowheads
derive from the interaction via a shared pure helper, and off-scope endpoints
render as read-only boundary-proxy passive nodes.

- `~/lib/connection-direction.ts` (`arrowEnds`): the canonical, framework-agnostic
  interaction→arrowheads helper (ADR-0027), shared with the exporter (#67). Canvas
  maps it to React Flow markers in `toRFEdge`; `@xyflow/react` stays out of `~/lib`.
- Boundary proxies render as passive `boundary-proxy` nodes (re-introducing the
  `isPassiveNode`/`CanvasRFNode` extension point per ADR-0016, in its per-edge form
  without the retired boundary-group). Lineal/ingress proxies are labelled
  "Inbound from {ancestor}" so they don't read as the host inside itself. A
  "Go to {title}" affordance navigates to the off-scope Component's own scope.
- Interaction picker: an inline segmented control on the selected Connection
  upgrades its interaction. Backed by a new `updateEdgeInteraction`
  mutation/service that re-checks the directional de-dupe key (mirroring
  `connectNodes`, with the edge's own id excluded) and returns ConflictError on a
  collision; draw order is preserved. `updateEdge` stays label-only.
- Fix the incident-edge removal to match on reprs (not raw endpoint ids) and clean
  up orphaned proxies on Component delete/undo — the repr swap would otherwise
  strand cross-scope edges/proxies in the store.
- Docs: CONTEXT.md tense flips (realized in #65); ADR-0027 amended with
  interaction-edit semantics; ADR-0031/0016 realized notes; stale ADR-0023 /
  boundary-group comments fixed.

Verified: `pnpm check` green; new helper + updateEdgeInteraction service tests
pass; dev-browser confirmed arrowheads (ASSOCIATION plain, REQUEST/SUBSCRIBE
directional), both proxy types with correct labels, the picker round-trip, and
go-to-real navigation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
infinite-docs Ready Ready Preview, Comment Jun 2, 2026 8:24pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 420be78d-0be0-4fb7-8e3a-570cee8e8ad8

📥 Commits

Reviewing files that changed from the base of the PR and between e5cb679 and 8364eda.

📒 Files selected for processing (26)
  • src/app/_components/connect-agent.tsx
  • src/app/llms.txt/route.ts
  • src/app/p/[slug]/_canvas/attach-spec-section.tsx
  • src/app/p/[slug]/_canvas/component-detail-panel.tsx
  • src/app/p/[slug]/_canvas/component-docs-editor.tsx
  • src/app/p/[slug]/_canvas/kind-palette.tsx
  • src/app/p/[slug]/_canvas/spec-conflict-modal.tsx
  • src/components/ui/command.tsx
  • src/server/architecture/__tests__/apply-graph.service.test.ts
  • src/server/architecture/__tests__/markdown-export.test.ts
  • src/server/architecture/__tests__/mcp-read.service.test.ts
  • src/server/architecture/__tests__/spec.service.test.ts
  • src/server/architecture/__tests__/token.service.test.ts
  • src/server/architecture/apply-graph.service.ts
  • src/server/architecture/markdown.ts
  • src/server/architecture/prisma-errors.ts
  • src/server/architecture/spec-parser/__tests__/bounds.test.ts
  • src/server/architecture/spec-parser/__tests__/diff.test.ts
  • src/server/architecture/spec-parser/__tests__/openapi.test.ts
  • src/server/architecture/spec-parser/diff.ts
  • src/server/architecture/spec-parser/openapi.ts
  • src/server/architecture/spec-parser/sql-ddl.ts
  • src/server/architecture/spec.service.ts
  • src/server/architecture/token-hash.ts
  • src/server/mcp/resources.ts
  • src/server/mcp/tool-catalog.ts

📝 Walkthrough

Walkthrough

Canvas now renders typed cross-scope connections with interaction-derived arrowheads, passive boundary-proxy nodes with “descend to real”, an inline per-edge interaction picker with optimistic commits and conflict-aware rollback, a server mutation for interaction updates, tests, and documentation/plan updates.

Changes

Typed Cross-Scope Connection Rendering

Layer / File(s) Summary
Arrow direction helpers and interaction UI constants
src/lib/connection-direction.ts, src/lib/connection-direction.test.ts, src/lib/interactions.ts, src/lib/schemas.ts, src/lib/connection-rules.ts
arrowEnds(interaction) maps REQUEST/PUSH/SUBSCRIBE/DUPLEX/ASSOCIATION to {atSource, atTarget}; exhaustive tests cover cases; INTERACTION_LABEL/HINT/ORDER drive the picker; updateEdgeInteractionInput schema added; canConnect doc clarified.
Server mutation for updating edge interactions
src/server/architecture/edge.service.ts, src/server/architecture/__tests__/edge.service.test.ts, src/server/api/routers/architecture.ts
updateEdgeInteraction validates input, authorizes owner, no-ops unchanged interactions, pre-checks de-dupe excluding the current edge, throws ConflictError on collision, maps dedupe races into ConflictError; router wires the mutation; tests cover upgrade, no-op, conflict, auth, and not-found paths.
Boundary-proxy node component
src/app/p/[slug]/_canvas/boundary-proxy.tsx, src/lib/node-kinds.ts
Adds BoundaryProxyNodeData and BoundaryProxyNodeView: passive proxy with realEndpointId and lineal inbound labeling, non-connectable handles, icon/aria labeling, and hover “descend to real” button; node-kind docs updated.
Canvas conversion & types
src/app/p/[slug]/_canvas/canvas.tsx (imports, node/edge conversion, node types)
toRFEdge now derives markerStart/markerEnd from arrowEnds(e.interaction) and includes interaction; toProxyRFNode creates boundary-proxy nodes with realEndpointId and lineal; adds CanvasRFNode union and isPassiveNode discriminator.
Canvas hydration & proxy seeding
src/app/p/[slug]/_canvas/canvas.tsx (hydration)
Canvas seeds React Flow with interior nodes/edges and generated boundary-proxy nodes using boundaryProxies and derived breadcrumbIds.
Canvas interaction edit and conflict handling
src/app/p/[slug]/_canvas/canvas.tsx (commitEdgeInteraction, messageForInteractionFailure, commitEdgeEdit)
Adds setEdgeInteraction mutation hook and commitEdgeInteraction optimistic flow that rebuilds RF edge via toRFEdge, conditionally rolls back only the interaction if cache still matches optimistic state, and shows conflict-specific toasts; label edits use toRFEdge for consistent markers.
Canvas component deletion and undo
src/app/p/[slug]/_canvas/canvas.tsx (removeComponent, undoRemoveComponent)
removeComponent computes incident edges by sourceRepr/targetRepr and removes associated boundary proxies; undoRemoveComponent accepts incidentProxies, restores proxies on successful undo (using breadcrumbIds for lineal), and removes restored proxies on rollback.
React Flow wiring and event guards
src/app/p/[slug]/_canvas/canvas.tsx, src/app/p/[slug]/_canvas/component-node.tsx
Wraps tree with SetEdgeInteractionContext, types ReactFlow with CanvasRFNode, configures node/edge types, and guards click/hover/descent handlers to ignore passive boundary-proxy and temp_ nodes; minor comment updates reference ADR-0027.
Connection edge interaction picker
src/app/p/[slug]/_canvas/connection-edge.tsx
Extends ConnectionEdgeData to include interaction, adds exported SetEdgeInteractionContext, normalizes default interaction, and renders a segmented 5-option interaction picker when the edge is selected and editable, invoking the context callback to upgrade interaction.
Documentation and planning
.claude/skills/bulletproof-plan/SKILL.md, CONTEXT.md, docs/adr/0016-*, docs/adr/0027-*, docs/adr/0031-*, docs/plans/typed-cross-scope-connection-rendering.md
Records realized cross-scope rendering and arrow derivation in CONTEXT/ADRs, adds the typed-cross-scope plan, and tightens SKILL.md quality/SDLC requirements (plan-mode approval, claim validation, branch→PR envelope).

Sequence Diagram(s)

sequenceDiagram
  participant ConnectionEdgeView
  participant Canvas
  participant ArchitectureRouter
  participant EdgeService
  participant DB
  ConnectionEdgeView->>Canvas: onSetInteraction(edgeId, interaction)
  Canvas->>ArchitectureRouter: updateEdgeInteraction mutation(input)
  ArchitectureRouter->>EdgeService: updateEdgeInteraction(db, actor, input)
  EdgeService->>DB: activeDuplicateWhere check / db.update
  DB-->>EdgeService: Edge or uniqueness error
  EdgeService-->>ArchitectureRouter: Edge | ConflictError
  ArchitectureRouter-->>Canvas: mutation result
  Canvas-->>ConnectionEdgeView: optimistic success or rollback/toast
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I hopped the canvas, arrows in my paw,

Proxies stood guard at the far-off draw,
I picked a mode and markers spun bright,
Descend to the real node — follow the light,
A rabbit cheers: cross-scope done right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat(#65): render typed cross-scope connections + far-end proxies' directly and clearly summarizes the main changes: typed arrowhead rendering and cross-scope boundary proxy implementation.
Linked Issues check ✅ Passed All acceptance criteria from #65 are met: arrowheads derived from interaction with shared helper (connection-direction), boundary proxies rendered with descend-to-real navigation, lineal proxies labeled clearly, interaction picker + label editor on selected connections, pnpm checks green.
Out of Scope Changes check ✅ Passed All code changes align with #65 scope: interaction rendering, boundary-proxy node views, arrowhead derivation, interaction picker UI, updateEdgeInteraction service, and documentation updates on related ADRs. No out-of-scope additions detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/65-typed-cross-scope-connections

Comment @coderabbitai help to get the list of available commands and usage tips.

Self-review found that commitEdgeEdit and commitEdgeInteraction both restored
the full CanvasEdge on rollback, so a failed label edit could clobber a
concurrently-succeeded interaction change (and vice versa). Roll back only the
field each path wrote, reading from the CURRENT cache row, and gate on the cache
still showing that write — the field-scoped analogue of commitRename's
conditional rollback. Also gate the empty-canvas hint on real Components so an
inbound-proxy-only scope still shows it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/p/[slug]/_canvas/canvas.tsx (1)

1168-1184: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rollback should merge edge fields instead of restoring whole snapshots.

Both failure paths write prev back as a full CanvasEdge. That means a newer successful change to the other field gets lost: a failed label save can revert a newer interaction/marker update, and a failed interaction save can revert a newer label edit. Roll back only the field owned by the failing mutation, using the current cached edge as the base.

Possible fix
-        if (prev) {
-          setEdges((es) => es.map((e) => (e.id === id ? toRFEdge(prev) : e)));
-          patchCanvas((c) => ({
-            interiorEdges: c.interiorEdges.map((e) => (e.id === id ? prev : e)),
-          }));
-        }
+        const current = utils.architecture.getCanvas
+          .getData(canvasInput)
+          ?.interiorEdges.find((e) => e.id === id);
+        if (current?.label === label) {
+          const rolledBack = { ...current, label: prev.label };
+          setEdges((es) =>
+            es.map((e) => (e.id === id ? toRFEdge(rolledBack) : e)),
+          );
+          patchCanvas((c) => ({
+            interiorEdges: c.interiorEdges.map((e) =>
+              e.id === id ? { ...e, label: prev.label } : e,
+            ),
+          }));
+        }
-        if (current?.interaction === interaction) {
-          setEdges((es) => es.map((e) => (e.id === id ? toRFEdge(prev) : e)));
-          patchCanvas((c) => ({
-            interiorEdges: c.interiorEdges.map((e) => (e.id === id ? prev : e)),
-          }));
-        }
+        if (current?.interaction === interaction) {
+          const rolledBack = { ...current, interaction: prev.interaction };
+          setEdges((es) =>
+            es.map((e) => (e.id === id ? toRFEdge(rolledBack) : e)),
+          );
+          patchCanvas((c) => ({
+            interiorEdges: c.interiorEdges.map((e) =>
+              e.id === id ? { ...e, interaction: prev.interaction } : e,
+            ),
+          }));
+        }

Also applies to: 1205-1223

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/p/`[slug]/_canvas/canvas.tsx around lines 1168 - 1184, The rollback
currently overwrites the entire CanvasEdge with the stored prev snapshot, losing
concurrent updates; change the failure handlers (the block around editEdge usage
and the similar block at ~1205-1223) to merge only the mutated field back into
the current cached edge: on label-save failure take the current edge (from
setEdges/patchCanvas or c.interiorEdges) and create merged = { ...current,
label: prev.label } and use toRFEdge(merged) for setEdges and merged for
patchCanvas; do the analogous merge for interaction/marker-save failures (only
restore the specific interaction field from prev), ensuring you reference prev,
next, setEdges, toRFEdge, patchCanvas and interiorEdges when locating and
updating the code paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/p/`[slug]/_canvas/canvas.tsx:
- Around line 817-827: The insertion logic in setNodes that maps incidentProxies
via toProxyRFNode currently places new proxies at y = i * 72 which can overlap
existing boundary proxies; change it to compute an offset based on the current
passive/proxy nodes in ns (e.g., count existing proxies or compute the max Y of
existing proxy nodes) and add that offset to the new node Y so each restore path
appends after existing rail entries; update both occurrences (the setNodes block
and the similar block around lines 1104-1112) to use the same offset calculation
and preserve prior rendered positions when available, referencing setNodes,
incidentProxies, toProxyRFNode, breadcrumbIds, ns and i in your fix.

In `@src/server/architecture/__tests__/edge.service.test.ts`:
- Around line 549-567: The test currently only checks that updateEdgeInteraction
throws a ConflictError; tighten it by asserting that the thrown error's
details.conflictingEdgeIds includes the pre-existing connection's id (the one
created via connectNodes) and that it does not include the upgraded edge's id
(edge.id). Locate the test using seedAssociation(), connectNodes(...), and
updateEdgeInteraction(...), capture the thrown error from updateEdgeInteraction,
and add assertions on error.details.conflictingEdgeIds (and/or its length) to
ensure the collision reports the expected conflicting edge id(s).
- Around line 569-583: The test currently calls updateEdgeInteraction on the
same seeded ASSOCIATION (seedAssociation) which converts and removes the
association row, so it never exercises an ASSOCIATION and a directional REQUEST
coexisting; change the test to keep the original association and create a new
directional edge instead of updating the existing one: after const { actor, a,
b, edge } = await seedAssociation(), call the function used to create a new edge
(e.g., createEdge or whatever helper creates an edge with interaction "REQUEST")
to insert a new A→B REQUEST row (do not call updateEdgeInteraction on the seeded
edge), then assert the newly created edge has interaction "REQUEST" and
sourceId/targetId match a.id/b.id and that the original association still exists
if needed.

---

Outside diff comments:
In `@src/app/p/`[slug]/_canvas/canvas.tsx:
- Around line 1168-1184: The rollback currently overwrites the entire CanvasEdge
with the stored prev snapshot, losing concurrent updates; change the failure
handlers (the block around editEdge usage and the similar block at ~1205-1223)
to merge only the mutated field back into the current cached edge: on label-save
failure take the current edge (from setEdges/patchCanvas or c.interiorEdges) and
create merged = { ...current, label: prev.label } and use toRFEdge(merged) for
setEdges and merged for patchCanvas; do the analogous merge for
interaction/marker-save failures (only restore the specific interaction field
from prev), ensuring you reference prev, next, setEdges, toRFEdge, patchCanvas
and interiorEdges when locating and updating the code paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 79875a4c-6743-46fc-8fa6-b878e7c6e0ee

📥 Commits

Reviewing files that changed from the base of the PR and between 9378fec and 51a52e5.

📒 Files selected for processing (19)
  • .claude/skills/bulletproof-plan/SKILL.md
  • CONTEXT.md
  • docs/adr/0016-passive-nodes-and-boundary-group-n1-stability.md
  • docs/adr/0027-connection-carries-its-own-interaction.md
  • docs/adr/0031-cross-scope-read-derivation-and-per-edge-boundary-proxy.md
  • docs/plans/typed-cross-scope-connection-rendering.md
  • src/app/p/[slug]/_canvas/boundary-proxy.tsx
  • src/app/p/[slug]/_canvas/canvas.tsx
  • src/app/p/[slug]/_canvas/component-node.tsx
  • src/app/p/[slug]/_canvas/connection-edge.tsx
  • src/lib/connection-direction.test.ts
  • src/lib/connection-direction.ts
  • src/lib/connection-rules.ts
  • src/lib/interactions.ts
  • src/lib/node-kinds.ts
  • src/lib/schemas.ts
  • src/server/api/routers/architecture.ts
  • src/server/architecture/__tests__/edge.service.test.ts
  • src/server/architecture/edge.service.ts

Comment thread src/app/p/[slug]/_canvas/canvas.tsx
Comment thread src/server/architecture/__tests__/edge.service.test.ts
Comment on lines +569 to +583
it("allows a directional upgrade even when an ASSOCIATION shares the unordered pair", async () => {
// The seeded ASSOCIATION A↔B and a new A→B REQUEST occupy different de-dupe
// slots (unordered vs. ordered triple), so upgrading a SECOND association is
// only blocked by a matching directional row, not by the association itself.
const { actor, a, b, edge } = await seedAssociation();

const updated = await updateEdgeInteraction(testDb, actor, {
id: edge.id,
interaction: "REQUEST",
});

expect(updated.interaction).toBe("REQUEST");
expect(updated.sourceId).toBe(a.id);
expect(updated.targetId).toBe(b.id);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

This test does not exercise the scenario named in its title.

After seedAssociation(), updating that same edge to "REQUEST" removes the association row, so there is never an association concurrently sharing the unordered pair. As written, this mostly duplicates the earlier happy-path upgrade test instead of proving association/directional slot separation.

Example of a case that actually keeps an association alive
-  it("allows a directional upgrade even when an ASSOCIATION shares the unordered pair", async () => {
-    // The seeded ASSOCIATION A↔B and a new A→B REQUEST occupy different de-dupe
-    // slots (unordered vs. ordered triple), so upgrading a SECOND association is
-    // only blocked by a matching directional row, not by the association itself.
-    const { actor, a, b, edge } = await seedAssociation();
-
-    const updated = await updateEdgeInteraction(testDb, actor, {
-      id: edge.id,
-      interaction: "REQUEST",
-    });
-
-    expect(updated.interaction).toBe("REQUEST");
-    expect(updated.sourceId).toBe(a.id);
-    expect(updated.targetId).toBe(b.id);
-  });
+  it("allows changing a directional interaction while an ASSOCIATION shares the same pair", async () => {
+    const { actor, project, a, b } = await seedAssociation();
+    const directional = await connectNodes(testDb, actor, {
+      projectId: project.id,
+      sourceId: a.id,
+      targetId: b.id,
+      interaction: "REQUEST",
+    });
+
+    const updated = await updateEdgeInteraction(testDb, actor, {
+      id: directional.id,
+      interaction: "PUSH",
+    });
+
+    expect(updated.interaction).toBe("PUSH");
+    expect(updated.sourceId).toBe(a.id);
+    expect(updated.targetId).toBe(b.id);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/architecture/__tests__/edge.service.test.ts` around lines 569 -
583, The test currently calls updateEdgeInteraction on the same seeded
ASSOCIATION (seedAssociation) which converts and removes the association row, so
it never exercises an ASSOCIATION and a directional REQUEST coexisting; change
the test to keep the original association and create a new directional edge
instead of updating the existing one: after const { actor, a, b, edge } = await
seedAssociation(), call the function used to create a new edge (e.g., createEdge
or whatever helper creates an edge with interaction "REQUEST") to insert a new
A→B REQUEST row (do not call updateEdgeInteraction on the seeded edge), then
assert the newly created edge has interaction "REQUEST" and sourceId/targetId
match a.id/b.id and that the original association still exists if needed.

- On Component delete-undo / rollback, re-add boundary proxies below any
  still-present rail entries (offset by existing proxy count) instead of
  restarting at y=0, so a re-added stand-in never overlaps an existing one.
- Tighten the interaction-conflict test to assert details.conflictingEdgeIds
  names the blocking Connection (not the upgraded edge).
- Refocus the coexistence test to genuinely exercise ordered-slot independence:
  a B→A REQUEST does not block upgrading A↔B to A→B REQUEST.

(The full-snapshot rollback CodeRabbit flagged was already fixed in 40e070a —
both paths now restore only the field each mutation wrote.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Run `pnpm format:write` across the repo so every file matches the Prettier
config. These files predate the current config and were committed not
prettier-clean, which made `pnpm format:write` churn them into every feature
branch. Landing the normalization once stops that recurring noise; this commit
is formatting-only (no behavioral change — `pnpm check` green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@CuriouslyCory CuriouslyCory merged commit b0a01bd into main Jun 2, 2026
1 of 3 checks passed
@CuriouslyCory CuriouslyCory deleted the feat/65-typed-cross-scope-connections branch June 2, 2026 20:24
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.

Canvas: render typed cross-scope connections + far-end proxies

1 participant