ChatCrystal exposes two kinds of graph data: directed relations between notes and the tag co-occurrence graph shown by the current interface. An edge is a reviewable clue produced by candidates, model output, or co-occurrence rules; it is not proof by itself.
This article follows the relation service and relation routes at fixed commit 7642768.
Keep the two projections separate
The note relation table stores source, target, type, confidence, description, and created_by. Automatic discovery accepts eight enum values: CAUSED_BY, LEADS_TO, RESOLVED_BY, SIMILAR_TO, CONTRADICTS, DEPENDS_ON, EXTENDS, and REFERENCES. A given source-target pair can have only one relation of each type. Manual relations get a fixed confidence of 1, while created_by records their origin; the number is not a measured probability.
The current Web graph page calls GET /api/graph/projection?level=tag, which projects tag nodes and co-occurrence edges with a 120-node limit and a minScore strength filter. Clicking a tag loads up to 20 notes for that tag. GET /api/relations/graph still returns note nodes and relation edges, but it is a separate endpoint and is not necessarily what the current page displays.
How automatic discovery selects candidates
POST /api/notes/<note-id>/discover-relations first runs semantic search with the source title, takes at most 20 candidates, and removes the source itself. If search fails or produces no candidates, it falls back to the 20 most recent notes. Candidate summaries are truncated to 200 characters; the source summary contributes at most 500 characters to the prompt. The candidate set is bounded, so discovery is not a scan of the entire library.
The model returns a structured array using the relation enum. The application keeps only candidate IDs with confidence at least 0.5, then takes at most five relations. The schema describes a short description as “within 20 characters” but does not enforce that length. Persistence uses INSERT OR IGNORE, so an existing relation is not duplicated.
Replace <note-id> and <relation-id> with actual IDs before execution. Also replace 42 and 58 in the manual example with notes that exist in your library.
curl --request POST \
'http://localhost:3721/api/notes/<note-id>/discover-relations'
curl 'http://localhost:3721/api/notes/<note-id>/relations'
This is a synchronous discovery request; a model failure is returned as an error. It is not a background guarantee. To create a relation manually, provide a target ID, enum type, and description:
curl --request POST http://localhost:3721/api/notes/42/relations \
--header 'Content-Type: application/json' \
--data '{"target_note_id":58,"relation_type":"RESOLVED_BY","description":"Verify against the test result"}'
curl --request DELETE 'http://localhost:3721/api/relations/<relation-id>'
The route validates both notes, rejects self-relations and unknown types, and returns HTTP 400 for a manual duplicate.
Expand search while retaining the source
Semantic search accepts expand=true to add one-hop relations. It expands only edges with confidence at least 0.5, and linked scores use direct score × 0.7 × confidence. Notes already in the result set are skipped. This is not multihop path search and does not choose the strongest path for every linked result. Read the note and original conversation rather than treating via_relation as validation.
The batch-discovery endpoint enqueues notes with no outgoing relation. Its queued value counts submissions; it does not promise that every relation will be generated and should not replace per-note checks.
Make the graph reviewable
Record source, target, type, confidence, and origin, then test a concrete claim: can both notes’ summaries or source conversations support this edge? Preserve counterexamples and check current project conditions. A tag co-occurrence edge only says that two tags occur in the same notes; it does not establish causality.
See the semantic search workflow for retrieval and projection boundaries, and the Agent knowledge loop for writeback quality gates.