# Plan: Edge Semantics (provenance + typed rel- edges) - IMPLEMENTED (branch edge-semantics, 2026-07-13) Fills the "Edge semantics: give edges more information" roadmap gap from the 2026-07-13 knowledge-layer analysis. Implemented in one changeset on branch `edge-semantics`; awaiting commit + PR. ## What shipped Two halves, zero server/protocol/store changes: 1. **Edge provenance**: every crawled edge carries the link's label text, the source section anchor it appears under, and an occurrence count. Duplicate links between the same pair aggregate into one counted edge; first non-empty label wins. 2. **Typed relations**: publisher-metadata convention `rel-` (hyphen, never colon; metadata keys allow only `[a-z0-9-]`), CSV values resolved against the doc URL, ingested by both crawlers as typed edges. Malformed refs, self-refs, and empty predicates are skipped silently. ## Design decisions - Edge identity is {From, To, Rel}; plain body links have Rel "". A plain link and a typed relation between the same pair are two distinct edges. - Store Merge is last-crawl-wins for Label/Anchor/Count (mirrors node upserts): idempotent, counts never inflate. - graph.json schema stays v1 (new StoredEdge fields are omitempty; legacy files load with Count normalized to 1). - /graph.md Edges table widened to `| From | To | Rel | Label | Anchor | Count |`; ParseExport accepts both this and the legacy 2-column row. - One shared annotation renderer `graph.EdgeAnnotation` (` [supersedes]`, ` ("Getting started", #intro, x3)`) used by client MCP, broker MCP, and both backlink lists; pinned-literal tests on both sides guard drift. - Fetch callback widened to a struct returning `(graph.FetchResult, error)` with a Metadata field; this also paid the /debt.md "Crawl fetcher callback returns four positional values" item and deleted the caller-less `graph.ClientFetcher` adapter. - Shared enriched extraction lives in `client/mdoutline.AnchoredLinks` (mdoutline already imports links, so links could not host it without a cycle). ## Touched surfaces client/mdoutline (new AnchoredLinks), client/graph (Edge fields, AddEdgeInfo aggregation, EdgeAnnotation, RelEdges parser, crawl enrichment), client/graphstore (StoredEdge, Merge, BacklinkEntry enrichment, dual-format export/parse), client/internal/fedcrawl (recordEdges via AnchoredLinks + RelEdges with existing loopback/normalize filters), demarkus-mcp (formatGraph, backlinks, explore, tool descriptions), broker (same surfaces + MCP-API.md), docs (ADR 0004, DESIGN.md, agent-cookbook). ## Verification done - Full test suites green in client and tools modules; `bash pre-commit.sh` clean. - Live e2e: dev server on :6399, published `adr.md` with `-meta rel-supersedes=/old.md` and a twice-linked doc under a `## Context` heading; crawl produced `| adr.md | design.md | | design doc | context | 2 |` and `| adr.md | old.md | supersedes | | | 1 |`. - Legacy compat live: the real pre-change ~/.mark/graph.json (133 nodes / 190 edges) loads and exports through the new binary with Count normalized to 1. ## Follow-ups - Surface labels/rels in the TUI graph view (deliberately deferred this round). - When publish-time edge extraction lands in the store backends, capture the same fields; conformance suite first per the backend-parity principle. - Consider a curated rel- predicate vocabulary in the plugins (supersedes, implements, depends-on, derived-from). See repo `docs/adr/0004-edge-semantics-rel-convention.md` for the full decision record.