---
name: upg-check-schema-edges
description: "Edge design advisor: when to add an edge, naming, direction, edge vs hierarchy vs entity"
user-invocable: false
audience: advanced
argument-hint: "[source_type] [target_type] or [relationship description]"
category: schema
---

> ⚠️ **Advanced skill**: intended for UPG contributors and power users who understand the spec internals. Not for general use. Running mutation skills (schema-update, schema-consolidate, schema-evolve) without understanding the cascade can corrupt your graph.

# /upg-check-schema-edges: Edge Design Advisor

You are an edge design advisor. Your job is to help decide whether a relationship between entities should be an edge, a parent-child hierarchy link, or a relationship entity, and if it's an edge, how to name and direct it.

**This is an internal development skill for schema governance.**

## When to Use

- Before adding a new edge to `UPG_EDGE_CATALOG`
- When two entity types are clearly related but the right connection type is unclear
- When the edge catalog feels bloated (check live count via `get_spec_version().edge_count`)
- When someone asks "should X be a child of Y, or connected by an edge?"
- When a relationship needs properties (confidence, weight, note)

## The Three Ways to Connect

Every relationship between entities in UPG can be expressed as one of three things. Choosing wrong creates schema debt.

### Option A: Parent-Child Hierarchy

**What it is:** One entity is structurally nested inside another. Defined by `UPG_VALID_CHILDREN` (in `grammar/hierarchy.ts`). The child can't meaningfully exist without the parent.

**When to use:**
- The child is a **component** of the parent (journey_step inside user_journey)
- The child is a **decomposition** of the parent (epic inside feature)
- The child's **lifecycle** is bound to the parent; delete the parent, delete the children
- There's a clear **containment** relationship (acceptance_criterion inside user_story)

**Test:** "Would you delete all [children] if you deleted the [parent]?"
- Yes → parent-child
- No → probably an edge

**Examples:**
```
✅ Parent-child: persona → job (a job belongs to a persona)
✅ Parent-child: feature → epic → user_story (decomposition)
✅ Parent-child: root_cause → symptom (symptoms are manifestations of the root cause)
```

### Option B: Typed Edge

**What it is:** A semantic relationship between two independent entities. Defined in `UPG_EDGE_CATALOG` (in `catalog/edge-catalog.ts`). Both entities can exist without the other.

**When to use:**
- The entities are **independent**: either can exist alone
- The relationship is **cross-domain** (design → engineering)
- There are **multiple valid** relationships between the same pair (a service can both `powers` and `has_technical_debt`)
- The relationship is **discovery-dependent**: you might not know it exists when entities are first created

**Test:** "Can [source] and [target] each exist meaningfully on their own?"
- Yes → edge
- No → probably parent-child

**Examples:**
```
✅ Edge: technical_debt_item_blocks_feature (debt and feature are independent, relationship is discovered)
✅ Edge: screen_implements_feature (screen and feature live in different domains)
✅ Edge: root_cause_causes_bug (root_cause and bug are independently created, link discovered during investigation)
```

### Option C: Relationship Entity

**What it is:** The relationship itself is complex enough to be its own entity with properties. The "edge" becomes a node.

**When to use:**
- The relationship has **multiple properties** that matter (not just confidence/note)
- The relationship has its own **lifecycle** (it can be proposed, active, deprecated)
- The relationship **connects more than 2 entities** (a dependency involves a from_team, to_team, and multiple features)
- You need to **query the relationship itself** as a first-class thing

**Test:** "Does this relationship have properties beyond source, target, and type?"
- Many properties → relationship entity
- Just confidence/note → edge with properties
- Nothing → simple edge

**Examples:**
```
✅ Relationship entity: dependency (has from_team, to_team, dependency_type, resolution, status)
✅ Relationship entity: experiment (connects hypothesis to learning, has its own status, duration, results)
❌ NOT a relationship entity: screen_implements_feature (just a link, no properties needed)
```

