**Class:** missing · **Severity:** cosmetic
**Source:** schema-evolution investigation, 2026-08-07 (sibling of issue-8f6576cd7bc9)
**Step:** narrowing `relation_types` in an inline-only schema · **Frequency:** rare — see *Exposure*

## Finding

`docir check` reports a tag the registry does not know (`unknown-tag`) and a status the type does
not declare (`unknown-status`), but says nothing about an **edge whose kind the registry no longer
knows**. The edge is served by `get`, traversed by `context`, and survives every write that does
not rewrite it.

Reproduced in a throwaway store: an `issue --related <adr>:depends_on`, then a schema declaring
`relation_types: [relates_to, supersedes]`.

```
docir schema show   -> ['relates_to', 'supersedes']
docir check         -> 0 findings;  --strict exit 0
docir get <id>      -> related: [{target: adr-…, kind: 'depends_on'}]
docir update <id> --set-title "B2"
                    -> written; the depends_on edge is still there
docir update <id> --set-related <adr>:depends_on
                    -> error: unknown relation kind 'depends_on'; known kinds: relates_to, supersedes
```

So Tier 0 refuses to *write* the kind while the corpus keeps *holding* it, and the one command
that exists to find that class of drift is silent. `GraphChecker.check` (`graph_checks.py:92`)
never consults `Schema.is_known_relation_kind` (`schema.py:211`), which has exactly one caller —
the write-path validator.

## Exposure

Narrower than it looks, which is why this is filed as cosmetic rather than material.
`_merge_profiled` merges `relation_types` as a **union** (`schema_loader.py:152`), so a schema with
a `profiles:` key can only ever *widen* the registry — the core six are always present and cannot
be removed by editing the file. Reaching the state above takes one of:

- an **inline-only** schema (no `profiles:` key), whose registry is exactly what it declares — the
  reproduction above;
- a release **dropping a kind from `CORE_SCHEMA_YAML`**, which would hit every store at once;
- an edge written by hand into frontmatter with a kind nobody registered.

The third is the same route `unknown-tag` and `unknown-status` exist to cover, and it is covered
for them.

## Impact

Low and bounded: the edge still resolves, and its behaviour is unchanged, because
`Schema.relation_kind` falls back to `CORE_RELATION_KINDS` for any kind the file does not describe
(`schema.py:215`) — so a dropped `depends_on` is still cycle-checked and still read as a
dependency by the layering check. What is lost is the report: the registry has stopped describing
the corpus, and `docir check` — which says so for tags, statuses and types — does not say so here.

## Proposed default

An `unknown-relation-kind` warning in Tier 1, listing the source, the target and the kind. Warning
severity and the same wording shape as `unknown-status`, for the same reason: nothing is broken,
the classification is simply no longer one the schema knows.

Deliberately not proposed: repairing it in `check --fix`. There is no safe substitute kind —
rewriting `depends_on` to `relates_to` would silently drop a dependency claim the layering check
reads, which is a guess about meaning, not a mechanical repair.

## Actors affected

- ACT-002 repository maintainer / developer

## Evidence

- `src/docir/modules/documents/domain/services/graph_checks.py:92` — `check`, no relation-kind pass
- `src/docir/modules/documents/domain/services/graph_checks.py:169,199` — the two sibling findings
- `src/docir/modules/documents/domain/schema.py:211,215` — `is_known_relation_kind`, and the core fallback
- `src/docir/modules/documents/infra/schema_loader.py:152` — `relation_types` merges as a union

## Resolution

FIXED 2026-08-07, as proposed. `GraphChecker._find_unknown_relation_kind` reports an
`unknown-relation-kind` warning naming the source, the target and the kind, wired into `check`
beside `missing-required`.

One finding per distinct `(source, target, kind)`: the edge is the thing that is misclassified,
and a document is free to have one bad edge and five good ones.

The permissive-when-empty rule is the load-bearing part, and it is not a special case here — the
check asks `Schema.is_known_relation_kind`, the same question Tier 0 asks, which answers *true*
for every kind when the registry is empty. That is every schema predating typed edges, and a
literal membership test would have fired on all of them.

Not repaired by `check --fix`, as proposed: rewriting `depends_on` to `relates_to` would silently
drop a dependency claim the layering check reads — a guess about meaning, not a mechanical repair.

Verified by injecting three bugs, each caught by a different test: unwiring the check, replacing
`is_known_relation_kind` with a literal membership test (which breaks the permissive case), and
promoting the kind to error severity. The integration test also pins the asymmetry that made this
worth fixing: the same edge is still served by `get` while `--set-related` on it raises
`UnknownRelationKindError`.