ADR-0009: Relationship Authoring and the Wiki-Link Convention's Layer
- Status: Accepted (2026-06-22)
- Date: 2026-06-22
Decision (2026-06-22): The synthesis described in the options below has been implemented:
- Structural typed relations → option 2 (name-resolved
ref).reffields stay asrefin the schema (clean edge declarations). The CLI/facade write path (node create/update --field "关系=名称") accepts a target name (natural key), resolves it to a node ID, and callsstore.set_links()to create the typed edge. Comma-separated for multi-value. No[[ ]]needed; no reactive.scmaction; no kernel. Seesrc/runex/cli.py:_coerce_cli_valueandnode_create/node_update. - Associative free links → body
[[ ]]→ genericmentions(the ZK layer).[[Atom标题]]embedded in prose (e.g.正文/描述) stays a kernel (extract-wiki-link-pairs) + reactive.scm(sync_mentionsinwiki-links.scm). This is a Zettelkasten convention, not a graph primitive — kept out of L2 core so it's swappable. - Scheduling relations (属于冲刺 etc.) remain on the option 1 hybrid as a working baseline — they are
textfields holding[[Target]]wired bywiki-links.scm. They can be migrated to option 2 when the write path is proven stable; the mechanism is identical.
Context
Building the scheduling layer (Task ↔ Sprint ↔ Goal ↔ Thread) surfaced a recurring question: how should a typed relationship be authored, and where does the wiki-link ([[ ]]) convention belong in the architecture?
Three facts frame the question:
A
reffield is already an edge, not a scalar. L2's scalar field types aretext/longtext/number/date/bool/blob—refis not among them. Areffield declaration is the schema-level declaration of a typed edge; the relationship lives in thelinkstable, never the fields table (store._migrate_ref_fields_to_linksenforces this). So "relationships are edges" is already true; the open part is how edges get authored.Interactive
refauthoring is unsolved.runex node update --field "属于冲刺=<id>"fails withunknown value_type: ref, and ADR-0005 forbids an imperativenode linkverb. So there is currently no clean high-level way for an agent to attach one existing node to another interactively.The wiki-link mechanism is already layered (
config/ontology/wiki_links.scm): parse = a built-in kernel (extract-wiki-link-pairs, L3 Python); wire = reactive.scmactions (find-by-name→create-link-with-props, with deferred back-fill); store = the L2linkstable. It mirrors Logseq/Roam (materialized refs recomputed on write, resolved by name) rather than Obsidian (computed on read). It works but has barely been used in anger.
The first scheduling implementation took a hybrid shortcut: relationship fields were made text holding [[Target]], then a per-field reactive action (packs/wiki-links.scm) projected them to typed edges. This stores the relation twice — as field text and as a derived edge — which is conceptually impure.
A competing value is at stake: conceptual simplicity of the node object (scalar fields + typed edges + body) versus the ergonomics of typed, interactively-authored relationships.
What seems settled (lean accept)
The wiki-link / Zettelkasten convention should stay a kernel + .scm policy, never sink into L2 core.
- The typed
linkstable is foundational (L2) — edges are core. [[ ]]parsing stays a kernel; the wiring stays optional.scm.- Rationale:
[[ ]]is a convention, not a graph primitive. Keeping it out of core (a) preserves node-object simplicity — L2 knows only "nodes have typed edges" — and (b) lets a user who dislikes Zettelkasten, or who wants a different link syntax, swap or drop it. The engine functions fully without wiki-links (ingest and actions create edges regardless). - Counter-pull we accept: Zettelkasten workflows need inline link-style titles (
[[a claim as a title]]) in prose — once we serve ZK users, the "impure" inline syntax is natural and expected in the body/knowledge layer. That is a reason to support[[ ]]well, not a reason to make it a core primitive.
Open options — how to author a typed relationship interactively
Hybrid (current): relationship field is
textholding[[Target]]; a per-field reactive action builds the typed edge (relation = field name), multi-value, deferred back-fill. Works today; impure (link syntax in a scalar field; relation stored twice).Name-resolved
ref(leaning): keep relationship fields asref(clean edge declarations); make the write path accept a name —--field "属于冲刺=W26冲刺"— and reactively resolve it to a typed edge. No[[ ]]ceremony, no scalar duplication, no link verb. Requires a small write-path/convention change so areffield set-by-name becomes an edge.Body-only
[[ ]](Logseq-pure): all links live in正文; give up typed structural relations (one genericmentions/wiki_linktype). Maximally simple node object, but loses typed scheduling relations.
A likely synthesis: structural typed relations → option 2 (ref, name-resolved); associative free links → body [[ ]] → generic mentions (the ZK layer). This maps each mechanism to the need and keeps the body-syntax impurity confined to the knowledge layer where it's natural. → This synthesis is accepted and partially implemented (2026-06-22).
What is decided
- Structural typed relations → option 2 (name-resolved
ref).reffields are now writable from the CLI:--field "主题=知识工程"→ facade resolves name to typed edge. Already in use for PKM relations (Atom/HowTo/MOC.主题, MOC.收录). The mechanism generalizes to allreffields — CRM, media, etc. can adopt the same pattern without schema changes. [[ ]]stays kernel +.scm, never L2 core. Settled.- Body
[[ ]]for associational/ZK links is correct — that's where the inline syntax belongs (embedded in prose).
What is still undecided
- Whether to migrate the scheduling hybrid (
text+[[ ]]+ per-fieldwire_*actions) to option 2. The mechanism is ready; the question is when. - Whether deferred name-resolution (auto-resolve when the target later appears) should be a general write-path feature or stay per-rule
.scm. Currently: ref fields require the target to exist (unresolved → error); wiki-links.scm handles deferred resolution for body[[ ]]and the schedulingtextfields.
Consequences
- Structural
reffields are now writable from the CLI. The facade write path (node create/update) resolves target names to typed edges. Agent skills can write--field "关系=名称"without[[ ]]ceremony. - The scheduling layer ships on the hybrid (option 1) as a working baseline. Not yet migrated to option 2; mechanism is ready.
- The kernel/data layering for
[[ ]]is settled. New link behavior defaults to.scm+ kernel for the ZK/prose layer, facade name-resolution for structuralreffields. - PKM relations (主题, 收录) are the first adopters of option 2. CRM and media
reffields (Deal.客户, 自媒体作品.作者/平台) can follow the same pattern — no schema changes needed, just CLI usage.
Related Documents
../architecture.md— node object, links table, layersadr-0005-reactive-semantics-over-imperative-orchestration.md— no imperative link verbadr-0006-runtime-loadable-ontology-over-python-registered-logic.md— behavior as dataconfig/ontology/wiki_links.scm,src/runex/skill/ontology/packs/wiki-links.scm— current implementations