# Memory.Link / Memory.Unlink

> State that two records are related, in your app's own words — "supersedes, because it was renegotiated after the Q2 review". Every search hit about either record then carries the link, its words and the other record's name, so whoever reads the result can decide whether to go and fetch it.

<!-- id: memory-link · area: memory · stability: stable · html: https://osysharp.com/reference/memory/link/ -->

## Summary        {#summary}
`Memory.Link(a, b, …)` records that two records are related and **why**, in a sentence you write. Search then
carries that sentence on every hit about either record (`SearchHit.Links`), and `related:` can follow the link to
widen a search when you ask it to. `Memory.Unlink(a, b)` withdraws the claim. Both are available under
`using Osysharp.Memory;`.

## Signature      {#signature}
```osy syntax
using Osysharp.Memory;

bool Memory.Link(
    Entity a, Entity b,                  // the two records to relate
    LinkKind as = LinkKind.CrossReference,  // Citation | CrossReference | DerivedFrom | Bookmark | Supersedes
    string label = null,                 // short, read a → b: "superseded by"
    string reason = null,                // why, from a's side: "renegotiated after the Q2 review"
    string reverseLabel = null,          // short, read b → a: "supersedes"
    string reverseReason = null)         // why, from b's side: "replaces the Q1 deal at better terms"

int Memory.Unlink(Entity a, Entity b)    // how many links were removed
```

## Description    {#description}
A link is a **claim your app makes about two of its records**, not a foreign key. Use it when the relationship is
something someone decided rather than something the data structure implies: this deal supersedes that one, this
invoice cites that contract, this ticket was derived from that report.

### Both directions are stated, on purpose   {#both-directions}
A link is read from whichever end the reader arrived at, and **one sentence cannot be read backwards**: "supersedes"
from one end is "superseded by" from the other. So the call asks for both sides. If you state only the forward side,
the reverse falls back to it — right for a symmetric relationship (`CrossReference`), and noticeably odd for an
asymmetric one, which is the point: the surface asks rather than inventing a sentence for you.

### The words ride along with the record   {#annotation}
A link is **not** indexed as something to find on its own. Instead, every hit about either record carries it —
`SearchHit.Links` gives you the other record's type, id and name, the relationship read from *your* end, and your
sentence:

- a hit on the old deal carries *"superseded by — renegotiated after the Q2 review → Deal D-2026-02"*;
- a hit on the new one carries *"supersedes — replaces the Q1 deal at better terms → Deal D-2025-11"*;
- neither ever shows a reader the sentence written for the other end.

**Why not index the sentence itself?** Because it is about a PAIR and means nothing without both ends. On its own,
"renegotiated after the Q2 review" names nobody — it would match weakly when you searched for the record and
ambiguously when you did not. Attached to the record, it arrives in context, and whoever is reading decides whether
to fetch the other end. `related:` is still there for when you want the search itself to travel.

### Who stated it   {#author}
`Memory.Link` records the acting principal as the author, because a stated relationship is somebody's claim. It
**refuses an unauthenticated call**, naming the fix, rather than storing a claim nobody made. `Memory.Unlink` needs
no principal: withdrawing a claim is not itself a claim.

### What a reader is allowed to see   {#visibility}
A link's words describe a PAIR, so they are shown only to a reader who can read both records. That is checked for
you, on every path: a link whose other end you cannot read simply is not there, and a `related:` hop will not travel
through — or quote — a record you cannot see. Links are not a table your app queries; they are reached through these
two verbs and through the hits search returns, which is what makes that rule enforceable at all.

⚠ **A withheld link is not counted.** You are never told "3 links (2 hidden)" — that number would itself disclose
that two related records exist and that you are not cleared for them. A link you may not see is indistinguishable
from a link that was never stated. Where a record has *many* links, the list is capped for length and
`SearchHit.LinksElided` says how many the cap left out — that count is the cap's alone.

## Examples       {#examples}
```osy title="state a relationship, both ways" test app=memory
using Osysharp.Memory;

entity Deal { [MaxLength(120)] string Title; [Searchable(Memory)] string Notes; }

bool Supersede(Deal older, Deal newer) {
  return Memory.Link(older, newer,
    as: LinkKind.Supersedes,
    label: "superseded by",     reason: "renegotiated after the Q2 review",
    reverseLabel: "supersedes", reverseReason: "replaces the Q1 deal at better terms");
}
```

```osy title="withdraw it" test app=memory
using Osysharp.Memory;

int Withdraw(Deal a, Deal b) {
  // either order names the same link; the return says whether there was one to remove
  return Memory.Unlink(a, b);
}
```

## See also       {#see-also}
- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — finding the words a link stated, and `related:` for following one
- [SearchHit](https://osysharp.com/reference/memory/searchhit/) — `Via`, the field a followed link fills in
- [Search](https://osysharp.com/reference/memory/index/) — how text becomes findable in the first place
