Skip to content

The link operator

The link operator is the three-part form

[[alice]] -> [[bob]] {
    relation: parent
    confidence: 0.9
}

-> constructs an edge, the brackets on either side name its endpoints, and {..} is the data carried on it. A bracket pair on its own is not this operator: [[bob]] is a doc reference and asserts nothing.

[[..]] is a doc reference

[[..]] written around a name references a document in the current namespace:

[[alice]]              // a name in this namespace
[[people/alice]]       // a name qualified by module
[[alice#Bio]]          // a heading fragment
[[alice#^claim]]       // an anchored block

The referenced document can be in any format the vault holds — Markdown, HMD, JSON, YAML, HTML. A reference addresses a document, and format is something the document has, not something the reference selects. Nor does it hand back a Card: card-ness is a knowledge property established by a checked narrowing, never by the name that points at the target. See doc-type for what a document is and card-type for the vault that resolves the name in it.

Inside the brackets is a name, not an expression: the operator has no variable form, and addressing a target whose name is computed at runtime needs its own spelling. Display text is HMD's, not HQL's — [[alice|Alice B.]] references the same document as [[alice]], and whether HQL source accepts the pipe form at all is open, since | is the pipe operator (see pipes).

In a document body, a bare reference is an edge, because the document it sits in supplies the missing endpoint. Written in alice:

[[bob]] {
    color: red
    relation: parent
}

means

[[alice]] -> [[bob]] {
    color: red
    relation: parent
}

[[this]] is the source, and [[this]] in alice is [[alice]]. This is what gives a reference two readings without giving it two meanings. As a value, x = [[bob]] is the reference. As an authored link, [[bob]] is that edge, and [[bob]] with no braces is the same edge with empty data. Uplinks and downlinks are the two directions of traversal over exactly these edges — see doc-type.

Authoring one adds it to the vault graph

The expansion is not only a reading. An authored link adds the edge to the vault's graph, and the vault is where uplinks, downlinks and cards already come from.

If the edge is already there, the write is an update rather than a second edge: the data in the braces is merged into the annotation the edge already carries. So the two links

[[bob]] { relation: parent }
[[bob]] { confidence: 0.9 }

leave one edge from alice to bob annotated with both keys, not two edges side by side.

Three things follow, and they are consequences rather than separate decisions:

  • An edge is identified by its endpoints. One alice -> bob edge exists, and writing the link again addresses it. A vault that needed two distinct edges between the same pair — two different claims, kept apart — could not express it this way; it would need an edge identity beyond the pair, which is what a RelationCard supplies.
  • The annotation accumulates. Data arrives from every place the link is authored, so the annotation on an edge is not necessarily any one author's statement.
  • Forward links still work. The target need not exist, because the edge is keyed by the reference, not by a resolved document.

Open: whether the merge is per key at the top level or deep through the tree, and what happens when two authored links give the same key different values. A last-write-wins rule would make the annotation depend on vault traversal order, which is the same objection that makes cards ordering a live question — see card-type. Retaining both values is the reading consistent with knowledge keeping conflicting claims, and that argues the conflict should surface rather than resolve silently.

Also open: whether [[this]] is a keyword a program may write, and whether an edge constructed in an HQL cell asserts into the graph the way an authored one does. Constructing a value does not persist a document, so a cell that wrote edges into the vault would be performing an effect, and that has to be explicit.

{..} is ordinary data

The braces are not graph syntax. They are the Data attached to the edge, and the keys in them are the author's:

Edge {
    source: [[alice]]
    target: [[bob]]
    data: {
        relation: parent
        confidence: 0.9
    }
}

relation, color and confidence are entries in a tree, not grammar. Nothing in the core knows what relation: parent means, which is what keeps the edge form from turning every vocabulary a vault invents into a language keyword — the same reason the header is Data rather than a declared record.

The graph model behind it

The edge types belong to the graph extension, not to the core:

type Edge<S, T> {
    source: S
    target: T
    data: Data
}

type Graph<N, E>

HMD specializes them for documents, and the knowledge extension specializes them independently for meaning:

type Link<S, T> : Edge<S, T>
type HmdGraph : Graph<Card, Link>

type Relation<S, T> : Edge<S, T>
type KnowledgeGraph : {Knowledge, Graph<Concept, Relation>}

That separation is the point. [[alice]] -> [[project-x]] is an edge in the document graph; a semantic claim such as WorksOn(alice, project_x) is an edge in a knowledge graph. Both render through the same graph machinery without being treated as the same object. The layering is core → graph → knowledge, and HMD sits beside them as one source of documents.

Data on an edge is not a semantic relation

An edge carries data; that is what {..} is for. What it does not do is make the edge a claim. relation: parent is an author's entry in a tree, and reading it as a typed relationship is an interpretation that has to be performed and retained — see hql/knowledge.

So a link can carry data. What it cannot do is acquire identity or assert meaning by carrying it. When a relationship needs identity, provenance, evidence, discussion or incoming links of its own, it should stop being an edge and become a RelationCard: an edge is cheap and anonymous, while a card is a document other documents can point at — see card-type for the two kinds of card and why this one takes no node position.

Failure

Resolution can fail in HMD's own ways: a target that does not exist, a fragment missing from a document that does, a name matching several documents, or a target escaping the namespace root. HMD treats an unresolved link as a warning by design — writing forward to a document that has not been written yet is permitted, and the diagnostic report is a to-do list. HQL must not turn that into a hard error by accident.

The edge form makes this concrete rather than settling it: an edge can be constructed over an endpoint that does not resolve, and the endpoint is the reference, not the document. Whether a reference used where a document is wanted yields an Option, a value carrying its unresolved state, or a diagnostic is the largest open question here. Since absence is Option and there is no null, the reading hardest to defend is the one that quietly yields a Doc.

Also open: whether <- exists as the mirrored form, how -> binds against |, and whether an endpoint may be an expression rather than a bracketed name.

See doc-type for uplinks and downlinks, card-type for the vault, hql/knowledge for relation cards, data-type for the edge's data and hql/design-status for what remains undecided.