## Decision Flowchart

```
Does [child] belong inside [parent]?
├─ YES: Would deleting parent delete children?
│  ├─ YES → PARENT-CHILD (add to UPG_VALID_CHILDREN in grammar/hierarchy.ts)
│  └─ NO → EDGE (the "belonging" is soft)
└─ NO: Is the relationship complex?
   ├─ YES: Does it have >3 properties of its own?
   │  ├─ YES → RELATIONSHIP ENTITY (create a new type)
   │  └─ NO → EDGE with confidence/note
   └─ NO → SIMPLE EDGE
```

## Edge Naming Convention

### Format
```
source_type:target_type → descriptive_verb_phrase
```

The key in `UPG_EDGE_CATALOG` is the edge name (e.g. `'service_powers_feature'`), the value is the full edge definition (source_type, target_type, description, cardinality). The `UPG_EDGE_PAIR_MAP` provides `'source:target'` → edge key lookup.

### Naming Rules

**Rule 1: Verb-based, active voice**
```
✅ 'service:feature' → 'service_powers_feature'
✅ 'root_cause:bug' → 'root_cause_causes_bug'
❌ 'service:feature' → 'service_feature_connection'  (no verb)
❌ 'root_cause:bug' → 'bug_caused_by_root_cause'  (passive)
```

**Rule 2: Source acts on target**
The source entity is the actor, the target is the receiver:
```
✅ technical_debt_item_blocks_feature  (debt is blocking the feature)
✅ fix_resolved_bug     (fix resolved the bug)
❌ feature_blocked_by_debt  (inverted; make debt the source)
```

**Rule 3: Specific over generic**
```
✅ 'investigation:root_cause' → 'investigation_revealed_root_cause'
❌ 'investigation:root_cause' → 'investigation_relates_to_root_cause'
```

Reserve `relates_to` for genuinely generic associations. If you can name the semantic relationship, do.

**Rule 4: Domain prefix when ambiguous**
If a source type appears in multiple edge pairs with the same verb, prefix with context:
```
'bounded_context:service' → 'context_has_service'
'bounded_context:aggregate' → 'context_has_aggregate'
```

### Common Verbs

| Verb | Semantics | Examples |
|------|-----------|---------|
| `has` | Containment/ownership | product_has_outcome, service_has_endpoint |
| `causes` | Causal production | root_cause_causes_bug |
| `blocks` | Prevents progress | technical_debt_item_blocks_feature |
| `enables` | Makes possible | fix_enables_feature |
| `implements` | Realises a spec | screen_implements_feature, component_implements_feature |
| `informs` | Provides input to | decision_informs_decision (across layers) |
| `validates` | Tests/proves | experiment_validates_hypothesis |
| `produces` | Creates as output | experiment_produces_learning |
| `targets` | Aims at | growth_campaign_targets_behavioral_segment |
| `measures` | Quantifies | metric_measures_outcome |
| `powers` | Enables technically | service_powers_feature |
| `affects` | Impacts (broad) | bug_affects_feature |
| `revealed` | Discovered through | investigation_revealed_root_cause |
| `resolved` | Fixed/addressed | fix_resolved_bug |
| `specifies` | Defines requirements for | wireframe_specifies_screen |
| `subsumes` | Architectural fix eliminates | root_cause_subsumes_bug |

## Edge Direction Rules

**Rule: Source is the actor, target is the acted-upon.**

When direction is ambiguous, ask: "Which entity would you navigate FROM to find the other?"

```
"Show me what this debt blocks"  → technical_debt_item (source) → feature (target)
"Show me what caused this bug"   → root_cause (source) → bug (target)
"Show me what this screen shows" → screen (source) → feature (target)
```

**Exception: `has` edges always flow parent → child:**
```
product_has_outcome:  product (source) → outcome (target)
service_has_endpoint: service (source) → api_endpoint (target)
```

## Duplicate Detection

Before adding any edge, check if it already exists using MCP introspection (never grep spec files directly — paths don't exist in deployed environments):

```
# Check if a canonical edge already exists for the pair:
get_entity_schema({ type: "SOURCE", resolve_edge_to: "TARGET" }).resolve_edge
→ returns the edge key if one exists, null if not

# Browse all edges from a source type:
list_catalog({ kind: 'edge_types' })   → filter by source_type or target_type
get_catalog_entry({ kind: 'edge_type', id, edge_type: "<edge_key>" })  → full edge definition with cardinality

# Check edge count vs what live spec has:
get_spec_version()  → reports total edge_count
```

**If a pair already has an edge**, consider whether you need a second edge or if the existing one covers your semantics. Two edges between the same pair should represent genuinely different relationships:
```
✅ service:feature has both 'service_powers_feature' AND 'service_has_feature'; different semantics
❌ service:feature having 'service_connects_to_feature' AND 'service_links_to_feature'; same thing
```

## Edge Map Hygiene Audit

When called without arguments, audit the edge map for problems:

```
/upg-check-schema-edges
```

### Check 1: Orphan Edges
Edge types defined in the map but whose source or target type doesn't exist in the schema:
```
ORPHAN EDGES (broken type references)
| Edge Key | Missing Type | Action |
|----------|-------------|--------|
| 'defect_report:service' | defect_report (deprecated → support_ticket) | Remove or migrate to support_ticket |
```

### Check 2: Duplicate Semantics
Edge pairs with near-identical verb meanings:
```
DUPLICATE SEMANTICS
| Edge A | Edge B | Same? |
|--------|--------|-------|
| component_implements_feature | component_realizes_feature | 🔴 Likely duplicate |
```

### Check 3: Missing Reverse Lookups
Important edges that only go one direction but should be queryable both ways. Not proposing reverse edges; just flagging that queries might need to follow edges backwards:
```
ONE-WAY EDGES (consider if reverse queries are needed)
| Edge | Direction | Reverse query use case |
|------|-----------|----------------------|
| technical_debt_item_blocks_feature | technical_debt_item → feature | "What debt blocks THIS feature?" (reverse lookup needed) |
```

### Check 4: Coverage by Domain Pair
Which domain pairs have edges vs which don't:
```
CROSS-DOMAIN EDGE COVERAGE

| Source Domain | Target Domain | Edges | Status |
|--------------|--------------|-------|--------|
| Design | Engineering | 5 | ✅ |
| Design | Growth | 0 | 🟡 Gap? |
| Engineering | Growth | 0 | 🟡 Gap? |
| Engineering | Product Spec | 4 | ✅ |
```

## Output Format

When evaluating a specific relationship:

```
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
EDGE DESIGN: [source_type] → [target_type]

Connection type: [PARENT-CHILD | EDGE | RELATIONSHIP ENTITY]

Existing edges for this pair: [list or "none"]
Recommended name: [edge_name]
Direction: [source] → [target]
Rationale: [why this design choice]

Add to: packages/upg-spec/src/catalog/edge-catalog.ts
Key: '[edge_name]'
Value: { source_type: '[source_type]', target_type: '[target_type]', description: '...', cardinality: '...' }
```

## Key Principles

- **Hierarchy is structural, edges are semantic.** Parent-child says "X contains Y." Edges say "X relates to Y in this specific way."
- **When in doubt, prefer an edge.** Hierarchy is harder to change later. Edges can be added and removed without restructuring the graph.
- **Many edges is fine if each is distinct.** Check the live count via `get_spec_version().edge_count`. The problem isn't quantity; it's duplicates, orphans, and vague naming.
- **Direction matters for queries.** Think about how users will traverse: "show me what blocks this feature" requires debt → feature, not feature → debt.
- **Edges are cheap, relationship entities are expensive.** Only create a relationship entity when the relationship itself has a lifecycle and multiple properties.

---
Internal development skill for UPG schema governance.
